Files
Retribusi/public/dashboard/js/realtime.js

171 lines
6.2 KiB
JavaScript

// 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 }
// Gunakan timezone Indonesia UTC+7 untuk mendapatkan tanggal lokal yang benar
const now = new Date();
// Format tanggal dalam timezone Asia/Jakarta (UTC+7)
const formatter = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Jakarta',
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
const today = formatter.format(now);
const snapshot = await apiGetRealtimeSnapshot({
date: today,
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();
});