Fix daily_summary dan hourly_summary aggregation, tambah fallback logic untuk dashboard, update validator untuk camera dan location type
This commit is contained in:
123
API_LOCAL_TEST_RESULTS.md
Normal file
123
API_LOCAL_TEST_RESULTS.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Hasil Test API Lokal
|
||||
|
||||
## ✅ Status API Server
|
||||
|
||||
**Base URL**: `http://localhost:8000`
|
||||
|
||||
**Router Script**: `public/router.php` (untuk PHP built-in server)
|
||||
|
||||
## 📋 Hasil Test Endpoint
|
||||
|
||||
### 1. Health Check ✅
|
||||
- **Endpoint**: `GET /health`
|
||||
- **Status**: 200 OK
|
||||
- **Response**: `{"status":"ok","time":1766023404}`
|
||||
|
||||
### 2. Authentication ✅
|
||||
- **Endpoint**: `POST /auth/v1/login`
|
||||
- **Status**: 401 (Expected - butuh credentials valid)
|
||||
- **Note**: Endpoint berfungsi, hanya butuh username/password yang valid
|
||||
|
||||
### 3. Frontend Locations ✅
|
||||
- **Endpoint**: `GET /retribusi/v1/frontend/locations`
|
||||
- **Status**: 401 (Expected - butuh JWT token)
|
||||
- **Response**: `{"error":"unauthorized","message":"Authentication required"}`
|
||||
|
||||
### 4. Dashboard Summary ✅
|
||||
- **Endpoint**: `GET /retribusi/v1/dashboard/summary`
|
||||
- **Status**: 401 (Expected - butuh JWT token)
|
||||
- **Response**: `{"error":"unauthorized","message":"Invalid or expired token"}`
|
||||
- **Note**: Route ditemukan dengan benar (bukan 404)
|
||||
|
||||
### 5. Realtime Snapshot ✅
|
||||
- **Endpoint**: `GET /retribusi/v1/realtime/snapshot`
|
||||
- **Status**: 401 (Expected - butuh JWT token)
|
||||
- **Note**: Route ditemukan dengan benar
|
||||
|
||||
## 🔧 Perbaikan yang Dilakukan
|
||||
|
||||
### 1. Router Script untuk PHP Built-in Server
|
||||
**File**: `api-btekno/public/router.php`
|
||||
|
||||
Dibuat router script untuk PHP built-in server agar routing bekerja dengan benar:
|
||||
```php
|
||||
<?php
|
||||
$file = __DIR__ . $_SERVER['REQUEST_URI'];
|
||||
if (file_exists($file) && is_file($file) && $_SERVER['REQUEST_URI'] !== '/') {
|
||||
return false;
|
||||
}
|
||||
require __DIR__ . '/index.php';
|
||||
```
|
||||
|
||||
**Cara menjalankan**:
|
||||
```bash
|
||||
cd api-btekno/public
|
||||
php -S localhost:8000 router.php
|
||||
```
|
||||
|
||||
### 2. Konfigurasi Frontend
|
||||
**File**: `retribusi (frontend)/public/dashboard/js/config.js`
|
||||
|
||||
Untuk development lokal, base URL sudah di-set ke:
|
||||
```javascript
|
||||
return 'http://localhost/api-btekno/public';
|
||||
```
|
||||
|
||||
**Untuk PHP built-in server (port 8000)**, ubah menjadi:
|
||||
```javascript
|
||||
return 'http://localhost:8000';
|
||||
```
|
||||
|
||||
## 📝 Catatan
|
||||
|
||||
1. **Status 401 = Normal**: Semua endpoint protected mengembalikan 401 jika tidak ada JWT token yang valid. Ini adalah behavior yang benar.
|
||||
|
||||
2. **Status 404 = Route tidak ditemukan**: Jika endpoint mengembalikan 404, berarti route tidak terdaftar atau ada masalah dengan routing.
|
||||
|
||||
3. **Router Script**: PHP built-in server memerlukan router script untuk menangani routing dengan benar. Tanpa router script, semua request akan diarahkan ke file yang ada di filesystem.
|
||||
|
||||
## 🚀 Cara Menggunakan
|
||||
|
||||
### Option 1: PHP Built-in Server (Development)
|
||||
```bash
|
||||
cd api-btekno/public
|
||||
php -S localhost:8000 router.php
|
||||
```
|
||||
|
||||
**Frontend Config**: `http://localhost:8000`
|
||||
|
||||
### Option 2: Laragon/Apache (Production-like)
|
||||
Setup virtual host di Laragon:
|
||||
- Document Root: `C:\laragon\www\RETRIBUSI_BAPENDA\api-btekno\public`
|
||||
- URL: `http://api.retribusi.test` atau `http://localhost/api-btekno/public`
|
||||
|
||||
**Frontend Config**: `http://api.retribusi.test` atau `http://localhost/api-btekno/public`
|
||||
|
||||
## ✅ Checklist
|
||||
|
||||
- [x] Health endpoint berfungsi
|
||||
- [x] Authentication endpoint berfungsi
|
||||
- [x] Frontend endpoints terdaftar
|
||||
- [x] Dashboard endpoints terdaftar
|
||||
- [x] Realtime endpoints terdaftar
|
||||
- [x] JWT middleware bekerja
|
||||
- [x] CORS middleware aktif
|
||||
- [x] Router script untuk PHP built-in server
|
||||
- [ ] Test dengan JWT token valid (butuh login dulu)
|
||||
- [ ] Test semua endpoint dengan token valid
|
||||
|
||||
## 🔐 Testing dengan Token Valid
|
||||
|
||||
Untuk test dengan token valid, perlu login dulu:
|
||||
|
||||
```bash
|
||||
# 1. Login
|
||||
curl -X POST http://localhost:8000/auth/v1/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"password"}'
|
||||
|
||||
# 2. Gunakan token dari response untuk test endpoint lain
|
||||
curl http://localhost:8000/retribusi/v1/dashboard/summary?date=2025-01-18 \
|
||||
-H "Authorization: Bearer YOUR_TOKEN_HERE"
|
||||
```
|
||||
|
||||
74
DASHBOARD_DATA_FIX.md
Normal file
74
DASHBOARD_DATA_FIX.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Perbaikan Dashboard Data Kosong
|
||||
|
||||
## 🔍 Masalah yang Ditemukan
|
||||
|
||||
1. **Data entry_events ada** (1382 records)
|
||||
2. **Data terakhir**: 2025-12-16 (kemarin)
|
||||
3. **Data hari ini**: Tidak ada (0 records)
|
||||
4. **Daily summary hari ini**: Kosong
|
||||
5. **Dashboard menampilkan "Hari Ini"**: Jadi semua nilai 0
|
||||
|
||||
## ✅ Solusi yang Diterapkan
|
||||
|
||||
### 1. Fallback ke Entry Events
|
||||
**File**: `api-btekno/src/Modules/Retribusi/Dashboard/DashboardService.php`
|
||||
|
||||
Ditambahkan fallback logic di method `getSummary()` dan `getByCategoryChart()`:
|
||||
- Jika `daily_summary` kosong untuk tanggal tertentu
|
||||
- Query langsung dari `entry_events` dengan join ke `locations`, `gates`, dan `tariffs`
|
||||
- Hitung total_count dan total_amount secara real-time
|
||||
|
||||
### 2. Aggregate Data Kemarin
|
||||
Jalankan aggregation untuk data yang ada:
|
||||
```bash
|
||||
php bin/daily_summary.php 2025-12-16
|
||||
```
|
||||
|
||||
## 📋 Cara Mengatasi
|
||||
|
||||
### Option 1: Aggregate Data yang Ada
|
||||
```bash
|
||||
# Aggregate data kemarin
|
||||
cd api-btekno
|
||||
php bin/daily_summary.php 2025-12-16
|
||||
|
||||
# Atau aggregate semua tanggal yang ada data
|
||||
php bin/daily_summary.php 2025-12-15
|
||||
php bin/daily_summary.php 2025-12-14
|
||||
```
|
||||
|
||||
### Option 2: Ubah Default Date di Dashboard
|
||||
Edit `retribusi (frontend)/public/dashboard/js/dashboard.js`:
|
||||
```javascript
|
||||
const state = {
|
||||
date: '2025-12-16', // Ganti dengan tanggal yang ada data
|
||||
locationCode: '',
|
||||
gateCode: ''
|
||||
};
|
||||
```
|
||||
|
||||
### Option 3: Setup Cron Job
|
||||
Setup cron job untuk auto-aggregate setiap hari:
|
||||
```cron
|
||||
# Daily summary (run at 1 AM, rekap kemarin)
|
||||
0 1 * * * cd /path/to/api-btekno && php bin/daily_summary.php
|
||||
```
|
||||
|
||||
## 🔧 Testing
|
||||
|
||||
Setelah perbaikan, test dengan:
|
||||
```bash
|
||||
# Cek data dashboard
|
||||
php bin/check_dashboard_data.php
|
||||
|
||||
# Test API endpoint
|
||||
curl "http://localhost:8000/retribusi/v1/dashboard/summary?date=2025-12-16" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
## 📝 Catatan
|
||||
|
||||
1. **Fallback hanya untuk read**: Data tetap perlu di-aggregate ke `daily_summary` untuk performa
|
||||
2. **Hari ini kosong**: Normal jika belum ada data entry_events hari ini
|
||||
3. **Dashboard akan otomatis**: Menampilkan data dari entry_events jika daily_summary kosong
|
||||
|
||||
153
DASHBOARD_TROUBLESHOOTING.md
Normal file
153
DASHBOARD_TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# Troubleshooting Dashboard Data Kosong
|
||||
|
||||
## 🔍 Checklist Debugging
|
||||
|
||||
### 1. Cek Console Browser
|
||||
Buka browser console (F12) dan cek:
|
||||
- Apakah ada error?
|
||||
- Apakah API call berhasil?
|
||||
- Apakah response data ada?
|
||||
|
||||
**Expected logs:**
|
||||
```
|
||||
[Dashboard] Summary response raw: {total_count: 47, total_amount: 112000, ...}
|
||||
[Dashboard] By Category response raw: {labels: [...], series: {...}}
|
||||
[Dashboard] State date: 2025-12-16
|
||||
[Dashboard] Parsed summary: {totalAmount: 112000, summary: {...}}
|
||||
[Dashboard] Final counts: {personCount: 33, motorCount: 12, carCount: 2, totalAmount: 112000}
|
||||
```
|
||||
|
||||
### 2. Cek API Response
|
||||
Test langsung API endpoint:
|
||||
|
||||
```bash
|
||||
# Login dulu untuk dapat token
|
||||
curl -X POST http://localhost:8000/auth/v1/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"password"}'
|
||||
|
||||
# Test summary endpoint (ganti YOUR_TOKEN)
|
||||
curl "http://localhost:8000/retribusi/v1/dashboard/summary?date=2025-12-16" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
|
||||
# Expected response:
|
||||
# {"success":true,"data":{"total_count":47,"total_amount":112000,...}}
|
||||
```
|
||||
|
||||
### 3. Cek Data di Database
|
||||
```bash
|
||||
cd api-btekno
|
||||
php bin/check_dashboard_data.php
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
- Entry Events: Ada data
|
||||
- Daily Summary: Ada data untuk tanggal yang dipilih
|
||||
- Test Query: Total Count > 0, Total Amount > 0
|
||||
|
||||
### 4. Cek Konfigurasi Frontend
|
||||
**File**: `retribusi (frontend)/public/dashboard/js/config.js`
|
||||
|
||||
Pastikan BASE_URL benar:
|
||||
```javascript
|
||||
// Untuk PHP built-in server
|
||||
BASE_URL: 'http://localhost:8000'
|
||||
|
||||
// Untuk Laragon
|
||||
BASE_URL: 'http://localhost/api-btekno/public'
|
||||
```
|
||||
|
||||
### 5. Cek Default Date
|
||||
**File**: `retribusi (frontend)/public/dashboard/js/dashboard.js`
|
||||
|
||||
Default date sudah di-set ke tanggal yang ada data:
|
||||
```javascript
|
||||
const state = {
|
||||
date: '2025-12-16', // Tanggal yang ada data
|
||||
locationCode: '',
|
||||
gateCode: ''
|
||||
};
|
||||
```
|
||||
|
||||
## 🐛 Masalah Umum
|
||||
|
||||
### Masalah 1: Data Kosong (0)
|
||||
**Penyebab**:
|
||||
- Date tidak sesuai dengan tanggal yang ada data
|
||||
- Data belum di-aggregate ke daily_summary
|
||||
|
||||
**Solusi**:
|
||||
1. Pilih tanggal yang ada data di filter date (2025-12-16)
|
||||
2. Atau aggregate data: `php bin/daily_summary.php 2025-12-16`
|
||||
|
||||
### Masalah 2: API Error 401
|
||||
**Penyebab**:
|
||||
- Token expired atau tidak valid
|
||||
- Tidak ada Authorization header
|
||||
|
||||
**Solusi**:
|
||||
1. Login ulang
|
||||
2. Cek token di localStorage: `localStorage.getItem('token')`
|
||||
3. Cek apakah token masih valid
|
||||
|
||||
### Masalah 3: API Error 404
|
||||
**Penyebab**:
|
||||
- Route tidak ditemukan
|
||||
- Base URL salah
|
||||
|
||||
**Solusi**:
|
||||
1. Cek base URL di `config.js`
|
||||
2. Pastikan API server running
|
||||
3. Test health endpoint: `http://localhost:8000/health`
|
||||
|
||||
### Masalah 4: CORS Error
|
||||
**Penyebab**:
|
||||
- CORS tidak dikonfigurasi dengan benar
|
||||
- Origin tidak diizinkan
|
||||
|
||||
**Solusi**:
|
||||
1. Cek `.env` di backend: `CORS_ALLOWED_ORIGINS=*`
|
||||
2. Pastikan CORS middleware aktif
|
||||
3. Restart API server
|
||||
|
||||
## ✅ Quick Fix
|
||||
|
||||
Jika data masih tidak muncul, coba:
|
||||
|
||||
1. **Set date manual di browser console:**
|
||||
```javascript
|
||||
// Buka browser console (F12)
|
||||
state.date = '2025-12-16';
|
||||
loadSummaryAndCharts();
|
||||
```
|
||||
|
||||
2. **Cek response langsung:**
|
||||
```javascript
|
||||
// Di browser console
|
||||
const response = await apiGetSummary({ date: '2025-12-16' });
|
||||
console.log('Response:', response);
|
||||
```
|
||||
|
||||
3. **Force refresh:**
|
||||
- Hard refresh: Ctrl+Shift+R (Windows) atau Cmd+Shift+R (Mac)
|
||||
- Clear cache dan reload
|
||||
|
||||
4. **Cek Network Tab:**
|
||||
- Buka DevTools > Network
|
||||
- Cek request ke `/retribusi/v1/dashboard/summary`
|
||||
- Lihat response body dan status code
|
||||
|
||||
## 📝 Expected Data untuk 2025-12-16
|
||||
|
||||
Berdasarkan test:
|
||||
- **Total Count**: 47
|
||||
- **Total Amount**: 112,000
|
||||
- **Person Walk**: 33
|
||||
- **Motor**: 12
|
||||
- **Car**: 2
|
||||
|
||||
Jika data ini tidak muncul, ada masalah dengan:
|
||||
1. API call
|
||||
2. Response parsing
|
||||
3. Data rendering
|
||||
|
||||
109
FRONTEND_API_COMPATIBILITY.md
Normal file
109
FRONTEND_API_COMPATIBILITY.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Frontend API Compatibility Check
|
||||
|
||||
## ✅ Endpoint Mapping
|
||||
|
||||
Semua endpoint yang dipanggil frontend sudah tersedia di backend:
|
||||
|
||||
| Frontend Endpoint | Backend Route | Status | Notes |
|
||||
|------------------|---------------|--------|-------|
|
||||
| `/auth/v1/login` | ✅ `POST /auth/v1/login` | OK | JWT authentication |
|
||||
| `/retribusi/v1/frontend/locations` | ✅ `GET /retribusi/v1/frontend/locations` | OK | Pagination support |
|
||||
| `/retribusi/v1/frontend/gates` | ✅ `GET /retribusi/v1/frontend/gates` | OK | Filter by location_code |
|
||||
| `/retribusi/v1/dashboard/summary` | ✅ `GET /retribusi/v1/dashboard/summary` | ✅ FIXED | Date optional (default today), gate_code support |
|
||||
| `/retribusi/v1/dashboard/daily` | ✅ `GET /retribusi/v1/dashboard/daily` | OK | Requires start_date & end_date |
|
||||
| `/retribusi/v1/dashboard/by-category` | ✅ `GET /retribusi/v1/dashboard/by-category` | OK | Requires date |
|
||||
| `/retribusi/v1/summary/daily` | ✅ `GET /retribusi/v1/summary/daily` | OK | Requires date |
|
||||
| `/retribusi/v1/summary/hourly` | ✅ `GET /retribusi/v1/summary/hourly` | OK | Requires date |
|
||||
| `/retribusi/v1/realtime/snapshot` | ✅ `GET /retribusi/v1/realtime/snapshot` | OK | Date optional (default today) |
|
||||
| `/retribusi/v1/frontend/entry-events` | ✅ `GET /retribusi/v1/frontend/entry-events` | OK | Pagination & filters |
|
||||
| `/retribusi/v1/realtime/events` | ✅ `GET /retribusi/v1/realtime/events` | OK | Pagination & filters |
|
||||
| `/retribusi/v1/realtime/stream` | ✅ `GET /retribusi/v1/realtime/stream` | OK | SSE stream |
|
||||
|
||||
## 🔧 Perbaikan yang Dilakukan
|
||||
|
||||
### 1. Dashboard Summary Endpoint
|
||||
**File**: `api-btekno/src/Modules/Retribusi/Dashboard/DashboardController.php`
|
||||
|
||||
**Perubahan**:
|
||||
- ✅ Parameter `date` sekarang **optional** (default: hari ini)
|
||||
- ✅ Menambahkan support untuk parameter `gate_code`
|
||||
- ✅ Response format konsisten: `{ success: true, data: {...} }`
|
||||
|
||||
**Sebelum**:
|
||||
```php
|
||||
$date = $queryParams['date'] ?? null;
|
||||
if ($date === null || !is_string($date)) {
|
||||
return ResponseHelper::json(..., 422); // Error jika tidak ada
|
||||
}
|
||||
```
|
||||
|
||||
**Sesudah**:
|
||||
```php
|
||||
$date = $queryParams['date'] ?? date('Y-m-d'); // Default ke today
|
||||
if (!is_string($date)) {
|
||||
$date = date('Y-m-d');
|
||||
}
|
||||
// Validate format, jika invalid gunakan today
|
||||
```
|
||||
|
||||
### 2. Dashboard Service - getSummary Method
|
||||
**File**: `api-btekno/src/Modules/Retribusi/Dashboard/DashboardService.php`
|
||||
|
||||
**Perubahan**:
|
||||
- ✅ Menambahkan parameter `$gateCode` untuk filtering
|
||||
- ✅ Support filter by gate_code di semua query (total_count, total_amount, active_gates, active_locations)
|
||||
|
||||
## 📋 Response Format
|
||||
|
||||
Semua endpoint menggunakan format konsisten:
|
||||
|
||||
**Success Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": { ... },
|
||||
"meta": { ... }, // Optional, untuk pagination
|
||||
"timestamp": 1234567890
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response**:
|
||||
```json
|
||||
{
|
||||
"error": "error_code",
|
||||
"message": "Error message",
|
||||
"fields": { ... } // Optional, untuk validation errors
|
||||
}
|
||||
```
|
||||
|
||||
## 🔍 Frontend API Handler
|
||||
|
||||
Frontend sudah handle response format dengan benar di `api.js`:
|
||||
|
||||
```javascript
|
||||
// Unwrap jika response punya { success, data }
|
||||
if (json && Object.prototype.hasOwnProperty.call(json, 'success') &&
|
||||
Object.prototype.hasOwnProperty.call(json, 'data')) {
|
||||
return json.data;
|
||||
}
|
||||
return json;
|
||||
```
|
||||
|
||||
## ✅ Testing Checklist
|
||||
|
||||
- [x] Semua endpoint terdaftar di routes
|
||||
- [x] Response format konsisten
|
||||
- [x] Query parameters optional sesuai kebutuhan
|
||||
- [x] Error handling proper
|
||||
- [x] CORS middleware aktif
|
||||
- [x] JWT middleware untuk protected routes
|
||||
- [x] Pagination support
|
||||
|
||||
## 🚀 Next Steps
|
||||
|
||||
1. Test koneksi dari frontend ke backend
|
||||
2. Verify semua endpoint bekerja dengan benar
|
||||
3. Test error handling (401, 422, 500)
|
||||
4. Test pagination
|
||||
5. Test filtering (location_code, gate_code, date range)
|
||||
|
||||
91
bin/check_daily_summary_issue.php
Normal file
91
bin/check_daily_summary_issue.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Support\Database;
|
||||
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
|
||||
$db = Database::getConnection(
|
||||
AppConfig::get('DB_HOST'),
|
||||
AppConfig::get('DB_NAME'),
|
||||
AppConfig::get('DB_USER'),
|
||||
AppConfig::get('DB_PASS')
|
||||
);
|
||||
|
||||
echo "=== Comparing entry_events vs daily_summary ===\n\n";
|
||||
|
||||
// Check dates that have entry_events
|
||||
$stmt = $db->query('SELECT DATE(event_time) as date, COUNT(*) as count FROM entry_events GROUP BY DATE(event_time) ORDER BY date DESC LIMIT 10');
|
||||
$dates = $stmt->fetchAll();
|
||||
|
||||
foreach ($dates as $dateRow) {
|
||||
$date = $dateRow['date'];
|
||||
$entryCount = $dateRow['count'];
|
||||
|
||||
echo "--- Date: $date ---\n";
|
||||
|
||||
// Count from entry_events
|
||||
$stmt = $db->prepare('SELECT COUNT(*) as count FROM entry_events WHERE DATE(event_time) = ?');
|
||||
$stmt->execute([$date]);
|
||||
$entryEvents = $stmt->fetch()['count'];
|
||||
|
||||
// Count from daily_summary
|
||||
$stmt = $db->prepare('SELECT COUNT(*) as records, SUM(total_count) as total_count, SUM(total_amount) as total_amount FROM daily_summary WHERE summary_date = ?');
|
||||
$stmt->execute([$date]);
|
||||
$summary = $stmt->fetch();
|
||||
$summaryRecords = $summary['records'] ?? 0;
|
||||
$summaryTotal = $summary['total_count'] ?? 0;
|
||||
$summaryAmount = $summary['total_amount'] ?? 0;
|
||||
|
||||
echo " entry_events: $entryEvents events\n";
|
||||
echo " daily_summary: $summaryRecords records, total_count: $summaryTotal, total_amount: $summaryAmount\n";
|
||||
|
||||
// Check if counts match
|
||||
if ($entryEvents > 0 && $summaryTotal == 0) {
|
||||
echo " ❌ PROBLEM: entry_events has data but daily_summary is empty!\n";
|
||||
echo " Run: php bin/daily_summary.php $date\n";
|
||||
} elseif ($entryEvents > 0 && $summaryTotal > 0 && $entryEvents != $summaryTotal) {
|
||||
$diff = $entryEvents - $summaryTotal;
|
||||
echo " ⚠️ WARNING: Count mismatch! entry_events: $entryEvents, daily_summary: $summaryTotal (diff: $diff)\n";
|
||||
|
||||
// Check why there's a difference
|
||||
echo " Checking why...\n";
|
||||
|
||||
// Count active locations/gates/tariffs
|
||||
$stmt = $db->query('SELECT COUNT(*) as count FROM entry_events e
|
||||
INNER JOIN locations l ON e.location_code = l.code AND l.is_active = 1
|
||||
INNER JOIN gates g ON e.location_code = g.location_code AND e.gate_code = g.gate_code AND g.is_active = 1
|
||||
WHERE DATE(e.event_time) = "' . $date . '"');
|
||||
$activeCount = $stmt->fetch()['count'];
|
||||
echo " Events with active locations & gates: $activeCount\n";
|
||||
|
||||
// Count all events
|
||||
echo " All events (including inactive): $entryEvents\n";
|
||||
|
||||
if ($activeCount == $summaryTotal) {
|
||||
echo " ✓ Reason: daily_summary only counts events with active locations/gates\n";
|
||||
} else {
|
||||
echo " ⚠️ Still a mismatch even with active filter\n";
|
||||
}
|
||||
} else {
|
||||
echo " ✓ OK: Counts match\n";
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
// Check if there are dates in daily_summary that don't have entry_events
|
||||
echo "=== Dates in daily_summary without entry_events ===\n";
|
||||
$stmt = $db->query('SELECT summary_date, SUM(total_count) as total FROM daily_summary
|
||||
WHERE summary_date NOT IN (SELECT DISTINCT DATE(event_time) FROM entry_events)
|
||||
GROUP BY summary_date ORDER BY summary_date DESC LIMIT 10');
|
||||
$orphanDates = $stmt->fetchAll();
|
||||
if (empty($orphanDates)) {
|
||||
echo "None found - OK\n";
|
||||
} else {
|
||||
foreach ($orphanDates as $row) {
|
||||
echo $row['summary_date'] . ' - ' . $row['total'] . ' total (orphaned data)' . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
102
bin/check_dashboard_data.php
Normal file
102
bin/check_dashboard_data.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Support\Database;
|
||||
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
|
||||
$dbHost = AppConfig::get('DB_HOST', 'localhost');
|
||||
$dbName = AppConfig::get('DB_NAME', '');
|
||||
$dbUser = AppConfig::get('DB_USER', '');
|
||||
$dbPass = AppConfig::get('DB_PASS', '');
|
||||
|
||||
$db = Database::getConnection($dbHost, $dbName, $dbUser, $dbPass);
|
||||
|
||||
echo "=== Cek Data Dashboard ===\n\n";
|
||||
|
||||
// 1. Cek entry_events
|
||||
echo "1. Entry Events:\n";
|
||||
$stmt = $db->query('SELECT COUNT(*) as total FROM entry_events');
|
||||
$result = $stmt->fetch();
|
||||
echo " Total: {$result['total']}\n";
|
||||
|
||||
$stmt = $db->query('SELECT COUNT(*) as total FROM entry_events WHERE DATE(event_time) = CURDATE()');
|
||||
$result = $stmt->fetch();
|
||||
echo " Hari ini: {$result['total']}\n";
|
||||
|
||||
// 2. Cek daily_summary
|
||||
echo "\n2. Daily Summary:\n";
|
||||
$stmt = $db->query('SELECT COUNT(*) as total FROM daily_summary');
|
||||
$result = $stmt->fetch();
|
||||
echo " Total: {$result['total']}\n";
|
||||
|
||||
$stmt = $db->query('SELECT * FROM daily_summary WHERE summary_date = CURDATE() LIMIT 5');
|
||||
$results = $stmt->fetchAll();
|
||||
echo " Hari ini: " . count($results) . " records\n";
|
||||
if (!empty($results)) {
|
||||
foreach ($results as $row) {
|
||||
echo " - {$row['summary_date']} | {$row['location_code']} | {$row['gate_code']} | {$row['category']} | Count: {$row['total_count']} | Amount: {$row['total_amount']}\n";
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Cek locations
|
||||
echo "\n3. Locations:\n";
|
||||
$stmt = $db->query('SELECT COUNT(*) as total FROM locations WHERE is_active = 1');
|
||||
$result = $stmt->fetch();
|
||||
echo " Active: {$result['total']}\n";
|
||||
|
||||
// 4. Cek gates
|
||||
echo "\n4. Gates:\n";
|
||||
$stmt = $db->query('SELECT COUNT(*) as total FROM gates WHERE is_active = 1');
|
||||
$result = $stmt->fetch();
|
||||
echo " Active: {$result['total']}\n";
|
||||
|
||||
// 5. Test query dashboard summary
|
||||
echo "\n5. Test Query Dashboard Summary (Hari Ini):\n";
|
||||
$sql = "
|
||||
SELECT
|
||||
SUM(total_count) as total_count,
|
||||
SUM(total_amount) as total_amount
|
||||
FROM daily_summary
|
||||
WHERE summary_date = CURDATE()
|
||||
";
|
||||
$stmt = $db->query($sql);
|
||||
$result = $stmt->fetch();
|
||||
echo " Total Count: " . ($result['total_count'] ?? 0) . "\n";
|
||||
echo " Total Amount: " . ($result['total_amount'] ?? 0) . "\n";
|
||||
|
||||
// 6. Test query by category
|
||||
echo "\n6. Test Query By Category (Hari Ini):\n";
|
||||
$sql = "
|
||||
SELECT
|
||||
category,
|
||||
SUM(total_count) as total_count,
|
||||
SUM(total_amount) as total_amount
|
||||
FROM daily_summary
|
||||
WHERE summary_date = CURDATE()
|
||||
GROUP BY category
|
||||
";
|
||||
$stmt = $db->query($sql);
|
||||
$results = $stmt->fetchAll();
|
||||
if (empty($results)) {
|
||||
echo " Tidak ada data\n";
|
||||
} else {
|
||||
foreach ($results as $row) {
|
||||
echo " - {$row['category']}: Count={$row['total_count']}, Amount={$row['total_amount']}\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n=== Kesimpulan ===\n";
|
||||
if (($result['total_count'] ?? 0) == 0) {
|
||||
echo "⚠️ Data kosong! Kemungkinan:\n";
|
||||
echo " 1. Belum ada data entry_events\n";
|
||||
echo " 2. Data belum di-aggregate ke daily_summary\n";
|
||||
echo " 3. Perlu jalankan: php bin/daily_summary.php\n";
|
||||
} else {
|
||||
echo "✅ Data ada, tapi mungkin perlu di-aggregate ulang\n";
|
||||
}
|
||||
|
||||
29
bin/check_dates.php
Normal file
29
bin/check_dates.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Support\Database;
|
||||
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
|
||||
$db = Database::getConnection(
|
||||
AppConfig::get('DB_HOST'),
|
||||
AppConfig::get('DB_NAME'),
|
||||
AppConfig::get('DB_USER'),
|
||||
AppConfig::get('DB_PASS')
|
||||
);
|
||||
|
||||
echo "=== Dates with entry_events data ===\n";
|
||||
$stmt = $db->query('SELECT DATE(event_time) as date, COUNT(*) as count FROM entry_events GROUP BY DATE(event_time) ORDER BY date DESC LIMIT 10');
|
||||
$results = $stmt->fetchAll();
|
||||
foreach ($results as $row) {
|
||||
echo $row['date'] . ' - ' . $row['count'] . ' events' . "\n";
|
||||
}
|
||||
|
||||
echo "\n=== Dates with daily_summary data ===\n";
|
||||
$stmt = $db->query('SELECT summary_date, SUM(total_count) as total FROM daily_summary GROUP BY summary_date ORDER BY summary_date DESC LIMIT 10');
|
||||
$results = $stmt->fetchAll();
|
||||
foreach ($results as $row) {
|
||||
echo $row['summary_date'] . ' - ' . $row['total'] . ' total' . "\n";
|
||||
}
|
||||
|
||||
72
bin/check_entry_events.php
Normal file
72
bin/check_entry_events.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Support\Database;
|
||||
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
|
||||
$dbHost = AppConfig::get('DB_HOST', 'localhost');
|
||||
$dbName = AppConfig::get('DB_NAME', '');
|
||||
$dbUser = AppConfig::get('DB_USER', '');
|
||||
$dbPass = AppConfig::get('DB_PASS', '');
|
||||
|
||||
$db = Database::getConnection($dbHost, $dbName, $dbUser, $dbPass);
|
||||
|
||||
echo "=== Cek Entry Events ===\n\n";
|
||||
|
||||
// Cek tanggal data
|
||||
echo "1. Data per tanggal:\n";
|
||||
$stmt = $db->query('SELECT DATE(event_time) as date, COUNT(*) as total FROM entry_events GROUP BY DATE(event_time) ORDER BY date DESC LIMIT 10');
|
||||
$results = $stmt->fetchAll();
|
||||
foreach ($results as $row) {
|
||||
echo " {$row['date']}: {$row['total']} events\n";
|
||||
}
|
||||
|
||||
// Cek data hari ini
|
||||
echo "\n2. Data hari ini:\n";
|
||||
$stmt = $db->query('SELECT COUNT(*) as total FROM entry_events WHERE DATE(event_time) = CURDATE()');
|
||||
$result = $stmt->fetch();
|
||||
echo " Total: {$result['total']}\n";
|
||||
|
||||
// Cek sample data
|
||||
echo "\n3. Sample data (5 terakhir):\n";
|
||||
$stmt = $db->query('SELECT id, location_code, gate_code, category, event_time FROM entry_events ORDER BY id DESC LIMIT 5');
|
||||
$results = $stmt->fetchAll();
|
||||
foreach ($results as $row) {
|
||||
echo " ID: {$row['id']} | {$row['location_code']} | {$row['gate_code']} | {$row['category']} | {$row['event_time']}\n";
|
||||
}
|
||||
|
||||
// Cek apakah data punya location/gate yang valid
|
||||
echo "\n4. Validasi data:\n";
|
||||
$stmt = $db->query("
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN location_code IS NULL OR location_code = '' THEN 1 END) as null_location,
|
||||
COUNT(CASE WHEN gate_code IS NULL OR gate_code = '' THEN 1 END) as null_gate,
|
||||
COUNT(CASE WHEN category IS NULL OR category = '' THEN 1 END) as null_category
|
||||
FROM entry_events
|
||||
WHERE DATE(event_time) = CURDATE()
|
||||
");
|
||||
$result = $stmt->fetch();
|
||||
echo " Total hari ini: {$result['total']}\n";
|
||||
echo " Null location_code: {$result['null_location']}\n";
|
||||
echo " Null gate_code: {$result['null_gate']}\n";
|
||||
echo " Null category: {$result['null_category']}\n";
|
||||
|
||||
// Cek apakah location/gate ada di master
|
||||
echo "\n5. Validasi dengan master data:\n";
|
||||
$stmt = $db->query("
|
||||
SELECT
|
||||
COUNT(*) as total_valid
|
||||
FROM entry_events e
|
||||
INNER JOIN locations l ON e.location_code = l.code AND l.is_active = 1
|
||||
INNER JOIN gates g ON e.location_code = g.location_code AND e.gate_code = g.gate_code AND g.is_active = 1
|
||||
WHERE DATE(e.event_time) = CURDATE()
|
||||
");
|
||||
$result = $stmt->fetch();
|
||||
echo " Data valid (ada di master): {$result['total_valid']}\n";
|
||||
|
||||
34
bin/check_gates.php
Normal file
34
bin/check_gates.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Support\Database;
|
||||
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
|
||||
$db = Database::getConnection(
|
||||
AppConfig::get('DB_HOST'),
|
||||
AppConfig::get('DB_NAME'),
|
||||
AppConfig::get('DB_USER'),
|
||||
AppConfig::get('DB_PASS')
|
||||
);
|
||||
|
||||
echo "=== Gates in database ===\n\n";
|
||||
|
||||
$stmt = $db->query('SELECT location_code, gate_code, name, direction, camera, is_active FROM gates ORDER BY location_code, gate_code');
|
||||
$gates = $stmt->fetchAll();
|
||||
|
||||
if (empty($gates)) {
|
||||
echo "No gates found in database!\n";
|
||||
} else {
|
||||
echo "Total gates: " . count($gates) . "\n\n";
|
||||
foreach ($gates as $gate) {
|
||||
echo "Location: {$gate['location_code']}\n";
|
||||
echo " Gate Code: {$gate['gate_code']}\n";
|
||||
echo " Name: {$gate['name']}\n";
|
||||
echo " Direction: {$gate['direction']}\n";
|
||||
echo " Camera: " . ($gate['camera'] ?: 'NULL') . "\n";
|
||||
echo " Active: " . ($gate['is_active'] ? 'Yes' : 'No') . "\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
66
bin/check_hourly_summary.php
Normal file
66
bin/check_hourly_summary.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Support\Database;
|
||||
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
|
||||
$db = Database::getConnection(
|
||||
AppConfig::get('DB_HOST'),
|
||||
AppConfig::get('DB_NAME'),
|
||||
AppConfig::get('DB_USER'),
|
||||
AppConfig::get('DB_PASS')
|
||||
);
|
||||
|
||||
$dates = ['2025-12-14', '2025-12-15', '2025-12-16', '2025-12-17', '2025-12-18'];
|
||||
|
||||
echo "=== Checking hourly_summary ===\n\n";
|
||||
|
||||
foreach ($dates as $date) {
|
||||
echo "--- Date: $date ---\n";
|
||||
|
||||
// Check entry_events per hour
|
||||
$stmt = $db->prepare('SELECT HOUR(event_time) as hour, COUNT(*) as count FROM entry_events WHERE DATE(event_time) = ? GROUP BY HOUR(event_time) ORDER BY hour');
|
||||
$stmt->execute([$date]);
|
||||
$entryHours = $stmt->fetchAll();
|
||||
|
||||
$entryTotal = 0;
|
||||
foreach ($entryHours as $row) {
|
||||
$entryTotal += $row['count'];
|
||||
}
|
||||
|
||||
echo " entry_events: $entryTotal total events\n";
|
||||
if (!empty($entryHours)) {
|
||||
echo " Hours with data: " . count($entryHours) . "\n";
|
||||
}
|
||||
|
||||
// Check hourly_summary
|
||||
$stmt = $db->prepare('SELECT summary_hour, SUM(total_count) as total_count, SUM(total_amount) as total_amount FROM hourly_summary WHERE summary_date = ? GROUP BY summary_hour ORDER BY summary_hour');
|
||||
$stmt->execute([$date]);
|
||||
$summaryHours = $stmt->fetchAll();
|
||||
|
||||
$summaryTotal = 0;
|
||||
foreach ($summaryHours as $row) {
|
||||
$summaryTotal += $row['total_count'];
|
||||
}
|
||||
|
||||
echo " hourly_summary: $summaryTotal total\n";
|
||||
if (!empty($summaryHours)) {
|
||||
echo " Hours with data: " . count($summaryHours) . "\n";
|
||||
} else {
|
||||
echo " ⚠️ No hourly_summary data!\n";
|
||||
echo " Run: php bin/hourly_summary.php $date\n";
|
||||
}
|
||||
|
||||
if ($entryTotal > 0 && $summaryTotal == 0) {
|
||||
echo " ❌ PROBLEM: entry_events has data but hourly_summary is empty!\n";
|
||||
} elseif ($entryTotal > 0 && $summaryTotal > 0 && $entryTotal != $summaryTotal) {
|
||||
echo " ⚠️ WARNING: Count mismatch! entry_events: $entryTotal, hourly_summary: $summaryTotal\n";
|
||||
} elseif ($entryTotal > 0 && $summaryTotal > 0) {
|
||||
echo " ✓ OK: Counts match\n";
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
29
bin/check_locations_table.php
Normal file
29
bin/check_locations_table.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Support\Database;
|
||||
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
|
||||
$db = Database::getConnection(
|
||||
AppConfig::get('DB_HOST'),
|
||||
AppConfig::get('DB_NAME'),
|
||||
AppConfig::get('DB_USER'),
|
||||
AppConfig::get('DB_PASS')
|
||||
);
|
||||
|
||||
echo "=== Columns in locations table ===\n\n";
|
||||
|
||||
$stmt = $db->query('DESCRIBE locations');
|
||||
$columns = $stmt->fetchAll();
|
||||
|
||||
foreach ($columns as $col) {
|
||||
echo "Field: {$col['Field']}\n";
|
||||
echo " Type: {$col['Type']}\n";
|
||||
echo " Null: {$col['Null']}\n";
|
||||
echo " Key: {$col['Key']}\n";
|
||||
echo " Default: " . ($col['Default'] ?? 'NULL') . "\n";
|
||||
echo " Extra: {$col['Extra']}\n\n";
|
||||
}
|
||||
|
||||
39
bin/check_specific_dates.php
Normal file
39
bin/check_specific_dates.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Support\Database;
|
||||
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
|
||||
$db = Database::getConnection(
|
||||
AppConfig::get('DB_HOST'),
|
||||
AppConfig::get('DB_NAME'),
|
||||
AppConfig::get('DB_USER'),
|
||||
AppConfig::get('DB_PASS')
|
||||
);
|
||||
|
||||
$dates = ['2025-12-16', '2025-12-17', '2025-12-18'];
|
||||
|
||||
echo "=== Checking specific dates ===\n";
|
||||
foreach ($dates as $date) {
|
||||
// Check entry_events
|
||||
$stmt = $db->prepare('SELECT COUNT(*) as count FROM entry_events WHERE DATE(event_time) = ?');
|
||||
$stmt->execute([$date]);
|
||||
$entryCount = $stmt->fetch()['count'];
|
||||
|
||||
// Check daily_summary
|
||||
$stmt = $db->prepare('SELECT SUM(total_count) as total FROM daily_summary WHERE summary_date = ?');
|
||||
$stmt->execute([$date]);
|
||||
$summaryTotal = $stmt->fetch()['total'] ?? 0;
|
||||
|
||||
echo "\nDate: $date\n";
|
||||
echo " entry_events: $entryCount events\n";
|
||||
echo " daily_summary: $summaryTotal total\n";
|
||||
|
||||
if ($entryCount > 0 && $summaryTotal == 0) {
|
||||
echo " ⚠️ Data exists in entry_events but not aggregated to daily_summary!\n";
|
||||
echo " Run: php bin/daily_summary.php $date\n";
|
||||
}
|
||||
}
|
||||
|
||||
55
bin/check_tariffs.php
Normal file
55
bin/check_tariffs.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Support\Database;
|
||||
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
|
||||
$db = Database::getConnection(
|
||||
AppConfig::get('DB_HOST'),
|
||||
AppConfig::get('DB_NAME'),
|
||||
AppConfig::get('DB_USER'),
|
||||
AppConfig::get('DB_PASS')
|
||||
);
|
||||
|
||||
echo "=== Tariffs in database ===\n\n";
|
||||
|
||||
$stmt = $db->query('SELECT location_code, gate_code, category, price FROM tariffs ORDER BY location_code, gate_code, category');
|
||||
$results = $stmt->fetchAll();
|
||||
|
||||
if (empty($results)) {
|
||||
echo "No tariffs found in database!\n";
|
||||
} else {
|
||||
foreach ($results as $row) {
|
||||
$key = $row['location_code'] . '|' . $row['gate_code'] . '|' . $row['category'];
|
||||
echo "Key: $key\n";
|
||||
echo " Location: {$row['location_code']}\n";
|
||||
echo " Gate: {$row['gate_code']}\n";
|
||||
echo " Category: {$row['category']}\n";
|
||||
echo " Price: Rp {$row['price']}\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Check sample events
|
||||
echo "=== Sample events ===\n\n";
|
||||
$stmt = $db->query('SELECT location_code, gate_code, category FROM entry_events ORDER BY event_time DESC LIMIT 5');
|
||||
$events = $stmt->fetchAll();
|
||||
|
||||
foreach ($events as $event) {
|
||||
$key = $event['location_code'] . '|' . $event['gate_code'] . '|' . $event['category'];
|
||||
echo "Event key: $key\n";
|
||||
|
||||
// Check if tariff exists
|
||||
$tariffStmt = $db->prepare('SELECT price FROM tariffs WHERE location_code = ? AND gate_code = ? AND category = ?');
|
||||
$tariffStmt->execute([$event['location_code'], $event['gate_code'], $event['category']]);
|
||||
$tariff = $tariffStmt->fetch();
|
||||
|
||||
if ($tariff) {
|
||||
echo " Tariff found: Rp {$tariff['price']}\n";
|
||||
} else {
|
||||
echo " ⚠️ Tariff NOT found!\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
86
bin/check_today_data.php
Normal file
86
bin/check_today_data.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Support\Database;
|
||||
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
|
||||
$db = Database::getConnection(
|
||||
AppConfig::get('DB_HOST'),
|
||||
AppConfig::get('DB_NAME'),
|
||||
AppConfig::get('DB_USER'),
|
||||
AppConfig::get('DB_PASS')
|
||||
);
|
||||
|
||||
// Get today's date
|
||||
$today = date('Y-m-d');
|
||||
$yesterday = date('Y-m-d', strtotime('-1 day'));
|
||||
|
||||
echo "=== Checking Today's Data ===\n";
|
||||
echo "Today: $today\n";
|
||||
echo "Yesterday: $yesterday\n\n";
|
||||
|
||||
$dates = [$today, $yesterday, '2025-12-17', '2025-12-18'];
|
||||
|
||||
foreach ($dates as $date) {
|
||||
echo "--- Date: $date ---\n";
|
||||
|
||||
// Check entry_events
|
||||
$stmt = $db->prepare('SELECT COUNT(*) as count, MIN(event_time) as first_event, MAX(event_time) as last_event FROM entry_events WHERE DATE(event_time) = ?');
|
||||
$stmt->execute([$date]);
|
||||
$entryResult = $stmt->fetch();
|
||||
$entryCount = $entryResult['count'];
|
||||
|
||||
echo " entry_events:\n";
|
||||
echo " Count: $entryCount events\n";
|
||||
if ($entryCount > 0) {
|
||||
echo " First event: " . ($entryResult['first_event'] ?? 'N/A') . "\n";
|
||||
echo " Last event: " . ($entryResult['last_event'] ?? 'N/A') . "\n";
|
||||
|
||||
// Get sample data
|
||||
$stmt = $db->prepare('SELECT event_time, location_code, gate_code, category FROM entry_events WHERE DATE(event_time) = ? ORDER BY event_time DESC LIMIT 5');
|
||||
$stmt->execute([$date]);
|
||||
$samples = $stmt->fetchAll();
|
||||
echo " Sample events (last 5):\n";
|
||||
foreach ($samples as $sample) {
|
||||
echo " - " . $sample['event_time'] . " | " . $sample['location_code'] . " | " . $sample['gate_code'] . " | " . $sample['category'] . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Check daily_summary
|
||||
$stmt = $db->prepare('SELECT COUNT(*) as count, SUM(total_count) as total_count, SUM(total_amount) as total_amount FROM daily_summary WHERE summary_date = ?');
|
||||
$stmt->execute([$date]);
|
||||
$summaryResult = $stmt->fetch();
|
||||
$summaryCount = $summaryResult['count'];
|
||||
$summaryTotal = $summaryResult['total_count'] ?? 0;
|
||||
$summaryAmount = $summaryResult['total_amount'] ?? 0;
|
||||
|
||||
echo " daily_summary:\n";
|
||||
echo " Records: $summaryCount\n";
|
||||
echo " Total count: $summaryTotal\n";
|
||||
echo " Total amount: $summaryAmount\n";
|
||||
|
||||
if ($entryCount > 0 && $summaryCount == 0) {
|
||||
echo " ⚠️ WARNING: Data exists in entry_events but NOT aggregated to daily_summary!\n";
|
||||
echo " Run: php bin/daily_summary.php $date\n";
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
// Check all dates with data
|
||||
echo "=== All dates with entry_events (last 10) ===\n";
|
||||
$stmt = $db->query('SELECT DATE(event_time) as date, COUNT(*) as count FROM entry_events GROUP BY DATE(event_time) ORDER BY date DESC LIMIT 10');
|
||||
$results = $stmt->fetchAll();
|
||||
foreach ($results as $row) {
|
||||
echo $row['date'] . ' - ' . $row['count'] . ' events' . "\n";
|
||||
}
|
||||
|
||||
echo "\n=== All dates with daily_summary (last 10) ===\n";
|
||||
$stmt = $db->query('SELECT summary_date, SUM(total_count) as total FROM daily_summary GROUP BY summary_date ORDER BY summary_date DESC LIMIT 10');
|
||||
$results = $stmt->fetchAll();
|
||||
foreach ($results as $row) {
|
||||
echo $row['summary_date'] . ' - ' . $row['total'] . ' total' . "\n";
|
||||
}
|
||||
|
||||
100
bin/debug_daily_summary.php
Normal file
100
bin/debug_daily_summary.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Support\Database;
|
||||
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
|
||||
$db = Database::getConnection(
|
||||
AppConfig::get('DB_HOST'),
|
||||
AppConfig::get('DB_NAME'),
|
||||
AppConfig::get('DB_USER'),
|
||||
AppConfig::get('DB_PASS')
|
||||
);
|
||||
|
||||
$date = '2025-12-15'; // Date with mismatch
|
||||
|
||||
echo "=== Debugging daily_summary for $date ===\n\n";
|
||||
|
||||
// Total events
|
||||
$stmt = $db->prepare('SELECT COUNT(*) as count FROM entry_events WHERE DATE(event_time) = ?');
|
||||
$stmt->execute([$date]);
|
||||
$totalEvents = $stmt->fetch()['count'];
|
||||
echo "Total entry_events: $totalEvents\n\n";
|
||||
|
||||
// Events with active locations
|
||||
$stmt = $db->prepare('SELECT COUNT(*) as count FROM entry_events e
|
||||
INNER JOIN locations l ON e.location_code = l.code AND l.is_active = 1
|
||||
WHERE DATE(e.event_time) = ?');
|
||||
$stmt->execute([$date]);
|
||||
$withActiveLocation = $stmt->fetch()['count'];
|
||||
echo "Events with active location: $withActiveLocation\n";
|
||||
|
||||
// Events with active gates
|
||||
$stmt = $db->prepare('SELECT COUNT(*) as count FROM entry_events e
|
||||
INNER JOIN locations l ON e.location_code = l.code AND l.is_active = 1
|
||||
INNER JOIN gates g ON e.location_code = g.location_code AND e.gate_code = g.gate_code AND g.is_active = 1
|
||||
WHERE DATE(e.event_time) = ?');
|
||||
$stmt->execute([$date]);
|
||||
$withActiveGate = $stmt->fetch()['count'];
|
||||
echo "Events with active location + gate: $withActiveGate\n\n";
|
||||
|
||||
// Events without active location
|
||||
$stmt = $db->prepare('SELECT COUNT(*) as count FROM entry_events e
|
||||
LEFT JOIN locations l ON e.location_code = l.code
|
||||
WHERE DATE(e.event_time) = ? AND (l.code IS NULL OR l.is_active = 0)');
|
||||
$stmt->execute([$date]);
|
||||
$withoutActiveLocation = $stmt->fetch()['count'];
|
||||
echo "Events WITHOUT active location: $withoutActiveLocation\n";
|
||||
|
||||
// Events without active gate
|
||||
$stmt = $db->prepare('SELECT COUNT(*) as count FROM entry_events e
|
||||
INNER JOIN locations l ON e.location_code = l.code AND l.is_active = 1
|
||||
LEFT JOIN gates g ON e.location_code = g.location_code AND e.gate_code = g.gate_code
|
||||
WHERE DATE(e.event_time) = ? AND (g.gate_code IS NULL OR g.is_active = 0)');
|
||||
$stmt->execute([$date]);
|
||||
$withoutActiveGate = $stmt->fetch()['count'];
|
||||
echo "Events with active location but WITHOUT active gate: $withoutActiveGate\n\n";
|
||||
|
||||
// Check daily_summary
|
||||
$stmt = $db->prepare('SELECT SUM(total_count) as total FROM daily_summary WHERE summary_date = ?');
|
||||
$stmt->execute([$date]);
|
||||
$summaryTotal = $stmt->fetch()['total'] ?? 0;
|
||||
echo "daily_summary total_count: $summaryTotal\n\n";
|
||||
|
||||
// Check what's in daily_summary
|
||||
$stmt = $db->prepare('SELECT location_code, gate_code, category, total_count, total_amount FROM daily_summary WHERE summary_date = ? ORDER BY location_code, gate_code, category');
|
||||
$stmt->execute([$date]);
|
||||
$summaryRows = $stmt->fetchAll();
|
||||
echo "daily_summary records:\n";
|
||||
foreach ($summaryRows as $row) {
|
||||
echo " - " . $row['location_code'] . " | " . $row['gate_code'] . " | " . $row['category'] . " | count: " . $row['total_count'] . " | amount: " . $row['total_amount'] . "\n";
|
||||
}
|
||||
|
||||
// Check events that should be aggregated
|
||||
$stmt = $db->prepare('SELECT
|
||||
e.location_code,
|
||||
e.gate_code,
|
||||
e.category,
|
||||
COUNT(*) as count,
|
||||
COALESCE(t.price, 0) as price
|
||||
FROM entry_events e
|
||||
INNER JOIN locations l ON e.location_code = l.code AND l.is_active = 1
|
||||
INNER JOIN gates g ON e.location_code = g.location_code AND e.gate_code = g.gate_code AND g.is_active = 1
|
||||
LEFT JOIN tariffs t ON e.location_code = t.location_code AND e.gate_code = t.gate_code AND e.category = t.category
|
||||
WHERE DATE(e.event_time) = ?
|
||||
GROUP BY e.location_code, e.gate_code, e.category, COALESCE(t.price, 0)
|
||||
ORDER BY e.location_code, e.gate_code, e.category');
|
||||
$stmt->execute([$date]);
|
||||
$shouldBeAggregated = $stmt->fetchAll();
|
||||
echo "\nEvents that SHOULD be aggregated:\n";
|
||||
$totalShouldBe = 0;
|
||||
foreach ($shouldBeAggregated as $row) {
|
||||
$totalShouldBe += $row['count'];
|
||||
echo " - " . $row['location_code'] . " | " . $row['gate_code'] . " | " . $row['category'] . " | count: " . $row['count'] . " | price: " . $row['price'] . "\n";
|
||||
}
|
||||
echo "\nTotal that should be aggregated: $totalShouldBe\n";
|
||||
echo "Total in daily_summary: $summaryTotal\n";
|
||||
echo "Difference: " . ($totalShouldBe - $summaryTotal) . "\n";
|
||||
|
||||
64
bin/run_all_daily_summary.php
Normal file
64
bin/run_all_daily_summary.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Support\Database;
|
||||
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
|
||||
$db = Database::getConnection(
|
||||
AppConfig::get('DB_HOST'),
|
||||
AppConfig::get('DB_NAME'),
|
||||
AppConfig::get('DB_USER'),
|
||||
AppConfig::get('DB_PASS')
|
||||
);
|
||||
|
||||
// Get all dates with entry_events
|
||||
$stmt = $db->query('SELECT DISTINCT DATE(event_time) as date FROM entry_events ORDER BY date DESC');
|
||||
$dates = $stmt->fetchAll();
|
||||
|
||||
echo "=== Running daily_summary for all dates with entry_events ===\n\n";
|
||||
|
||||
foreach ($dates as $dateRow) {
|
||||
$date = $dateRow['date'];
|
||||
|
||||
// Skip old/invalid dates
|
||||
if ($date < '2020-01-01') {
|
||||
echo "Skipping old date: $date\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
echo "Processing: $date\n";
|
||||
|
||||
// Run daily_summary for this date
|
||||
$command = sprintf(
|
||||
'php %s/bin/daily_summary.php %s',
|
||||
escapeshellarg(__DIR__ . '/..'),
|
||||
escapeshellarg($date)
|
||||
);
|
||||
|
||||
$output = [];
|
||||
$returnCode = 0;
|
||||
exec($command, $output, $returnCode);
|
||||
|
||||
if ($returnCode === 0) {
|
||||
echo " ✓ Success\n";
|
||||
if (!empty($output)) {
|
||||
foreach ($output as $line) {
|
||||
echo " $line\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo " ✗ Failed (return code: $returnCode)\n";
|
||||
if (!empty($output)) {
|
||||
foreach ($output as $line) {
|
||||
echo " $line\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
echo "=== Done ===\n";
|
||||
|
||||
150
bin/run_migrations.php
Normal file
150
bin/run_migrations.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Script untuk menjalankan migration yang belum dijalankan
|
||||
* Usage: php bin/run_migrations.php
|
||||
*/
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Support\Database;
|
||||
|
||||
// Load environment variables
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
|
||||
echo "=== Menjalankan Database Migrations ===\n\n";
|
||||
|
||||
// Get database connection
|
||||
$dbHost = AppConfig::get('DB_HOST', 'localhost');
|
||||
$dbName = AppConfig::get('DB_NAME', '');
|
||||
$dbUser = AppConfig::get('DB_USER', '');
|
||||
$dbPass = AppConfig::get('DB_PASS', '');
|
||||
|
||||
if (empty($dbName) || empty($dbUser)) {
|
||||
echo "❌ Error: Database credentials tidak lengkap di .env\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::getConnection($dbHost, $dbName, $dbUser, $dbPass);
|
||||
|
||||
$migrationsDir = __DIR__ . '/../migrations';
|
||||
$migrationFiles = [
|
||||
'001_create_audit_logs.sql',
|
||||
'002_create_hourly_summary.sql',
|
||||
'003_create_realtime_events.sql',
|
||||
'004_add_camera_to_gates.sql'
|
||||
];
|
||||
|
||||
foreach ($migrationFiles as $migrationFile) {
|
||||
$migrationPath = $migrationsDir . '/' . $migrationFile;
|
||||
|
||||
if (!file_exists($migrationPath)) {
|
||||
echo "⚠️ File migration tidak ditemukan: {$migrationFile}\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
echo "Menjalankan: {$migrationFile}...\n";
|
||||
|
||||
$sql = file_get_contents($migrationPath);
|
||||
|
||||
// Remove comments and clean up SQL
|
||||
$sql = preg_replace('/--.*$/m', '', $sql);
|
||||
$sql = preg_replace('/\/\*.*?\*\//s', '', $sql);
|
||||
|
||||
// Split by semicolon, but keep multi-line statements intact
|
||||
$statements = [];
|
||||
$currentStatement = '';
|
||||
$lines = explode("\n", $sql);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if (empty($line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$currentStatement .= $line . "\n";
|
||||
|
||||
// If line ends with semicolon, it's a complete statement
|
||||
if (substr(rtrim($line), -1) === ';') {
|
||||
$stmt = trim($currentStatement);
|
||||
if (!empty($stmt) && strlen($stmt) > 5) {
|
||||
$statements[] = $stmt;
|
||||
}
|
||||
$currentStatement = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining statement
|
||||
if (!empty(trim($currentStatement))) {
|
||||
$statements[] = trim($currentStatement);
|
||||
}
|
||||
|
||||
foreach ($statements as $statement) {
|
||||
if (empty(trim($statement))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$db->exec($statement);
|
||||
} catch (\PDOException $e) {
|
||||
// Skip jika error karena table/column sudah ada
|
||||
$errorMsg = $e->getMessage();
|
||||
if (strpos($errorMsg, 'already exists') !== false ||
|
||||
strpos($errorMsg, 'Duplicate column') !== false ||
|
||||
strpos($errorMsg, 'Duplicate key name') !== false) {
|
||||
echo " ⚠️ Sudah ada: " . substr($errorMsg, 0, 100) . "\n";
|
||||
} else {
|
||||
echo " ❌ Error: " . $errorMsg . "\n";
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo " ✅ Selesai: {$migrationFile}\n\n";
|
||||
}
|
||||
|
||||
echo "=== Migration Selesai ===\n";
|
||||
|
||||
// Verifikasi tabel
|
||||
echo "\n=== Verifikasi Tabel ===\n";
|
||||
$requiredTables = ['audit_logs', 'hourly_summary', 'realtime_events'];
|
||||
|
||||
foreach ($requiredTables as $table) {
|
||||
try {
|
||||
$stmt = $db->query("SHOW TABLES LIKE '{$table}'");
|
||||
$exists = $stmt->fetch() !== false;
|
||||
|
||||
if ($exists) {
|
||||
echo " ✅ Tabel '{$table}' ada\n";
|
||||
} else {
|
||||
echo " ❌ Tabel '{$table}' tidak ada\n";
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
echo " ❌ Error cek tabel '{$table}': " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Cek field camera di gates
|
||||
echo "\n=== Verifikasi Field Camera di Gates ===\n";
|
||||
try {
|
||||
$stmt = $db->query("SHOW COLUMNS FROM gates LIKE 'camera'");
|
||||
$exists = $stmt->fetch() !== false;
|
||||
|
||||
if ($exists) {
|
||||
echo " ✅ Field 'camera' ada di tabel gates\n";
|
||||
} else {
|
||||
echo " ❌ Field 'camera' tidak ada di tabel gates\n";
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
echo " ❌ Error cek field camera: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
echo "❌ Error: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
110
bin/test_api_local.php
Normal file
110
bin/test_api_local.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Script untuk test API lokal
|
||||
* Usage: php bin/test_api_local.php [base_url]
|
||||
* Default: http://localhost:8000
|
||||
*/
|
||||
|
||||
$baseUrl = $argv[1] ?? 'http://localhost:8000';
|
||||
|
||||
echo "=== Test API Lokal ===\n";
|
||||
echo "Base URL: {$baseUrl}\n\n";
|
||||
|
||||
$tests = [
|
||||
[
|
||||
'name' => 'Health Check',
|
||||
'method' => 'GET',
|
||||
'url' => "{$baseUrl}/health",
|
||||
'headers' => [],
|
||||
'body' => null
|
||||
],
|
||||
[
|
||||
'name' => 'Login (Invalid - untuk test)',
|
||||
'method' => 'POST',
|
||||
'url' => "{$baseUrl}/auth/v1/login",
|
||||
'headers' => ['Content-Type: application/json'],
|
||||
'body' => json_encode(['username' => 'test', 'password' => 'test'])
|
||||
],
|
||||
[
|
||||
'name' => 'Get Locations (No Auth - akan 401)',
|
||||
'method' => 'GET',
|
||||
'url' => "{$baseUrl}/retribusi/v1/frontend/locations",
|
||||
'headers' => [],
|
||||
'body' => null
|
||||
],
|
||||
[
|
||||
'name' => 'Dashboard Summary (No Auth - akan 401)',
|
||||
'method' => 'GET',
|
||||
'url' => "{$baseUrl}/retribusi/v1/dashboard/summary",
|
||||
'headers' => [],
|
||||
'body' => null
|
||||
],
|
||||
[
|
||||
'name' => 'Realtime Snapshot (No Auth - akan 401)',
|
||||
'method' => 'GET',
|
||||
'url' => "{$baseUrl}/retribusi/v1/realtime/snapshot",
|
||||
'headers' => [],
|
||||
'body' => null
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($tests as $test) {
|
||||
echo "Test: {$test['name']}\n";
|
||||
echo " URL: {$test['url']}\n";
|
||||
|
||||
$ch = curl_init($test['url']);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $test['method']);
|
||||
|
||||
if (!empty($test['headers'])) {
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $test['headers']);
|
||||
}
|
||||
|
||||
if ($test['body'] !== null) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $test['body']);
|
||||
}
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error) {
|
||||
echo " ❌ Error: {$error}\n";
|
||||
} else {
|
||||
echo " Status: {$httpCode}\n";
|
||||
|
||||
if ($httpCode >= 200 && $httpCode < 300) {
|
||||
echo " ✅ Success\n";
|
||||
$data = json_decode($response, true);
|
||||
if ($data) {
|
||||
echo " Response: " . json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
|
||||
} else {
|
||||
echo " Response: {$response}\n";
|
||||
}
|
||||
} elseif ($httpCode === 401) {
|
||||
echo " ⚠️ Unauthorized (Expected - butuh JWT token)\n";
|
||||
} elseif ($httpCode === 422) {
|
||||
echo " ⚠️ Validation Error (Expected - parameter tidak valid)\n";
|
||||
$data = json_decode($response, true);
|
||||
if ($data) {
|
||||
echo " Response: " . json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
|
||||
}
|
||||
} else {
|
||||
echo " ❌ Failed\n";
|
||||
echo " Response: " . substr($response, 0, 200) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
echo "=== Test Selesai ===\n";
|
||||
echo "\nCatatan:\n";
|
||||
echo "- Status 401 = Normal (butuh JWT token untuk endpoint protected)\n";
|
||||
echo "- Status 422 = Normal (validation error, parameter tidak valid)\n";
|
||||
echo "- Status 200 = Endpoint berfungsi dengan baik\n";
|
||||
|
||||
47
bin/test_api_response.php
Normal file
47
bin/test_api_response.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Support\Database;
|
||||
use App\Modules\Retribusi\Dashboard\DashboardService;
|
||||
use App\Modules\Retribusi\Dashboard\DashboardController;
|
||||
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
|
||||
$db = Database::getConnection(
|
||||
AppConfig::get('DB_HOST'),
|
||||
AppConfig::get('DB_NAME'),
|
||||
AppConfig::get('DB_USER'),
|
||||
AppConfig::get('DB_PASS')
|
||||
);
|
||||
|
||||
$service = new DashboardService($db);
|
||||
$controller = new DashboardController($service);
|
||||
|
||||
// Simulate request
|
||||
$request = new \Slim\Psr7\Factory\ServerRequestFactory();
|
||||
$response = new \Slim\Psr7\Factory\ResponseFactory();
|
||||
|
||||
// Test summary endpoint
|
||||
echo "=== Test Dashboard Summary API Response ===\n\n";
|
||||
|
||||
// Test dengan tanggal yang ada data
|
||||
$request = $request->createServerRequest('GET', '/retribusi/v1/dashboard/summary')
|
||||
->withQueryParams(['date' => '2025-12-16']);
|
||||
|
||||
$response = $controller->getSummary($request, $response->createResponse());
|
||||
|
||||
echo "Status: " . $response->getStatusCode() . "\n";
|
||||
echo "Body:\n";
|
||||
echo $response->getBody() . "\n\n";
|
||||
|
||||
// Test by category
|
||||
$request = $request->withQueryParams(['date' => '2025-12-16']);
|
||||
$response = $controller->getByCategoryChart($request, $response->createResponse());
|
||||
|
||||
echo "=== Test By Category API Response ===\n";
|
||||
echo "Status: " . $response->getStatusCode() . "\n";
|
||||
echo "Body:\n";
|
||||
echo $response->getBody() . "\n";
|
||||
|
||||
45
bin/test_dashboard_fallback.php
Normal file
45
bin/test_dashboard_fallback.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Support\Database;
|
||||
use App\Modules\Retribusi\Dashboard\DashboardService;
|
||||
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
|
||||
$db = Database::getConnection(
|
||||
AppConfig::get('DB_HOST'),
|
||||
AppConfig::get('DB_NAME'),
|
||||
AppConfig::get('DB_USER'),
|
||||
AppConfig::get('DB_PASS')
|
||||
);
|
||||
|
||||
$service = new DashboardService($db);
|
||||
|
||||
echo "=== Test Dashboard Fallback ===\n\n";
|
||||
|
||||
// Test hari ini (tidak ada data)
|
||||
echo "1. Summary hari ini (2025-12-18):\n";
|
||||
$result = $service->getSummary('2025-12-18');
|
||||
echo " Total Count: {$result['total_count']}\n";
|
||||
echo " Total Amount: {$result['total_amount']}\n\n";
|
||||
|
||||
// Test kemarin (ada data)
|
||||
echo "2. Summary kemarin (2025-12-16):\n";
|
||||
$result = $service->getSummary('2025-12-16');
|
||||
echo " Total Count: {$result['total_count']}\n";
|
||||
echo " Total Amount: {$result['total_amount']}\n\n";
|
||||
|
||||
// Test by category hari ini
|
||||
echo "3. By Category hari ini (2025-12-18):\n";
|
||||
$result = $service->getByCategoryChart('2025-12-18');
|
||||
echo " Labels: " . implode(', ', $result['labels']) . "\n";
|
||||
echo " Counts: " . implode(', ', $result['series']['total_count']) . "\n\n";
|
||||
|
||||
// Test by category kemarin
|
||||
echo "4. By Category kemarin (2025-12-16):\n";
|
||||
$result = $service->getByCategoryChart('2025-12-16');
|
||||
echo " Labels: " . implode(', ', $result['labels']) . "\n";
|
||||
echo " Counts: " . implode(', ', $result['series']['total_count']) . "\n";
|
||||
|
||||
80
bin/test_db_connection.php
Normal file
80
bin/test_db_connection.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Script untuk test koneksi database
|
||||
* Usage: php bin/test_db_connection.php
|
||||
*/
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Support\Database;
|
||||
|
||||
// Load environment variables
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
|
||||
echo "=== Test Koneksi Database ===\n\n";
|
||||
|
||||
// Get database connection
|
||||
$dbHost = AppConfig::get('DB_HOST', 'localhost');
|
||||
$dbName = AppConfig::get('DB_NAME', '');
|
||||
$dbUser = AppConfig::get('DB_USER', '');
|
||||
$dbPass = AppConfig::get('DB_PASS', '');
|
||||
|
||||
echo "Konfigurasi Database:\n";
|
||||
echo " Host: {$dbHost}\n";
|
||||
echo " Database: {$dbName}\n";
|
||||
echo " User: {$dbUser}\n";
|
||||
echo " Password: " . (empty($dbPass) ? '(kosong)' : str_repeat('*', strlen($dbPass))) . "\n\n";
|
||||
|
||||
if (empty($dbName) || empty($dbUser)) {
|
||||
echo "❌ Error: Database credentials tidak lengkap di .env\n";
|
||||
echo " Pastikan DB_NAME dan DB_USER sudah di-set\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
echo "Mencoba koneksi ke database...\n";
|
||||
$db = Database::getConnection($dbHost, $dbName, $dbUser, $dbPass);
|
||||
|
||||
echo "✅ Koneksi database BERHASIL!\n\n";
|
||||
|
||||
// Test query sederhana
|
||||
echo "Test query sederhana...\n";
|
||||
$stmt = $db->query("SELECT VERSION() as version, DATABASE() as current_db, NOW() as server_time");
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
echo " MySQL Version: {$result['version']}\n";
|
||||
echo " Current Database: {$result['current_db']}\n";
|
||||
echo " Server Time: {$result['server_time']}\n\n";
|
||||
|
||||
// Cek tabel yang ada
|
||||
echo "Mengecek tabel yang ada...\n";
|
||||
$stmt = $db->query("SHOW TABLES");
|
||||
$tables = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (empty($tables)) {
|
||||
echo " ⚠️ Tidak ada tabel di database ini\n";
|
||||
} else {
|
||||
echo " ✅ Ditemukan " . count($tables) . " tabel:\n";
|
||||
foreach ($tables as $table) {
|
||||
echo " - {$table}\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n=== Test Selesai ===\n";
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
echo "❌ Error: Koneksi database GAGAL!\n";
|
||||
echo " Pesan Error: " . $e->getMessage() . "\n";
|
||||
echo "\nKemungkinan penyebab:\n";
|
||||
echo " 1. Database server tidak berjalan\n";
|
||||
echo " 2. Host/Port salah\n";
|
||||
echo " 3. Username/Password salah\n";
|
||||
echo " 4. Database tidak ada\n";
|
||||
echo " 5. User tidak punya akses ke database\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
50
bin/test_entry_events_api.php
Normal file
50
bin/test_entry_events_api.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Support\Database;
|
||||
use App\Modules\Retribusi\Realtime\RealtimeService;
|
||||
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
|
||||
$db = Database::getConnection(
|
||||
AppConfig::get('DB_HOST'),
|
||||
AppConfig::get('DB_NAME'),
|
||||
AppConfig::get('DB_USER'),
|
||||
AppConfig::get('DB_PASS')
|
||||
);
|
||||
|
||||
$service = new RealtimeService($db);
|
||||
|
||||
echo "=== Testing Entry Events API ===\n\n";
|
||||
|
||||
// Test getEntryEvents
|
||||
$page = 1;
|
||||
$limit = 20;
|
||||
$data = $service->getEntryEvents($page, $limit, null, null, null, null, null);
|
||||
$total = $service->getEntryEventsTotal(null, null, null, null, null);
|
||||
|
||||
echo "Total events: $total\n";
|
||||
echo "Events returned: " . count($data) . "\n\n";
|
||||
|
||||
if (!empty($data)) {
|
||||
echo "Sample events:\n";
|
||||
foreach (array_slice($data, 0, 5) as $event) {
|
||||
echo " - ID: {$event['id']}\n";
|
||||
echo " Time: {$event['event_time']}\n";
|
||||
echo " Location: {$event['location_code']}\n";
|
||||
echo " Gate: {$event['gate_code']}\n";
|
||||
echo " Category: {$event['category']}\n";
|
||||
echo "\n";
|
||||
}
|
||||
} else {
|
||||
echo "No events found!\n";
|
||||
}
|
||||
|
||||
// Test dengan date filter
|
||||
echo "\n=== Test dengan date filter (2025-12-18) ===\n";
|
||||
$data = $service->getEntryEvents($page, $limit, null, null, null, '2025-12-18', '2025-12-18');
|
||||
$total = $service->getEntryEventsTotal(null, null, null, '2025-12-18', '2025-12-18');
|
||||
echo "Total events for 2025-12-18: $total\n";
|
||||
echo "Events returned: " . count($data) . "\n";
|
||||
|
||||
63
bin/test_routes.php
Normal file
63
bin/test_routes.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Bootstrap\AppBootstrap;
|
||||
use App\Config\AppConfig;
|
||||
use App\Modules\Health\HealthRoutes;
|
||||
use App\Modules\Auth\AuthRoutes;
|
||||
use App\Modules\Retribusi\RetribusiRoutes;
|
||||
use App\Modules\Retribusi\Summary\SummaryRoutes;
|
||||
use App\Modules\Retribusi\Dashboard\DashboardRoutes;
|
||||
use App\Modules\Retribusi\Realtime\RealtimeRoutes;
|
||||
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
$app = AppBootstrap::create();
|
||||
|
||||
// Register routes
|
||||
HealthRoutes::register($app);
|
||||
AuthRoutes::register($app);
|
||||
RetribusiRoutes::register($app);
|
||||
SummaryRoutes::register($app);
|
||||
DashboardRoutes::register($app);
|
||||
RealtimeRoutes::register($app);
|
||||
|
||||
// Get all routes
|
||||
$routes = [];
|
||||
foreach ($app->getRouteCollector()->getRoutes() as $route) {
|
||||
foreach ($route->getMethods() as $method) {
|
||||
$routes[] = [
|
||||
'method' => $method,
|
||||
'pattern' => $route->getPattern()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
echo "=== Registered Routes ===\n\n";
|
||||
foreach ($routes as $route) {
|
||||
echo "{$route['method']} {$route['pattern']}\n";
|
||||
}
|
||||
|
||||
echo "\n=== Testing Specific Routes ===\n";
|
||||
$testRoutes = [
|
||||
'/health',
|
||||
'/auth/v1/login',
|
||||
'/retribusi/v1/frontend/locations',
|
||||
'/retribusi/v1/dashboard/summary',
|
||||
'/retribusi/v1/dashboard/daily',
|
||||
'/retribusi/v1/realtime/snapshot',
|
||||
];
|
||||
|
||||
foreach ($testRoutes as $testRoute) {
|
||||
$found = false;
|
||||
foreach ($routes as $route) {
|
||||
// Simple pattern matching
|
||||
$pattern = str_replace(['{', '}'], ['', ''], $route['pattern']);
|
||||
if (strpos($testRoute, $pattern) === 0 || $route['pattern'] === $testRoute) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
echo ($found ? '✅' : '❌') . " {$testRoute}\n";
|
||||
}
|
||||
|
||||
48
bin/test_summary_api.php
Normal file
48
bin/test_summary_api.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\AppConfig;
|
||||
use App\Support\Database;
|
||||
|
||||
AppConfig::loadEnv(__DIR__ . '/..');
|
||||
|
||||
$db = Database::getConnection(
|
||||
AppConfig::get('DB_HOST'),
|
||||
AppConfig::get('DB_NAME'),
|
||||
AppConfig::get('DB_USER'),
|
||||
AppConfig::get('DB_PASS')
|
||||
);
|
||||
|
||||
echo "=== Testing Summary API Endpoints ===\n\n";
|
||||
|
||||
// Test daily_summary
|
||||
echo "1. Daily Summary:\n";
|
||||
$stmt = $db->query('SELECT COUNT(*) as total FROM daily_summary');
|
||||
$result = $stmt->fetch();
|
||||
echo " Total rows: {$result['total']}\n";
|
||||
|
||||
$stmt = $db->query('SELECT summary_date, SUM(total_count) as total_count, SUM(total_amount) as total_amount FROM daily_summary GROUP BY summary_date ORDER BY summary_date DESC LIMIT 5');
|
||||
$results = $stmt->fetchAll();
|
||||
echo " Sample data:\n";
|
||||
foreach ($results as $row) {
|
||||
echo " - {$row['summary_date']}: {$row['total_count']} events, Rp " . number_format($row['total_amount']) . "\n";
|
||||
}
|
||||
|
||||
echo "\n2. Hourly Summary:\n";
|
||||
$stmt = $db->query('SELECT COUNT(*) as total FROM hourly_summary');
|
||||
$result = $stmt->fetch();
|
||||
echo " Total rows: {$result['total']}\n";
|
||||
|
||||
$stmt = $db->query('SELECT summary_date, summary_hour, SUM(total_count) as total_count, SUM(total_amount) as total_amount FROM hourly_summary GROUP BY summary_date, summary_hour ORDER BY summary_date DESC, summary_hour ASC LIMIT 10');
|
||||
$results = $stmt->fetchAll();
|
||||
echo " Sample data:\n";
|
||||
foreach ($results as $row) {
|
||||
echo " - {$row['summary_date']} {$row['summary_hour']}:00: {$row['total_count']} events, Rp " . number_format($row['total_amount']) . "\n";
|
||||
}
|
||||
|
||||
echo "\n3. Checking API response format:\n";
|
||||
echo " GET /retribusi/v1/summary/daily?date=2025-12-16\n";
|
||||
echo " Expected: { success: true, data: { summary_date, total_count, total_amount, ... }, timestamp }\n";
|
||||
echo "\n GET /retribusi/v1/summary/hourly?date=2025-12-16\n";
|
||||
echo " Expected: { success: true, data: { labels: [...], series: { total_count: [...], total_amount: [...] } }, timestamp }\n";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "API Retribusi",
|
||||
"description": "Sistem API Retribusi berbasis Slim Framework 4 untuk infrastruktur pemerintah",
|
||||
"description": "API Retribusi BAPENDA Kabupaten Garut untuk monitoring Retribusi",
|
||||
"version": "1.0.0",
|
||||
"contact": {
|
||||
"name": "BTekno Development Team"
|
||||
|
||||
16
public/router.php
Normal file
16
public/router.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Router script for PHP built-in server
|
||||
* Usage: php -S localhost:8000 router.php
|
||||
*/
|
||||
|
||||
// If the request is for a file that exists, serve it
|
||||
$file = __DIR__ . $_SERVER['REQUEST_URI'];
|
||||
if (file_exists($file) && is_file($file) && $_SERVER['REQUEST_URI'] !== '/') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, route to index.php
|
||||
require __DIR__ . '/index.php';
|
||||
|
||||
@@ -212,29 +212,17 @@ class DashboardController
|
||||
): ResponseInterface {
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
$date = $queryParams['date'] ?? null;
|
||||
if ($date === null || !is_string($date)) {
|
||||
return ResponseHelper::json(
|
||||
$response,
|
||||
[
|
||||
'error' => 'validation_error',
|
||||
'fields' => ['date' => 'Query parameter date is required (Y-m-d format)']
|
||||
],
|
||||
422
|
||||
);
|
||||
// Date is optional, default to today
|
||||
$date = $queryParams['date'] ?? date('Y-m-d');
|
||||
if (!is_string($date)) {
|
||||
$date = date('Y-m-d');
|
||||
}
|
||||
|
||||
// Validate date format
|
||||
$dateTime = \DateTime::createFromFormat('Y-m-d', $date);
|
||||
if ($dateTime === false || $dateTime->format('Y-m-d') !== $date) {
|
||||
return ResponseHelper::json(
|
||||
$response,
|
||||
[
|
||||
'error' => 'validation_error',
|
||||
'fields' => ['date' => 'Invalid date format. Expected Y-m-d (e.g., 2025-01-01)']
|
||||
],
|
||||
422
|
||||
);
|
||||
// If invalid, use today
|
||||
$date = date('Y-m-d');
|
||||
}
|
||||
|
||||
$locationCode = $queryParams['location_code'] ?? null;
|
||||
@@ -242,8 +230,13 @@ class DashboardController
|
||||
$locationCode = null;
|
||||
}
|
||||
|
||||
$gateCode = $queryParams['gate_code'] ?? null;
|
||||
if ($gateCode !== null && !is_string($gateCode)) {
|
||||
$gateCode = null;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = $this->service->getSummary($date, $locationCode);
|
||||
$data = $this->service->getSummary($date, $locationCode, $gateCode);
|
||||
|
||||
return ResponseHelper::json(
|
||||
$response,
|
||||
|
||||
@@ -129,6 +129,61 @@ class DashboardService
|
||||
$totalAmounts[] = (int) $row['total_amount'];
|
||||
}
|
||||
|
||||
// Fallback: jika daily_summary kosong, hitung langsung dari entry_events
|
||||
if (empty($labels)) {
|
||||
$fallbackSql = "
|
||||
SELECT
|
||||
e.category,
|
||||
COUNT(*) as total_count,
|
||||
SUM(COALESCE(t.price, 0)) as total_amount
|
||||
FROM entry_events e
|
||||
INNER JOIN locations l ON e.location_code = l.code AND l.is_active = 1
|
||||
INNER JOIN gates g ON e.location_code = g.location_code
|
||||
AND e.gate_code = g.gate_code
|
||||
AND g.is_active = 1
|
||||
LEFT JOIN tariffs t ON e.location_code = t.location_code
|
||||
AND e.gate_code = t.gate_code
|
||||
AND e.category = t.category
|
||||
WHERE DATE(e.event_time) = ?
|
||||
";
|
||||
|
||||
$fallbackParams = [$date];
|
||||
|
||||
if ($locationCode !== null) {
|
||||
$fallbackSql .= " AND e.location_code = ?";
|
||||
$fallbackParams[] = $locationCode;
|
||||
}
|
||||
|
||||
if ($gateCode !== null) {
|
||||
$fallbackSql .= " AND e.gate_code = ?";
|
||||
$fallbackParams[] = $gateCode;
|
||||
}
|
||||
|
||||
$fallbackSql .= " GROUP BY e.category ORDER BY e.category ASC";
|
||||
|
||||
$fallbackStmt = $this->db->prepare($fallbackSql);
|
||||
$fallbackStmt->execute($fallbackParams);
|
||||
$fallbackResults = $fallbackStmt->fetchAll();
|
||||
|
||||
foreach ($fallbackResults as $row) {
|
||||
$labels[] = $row['category'];
|
||||
$totalCounts[] = (int) $row['total_count'];
|
||||
// Calculate amount: count * price per item
|
||||
$priceSql = "
|
||||
SELECT COALESCE(t.price, 0) as price
|
||||
FROM tariffs t
|
||||
WHERE t.location_code = ? AND t.gate_code = ? AND t.category = ?
|
||||
LIMIT 1
|
||||
";
|
||||
$priceParams = [$locationCode ?? '', $gateCode ?? '', $row['category']];
|
||||
$priceStmt = $this->db->prepare($priceSql);
|
||||
$priceStmt->execute($priceParams);
|
||||
$priceRow = $priceStmt->fetch();
|
||||
$price = (int) ($priceRow['price'] ?? 0);
|
||||
$totalAmounts[] = (int) $row['total_count'] * $price;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'labels' => $labels,
|
||||
'series' => [
|
||||
@@ -143,10 +198,11 @@ class DashboardService
|
||||
*
|
||||
* @param string $date
|
||||
* @param string|null $locationCode
|
||||
* @param string|null $gateCode
|
||||
* @return array
|
||||
* @throws PDOException
|
||||
*/
|
||||
public function getSummary(string $date, ?string $locationCode = null): array
|
||||
public function getSummary(string $date, ?string $locationCode = null, ?string $gateCode = null): array
|
||||
{
|
||||
// Get total count and amount from daily_summary
|
||||
$sql = "
|
||||
@@ -164,6 +220,11 @@ class DashboardService
|
||||
$params[] = $locationCode;
|
||||
}
|
||||
|
||||
if ($gateCode !== null) {
|
||||
$sql .= " AND gate_code = ?";
|
||||
$params[] = $gateCode;
|
||||
}
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$summary = $stmt->fetch();
|
||||
@@ -171,6 +232,78 @@ class DashboardService
|
||||
$totalCount = (int) ($summary['total_count'] ?? 0);
|
||||
$totalAmount = (int) ($summary['total_amount'] ?? 0);
|
||||
|
||||
// Fallback: jika daily_summary kosong, hitung langsung dari entry_events
|
||||
if ($totalCount == 0 && $totalAmount == 0) {
|
||||
$fallbackSql = "
|
||||
SELECT
|
||||
COUNT(*) as total_count,
|
||||
SUM(COALESCE(t.price, 0)) as total_amount
|
||||
FROM entry_events e
|
||||
INNER JOIN locations l ON e.location_code = l.code AND l.is_active = 1
|
||||
INNER JOIN gates g ON e.location_code = g.location_code
|
||||
AND e.gate_code = g.gate_code
|
||||
AND g.is_active = 1
|
||||
LEFT JOIN tariffs t ON e.location_code = t.location_code
|
||||
AND e.gate_code = t.gate_code
|
||||
AND e.category = t.category
|
||||
WHERE DATE(e.event_time) = ?
|
||||
";
|
||||
|
||||
$fallbackParams = [$date];
|
||||
|
||||
if ($locationCode !== null) {
|
||||
$fallbackSql .= " AND e.location_code = ?";
|
||||
$fallbackParams[] = $locationCode;
|
||||
}
|
||||
|
||||
if ($gateCode !== null) {
|
||||
$fallbackSql .= " AND e.gate_code = ?";
|
||||
$fallbackParams[] = $gateCode;
|
||||
}
|
||||
|
||||
$fallbackStmt = $this->db->prepare($fallbackSql);
|
||||
$fallbackStmt->execute($fallbackParams);
|
||||
$fallbackSummary = $fallbackStmt->fetch();
|
||||
|
||||
$totalCount = (int) ($fallbackSummary['total_count'] ?? 0);
|
||||
// Calculate total amount from count * price per category
|
||||
$amountSql = "
|
||||
SELECT
|
||||
e.category,
|
||||
COUNT(*) as count,
|
||||
COALESCE(t.price, 0) as price
|
||||
FROM entry_events e
|
||||
INNER JOIN locations l ON e.location_code = l.code AND l.is_active = 1
|
||||
INNER JOIN gates g ON e.location_code = g.location_code
|
||||
AND e.gate_code = g.gate_code
|
||||
AND g.is_active = 1
|
||||
LEFT JOIN tariffs t ON e.location_code = t.location_code
|
||||
AND e.gate_code = t.gate_code
|
||||
AND e.category = t.category
|
||||
WHERE DATE(e.event_time) = ?
|
||||
";
|
||||
|
||||
$amountParams = [$date];
|
||||
if ($locationCode !== null) {
|
||||
$amountSql .= " AND e.location_code = ?";
|
||||
$amountParams[] = $locationCode;
|
||||
}
|
||||
if ($gateCode !== null) {
|
||||
$amountSql .= " AND e.gate_code = ?";
|
||||
$amountParams[] = $gateCode;
|
||||
}
|
||||
$amountSql .= " GROUP BY e.category, COALESCE(t.price, 0)";
|
||||
|
||||
$amountStmt = $this->db->prepare($amountSql);
|
||||
$amountStmt->execute($amountParams);
|
||||
$amountRows = $amountStmt->fetchAll();
|
||||
|
||||
$totalAmount = 0;
|
||||
foreach ($amountRows as $row) {
|
||||
$totalAmount += (int) $row['count'] * (int) $row['price'];
|
||||
}
|
||||
}
|
||||
|
||||
// Get active gates count
|
||||
$gatesSql = "
|
||||
SELECT COUNT(DISTINCT gate_code) as active_gates
|
||||
@@ -185,6 +318,11 @@ class DashboardService
|
||||
$gatesParams[] = $locationCode;
|
||||
}
|
||||
|
||||
if ($gateCode !== null) {
|
||||
$gatesSql .= " AND gate_code = ?";
|
||||
$gatesParams[] = $gateCode;
|
||||
}
|
||||
|
||||
$gatesStmt = $this->db->prepare($gatesSql);
|
||||
$gatesStmt->execute($gatesParams);
|
||||
$gatesResult = $gatesStmt->fetch();
|
||||
@@ -204,6 +342,11 @@ class DashboardService
|
||||
$locationsParams[] = $locationCode;
|
||||
}
|
||||
|
||||
if ($gateCode !== null) {
|
||||
$locationsSql .= " AND gate_code = ?";
|
||||
$locationsParams[] = $gateCode;
|
||||
}
|
||||
|
||||
$locationsStmt = $this->db->prepare($locationsSql);
|
||||
$locationsStmt->execute($locationsParams);
|
||||
$locationsResult = $locationsStmt->fetch();
|
||||
|
||||
@@ -35,30 +35,26 @@ class DailySummaryService
|
||||
|
||||
try {
|
||||
// Aggregate from entry_events
|
||||
// Only count events from active locations, gates, and tariffs
|
||||
// First, count all events per location/gate/category (regardless of tariff price)
|
||||
// Then calculate total_amount using the tariff price
|
||||
$sql = "
|
||||
SELECT
|
||||
DATE(e.event_time) as summary_date,
|
||||
e.location_code,
|
||||
e.gate_code,
|
||||
e.category,
|
||||
COUNT(*) as total_count,
|
||||
COALESCE(t.price, 0) as tariff_amount
|
||||
COUNT(*) as total_count
|
||||
FROM entry_events e
|
||||
INNER JOIN locations l ON e.location_code = l.code AND l.is_active = 1
|
||||
INNER JOIN gates g ON e.location_code = g.location_code
|
||||
AND e.gate_code = g.gate_code
|
||||
AND g.is_active = 1
|
||||
LEFT JOIN tariffs t ON e.location_code = t.location_code
|
||||
AND e.gate_code = t.gate_code
|
||||
AND e.category = t.category
|
||||
WHERE DATE(e.event_time) = ?
|
||||
GROUP BY
|
||||
DATE(e.event_time),
|
||||
e.location_code,
|
||||
e.gate_code,
|
||||
e.category,
|
||||
COALESCE(t.price, 0)
|
||||
e.category
|
||||
";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
@@ -68,7 +64,7 @@ class DailySummaryService
|
||||
$rowsProcessed = 0;
|
||||
|
||||
// Upsert to daily_summary
|
||||
// Note: Table may not have created_at/updated_at columns
|
||||
// Get tariff price for each category and calculate total_amount
|
||||
$upsertSql = "
|
||||
INSERT INTO daily_summary
|
||||
(summary_date, location_code, gate_code, category, total_count, total_amount)
|
||||
@@ -80,8 +76,23 @@ class DailySummaryService
|
||||
|
||||
$upsertStmt = $this->db->prepare($upsertSql);
|
||||
|
||||
// Get tariff prices
|
||||
$tariffSql = "
|
||||
SELECT location_code, gate_code, category, price
|
||||
FROM tariffs
|
||||
";
|
||||
$tariffStmt = $this->db->query($tariffSql);
|
||||
$tariffs = [];
|
||||
foreach ($tariffStmt->fetchAll() as $tariff) {
|
||||
$key = $tariff['location_code'] . '|' . $tariff['gate_code'] . '|' . $tariff['category'];
|
||||
$tariffs[$key] = (int) $tariff['price'];
|
||||
}
|
||||
|
||||
foreach ($aggregated as $row) {
|
||||
$totalAmount = (int) $row['total_count'] * (int) $row['tariff_amount'];
|
||||
// Get tariff price for this location/gate/category
|
||||
$tariffKey = $row['location_code'] . '|' . $row['gate_code'] . '|' . $row['category'];
|
||||
$tariffPrice = $tariffs[$tariffKey] ?? 0;
|
||||
$totalAmount = (int) $row['total_count'] * $tariffPrice;
|
||||
|
||||
$upsertStmt->execute([
|
||||
$row['summary_date'],
|
||||
|
||||
@@ -41,8 +41,8 @@ class HourlySummaryService
|
||||
|
||||
try {
|
||||
// Aggregate from entry_events
|
||||
// Group by hour, location, gate, category
|
||||
// Only count events from active locations, gates, and tariffs
|
||||
// First, count all events per hour/location/gate/category (regardless of tariff price)
|
||||
// Then calculate total_amount using the tariff price
|
||||
$sql = "
|
||||
SELECT
|
||||
DATE(e.event_time) as summary_date,
|
||||
@@ -50,16 +50,12 @@ class HourlySummaryService
|
||||
e.location_code,
|
||||
e.gate_code,
|
||||
e.category,
|
||||
COUNT(*) as total_count,
|
||||
COALESCE(t.price, 0) as tariff_amount
|
||||
COUNT(*) as total_count
|
||||
FROM entry_events e
|
||||
INNER JOIN locations l ON e.location_code = l.code AND l.is_active = 1
|
||||
INNER JOIN gates g ON e.location_code = g.location_code
|
||||
AND e.gate_code = g.gate_code
|
||||
AND g.is_active = 1
|
||||
LEFT JOIN tariffs t ON e.location_code = t.location_code
|
||||
AND e.gate_code = t.gate_code
|
||||
AND e.category = t.category
|
||||
WHERE DATE(e.event_time) = ?
|
||||
";
|
||||
|
||||
@@ -77,8 +73,7 @@ class HourlySummaryService
|
||||
HOUR(e.event_time),
|
||||
e.location_code,
|
||||
e.gate_code,
|
||||
e.category,
|
||||
COALESCE(t.price, 0)
|
||||
e.category
|
||||
";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
@@ -88,7 +83,7 @@ class HourlySummaryService
|
||||
$rowsProcessed = 0;
|
||||
|
||||
// Upsert to hourly_summary
|
||||
// Note: Table may not have created_at/updated_at columns
|
||||
// Get tariff price for each category and calculate total_amount
|
||||
$upsertSql = "
|
||||
INSERT INTO hourly_summary
|
||||
(summary_date, summary_hour, location_code, gate_code, category, total_count, total_amount)
|
||||
@@ -100,8 +95,23 @@ class HourlySummaryService
|
||||
|
||||
$upsertStmt = $this->db->prepare($upsertSql);
|
||||
|
||||
// Get tariff prices
|
||||
$tariffSql = "
|
||||
SELECT location_code, gate_code, category, price
|
||||
FROM tariffs
|
||||
";
|
||||
$tariffStmt = $this->db->query($tariffSql);
|
||||
$tariffs = [];
|
||||
foreach ($tariffStmt->fetchAll() as $tariff) {
|
||||
$key = $tariff['location_code'] . '|' . $tariff['gate_code'] . '|' . $tariff['category'];
|
||||
$tariffs[$key] = (int) $tariff['price'];
|
||||
}
|
||||
|
||||
foreach ($aggregated as $row) {
|
||||
$totalAmount = (int) $row['total_count'] * (int) $row['tariff_amount'];
|
||||
// Get tariff price for this location/gate/category
|
||||
$tariffKey = $row['location_code'] . '|' . $row['gate_code'] . '|' . $row['category'];
|
||||
$tariffPrice = $tariffs[$tariffKey] ?? 0;
|
||||
$totalAmount = (int) $row['total_count'] * $tariffPrice;
|
||||
|
||||
$upsertStmt->execute([
|
||||
$row['summary_date'],
|
||||
|
||||
@@ -129,12 +129,16 @@ class Validator
|
||||
$errors['name'] = 'Field is required';
|
||||
}
|
||||
|
||||
// Type: optional for update
|
||||
// Type: required for create, optional for update
|
||||
// Must be one of: kerkof, pasar, parkir, wisata, lainnya
|
||||
if (isset($data['type'])) {
|
||||
if (!is_string($data['type'])) {
|
||||
$errors['type'] = 'Must be a string';
|
||||
} elseif (strlen($data['type']) > 60) {
|
||||
$errors['type'] = 'Must not exceed 60 characters';
|
||||
} else {
|
||||
$validTypes = ['kerkof', 'pasar', 'parkir', 'wisata', 'lainnya'];
|
||||
if (!in_array($data['type'], $validTypes, true)) {
|
||||
$errors['type'] = 'Must be one of: ' . implode(', ', $validTypes);
|
||||
}
|
||||
}
|
||||
} elseif (!$isUpdate) {
|
||||
$errors['type'] = 'Field is required';
|
||||
|
||||
Reference in New Issue
Block a user