feat: tambah endpoint frontend dan field camera di gate
- Tambah endpoint GET /tariffs (list tariffs)
- Tambah endpoint GET /locations/{code} (detail location)
- Tambah endpoint GET /gates/{location_code}/{gate_code} (detail gate)
- Tambah endpoint GET /tariffs/{location_code}/{gate_code}/{category} (detail tariff)
- Tambah endpoint GET /audit-logs (audit trail history)
- Tambah endpoint GET /entry-events (raw entry events)
- Tambah endpoint GET /realtime/events (realtime events list)
- Tambah field camera di gates (support HLS, RTSP, HTTP streaming)
- Migration 004: add camera column to gates table
- Update validasi dan service untuk support camera field
This commit is contained in:
15
migrations/004_add_camera_to_gates.sql
Normal file
15
migrations/004_add_camera_to_gates.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
-- Migration: Add camera field to gates table
|
||||||
|
-- Description: Tambah field camera untuk menyimpan URL atau identifier kamera di setiap gate
|
||||||
|
-- Supports: HLS (.m3u8), RTSP, HTTP streaming, camera ID, dll
|
||||||
|
-- Date: 2025-01-17
|
||||||
|
|
||||||
|
-- Add camera column to gates table
|
||||||
|
ALTER TABLE gates
|
||||||
|
ADD COLUMN camera VARCHAR(500) NULL
|
||||||
|
COMMENT 'URL streaming kamera (HLS .m3u8, RTSP, HTTP) atau identifier kamera untuk gate ini'
|
||||||
|
AFTER direction;
|
||||||
|
|
||||||
|
-- Add index untuk performa query jika perlu filter by camera
|
||||||
|
-- (optional, uncomment jika diperlukan)
|
||||||
|
-- CREATE INDEX idx_camera ON gates(camera);
|
||||||
|
|
||||||
@@ -48,6 +48,25 @@ DESCRIBE audit_logs;
|
|||||||
- **Tabel**: `audit_logs`
|
- **Tabel**: `audit_logs`
|
||||||
- **Rollback**: Tidak ada (tabel ini critical untuk audit, tidak boleh dihapus)
|
- **Rollback**: Tidak ada (tabel ini critical untuk audit, tidak boleh dihapus)
|
||||||
|
|
||||||
|
### 002_create_hourly_summary.sql
|
||||||
|
- **Tanggal**: 2024-12-28
|
||||||
|
- **Deskripsi**: Membuat tabel `hourly_summary` untuk rekap per jam
|
||||||
|
- **Tabel**: `hourly_summary`
|
||||||
|
|
||||||
|
### 003_create_realtime_events.sql
|
||||||
|
- **Tanggal**: 2024-12-28
|
||||||
|
- **Deskripsi**: Membuat tabel `realtime_events` untuk ring buffer SSE events
|
||||||
|
- **Tabel**: `realtime_events`
|
||||||
|
|
||||||
|
### 004_add_camera_to_gates.sql
|
||||||
|
- **Tanggal**: 2025-01-17
|
||||||
|
- **Deskripsi**: Menambahkan field `camera` ke tabel `gates` untuk menyimpan URL atau identifier kamera
|
||||||
|
- **Tabel**: `gates`
|
||||||
|
- **Rollback**:
|
||||||
|
```sql
|
||||||
|
ALTER TABLE gates DROP COLUMN camera;
|
||||||
|
```
|
||||||
|
|
||||||
## Catatan Penting
|
## Catatan Penting
|
||||||
|
|
||||||
- **JANGAN** hapus atau modify migration file yang sudah di-apply
|
- **JANGAN** hapus atau modify migration file yang sudah di-apply
|
||||||
|
|||||||
98
src/Modules/Retribusi/Frontend/AuditController.php
Normal file
98
src/Modules/Retribusi/Frontend/AuditController.php
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Retribusi\Frontend;
|
||||||
|
|
||||||
|
use App\Support\ResponseHelper;
|
||||||
|
use App\Support\Validator;
|
||||||
|
use PDOException;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
class AuditController
|
||||||
|
{
|
||||||
|
private AuditService $auditService;
|
||||||
|
|
||||||
|
public function __construct(AuditService $auditService)
|
||||||
|
{
|
||||||
|
$this->auditService = $auditService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAuditLogs(
|
||||||
|
ServerRequestInterface $request,
|
||||||
|
ResponseInterface $response
|
||||||
|
): ResponseInterface {
|
||||||
|
$queryParams = $request->getQueryParams();
|
||||||
|
[$page, $limit] = Validator::validatePagination($queryParams);
|
||||||
|
|
||||||
|
$entity = $queryParams['entity'] ?? null;
|
||||||
|
if ($entity !== null && !is_string($entity)) {
|
||||||
|
$entity = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $queryParams['action'] ?? null;
|
||||||
|
if ($action !== null && !is_string($action)) {
|
||||||
|
$action = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$entityKey = $queryParams['entity_key'] ?? null;
|
||||||
|
if ($entityKey !== null && !is_string($entityKey)) {
|
||||||
|
$entityKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$startDate = $queryParams['start_date'] ?? null;
|
||||||
|
if ($startDate !== null && !is_string($startDate)) {
|
||||||
|
$startDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$endDate = $queryParams['end_date'] ?? null;
|
||||||
|
if ($endDate !== null && !is_string($endDate)) {
|
||||||
|
$endDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$data = $this->auditService->getAuditLogs(
|
||||||
|
$page,
|
||||||
|
$limit,
|
||||||
|
$entity,
|
||||||
|
$action,
|
||||||
|
$entityKey,
|
||||||
|
$startDate,
|
||||||
|
$endDate
|
||||||
|
);
|
||||||
|
$total = $this->auditService->getAuditLogsTotal(
|
||||||
|
$entity,
|
||||||
|
$action,
|
||||||
|
$entityKey,
|
||||||
|
$startDate,
|
||||||
|
$endDate
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseHelper::json(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'success' => true,
|
||||||
|
'data' => $data,
|
||||||
|
'meta' => [
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
'total' => $total,
|
||||||
|
'pages' => (int) ceil($total / $limit)
|
||||||
|
],
|
||||||
|
'timestamp' => time()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return ResponseHelper::json(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'error' => 'server_error',
|
||||||
|
'message' => 'Database error occurred'
|
||||||
|
],
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -68,6 +68,149 @@ class AuditService
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get audit logs with pagination and optional filters
|
||||||
|
*
|
||||||
|
* @param int $page
|
||||||
|
* @param int $limit
|
||||||
|
* @param string|null $entity Optional filter by entity (locations|gates|tariffs)
|
||||||
|
* @param string|null $action Optional filter by action (create|update|delete)
|
||||||
|
* @param string|null $entityKey Optional filter by entity key
|
||||||
|
* @param string|null $startDate Optional start date (YYYY-MM-DD)
|
||||||
|
* @param string|null $endDate Optional end date (YYYY-MM-DD)
|
||||||
|
* @return array
|
||||||
|
* @throws PDOException
|
||||||
|
*/
|
||||||
|
public function getAuditLogs(
|
||||||
|
int $page,
|
||||||
|
int $limit,
|
||||||
|
?string $entity = null,
|
||||||
|
?string $action = null,
|
||||||
|
?string $entityKey = null,
|
||||||
|
?string $startDate = null,
|
||||||
|
?string $endDate = null
|
||||||
|
): array {
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($entity !== null) {
|
||||||
|
$where[] = 'entity = ?';
|
||||||
|
$params[] = $entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action !== null) {
|
||||||
|
$where[] = 'action = ?';
|
||||||
|
$params[] = $action;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entityKey !== null) {
|
||||||
|
$where[] = 'entity_key = ?';
|
||||||
|
$params[] = $entityKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($startDate !== null) {
|
||||||
|
$where[] = 'DATE(created_at) >= ?';
|
||||||
|
$params[] = $startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($endDate !== null) {
|
||||||
|
$where[] = 'DATE(created_at) <= ?';
|
||||||
|
$params[] = $endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||||
|
|
||||||
|
$sql = "SELECT id, actor_user_id, actor_username, actor_role, action, entity, entity_key,
|
||||||
|
before_json, after_json, ip_address, user_agent, created_at
|
||||||
|
FROM audit_logs
|
||||||
|
{$whereClause}
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT ? OFFSET ?";
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$paramIndex = 1;
|
||||||
|
foreach ($params as $param) {
|
||||||
|
$stmt->bindValue($paramIndex++, $param, PDO::PARAM_STR);
|
||||||
|
}
|
||||||
|
$stmt->bindValue($paramIndex++, $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue($paramIndex, $offset, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$results = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Parse JSON fields
|
||||||
|
foreach ($results as &$result) {
|
||||||
|
if ($result['before_json'] !== null) {
|
||||||
|
$result['before_json'] = json_decode($result['before_json'], true);
|
||||||
|
}
|
||||||
|
if ($result['after_json'] !== null) {
|
||||||
|
$result['after_json'] = json_decode($result['after_json'], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total count of audit logs
|
||||||
|
*
|
||||||
|
* @param string|null $entity Optional filter by entity
|
||||||
|
* @param string|null $action Optional filter by action
|
||||||
|
* @param string|null $entityKey Optional filter by entity key
|
||||||
|
* @param string|null $startDate Optional start date
|
||||||
|
* @param string|null $endDate Optional end date
|
||||||
|
* @return int
|
||||||
|
* @throws PDOException
|
||||||
|
*/
|
||||||
|
public function getAuditLogsTotal(
|
||||||
|
?string $entity = null,
|
||||||
|
?string $action = null,
|
||||||
|
?string $entityKey = null,
|
||||||
|
?string $startDate = null,
|
||||||
|
?string $endDate = null
|
||||||
|
): int {
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($entity !== null) {
|
||||||
|
$where[] = 'entity = ?';
|
||||||
|
$params[] = $entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action !== null) {
|
||||||
|
$where[] = 'action = ?';
|
||||||
|
$params[] = $action;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entityKey !== null) {
|
||||||
|
$where[] = 'entity_key = ?';
|
||||||
|
$params[] = $entityKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($startDate !== null) {
|
||||||
|
$where[] = 'DATE(created_at) >= ?';
|
||||||
|
$params[] = $startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($endDate !== null) {
|
||||||
|
$where[] = 'DATE(created_at) <= ?';
|
||||||
|
$params[] = $endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||||
|
$sql = "SELECT COUNT(*) FROM audit_logs {$whereClause}";
|
||||||
|
|
||||||
|
if (!empty($params)) {
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
} else {
|
||||||
|
$stmt = $this->db->query($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $stmt->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get client IP address from request
|
* Get client IP address from request
|
||||||
*
|
*
|
||||||
|
|||||||
99
src/Modules/Retribusi/Frontend/EntryEventsController.php
Normal file
99
src/Modules/Retribusi/Frontend/EntryEventsController.php
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Retribusi\Frontend;
|
||||||
|
|
||||||
|
use App\Modules\Retribusi\Realtime\RealtimeService;
|
||||||
|
use App\Support\ResponseHelper;
|
||||||
|
use App\Support\Validator;
|
||||||
|
use PDOException;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
class EntryEventsController
|
||||||
|
{
|
||||||
|
private RealtimeService $realtimeService;
|
||||||
|
|
||||||
|
public function __construct(RealtimeService $realtimeService)
|
||||||
|
{
|
||||||
|
$this->realtimeService = $realtimeService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntryEvents(
|
||||||
|
ServerRequestInterface $request,
|
||||||
|
ResponseInterface $response
|
||||||
|
): ResponseInterface {
|
||||||
|
$queryParams = $request->getQueryParams();
|
||||||
|
[$page, $limit] = Validator::validatePagination($queryParams);
|
||||||
|
|
||||||
|
$locationCode = $queryParams['location_code'] ?? null;
|
||||||
|
if ($locationCode !== null && !is_string($locationCode)) {
|
||||||
|
$locationCode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$gateCode = $queryParams['gate_code'] ?? null;
|
||||||
|
if ($gateCode !== null && !is_string($gateCode)) {
|
||||||
|
$gateCode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$category = $queryParams['category'] ?? null;
|
||||||
|
if ($category !== null && !is_string($category)) {
|
||||||
|
$category = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$startDate = $queryParams['start_date'] ?? null;
|
||||||
|
if ($startDate !== null && !is_string($startDate)) {
|
||||||
|
$startDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$endDate = $queryParams['end_date'] ?? null;
|
||||||
|
if ($endDate !== null && !is_string($endDate)) {
|
||||||
|
$endDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$data = $this->realtimeService->getEntryEvents(
|
||||||
|
$page,
|
||||||
|
$limit,
|
||||||
|
$locationCode,
|
||||||
|
$gateCode,
|
||||||
|
$category,
|
||||||
|
$startDate,
|
||||||
|
$endDate
|
||||||
|
);
|
||||||
|
$total = $this->realtimeService->getEntryEventsTotal(
|
||||||
|
$locationCode,
|
||||||
|
$gateCode,
|
||||||
|
$category,
|
||||||
|
$startDate,
|
||||||
|
$endDate
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseHelper::json(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'success' => true,
|
||||||
|
'data' => $data,
|
||||||
|
'meta' => [
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
'total' => $total,
|
||||||
|
'pages' => (int) ceil($total / $limit)
|
||||||
|
],
|
||||||
|
'timestamp' => time()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return ResponseHelper::json(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'error' => 'server_error',
|
||||||
|
'message' => 'Database error occurred'
|
||||||
|
],
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -57,6 +57,48 @@ class GateController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getGate(
|
||||||
|
ServerRequestInterface $request,
|
||||||
|
ResponseInterface $response,
|
||||||
|
array $args
|
||||||
|
): ResponseInterface {
|
||||||
|
$locationCode = $args['location_code'] ?? null;
|
||||||
|
$gateCode = $args['gate_code'] ?? null;
|
||||||
|
|
||||||
|
if ($locationCode === null || !is_string($locationCode) ||
|
||||||
|
$gateCode === null || !is_string($gateCode)) {
|
||||||
|
return ResponseHelper::json(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'error' => 'validation_error',
|
||||||
|
'fields' => ['location_code' => 'Invalid location_code or gate_code']
|
||||||
|
],
|
||||||
|
422
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->writeService->getGate($locationCode, $gateCode);
|
||||||
|
if ($data === null) {
|
||||||
|
return ResponseHelper::json(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'error' => 'not_found',
|
||||||
|
'message' => 'Gate not found'
|
||||||
|
],
|
||||||
|
404
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseHelper::json(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'success' => true,
|
||||||
|
'data' => $data,
|
||||||
|
'timestamp' => time()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function createGate(
|
public function createGate(
|
||||||
ServerRequestInterface $request,
|
ServerRequestInterface $request,
|
||||||
ResponseInterface $response
|
ResponseInterface $response
|
||||||
|
|||||||
@@ -52,6 +52,45 @@ class LocationController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getLocation(
|
||||||
|
ServerRequestInterface $request,
|
||||||
|
ResponseInterface $response,
|
||||||
|
array $args
|
||||||
|
): ResponseInterface {
|
||||||
|
$code = $args['code'] ?? null;
|
||||||
|
if ($code === null || !is_string($code)) {
|
||||||
|
return ResponseHelper::json(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'error' => 'validation_error',
|
||||||
|
'fields' => ['code' => 'Invalid location code']
|
||||||
|
],
|
||||||
|
422
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->writeService->getLocation($code);
|
||||||
|
if ($data === null) {
|
||||||
|
return ResponseHelper::json(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'error' => 'not_found',
|
||||||
|
'message' => 'Location not found'
|
||||||
|
],
|
||||||
|
404
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseHelper::json(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'success' => true,
|
||||||
|
'data' => $data,
|
||||||
|
'timestamp' => time()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function createLocation(
|
public function createLocation(
|
||||||
ServerRequestInterface $request,
|
ServerRequestInterface $request,
|
||||||
ResponseInterface $response
|
ResponseInterface $response
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class RetribusiReadService
|
|||||||
|
|
||||||
if ($locationCode !== null) {
|
if ($locationCode !== null) {
|
||||||
$stmt = $this->db->prepare(
|
$stmt = $this->db->prepare(
|
||||||
'SELECT g.location_code, g.gate_code, g.name, g.direction, g.is_active,
|
'SELECT g.location_code, g.gate_code, g.name, g.direction, g.camera, g.is_active,
|
||||||
l.name as location_name
|
l.name as location_name
|
||||||
FROM gates g
|
FROM gates g
|
||||||
INNER JOIN locations l ON g.location_code = l.code
|
INNER JOIN locations l ON g.location_code = l.code
|
||||||
@@ -83,7 +83,7 @@ class RetribusiReadService
|
|||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
} else {
|
} else {
|
||||||
$stmt = $this->db->prepare(
|
$stmt = $this->db->prepare(
|
||||||
'SELECT g.location_code, g.gate_code, g.name, g.direction, g.is_active,
|
'SELECT g.location_code, g.gate_code, g.name, g.direction, g.camera, g.is_active,
|
||||||
l.name as location_name
|
l.name as location_name
|
||||||
FROM gates g
|
FROM gates g
|
||||||
INNER JOIN locations l ON g.location_code = l.code
|
INNER JOIN locations l ON g.location_code = l.code
|
||||||
@@ -142,4 +142,90 @@ class RetribusiReadService
|
|||||||
{
|
{
|
||||||
return $this->getGatesTotal($locationCode);
|
return $this->getGatesTotal($locationCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tariffs list with pagination and optional filters
|
||||||
|
*
|
||||||
|
* @param int $page
|
||||||
|
* @param int $limit
|
||||||
|
* @param string|null $locationCode Optional filter by location
|
||||||
|
* @param string|null $gateCode Optional filter by gate
|
||||||
|
* @return array
|
||||||
|
* @throws PDOException
|
||||||
|
*/
|
||||||
|
public function getTariffs(int $page, int $limit, ?string $locationCode = null, ?string $gateCode = null): array
|
||||||
|
{
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($locationCode !== null) {
|
||||||
|
$where[] = 't.location_code = ?';
|
||||||
|
$params[] = $locationCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($gateCode !== null) {
|
||||||
|
$where[] = 't.gate_code = ?';
|
||||||
|
$params[] = $gateCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||||
|
|
||||||
|
$sql = "SELECT t.location_code, t.gate_code, t.category, t.price,
|
||||||
|
l.name as location_name,
|
||||||
|
g.name as gate_name
|
||||||
|
FROM tariffs t
|
||||||
|
INNER JOIN locations l ON t.location_code = l.code
|
||||||
|
INNER JOIN gates g ON t.location_code = g.location_code AND t.gate_code = g.gate_code
|
||||||
|
{$whereClause}
|
||||||
|
ORDER BY t.location_code, t.gate_code, t.category ASC
|
||||||
|
LIMIT ? OFFSET ?";
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$paramIndex = 1;
|
||||||
|
foreach ($params as $param) {
|
||||||
|
$stmt->bindValue($paramIndex++, $param, PDO::PARAM_STR);
|
||||||
|
}
|
||||||
|
$stmt->bindValue($paramIndex++, $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue($paramIndex, $offset, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total count of tariffs
|
||||||
|
*
|
||||||
|
* @param string|null $locationCode Optional filter by location
|
||||||
|
* @param string|null $gateCode Optional filter by gate
|
||||||
|
* @return int
|
||||||
|
* @throws PDOException
|
||||||
|
*/
|
||||||
|
public function getTariffsTotal(?string $locationCode = null, ?string $gateCode = null): int
|
||||||
|
{
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($locationCode !== null) {
|
||||||
|
$where[] = 'location_code = ?';
|
||||||
|
$params[] = $locationCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($gateCode !== null) {
|
||||||
|
$where[] = 'gate_code = ?';
|
||||||
|
$params[] = $gateCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||||
|
$sql = "SELECT COUNT(*) FROM tariffs {$whereClause}";
|
||||||
|
|
||||||
|
if (!empty($params)) {
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
} else {
|
||||||
|
$stmt = $this->db->query($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $stmt->fetchColumn();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class RetribusiWriteService
|
|||||||
public function getGate(string $locationCode, string $gateCode): ?array
|
public function getGate(string $locationCode, string $gateCode): ?array
|
||||||
{
|
{
|
||||||
$stmt = $this->db->prepare(
|
$stmt = $this->db->prepare(
|
||||||
'SELECT location_code, gate_code, name, direction, is_active
|
'SELECT location_code, gate_code, name, direction, camera, is_active
|
||||||
FROM gates
|
FROM gates
|
||||||
WHERE location_code = ? AND gate_code = ?
|
WHERE location_code = ? AND gate_code = ?
|
||||||
LIMIT 1'
|
LIMIT 1'
|
||||||
@@ -147,10 +147,11 @@ class RetribusiWriteService
|
|||||||
public function createGate(array $data): array
|
public function createGate(array $data): array
|
||||||
{
|
{
|
||||||
$direction = isset($data['direction']) ? strtolower($data['direction']) : $data['direction'];
|
$direction = isset($data['direction']) ? strtolower($data['direction']) : $data['direction'];
|
||||||
|
$camera = $data['camera'] ?? null;
|
||||||
|
|
||||||
$stmt = $this->db->prepare(
|
$stmt = $this->db->prepare(
|
||||||
'INSERT INTO gates (location_code, gate_code, name, direction, is_active)
|
'INSERT INTO gates (location_code, gate_code, name, direction, camera, is_active)
|
||||||
VALUES (?, ?, ?, ?, ?)'
|
VALUES (?, ?, ?, ?, ?, ?)'
|
||||||
);
|
);
|
||||||
|
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
@@ -158,6 +159,7 @@ class RetribusiWriteService
|
|||||||
$data['gate_code'],
|
$data['gate_code'],
|
||||||
$data['name'],
|
$data['name'],
|
||||||
$direction,
|
$direction,
|
||||||
|
$camera,
|
||||||
$data['is_active']
|
$data['is_active']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -188,6 +190,11 @@ class RetribusiWriteService
|
|||||||
$params[] = strtolower($data['direction']);
|
$params[] = strtolower($data['direction']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($data['camera'])) {
|
||||||
|
$updates[] = 'camera = ?';
|
||||||
|
$params[] = $data['camera'];
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($data['is_active'])) {
|
if (isset($data['is_active'])) {
|
||||||
$updates[] = 'is_active = ?';
|
$updates[] = 'is_active = ?';
|
||||||
$params[] = $data['is_active'];
|
$params[] = $data['is_active'];
|
||||||
|
|||||||
@@ -12,17 +12,100 @@ use Psr\Http\Message\ServerRequestInterface;
|
|||||||
|
|
||||||
class TariffController
|
class TariffController
|
||||||
{
|
{
|
||||||
|
private RetribusiReadService $readService;
|
||||||
private RetribusiWriteService $writeService;
|
private RetribusiWriteService $writeService;
|
||||||
private AuditService $auditService;
|
private AuditService $auditService;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
RetribusiReadService $readService,
|
||||||
RetribusiWriteService $writeService,
|
RetribusiWriteService $writeService,
|
||||||
AuditService $auditService
|
AuditService $auditService
|
||||||
) {
|
) {
|
||||||
|
$this->readService = $readService;
|
||||||
$this->writeService = $writeService;
|
$this->writeService = $writeService;
|
||||||
$this->auditService = $auditService;
|
$this->auditService = $auditService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTariffs(
|
||||||
|
ServerRequestInterface $request,
|
||||||
|
ResponseInterface $response
|
||||||
|
): ResponseInterface {
|
||||||
|
$queryParams = $request->getQueryParams();
|
||||||
|
[$page, $limit] = Validator::validatePagination($queryParams);
|
||||||
|
|
||||||
|
$locationCode = $queryParams['location_code'] ?? null;
|
||||||
|
if ($locationCode !== null && !is_string($locationCode)) {
|
||||||
|
$locationCode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$gateCode = $queryParams['gate_code'] ?? null;
|
||||||
|
if ($gateCode !== null && !is_string($gateCode)) {
|
||||||
|
$gateCode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->readService->getTariffs($page, $limit, $locationCode, $gateCode);
|
||||||
|
$total = $this->readService->getTariffsTotal($locationCode, $gateCode);
|
||||||
|
|
||||||
|
return ResponseHelper::json(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'success' => true,
|
||||||
|
'data' => $data,
|
||||||
|
'meta' => [
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
'total' => $total,
|
||||||
|
'pages' => (int) ceil($total / $limit)
|
||||||
|
],
|
||||||
|
'timestamp' => time()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTariff(
|
||||||
|
ServerRequestInterface $request,
|
||||||
|
ResponseInterface $response,
|
||||||
|
array $args
|
||||||
|
): ResponseInterface {
|
||||||
|
$locationCode = $args['location_code'] ?? null;
|
||||||
|
$gateCode = $args['gate_code'] ?? null;
|
||||||
|
$category = $args['category'] ?? null;
|
||||||
|
|
||||||
|
if ($locationCode === null || !is_string($locationCode) ||
|
||||||
|
$gateCode === null || !is_string($gateCode) ||
|
||||||
|
$category === null || !is_string($category)) {
|
||||||
|
return ResponseHelper::json(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'error' => 'validation_error',
|
||||||
|
'fields' => ['location_code' => 'Invalid location_code, gate_code, or category']
|
||||||
|
],
|
||||||
|
422
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->writeService->getTariff($locationCode, $gateCode, $category);
|
||||||
|
if ($data === null) {
|
||||||
|
return ResponseHelper::json(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'error' => 'not_found',
|
||||||
|
'message' => 'Tariff not found'
|
||||||
|
],
|
||||||
|
404
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseHelper::json(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'success' => true,
|
||||||
|
'data' => $data,
|
||||||
|
'timestamp' => time()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function createTariff(
|
public function createTariff(
|
||||||
ServerRequestInterface $request,
|
ServerRequestInterface $request,
|
||||||
ResponseInterface $response
|
ResponseInterface $response
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Modules\Retribusi\Realtime;
|
namespace App\Modules\Retribusi\Realtime;
|
||||||
|
|
||||||
|
use App\Support\ResponseHelper;
|
||||||
|
use App\Support\Validator;
|
||||||
use PDOException;
|
use PDOException;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
@@ -200,5 +202,171 @@ class RealtimeController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get entry events list
|
||||||
|
*
|
||||||
|
* @param ServerRequestInterface $request
|
||||||
|
* @param ResponseInterface $response
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function getEntryEvents(
|
||||||
|
ServerRequestInterface $request,
|
||||||
|
ResponseInterface $response
|
||||||
|
): ResponseInterface {
|
||||||
|
$queryParams = $request->getQueryParams();
|
||||||
|
[$page, $limit] = Validator::validatePagination($queryParams);
|
||||||
|
|
||||||
|
$locationCode = $queryParams['location_code'] ?? null;
|
||||||
|
if ($locationCode !== null && !is_string($locationCode)) {
|
||||||
|
$locationCode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$gateCode = $queryParams['gate_code'] ?? null;
|
||||||
|
if ($gateCode !== null && !is_string($gateCode)) {
|
||||||
|
$gateCode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$category = $queryParams['category'] ?? null;
|
||||||
|
if ($category !== null && !is_string($category)) {
|
||||||
|
$category = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$startDate = $queryParams['start_date'] ?? null;
|
||||||
|
if ($startDate !== null && !is_string($startDate)) {
|
||||||
|
$startDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$endDate = $queryParams['end_date'] ?? null;
|
||||||
|
if ($endDate !== null && !is_string($endDate)) {
|
||||||
|
$endDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$data = $this->service->getEntryEvents(
|
||||||
|
$page,
|
||||||
|
$limit,
|
||||||
|
$locationCode,
|
||||||
|
$gateCode,
|
||||||
|
$category,
|
||||||
|
$startDate,
|
||||||
|
$endDate
|
||||||
|
);
|
||||||
|
$total = $this->service->getEntryEventsTotal(
|
||||||
|
$locationCode,
|
||||||
|
$gateCode,
|
||||||
|
$category,
|
||||||
|
$startDate,
|
||||||
|
$endDate
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseHelper::json(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'success' => true,
|
||||||
|
'data' => $data,
|
||||||
|
'meta' => [
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
'total' => $total,
|
||||||
|
'pages' => (int) ceil($total / $limit)
|
||||||
|
],
|
||||||
|
'timestamp' => time()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return ResponseHelper::json(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'error' => 'server_error',
|
||||||
|
'message' => 'Database error occurred'
|
||||||
|
],
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get realtime events list
|
||||||
|
*
|
||||||
|
* @param ServerRequestInterface $request
|
||||||
|
* @param ResponseInterface $response
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function getRealtimeEvents(
|
||||||
|
ServerRequestInterface $request,
|
||||||
|
ResponseInterface $response
|
||||||
|
): ResponseInterface {
|
||||||
|
$queryParams = $request->getQueryParams();
|
||||||
|
[$page, $limit] = Validator::validatePagination($queryParams);
|
||||||
|
|
||||||
|
$locationCode = $queryParams['location_code'] ?? null;
|
||||||
|
if ($locationCode !== null && !is_string($locationCode)) {
|
||||||
|
$locationCode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$gateCode = $queryParams['gate_code'] ?? null;
|
||||||
|
if ($gateCode !== null && !is_string($gateCode)) {
|
||||||
|
$gateCode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$category = $queryParams['category'] ?? null;
|
||||||
|
if ($category !== null && !is_string($category)) {
|
||||||
|
$category = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$startDate = $queryParams['start_date'] ?? null;
|
||||||
|
if ($startDate !== null && !is_string($startDate)) {
|
||||||
|
$startDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$endDate = $queryParams['end_date'] ?? null;
|
||||||
|
if ($endDate !== null && !is_string($endDate)) {
|
||||||
|
$endDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$data = $this->service->getRealtimeEvents(
|
||||||
|
$page,
|
||||||
|
$limit,
|
||||||
|
$locationCode,
|
||||||
|
$gateCode,
|
||||||
|
$category,
|
||||||
|
$startDate,
|
||||||
|
$endDate
|
||||||
|
);
|
||||||
|
$total = $this->service->getRealtimeEventsTotal(
|
||||||
|
$locationCode,
|
||||||
|
$gateCode,
|
||||||
|
$category,
|
||||||
|
$startDate,
|
||||||
|
$endDate
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseHelper::json(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'success' => true,
|
||||||
|
'data' => $data,
|
||||||
|
'meta' => [
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
'total' => $total,
|
||||||
|
'pages' => (int) ceil($total / $limit)
|
||||||
|
],
|
||||||
|
'timestamp' => time()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return ResponseHelper::json(
|
||||||
|
$response,
|
||||||
|
[
|
||||||
|
'error' => 'server_error',
|
||||||
|
'message' => 'Database error occurred'
|
||||||
|
],
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class RealtimeRoutes
|
|||||||
$v1Group->group('/realtime', function ($realtimeGroup) use ($realtimeController) {
|
$v1Group->group('/realtime', function ($realtimeGroup) use ($realtimeController) {
|
||||||
$realtimeGroup->get('/stream', [$realtimeController, 'stream']);
|
$realtimeGroup->get('/stream', [$realtimeController, 'stream']);
|
||||||
$realtimeGroup->get('/snapshot', [$realtimeController, 'getSnapshot']);
|
$realtimeGroup->get('/snapshot', [$realtimeController, 'getSnapshot']);
|
||||||
|
$realtimeGroup->get('/events', [$realtimeController, 'getRealtimeEvents']);
|
||||||
})->add($jwtMiddleware);
|
})->add($jwtMiddleware);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -162,5 +162,265 @@ class RealtimeService
|
|||||||
'by_category' => $byCategoryFormatted
|
'by_category' => $byCategoryFormatted
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get entry events list with pagination and optional filters
|
||||||
|
*
|
||||||
|
* @param int $page
|
||||||
|
* @param int $limit
|
||||||
|
* @param string|null $locationCode Optional filter by location
|
||||||
|
* @param string|null $gateCode Optional filter by gate
|
||||||
|
* @param string|null $category Optional filter by category
|
||||||
|
* @param string|null $startDate Optional start date (YYYY-MM-DD)
|
||||||
|
* @param string|null $endDate Optional end date (YYYY-MM-DD)
|
||||||
|
* @return array
|
||||||
|
* @throws PDOException
|
||||||
|
*/
|
||||||
|
public function getEntryEvents(
|
||||||
|
int $page,
|
||||||
|
int $limit,
|
||||||
|
?string $locationCode = null,
|
||||||
|
?string $gateCode = null,
|
||||||
|
?string $category = null,
|
||||||
|
?string $startDate = null,
|
||||||
|
?string $endDate = null
|
||||||
|
): array {
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($locationCode !== null) {
|
||||||
|
$where[] = 'location_code = ?';
|
||||||
|
$params[] = $locationCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($gateCode !== null) {
|
||||||
|
$where[] = 'gate_code = ?';
|
||||||
|
$params[] = $gateCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($category !== null) {
|
||||||
|
$where[] = 'category = ?';
|
||||||
|
$params[] = $category;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($startDate !== null) {
|
||||||
|
$where[] = 'DATE(event_time) >= ?';
|
||||||
|
$params[] = $startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($endDate !== null) {
|
||||||
|
$where[] = 'DATE(event_time) <= ?';
|
||||||
|
$params[] = $endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||||
|
|
||||||
|
$sql = "SELECT id, location_code, gate_code, category, event_time, source_ip, created_at
|
||||||
|
FROM entry_events
|
||||||
|
{$whereClause}
|
||||||
|
ORDER BY event_time DESC, id DESC
|
||||||
|
LIMIT ? OFFSET ?";
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$paramIndex = 1;
|
||||||
|
foreach ($params as $param) {
|
||||||
|
$stmt->bindValue($paramIndex++, $param, PDO::PARAM_STR);
|
||||||
|
}
|
||||||
|
$stmt->bindValue($paramIndex++, $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue($paramIndex, $offset, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total count of entry events
|
||||||
|
*
|
||||||
|
* @param string|null $locationCode Optional filter by location
|
||||||
|
* @param string|null $gateCode Optional filter by gate
|
||||||
|
* @param string|null $category Optional filter by category
|
||||||
|
* @param string|null $startDate Optional start date
|
||||||
|
* @param string|null $endDate Optional end date
|
||||||
|
* @return int
|
||||||
|
* @throws PDOException
|
||||||
|
*/
|
||||||
|
public function getEntryEventsTotal(
|
||||||
|
?string $locationCode = null,
|
||||||
|
?string $gateCode = null,
|
||||||
|
?string $category = null,
|
||||||
|
?string $startDate = null,
|
||||||
|
?string $endDate = null
|
||||||
|
): int {
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($locationCode !== null) {
|
||||||
|
$where[] = 'location_code = ?';
|
||||||
|
$params[] = $locationCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($gateCode !== null) {
|
||||||
|
$where[] = 'gate_code = ?';
|
||||||
|
$params[] = $gateCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($category !== null) {
|
||||||
|
$where[] = 'category = ?';
|
||||||
|
$params[] = $category;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($startDate !== null) {
|
||||||
|
$where[] = 'DATE(event_time) >= ?';
|
||||||
|
$params[] = $startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($endDate !== null) {
|
||||||
|
$where[] = 'DATE(event_time) <= ?';
|
||||||
|
$params[] = $endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||||
|
$sql = "SELECT COUNT(*) FROM entry_events {$whereClause}";
|
||||||
|
|
||||||
|
if (!empty($params)) {
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
} else {
|
||||||
|
$stmt = $this->db->query($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $stmt->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get realtime events list with pagination and optional filters
|
||||||
|
*
|
||||||
|
* @param int $page
|
||||||
|
* @param int $limit
|
||||||
|
* @param string|null $locationCode Optional filter by location
|
||||||
|
* @param string|null $gateCode Optional filter by gate
|
||||||
|
* @param string|null $category Optional filter by category
|
||||||
|
* @param string|null $startDate Optional start date (YYYY-MM-DD)
|
||||||
|
* @param string|null $endDate Optional end date (YYYY-MM-DD)
|
||||||
|
* @return array
|
||||||
|
* @throws PDOException
|
||||||
|
*/
|
||||||
|
public function getRealtimeEvents(
|
||||||
|
int $page,
|
||||||
|
int $limit,
|
||||||
|
?string $locationCode = null,
|
||||||
|
?string $gateCode = null,
|
||||||
|
?string $category = null,
|
||||||
|
?string $startDate = null,
|
||||||
|
?string $endDate = null
|
||||||
|
): array {
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($locationCode !== null) {
|
||||||
|
$where[] = 'location_code = ?';
|
||||||
|
$params[] = $locationCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($gateCode !== null) {
|
||||||
|
$where[] = 'gate_code = ?';
|
||||||
|
$params[] = $gateCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($category !== null) {
|
||||||
|
$where[] = 'category = ?';
|
||||||
|
$params[] = $category;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($startDate !== null) {
|
||||||
|
$where[] = 'DATE(created_at) >= ?';
|
||||||
|
$params[] = $startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($endDate !== null) {
|
||||||
|
$where[] = 'DATE(created_at) <= ?';
|
||||||
|
$params[] = $endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||||
|
|
||||||
|
$sql = "SELECT id, location_code, gate_code, category, event_time, total_count_delta, created_at
|
||||||
|
FROM realtime_events
|
||||||
|
{$whereClause}
|
||||||
|
ORDER BY created_at DESC, id DESC
|
||||||
|
LIMIT ? OFFSET ?";
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$paramIndex = 1;
|
||||||
|
foreach ($params as $param) {
|
||||||
|
$stmt->bindValue($paramIndex++, $param, PDO::PARAM_STR);
|
||||||
|
}
|
||||||
|
$stmt->bindValue($paramIndex++, $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue($paramIndex, $offset, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total count of realtime events
|
||||||
|
*
|
||||||
|
* @param string|null $locationCode Optional filter by location
|
||||||
|
* @param string|null $gateCode Optional filter by gate
|
||||||
|
* @param string|null $category Optional filter by category
|
||||||
|
* @param string|null $startDate Optional start date
|
||||||
|
* @param string|null $endDate Optional end date
|
||||||
|
* @return int
|
||||||
|
* @throws PDOException
|
||||||
|
*/
|
||||||
|
public function getRealtimeEventsTotal(
|
||||||
|
?string $locationCode = null,
|
||||||
|
?string $gateCode = null,
|
||||||
|
?string $category = null,
|
||||||
|
?string $startDate = null,
|
||||||
|
?string $endDate = null
|
||||||
|
): int {
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($locationCode !== null) {
|
||||||
|
$where[] = 'location_code = ?';
|
||||||
|
$params[] = $locationCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($gateCode !== null) {
|
||||||
|
$where[] = 'gate_code = ?';
|
||||||
|
$params[] = $gateCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($category !== null) {
|
||||||
|
$where[] = 'category = ?';
|
||||||
|
$params[] = $category;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($startDate !== null) {
|
||||||
|
$where[] = 'DATE(created_at) >= ?';
|
||||||
|
$params[] = $startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($endDate !== null) {
|
||||||
|
$where[] = 'DATE(created_at) <= ?';
|
||||||
|
$params[] = $endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||||
|
$sql = "SELECT COUNT(*) FROM realtime_events {$whereClause}";
|
||||||
|
|
||||||
|
if (!empty($params)) {
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
} else {
|
||||||
|
$stmt = $this->db->query($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $stmt->fetchColumn();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,16 @@ use App\Config\AppConfig;
|
|||||||
use App\Middleware\ApiKeyMiddleware;
|
use App\Middleware\ApiKeyMiddleware;
|
||||||
use App\Middleware\JwtMiddleware;
|
use App\Middleware\JwtMiddleware;
|
||||||
use App\Middleware\RoleMiddleware;
|
use App\Middleware\RoleMiddleware;
|
||||||
|
use App\Modules\Retribusi\Frontend\AuditController;
|
||||||
use App\Modules\Retribusi\Frontend\AuditService;
|
use App\Modules\Retribusi\Frontend\AuditService;
|
||||||
|
use App\Modules\Retribusi\Frontend\EntryEventsController;
|
||||||
use App\Modules\Retribusi\Frontend\GateController;
|
use App\Modules\Retribusi\Frontend\GateController;
|
||||||
use App\Modules\Retribusi\Frontend\LocationController;
|
use App\Modules\Retribusi\Frontend\LocationController;
|
||||||
use App\Modules\Retribusi\Frontend\RetribusiReadService;
|
use App\Modules\Retribusi\Frontend\RetribusiReadService;
|
||||||
use App\Modules\Retribusi\Frontend\RetribusiWriteService;
|
use App\Modules\Retribusi\Frontend\RetribusiWriteService;
|
||||||
use App\Modules\Retribusi\Frontend\StreamController;
|
use App\Modules\Retribusi\Frontend\StreamController;
|
||||||
use App\Modules\Retribusi\Frontend\TariffController;
|
use App\Modules\Retribusi\Frontend\TariffController;
|
||||||
|
use App\Modules\Retribusi\Realtime\RealtimeService;
|
||||||
use App\Modules\Retribusi\Ingest\IngestController;
|
use App\Modules\Retribusi\Ingest\IngestController;
|
||||||
use App\Modules\Retribusi\Ingest\IngestService;
|
use App\Modules\Retribusi\Ingest\IngestService;
|
||||||
use App\Support\Database;
|
use App\Support\Database;
|
||||||
@@ -62,7 +65,11 @@ class RetribusiRoutes
|
|||||||
$gateController = new GateController($readService, $writeService, $auditService);
|
$gateController = new GateController($readService, $writeService, $auditService);
|
||||||
$locationController = new LocationController($readService, $writeService, $auditService);
|
$locationController = new LocationController($readService, $writeService, $auditService);
|
||||||
$streamController = new StreamController($readService);
|
$streamController = new StreamController($readService);
|
||||||
$tariffController = new TariffController($writeService, $auditService);
|
$tariffController = new TariffController($readService, $writeService, $auditService);
|
||||||
|
$auditController = new AuditController($auditService);
|
||||||
|
|
||||||
|
$realtimeService = new RealtimeService($db);
|
||||||
|
$entryEventsController = new EntryEventsController($realtimeService);
|
||||||
|
|
||||||
// Register routes
|
// Register routes
|
||||||
$app->group('/retribusi', function ($group) use (
|
$app->group('/retribusi', function ($group) use (
|
||||||
@@ -74,7 +81,9 @@ class RetribusiRoutes
|
|||||||
$gateController,
|
$gateController,
|
||||||
$locationController,
|
$locationController,
|
||||||
$streamController,
|
$streamController,
|
||||||
$tariffController
|
$tariffController,
|
||||||
|
$auditController,
|
||||||
|
$entryEventsController
|
||||||
) {
|
) {
|
||||||
$group->group('/v1', function ($v1Group) use (
|
$group->group('/v1', function ($v1Group) use (
|
||||||
$apiKeyMiddleware,
|
$apiKeyMiddleware,
|
||||||
@@ -85,7 +94,9 @@ class RetribusiRoutes
|
|||||||
$gateController,
|
$gateController,
|
||||||
$locationController,
|
$locationController,
|
||||||
$streamController,
|
$streamController,
|
||||||
$tariffController
|
$tariffController,
|
||||||
|
$auditController,
|
||||||
|
$entryEventsController
|
||||||
) {
|
) {
|
||||||
// Ingest routes (with API key middleware)
|
// Ingest routes (with API key middleware)
|
||||||
$v1Group->post('/ingest', [$ingestController, 'ingest'])
|
$v1Group->post('/ingest', [$ingestController, 'ingest'])
|
||||||
@@ -98,12 +109,20 @@ class RetribusiRoutes
|
|||||||
$gateController,
|
$gateController,
|
||||||
$locationController,
|
$locationController,
|
||||||
$streamController,
|
$streamController,
|
||||||
$tariffController
|
$tariffController,
|
||||||
|
$auditController,
|
||||||
|
$entryEventsController
|
||||||
) {
|
) {
|
||||||
// Read routes (viewer, operator, admin)
|
// Read routes (viewer, operator, admin)
|
||||||
$frontendGroup->get('/gates', [$gateController, 'getGates']);
|
$frontendGroup->get('/gates', [$gateController, 'getGates']);
|
||||||
|
$frontendGroup->get('/gates/{location_code}/{gate_code}', [$gateController, 'getGate']);
|
||||||
$frontendGroup->get('/locations', [$locationController, 'getLocations']);
|
$frontendGroup->get('/locations', [$locationController, 'getLocations']);
|
||||||
|
$frontendGroup->get('/locations/{code}', [$locationController, 'getLocation']);
|
||||||
$frontendGroup->get('/streams', [$streamController, 'getStreams']);
|
$frontendGroup->get('/streams', [$streamController, 'getStreams']);
|
||||||
|
$frontendGroup->get('/tariffs', [$tariffController, 'getTariffs']);
|
||||||
|
$frontendGroup->get('/tariffs/{location_code}/{gate_code}/{category}', [$tariffController, 'getTariff']);
|
||||||
|
$frontendGroup->get('/audit-logs', [$auditController, 'getAuditLogs']);
|
||||||
|
$frontendGroup->get('/entry-events', [$entryEventsController, 'getEntryEvents']);
|
||||||
|
|
||||||
// Write routes (operator, admin)
|
// Write routes (operator, admin)
|
||||||
$frontendGroup->post('/locations', [$locationController, 'createLocation'])
|
$frontendGroup->post('/locations', [$locationController, 'createLocation'])
|
||||||
|
|||||||
@@ -216,6 +216,18 @@ class Validator
|
|||||||
$errors['direction'] = 'Field is required';
|
$errors['direction'] = 'Field is required';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// camera: optional, but if provided must be valid string
|
||||||
|
// Supports various formats: HLS (.m3u8), RTSP, HTTP, camera ID, etc.
|
||||||
|
if (isset($data['camera'])) {
|
||||||
|
if (!is_string($data['camera'])) {
|
||||||
|
$errors['camera'] = 'Must be a string';
|
||||||
|
} elseif (strlen($data['camera']) > 500) {
|
||||||
|
$errors['camera'] = 'Must not exceed 500 characters';
|
||||||
|
} elseif (trim($data['camera']) === '') {
|
||||||
|
$errors['camera'] = 'Cannot be empty string (use null to remove)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// is_active: optional, but if provided must be 0 or 1
|
// is_active: optional, but if provided must be 0 or 1
|
||||||
if (isset($data['is_active'])) {
|
if (isset($data['is_active'])) {
|
||||||
if (!in_array($data['is_active'], [0, 1], true)) {
|
if (!in_array($data['is_active'], [0, 1], true)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user