2025-12-18 11:21:40 +07:00
|
|
|
// 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
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-19 05:22:27 +07:00
|
|
|
console.log('[API] Request:', { method: config.method, url, headers: { ...headers, Authorization: token ? 'Bearer ***' : 'none' } });
|
|
|
|
|
|
2025-12-18 11:21:40 +07:00
|
|
|
try {
|
|
|
|
|
const res = await fetch(url, config);
|
2025-12-19 05:22:27 +07:00
|
|
|
console.log('[API] Response status:', res.status, res.statusText);
|
2025-12-18 11:21:40 +07:00
|
|
|
|
|
|
|
|
if (res.status === 401) {
|
|
|
|
|
// Unauthorized → clear token & redirect to login
|
|
|
|
|
localStorage.removeItem('token');
|
|
|
|
|
localStorage.removeItem('user');
|
2025-12-18 11:34:20 +07:00
|
|
|
// Cek apakah sudah di login page untuk menghindari redirect loop
|
2025-12-18 13:25:50 +07:00
|
|
|
const currentPath = window.location.pathname.toLowerCase();
|
2025-12-18 13:36:36 +07:00
|
|
|
const isLoginPage = currentPath.includes('index.html') ||
|
|
|
|
|
currentPath.includes('index.php') ||
|
2025-12-18 13:25:50 +07:00
|
|
|
currentPath === '/' ||
|
2025-12-18 13:36:36 +07:00
|
|
|
currentPath === '/index.html' ||
|
2025-12-18 13:25:50 +07:00
|
|
|
currentPath === '/index.php' ||
|
|
|
|
|
currentPath.endsWith('/') ||
|
|
|
|
|
currentPath === '';
|
|
|
|
|
// Hanya redirect jika benar-benar di halaman dashboard, bukan di login page
|
|
|
|
|
if (!isLoginPage && currentPath.includes('dashboard')) {
|
2025-12-18 13:36:36 +07:00
|
|
|
window.location.href = '../index.html';
|
2025-12-18 11:34:20 +07:00
|
|
|
}
|
2025-12-18 11:21:40 +07:00
|
|
|
throw new Error('Unauthorized');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const text = await res.text();
|
|
|
|
|
let json;
|
|
|
|
|
try {
|
|
|
|
|
json = text ? JSON.parse(text) : {};
|
|
|
|
|
} catch (e) {
|
|
|
|
|
json = { raw: text };
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 23:38:42 +07:00
|
|
|
// 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
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 11:21:40 +07:00
|
|
|
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) {
|
2025-12-19 05:22:27 +07:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 11:21:40 +07:00
|
|
|
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 !== '') {
|
2026-01-01 23:38:42 +07:00
|
|
|
// 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);
|
2025-12-18 11:21:40 +07:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
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
|
|
|
|
|
});
|
2026-01-01 23:38:42 +07:00
|
|
|
const url = `/retribusi/v1/dashboard/summary${qs}`;
|
|
|
|
|
console.log('[API] apiGetSummary - URL:', url, 'Params:', { date, locationCode, gateCode });
|
|
|
|
|
return apiRequest(url);
|
2025-12-18 11:21:40 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
});
|
2026-01-01 23:38:42 +07:00
|
|
|
const url = `/retribusi/v1/dashboard/by-category${qs}`;
|
|
|
|
|
console.log('[API] apiGetByCategory - URL:', url, 'Params:', { date, locationCode, gateCode });
|
|
|
|
|
return apiRequest(url);
|
2025-12-18 11:21:40 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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);
|
2026-01-01 23:38:42 +07:00
|
|
|
const url = `/retribusi/v1/summary/hourly${qs}`;
|
|
|
|
|
console.log('[API] apiGetSummaryHourly - URL:', url, 'Params:', params);
|
|
|
|
|
return apiRequest(url);
|
2025-12-18 11:21:40 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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).
|
|
|
|
|
|
2026-01-01 23:38:42 +07:00
|
|
|
// 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
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|