const ADMIN_TOKEN_KEY = "admin_token"; const ADMIN_PROFILE_KEY = "admin_profile"; function formatMoney(value) { const number = Number(value || 0); if (!Number.isFinite(number)) { return "Rp 0"; } return new Intl.NumberFormat("id-ID", { style: "currency", currency: "IDR", maximumFractionDigits: 0 }).format(number); } function formatDateTime(value) { if (!value) { return "-"; } const date = new Date(value); if (Number.isNaN(date.getTime())) { return value; } return new Intl.DateTimeFormat("en-GB", { dateStyle: "medium", timeStyle: "short" }).format(date); } function buildQuery(query) { const search = new URLSearchParams(); Object.entries(query || {}).forEach(([key, val]) => { if (val !== undefined && val !== null && val !== "") { search.append(key, String(val)); } }); return search.toString(); } async function adminFetch(path, options = {}) { const token = localStorage.getItem(ADMIN_TOKEN_KEY); const { method = "GET", query, body, headers: extraHeaders = {}, auth = true } = options; const suffix = buildQuery(query || {}); const url = suffix ? `${path}?${suffix}` : path; const headers = { ...extraHeaders }; if (auth) { if (!token) { throw new Error("ADMIN_AUTH_MISSING"); } headers.Authorization = `Bearer ${token}`; } if (method !== "GET" && body !== undefined) { headers["Content-Type"] = "application/json"; } const response = await fetch(url, { method, headers, body: method === "GET" || body === undefined ? undefined : JSON.stringify(body) }); const raw = await response.text(); let payload; try { payload = raw ? JSON.parse(raw) : {}; } catch (error) { payload = {}; } if (!response.ok) { const message = payload?.message || payload?.error || `Request failed with status ${response.status}`; throw new Error(message); } return payload?.data !== undefined ? payload.data : payload; } function hasPermission(profile, permission) { if (!permission) { return true; } const permissions = profile?.permissions; if (!permissions) { return false; } if (Array.isArray(permissions)) { return permissions.includes("*") || permissions.includes(permission); } if (permissions.admin === "*" || permissions.admin === true) { return true; } if (permissions[permission] === "*" || permissions[permission] === true) { return true; } const [domain, action] = permission.split(":"); const domainValue = permissions[domain]; if (domainValue === "*" || domainValue === true) { return true; } if (Array.isArray(domainValue)) { return domainValue.includes("*") || domainValue.includes(action); } if (domainValue && typeof domainValue === "object") { return domainValue[action] === "*" || domainValue[action] === true; } return false; } function getStoredProfile() { try { return JSON.parse(localStorage.getItem(ADMIN_PROFILE_KEY) || "null"); } catch (_error) { return null; } } function applyPermissionAttributes(profile) { document.querySelectorAll("[data-admin-permission]").forEach((el) => { const allowed = hasPermission(profile, el.getAttribute("data-admin-permission")); el.classList.toggle("hidden", !allowed); if ("disabled" in el) { el.disabled = !allowed; } }); } window.AdminUIAPI = { ADMIN_TOKEN_KEY, ADMIN_PROFILE_KEY, setToken: (token) => localStorage.setItem(ADMIN_TOKEN_KEY, token), getToken: () => localStorage.getItem(ADMIN_TOKEN_KEY), clearToken: () => { localStorage.removeItem(ADMIN_TOKEN_KEY); localStorage.removeItem(ADMIN_PROFILE_KEY); }, requireToken: () => { const token = localStorage.getItem(ADMIN_TOKEN_KEY); if (!token) { window.location.href = "/ui/admin-login"; throw new Error("ADMIN_AUTH_MISSING"); } return token; }, login: async ({ username, password }) => { const data = await adminFetch("/admin/login", { method: "POST", auth: false, body: { username, password } }); if (data?.token) { window.AdminUIAPI.setToken(data.token); if (data.user) { localStorage.setItem(ADMIN_PROFILE_KEY, JSON.stringify(data.user)); } } return data; }, getMe: async () => { const profile = await adminFetch("/admin/me"); localStorage.setItem(ADMIN_PROFILE_KEY, JSON.stringify(profile)); return profile; }, getStoredProfile, hasPermission: (permission, profile) => hasPermission(profile || getStoredProfile(), permission), applyPermissions: async () => { let profile = getStoredProfile(); if (!profile?.permissions) { profile = await window.AdminUIAPI.getMe(); } applyPermissionAttributes(profile); return profile; }, listMerchants: () => adminFetch("/admin/merchants"), listOutlets: (query) => adminFetch("/admin/outlets", { query }), getOutlet: (id) => adminFetch(`/admin/outlets/${id}`), getMerchant: (id) => adminFetch(`/admin/merchants/${id}`), patchMerchant: (id, payload) => adminFetch(`/admin/merchants/${id}`, { method: "PATCH", body: payload }), approveMerchant: (id) => adminFetch(`/admin/merchants/${id}/approve`, { method: "POST" }), rejectMerchant: (id, payload) => adminFetch(`/admin/merchants/${id}/reject`, { method: "POST", body: payload || {} }), listTerminals: (query) => adminFetch("/admin/terminals", { query }), getTerminal: (id) => adminFetch(`/admin/terminals/${id}`), listDevices: (query) => adminFetch("/admin/devices", { query }), getDevice: (id) => adminFetch(`/admin/devices/${id}`), rotateDeviceCredential: (id) => adminFetch(`/admin/devices/${id}/credentials/rotate`, { method: "POST", body: {} }), getDeviceHeartbeats: (id, query) => adminFetch(`/admin/devices/${id}/heartbeats`, { query }), getDeviceConfig: (id) => adminFetch(`/admin/devices/${id}/config`), getDeviceConfigStatus: (id) => adminFetch(`/admin/devices/${id}/config/status`), retryDeviceConfigPush: (id, payload) => adminFetch(`/admin/devices/${id}/config/retry-push`, { method: "POST", body: payload || {} }), listTransactions: (query) => adminFetch("/admin/transactions", { query }), getDynamicQrExpiryScheduler: () => adminFetch("/admin/transactions/expiry-scheduler"), createSettlementBatches: (payload) => adminFetch("/admin/settlement-batches", { method: "POST", body: payload || {} }), listSettlementBatches: (query) => adminFetch("/admin/settlement-batches", { query }), getSettlementBatch: (id) => adminFetch(`/admin/settlement-batches/${id}`), listSettlementAdjustments: (query) => adminFetch("/admin/settlement-adjustments", { query }), createSettlementAdjustmentExportJob: (payload) => adminFetch("/admin/exports/settlement-adjustments", { method: "POST", body: payload || {} }), getExportJob: (id) => adminFetch(`/admin/exports/${id}`), listExportJobs: (query) => adminFetch("/admin/exports", { query }), downloadExportJob: async (id) => { const token = localStorage.getItem(ADMIN_TOKEN_KEY); if (!token) { throw new Error("ADMIN_AUTH_MISSING"); } const response = await fetch(`/admin/exports/${id}/download`, { headers: { Authorization: `Bearer ${token}` } }); if (!response.ok) { throw new Error(`Export download failed: ${response.status}`); } return { blob: await response.blob(), filename: response.headers.get("content-disposition")?.match(/filename="([^"]+)"/)?.[1] || `${id}.csv` }; }, getSettlementReconciliationReport: (query) => adminFetch("/admin/reconciliation/settlement-batches", { query }), markSettlementBatchPaid: (id, payload) => adminFetch(`/admin/settlement-batches/${id}/mark-paid`, { method: "POST", body: payload || {} }), updateSettlementBatchReference: (id, payload) => adminFetch(`/admin/settlement-batches/${id}/reference`, { method: "PATCH", body: payload || {} }), recordSettlementBatchAdjustment: (id, payload) => adminFetch(`/admin/settlement-batches/${id}/adjustments`, { method: "POST", body: payload || {} }), approveSettlementAdjustment: (id, payload) => adminFetch(`/admin/settlement-adjustments/${id}/approve`, { method: "POST", body: payload || {} }), rejectSettlementAdjustment: (id, payload) => adminFetch(`/admin/settlement-adjustments/${id}/reject`, { method: "POST", body: payload || {} }), markSettlementBatchFailed: (id, payload) => adminFetch(`/admin/settlement-batches/${id}/mark-failed`, { method: "POST", body: payload || {} }), cancelSettlementBatch: (id, payload) => adminFetch(`/admin/settlement-batches/${id}/cancel`, { method: "POST", body: payload || {} }), reprocessSettlementBatch: (id) => adminFetch(`/admin/settlement-batches/${id}/reprocess`, { method: "POST", body: {} }), getDashboardSummary: () => adminFetch("/admin/dashboard/summary"), getMqttStatus: (query) => adminFetch("/admin/mqtt/status", { query }), getObservabilitySummary: () => adminFetch("/admin/observability/summary"), listFailedNotifications: (query) => adminFetch("/admin/notifications/failed", { query }), listAuditLogs: (query) => adminFetch("/admin/audit-logs", { query }), formatMoney, formatDateTime };