Fix: Data inconsistency pada transisi tahun/bulan dan setup API lokal untuk testing
- Fix deteksi transisi bulan untuk semua bulan (tidak hanya 1 Januari) - Perbaikan validasi data dengan threshold 20% untuk transisi bulan/tahun - Auto-reset chart jika data hourly tidak valid - Setup force local mode untuk testing API lokal - Perbaikan normalisasi dan validasi tanggal - Enhanced logging untuk debugging transisi
This commit is contained in:
@@ -6,16 +6,17 @@
|
|||||||
|
|
||||||
## Cara Kerja
|
## Cara Kerja
|
||||||
|
|
||||||
File `config.js` akan auto-detect environment berdasarkan hostname:
|
File `config.js` akan **auto-detect** environment berdasarkan hostname:
|
||||||
|
|
||||||
### Development Lokal
|
### Development Lokal (Auto-detect)
|
||||||
- Jika hostname = `localhost`, `127.0.0.1`, atau IP lokal (`192.168.x.x`)
|
- Jika hostname = `localhost`, `127.0.0.1`, atau IP lokal (`192.168.x.x`, `10.x.x.x`, `172.x.x.x`)
|
||||||
- Base URL default: `http://localhost/api-btekno/public`
|
- Base URL: `http://localhost/api-btekno/public`
|
||||||
- **Sesuaikan** dengan path API backend di Laragon/XAMPP Anda
|
- **Otomatis** menggunakan API lokal saat development
|
||||||
|
|
||||||
### Production
|
### Production (Auto-detect)
|
||||||
- Jika hostname bukan localhost
|
- Jika hostname bukan localhost/IP lokal
|
||||||
- Base URL: `https://api.btekno.cloud`
|
- Base URL: `https://api.btekno.cloud`
|
||||||
|
- **Otomatis** menggunakan API produksi saat di production
|
||||||
|
|
||||||
## Cara Mengubah Base URL
|
## Cara Mengubah Base URL
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,19 @@ async function apiRequest(path, options = {}) {
|
|||||||
json = { raw: text };
|
json = { raw: text };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log response body untuk debug (khusus untuk summary endpoint)
|
||||||
|
if (url.includes('/dashboard/summary') || url.includes('/summary/hourly') || url.includes('/by-category')) {
|
||||||
|
console.log('[API] Response body for', url, ':', {
|
||||||
|
textLength: text.length,
|
||||||
|
jsonKeys: Object.keys(json || {}),
|
||||||
|
jsonPreview: JSON.stringify(json).substring(0, 500),
|
||||||
|
hasSuccess: json && 'success' in json,
|
||||||
|
hasData: json && 'data' in json,
|
||||||
|
totalCount: json?.total_count ?? json?.data?.total_count,
|
||||||
|
totalAmount: json?.total_amount ?? json?.data?.total_amount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const msg = json.message || json.error || `HTTP ${res.status}`;
|
const msg = json.message || json.error || `HTTP ${res.status}`;
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
@@ -103,7 +116,48 @@ function buildQuery(params = {}) {
|
|||||||
const search = new URLSearchParams();
|
const search = new URLSearchParams();
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
if (value !== undefined && value !== null && value !== '') {
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
search.append(key, value);
|
// Pastikan value adalah string dan sudah di-encode dengan benar
|
||||||
|
let stringValue = String(value).trim();
|
||||||
|
|
||||||
|
// Validasi khusus untuk parameter date: harus format YYYY-MM-DD
|
||||||
|
if (key === 'date' || key === 'start_date' || key === 'end_date') {
|
||||||
|
// Validasi format tanggal
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(stringValue)) {
|
||||||
|
console.error('[API] buildQuery - Invalid date format:', stringValue);
|
||||||
|
// Coba normalisasi jika mungkin
|
||||||
|
const dateObj = new Date(stringValue);
|
||||||
|
if (!isNaN(dateObj.getTime())) {
|
||||||
|
const year = dateObj.getFullYear();
|
||||||
|
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(dateObj.getDate()).padStart(2, '0');
|
||||||
|
stringValue = `${year}-${month}-${day}`;
|
||||||
|
console.warn('[API] buildQuery - Date normalized to:', stringValue);
|
||||||
|
} else {
|
||||||
|
console.error('[API] buildQuery - Cannot normalize date, skipping:', stringValue);
|
||||||
|
return; // Skip parameter ini jika tidak valid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validasi bahwa tanggal valid (tidak ada 2025-13-45)
|
||||||
|
const [year, month, day] = stringValue.split('-').map(Number);
|
||||||
|
const dateObj = new Date(year, month - 1, day);
|
||||||
|
if (dateObj.getFullYear() !== year || dateObj.getMonth() !== month - 1 || dateObj.getDate() !== day) {
|
||||||
|
console.error('[API] buildQuery - Invalid date values:', { year, month, day });
|
||||||
|
return; // Skip parameter ini jika tidak valid
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[API] buildQuery - Date param validated:', {
|
||||||
|
key,
|
||||||
|
originalValue: value,
|
||||||
|
stringValue: stringValue,
|
||||||
|
encoded: stringValue, // URLSearchParams akan encode otomatis
|
||||||
|
year: year,
|
||||||
|
month: month,
|
||||||
|
day: day
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
search.append(key, stringValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const qs = search.toString();
|
const qs = search.toString();
|
||||||
@@ -139,7 +193,9 @@ export async function apiGetSummary({ date, locationCode, gateCode }) {
|
|||||||
location_code: locationCode,
|
location_code: locationCode,
|
||||||
gate_code: gateCode
|
gate_code: gateCode
|
||||||
});
|
});
|
||||||
return apiRequest(`/retribusi/v1/dashboard/summary${qs}`);
|
const url = `/retribusi/v1/dashboard/summary${qs}`;
|
||||||
|
console.log('[API] apiGetSummary - URL:', url, 'Params:', { date, locationCode, gateCode });
|
||||||
|
return apiRequest(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function apiGetDaily({ startDate, endDate, locationCode }) {
|
export async function apiGetDaily({ startDate, endDate, locationCode }) {
|
||||||
@@ -157,7 +213,9 @@ export async function apiGetByCategory({ date, locationCode, gateCode }) {
|
|||||||
location_code: locationCode,
|
location_code: locationCode,
|
||||||
gate_code: gateCode
|
gate_code: gateCode
|
||||||
});
|
});
|
||||||
return apiRequest(`/retribusi/v1/dashboard/by-category${qs}`);
|
const url = `/retribusi/v1/dashboard/by-category${qs}`;
|
||||||
|
console.log('[API] apiGetByCategory - URL:', url, 'Params:', { date, locationCode, gateCode });
|
||||||
|
return apiRequest(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ringkasan global harian (daily_summary)
|
// Ringkasan global harian (daily_summary)
|
||||||
@@ -169,7 +227,9 @@ export async function apiGetSummaryDaily(params = {}) {
|
|||||||
// Ringkasan per jam (hourly_summary)
|
// Ringkasan per jam (hourly_summary)
|
||||||
export async function apiGetSummaryHourly(params = {}) {
|
export async function apiGetSummaryHourly(params = {}) {
|
||||||
const qs = buildQuery(params);
|
const qs = buildQuery(params);
|
||||||
return apiRequest(`/retribusi/v1/summary/hourly${qs}`);
|
const url = `/retribusi/v1/summary/hourly${qs}`;
|
||||||
|
console.log('[API] apiGetSummaryHourly - URL:', url, 'Params:', params);
|
||||||
|
return apiRequest(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snapshot realtime (untuk panel live / TV wall)
|
// Snapshot realtime (untuk panel live / TV wall)
|
||||||
@@ -202,3 +262,55 @@ export async function apiGetEvents(params = {}) {
|
|||||||
// Catatan: realtime SSE /retribusi/v1/realtime/stream akan diakses langsung via EventSource,
|
// Catatan: realtime SSE /retribusi/v1/realtime/stream akan diakses langsung via EventSource,
|
||||||
// bukan lewat fetch/apiRequest karena menggunakan Server-Sent Events (SSE).
|
// bukan lewat fetch/apiRequest karena menggunakan Server-Sent Events (SSE).
|
||||||
|
|
||||||
|
// Test function untuk debug - bandingkan URL yang dihasilkan untuk tanggal berbeda
|
||||||
|
// Bisa dipanggil dari browser console: window.testDateUrls('2025-12-31', '2026-01-01')
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.testDateUrls = function(date1, date2) {
|
||||||
|
console.log('=== TEST: Membandingkan URL untuk tanggal berbeda ===');
|
||||||
|
|
||||||
|
const testParams = {
|
||||||
|
date: date1 || '2025-12-31',
|
||||||
|
locationCode: '',
|
||||||
|
gateCode: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const testParams2 = {
|
||||||
|
date: date2 || '2026-01-01',
|
||||||
|
locationCode: '',
|
||||||
|
gateCode: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('\n--- Tanggal 1:', testParams.date, '---');
|
||||||
|
const qs1 = buildQuery(testParams);
|
||||||
|
console.log('Query string:', qs1);
|
||||||
|
console.log('Full URL:', `${API_CONFIG.BASE_URL}/retribusi/v1/dashboard/summary${qs1}`);
|
||||||
|
|
||||||
|
console.log('\n--- Tanggal 2:', testParams2.date, '---');
|
||||||
|
const qs2 = buildQuery(testParams2);
|
||||||
|
console.log('Query string:', qs2);
|
||||||
|
console.log('Full URL:', `${API_CONFIG.BASE_URL}/retribusi/v1/dashboard/summary${qs2}`);
|
||||||
|
|
||||||
|
console.log('\n--- Perbandingan ---');
|
||||||
|
console.log('Query string sama?', qs1 === qs2);
|
||||||
|
console.log('Date param sama?', testParams.date === testParams2.date);
|
||||||
|
console.log('Date length sama?', testParams.date.length === testParams2.date.length);
|
||||||
|
|
||||||
|
// Test URLSearchParams parsing
|
||||||
|
const params1 = new URLSearchParams(qs1.substring(1));
|
||||||
|
const params2 = new URLSearchParams(qs2.substring(1));
|
||||||
|
console.log('Parsed date 1:', params1.get('date'));
|
||||||
|
console.log('Parsed date 2:', params2.get('date'));
|
||||||
|
console.log('Parsed dates sama?', params1.get('date') === params2.get('date'));
|
||||||
|
|
||||||
|
return {
|
||||||
|
date1: testParams.date,
|
||||||
|
date2: testParams2.date,
|
||||||
|
url1: `${API_CONFIG.BASE_URL}/retribusi/v1/dashboard/summary${qs1}`,
|
||||||
|
url2: `${API_CONFIG.BASE_URL}/retribusi/v1/dashboard/summary${qs2}`,
|
||||||
|
qs1,
|
||||||
|
qs2,
|
||||||
|
areEqual: qs1 === qs2
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,11 +107,41 @@ export function updateDailyChart({ labels, counts, amounts }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dailyLineChart.data.labels = labels || [];
|
// Pastikan data arrays memiliki length yang sama
|
||||||
dailyLineChart.data.datasets[0].data = counts || [];
|
const safeLabels = labels || [];
|
||||||
dailyLineChart.data.datasets[1].data = amounts || [];
|
const safeCounts = counts || [];
|
||||||
dailyLineChart.update();
|
const safeAmounts = amounts || [];
|
||||||
console.log('[Charts] Daily chart updated:', { labelsCount: labels?.length, countsCount: counts?.length, amountsCount: amounts?.length });
|
|
||||||
|
// Pastikan semua array memiliki length yang sama (24 jam)
|
||||||
|
const maxLength = Math.max(safeLabels.length, safeCounts.length, safeAmounts.length, 24);
|
||||||
|
const finalLabels = Array.from({ length: maxLength }, (_, i) => {
|
||||||
|
if (i < safeLabels.length) return safeLabels[i];
|
||||||
|
return `${String(i).padStart(2, '0')}:00`;
|
||||||
|
});
|
||||||
|
const finalCounts = Array.from({ length: maxLength }, (_, i) => {
|
||||||
|
if (i < safeCounts.length) return Number(safeCounts[i]) || 0;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
const finalAmounts = Array.from({ length: maxLength }, (_, i) => {
|
||||||
|
if (i < safeAmounts.length) return Number(safeAmounts[i]) || 0;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update chart data
|
||||||
|
dailyLineChart.data.labels = finalLabels;
|
||||||
|
dailyLineChart.data.datasets[0].data = finalCounts;
|
||||||
|
dailyLineChart.data.datasets[1].data = finalAmounts;
|
||||||
|
|
||||||
|
// Update chart dengan mode 'none' untuk menghindari animasi yang tidak perlu
|
||||||
|
dailyLineChart.update('none');
|
||||||
|
|
||||||
|
console.log('[Charts] Daily chart updated:', {
|
||||||
|
labelsCount: finalLabels.length,
|
||||||
|
countsCount: finalCounts.length,
|
||||||
|
amountsCount: finalAmounts.length,
|
||||||
|
totalCount: finalCounts.reduce((a, b) => a + b, 0),
|
||||||
|
totalAmount: finalAmounts.reduce((a, b) => a + b, 0)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initCategoryChart(ctx) {
|
export function initCategoryChart(ctx) {
|
||||||
@@ -169,18 +199,29 @@ export function updateCategoryChart({ labels, values }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pastikan labels dan values tidak kosong
|
// Pastikan labels dan values tidak kosong
|
||||||
const finalLabels = labels && labels.length > 0 ? labels : ['Orang', 'Motor', 'Mobil'];
|
// Default categories: Orang, Motor, Mobil
|
||||||
const finalValues = values && values.length > 0 ? values : [0, 0, 0];
|
const defaultLabels = ['Orang', 'Motor', 'Mobil'];
|
||||||
|
const defaultValues = [0, 0, 0];
|
||||||
|
|
||||||
// Pastikan length sama
|
const finalLabels = labels && labels.length > 0 ? labels : defaultLabels;
|
||||||
const minLength = Math.min(finalLabels.length, finalValues.length);
|
const finalValues = values && values.length > 0 ? values : defaultValues;
|
||||||
const safeLabels = finalLabels.slice(0, minLength);
|
|
||||||
const safeValues = finalValues.slice(0, minLength);
|
|
||||||
|
|
||||||
|
// Pastikan length sama (minimal 3 untuk 3 kategori)
|
||||||
|
const maxLength = Math.max(finalLabels.length, finalValues.length, 3);
|
||||||
|
const safeLabels = Array.from({ length: maxLength }, (_, i) => {
|
||||||
|
if (i < finalLabels.length) return finalLabels[i];
|
||||||
|
return defaultLabels[i] || `Kategori ${i + 1}`;
|
||||||
|
});
|
||||||
|
const safeValues = Array.from({ length: maxLength }, (_, i) => {
|
||||||
|
if (i < finalValues.length) return Number(finalValues[i]) || 0;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update chart data
|
||||||
categoryChart.data.labels = safeLabels;
|
categoryChart.data.labels = safeLabels;
|
||||||
categoryChart.data.datasets[0].data = safeValues;
|
categoryChart.data.datasets[0].data = safeValues;
|
||||||
|
|
||||||
// Update chart dengan mode 'none' untuk animasi halus
|
// Update chart dengan mode 'none' untuk menghindari animasi yang tidak perlu
|
||||||
categoryChart.update('none');
|
categoryChart.update('none');
|
||||||
|
|
||||||
console.log('[Charts] Category chart updated:', {
|
console.log('[Charts] Category chart updated:', {
|
||||||
|
|||||||
@@ -1,26 +1,47 @@
|
|||||||
// public/dashboard/js/config.js
|
// public/dashboard/js/config.js
|
||||||
// Konfigurasi API Base URL untuk frontend
|
// Konfigurasi API Base URL untuk frontend
|
||||||
|
|
||||||
// Auto-detect environment berdasarkan hostname
|
// FORCE LOCAL MODE - Set ke true untuk force menggunakan API lokal
|
||||||
|
const FORCE_LOCAL_MODE = true; // Set ke false untuk auto-detect
|
||||||
|
|
||||||
|
// Auto-detect API Base URL berdasarkan hostname
|
||||||
function getApiBaseUrl() {
|
function getApiBaseUrl() {
|
||||||
const hostname = window.location.hostname;
|
// Force local mode untuk testing
|
||||||
|
if (FORCE_LOCAL_MODE) {
|
||||||
// Development lokal (Laragon/XAMPP)
|
console.log('[Config] FORCE_LOCAL_MODE enabled, using local API');
|
||||||
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('192.168.')) {
|
return 'http://localhost/api-btekno/public';
|
||||||
// Untuk PHP built-in server (port 8000)
|
|
||||||
// return 'http://localhost:8000';
|
|
||||||
|
|
||||||
// Untuk Laragon/Apache (path-based)
|
|
||||||
// return 'http://localhost/api-btekno/public';
|
|
||||||
|
|
||||||
// Untuk Laragon virtual host
|
|
||||||
// return 'http://api.retribusi.test';
|
|
||||||
|
|
||||||
// Default: PHP built-in server
|
|
||||||
return 'http://localhost:8000';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Production
|
// Jika di localhost atau 127.0.0.1, gunakan API lokal
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const hostname = window.location.hostname;
|
||||||
|
const port = window.location.port;
|
||||||
|
const protocol = window.location.protocol;
|
||||||
|
|
||||||
|
console.log('[Config] Detecting environment:', { hostname, port, protocol, fullUrl: window.location.href });
|
||||||
|
|
||||||
|
const isLocal = hostname === 'localhost' ||
|
||||||
|
hostname === '127.0.0.1' ||
|
||||||
|
hostname === '' ||
|
||||||
|
hostname.startsWith('192.168.') ||
|
||||||
|
hostname.startsWith('10.') ||
|
||||||
|
hostname.startsWith('172.') ||
|
||||||
|
protocol === 'file:'; // File protocol (file://) juga dianggap local
|
||||||
|
|
||||||
|
console.log('[Config] Is Local:', isLocal);
|
||||||
|
|
||||||
|
if (isLocal) {
|
||||||
|
// API lokal - sesuaikan dengan konfigurasi server lokal Anda
|
||||||
|
// Untuk Laragon, biasanya: http://localhost/api-btekno/public
|
||||||
|
// Atau jika menggunakan PHP built-in server: http://localhost:8000
|
||||||
|
const localApiUrl = 'http://localhost/api-btekno/public';
|
||||||
|
console.log('[Config] Using local API:', localApiUrl);
|
||||||
|
return localApiUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: API produksi
|
||||||
|
console.log('[Config] Using production API');
|
||||||
return 'https://api.btekno.cloud';
|
return 'https://api.btekno.cloud';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,5 +55,12 @@ export const API_CONFIG = {
|
|||||||
// Untuk debugging
|
// Untuk debugging
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
console.log('API Base URL:', API_CONFIG.BASE_URL);
|
console.log('API Base URL:', API_CONFIG.BASE_URL);
|
||||||
|
console.log('Hostname:', window.location.hostname);
|
||||||
|
console.log('Is Local:', window.location.hostname === 'localhost' ||
|
||||||
|
window.location.hostname === '127.0.0.1' ||
|
||||||
|
window.location.hostname === '' ||
|
||||||
|
window.location.hostname.startsWith('192.168.') ||
|
||||||
|
window.location.hostname.startsWith('10.') ||
|
||||||
|
window.location.hostname.startsWith('172.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,93 @@ function getTodayIndonesia() {
|
|||||||
return formatter.format(now);
|
return formatter.format(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function untuk normalisasi format tanggal ke YYYY-MM-DD
|
||||||
|
// Memastikan format konsisten terlepas dari input browser
|
||||||
|
function normalizeDate(dateString) {
|
||||||
|
if (!dateString) return '';
|
||||||
|
|
||||||
|
// Log input untuk debug
|
||||||
|
console.log('[Dashboard] normalizeDate - Input:', {
|
||||||
|
value: dateString,
|
||||||
|
type: typeof dateString,
|
||||||
|
length: dateString.length,
|
||||||
|
charCodes: dateString.split('').map(c => c.charCodeAt(0))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Jika sudah format YYYY-MM-DD, validasi dan return
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
|
||||||
|
// Validasi bahwa tanggal valid (misal tidak ada 2025-13-45)
|
||||||
|
const [year, month, day] = dateString.split('-').map(Number);
|
||||||
|
const date = new Date(year, month - 1, day);
|
||||||
|
if (date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day) {
|
||||||
|
console.log('[Dashboard] normalizeDate - Valid YYYY-MM-DD format:', dateString);
|
||||||
|
return dateString;
|
||||||
|
} else {
|
||||||
|
console.warn('[Dashboard] normalizeDate - Invalid date values:', { year, month, day });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika format lain, coba parse dengan berbagai cara
|
||||||
|
let date;
|
||||||
|
|
||||||
|
// Coba parse sebagai ISO string dulu
|
||||||
|
date = new Date(dateString);
|
||||||
|
if (!isNaN(date.getTime())) {
|
||||||
|
// Format ke YYYY-MM-DD menggunakan timezone Indonesia
|
||||||
|
const formatter = new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: 'Asia/Jakarta',
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
});
|
||||||
|
const normalized = formatter.format(date);
|
||||||
|
console.log('[Dashboard] normalizeDate - Parsed and normalized:', dateString, '->', normalized);
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika masih gagal, coba parse manual (untuk format DD/MM/YYYY atau MM/DD/YYYY)
|
||||||
|
const parts = dateString.split(/[-\/]/);
|
||||||
|
if (parts.length === 3) {
|
||||||
|
let year, month, day;
|
||||||
|
// Coba deteksi format: jika bagian pertama > 12, kemungkinan DD/MM/YYYY
|
||||||
|
if (parseInt(parts[0]) > 12) {
|
||||||
|
// DD/MM/YYYY
|
||||||
|
day = parseInt(parts[0]);
|
||||||
|
month = parseInt(parts[1]);
|
||||||
|
year = parseInt(parts[2]);
|
||||||
|
} else {
|
||||||
|
// MM/DD/YYYY atau YYYY-MM-DD
|
||||||
|
if (parts[0].length === 4) {
|
||||||
|
// YYYY-MM-DD
|
||||||
|
year = parseInt(parts[0]);
|
||||||
|
month = parseInt(parts[1]);
|
||||||
|
day = parseInt(parts[2]);
|
||||||
|
} else {
|
||||||
|
// MM/DD/YYYY
|
||||||
|
month = parseInt(parts[0]);
|
||||||
|
day = parseInt(parts[1]);
|
||||||
|
year = parseInt(parts[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
date = new Date(year, month - 1, day);
|
||||||
|
if (!isNaN(date.getTime())) {
|
||||||
|
const formatter = new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: 'Asia/Jakarta',
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
});
|
||||||
|
const normalized = formatter.format(date);
|
||||||
|
console.log('[Dashboard] normalizeDate - Manual parse:', dateString, '->', normalized);
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn('[Dashboard] normalizeDate - Failed to parse:', dateString);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
// State akan di-set ke hari ini saat DOMContentLoaded
|
// State akan di-set ke hari ini saat DOMContentLoaded
|
||||||
const state = {
|
const state = {
|
||||||
date: '', // Akan di-set ke hari ini saat DOMContentLoaded
|
date: '', // Akan di-set ke hari ini saat DOMContentLoaded
|
||||||
@@ -233,6 +320,71 @@ async function loadSummaryAndCharts() {
|
|||||||
const loadingOverlay = document.getElementById('summary-loading');
|
const loadingOverlay = document.getElementById('summary-loading');
|
||||||
if (loadingOverlay) loadingOverlay.classList.add('visible');
|
if (loadingOverlay) loadingOverlay.classList.add('visible');
|
||||||
|
|
||||||
|
// Validasi dan normalisasi tanggal sebelum request
|
||||||
|
if (!state.date || !/^\d{4}-\d{2}-\d{2}$/.test(state.date)) {
|
||||||
|
console.error('[Dashboard] Invalid date format:', state.date);
|
||||||
|
const today = getTodayIndonesia();
|
||||||
|
state.date = today;
|
||||||
|
const dateInput = document.getElementById('filter-date');
|
||||||
|
if (dateInput) {
|
||||||
|
dateInput.value = today;
|
||||||
|
dateInput.setAttribute('value', today);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pastikan format tanggal konsisten (YYYY-MM-DD)
|
||||||
|
const normalizedDate = normalizeDate(state.date);
|
||||||
|
if (normalizedDate && normalizedDate !== state.date) {
|
||||||
|
console.warn('[Dashboard] Date normalized:', state.date, '->', normalizedDate);
|
||||||
|
state.date = normalizedDate;
|
||||||
|
const dateInput = document.getElementById('filter-date');
|
||||||
|
if (dateInput) {
|
||||||
|
dateInput.value = normalizedDate;
|
||||||
|
dateInput.setAttribute('value', normalizedDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse tanggal untuk deteksi transisi tahun/bulan
|
||||||
|
const [year, month, day] = state.date.split('-').map(Number);
|
||||||
|
// Deteksi transisi: hari pertama bulan (day === 1)
|
||||||
|
// Ini berlaku untuk semua bulan, termasuk transisi tahun (1 Januari)
|
||||||
|
const isMonthTransition = day === 1;
|
||||||
|
// Deteksi khusus untuk transisi tahun (1 Januari)
|
||||||
|
const isYearTransition = month === 1 && day === 1;
|
||||||
|
|
||||||
|
if (isMonthTransition) {
|
||||||
|
const transitionType = isYearTransition ? 'Tahun' : 'Bulan';
|
||||||
|
console.log('[Dashboard] ⚠️ Transisi terdeteksi:', {
|
||||||
|
date: state.date,
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day,
|
||||||
|
isYearTransition,
|
||||||
|
isMonthTransition,
|
||||||
|
transitionType,
|
||||||
|
note: `Transisi ${transitionType} - Data hourly mungkin belum ter-aggregate dengan benar di backend`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset data sementara sebelum load (untuk menghindari data lama terlihat)
|
||||||
|
renderSummary({
|
||||||
|
totalAmount: 0,
|
||||||
|
personCount: 0,
|
||||||
|
motorCount: 0,
|
||||||
|
carCount: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log detail tanggal yang akan dikirim ke API
|
||||||
|
console.log('[Dashboard] loadSummaryAndCharts - Request params:', {
|
||||||
|
date: state.date,
|
||||||
|
dateType: typeof state.date,
|
||||||
|
dateLength: state.date ? state.date.length : 0,
|
||||||
|
dateValid: /^\d{4}-\d{2}-\d{2}$/.test(state.date),
|
||||||
|
locationCode: state.locationCode,
|
||||||
|
gateCode: state.gateCode,
|
||||||
|
dateInputValue: document.getElementById('filter-date')?.value
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [summaryResp, hourlyResp, byCategoryResp] = await Promise.all([
|
const [summaryResp, hourlyResp, byCategoryResp] = await Promise.all([
|
||||||
apiGetSummary({
|
apiGetSummary({
|
||||||
@@ -256,7 +408,15 @@ async function loadSummaryAndCharts() {
|
|||||||
// Kartu KPI: pakai total_count & total_amount dari summary endpoint
|
// Kartu KPI: pakai total_count & total_amount dari summary endpoint
|
||||||
// Struktur summaryResp setelah di-unwrap: { total_count, total_amount, active_gates, active_locations }
|
// Struktur summaryResp setelah di-unwrap: { total_count, total_amount, active_gates, active_locations }
|
||||||
console.log('[Dashboard] Summary response raw:', summaryResp);
|
console.log('[Dashboard] Summary response raw:', summaryResp);
|
||||||
|
console.log('[Dashboard] Summary response keys:', Object.keys(summaryResp || {}));
|
||||||
|
console.log('[Dashboard] Summary response values:', {
|
||||||
|
total_count: summaryResp?.total_count,
|
||||||
|
total_amount: summaryResp?.total_amount,
|
||||||
|
active_gates: summaryResp?.active_gates,
|
||||||
|
active_locations: summaryResp?.active_locations
|
||||||
|
});
|
||||||
console.log('[Dashboard] By Category response raw:', byCategoryResp);
|
console.log('[Dashboard] By Category response raw:', byCategoryResp);
|
||||||
|
console.log('[Dashboard] By Category response keys:', Object.keys(byCategoryResp || {}));
|
||||||
console.log('[Dashboard] State date:', state.date);
|
console.log('[Dashboard] State date:', state.date);
|
||||||
|
|
||||||
// Handle jika response masih wrapped
|
// Handle jika response masih wrapped
|
||||||
@@ -393,15 +553,185 @@ async function loadSummaryAndCharts() {
|
|||||||
totalAmounts = Array(24).fill(0);
|
totalAmounts = Array(24).fill(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hitung total dari hourly data untuk validasi
|
||||||
|
const hourlyTotalCount = totalCounts.reduce((sum, val) => sum + (Number(val) || 0), 0);
|
||||||
|
const hourlyTotalAmount = totalAmounts.reduce((sum, val) => sum + (Number(val) || 0), 0);
|
||||||
|
|
||||||
console.log('[Dashboard] Hourly data processed:', {
|
console.log('[Dashboard] Hourly data processed:', {
|
||||||
date: state.date,
|
date: state.date,
|
||||||
labels: labels.length,
|
labels: labels.length,
|
||||||
counts: totalCounts.length,
|
counts: totalCounts.length,
|
||||||
amounts: totalAmounts.length,
|
amounts: totalAmounts.length,
|
||||||
totalCount: totalCounts.reduce((a, b) => a + b, 0),
|
totalCount: hourlyTotalCount,
|
||||||
totalAmount: totalAmounts.reduce((a, b) => a + b, 0)
|
totalAmount: hourlyTotalAmount
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Validasi: Bandingkan total dari hourly dengan summary
|
||||||
|
const summaryTotalCount = summary.total_count || 0;
|
||||||
|
const summaryTotalAmount = summary.total_amount || 0;
|
||||||
|
|
||||||
|
// Parse tanggal untuk deteksi transisi tahun/bulan
|
||||||
|
const [year, month, day] = state.date.split('-').map(Number);
|
||||||
|
// Deteksi transisi: hari pertama bulan (day === 1)
|
||||||
|
// Ini berlaku untuk semua bulan, termasuk transisi tahun (1 Januari)
|
||||||
|
const isMonthTransition = day === 1;
|
||||||
|
// Deteksi khusus untuk transisi tahun (1 Januari)
|
||||||
|
const isYearTransition = month === 1 && day === 1;
|
||||||
|
|
||||||
|
// Log detail untuk debugging
|
||||||
|
console.log('[Dashboard] Data validation:', {
|
||||||
|
date: state.date,
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day,
|
||||||
|
isYearTransition,
|
||||||
|
isMonthTransition,
|
||||||
|
summaryTotalCount,
|
||||||
|
summaryTotalAmount,
|
||||||
|
hourlyTotalCount,
|
||||||
|
hourlyTotalAmount,
|
||||||
|
countDifference: Math.abs(hourlyTotalCount - summaryTotalCount),
|
||||||
|
amountDifference: Math.abs(hourlyTotalAmount - summaryTotalAmount),
|
||||||
|
ratio: hourlyTotalCount > 0 && summaryTotalCount > 0
|
||||||
|
? (hourlyTotalCount / summaryTotalCount).toFixed(2)
|
||||||
|
: 'N/A'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Flag untuk menandai apakah hourly data valid
|
||||||
|
let hourlyDataValid = true;
|
||||||
|
let useHourlyData = true;
|
||||||
|
|
||||||
|
if (hourlyTotalCount > 0 && summaryTotalCount > 0) {
|
||||||
|
const countDiff = Math.abs(hourlyTotalCount - summaryTotalCount);
|
||||||
|
const amountDiff = Math.abs(hourlyTotalAmount - summaryTotalAmount);
|
||||||
|
const maxCount = Math.max(hourlyTotalCount, summaryTotalCount);
|
||||||
|
const maxAmount = Math.max(hourlyTotalAmount, summaryTotalAmount);
|
||||||
|
const countDiffPercent = maxCount > 0
|
||||||
|
? Number(((countDiff / maxCount) * 100).toFixed(2))
|
||||||
|
: 0;
|
||||||
|
const amountDiffPercent = maxAmount > 0
|
||||||
|
? Number(((amountDiff / maxAmount) * 100).toFixed(2))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// Hitung ratio untuk deteksi data yang sangat tidak proporsional
|
||||||
|
const countRatio = hourlyTotalCount > 0 && summaryTotalCount > 0
|
||||||
|
? hourlyTotalCount / summaryTotalCount
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// Deteksi khusus untuk transisi tahun/bulan: jika hourly jauh lebih besar dari summary
|
||||||
|
// Kemungkinan hourly data dari tanggal lain atau semua data
|
||||||
|
// Threshold lebih ketat untuk transisi tahun (20% vs 50%)
|
||||||
|
const threshold = (isYearTransition || isMonthTransition) ? 20 : 50;
|
||||||
|
const ratioThreshold = 2.0; // Jika hourly > 2x summary, kemungkinan salah
|
||||||
|
|
||||||
|
// Jika perbedaan sangat besar atau ratio tidak proporsional, kemungkinan hourly data salah
|
||||||
|
if (countDiffPercent > threshold || amountDiffPercent > threshold || countRatio > ratioThreshold) {
|
||||||
|
hourlyDataValid = false;
|
||||||
|
useHourlyData = false;
|
||||||
|
|
||||||
|
console.error('[Dashboard] ❌ Hourly data TIDAK VALID - perbedaan terlalu besar:', {
|
||||||
|
date: state.date,
|
||||||
|
isYearTransition,
|
||||||
|
isMonthTransition,
|
||||||
|
summaryTotalCount,
|
||||||
|
hourlyTotalCount,
|
||||||
|
countDiffPercent: countDiffPercent + '%',
|
||||||
|
amountDiffPercent: amountDiffPercent + '%',
|
||||||
|
countRatio: countRatio.toFixed(2),
|
||||||
|
threshold: threshold + '%',
|
||||||
|
action: 'Mengabaikan hourly data, menggunakan data kosong untuk chart'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset hourly data ke kosong karena tidak valid
|
||||||
|
labels = Array.from({ length: 24 }, (_, i) => `${String(i).padStart(2, '0')}:00`);
|
||||||
|
totalCounts = Array(24).fill(0);
|
||||||
|
totalAmounts = Array(24).fill(0);
|
||||||
|
|
||||||
|
// Tampilkan error di UI dengan info lebih detail
|
||||||
|
const transitionInfo = isYearTransition
|
||||||
|
? ' (Transisi Tahun - Data mungkin belum ter-aggregate)'
|
||||||
|
: isMonthTransition
|
||||||
|
? ' (Transisi Bulan - Data mungkin belum ter-aggregate)'
|
||||||
|
: '';
|
||||||
|
const errorMsg = `⚠️ PERINGATAN: Data hourly tidak valid untuk tanggal ${state.date}${transitionInfo}\n` +
|
||||||
|
`Summary: ${summaryTotalCount} events (Rp ${new Intl.NumberFormat('id-ID').format(summaryTotalAmount || 0)})\n` +
|
||||||
|
`Hourly: ${hourlyTotalCount} events (Rp ${new Intl.NumberFormat('id-ID').format(hourlyTotalAmount || 0)})\n` +
|
||||||
|
`Perbedaan: ${countDiffPercent.toFixed(2)}% (Ratio: ${countRatio.toFixed(2)}x) - Kemungkinan data hourly dari tanggal lain.\n` +
|
||||||
|
`Chart hourly akan kosong. Silakan hubungi administrator untuk memperbaiki backend API atau trigger aggregation manual.`;
|
||||||
|
showError(errorMsg);
|
||||||
|
} else if (countDiffPercent > 5 || amountDiffPercent > 5) { // Threshold 5% untuk deteksi lebih sensitif
|
||||||
|
const warningMsg = `⚠️ DATA INCONSISTENCY DETECTED untuk tanggal ${state.date}:\n` +
|
||||||
|
`- Summary: ${summaryTotalCount} events (Rp ${new Intl.NumberFormat('id-ID').format(summaryTotalAmount || 0)})\n` +
|
||||||
|
`- Hourly: ${hourlyTotalCount} events (Rp ${new Intl.NumberFormat('id-ID').format(hourlyTotalAmount || 0)})\n` +
|
||||||
|
`- Perbedaan: ${countDiff} events (${countDiffPercent.toFixed(2)}%), Rp ${new Intl.NumberFormat('id-ID').format(amountDiff)} (${amountDiffPercent.toFixed(2)}%)\n` +
|
||||||
|
`- Catatan: Menggunakan data Summary sebagai sumber utama. Perbedaan kecil mungkin normal karena timing aggregation.`;
|
||||||
|
|
||||||
|
const warningData = {
|
||||||
|
date: state.date,
|
||||||
|
summaryTotalCount,
|
||||||
|
summaryTotalAmount,
|
||||||
|
hourlyTotalCount,
|
||||||
|
hourlyTotalAmount,
|
||||||
|
countDifference: countDiff,
|
||||||
|
countDifferencePercent: countDiffPercent + '%',
|
||||||
|
amountDifference: amountDiff,
|
||||||
|
amountDifferencePercent: amountDiffPercent + '%',
|
||||||
|
message: 'Summary dan Hourly data tidak konsisten. Menggunakan Summary sebagai sumber utama.'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.warn('[Dashboard] ⚠️ DATA INCONSISTENCY DETECTED:', warningData);
|
||||||
|
console.warn('[Dashboard]', warningMsg);
|
||||||
|
|
||||||
|
// Tampilkan warning di UI hanya jika perbedaan sangat besar (>30%)
|
||||||
|
// Perbedaan kecil (<30%) mungkin normal karena timing aggregation atau rounding
|
||||||
|
if (countDiffPercent > 30 || amountDiffPercent > 30) {
|
||||||
|
const errorMsg = `⚠️ PERINGATAN: Data tidak konsisten untuk tanggal ${state.date}\n` +
|
||||||
|
`Summary: ${summaryTotalCount} events (Rp ${new Intl.NumberFormat('id-ID').format(summaryTotalAmount || 0)})\n` +
|
||||||
|
`Hourly: ${hourlyTotalCount} events (Rp ${new Intl.NumberFormat('id-ID').format(hourlyTotalAmount || 0)})\n` +
|
||||||
|
`Selisih: ${countDiffPercent.toFixed(2)}% events, ${amountDiffPercent.toFixed(2)}% amount\n` +
|
||||||
|
`Silakan hubungi administrator jika perbedaan ini tidak normal.`;
|
||||||
|
showError(errorMsg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('[Dashboard] ✅ Data konsisten antara Summary dan Hourly:', {
|
||||||
|
date: state.date,
|
||||||
|
summaryTotalCount,
|
||||||
|
hourlyTotalCount,
|
||||||
|
difference: countDiff,
|
||||||
|
differencePercent: countDiffPercent + '%'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (hourlyTotalCount === 0 && summaryTotalCount === 0) {
|
||||||
|
console.log('[Dashboard] ℹ️ Tidak ada data untuk tanggal', state.date);
|
||||||
|
useHourlyData = true; // Data kosong adalah valid
|
||||||
|
} else if (hourlyTotalCount > 0 && summaryTotalCount === 0) {
|
||||||
|
// Jika hourly ada data tapi summary kosong, kemungkinan summary belum ter-aggregate
|
||||||
|
// Tapi kita tetap gunakan hourly data dengan warning
|
||||||
|
console.warn('[Dashboard] ⚠️ Summary kosong tapi Hourly ada data:', {
|
||||||
|
date: state.date,
|
||||||
|
hourlyTotalCount,
|
||||||
|
message: 'Summary endpoint tidak mengembalikan data, tapi Hourly endpoint ada data. Menggunakan data Hourly dengan hati-hati.'
|
||||||
|
});
|
||||||
|
useHourlyData = true;
|
||||||
|
hourlyDataValid = false; // Mark as potentially invalid
|
||||||
|
} else if (hourlyTotalCount === 0 && summaryTotalCount > 0) {
|
||||||
|
console.warn('[Dashboard] ⚠️ Hourly kosong tapi Summary ada data:', {
|
||||||
|
date: state.date,
|
||||||
|
summaryTotalCount,
|
||||||
|
message: 'Hourly endpoint tidak mengembalikan data, tapi Summary endpoint ada data. Chart akan kosong.'
|
||||||
|
});
|
||||||
|
useHourlyData = true; // Data kosong adalah valid (tidak ada data per jam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log final decision
|
||||||
|
if (!useHourlyData) {
|
||||||
|
console.log('[Dashboard] Decision: Mengabaikan hourly data karena tidak valid, menggunakan data kosong untuk chart');
|
||||||
|
} else if (!hourlyDataValid) {
|
||||||
|
console.log('[Dashboard] Decision: Menggunakan hourly data dengan peringatan (potentially invalid)');
|
||||||
|
} else {
|
||||||
|
console.log('[Dashboard] Decision: Menggunakan hourly data (valid)');
|
||||||
|
}
|
||||||
|
|
||||||
// Pastikan chart sudah di-init sebelum update
|
// Pastikan chart sudah di-init sebelum update
|
||||||
const dailyCanvas = document.getElementById('daily-chart');
|
const dailyCanvas = document.getElementById('daily-chart');
|
||||||
const currentDailyChart = getDailyChart();
|
const currentDailyChart = getDailyChart();
|
||||||
@@ -413,12 +743,13 @@ async function loadSummaryAndCharts() {
|
|||||||
// Selalu update chart, meskipun data kosong (agar user tahu memang tidak ada data)
|
// Selalu update chart, meskipun data kosong (agar user tahu memang tidak ada data)
|
||||||
const dailyChartInstance = getDailyChart();
|
const dailyChartInstance = getDailyChart();
|
||||||
if (dailyChartInstance) {
|
if (dailyChartInstance) {
|
||||||
|
// Set data baru (updateDailyChart sudah handle reset dan validasi)
|
||||||
updateDailyChart({
|
updateDailyChart({
|
||||||
labels,
|
labels,
|
||||||
counts: totalCounts,
|
counts: totalCounts,
|
||||||
amounts: totalAmounts
|
amounts: totalAmounts
|
||||||
});
|
});
|
||||||
console.log('[Dashboard] Daily chart updated successfully');
|
console.log('[Dashboard] Daily chart updated successfully for date:', state.date);
|
||||||
} else {
|
} else {
|
||||||
console.error('[Dashboard] Daily chart tidak bisa di-update, chart belum di-init! Canvas:', dailyCanvas);
|
console.error('[Dashboard] Daily chart tidak bisa di-update, chart belum di-init! Canvas:', dailyCanvas);
|
||||||
}
|
}
|
||||||
@@ -490,11 +821,12 @@ async function loadSummaryAndCharts() {
|
|||||||
// Selalu update chart, meskipun data kosong (agar user tahu memang tidak ada data)
|
// Selalu update chart, meskipun data kosong (agar user tahu memang tidak ada data)
|
||||||
const currentCategoryChart = getCategoryChart();
|
const currentCategoryChart = getCategoryChart();
|
||||||
if (currentCategoryChart) {
|
if (currentCategoryChart) {
|
||||||
|
// Set data baru (updateCategoryChart sudah handle reset dan validasi)
|
||||||
updateCategoryChart({
|
updateCategoryChart({
|
||||||
labels: catLabels,
|
labels: catLabels,
|
||||||
values: catValues
|
values: catValues
|
||||||
});
|
});
|
||||||
console.log('[Dashboard] Category chart updated successfully');
|
console.log('[Dashboard] Category chart updated successfully for date:', state.date);
|
||||||
} else {
|
} else {
|
||||||
console.error('[Dashboard] Category chart tidak bisa di-update, chart belum di-init!');
|
console.error('[Dashboard] Category chart tidak bisa di-update, chart belum di-init!');
|
||||||
}
|
}
|
||||||
@@ -712,8 +1044,97 @@ function setupFilters() {
|
|||||||
dateInput.setAttribute('value', state.date);
|
dateInput.setAttribute('value', state.date);
|
||||||
}
|
}
|
||||||
dateInput.addEventListener('change', () => {
|
dateInput.addEventListener('change', () => {
|
||||||
state.date = dateInput.value || state.date;
|
// Normalisasi tanggal yang dipilih user untuk memastikan format YYYY-MM-DD
|
||||||
loadSummaryAndCharts();
|
const originalValue = dateInput.value;
|
||||||
|
const selectedDate = normalizeDate(originalValue);
|
||||||
|
|
||||||
|
if (selectedDate) {
|
||||||
|
// Validasi bahwa tanggal valid (cek apakah tanggal yang dipilih sama dengan yang dinormalisasi)
|
||||||
|
const [year, month, day] = selectedDate.split('-').map(Number);
|
||||||
|
const dateObj = new Date(year, month - 1, day);
|
||||||
|
const isValidDate = dateObj.getFullYear() === year &&
|
||||||
|
dateObj.getMonth() === month - 1 &&
|
||||||
|
dateObj.getDate() === day;
|
||||||
|
|
||||||
|
if (!isValidDate) {
|
||||||
|
console.warn('[Dashboard] Invalid date after normalization:', selectedDate);
|
||||||
|
dateInput.value = state.date;
|
||||||
|
dateInput.setAttribute('value', state.date);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek apakah tanggal benar-benar berubah
|
||||||
|
const dateChanged = selectedDate !== state.date;
|
||||||
|
|
||||||
|
// Deteksi transisi tahun/bulan
|
||||||
|
// Deteksi transisi: hari pertama bulan (day === 1)
|
||||||
|
const isMonthTransition = day === 1;
|
||||||
|
// Deteksi khusus untuk transisi tahun (1 Januari)
|
||||||
|
const isYearTransition = month === 1 && day === 1;
|
||||||
|
const wasYearTransition = state.date && (() => {
|
||||||
|
const [prevYear, prevMonth, prevDay] = state.date.split('-').map(Number);
|
||||||
|
return prevYear === 2026 && prevMonth === 1 && prevDay === 1;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (dateChanged) {
|
||||||
|
console.log('[Dashboard] Date changed by user:', {
|
||||||
|
original: originalValue,
|
||||||
|
normalized: selectedDate,
|
||||||
|
previousDate: state.date,
|
||||||
|
year: year,
|
||||||
|
month: month,
|
||||||
|
day: day,
|
||||||
|
isYearTransition,
|
||||||
|
isMonthTransition,
|
||||||
|
wasYearTransition,
|
||||||
|
note: 'Resetting all data before loading new date'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset semua data sebelum load tanggal baru (penting untuk transisi tahun/bulan)
|
||||||
|
renderSummary({
|
||||||
|
totalAmount: 0,
|
||||||
|
personCount: 0,
|
||||||
|
motorCount: 0,
|
||||||
|
carCount: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset chart juga
|
||||||
|
const dailyChartInstance = getDailyChart();
|
||||||
|
if (dailyChartInstance) {
|
||||||
|
dailyChartInstance.data.labels = [];
|
||||||
|
dailyChartInstance.data.datasets[0].data = [];
|
||||||
|
dailyChartInstance.data.datasets[1].data = [];
|
||||||
|
dailyChartInstance.update('none');
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryChartInstance = getCategoryChart();
|
||||||
|
if (categoryChartInstance) {
|
||||||
|
categoryChartInstance.data.labels = [];
|
||||||
|
categoryChartInstance.data.datasets[0].data = [];
|
||||||
|
categoryChartInstance.update('none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.date = selectedDate;
|
||||||
|
// Pastikan dateInput juga menggunakan format yang sudah dinormalisasi
|
||||||
|
if (dateInput.value !== selectedDate) {
|
||||||
|
dateInput.value = selectedDate;
|
||||||
|
dateInput.setAttribute('value', selectedDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hanya reload jika tanggal benar-benar berubah
|
||||||
|
if (dateChanged) {
|
||||||
|
// Small delay untuk memastikan UI sudah di-reset
|
||||||
|
setTimeout(() => {
|
||||||
|
loadSummaryAndCharts();
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('[Dashboard] Invalid date selected, keeping current date:', state.date);
|
||||||
|
// Reset ke state.date jika invalid
|
||||||
|
dateInput.value = state.date;
|
||||||
|
dateInput.setAttribute('value', state.date);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,8 +111,12 @@
|
|||||||
|
|
||||||
// Tambahkan info lebih detail untuk debugging
|
// Tambahkan info lebih detail untuk debugging
|
||||||
if (error.message === 'Failed to fetch' || error.message.includes('fetch')) {
|
if (error.message === 'Failed to fetch' || error.message.includes('fetch')) {
|
||||||
errorMessage = 'Gagal terhubung ke server API. Pastikan backend API sudah running di ' +
|
try {
|
||||||
(await import('./dashboard/js/config.js')).then(m => m.API_CONFIG.BASE_URL).catch(() => 'http://localhost:8000');
|
const { API_CONFIG } = await import('./dashboard/js/config.js');
|
||||||
|
errorMessage = 'Gagal terhubung ke server API. Pastikan backend API sudah running di ' + API_CONFIG.BASE_URL;
|
||||||
|
} catch (e) {
|
||||||
|
errorMessage = 'Gagal terhubung ke server API. Pastikan backend API sudah running di https://api.btekno.cloud';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
errorDiv.textContent = errorMessage;
|
errorDiv.textContent = errorMessage;
|
||||||
|
|||||||
Reference in New Issue
Block a user