(() => { const API_BASE_URL = (window.DASHBOARD_API_BASE_URL || "http://127.0.0.1:3000/api").replace(/\/$/, ""); if (!window.L) { console.error("Leaflet failed to load."); return; } const state = { mapFilter: "central", tab: "all", selectedAreaKey: null, selectedOwnerKey: null, search: "", sortBy: "waste", modalRequestId: 0, modal: { areaType: "region", areaKey: null, ownerName: "", page: 1, pageSize: 25, search: "", ownerType: "", severity: "", priorityOnly: false, }, }; const dom = { kpi: document.getElementById("kpi"), mapRoot: document.getElementById("map"), mapFilters: document.getElementById("mf"), tabs: document.getElementById("tabs"), legend: document.getElementById("legend"), sidebarContent: document.getElementById("sbc"), modal: document.getElementById("rupModal"), modalTop: document.getElementById("modalTop"), modalBody: document.getElementById("modalBody"), }; if (Object.values(dom).some((element) => !element)) { console.error("Dashboard shell is incomplete."); return; } const FILTERS = [ { key: "central", label: "Kementerian/Lembaga" }, { key: "provinsi", label: "Pemprov" }, { key: "kabkota", label: "Pemkot" }, { key: "other", label: "Others" }, ]; const TABS = [ { key: "all", label: "Semua" }, { key: "kabupaten", label: "Kabupaten" }, { key: "kota", label: "Kota" }, ]; const SEVERITY_FILTERS = [ { key: "", label: "Semua Severity" }, { key: "low", label: "Low" }, { key: "med", label: "Medium" }, { key: "high", label: "High" }, { key: "absurd", label: "Absurd" }, ]; let dashboardData = null; let regionsByKey = new Map(); let provincesByKey = new Map(); let map = null; let geoLayer = null; function escapeHtml(value) { return String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function escapeAttr(value) { return String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function escapeJsString(value) { return String(value) .replace(/\\/g, "\\\\") .replace(/'/g, "\\'") .replace(/\r/g, "\\r") .replace(/\n/g, "\\n"); } function jsArg(value) { if (typeof value === "boolean") { return value ? "true" : "false"; } if (typeof value === "number") { return String(value); } return `'${escapeJsString(value)}'`; } function actionCall(action, ...args) { return escapeAttr(`dashboardActions.${action}(${args.map(jsArg).join(",")})`); } function actionExpr(expression) { return escapeAttr(expression); } function normalizeSourceId(sourceId) { if (sourceId === null || sourceId === undefined) { return null; } const normalized = String(sourceId).trim(); if (!/^\d+$/.test(normalized)) { return null; } const parsed = Number(normalized); if (!Number.isSafeInteger(parsed) || parsed <= 0) { return null; } return String(parsed); } function buildInaprocUrl(sourceId) { const kode = normalizeSourceId(sourceId); return kode ? `https://data.inaproc.id/rup?kode=${encodeURIComponent(kode)}` : null; } function isProvinceView() { return state.mapFilter === "provinsi"; } function isCentralOwnerMode() { return state.mapFilter === "central"; } function currentAreaType() { return isProvinceView() ? "province" : "region"; } function formatCompactCurrency(value) { const amount = Number(value) || 0; const abs = Math.abs(amount); if (abs >= 1e12) return `${(amount / 1e12).toFixed(amount % 1e12 === 0 ? 0 : 1)} T`; if (abs >= 1e9) return `${(amount / 1e9).toFixed(amount % 1e9 === 0 ? 0 : 1)} B`; if (abs >= 1e6) return `${(amount / 1e6).toFixed(amount % 1e6 === 0 ? 0 : 1)} M`; if (abs >= 1e3) return `${(amount / 1e3).toFixed(amount % 1e3 === 0 ? 0 : 1)} K`; return `${amount.toFixed(0)}`; } function formatCurrencyLong(value) { const number = Math.round(Number(value) || 0); return `Rp ${number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".")}`; } function formatNumber(value) { const number = Math.round(Number(value) || 0); return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, "."); } function formatDecimal(value) { const amount = Number(value) || 0; return amount % 1 === 0 ? formatNumber(amount) : amount.toFixed(2).replace(".", ","); } function ownerTypeLabel(value) { if (value === "central") return "Kementerian/Lembaga"; if (value === "provinsi") return "Pemprov"; if (value === "kabkota") return "Pemkot"; if (value === "other") return "Others"; return "Tidak diketahui"; } function ownerTypeCount(area, ownerType) { return Number(area && area.ownerMix ? area.ownerMix[ownerType] : 0) || 0; } function ownerMixSummary(area) { return `K/L ${formatNumber(ownerTypeCount(area, "central"))} | Pemprov ${formatNumber( ownerTypeCount(area, "provinsi") )} | Pemkot ${formatNumber(ownerTypeCount(area, "kabkota"))} | Others ${formatNumber( ownerTypeCount(area, "other") )}`; } function areaOwnerSummary(area) { return `${activeSidebarOwnerLabel()} saja`; } function areaBadgeLabel(area) { if (area.regionType === "Provinsi") return "Prov."; if (area.regionType === "Kota") return "Kota"; return "Kab."; } function areaBadgeClass(area) { return area.regionType === "Kota" ? "bk" : "bp"; } function areaSecondaryLine(area) { return isProvinceView() ? "Hanya paket Pemprov" : area.provinceName; } function severityColor(severity) { if (severity === "absurd") return "var(--rose)"; if (severity === "high") return "var(--brick)"; if (severity === "med") return "var(--olive)"; return "var(--steel)"; } function severityLabel(severity) { if (severity === "absurd") return "Absurd"; if (severity === "high") return "High"; if (severity === "med") return "Medium"; return "Low"; } function totalAreaMetrics(area) { return { totalPackages: Number(area?.totalPackages) || 0, totalPriorityPackages: Number(area?.totalPriorityPackages) || 0, totalPotentialWaste: Number(area?.totalPotentialWaste) || 0, totalBudget: Number(area?.totalBudget) || 0, }; } function getActiveSidebarOwnerKey() { return isProvinceView() ? "provinsi" : state.mapFilter; } function activeSidebarOwnerLabel() { return ownerTypeLabel(getActiveSidebarOwnerKey()); } function getAreaMetricsForOwner(area, ownerKey) { if (!area) { return totalAreaMetrics(null); } const metrics = area.ownerMetrics && area.ownerMetrics[ownerKey]; if (metrics) { return { totalPackages: Number(metrics.totalPackages) || 0, totalPriorityPackages: Number(metrics.totalPriorityPackages) || 0, totalPotentialWaste: Number(metrics.totalPotentialWaste) || 0, totalBudget: Number(metrics.totalBudget) || 0, }; } if (isProvinceView() && ownerKey === "provinsi") { return totalAreaMetrics(area); } return { totalPackages: ownerTypeCount(area, ownerKey), totalPriorityPackages: 0, totalPotentialWaste: 0, totalBudget: 0, }; } function getSidebarAreaMetrics(area) { const ownerKey = getActiveSidebarOwnerKey(); return ownerKey ? getAreaMetricsForOwner(area, ownerKey) : totalAreaMetrics(area); } function renderSeverityFilterOptions(selectedValue) { return SEVERITY_FILTERS.map( (filter) => `` ).join(""); } function getOwnerCardKey(ownerType, ownerName) { return `${ownerType}::${ownerName}`; } function getAreaKey(area, areaType = currentAreaType()) { return areaType === "province" ? area.provinceKey : area.regionKey; } function getAreaByKey(areaType, areaKey) { return (areaType === "province" ? provincesByKey : regionsByKey).get(areaKey) || null; } function getActiveAreaByKey(areaKey) { return getAreaByKey(currentAreaType(), areaKey); } function getActiveAreas() { return isProvinceView() ? dashboardData.provinceView.provinces : dashboardData.regions; } function getCentralOwnersForSidebar() { return dashboardData && dashboardData.ownerLists && Array.isArray(dashboardData.ownerLists.central) ? dashboardData.ownerLists.central : []; } function getActiveGeo() { return isProvinceView() ? dashboardData.provinceView.geo : dashboardData.geo; } function getActiveLegend() { return isProvinceView() ? dashboardData.provinceView.legend : dashboardData.legend; } function getFeatureAreaKey(feature) { return isProvinceView() ? feature.properties.provinceKey : feature.properties.regionKey; } function ensureMapStatus() { let status = document.getElementById("mapStatus"); if (!status) { status = document.createElement("div"); status.id = "mapStatus"; status.className = "map-status"; dom.mapRoot.parentElement.appendChild(status); } return status; } function setMapStatus(message, isError) { const status = ensureMapStatus(); status.className = `map-status${isError ? " error" : ""}`; status.textContent = message; } function clearMapStatus() { const status = document.getElementById("mapStatus"); if (status) { status.remove(); } } function renderKpiCards(cards) { dom.kpi.innerHTML = cards .map( (item) => `
${escapeHtml(item.label)}
${escapeHtml( item.value )}
${escapeHtml(item.sublabel)}
` ) .join(""); } function renderSidebarMessage(message, isError) { dom.sidebarContent.innerHTML = `
${escapeHtml(message)}
`; } function renderModalState(title, message, isError) { dom.modalTop.innerHTML = ``; dom.modalBody.innerHTML = ``; } function renderBootstrapLoading() { renderKpiCards([ { label: "Total Potensi Pemborosan", value: "...", sublabel: "Menghitung agregat audit" }, { label: "Paket Prioritas Audit", value: "...", sublabel: "Memuat daftar area" }, { label: "Total Pagu Teraudit", value: "...", sublabel: "Menyiapkan peta kab/kota dan provinsi" }, { label: "Paket Terpetakan", value: "...", sublabel: "Memeriksa cakupan lokasi" }, ]); renderSidebarMessage("Memuat audit pengadaan per area...", false); setMapStatus("Memuat peta audit...", false); } function renderBootstrapError(error) { renderKpiCards([ { label: "Total Potensi Pemborosan", value: "-", sublabel: "Backend belum siap" }, { label: "Paket Prioritas Audit", value: "-", sublabel: "Periksa ingest hasil analyze" }, { label: "Total Pagu Teraudit", value: "-", sublabel: "Ulangi db:reset bila perlu" }, { label: "Paket Terpetakan", value: "-", sublabel: "Map belum dapat dibuat" }, ]); renderSidebarMessage(`Gagal memuat dashboard audit: ${error}`, true); setMapStatus(`Gagal memuat dashboard audit: ${error}`, true); } function formatFetchError(error) { return error instanceof Error ? error.message : String(error); } async function fetchJson(path) { const response = await fetch(`${API_BASE_URL}${path}`); const text = await response.text(); let payload = null; if (text) { try { payload = JSON.parse(text); } catch (_error) { throw new Error(`Invalid JSON response from ${path}`); } } if (!response.ok) { throw new Error(payload && payload.error ? payload.error : `Request failed (${response.status})`); } return payload; } function normalizeDashboardData(payload) { if (!payload || typeof payload !== "object") { throw new Error("Bootstrap payload tidak valid."); } return { summary: payload.summary || { totalPackages: 0, totalPriorityPackages: 0, totalPotentialWaste: 0, totalBudget: 0, unmappedPackages: 0, multiLocationPackages: 0, }, legend: payload.legend || { zeroColor: "#243155", ranges: [] }, geo: payload.geo || { type: "FeatureCollection", features: [] }, regions: Array.isArray(payload.regions) ? payload.regions : [], provinceView: { legend: (payload.provinceView && payload.provinceView.legend) || { zeroColor: "#243155", ranges: [] }, geo: (payload.provinceView && payload.provinceView.geo) || { type: "FeatureCollection", features: [] }, provinces: payload.provinceView && Array.isArray(payload.provinceView.provinces) ? payload.provinceView.provinces : [], }, ownerLists: { central: payload.ownerLists && Array.isArray(payload.ownerLists.central) ? payload.ownerLists.central : [], }, }; } function getLegendColor(value) { const legend = getActiveLegend(); if (!legend) { return "#243155"; } if (!value || value <= 0) { return legend.zeroColor || "#243155"; } const range = (legend.ranges || []).find((item) => value >= item.min && value <= item.max); return range ? range.color : legend.ranges[legend.ranges.length - 1]?.color || "#a83c2e"; } function areaMatchesCurrentView(area) { if (!area) { return false; } if (isProvinceView()) { return area.totalPackages > 0; } if (state.tab === "kabupaten" && area.regionType !== "Kabupaten") { return false; } if (state.tab === "kota" && area.regionType !== "Kota") { return false; } if (FILTERS.some((filter) => filter.key === state.mapFilter)) { return ownerTypeCount(area, state.mapFilter) > 0; } return true; } function getFilteredAreasForSidebar() { let areas = getActiveAreas().filter((area) => areaMatchesCurrentView(area)); if (state.search) { const query = state.search.toLowerCase(); const activeOwnerQuery = activeSidebarOwnerLabel().toLowerCase(); areas = areas.filter((area) => { const matchesName = area.displayName.toLowerCase().includes(query) || area.provinceName.toLowerCase().includes(query); if (isProvinceView()) { return matchesName; } return matchesName || activeOwnerQuery.includes(query); }); } const metricsByAreaKey = new Map(areas.map((area) => [getAreaKey(area), getSidebarAreaMetrics(area)])); const sorters = { waste: (left, right) => metricsByAreaKey.get(getAreaKey(right)).totalPotentialWaste - metricsByAreaKey.get(getAreaKey(left)).totalPotentialWaste, priority: (left, right) => metricsByAreaKey.get(getAreaKey(right)).totalPriorityPackages - metricsByAreaKey.get(getAreaKey(left)).totalPriorityPackages, packages: (left, right) => metricsByAreaKey.get(getAreaKey(right)).totalPackages - metricsByAreaKey.get(getAreaKey(left)).totalPackages, budget: (left, right) => metricsByAreaKey.get(getAreaKey(right)).totalBudget - metricsByAreaKey.get(getAreaKey(left)).totalBudget, }; return areas.sort((left, right) => { const primary = (sorters[state.sortBy] || sorters.waste)(left, right); return primary !== 0 ? primary : left.displayName.localeCompare(right.displayName, "id"); }); } function getFilteredOwnersForSidebar() { let owners = getCentralOwnersForSidebar().slice(); if (state.search) { const query = state.search.toLowerCase(); owners = owners.filter((owner) => owner.ownerName.toLowerCase().includes(query)); } const sorters = { waste: (left, right) => right.totalPotentialWaste - left.totalPotentialWaste, priority: (left, right) => right.totalPriorityPackages - left.totalPriorityPackages, packages: (left, right) => right.totalPackages - left.totalPackages, budget: (left, right) => right.totalBudget - left.totalBudget, }; return owners.sort((left, right) => { const primary = (sorters[state.sortBy] || sorters.waste)(left, right); return primary !== 0 ? primary : left.ownerName.localeCompare(right.ownerName, "id"); }); } function renderKpis() { const summary = dashboardData.summary; const mappedPackages = summary.totalPackages - summary.unmappedPackages; renderKpiCards([ { label: "Total Potensi Pemborosan", value: `Rp ${formatCompactCurrency(summary.totalPotentialWaste)}`, sublabel: "Nilai nasional raw, tanpa duplikasi multi-lokasi", }, { label: "Paket Prioritas Audit", value: formatNumber(summary.totalPriorityPackages), sublabel: `${formatNumber(summary.totalPackages)} paket teraudit`, }, { label: "Total Pagu Teraudit", value: `Rp ${formatCompactCurrency(summary.totalBudget)}`, sublabel: "Akumulasi pagu dari seluruh artifact audit", }, { label: "Paket Terpetakan", value: `${formatNumber(mappedPackages)} / ${formatNumber(summary.totalPackages)}`, sublabel: `${formatNumber(summary.unmappedPackages)} unmapped | ${formatNumber(summary.multiLocationPackages)} multi-lokasi`, }, ]); } function renderLegend() { const legend = getActiveLegend(); const title = isProvinceView() ? "Potensi Pemborosan Paket Pemprov per Provinsi" : "Potensi Pemborosan per Kab/Kota"; const zeroLabel = isProvinceView() ? "Tidak ada paket pemprov terdeteksi" : "Tidak ada potensi terdeteksi"; const note = isProvinceView() ? "Agregasi provinsi mendeduplikasi paket multi-kab/kota di provinsi yang sama." : "Map region menghitung penuh paket multi-lokasi, sehingga agregat region bisa lebih besar dari KPI nasional."; const rows = [ `
${escapeHtml(title)}
`, `
${escapeHtml( zeroLabel )}
`, ]; (legend.ranges || []).forEach((range) => { rows.push( `
Rp ${escapeHtml( formatCompactCurrency(range.min) )} – Rp ${escapeHtml(formatCompactCurrency(range.max))}
` ); }); rows.push(`
${escapeHtml(note)}
`); dom.legend.innerHTML = rows.join(""); } function renderFilterChips() { dom.mapFilters.innerHTML = FILTERS.map( (filter) => `
${escapeHtml( filter.label )}
` ).join(""); } function renderTabs() { const provinceView = isProvinceView(); const centralOwnerMode = isCentralOwnerMode(); dom.tabs.innerHTML = TABS.map((tab) => { const active = provinceView || centralOwnerMode ? tab.key === "all" : tab.key === state.tab; const disabled = (provinceView || centralOwnerMode) && tab.key !== "all"; return ``; }).join(""); } function sortControl() { const placeholder = isCentralOwnerMode() ? "Cari kementerian/lembaga..." : isProvinceView() ? "Cari provinsi..." : "Cari kabupaten/kota..."; return ( `
🔍
` + `
` ); } function renderOwnerSidebarContent() { const owners = getFilteredOwnersForSidebar(); if (!owners.length) { dom.sidebarContent.innerHTML = sortControl() + `
Tidak ada kementerian/lembaga yang cocok dengan filter saat ini.
`; return; } const maxWaste = Math.max(...owners.map((owner) => owner.totalPotentialWaste), 1); dom.sidebarContent.innerHTML = sortControl() + owners .map((owner, index) => { const selectedClass = state.selectedOwnerKey === getOwnerCardKey(owner.ownerType, owner.ownerName) ? " a" : ""; return ( `
` + `
#${index + 1}${escapeHtml( owner.ownerName )}
K/L
` + `
Kementerian/Lembaga
` + `
Rp ${escapeHtml(formatCompactCurrency(owner.totalPotentialWaste))} · ${escapeHtml( formatNumber(owner.totalPriorityPackages) )} prioritas
` + `
` + `
Total Paket: ${escapeHtml( formatNumber(owner.totalPackages) )}
Severity High: ${escapeHtml( formatNumber(owner.severityCounts.high) )}
` + `
Severity Absurd ${escapeHtml(formatNumber(owner.severityCounts.absurd))}
` + `
Pagu Teraudit${escapeHtml( `Rp ${formatCompactCurrency(owner.totalBudget)}` )}
` + `
` ); }) .join(""); } function renderSidebarContent() { if (!dashboardData) { renderSidebarMessage("Data dashboard belum tersedia.", true); return; } if (isCentralOwnerMode()) { renderOwnerSidebarContent(); return; } const areas = getFilteredAreasForSidebar(); if (!areas.length) { dom.sidebarContent.innerHTML = sortControl() + `
Tidak ada ${escapeHtml( isProvinceView() ? "provinsi" : "region" )} yang cocok dengan filter saat ini.
`; return; } const areaEntries = areas.map((area) => ({ area, metrics: getSidebarAreaMetrics(area), })); const maxWaste = Math.max(...areaEntries.map(({ metrics }) => metrics.totalPotentialWaste), 1); const ownerLabel = activeSidebarOwnerLabel(); dom.sidebarContent.innerHTML = sortControl() + areaEntries .map(({ area, metrics }, index) => { const areaKey = getAreaKey(area); const selectedClass = state.selectedAreaKey === areaKey ? " a" : ""; return ( `
` + `
#${index + 1}${escapeHtml( area.displayName )}
${escapeHtml(areaBadgeLabel(area))}
` + `
${escapeHtml(areaSecondaryLine(area))}
` + `
Rp ${escapeHtml(formatCompactCurrency(metrics.totalPotentialWaste))} · ${escapeHtml( formatNumber(metrics.totalPriorityPackages) )} prioritas
` + `
` + `
Total Paket: ${escapeHtml( formatNumber(metrics.totalPackages) )}
Pemilik: ${escapeHtml(ownerLabel)}
` + `
${escapeHtml(areaOwnerSummary(area))}
` + `
Pagu Teraudit${escapeHtml( `Rp ${formatCompactCurrency(metrics.totalBudget)}` )}
` + `
` ); }) .join(""); } function featureStyle(feature) { const areaKey = getFeatureAreaKey(feature); const area = getActiveAreaByKey(areaKey); const visible = areaMatchesCurrentView(area); const selected = state.selectedAreaKey === areaKey; return { fillColor: area ? getLegendColor(area.totalPotentialWaste) : "#243155", fillOpacity: selected ? 0.72 : visible ? 0.52 : 0.08, color: selected ? "#f0d8a8" : "rgba(181,168,130,0.2)", weight: selected ? 2.1 : 0.8, opacity: visible ? 0.85 : 0.2, }; } function popupHtml(area) { if (!area) { return `
Belum ada data
`; } if (isProvinceView()) { return ( `
${escapeHtml(area.displayName)}
` + `` + `
Potensi PemborosanRp ${escapeHtml( formatCompactCurrency(area.totalPotentialWaste) )}
` + `
Paket Prioritas${escapeHtml( formatNumber(area.totalPriorityPackages) )}
` + `
Total Paket${escapeHtml( formatNumber(area.totalPackages) )}
` + `
Total Pagu${escapeHtml( formatCompactCurrency(area.totalBudget) )}
` + `
Severity High${escapeHtml( formatNumber(area.severityCounts.high) )}
` + `
` ); } return ( `
${escapeHtml(area.displayName)}
` + `` + `
Potensi PemborosanRp ${escapeHtml( formatCompactCurrency(area.totalPotentialWaste) )}
` + `
Paket Prioritas${escapeHtml( formatNumber(area.totalPriorityPackages) )}
` + `
Total Paket${escapeHtml( formatNumber(area.totalPackages) )}
` + `
Kementerian/Lembaga${escapeHtml( formatNumber(ownerTypeCount(area, "central")) )}
` + `
Pemprov${escapeHtml( formatNumber(ownerTypeCount(area, "provinsi")) )}
` + `
Pemkot${escapeHtml( formatNumber(ownerTypeCount(area, "kabkota")) )}
` + `
Others${escapeHtml( formatNumber(ownerTypeCount(area, "other")) )}
` + `
` ); } function ensureMap() { if (map) { return; } map = L.map(dom.mapRoot, { center: [-2.5, 118], zoom: 5, minZoom: 4, maxZoom: 12, attributionControl: false, }); L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png", { subdomains: "abcd", maxZoom: 19, }).addTo(map); L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_only_labels/{z}/{x}/{y}{r}.png", { subdomains: "abcd", maxZoom: 19, opacity: 0.35, }).addTo(map); } function renderGeoLayer(fitToBounds) { ensureMap(); if (geoLayer) { geoLayer.remove(); geoLayer = null; } const geo = getActiveGeo(); if (!geo || !Array.isArray(geo.features) || !geo.features.length) { setMapStatus("Tidak ada geometri untuk mode peta saat ini.", true); return; } geoLayer = L.geoJSON(geo, { style: featureStyle, onEachFeature: (feature, layer) => { const areaKey = getFeatureAreaKey(feature); const area = getActiveAreaByKey(areaKey); layer.bindPopup(popupHtml(area), { maxWidth: 320 }); layer.on({ mouseover: (event) => { if (!area) { return; } event.target.setStyle({ weight: 1.8, color: "#f0d8a8", fillOpacity: Math.min(featureStyle(feature).fillOpacity + 0.16, 0.85), }); event.target.bringToFront(); }, mouseout: (event) => { geoLayer.resetStyle(event.target); }, click: () => { openAreaModal(areaKey); }, }); }, }).addTo(map); const bounds = geoLayer.getBounds(); if (fitToBounds && bounds.isValid()) { map.fitBounds(bounds.pad(isProvinceView() ? 0.08 : 0.05)); } clearMapStatus(); } function initMap() { renderGeoLayer(true); } function refreshMapStyles() { if (geoLayer) { geoLayer.setStyle(featureStyle); } } function renderPackageTableRows(items) { return items.length ? items .map((item) => { const packageUrl = buildInaprocUrl(item.sourceId); return ( `` + `${escapeHtml(String(item.sourceId || item.id))}` + `${escapeHtml(item.packageName)}` + `
${escapeHtml(item.ownerName)}
${escapeHtml( ownerTypeLabel(item.ownerType) )}
` + `
${escapeHtml(item.satker || "-")}
${escapeHtml( item.locationRaw || "-" )}
` + `${escapeHtml(item.budget === null ? "-" : formatCurrencyLong(item.budget))}` + `${escapeHtml( severityLabel(item.audit.severity) )}` + `${escapeHtml(item.audit.reason || "-")}` + `` ); }) .join("") : `Tidak ada paket untuk filter saat ini.`; } function renderPagination(pagination) { return ( `
Halaman ${escapeHtml(formatNumber(pagination.page))} / ${escapeHtml( formatNumber(pagination.totalPages) )} · ${escapeHtml(formatNumber(pagination.totalItems))} paket
` ); } function renderRegionModalContent(payload) { const region = payload.region; const rowsHtml = renderPackageTableRows(payload.items); dom.modalTop.innerHTML = `` + ``; dom.modalBody.innerHTML = `` + `` + `` + `${rowsHtml}
IDNama PaketPemilikSatker / LokasiPaguSeverityAlasan
` + renderPagination(payload.pagination); } function renderProvinceModalContent(payload) { const province = payload.province; const rowsHtml = renderPackageTableRows(payload.items); dom.modalTop.innerHTML = `` + ``; dom.modalBody.innerHTML = `` + `` + `` + `${rowsHtml}
IDNama PaketPemilikSatker / LokasiPaguSeverityAlasan
` + renderPagination(payload.pagination); } function renderOwnerModalContent(payload) { const owner = payload.owner; const rowsHtml = renderPackageTableRows(payload.items); dom.modalTop.innerHTML = `` + ``; dom.modalBody.innerHTML = `` + `` + `` + `${rowsHtml}
IDNama PaketPemilikSatker / LokasiPaguSeverityAlasan
` + renderPagination(payload.pagination); } function renderModalContent(payload) { if (state.modal.areaType === "owner") { renderOwnerModalContent(payload); return; } if (state.modal.areaType === "province") { renderProvinceModalContent(payload); return; } renderRegionModalContent(payload); } async function loadAreaPackages() { if ( (state.modal.areaType === "owner" && (!state.modal.ownerType || !state.modal.ownerName)) || (state.modal.areaType !== "owner" && !state.modal.areaKey) ) { return; } state.modalRequestId += 1; const requestId = state.modalRequestId; renderModalState( state.modal.areaType === "owner" ? "Memuat pemilik..." : "Memuat area...", state.modal.areaType === "owner" ? "Mengambil paket dari pemilik terpilih..." : "Mengambil paket dari backend audit...", false ); const params = new URLSearchParams({ page: String(state.modal.page), pageSize: String(state.modal.pageSize), }); if (state.modal.search) { params.set("search", state.modal.search); } if (state.modal.areaType === "region" && state.modal.ownerType) { params.set("ownerType", state.modal.ownerType); } if (state.modal.severity) { params.set("severity", state.modal.severity); } if (state.modal.priorityOnly) { params.set("priorityOnly", "true"); } const path = state.modal.areaType === "owner" ? (() => { params.set("ownerType", state.modal.ownerType); params.set("ownerName", state.modal.ownerName); return `/owners/packages?${params.toString()}`; })() : state.modal.areaType === "province" ? `/provinces/${encodeURIComponent(state.modal.areaKey)}/packages?${params.toString()}` : `/regions/${encodeURIComponent(state.modal.areaKey)}/packages?${params.toString()}`; try { const payload = await fetchJson(path); if (requestId !== state.modalRequestId) { return; } renderModalContent(payload); } catch (error) { if (requestId !== state.modalRequestId) { return; } renderModalState("Gagal memuat paket", formatFetchError(error), true); } } function openAreaModal(areaKey) { state.selectedAreaKey = areaKey; state.selectedOwnerKey = null; state.modal = { areaType: currentAreaType(), areaKey, ownerName: "", page: 1, pageSize: 25, search: "", ownerType: "", severity: "", priorityOnly: false, }; refreshMapStyles(); renderSidebarContent(); dom.modal.classList.add("open"); document.body.style.overflow = "hidden"; loadAreaPackages(); } function openOwnerModal(ownerName, ownerType) { state.selectedAreaKey = null; state.selectedOwnerKey = getOwnerCardKey(ownerType, ownerName); state.modal = { areaType: "owner", areaKey: null, ownerName, page: 1, pageSize: 25, search: "", ownerType, severity: "", priorityOnly: false, }; refreshMapStyles(); renderSidebarContent(); dom.modal.classList.add("open"); document.body.style.overflow = "hidden"; loadAreaPackages(); } function closeRegionModal() { state.modalRequestId += 1; state.modal = { areaType: currentAreaType(), areaKey: null, ownerName: "", page: 1, pageSize: 25, search: "", ownerType: "", severity: "", priorityOnly: false, }; dom.modal.classList.remove("open"); document.body.style.overflow = ""; } function setSearch(value) { state.search = value; renderSidebarContent(); } function setSort(value) { state.sortBy = value; renderSidebarContent(); } function setTab(value) { if (isProvinceView() || isCentralOwnerMode()) { state.tab = "all"; renderTabs(); return; } state.tab = value; refreshMapStyles(); renderTabs(); renderSidebarContent(); } function setMapFilter(value) { const wasProvinceView = isProvinceView(); const wasCentralOwnerMode = isCentralOwnerMode(); state.mapFilter = value; const viewChanged = wasProvinceView !== isProvinceView(); const centralOwnerModeChanged = wasCentralOwnerMode !== isCentralOwnerMode(); if (viewChanged) { state.tab = "all"; state.selectedAreaKey = null; state.selectedOwnerKey = null; closeRegionModal(); renderLegend(); renderFilterChips(); renderTabs(); renderSidebarContent(); renderGeoLayer(true); return; } if (centralOwnerModeChanged) { state.tab = "all"; state.selectedAreaKey = null; state.selectedOwnerKey = null; if (state.modal.areaType === "owner" && !isCentralOwnerMode()) { closeRegionModal(); } } refreshMapStyles(); renderFilterChips(); renderTabs(); renderSidebarContent(); } function setModalSearch(value) { state.modal.search = value; state.modal.page = 1; loadAreaPackages(); } function setModalOwnerType(value) { if (state.modal.areaType === "province" || state.modal.areaType === "owner") { return; } state.modal.ownerType = value; state.modal.page = 1; loadAreaPackages(); } function setModalSeverity(value) { state.modal.severity = value; state.modal.page = 1; loadAreaPackages(); } function setModalPriorityOnly(value) { state.modal.priorityOnly = Boolean(value); state.modal.page = 1; loadAreaPackages(); } function changeModalPage(page) { state.modal.page = page; loadAreaPackages(); } function openPackageDetail(sourceId) { const url = buildInaprocUrl(sourceId); if (!url) { return; } window.open(url, "_blank", "noopener,noreferrer"); } function handlePackageRowKeydown(event, sourceId) { if (!event) { return; } if (event.key !== "Enter" && event.key !== " " && event.key !== "Spacebar") { return; } event.preventDefault(); openPackageDetail(sourceId); } function bindEvents() { document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeRegionModal(); } }); dom.modal.addEventListener("click", (event) => { if (event.target === dom.modal) { closeRegionModal(); } }); } async function bootstrap() { renderBootstrapLoading(); try { dashboardData = normalizeDashboardData(await fetchJson("/bootstrap")); regionsByKey = new Map(dashboardData.regions.map((region) => [region.regionKey, region])); provincesByKey = new Map(dashboardData.provinceView.provinces.map((province) => [province.provinceKey, province])); renderKpis(); renderLegend(); initMap(); renderFilterChips(); renderTabs(); renderSidebarContent(); } catch (error) { renderBootstrapError(formatFetchError(error)); } } window.dashboardActions = { changeModalPage, closeRegionModal, handlePackageRowKeydown, openAreaModal, openOwnerModal, openPackageDetail, setMapFilter, setModalOwnerType, setModalPriorityOnly, setModalSearch, setModalSeverity, setSearch, setSort, setTab, }; bindEvents(); bootstrap(); })();