Fix daily_summary dan hourly_summary aggregation, tambah fallback logic untuk dashboard, update validator untuk camera dan location type

This commit is contained in:
mwpn
2025-12-18 11:13:06 +07:00
parent 9416de7d87
commit d05fa2f4cd
31 changed files with 2041 additions and 45 deletions

123
API_LOCAL_TEST_RESULTS.md Normal file
View 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
View 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

View 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

View 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)

View 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";
}
}

View 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
View 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";
}

View 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
View 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";
}
}

View 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";
}

View 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";
}

View 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
View 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
View 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
View 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";

View 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
View 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
View 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
View 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";

View 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";

View 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);
}

View 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
View 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
View 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";

View File

@@ -2,7 +2,7 @@
"openapi": "3.0.0", "openapi": "3.0.0",
"info": { "info": {
"title": "API Retribusi", "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", "version": "1.0.0",
"contact": { "contact": {
"name": "BTekno Development Team" "name": "BTekno Development Team"

16
public/router.php Normal file
View 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';

View File

@@ -212,29 +212,17 @@ class DashboardController
): ResponseInterface { ): ResponseInterface {
$queryParams = $request->getQueryParams(); $queryParams = $request->getQueryParams();
$date = $queryParams['date'] ?? null; // Date is optional, default to today
if ($date === null || !is_string($date)) { $date = $queryParams['date'] ?? date('Y-m-d');
return ResponseHelper::json( if (!is_string($date)) {
$response, $date = date('Y-m-d');
[
'error' => 'validation_error',
'fields' => ['date' => 'Query parameter date is required (Y-m-d format)']
],
422
);
} }
// Validate date format // Validate date format
$dateTime = \DateTime::createFromFormat('Y-m-d', $date); $dateTime = \DateTime::createFromFormat('Y-m-d', $date);
if ($dateTime === false || $dateTime->format('Y-m-d') !== $date) { if ($dateTime === false || $dateTime->format('Y-m-d') !== $date) {
return ResponseHelper::json( // If invalid, use today
$response, $date = date('Y-m-d');
[
'error' => 'validation_error',
'fields' => ['date' => 'Invalid date format. Expected Y-m-d (e.g., 2025-01-01)']
],
422
);
} }
$locationCode = $queryParams['location_code'] ?? null; $locationCode = $queryParams['location_code'] ?? null;
@@ -242,8 +230,13 @@ class DashboardController
$locationCode = null; $locationCode = null;
} }
$gateCode = $queryParams['gate_code'] ?? null;
if ($gateCode !== null && !is_string($gateCode)) {
$gateCode = null;
}
try { try {
$data = $this->service->getSummary($date, $locationCode); $data = $this->service->getSummary($date, $locationCode, $gateCode);
return ResponseHelper::json( return ResponseHelper::json(
$response, $response,

View File

@@ -129,6 +129,61 @@ class DashboardService
$totalAmounts[] = (int) $row['total_amount']; $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 [ return [
'labels' => $labels, 'labels' => $labels,
'series' => [ 'series' => [
@@ -143,10 +198,11 @@ class DashboardService
* *
* @param string $date * @param string $date
* @param string|null $locationCode * @param string|null $locationCode
* @param string|null $gateCode
* @return array * @return array
* @throws PDOException * @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 // Get total count and amount from daily_summary
$sql = " $sql = "
@@ -164,6 +220,11 @@ class DashboardService
$params[] = $locationCode; $params[] = $locationCode;
} }
if ($gateCode !== null) {
$sql .= " AND gate_code = ?";
$params[] = $gateCode;
}
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute($params); $stmt->execute($params);
$summary = $stmt->fetch(); $summary = $stmt->fetch();
@@ -171,6 +232,78 @@ class DashboardService
$totalCount = (int) ($summary['total_count'] ?? 0); $totalCount = (int) ($summary['total_count'] ?? 0);
$totalAmount = (int) ($summary['total_amount'] ?? 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 // Get active gates count
$gatesSql = " $gatesSql = "
SELECT COUNT(DISTINCT gate_code) as active_gates SELECT COUNT(DISTINCT gate_code) as active_gates
@@ -185,6 +318,11 @@ class DashboardService
$gatesParams[] = $locationCode; $gatesParams[] = $locationCode;
} }
if ($gateCode !== null) {
$gatesSql .= " AND gate_code = ?";
$gatesParams[] = $gateCode;
}
$gatesStmt = $this->db->prepare($gatesSql); $gatesStmt = $this->db->prepare($gatesSql);
$gatesStmt->execute($gatesParams); $gatesStmt->execute($gatesParams);
$gatesResult = $gatesStmt->fetch(); $gatesResult = $gatesStmt->fetch();
@@ -204,6 +342,11 @@ class DashboardService
$locationsParams[] = $locationCode; $locationsParams[] = $locationCode;
} }
if ($gateCode !== null) {
$locationsSql .= " AND gate_code = ?";
$locationsParams[] = $gateCode;
}
$locationsStmt = $this->db->prepare($locationsSql); $locationsStmt = $this->db->prepare($locationsSql);
$locationsStmt->execute($locationsParams); $locationsStmt->execute($locationsParams);
$locationsResult = $locationsStmt->fetch(); $locationsResult = $locationsStmt->fetch();

View File

@@ -35,30 +35,26 @@ class DailySummaryService
try { try {
// Aggregate from entry_events // 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 = " $sql = "
SELECT SELECT
DATE(e.event_time) as summary_date, DATE(e.event_time) as summary_date,
e.location_code, e.location_code,
e.gate_code, e.gate_code,
e.category, e.category,
COUNT(*) as total_count, COUNT(*) as total_count
COALESCE(t.price, 0) as tariff_amount
FROM entry_events e FROM entry_events e
INNER JOIN locations l ON e.location_code = l.code AND l.is_active = 1 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 INNER JOIN gates g ON e.location_code = g.location_code
AND e.gate_code = g.gate_code AND e.gate_code = g.gate_code
AND g.is_active = 1 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) = ? WHERE DATE(e.event_time) = ?
GROUP BY GROUP BY
DATE(e.event_time), DATE(e.event_time),
e.location_code, e.location_code,
e.gate_code, e.gate_code,
e.category, e.category
COALESCE(t.price, 0)
"; ";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
@@ -68,7 +64,7 @@ class DailySummaryService
$rowsProcessed = 0; $rowsProcessed = 0;
// Upsert to daily_summary // 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 = " $upsertSql = "
INSERT INTO daily_summary INSERT INTO daily_summary
(summary_date, location_code, gate_code, category, total_count, total_amount) (summary_date, location_code, gate_code, category, total_count, total_amount)
@@ -80,8 +76,23 @@ class DailySummaryService
$upsertStmt = $this->db->prepare($upsertSql); $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) { 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([ $upsertStmt->execute([
$row['summary_date'], $row['summary_date'],

View File

@@ -41,8 +41,8 @@ class HourlySummaryService
try { try {
// Aggregate from entry_events // Aggregate from entry_events
// Group by hour, location, gate, category // First, count all events per hour/location/gate/category (regardless of tariff price)
// Only count events from active locations, gates, and tariffs // Then calculate total_amount using the tariff price
$sql = " $sql = "
SELECT SELECT
DATE(e.event_time) as summary_date, DATE(e.event_time) as summary_date,
@@ -50,16 +50,12 @@ class HourlySummaryService
e.location_code, e.location_code,
e.gate_code, e.gate_code,
e.category, e.category,
COUNT(*) as total_count, COUNT(*) as total_count
COALESCE(t.price, 0) as tariff_amount
FROM entry_events e FROM entry_events e
INNER JOIN locations l ON e.location_code = l.code AND l.is_active = 1 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 INNER JOIN gates g ON e.location_code = g.location_code
AND e.gate_code = g.gate_code AND e.gate_code = g.gate_code
AND g.is_active = 1 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) = ? WHERE DATE(e.event_time) = ?
"; ";
@@ -77,8 +73,7 @@ class HourlySummaryService
HOUR(e.event_time), HOUR(e.event_time),
e.location_code, e.location_code,
e.gate_code, e.gate_code,
e.category, e.category
COALESCE(t.price, 0)
"; ";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
@@ -88,7 +83,7 @@ class HourlySummaryService
$rowsProcessed = 0; $rowsProcessed = 0;
// Upsert to hourly_summary // 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 = " $upsertSql = "
INSERT INTO hourly_summary INSERT INTO hourly_summary
(summary_date, summary_hour, location_code, gate_code, category, total_count, total_amount) (summary_date, summary_hour, location_code, gate_code, category, total_count, total_amount)
@@ -100,8 +95,23 @@ class HourlySummaryService
$upsertStmt = $this->db->prepare($upsertSql); $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) { 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([ $upsertStmt->execute([
$row['summary_date'], $row['summary_date'],

View File

@@ -129,12 +129,16 @@ class Validator
$errors['name'] = 'Field is required'; $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 (isset($data['type'])) {
if (!is_string($data['type'])) { if (!is_string($data['type'])) {
$errors['type'] = 'Must be a string'; $errors['type'] = 'Must be a string';
} elseif (strlen($data['type']) > 60) { } else {
$errors['type'] = 'Must not exceed 60 characters'; $validTypes = ['kerkof', 'pasar', 'parkir', 'wisata', 'lainnya'];
if (!in_array($data['type'], $validTypes, true)) {
$errors['type'] = 'Must be one of: ' . implode(', ', $validTypes);
}
} }
} elseif (!$isUpdate) { } elseif (!$isUpdate) {
$errors['type'] = 'Field is required'; $errors['type'] = 'Field is required';