// 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'; // Helper function untuk mendapatkan tanggal hari ini dalam timezone Indonesia (UTC+7) // Menggunakan Intl.DateTimeFormat untuk mendapatkan tanggal yang konsisten di semua browser function getTodayIndonesia() { 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' }); // Format: YYYY-MM-DD return formatter.format(now); } // State akan di-set ke hari ini saat DOMContentLoaded const state = { date: '', // Akan di-set ke hari ini saat DOMContentLoaded locationCode: '', gateCode: '' }; // Function untuk auto-detect tanggal terakhir yang ada data async function getLastAvailableDate() { try { // Coba ambil data hari ini dulu (gunakan timezone Indonesia UTC+7) const today = getTodayIndonesia(); 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 (gunakan timezone Indonesia UTC+7) const now = new Date(); const yesterdayDate = new Date(now.getTime() - (24 * 60 * 60 * 1000)); const formatter = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Jakarta', year: 'numeric', month: '2-digit', day: '2-digit' }); const yesterdayStr = formatter.format(yesterdayDate); 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 (gunakan timezone Indonesia UTC+7) const now = new Date(); const formatter = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Jakarta', year: 'numeric', month: '2-digit', day: '2-digit' }); for (let i = 2; i <= 7; i++) { const prevDate = new Date(now.getTime() - (i * 24 * 60 * 60 * 1000)); const prevDateStr = formatter.format(prevDate); 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, tetap return hari ini // User bisa pilih tanggal manual jika perlu console.log('[Dashboard] No data found in last 7 days, using today:', today); return today; } catch (error) { console.error('[Dashboard] Error getting last available date:', error); // Fallback ke hari ini (selalu gunakan hari ini, bukan hardcoded date) // Gunakan timezone Indonesia UTC+7 return getTodayIndonesia(); } } // 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 = ''; 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 = ''; 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 = []; if (Array.isArray(response)) { locations = response; } else if (response && Array.isArray(response.data)) { locations = response.data; } locationsCache = {}; locations.forEach(loc => { const code = loc.code || loc.location_code || ''; locationsCache[code] = { name: loc.name || loc.label || code }; }); console.log('[Dashboard] Locations loaded for camera:', Object.keys(locationsCache).length, locationsCache); } catch (err) { console.error('[Dashboard] Error loading locations for camera:', err); } } function getCameraForLocation(locationCode) { if (!locationCode) { console.log('[Dashboard] getCameraForLocation: no locationCode'); return null; } const camera = gatesCache[locationCode] || null; console.log('[Dashboard] getCameraForLocation:', { locationCode, found: !!camera, gatesCacheKeys: Object.keys(gatesCache) }); return camera; } 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) { // Jangan override value yang sudah di-set di DOMContentLoaded // Hanya set jika value masih kosong atau berbeda dengan state.date if (!dateInput.value || dateInput.value !== state.date) { dateInput.value = state.date; dateInput.setAttribute('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 (selalu update ke hari ini setiap kali page load) // Gunakan timezone Indonesia (UTC+7) untuk mendapatkan tanggal lokal yang benar const today = getTodayIndonesia(); state.date = today; const now = new Date(); console.log('[Dashboard] Default date set to today (Indonesia timezone):', state.date, 'UTC time:', now.toISOString()); // Set dateInput value SECARA LANGSUNG untuk override browser cache/autofill // Lakukan ini SEBELUM setupFilters() untuk memastikan value ter-set dengan benar const dateInput = document.getElementById('filter-date'); if (dateInput) { // AGGRESIF: Remove value dulu, lalu set lagi untuk force override browser cache dateInput.removeAttribute('value'); dateInput.value = ''; // Set value multiple times dengan berbagai cara untuk memastikan dateInput.value = today; dateInput.setAttribute('value', today); dateInput.defaultValue = today; // Force update dengan requestAnimationFrame untuk memastikan DOM sudah ready requestAnimationFrame(() => { dateInput.value = today; dateInput.setAttribute('value', today); console.log('[Dashboard] Date input force set (RAF):', today, 'actual value:', dateInput.value); }); // Set lagi setelah semua async operations selesai setTimeout(() => { if (dateInput.value !== today) { dateInput.value = today; dateInput.setAttribute('value', today); console.log('[Dashboard] Date input force set (timeout):', today, 'actual value:', dateInput.value); } }, 100); console.log('[Dashboard] Date input set to:', today, 'actual value:', dateInput.value); } setTopbarDate(); initCharts(); // Setup filters SETELAH state.date dan dateInput.value sudah di-set 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(); // FINAL CHECK: Pastikan dateInput masih hari ini setelah semua async operations const finalDateInput = document.getElementById('filter-date'); if (finalDateInput && finalDateInput.value !== state.date) { console.warn('[Dashboard] Date input value changed after async operations, resetting to:', state.date); finalDateInput.value = state.date; finalDateInput.setAttribute('value', state.date); } });