diff --git a/API_LOCAL_TEST_RESULTS.md b/API_LOCAL_TEST_RESULTS.md new file mode 100644 index 0000000..e61bf5b --- /dev/null +++ b/API_LOCAL_TEST_RESULTS.md @@ -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 + 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 + diff --git a/FRONTEND_API_COMPATIBILITY.md b/FRONTEND_API_COMPATIBILITY.md new file mode 100644 index 0000000..6aa8d20 --- /dev/null +++ b/FRONTEND_API_COMPATIBILITY.md @@ -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) + diff --git a/bin/check_daily_summary_issue.php b/bin/check_daily_summary_issue.php new file mode 100644 index 0000000..39be7d5 --- /dev/null +++ b/bin/check_daily_summary_issue.php @@ -0,0 +1,91 @@ +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"; + } +} + diff --git a/bin/check_dashboard_data.php b/bin/check_dashboard_data.php new file mode 100644 index 0000000..ace9288 --- /dev/null +++ b/bin/check_dashboard_data.php @@ -0,0 +1,102 @@ +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"; +} + diff --git a/bin/check_dates.php b/bin/check_dates.php new file mode 100644 index 0000000..bb20620 --- /dev/null +++ b/bin/check_dates.php @@ -0,0 +1,29 @@ +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"; +} + diff --git a/bin/check_entry_events.php b/bin/check_entry_events.php new file mode 100644 index 0000000..75377ab --- /dev/null +++ b/bin/check_entry_events.php @@ -0,0 +1,72 @@ +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"; + diff --git a/bin/check_gates.php b/bin/check_gates.php new file mode 100644 index 0000000..707114b --- /dev/null +++ b/bin/check_gates.php @@ -0,0 +1,34 @@ +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"; + } +} + diff --git a/bin/check_hourly_summary.php b/bin/check_hourly_summary.php new file mode 100644 index 0000000..f3f3475 --- /dev/null +++ b/bin/check_hourly_summary.php @@ -0,0 +1,66 @@ +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"; +} + diff --git a/bin/check_locations_table.php b/bin/check_locations_table.php new file mode 100644 index 0000000..ce13e6c --- /dev/null +++ b/bin/check_locations_table.php @@ -0,0 +1,29 @@ +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"; +} + diff --git a/bin/check_specific_dates.php b/bin/check_specific_dates.php new file mode 100644 index 0000000..5be6746 --- /dev/null +++ b/bin/check_specific_dates.php @@ -0,0 +1,39 @@ +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"; + } +} + diff --git a/bin/check_tariffs.php b/bin/check_tariffs.php new file mode 100644 index 0000000..8540f70 --- /dev/null +++ b/bin/check_tariffs.php @@ -0,0 +1,55 @@ +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"; +} + diff --git a/bin/check_today_data.php b/bin/check_today_data.php new file mode 100644 index 0000000..e27013d --- /dev/null +++ b/bin/check_today_data.php @@ -0,0 +1,86 @@ +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"; +} + diff --git a/bin/debug_daily_summary.php b/bin/debug_daily_summary.php new file mode 100644 index 0000000..7541c47 --- /dev/null +++ b/bin/debug_daily_summary.php @@ -0,0 +1,100 @@ +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"; + diff --git a/bin/run_all_daily_summary.php b/bin/run_all_daily_summary.php new file mode 100644 index 0000000..641a5a2 --- /dev/null +++ b/bin/run_all_daily_summary.php @@ -0,0 +1,64 @@ +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"; + diff --git a/bin/run_migrations.php b/bin/run_migrations.php new file mode 100644 index 0000000..0fc6d1e --- /dev/null +++ b/bin/run_migrations.php @@ -0,0 +1,150 @@ + 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); +} + diff --git a/bin/test_api_local.php b/bin/test_api_local.php new file mode 100644 index 0000000..e46d9d5 --- /dev/null +++ b/bin/test_api_local.php @@ -0,0 +1,110 @@ + '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"; + diff --git a/bin/test_api_response.php b/bin/test_api_response.php new file mode 100644 index 0000000..6d0b599 --- /dev/null +++ b/bin/test_api_response.php @@ -0,0 +1,47 @@ +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"; + diff --git a/bin/test_dashboard_fallback.php b/bin/test_dashboard_fallback.php new file mode 100644 index 0000000..0a8eaa1 --- /dev/null +++ b/bin/test_dashboard_fallback.php @@ -0,0 +1,45 @@ +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"; + diff --git a/bin/test_db_connection.php b/bin/test_db_connection.php new file mode 100644 index 0000000..248e3a6 --- /dev/null +++ b/bin/test_db_connection.php @@ -0,0 +1,80 @@ +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); +} + diff --git a/bin/test_entry_events_api.php b/bin/test_entry_events_api.php new file mode 100644 index 0000000..0922acc --- /dev/null +++ b/bin/test_entry_events_api.php @@ -0,0 +1,50 @@ +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"; + diff --git a/bin/test_routes.php b/bin/test_routes.php new file mode 100644 index 0000000..20803dc --- /dev/null +++ b/bin/test_routes.php @@ -0,0 +1,63 @@ +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"; +} + diff --git a/bin/test_summary_api.php b/bin/test_summary_api.php new file mode 100644 index 0000000..2dfdcad --- /dev/null +++ b/bin/test_summary_api.php @@ -0,0 +1,48 @@ +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"; + diff --git a/public/docs/openapi.json b/public/docs/openapi.json index 180f80b..1e7cf3d 100644 --- a/public/docs/openapi.json +++ b/public/docs/openapi.json @@ -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" diff --git a/public/router.php b/public/router.php new file mode 100644 index 0000000..185bea8 --- /dev/null +++ b/public/router.php @@ -0,0 +1,16 @@ +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, diff --git a/src/Modules/Retribusi/Dashboard/DashboardService.php b/src/Modules/Retribusi/Dashboard/DashboardService.php index 92ccf16..79fa432 100644 --- a/src/Modules/Retribusi/Dashboard/DashboardService.php +++ b/src/Modules/Retribusi/Dashboard/DashboardService.php @@ -128,6 +128,61 @@ class DashboardService $totalCounts[] = (int) $row['total_count']; $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, @@ -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,12 +220,89 @@ class DashboardService $params[] = $locationCode; } + if ($gateCode !== null) { + $sql .= " AND gate_code = ?"; + $params[] = $gateCode; + } + $stmt = $this->db->prepare($sql); $stmt->execute($params); $summary = $stmt->fetch(); $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 = " @@ -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(); diff --git a/src/Modules/Retribusi/Summary/DailySummaryService.php b/src/Modules/Retribusi/Summary/DailySummaryService.php index 02b0ac9..1b7811b 100644 --- a/src/Modules/Retribusi/Summary/DailySummaryService.php +++ b/src/Modules/Retribusi/Summary/DailySummaryService.php @@ -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) @@ -79,9 +75,24 @@ 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'], diff --git a/src/Modules/Retribusi/Summary/HourlySummaryService.php b/src/Modules/Retribusi/Summary/HourlySummaryService.php index 25b3514..877ef0d 100644 --- a/src/Modules/Retribusi/Summary/HourlySummaryService.php +++ b/src/Modules/Retribusi/Summary/HourlySummaryService.php @@ -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) @@ -99,9 +94,24 @@ 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'], diff --git a/src/Support/Validator.php b/src/Support/Validator.php index 7f9e976..c0e7699 100644 --- a/src/Support/Validator.php +++ b/src/Support/Validator.php @@ -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';