2025-12-18 11:21:40 +07:00
|
|
|
// public/dashboard/js/realtime.js
|
|
|
|
|
// Realtime dashboard (SSE + fallback snapshot)
|
|
|
|
|
|
|
|
|
|
import { apiGetRealtimeSnapshot } from './api.js';
|
|
|
|
|
import { API_CONFIG } from './config.js';
|
|
|
|
|
|
|
|
|
|
const REALTIME_STREAM_URL = `${API_CONFIG.BASE_URL}/retribusi/v1/realtime/stream`;
|
|
|
|
|
|
|
|
|
|
class RealtimeManager {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.eventSource = null;
|
|
|
|
|
this.snapshotTimer = null;
|
|
|
|
|
this.isConnected = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init() {
|
|
|
|
|
// Mulai SSE, kalau gagal pakai fallback polling snapshot
|
|
|
|
|
this.startSSE();
|
|
|
|
|
this.startSnapshotFallback();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
startSSE() {
|
|
|
|
|
try {
|
|
|
|
|
const token = localStorage.getItem('token') || '';
|
|
|
|
|
// SSE tidak support custom headers, jadi token harus di query string
|
|
|
|
|
// TAPI sesuai spec, parameter yang benar adalah 'last_id', bukan 'token'
|
|
|
|
|
// Token tetap dikirim via Authorization header jika backend support
|
|
|
|
|
// Untuk SSE, kita pakai query string karena EventSource tidak support custom headers
|
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
|
// Jika ada last_id dari state, tambahkan
|
|
|
|
|
// params.append('last_id', this.lastEventId || '');
|
|
|
|
|
// Token tetap di query string untuk SSE (limitation EventSource)
|
|
|
|
|
if (token) {
|
|
|
|
|
// Backend harus handle token dari query string atau implement custom SSE handler
|
|
|
|
|
// Untuk sekarang, kita tetap pakai query string karena EventSource limitation
|
|
|
|
|
params.append('token', token);
|
|
|
|
|
}
|
|
|
|
|
const url = `${REALTIME_STREAM_URL}?${params.toString()}`;
|
|
|
|
|
|
|
|
|
|
console.log('[Realtime] Connect SSE:', url);
|
|
|
|
|
|
|
|
|
|
// EventSource tidak support custom headers, jadi token harus di query string
|
|
|
|
|
// Backend harus handle ini atau implement custom SSE handler dengan fetch + stream
|
|
|
|
|
this.eventSource = new EventSource(url);
|
|
|
|
|
|
|
|
|
|
this.eventSource.onopen = () => {
|
|
|
|
|
console.log('[Realtime] SSE opened');
|
|
|
|
|
this.isConnected = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.eventSource.onmessage = (event) => {
|
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(event.data);
|
|
|
|
|
console.log('[Realtime] SSE event:', data);
|
|
|
|
|
|
|
|
|
|
// Backend kirim realtime_events, bisa berupa:
|
|
|
|
|
// - event baru (entry baru)
|
|
|
|
|
// - snapshot agregat (total_count_today, by_category, dll)
|
|
|
|
|
// Sesuaikan dengan struktur yang backend kirim via SSE
|
|
|
|
|
|
|
|
|
|
// Jika backend kirim snapshot via SSE, parse sama seperti fetchSnapshot()
|
|
|
|
|
if (data.total_count_today !== undefined || data.by_category) {
|
|
|
|
|
const personCat = (data.by_category || []).find(c => c.category === 'person_walk') || { total_count: 0 };
|
|
|
|
|
const motorCat = (data.by_category || []).find(c => c.category === 'motor') || { total_count: 0 };
|
|
|
|
|
const carCat = (data.by_category || []).find(c => c.category === 'car') || { total_count: 0 };
|
|
|
|
|
|
|
|
|
|
const kpiData = {
|
|
|
|
|
totalPeople: personCat.total_count || 0,
|
|
|
|
|
totalVehicles: (motorCat.total_count || 0) + (carCat.total_count || 0),
|
|
|
|
|
totalCount: data.total_count_today || 0,
|
|
|
|
|
totalAmount: data.total_amount_today || 0
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.dispatchEvent(new CustomEvent('realtime:snapshot', { detail: kpiData }));
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[Realtime] gagal parse event data', e, event.data);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.eventSource.onerror = (err) => {
|
|
|
|
|
console.error('[Realtime] SSE error', err);
|
|
|
|
|
this.isConnected = false;
|
|
|
|
|
// Biarkan browser auto-reconnect, kalau tetap gagal nanti fallback snapshot yang jalan
|
|
|
|
|
};
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('[Realtime] tidak bisa inisialisasi SSE', e);
|
|
|
|
|
this.isConnected = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fetchSnapshot() {
|
|
|
|
|
try {
|
|
|
|
|
// Struktur response setelah di-unwrap: { total_count_today, total_amount_today, by_gate, by_category }
|
2025-12-19 05:36:42 +07:00
|
|
|
// Gunakan timezone Indonesia UTC+7 untuk mendapatkan tanggal lokal yang benar
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const indonesiaTime = new Date(now.getTime() + (7 * 60 * 60 * 1000)); // UTC+7
|
|
|
|
|
const today = indonesiaTime.toISOString().split('T')[0];
|
2025-12-18 11:21:40 +07:00
|
|
|
const snapshot = await apiGetRealtimeSnapshot({
|
2025-12-19 05:36:42 +07:00
|
|
|
date: today,
|
2025-12-18 11:21:40 +07:00
|
|
|
location_code: '' // bisa diambil dari state dashboard jika perlu
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('[Realtime] snapshot:', snapshot);
|
|
|
|
|
|
|
|
|
|
// Parse data snapshot sesuai struktur resmi
|
|
|
|
|
const parsed = {
|
|
|
|
|
totalCount: snapshot.total_count_today || 0,
|
|
|
|
|
totalAmount: snapshot.total_amount_today || 0,
|
|
|
|
|
byGate: Array.isArray(snapshot.by_gate) ? snapshot.by_gate : [],
|
|
|
|
|
byCategory: Array.isArray(snapshot.by_category) ? snapshot.by_category : []
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Hitung total orang & kendaraan dari by_category
|
|
|
|
|
const personCat = parsed.byCategory.find(c => c.category === 'person_walk') || { total_count: 0 };
|
|
|
|
|
const motorCat = parsed.byCategory.find(c => c.category === 'motor') || { total_count: 0 };
|
|
|
|
|
const carCat = parsed.byCategory.find(c => c.category === 'car') || { total_count: 0 };
|
|
|
|
|
|
|
|
|
|
const kpiData = {
|
|
|
|
|
totalPeople: personCat.total_count || 0,
|
|
|
|
|
totalVehicles: (motorCat.total_count || 0) + (carCat.total_count || 0),
|
|
|
|
|
totalCount: parsed.totalCount,
|
|
|
|
|
totalAmount: parsed.totalAmount
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Dispatch event untuk update dashboard real-time
|
|
|
|
|
window.dispatchEvent(new CustomEvent('realtime:snapshot', { detail: kpiData }));
|
|
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('[Realtime] gagal ambil snapshot', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
startSnapshotFallback() {
|
|
|
|
|
// Polling ringan tiap 5 detik, hanya kalau SSE belum stable
|
|
|
|
|
if (this.snapshotTimer) clearInterval(this.snapshotTimer);
|
|
|
|
|
this.snapshotTimer = setInterval(() => {
|
|
|
|
|
if (!this.isConnected) {
|
|
|
|
|
this.fetchSnapshot();
|
|
|
|
|
}
|
|
|
|
|
}, 5000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stop() {
|
|
|
|
|
if (this.eventSource) {
|
|
|
|
|
this.eventSource.close();
|
|
|
|
|
this.eventSource = null;
|
|
|
|
|
}
|
|
|
|
|
if (this.snapshotTimer) {
|
|
|
|
|
clearInterval(this.snapshotTimer);
|
|
|
|
|
this.snapshotTimer = null;
|
|
|
|
|
}
|
|
|
|
|
this.isConnected = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const Realtime = new RealtimeManager();
|
|
|
|
|
|
|
|
|
|
// Auto-init saat dashboard dibuka
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
Realtime.init();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|