308 lines
9.3 KiB
JavaScript
308 lines
9.3 KiB
JavaScript
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
|
|
};
|