Initial WebGIS portal project
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: #172033;
|
||||
background: #eef2f7;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.guide-page {
|
||||
width: min(1040px, calc(100% - 32px));
|
||||
margin: 28px auto;
|
||||
background: #ffffff;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 18px 42px rgba(15, 23, 42, 0.13);
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.guide-hero {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
border-bottom: 2px solid #1f2937;
|
||||
padding-bottom: 22px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 8px;
|
||||
color: #4f46e5;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 14px;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-top: 26px;
|
||||
}
|
||||
|
||||
.lead {
|
||||
max-width: 720px;
|
||||
color: #475467;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.print-btn {
|
||||
align-self: flex-start;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: #4f46e5;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-weight: 800;
|
||||
padding: 11px 15px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.access-panel {
|
||||
border: 1px solid #d8dee9;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.info-grid div,
|
||||
.legend-grid div {
|
||||
border: 1px solid #d8dee9;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.info-grid span {
|
||||
display: block;
|
||||
color: #667085;
|
||||
font-size: 0.82rem;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.info-grid strong {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.note {
|
||||
color: #667085;
|
||||
font-size: 0.9rem;
|
||||
margin: 12px 0 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #d8dee9;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #eef2ff;
|
||||
border-radius: 6px;
|
||||
color: #3730a3;
|
||||
font-weight: 700;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.steps ol {
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.steps li,
|
||||
ul li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.legend-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.legend-grid strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.legend-grid p {
|
||||
color: #667085;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dot {
|
||||
display: inline-block;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dot.worship {
|
||||
background: #4f46e5;
|
||||
}
|
||||
|
||||
.dot.reached {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.dot.outside {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.guide-hero,
|
||||
.info-grid,
|
||||
.legend-grid {
|
||||
grid-template-columns: 1fr;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.guide-page {
|
||||
padding: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.guide-page {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.print-btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
:root {
|
||||
--primary: #4f46e5;
|
||||
--primary-hover: #4338ca;
|
||||
--dark: #1e293b;
|
||||
--gray: #64748b;
|
||||
--gray-light: #e2e8f0;
|
||||
--light: #f8fafc;
|
||||
--white: #ffffff;
|
||||
--danger: #ef4444;
|
||||
--shadow-lg: 0 18px 40px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--dark);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(79, 70, 229, 0.16), transparent 34%),
|
||||
linear-gradient(135deg, #f8fafc 0%, #eef2ff 52%, #f8fafc 100%);
|
||||
}
|
||||
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
width: min(100%, 430px);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(226, 232, 240, 0.9);
|
||||
border-radius: 18px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 34px;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 16px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--white);
|
||||
background: var(--primary);
|
||||
font-size: 1.45rem;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
color: var(--primary);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--gray);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
color: #991b1b;
|
||||
background-color: #fee2e2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 18px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-group span {
|
||||
color: var(--dark);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.input-shell {
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border: 1px solid var(--gray-light);
|
||||
border-radius: 12px;
|
||||
background: var(--light);
|
||||
padding: 0 14px;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.input-shell:focus-within {
|
||||
background: var(--white);
|
||||
border-color: rgba(79, 70, 229, 0.45);
|
||||
box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.1);
|
||||
}
|
||||
|
||||
.input-shell i {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.input-shell input {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: var(--dark);
|
||||
font-size: 0.96rem;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
height: 48px;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
background: var(--primary);
|
||||
color: var(--white);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 0.98rem;
|
||||
font-weight: 800;
|
||||
margin-top: 4px;
|
||||
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.login-hint {
|
||||
color: var(--gray);
|
||||
font-size: 0.86rem;
|
||||
margin-top: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.login-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
padding: 26px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: #172033;
|
||||
background: #eef2f7;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.report-page {
|
||||
width: min(1180px, calc(100% - 32px));
|
||||
margin: 24px auto;
|
||||
background: #fff;
|
||||
padding: 28px;
|
||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.report-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
border-bottom: 2px solid #1f2937;
|
||||
padding-bottom: 18px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 6px;
|
||||
color: #4f46e5;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.print-btn {
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: #4f46e5;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.summary-grid div {
|
||||
border: 1px solid #d8dee9;
|
||||
border-left: 4px solid #4f46e5;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.summary-grid span {
|
||||
display: block;
|
||||
color: #667085;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.summary-grid strong {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.report-note {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #d8dee9;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #d8dee9;
|
||||
padding: 8px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f1f5f9;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
small {
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #667085;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.report-page {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.print-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
table {
|
||||
page-break-inside: auto;
|
||||
}
|
||||
|
||||
tr {
|
||||
page-break-inside: avoid;
|
||||
page-break-after: auto;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,793 @@
|
||||
// Globals
|
||||
let map;
|
||||
let layerRI, layerPM, layerRadius;
|
||||
let riData = [];
|
||||
let pmData = [];
|
||||
let pickingMode = null; // 'ri' or 'pm'
|
||||
let tempMarker = null;
|
||||
let currentView = 'dashboard';
|
||||
let searchTerm = '';
|
||||
|
||||
const rumahIbadahIconConfig = {
|
||||
Masjid: { icon: 'fa-mosque', className: 'mosque' },
|
||||
Gereja: { icon: 'fa-church', className: 'church' },
|
||||
Pura: { icon: 'fa-gopuram', className: 'temple' },
|
||||
Vihara: { icon: 'fa-vihara', className: 'vihara' },
|
||||
Klenteng: { icon: 'fa-torii-gate', className: 'klenteng' },
|
||||
Lainnya: { icon: 'fa-place-of-worship', className: 'other' }
|
||||
};
|
||||
|
||||
function getRumahIbadahIcon(jenis) {
|
||||
const config = rumahIbadahIconConfig[jenis] || rumahIbadahIconConfig.Lainnya;
|
||||
|
||||
return L.divIcon({
|
||||
className: 'ri-div-icon',
|
||||
html: `<div class="ri-marker ri-${config.className}" title="${jenis || 'Rumah Ibadah'}"><i class="fas ${config.icon}"></i></div>`,
|
||||
iconSize: [36, 46],
|
||||
iconAnchor: [18, 43],
|
||||
popupAnchor: [0, -43]
|
||||
});
|
||||
}
|
||||
|
||||
// PM Icons are now circleMarkers generated inline in renderPM
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await initMap();
|
||||
initUI();
|
||||
loadData();
|
||||
});
|
||||
|
||||
async function initMap() {
|
||||
map = L.map('map', {
|
||||
zoomControl: false,
|
||||
preferCanvas: true
|
||||
}).setView([-0.060547, 109.344902], 13);
|
||||
window.map = map;
|
||||
|
||||
L.control.zoom({ position: 'bottomright' }).addTo(map);
|
||||
|
||||
const tileOptions = {
|
||||
keepBuffer: 6,
|
||||
updateWhenIdle: false,
|
||||
updateWhenZooming: false,
|
||||
updateInterval: 100
|
||||
};
|
||||
|
||||
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
...tileOptions,
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
});
|
||||
|
||||
const satelit = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
...tileOptions,
|
||||
attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
|
||||
});
|
||||
|
||||
const darkMap = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
...tileOptions,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
subdomains: 'abcd',
|
||||
maxZoom: 20
|
||||
});
|
||||
|
||||
osm.addTo(map);
|
||||
|
||||
const baseMaps = {
|
||||
"OSM Standard": osm,
|
||||
"Citra Satelit": satelit,
|
||||
"Dark Mode": darkMap
|
||||
};
|
||||
|
||||
layerRadius = L.featureGroup().addTo(map);
|
||||
layerRI = L.featureGroup().addTo(map);
|
||||
layerPM = L.featureGroup().addTo(map);
|
||||
|
||||
const overlays = {
|
||||
"Radius Layanan": layerRadius,
|
||||
"Rumah Ibadah": layerRI,
|
||||
"Penduduk Miskin": layerPM
|
||||
};
|
||||
|
||||
L.control.layers(baseMaps, overlays, { position: 'topright' }).addTo(map);
|
||||
|
||||
// Map Click for Picking Location
|
||||
map.on('click', async (e) => {
|
||||
if (!pickingMode) return;
|
||||
|
||||
const lat = e.latlng.lat;
|
||||
const lng = e.latlng.lng;
|
||||
|
||||
if (tempMarker) map.removeLayer(tempMarker);
|
||||
tempMarker = L.marker([lat, lng]).addTo(map);
|
||||
|
||||
if (pickingMode === 'ri') {
|
||||
document.getElementById('ri-lat').value = lat.toFixed(6);
|
||||
document.getElementById('ri-lng').value = lng.toFixed(6);
|
||||
document.getElementById('ri-alamat').value = 'Mencari alamat...';
|
||||
document.getElementById('ri-alamat').value = await reverseGeocode(lat, lng);
|
||||
document.getElementById('modal-ri').classList.add('show');
|
||||
} else if (pickingMode === 'pm') {
|
||||
document.getElementById('pm-lat').value = lat.toFixed(6);
|
||||
document.getElementById('pm-lng').value = lng.toFixed(6);
|
||||
document.getElementById('pm-alamat').value = 'Mencari alamat...';
|
||||
document.getElementById('pm-alamat').value = await reverseGeocode(lat, lng);
|
||||
document.getElementById('modal-pm').classList.add('show');
|
||||
}
|
||||
|
||||
pickingMode = null;
|
||||
if(tempMarker) map.removeLayer(tempMarker);
|
||||
});
|
||||
}
|
||||
|
||||
function initUI() {
|
||||
// Sidebar Toggle
|
||||
document.getElementById('toggle-sidebar').addEventListener('click', () => {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (window.innerWidth <= 768) {
|
||||
sidebar.classList.toggle('open');
|
||||
} else {
|
||||
sidebar.classList.toggle('collapsed');
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.menu-item[data-view]').forEach(item => {
|
||||
item.addEventListener('click', () => switchView(item.dataset.view));
|
||||
});
|
||||
|
||||
// Menus
|
||||
const openAddRI = () => {
|
||||
switchView('map');
|
||||
document.getElementById('form-ri').reset();
|
||||
document.getElementById('ri-id').value = '';
|
||||
document.getElementById('modal-ri-title').innerText = 'Tambah Rumah Ibadah';
|
||||
document.getElementById('modal-ri').classList.add('show');
|
||||
};
|
||||
document.getElementById('menu-add-ri').addEventListener('click', openAddRI);
|
||||
document.getElementById('fab-add-ri')?.addEventListener('click', openAddRI);
|
||||
|
||||
const openAddPM = () => {
|
||||
switchView('map');
|
||||
document.getElementById('form-pm').reset();
|
||||
document.getElementById('pm-id').value = '';
|
||||
document.getElementById('anggota-container').innerHTML = '';
|
||||
document.getElementById('bantuan-fields').style.display = 'none';
|
||||
document.getElementById('modal-pm-title').innerText = 'Tambah Penduduk Miskin';
|
||||
document.getElementById('modal-pm').classList.add('show');
|
||||
};
|
||||
document.getElementById('menu-add-pm').addEventListener('click', openAddPM);
|
||||
document.getElementById('fab-add-pm')?.addEventListener('click', openAddPM);
|
||||
document.getElementById('dashboard-add-ri')?.addEventListener('click', openAddRI);
|
||||
document.getElementById('dashboard-add-pm')?.addEventListener('click', openAddPM);
|
||||
document.getElementById('dashboard-open-map')?.addEventListener('click', () => switchView('map'));
|
||||
|
||||
// Filters
|
||||
document.getElementById('filter-status').addEventListener('change', renderMapLayers);
|
||||
document.getElementById('filter-jenis').addEventListener('change', renderMapLayers);
|
||||
document.getElementById('filter-kebutuhan').addEventListener('change', renderPM);
|
||||
|
||||
// Search
|
||||
document.getElementById('search-input').addEventListener('input', () => {
|
||||
searchTerm = getSearchTerm();
|
||||
renderMapLayers();
|
||||
});
|
||||
document.getElementById('search-input').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
applyMapSearch(true);
|
||||
}
|
||||
});
|
||||
document.getElementById('search-button').addEventListener('click', () => applyMapSearch(true));
|
||||
|
||||
// Location
|
||||
document.getElementById('btn-my-location').addEventListener('click', () => {
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const lat = position.coords.latitude;
|
||||
const lng = position.coords.longitude;
|
||||
map.flyTo([lat, lng], 16);
|
||||
L.marker([lat, lng]).addTo(map).bindPopup('Lokasi Anda').openPopup();
|
||||
},
|
||||
(err) => alert('Gagal mendapatkan lokasi: ' + err.message)
|
||||
);
|
||||
} else {
|
||||
alert('Geolocation tidak didukung browser ini.');
|
||||
}
|
||||
});
|
||||
|
||||
switchView(currentView);
|
||||
}
|
||||
|
||||
function switchView(view) {
|
||||
currentView = view;
|
||||
const mainContent = document.querySelector('.main-content');
|
||||
|
||||
document.querySelectorAll('.view-panel').forEach(panel => {
|
||||
panel.classList.toggle('active', panel.id === `${view}-view`);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.menu-item[data-view]').forEach(item => {
|
||||
item.classList.toggle('active', item.dataset.view === view);
|
||||
});
|
||||
|
||||
mainContent?.classList.toggle('map-mode', view === 'map');
|
||||
mainContent?.classList.toggle('dashboard-mode', view !== 'map');
|
||||
document.querySelector('.search-bar')?.classList.toggle('hidden', view !== 'map');
|
||||
document.querySelector('.map-header-controls')?.classList.toggle('hidden', view !== 'map');
|
||||
|
||||
if (window.innerWidth <= 768) {
|
||||
document.getElementById('sidebar').classList.remove('open');
|
||||
}
|
||||
|
||||
if (view === 'map' && map) {
|
||||
refreshMapSize();
|
||||
}
|
||||
}
|
||||
|
||||
function refreshMapSize() {
|
||||
[0, 100, 300].forEach(delay => {
|
||||
setTimeout(() => map.invalidateSize({ pan: false }), delay);
|
||||
});
|
||||
}
|
||||
|
||||
function getSearchTerm() {
|
||||
return document.getElementById('search-input').value.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function textIncludes(value, term) {
|
||||
return String(value || '').toLowerCase().includes(term);
|
||||
}
|
||||
|
||||
function matchesRI(ri) {
|
||||
if (!searchTerm) return true;
|
||||
|
||||
return [
|
||||
ri.nama,
|
||||
ri.jenis,
|
||||
ri.kontak,
|
||||
ri.alamat
|
||||
].some(value => textIncludes(value, searchTerm));
|
||||
}
|
||||
|
||||
function matchesPM(pm) {
|
||||
if (!searchTerm) return true;
|
||||
|
||||
const anggotaMatch = Array.isArray(pm.anggota) && pm.anggota.some(anggota => [
|
||||
anggota.nama,
|
||||
anggota.hubungan,
|
||||
anggota.pekerjaan
|
||||
].some(value => textIncludes(value, searchTerm)));
|
||||
|
||||
return anggotaMatch || [
|
||||
pm.kk_nama,
|
||||
pm.kk_nik,
|
||||
pm.alamat,
|
||||
pm.status_bantuan,
|
||||
pm.jenis_bantuan,
|
||||
pm.kategori_kebutuhan,
|
||||
pm.deskripsi_kebutuhan,
|
||||
pm.tindak_lanjut,
|
||||
pm.kondisi_rumah,
|
||||
pm.rumah_ibadah_nama
|
||||
].some(value => textIncludes(value, searchTerm));
|
||||
}
|
||||
|
||||
function getFilteredRI() {
|
||||
const filterJenis = document.getElementById('filter-jenis').value;
|
||||
|
||||
return riData.filter(ri => {
|
||||
if (filterJenis !== 'all' && ri.jenis !== filterJenis) return false;
|
||||
return matchesRI(ri);
|
||||
});
|
||||
}
|
||||
|
||||
function getFilteredPM() {
|
||||
const filterStatus = document.getElementById('filter-status').value;
|
||||
const filterKebutuhan = document.getElementById('filter-kebutuhan').value;
|
||||
|
||||
return pmData.filter(pm => {
|
||||
if (filterStatus !== 'all' && pm.status_bantuan !== filterStatus) return false;
|
||||
if (filterKebutuhan !== 'all' && pm.kategori_kebutuhan !== filterKebutuhan) return false;
|
||||
return matchesPM(pm);
|
||||
});
|
||||
}
|
||||
|
||||
function renderMapLayers() {
|
||||
renderRI();
|
||||
renderPM();
|
||||
}
|
||||
|
||||
function applyMapSearch(focusResult = false) {
|
||||
searchTerm = getSearchTerm();
|
||||
renderMapLayers();
|
||||
|
||||
if (focusResult) {
|
||||
switchView('map');
|
||||
focusSearchResults();
|
||||
}
|
||||
}
|
||||
|
||||
function focusSearchResults() {
|
||||
const results = [
|
||||
...getFilteredRI().map(item => [parseFloat(item.lat), parseFloat(item.lng)]),
|
||||
...getFilteredPM().map(item => [parseFloat(item.lat), parseFloat(item.lng)])
|
||||
].filter(([lat, lng]) => Number.isFinite(lat) && Number.isFinite(lng));
|
||||
|
||||
if (!searchTerm || results.length === 0) {
|
||||
alert(searchTerm ? 'Data tidak ditemukan di peta.' : 'Masukkan kata kunci pencarian.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.length === 1) {
|
||||
map.flyTo(results[0], 16);
|
||||
return;
|
||||
}
|
||||
|
||||
map.fitBounds(L.latLngBounds(results), { padding: [40, 40], maxZoom: 16 });
|
||||
}
|
||||
|
||||
function pickLocation(mode) {
|
||||
switchView('map');
|
||||
pickingMode = mode;
|
||||
closeModal('modal-' + mode);
|
||||
}
|
||||
|
||||
function closeModal(id) {
|
||||
document.getElementById(id).classList.remove('show');
|
||||
}
|
||||
|
||||
async function reverseGeocode(lat, lng) {
|
||||
try {
|
||||
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`);
|
||||
const data = await res.json();
|
||||
return data.display_name || 'Alamat tidak ditemukan';
|
||||
} catch (e) {
|
||||
return 'Gagal memuat alamat';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleBantuanFields() {
|
||||
const status = document.getElementById('pm-status').value;
|
||||
document.getElementById('bantuan-fields').style.display = status === 'Sudah dibantu' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function addAnggotaField(data = null) {
|
||||
const container = document.getElementById('anggota-container');
|
||||
const id = Date.now() + Math.floor(Math.random() * 100);
|
||||
const html = `
|
||||
<div class="dynamic-item" id="anggota-${id}">
|
||||
<button type="button" class="btn-remove" onclick="document.getElementById('anggota-${id}').remove()">X</button>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<div class="form-group">
|
||||
<label>Nama</label>
|
||||
<input type="text" class="form-control a-nama" value="${data ? data.nama : ''}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Hubungan</label>
|
||||
<input type="text" class="form-control a-hubungan" value="${data ? data.hubungan : ''}" placeholder="Anak/Istri/dll" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Umur</label>
|
||||
<input type="number" class="form-control a-umur" value="${data ? data.umur : ''}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Pekerjaan</label>
|
||||
<input type="text" class="form-control a-pekerjaan" value="${data && data.pekerjaan ? data.pekerjaan : ''}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
// Data Fetching and Rendering
|
||||
async function loadData() {
|
||||
await loadStats();
|
||||
await fetchRI();
|
||||
await fetchPM();
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const res = await fetch('api/dashboard_stats.php');
|
||||
const json = await res.json();
|
||||
if(json.success) {
|
||||
document.getElementById('stat-ri').innerText = json.data.total_rumah_ibadah;
|
||||
document.getElementById('stat-pm').innerText = json.data.total_penduduk;
|
||||
document.getElementById('stat-sudah').innerText = json.data.sudah_dibantu;
|
||||
document.getElementById('stat-belum').innerText = json.data.belum_dibantu;
|
||||
document.getElementById('stat-menunggu').innerText = json.data.menunggu_verifikasi || 0;
|
||||
document.getElementById('stat-prioritas').innerText = json.data.prioritas_tinggi || 0;
|
||||
document.getElementById('stat-luar-radius').innerText = json.data.luar_radius || 0;
|
||||
document.getElementById('summary-sudah').innerText = json.data.sudah_dibantu;
|
||||
document.getElementById('summary-belum').innerText = json.data.belum_dibantu;
|
||||
document.getElementById('summary-menunggu').innerText = json.data.menunggu_verifikasi || 0;
|
||||
renderDashboardLists(json.data);
|
||||
}
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
function renderDashboardLists(stats) {
|
||||
const priorityList = document.getElementById('priority-list');
|
||||
const needList = document.getElementById('need-list');
|
||||
|
||||
if (priorityList) {
|
||||
const items = stats.top_prioritas || [];
|
||||
priorityList.innerHTML = items.length ? items.map(item => `
|
||||
<div class="priority-item">
|
||||
<div class="priority-score">${item.skor_prioritas || 0}</div>
|
||||
<div>
|
||||
<strong>${item.kk_nama}</strong>
|
||||
<p>${item.kategori_kebutuhan || 'Kebutuhan belum diisi'} - ${item.tindak_lanjut || 'Belum disurvei'}</p>
|
||||
</div>
|
||||
</div>
|
||||
`).join('') : '<div class="need-item"><p>Belum ada data prioritas.</p></div>';
|
||||
}
|
||||
|
||||
if (needList) {
|
||||
const items = stats.kebutuhan_terbanyak || [];
|
||||
needList.innerHTML = items.length ? items.map(item => `
|
||||
<div class="need-item">
|
||||
<strong>${item.kategori}</strong>
|
||||
<p>${item.total} keluarga membutuhkan kategori ini</p>
|
||||
</div>
|
||||
`).join('') : '<div class="need-item"><p>Belum ada data kebutuhan.</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRI() {
|
||||
try {
|
||||
const res = await fetch('api/rumah_ibadah_read.php');
|
||||
const json = await res.json();
|
||||
if(json.success) {
|
||||
riData = json.data;
|
||||
renderRI();
|
||||
}
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function fetchPM() {
|
||||
try {
|
||||
const res = await fetch('api/penduduk_read.php');
|
||||
const json = await res.json();
|
||||
if(json.success) {
|
||||
pmData = json.data;
|
||||
renderPM();
|
||||
}
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
function renderRI() {
|
||||
layerRI.clearLayers();
|
||||
layerRadius.clearLayers();
|
||||
|
||||
getFilteredRI().forEach(ri => {
|
||||
const lat = parseFloat(ri.lat);
|
||||
const lng = parseFloat(ri.lng);
|
||||
const rad = parseInt(ri.radius) || 500;
|
||||
|
||||
// Circle
|
||||
L.circle([lat, lng], {
|
||||
color: '#4f46e5',
|
||||
fillColor: '#4f46e5',
|
||||
fillOpacity: 0.1,
|
||||
radius: rad,
|
||||
interactive: false
|
||||
}).addTo(layerRadius);
|
||||
|
||||
// Marker
|
||||
const marker = L.marker([lat, lng], { icon: getRumahIbadahIcon(ri.jenis) }).addTo(layerRI);
|
||||
marker.feature = ri;
|
||||
|
||||
let popup = `
|
||||
<div class="popup-header">
|
||||
<h3>${ri.nama}</h3>
|
||||
<p>${ri.jenis}</p>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<div class="popup-row"><div class="label">Kontak</div><div class="value">${ri.kontak || '-'}</div></div>
|
||||
<div class="popup-row"><div class="label">Radius</div><div class="value">${rad} m</div></div>
|
||||
<div class="popup-row" style="flex-direction: column;"><div class="label" style="width:100%">Alamat</div><div class="value" style="font-size:0.8rem; font-weight:400">${ri.alamat}</div></div>
|
||||
<div class="popup-actions">
|
||||
<button class="btn-edit" onclick='editRI(${JSON.stringify(ri)})'>Edit</button>
|
||||
<button class="btn-delete" onclick='deleteRI(${ri.id})'>Hapus</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
marker.bindPopup(popup);
|
||||
});
|
||||
|
||||
layerRadius.eachLayer(circle => circle.bringToBack());
|
||||
}
|
||||
|
||||
function renderPM() {
|
||||
layerPM.clearLayers();
|
||||
|
||||
getFilteredPM().forEach(pm => {
|
||||
const lat = parseFloat(pm.lat);
|
||||
const lng = parseFloat(pm.lng);
|
||||
const anggotaCount = pm.anggota ? pm.anggota.length : 0;
|
||||
|
||||
let badgeClass = 'badge-danger';
|
||||
if(pm.status_bantuan === 'Sudah dibantu') badgeClass = 'badge-success';
|
||||
else if(pm.status_bantuan === 'Menunggu verifikasi') badgeClass = 'badge-warning';
|
||||
|
||||
const isBlankSpot = pm.is_blank_spot == 1;
|
||||
const pmColor = isBlankSpot ? '#ef4444' : '#10b981'; // Merah (Blank Spot), Hijau (Terjangkau)
|
||||
const riwayatHtml = Array.isArray(pm.riwayat_bantuan) && pm.riwayat_bantuan.length
|
||||
? pm.riwayat_bantuan.slice(0, 3).map(r => `${r.tanggal_bantuan}: ${r.jenis_bantuan}`).join('<br>')
|
||||
: '-';
|
||||
const buktiHtml = pm.bukti_file
|
||||
? `<a href="${pm.bukti_file}" target="_blank">Lihat bukti</a>`
|
||||
: '-';
|
||||
|
||||
const marker = L.circleMarker([lat, lng], {
|
||||
radius: 7,
|
||||
fillColor: pmColor,
|
||||
color: '#ffffff',
|
||||
weight: 1.5,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.9
|
||||
}).addTo(layerPM);
|
||||
marker.feature = pm;
|
||||
|
||||
let popup = `
|
||||
<div class="popup-header ${pm.status_bantuan === 'Sudah dibantu' ? 'success' : pm.status_bantuan === 'Menunggu verifikasi' ? 'warning' : 'danger'}">
|
||||
<h3>Keluarga ${pm.kk_nama}</h3>
|
||||
<span class="badge ${badgeClass}">${pm.status_bantuan}</span>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<div class="popup-row"><div class="label">Jumlah Anggota</div><div class="value">${anggotaCount} orang</div></div>
|
||||
<div class="popup-row"><div class="label">Rumah Ibadah Terdekat</div><div class="value">${pm.rumah_ibadah_nama || '-'}</div></div>
|
||||
<div class="popup-row"><div class="label">Prioritas</div><div class="value">${pm.skor_prioritas || 0} (${pm.label_prioritas || '-'})</div></div>
|
||||
<div class="popup-row"><div class="label">Kebutuhan</div><div class="value">${pm.kategori_kebutuhan || '-'}</div></div>
|
||||
<div class="popup-row"><div class="label">Tindak Lanjut</div><div class="value">${pm.tindak_lanjut || 'Belum disurvei'}</div></div>
|
||||
<div class="popup-row"><div class="label">Jarak Lurus</div><div class="value">${pm.jarak_m ? pm.jarak_m + ' meter' : '-'} ${isBlankSpot ? '<i style="color:red">(Di luar radius)</i>' : '<i style="color:green">(Terjangkau)</i>'}</div></div>
|
||||
<div class="popup-row"><div class="label">Jarak Rute</div><div class="value">${pm.jarak_rute_m ? pm.jarak_rute_m + ' meter' : '-'}</div></div>
|
||||
${pm.status_bantuan === 'Sudah dibantu' ? `<div class="popup-row"><div class="label">Bantuan</div><div class="value">${pm.jenis_bantuan} (${pm.tanggal_bantuan})</div></div>` : ''}
|
||||
<div class="popup-row"><div class="label">Riwayat</div><div class="value">${riwayatHtml}</div></div>
|
||||
<div class="popup-row"><div class="label">Bukti</div><div class="value">${buktiHtml}</div></div>
|
||||
<div class="popup-row" style="flex-direction: column;"><div class="label" style="width:100%">Alamat</div><div class="value" style="font-size:0.8rem; font-weight:400">${pm.alamat}</div></div>
|
||||
<div class="popup-actions">
|
||||
<button class="btn-edit" onclick='editPM(${JSON.stringify(pm).replace(/'/g, "\\'")})'>Edit</button>
|
||||
<button class="btn-edit" onclick='openBantuanModal(${pm.id})'>Bantuan</button>
|
||||
<button class="btn-delete" onclick='deletePM(${pm.id})'>Hapus</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
marker.bindPopup(popup);
|
||||
});
|
||||
|
||||
layerPM.bringToFront();
|
||||
}
|
||||
|
||||
// CRUD Operations
|
||||
async function saveRI() {
|
||||
const id = document.getElementById('ri-id').value;
|
||||
const data = {
|
||||
nama: document.getElementById('ri-nama').value,
|
||||
jenis: document.getElementById('ri-jenis').value,
|
||||
kontak: document.getElementById('ri-kontak').value,
|
||||
lat: document.getElementById('ri-lat').value,
|
||||
lng: document.getElementById('ri-lng').value,
|
||||
alamat: document.getElementById('ri-alamat').value,
|
||||
radius: document.getElementById('ri-radius').value
|
||||
};
|
||||
|
||||
if(!data.nama || !data.lat) { alert("Isi form dengan lengkap"); return; }
|
||||
|
||||
const url = id ? 'api/rumah_ibadah_update.php' : 'api/rumah_ibadah_create.php';
|
||||
if(id) data.id = id;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const json = await res.json();
|
||||
if(json.success) {
|
||||
closeModal('modal-ri');
|
||||
await fetchRI();
|
||||
await fetchPM();
|
||||
await loadStats();
|
||||
} else {
|
||||
alert(json.message);
|
||||
}
|
||||
} catch(e) { alert('Error saving data'); }
|
||||
}
|
||||
|
||||
async function savePM() {
|
||||
const id = document.getElementById('pm-id').value;
|
||||
const data = new FormData();
|
||||
const anggota = [];
|
||||
|
||||
data.append('kk_nama', document.getElementById('pm-nama').value);
|
||||
data.append('kk_nik', document.getElementById('pm-nik').value);
|
||||
data.append('lat', document.getElementById('pm-lat').value);
|
||||
data.append('lng', document.getElementById('pm-lng').value);
|
||||
data.append('alamat', document.getElementById('pm-alamat').value);
|
||||
data.append('kategori_kebutuhan', document.getElementById('pm-kategori-kebutuhan').value);
|
||||
data.append('deskripsi_kebutuhan', document.getElementById('pm-deskripsi-kebutuhan').value);
|
||||
data.append('kondisi_rumah', document.getElementById('pm-kondisi-rumah').value);
|
||||
data.append('penghasilan', document.getElementById('pm-penghasilan').value);
|
||||
data.append('status_bantuan', document.getElementById('pm-status').value);
|
||||
data.append('jenis_bantuan', document.getElementById('pm-jenis-bantuan').value);
|
||||
data.append('tanggal_bantuan', document.getElementById('pm-tanggal-bantuan').value);
|
||||
data.append('tindak_lanjut', document.getElementById('pm-tindak-lanjut').value);
|
||||
data.append('catatan_survei', document.getElementById('pm-catatan-survei').value);
|
||||
data.append('existing_bukti_file', document.getElementById('pm-existing-bukti-file').value);
|
||||
|
||||
const buktiFile = document.getElementById('pm-bukti-file').files[0];
|
||||
if (buktiFile) data.append('bukti_file', buktiFile);
|
||||
|
||||
if(!data.get('kk_nama') || !data.get('lat')) { alert("Isi form dengan lengkap"); return; }
|
||||
if (data.get('penghasilan') !== '' && Number(data.get('penghasilan')) < 0) {
|
||||
alert('Penghasilan tidak boleh bernilai negatif.');
|
||||
document.getElementById('pm-penghasilan').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const items = document.querySelectorAll('.dynamic-item');
|
||||
items.forEach(el => {
|
||||
anggota.push({
|
||||
nama: el.querySelector('.a-nama').value,
|
||||
hubungan: el.querySelector('.a-hubungan').value,
|
||||
umur: el.querySelector('.a-umur').value,
|
||||
pekerjaan: el.querySelector('.a-pekerjaan').value
|
||||
});
|
||||
});
|
||||
data.append('anggota', JSON.stringify(anggota));
|
||||
|
||||
const url = id ? 'api/penduduk_update.php' : 'api/penduduk_create.php';
|
||||
if(id) data.append('id', id);
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: data
|
||||
});
|
||||
const json = await res.json();
|
||||
if(json.success) {
|
||||
closeModal('modal-pm');
|
||||
await fetchPM();
|
||||
await loadStats();
|
||||
if(json.ibadah_id) {
|
||||
const ri = riData.find(r => r.id == json.ibadah_id);
|
||||
if(ri) alert(`Warga otomatis dimasukkan ke binaan: ${ri.nama}`);
|
||||
}
|
||||
} else {
|
||||
alert(json.message);
|
||||
}
|
||||
} catch(e) { alert('Error saving data'); }
|
||||
}
|
||||
|
||||
function editRI(data) {
|
||||
document.getElementById('ri-id').value = data.id;
|
||||
document.getElementById('ri-nama').value = data.nama;
|
||||
document.getElementById('ri-jenis').value = data.jenis;
|
||||
document.getElementById('ri-kontak').value = data.kontak;
|
||||
document.getElementById('ri-lat').value = data.lat;
|
||||
document.getElementById('ri-lng').value = data.lng;
|
||||
document.getElementById('ri-alamat').value = data.alamat;
|
||||
document.getElementById('ri-radius').value = data.radius;
|
||||
document.getElementById('modal-ri-title').innerText = 'Edit Rumah Ibadah';
|
||||
document.getElementById('modal-ri').classList.add('show');
|
||||
}
|
||||
|
||||
function editPM(data) {
|
||||
document.getElementById('pm-id').value = data.id;
|
||||
document.getElementById('pm-nama').value = data.kk_nama;
|
||||
document.getElementById('pm-nik').value = data.kk_nik;
|
||||
document.getElementById('pm-lat').value = data.lat;
|
||||
document.getElementById('pm-lng').value = data.lng;
|
||||
document.getElementById('pm-alamat').value = data.alamat;
|
||||
document.getElementById('pm-kategori-kebutuhan').value = data.kategori_kebutuhan || '';
|
||||
document.getElementById('pm-deskripsi-kebutuhan').value = data.deskripsi_kebutuhan || '';
|
||||
document.getElementById('pm-kondisi-rumah').value = data.kondisi_rumah || '';
|
||||
document.getElementById('pm-penghasilan').value = data.penghasilan || '';
|
||||
document.getElementById('pm-status').value = data.status_bantuan;
|
||||
document.getElementById('pm-jenis-bantuan').value = data.jenis_bantuan;
|
||||
document.getElementById('pm-tanggal-bantuan').value = data.tanggal_bantuan;
|
||||
document.getElementById('pm-tindak-lanjut').value = data.tindak_lanjut || 'Belum disurvei';
|
||||
document.getElementById('pm-catatan-survei').value = data.catatan_survei || '';
|
||||
document.getElementById('pm-existing-bukti-file').value = data.bukti_file || '';
|
||||
document.getElementById('pm-bukti-file').value = '';
|
||||
|
||||
toggleBantuanFields();
|
||||
|
||||
document.getElementById('anggota-container').innerHTML = '';
|
||||
if(data.anggota && data.anggota.length > 0) {
|
||||
data.anggota.forEach(a => addAnggotaField(a));
|
||||
}
|
||||
|
||||
document.getElementById('modal-pm-title').innerText = 'Edit Penduduk Miskin';
|
||||
document.getElementById('modal-pm').classList.add('show');
|
||||
}
|
||||
|
||||
function setBantuanAlert(message = '', type = 'error') {
|
||||
const alertBox = document.getElementById('bantuan-alert');
|
||||
if (!alertBox) return;
|
||||
|
||||
alertBox.textContent = message;
|
||||
alertBox.hidden = !message;
|
||||
alertBox.classList.toggle('success', type === 'success');
|
||||
}
|
||||
|
||||
function openBantuanModal(id) {
|
||||
const penerima = pmData.find(pm => String(pm.id) === String(id));
|
||||
const form = document.getElementById('form-bantuan');
|
||||
|
||||
form.reset();
|
||||
setBantuanAlert();
|
||||
document.getElementById('bantuan-penduduk-id').value = id;
|
||||
document.getElementById('bantuan-penerima').value = penerima ? `Keluarga ${penerima.kk_nama}` : `ID ${id}`;
|
||||
document.getElementById('bantuan-jenis').value = penerima?.kategori_kebutuhan || '';
|
||||
document.getElementById('bantuan-tanggal').value = new Date().toISOString().slice(0, 10);
|
||||
document.getElementById('bantuan-pemberi').value = 'Admin';
|
||||
document.getElementById('bantuan-bukti-file').value = '';
|
||||
document.getElementById('btn-save-bantuan').disabled = false;
|
||||
document.getElementById('modal-bantuan').classList.add('show');
|
||||
}
|
||||
|
||||
async function saveBantuan() {
|
||||
const id = document.getElementById('bantuan-penduduk-id').value;
|
||||
const jenis = document.getElementById('bantuan-jenis').value.trim();
|
||||
const tanggal = document.getElementById('bantuan-tanggal').value;
|
||||
const nilai = document.getElementById('bantuan-nilai').value;
|
||||
const pemberi = document.getElementById('bantuan-pemberi').value.trim() || 'Admin';
|
||||
const catatan = document.getElementById('bantuan-catatan').value.trim();
|
||||
const buktiFile = document.getElementById('bantuan-bukti-file').files[0];
|
||||
const saveButton = document.getElementById('btn-save-bantuan');
|
||||
|
||||
if (!id || !jenis || !tanggal) {
|
||||
setBantuanAlert('Keluarga penerima, jenis bantuan, dan tanggal wajib diisi.');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('penduduk_id', id);
|
||||
formData.append('jenis_bantuan', jenis);
|
||||
formData.append('tanggal_bantuan', tanggal);
|
||||
formData.append('nilai_bantuan', nilai || '');
|
||||
formData.append('pemberi_bantuan', pemberi);
|
||||
formData.append('catatan', catatan || '');
|
||||
if (buktiFile) formData.append('bukti_file', buktiFile);
|
||||
|
||||
try {
|
||||
saveButton.disabled = true;
|
||||
saveButton.innerText = 'Menyimpan...';
|
||||
const res = await fetch('api/bantuan_create.php', { method: 'POST', body: formData });
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
closeModal('modal-bantuan');
|
||||
await fetchPM();
|
||||
await loadStats();
|
||||
renderMapLayers();
|
||||
} else {
|
||||
setBantuanAlert(json.message || 'Gagal menambah bantuan.');
|
||||
}
|
||||
} catch(e) {
|
||||
setBantuanAlert('Terjadi kesalahan saat menyimpan bantuan.');
|
||||
} finally {
|
||||
saveButton.disabled = false;
|
||||
saveButton.innerText = 'Simpan Bantuan';
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRI(id) {
|
||||
if(!confirm('Hapus rumah ibadah ini?')) return;
|
||||
try {
|
||||
const res = await fetch('api/rumah_ibadah_delete.php', { method:'POST', body:JSON.stringify({id}) });
|
||||
const json = await res.json();
|
||||
if(json.success) { fetchRI(); fetchPM(); loadStats(); }
|
||||
} catch(e) { alert('Error'); }
|
||||
}
|
||||
|
||||
async function deletePM(id) {
|
||||
if(!confirm('Hapus penduduk miskin ini?')) return;
|
||||
try {
|
||||
const res = await fetch('api/penduduk_delete.php', { method:'POST', body:JSON.stringify({id}) });
|
||||
const json = await res.json();
|
||||
if(json.success) { fetchPM(); loadStats(); }
|
||||
} catch(e) { alert('Error'); }
|
||||
}
|
||||
Reference in New Issue
Block a user