// public/dashboard/js/api.js // Centralized REST API client for Btekno Retribusi Admin Dashboard import { API_CONFIG } from './config.js'; // Export API_CONFIG untuk digunakan di file lain export { API_CONFIG }; const API_BASE_URL = API_CONFIG.BASE_URL; function getToken() { return localStorage.getItem('token') || ''; } async function apiRequest(path, options = {}) { const url = path.startsWith('http') ? path : `${API_BASE_URL}${path}`; const headers = { 'Content-Type': 'application/json', // X-API-KEY dari konfigurasi backend (RETRIBUSI_API_KEY) 'X-API-KEY': API_CONFIG.API_KEY, ...(options.headers || {}) }; const token = getToken(); if (token) { headers['Authorization'] = `Bearer ${token}`; } const config = { method: options.method || 'GET', headers, body: options.body ? JSON.stringify(options.body) : null }; console.log('[API] Request:', { method: config.method, url, headers: { ...headers, Authorization: token ? 'Bearer ***' : 'none' } }); try { const res = await fetch(url, config); console.log('[API] Response status:', res.status, res.statusText); if (res.status === 401) { // Unauthorized → clear token & redirect to login localStorage.removeItem('token'); localStorage.removeItem('user'); // Cek apakah sudah di login page untuk menghindari 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 === ''; // Hanya redirect jika benar-benar di halaman dashboard, bukan di login page if (!isLoginPage && currentPath.includes('dashboard')) { window.location.href = '../index.html'; } throw new Error('Unauthorized'); } const text = await res.text(); let json; try { json = text ? JSON.parse(text) : {}; } catch (e) { 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) { const msg = json.message || json.error || `HTTP ${res.status}`; throw new Error(msg); } // Some endpoints might wrap data as { success, data, ... } if (json && Object.prototype.hasOwnProperty.call(json, 'success') && Object.prototype.hasOwnProperty.call(json, 'data')) { return json.data; } return json; } catch (err) { console.error('[API] Request failed:', { url, error: err, message: err.message, stack: err.stack }); // Tambahkan info lebih detail untuk "Failed to fetch" if (err.message === 'Failed to fetch' || err.message.includes('fetch')) { const detailedError = new Error(`Gagal terhubung ke API: ${url}. Pastikan backend API sudah running di ${API_BASE_URL}`); detailedError.originalError = err; throw detailedError; } throw err; } } // Helper untuk build query string dari object params function buildQuery(params = {}) { const search = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null && 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(); return qs ? `?${qs}` : ''; } // Typed helpers export async function apiLogin(username, password) { return apiRequest('/auth/v1/login', { method: 'POST', body: { username, password } }); } export async function apiGetLocations(params = {}) { // Handle pagination: { page, limit } const qs = buildQuery(params); return apiRequest(`/retribusi/v1/frontend/locations${qs}`); } export async function apiGetGates(locationCode, params = {}) { // Handle pagination: { page, limit, location_code } const queryParams = { ...params }; if (locationCode) queryParams.location_code = locationCode; const qs = buildQuery(queryParams); return apiRequest(`/retribusi/v1/frontend/gates${qs}`); } export async function apiGetSummary({ date, locationCode, gateCode }) { const qs = buildQuery({ date, location_code: locationCode, gate_code: gateCode }); 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 }) { const qs = buildQuery({ start_date: startDate, end_date: endDate, location_code: locationCode }); return apiRequest(`/retribusi/v1/dashboard/daily${qs}`); } export async function apiGetByCategory({ date, locationCode, gateCode }) { const qs = buildQuery({ date, location_code: locationCode, gate_code: gateCode }); 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) export async function apiGetSummaryDaily(params = {}) { const qs = buildQuery(params); return apiRequest(`/retribusi/v1/summary/daily${qs}`); } // Ringkasan per jam (hourly_summary) export async function apiGetSummaryHourly(params = {}) { const qs = buildQuery(params); 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) export async function apiGetRealtimeSnapshot(params = {}) { const qs = buildQuery(params); return apiRequest(`/retribusi/v1/realtime/snapshot${qs}`); } // Entry events list (raw events dari mesin YOLO) // GET /retribusi/v1/frontend/entry-events // Parameters: page, limit, location_code, gate_code, category, start_date, end_date export async function apiGetEntryEvents(params = {}) { const qs = buildQuery(params); return apiRequest(`/retribusi/v1/frontend/entry-events${qs}`); } // Realtime events list (history untuk SSE) // GET /retribusi/v1/realtime/events // Parameters: page, limit, location_code, gate_code, category, start_date, end_date export async function apiGetRealtimeEvents(params = {}) { const qs = buildQuery(params); return apiRequest(`/retribusi/v1/realtime/events${qs}`); } // Alias untuk backward compatibility export async function apiGetEvents(params = {}) { return apiGetEntryEvents(params); } // Catatan: realtime SSE /retribusi/v1/realtime/stream akan diakses langsung via EventSource, // 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 }; }; }