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

808 lines
27 KiB
JavaScript

// public/dashboard/js/dashboard.js
// Main dashboard logic: filters, KPI cards, charts.
import { Auth } from './auth.js';
import {
apiGetLocations,
apiGetGates,
apiGetSummary,
apiGetDaily,
apiGetByCategory,
apiGetSummaryHourly
} from './api.js';
import {
initDailyChart,
initCategoryChart,
updateDailyChart,
updateCategoryChart,
getDailyChart,
getCategoryChart
} from './charts.js';
// Default date: selalu hari ini (tidak auto-detect ke tanggal lama)
const state = {
date: new Date().toISOString().split('T')[0], // Default: hari ini
locationCode: '',
gateCode: ''
};
// Function untuk auto-detect tanggal terakhir yang ada data
async function getLastAvailableDate() {
try {
// Coba ambil data hari ini dulu
const today = new Date().toISOString().split('T')[0];
const todayData = await apiGetSummary({ date: today });
console.log('[Dashboard] getLastAvailableDate - today data:', todayData);
// Handle jika response masih wrapped (seharusnya sudah di-unwrap oleh api.js)
let todaySummary = todayData;
if (todayData && typeof todayData === 'object' && 'data' in todayData && !('total_count' in todayData)) {
todaySummary = todayData.data || {};
}
// Jika hari ini ada data, return hari ini
if (todaySummary && (todaySummary.total_count > 0 || todaySummary.total_amount > 0)) {
console.log('[Dashboard] Using today:', today);
return today;
}
// Jika tidak, coba kemarin
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const yesterdayStr = yesterday.toISOString().split('T')[0];
const yesterdayData = await apiGetSummary({ date: yesterdayStr });
console.log('[Dashboard] getLastAvailableDate - yesterday data:', yesterdayData);
// Handle jika response masih wrapped
let yesterdaySummary = yesterdayData;
if (yesterdayData && typeof yesterdayData === 'object' && 'data' in yesterdayData && !('total_count' in yesterdayData)) {
yesterdaySummary = yesterdayData.data || {};
}
if (yesterdaySummary && (yesterdaySummary.total_count > 0 || yesterdaySummary.total_amount > 0)) {
console.log('[Dashboard] Using yesterday:', yesterdayStr);
return yesterdayStr;
}
// Jika tidak ada data kemarin, cek 7 hari terakhir
for (let i = 2; i <= 7; i++) {
const prevDate = new Date();
prevDate.setDate(prevDate.getDate() - i);
const prevDateStr = prevDate.toISOString().split('T')[0];
const prevData = await apiGetSummary({ date: prevDateStr });
console.log(`[Dashboard] getLastAvailableDate - ${i} days ago (${prevDateStr}) data:`, prevData);
// Handle jika response masih wrapped
let prevSummary = prevData;
if (prevData && typeof prevData === 'object' && 'data' in prevData && !('total_count' in prevData)) {
prevSummary = prevData.data || {};
}
if (prevSummary && (prevSummary.total_count > 0 || prevSummary.total_amount > 0)) {
console.log(`[Dashboard] Using ${i} days ago:`, prevDateStr);
return prevDateStr;
}
}
// Jika tidak ada data sama sekali, cari tanggal terakhir yang ada data dari API
// Coba query langsung ke API untuk dapat list tanggal yang ada data
// Untuk sementara, return tanggal terakhir yang diketahui ada data (2025-12-16)
// Atau bisa return null dan biarkan user pilih manual
console.log('[Dashboard] No data found in last 7 days');
// Cek tanggal 15 dan 14 juga (karena kita tahu ada data di sana)
const knownDates = ['2025-12-16', '2025-12-15', '2025-12-14'];
for (const knownDate of knownDates) {
const knownData = await apiGetSummary({ date: knownDate });
let knownSummary = knownData;
if (knownData && typeof knownData === 'object' && 'data' in knownData && !('total_count' in knownData)) {
knownSummary = knownData.data || {};
}
if (knownSummary && (knownSummary.total_count > 0 || knownSummary.total_amount > 0)) {
console.log('[Dashboard] Found data in known dates, using:', knownDate);
return knownDate;
}
}
// Default: tetap hari ini (meskipun tidak ada data)
console.log('[Dashboard] No data found anywhere, using today:', today);
return today;
} catch (error) {
console.error('[Dashboard] Error getting last available date:', error);
// Fallback ke tanggal yang pasti ada data
return '2025-12-16';
}
}
// Video HLS setup - menggunakan URL kamera dari database
let gatesCache = {}; // Cache untuk gates dengan camera URL
let locationsCache = {}; // Cache untuk locations
let hls = null;
let isVideoPlaying = false;
let currentVideoUrl = null;
function formatNumber(value) {
return new Intl.NumberFormat('id-ID').format(value || 0);
}
function formatCurrency(value) {
return 'Rp ' + new Intl.NumberFormat('id-ID').format(value || 0);
}
function setTopbarDate() {
const el = document.getElementById('topbar-date');
if (!el) return;
const d = new Date();
el.textContent = d.toLocaleDateString('id-ID', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: '2-digit'
});
}
async function loadLocations() {
const select = document.getElementById('filter-location');
if (!select) return;
select.innerHTML = '<option value="">Semua Lokasi</option>';
try {
const data = await apiGetLocations({ limit: 100 }); // Ambil semua lokasi
// Handle pagination response: { data: [...], total, page, limit }
// atau langsung array: [...]
let items = [];
if (Array.isArray(data)) {
items = data;
} else if (data && Array.isArray(data.data)) {
items = data.data;
}
items.forEach(loc => {
const opt = document.createElement('option');
opt.value = loc.code || loc.location_code || '';
opt.textContent = loc.name || loc.label || opt.value;
select.appendChild(opt);
});
} catch (err) {
console.error('loadLocations error', err);
}
}
async function loadGates() {
const select = document.getElementById('filter-gate');
if (!select) return;
select.innerHTML = '<option value="">Semua Gate</option>';
if (!state.locationCode) return;
try {
const data = await apiGetGates(state.locationCode, { limit: 100 }); // Ambil semua gate
// Handle pagination response: { data: [...], total, page, limit }
// atau langsung array: [...]
let items = [];
if (Array.isArray(data)) {
items = data;
} else if (data && Array.isArray(data.data)) {
items = data.data;
}
items.forEach(g => {
const opt = document.createElement('option');
opt.value = g.code || g.gate_code || '';
opt.textContent = g.name || g.label || opt.value;
select.appendChild(opt);
});
} catch (err) {
console.error('loadGates error', err);
}
}
function renderSummary({ totalAmount, personCount, motorCount, carCount }) {
const amountEl = document.getElementById('card-total-amount');
const personEl = document.getElementById('card-person-count');
const motorEl = document.getElementById('card-motor-count');
const carEl = document.getElementById('card-car-count');
if (amountEl) amountEl.textContent = formatCurrency(totalAmount || 0);
if (personEl) personEl.textContent = formatNumber(personCount || 0);
if (motorEl) motorEl.textContent = formatNumber(motorCount || 0);
if (carEl) carEl.textContent = formatNumber(carCount || 0);
}
function showError(message) {
const el = document.getElementById('summary-error');
if (!el) return;
el.textContent = message;
el.classList.remove('hidden');
setTimeout(() => {
el.classList.add('hidden');
}, 4000);
}
async function loadSummaryAndCharts() {
const loadingOverlay = document.getElementById('summary-loading');
if (loadingOverlay) loadingOverlay.classList.add('visible');
try {
const [summaryResp, hourlyResp, byCategoryResp] = await Promise.all([
apiGetSummary({
date: state.date,
locationCode: state.locationCode,
gateCode: state.gateCode
}),
// pakai summary hourly untuk chart: data hari ini per jam
apiGetSummaryHourly({
date: state.date,
location_code: state.locationCode,
gate_code: state.gateCode
}),
apiGetByCategory({
date: state.date,
locationCode: state.locationCode,
gateCode: state.gateCode
})
]);
// Kartu KPI: pakai total_count & total_amount dari summary endpoint
// Struktur summaryResp setelah di-unwrap: { total_count, total_amount, active_gates, active_locations }
console.log('[Dashboard] Summary response raw:', summaryResp);
console.log('[Dashboard] By Category response raw:', byCategoryResp);
console.log('[Dashboard] State date:', state.date);
// Handle jika response masih wrapped
let summary = summaryResp || {};
if (summaryResp && typeof summaryResp === 'object' && 'data' in summaryResp && !('total_count' in summaryResp)) {
summary = summaryResp.data || {};
}
const totalAmount = summary.total_amount || 0;
console.log('[Dashboard] Parsed summary:', { totalAmount, summary });
// Hitung per kategori dari byCategoryResp
let personCount = 0;
let motorCount = 0;
let carCount = 0;
if (byCategoryResp) {
let categoryData = [];
// Handle jika response masih wrapped
let byCategory = byCategoryResp;
if (byCategoryResp && typeof byCategoryResp === 'object' && 'data' in byCategoryResp && !('labels' in byCategoryResp)) {
byCategory = byCategoryResp.data || byCategoryResp;
}
// Handle struktur response dengan data array (sesuai spec)
if (Array.isArray(byCategory)) {
categoryData = byCategory;
}
// Handle struktur response dengan labels & series (format chart)
else if (byCategory.labels && byCategory.series) {
const labels = byCategory.labels || [];
const counts = byCategory.series.total_count || [];
labels.forEach((label, index) => {
categoryData.push({
category: label,
total_count: counts[index] || 0
});
});
}
console.log('[Dashboard] Category data parsed:', categoryData);
// Extract data per kategori
categoryData.forEach(item => {
const count = item.total_count || 0;
if (item.category === 'person_walk') {
personCount = count;
} else if (item.category === 'motor') {
motorCount = count;
} else if (item.category === 'car') {
carCount = count;
}
});
}
console.log('[Dashboard] Final counts:', { personCount, motorCount, carCount, totalAmount });
console.log('[Dashboard] Summary processed:', {
date: state.date,
locationCode: state.locationCode,
gateCode: state.gateCode,
totalAmount,
personCount,
motorCount,
carCount
});
// Pastikan nilai tidak undefined
const finalTotalAmount = totalAmount || 0;
const finalPersonCount = personCount || 0;
const finalMotorCount = motorCount || 0;
const finalCarCount = carCount || 0;
console.log('[Dashboard] Rendering summary with values:', {
totalAmount: finalTotalAmount,
personCount: finalPersonCount,
motorCount: finalMotorCount,
carCount: finalCarCount
});
renderSummary({
totalAmount: finalTotalAmount,
personCount: finalPersonCount,
motorCount: finalMotorCount,
carCount: finalCarCount
});
// Daily chart data → tampilkan data hari ini per jam
// Struktur response: { labels: ["00","01",...], series: { total_count: [...], total_amount: [...] } }
// ATAU: { data: [{ hour: 0, total_count: 0, total_amount: 0 }, ...] }
let labels = [];
let totalCounts = [];
let totalAmounts = [];
console.log('[Dashboard] Hourly response raw:', hourlyResp);
// Handle jika response masih wrapped
let hourly = hourlyResp;
if (hourlyResp && typeof hourlyResp === 'object' && 'data' in hourlyResp && !('labels' in hourlyResp)) {
hourly = hourlyResp.data || hourlyResp;
}
// Handle struktur response dengan labels & series
if (hourly && hourly.labels && hourly.series) {
labels = hourly.labels.map(h => String(h).padStart(2, '0') + ':00'); // "00" -> "00:00", "1" -> "01:00"
totalCounts = hourly.series.total_count || [];
totalAmounts = hourly.series.total_amount || [];
console.log('[Dashboard] Hourly data from labels & series:', { labelsCount: labels.length, countsCount: totalCounts.length });
}
// Handle struktur response dengan data array (sesuai spec)
else if (hourly && Array.isArray(hourly.data)) {
// Generate 24 jam (0-23)
labels = Array.from({ length: 24 }, (_, i) => `${String(i).padStart(2, '0')}:00`);
totalCounts = Array(24).fill(0);
totalAmounts = Array(24).fill(0);
// Map data dari response ke array per jam
hourly.data.forEach(item => {
const hour = item.hour || 0;
if (hour >= 0 && hour < 24) {
totalCounts[hour] = item.total_count || 0;
totalAmounts[hour] = item.total_amount || 0;
}
});
}
// Jika response kosong/null, tetap buat chart dengan data 0
else {
console.warn('[Dashboard] Hourly response tidak sesuai format, menggunakan data kosong');
labels = Array.from({ length: 24 }, (_, i) => `${String(i).padStart(2, '0')}:00`);
totalCounts = Array(24).fill(0);
totalAmounts = Array(24).fill(0);
}
console.log('[Dashboard] Hourly data processed:', {
date: state.date,
labels: labels.length,
counts: totalCounts.length,
amounts: totalAmounts.length,
totalCount: totalCounts.reduce((a, b) => a + b, 0),
totalAmount: totalAmounts.reduce((a, b) => a + b, 0)
});
// Pastikan chart sudah di-init sebelum update
const dailyCanvas = document.getElementById('daily-chart');
const currentDailyChart = getDailyChart();
if (dailyCanvas && !currentDailyChart) {
console.log('[Dashboard] Daily chart belum di-init, initializing...');
initDailyChart(dailyCanvas.getContext('2d'));
}
// Selalu update chart, meskipun data kosong (agar user tahu memang tidak ada data)
const dailyChartInstance = getDailyChart();
if (dailyChartInstance) {
updateDailyChart({
labels,
counts: totalCounts,
amounts: totalAmounts
});
console.log('[Dashboard] Daily chart updated successfully');
} else {
console.error('[Dashboard] Daily chart tidak bisa di-update, chart belum di-init! Canvas:', dailyCanvas);
}
// Category chart data → pakai total_count per kategori
// Struktur response: { labels: ["person_walk","motor","car"], series: { total_count: [33,12,2], total_amount: [...] } }
// ATAU: { data: [{ category: "person_walk", total_count: 33, total_amount: 66000 }, ...] }
let catLabels = [];
let catValues = [];
console.log('[Dashboard] Category response raw:', byCategoryResp);
// Handle jika response masih wrapped
let byCategory = byCategoryResp;
if (byCategoryResp && typeof byCategoryResp === 'object' && 'data' in byCategoryResp && !('labels' in byCategoryResp)) {
byCategory = byCategoryResp.data || byCategoryResp;
}
// Handle struktur response dengan labels & series
if (byCategory && byCategory.labels && byCategory.series) {
const categoryLabels = {
'person_walk': 'Orang',
'motor': 'Motor',
'car': 'Mobil'
};
const rawLabels = byCategory.labels || [];
catLabels = rawLabels.map(label => categoryLabels[label] || label);
catValues = byCategory.series.total_count || [];
console.log('[Dashboard] Category data from labels & series:', { catLabels, catValues });
}
// Handle struktur response dengan data array (sesuai spec)
else if (byCategory && Array.isArray(byCategory.data)) {
// Default categories sesuai spec
const defaultCategories = ['person_walk', 'motor', 'car'];
const categoryLabels = {
'person_walk': 'Orang',
'motor': 'Motor',
'car': 'Mobil'
};
catLabels = defaultCategories.map(cat => categoryLabels[cat] || cat);
catValues = defaultCategories.map(cat => {
const item = byCategory.data.find(d => d.category === cat);
return item ? (item.total_count || 0) : 0;
});
console.log('[Dashboard] Category data from array:', { catLabels, catValues });
}
// Jika response kosong/null, tetap buat chart dengan data 0
else {
console.warn('[Dashboard] Category response tidak sesuai format, menggunakan data kosong');
catLabels = ['Orang', 'Motor', 'Mobil'];
catValues = [0, 0, 0];
}
console.log('[Dashboard] Category data processed:', {
date: state.date,
labels: catLabels,
values: catValues,
total: catValues.reduce((a, b) => a + b, 0)
});
// Pastikan chart sudah di-init sebelum update
const categoryCanvas = document.getElementById('category-chart');
const categoryChartInstance = getCategoryChart();
if (categoryCanvas && !categoryChartInstance) {
console.log('[Dashboard] Initializing category chart...');
initCategoryChart(categoryCanvas.getContext('2d'));
}
// Selalu update chart, meskipun data kosong (agar user tahu memang tidak ada data)
const currentCategoryChart = getCategoryChart();
if (currentCategoryChart) {
updateCategoryChart({
labels: catLabels,
values: catValues
});
console.log('[Dashboard] Category chart updated successfully');
} else {
console.error('[Dashboard] Category chart tidak bisa di-update, chart belum di-init!');
}
} catch (err) {
console.error('loadSummaryAndCharts error', err);
showError(err.message || 'Gagal memuat data dashboard');
} finally {
if (loadingOverlay) loadingOverlay.classList.remove('visible');
}
}
// Load gates untuk mendapatkan camera URL dari database
async function loadGatesForCamera() {
try {
const response = await apiGetGates(null, { limit: 1000 });
let gates = [];
if (Array.isArray(response)) {
gates = response;
} else if (response && Array.isArray(response.data)) {
gates = response.data;
}
gatesCache = {};
gates.forEach(gate => {
const locationCode = gate.location_code || '';
const camera = gate.camera || null;
// Hanya simpan gate yang punya camera URL
if (camera && camera.trim() !== '') {
// Jika sudah ada, gunakan yang pertama (atau bisa dipilih gate tertentu)
if (!gatesCache[locationCode]) {
const locationName = locationsCache[locationCode]?.name || locationCode;
gatesCache[locationCode] = {
url: camera.trim(),
name: locationName,
gate_name: gate.name || gate.gate_code || '',
location_code: locationCode
};
}
}
});
console.log('[Dashboard] Gates dengan camera loaded:', Object.keys(gatesCache).length, gatesCache);
} catch (err) {
console.error('[Dashboard] Error loading gates for camera:', err);
}
}
// Load locations untuk mendapatkan nama lokasi
async function loadLocationsForCamera() {
try {
const response = await apiGetLocations({ limit: 1000 });
let locations = Array.isArray(response) ? response : (response && Array.isArray(response.data) ? response.data : []);
locationsCache = {};
locations.forEach(loc => {
const code = loc.code || loc.location_code || '';
locationsCache[code] = {
name: loc.name || loc.label || code
};
});
} catch (err) {
console.error('[Dashboard] Error loading locations for camera:', err);
}
}
function getCameraForLocation(locationCode) {
if (!locationCode) return null;
return gatesCache[locationCode] || null;
}
function showVideoPanel(locationCode) {
const videoSection = document.getElementById('video-section');
const videoPanelTitle = document.getElementById('video-panel-title');
const camera = getCameraForLocation(locationCode);
console.log('[Dashboard] showVideoPanel:', {
locationCode,
camera,
gatesCacheKeys: Object.keys(gatesCache),
locationsCacheKeys: Object.keys(locationsCache)
});
if (camera && camera.url && videoSection && videoPanelTitle) {
videoSection.style.display = 'block';
const displayName = camera.gate_name ? `${camera.name} - ${camera.gate_name}` : camera.name;
videoPanelTitle.textContent = displayName;
currentVideoUrl = camera.url;
console.log('[Dashboard] Video panel shown:', { displayName, url: currentVideoUrl });
// Auto-stop video kalau lokasi berubah
if (isVideoPlaying) {
stopVideo();
}
} else {
if (videoSection) videoSection.style.display = 'none';
if (isVideoPlaying) {
stopVideo();
}
currentVideoUrl = null;
console.log('[Dashboard] Video panel hidden - no camera for location:', locationCode);
}
}
function initVideo() {
if (!currentVideoUrl || typeof Hls === 'undefined') {
console.warn('[Dashboard] Video URL atau HLS.js tidak tersedia');
return;
}
const videoEl = document.getElementById('video-player');
if (!videoEl) return;
if (Hls.isSupported()) {
hls = new Hls({
enableWorker: true,
lowLatencyMode: true,
backBufferLength: 90
});
hls.loadSource(currentVideoUrl);
hls.attachMedia(videoEl);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
console.log('[Dashboard] HLS manifest parsed');
});
hls.on(Hls.Events.ERROR, (event, data) => {
console.error('[Dashboard] HLS error:', data);
if (data.fatal) {
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
console.log('[Dashboard] Network error, retrying...');
hls.startLoad();
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
console.log('[Dashboard] Media error, recovering...');
hls.recoverMediaError();
} else {
console.error('[Dashboard] Fatal error, destroying HLS');
hls.destroy();
hls = null;
stopVideo();
}
}
});
} else if (videoEl.canPlayType('application/vnd.apple.mpegurl')) {
// Native HLS support (Safari)
videoEl.src = currentVideoUrl;
} else {
console.error('[Dashboard] HLS tidak didukung di browser ini');
}
}
function startVideo() {
const videoEl = document.getElementById('video-player');
const placeholderEl = document.getElementById('video-placeholder');
const toggleBtn = document.getElementById('video-toggle');
if (!videoEl || !currentVideoUrl) {
console.warn('[Dashboard] Video element atau URL tidak tersedia');
return;
}
if (!hls && typeof Hls !== 'undefined' && Hls.isSupported()) {
initVideo();
}
if (videoEl) {
videoEl.style.display = 'block';
if (placeholderEl) placeholderEl.style.display = 'none';
}
if (toggleBtn) toggleBtn.textContent = 'Matikan';
isVideoPlaying = true;
if (videoEl && (videoEl.src || (hls && hls.media))) {
videoEl.play().catch(e => console.error('[Dashboard] Play error:', e));
}
}
function stopVideo() {
const videoEl = document.getElementById('video-player');
const placeholderEl = document.getElementById('video-placeholder');
const toggleBtn = document.getElementById('video-toggle');
if (hls) {
hls.destroy();
hls = null;
}
if (videoEl) {
videoEl.pause();
videoEl.src = '';
videoEl.style.display = 'none';
}
if (placeholderEl) placeholderEl.style.display = 'flex';
if (toggleBtn) toggleBtn.textContent = 'Hidupkan';
isVideoPlaying = false;
}
function setupFilters() {
const dateInput = document.getElementById('filter-date');
if (dateInput) {
dateInput.value = state.date;
dateInput.addEventListener('change', () => {
state.date = dateInput.value || state.date;
loadSummaryAndCharts();
});
}
const locationSelect = document.getElementById('filter-location');
if (locationSelect) {
locationSelect.addEventListener('change', async () => {
state.locationCode = locationSelect.value;
state.gateCode = '';
const gateSelect = document.getElementById('filter-gate');
if (gateSelect) gateSelect.value = '';
// Reload gates untuk update camera cache
await loadGates();
await loadGatesForCamera(); // Reload camera URLs
// Show/hide video panel berdasarkan lokasi
showVideoPanel(state.locationCode);
loadSummaryAndCharts();
});
}
const gateSelect = document.getElementById('filter-gate');
if (gateSelect) {
gateSelect.addEventListener('change', () => {
state.gateCode = gateSelect.value;
loadSummaryAndCharts();
});
}
// Video toggle button akan di-setup di DOMContentLoaded setelah load camera data
}
function initCharts() {
console.log('[Dashboard] Initializing charts...');
const dailyCanvas = document.getElementById('daily-chart');
const categoryCanvas = document.getElementById('category-chart');
if (dailyCanvas) {
console.log('[Dashboard] Found daily chart canvas, initializing...');
initDailyChart(dailyCanvas.getContext('2d'));
console.log('[Dashboard] Daily chart initialized:', getDailyChart() !== null);
} else {
console.error('[Dashboard] Daily chart canvas not found!');
}
if (categoryCanvas) {
console.log('[Dashboard] Found category chart canvas, initializing...');
initCategoryChart(categoryCanvas.getContext('2d'));
console.log('[Dashboard] Category chart initialized:', getCategoryChart() !== null);
} else {
console.error('[Dashboard] Category chart canvas not found!');
}
}
document.addEventListener('DOMContentLoaded', async () => {
// Require auth
if (!Auth.isAuthenticated()) {
// Cek apakah sudah di login page untuk mencegah redirect loop
const currentPath = window.location.pathname.toLowerCase();
const isLoginPage = currentPath.includes('index.html') ||
currentPath.includes('index.php') ||
currentPath === '/' ||
currentPath === '/index.html' ||
currentPath === '/index.php' ||
currentPath.endsWith('/') ||
currentPath === '';
// JANGAN redirect jika sudah di login page atau root
if (!isLoginPage && currentPath.includes('dashboard')) {
// Hanya redirect jika benar-benar di halaman dashboard
window.location.href = '../index.html';
}
return;
}
// Set default date ke hari ini (jangan auto-detect ke tanggal lama)
const today = new Date().toISOString().split('T')[0];
state.date = today;
const dateInput = document.getElementById('filter-date');
if (dateInput) {
dateInput.value = state.date;
console.log('[Dashboard] Default date set to today:', state.date);
}
setTopbarDate();
initCharts();
setupFilters();
await loadLocations();
await loadGates();
// Load locations dan gates untuk camera URL dari database
await loadLocationsForCamera();
await loadGatesForCamera();
// Setup video toggle button SETELAH load camera data
const toggleBtn = document.getElementById('video-toggle');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => {
if (isVideoPlaying) {
stopVideo();
} else {
startVideo();
}
});
}
// Cek lokasi awal untuk show/hide video
showVideoPanel(state.locationCode);
await loadSummaryAndCharts();
});