2446 lines
154 KiB
HTML
2446 lines
154 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>WebGIS</title>
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
|
|
<style>
|
|
html,body{height:100%;margin:0;padding:0;overflow:hidden}
|
|
body{font-family:Inter,ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial;line-height:1.35;color:#1f2937;background:#f8fafc}
|
|
#app-shell{display:flex;flex-direction:column;height:100%;width:100%}
|
|
#map-wrap{flex:1;min-width:0;position:relative}
|
|
#map{height:100%;width:100%}
|
|
#data-panel{background:linear-gradient(180deg,#ffffff 0%,#f8fafc 100%);display:flex;flex-direction:column;overflow:hidden;flex:1}
|
|
#data-panel-header-mini{padding:18px 18px 14px;border-bottom:1px solid #e5e7eb;background:linear-gradient(180deg,#ffffff 0%,#f9fafb 100%)}
|
|
.data-panel-header{display:none}
|
|
.data-panel-kicker{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:#64748b;font-weight:700}
|
|
.data-panel-title{margin:4px 0 0;font-size:20px;line-height:1.1;color:#0f172a}
|
|
.data-panel-subtitle{margin:8px 0 0;color:#64748b;font-size:13px}
|
|
.data-view-toggle{display:inline-flex;gap:6px;margin-top:14px;padding:4px;background:#eef2ff;border-radius:14px}
|
|
.data-view-toggle button{border:0;background:transparent;color:#334155;padding:9px 12px;border-radius:10px;font-weight:600;cursor:pointer;transition:background .15s,color .15s,box-shadow .15s}
|
|
.data-view-toggle button.active{background:#1e40af;color:#fff;box-shadow:0 2px 8px rgba(30,64,175,0.3)}
|
|
.data-panel-summary{padding:12px 18px 0;color:#475569;font-size:13px}
|
|
.data-table-wrap{flex:1;overflow:auto;padding:14px 18px 0}
|
|
.data-table{width:100%;border-collapse:separate;border-spacing:0 10px;table-layout:fixed}
|
|
.data-table thead th{font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:#64748b;text-align:center;padding:0 8px;vertical-align:middle}
|
|
.data-table tbody tr{background:#fff;box-shadow:0 8px 24px rgba(15,23,42,0.06);border-radius:14px}
|
|
.data-table tbody td{padding:12px 8px;vertical-align:middle;text-align:center;border-top:1px solid rgba(255,255,255,0);word-wrap:break-word;overflow-wrap:anywhere}
|
|
.data-table tbody td:first-child{border-top-left-radius:14px;border-bottom-left-radius:14px;width:64px}
|
|
.data-table tbody td:last-child{border-top-right-radius:14px;border-bottom-right-radius:14px;width:118px}
|
|
.data-table thead th:first-child{width:64px}
|
|
.data-table thead th:last-child{width:118px}
|
|
.data-table thead th:nth-child(4), .data-table tbody td:nth-child(4){width:90px}
|
|
.data-table thead th:nth-child(5), .data-table tbody td:nth-child(5){width:130px}
|
|
.data-table tbody td .data-muted, .data-table tbody td .data-name, .data-table tbody td .data-pill, .data-table tbody td .data-badge{display:inline-flex;justify-content:center;align-items:center;max-width:100%}
|
|
.data-table tbody td .data-name, .data-table tbody td .data-muted{line-height:1.25}
|
|
.data-index{display:inline-flex;align-items:center;justify-content:center;min-width:28px;height:28px;border-radius:999px;background:#e2e8f0;color:#0f172a;font-weight:700;font-size:13px}
|
|
.data-name{font-weight:700;color:#0f172a}
|
|
.data-address{color:#475569;font-size:13px;margin-top:2px}
|
|
.data-muted{color:#64748b;font-size:13px}
|
|
.data-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;font-size:12px;font-weight:700;white-space:nowrap}
|
|
.data-badge.is-green{background:#dcfce7;color:#166534}
|
|
.data-badge.is-yellow{background:#fef3c7;color:#92400e}
|
|
.data-badge.is-blue{background:#dbeafe;color:#1d4ed8}
|
|
.data-pill{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;background:#f1f5f9;color:#0f172a;font-size:12px;font-weight:600;margin-right:6px;margin-bottom:6px}
|
|
.data-actions{display:flex;gap:8px;justify-content:flex-end;flex-wrap:nowrap;white-space:nowrap}
|
|
.data-action-btn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:10px;border:1px solid #e2e8f0;background:#fff;color:#0f172a;cursor:pointer;transition:transform .12s,box-shadow .12s,background .12s}
|
|
.data-action-btn:hover{transform:translateY(-1px);box-shadow:0 10px 18px rgba(15,23,42,0.08);background:#f8fafc}
|
|
.data-action-btn svg{width:16px;height:16px;display:block}
|
|
.data-empty{padding:26px 18px;color:#64748b;font-size:14px;text-align:center}
|
|
.data-pagination{padding:14px 18px 18px;border-top:1px solid #e5e7eb;background:linear-gradient(180deg,rgba(255,255,255,0.65),#fff)}
|
|
.data-pagination-inner{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}
|
|
.data-pagination-info{color:#64748b;font-size:13px}
|
|
.data-pagination-controls{display:flex;gap:6px;flex-wrap:wrap}
|
|
.data-page-btn{border:1px solid #dbe3ef;background:#fff;color:#0f172a;padding:7px 11px;border-radius:10px;cursor:pointer;font-weight:600}
|
|
.data-page-btn.active{background:#0f172a;color:#fff;border-color:#0f172a}
|
|
.data-page-btn:disabled{opacity:.45;cursor:not-allowed}
|
|
.data-page-ellipsis{display:inline-flex;align-items:center;padding:0 6px;color:#64748b}
|
|
.data-link{color:#1d4ed8;text-decoration:none;font-weight:600}
|
|
.data-link:hover{text-decoration:underline}
|
|
.table-chip-list{display:flex;flex-wrap:wrap}
|
|
@media (max-width: 980px){
|
|
body{overflow:auto}
|
|
}
|
|
/* Form element styling */
|
|
.modal-content{background:#fff;padding:18px;border-radius:10px;min-width:340px;max-width:640px;box-shadow:0 10px 30px rgba(15,23,42,0.12)}
|
|
.modal-content label{display:block;margin-top:10px;color:#111827}
|
|
.auth-field{margin-bottom:16px}
|
|
.auth-field label{display:block;color:#111827;font-weight:500;font-size:14px;margin-bottom:8px}
|
|
.auth-field input[type="text"],
|
|
.auth-field input[type="email"],
|
|
.auth-field input[type="password"]{width:100%;padding:10px 12px;border-radius:8px;border:1px solid #e5e7eb;background:#fff;box-sizing:border-box;margin:0 !important}
|
|
.modal-content input[type="text"], .modal-content input[type="number"], .modal-content input[type="date"], .modal-content input[type="search"], .modal-content select{width:100%;padding:8px 10px;border-radius:8px;border:1px solid #e5e7eb;background:#fff;margin-top:6px;box-sizing:border-box}
|
|
.modal-content textarea{width:100%;padding:8px 10px;border-radius:8px;border:1px solid #e5e7eb;background:#fff;margin-top:6px;box-sizing:border-box;min-height:84px}
|
|
.modal-content input:disabled, .modal-content select:disabled{background:#f3f4f6;color:#9ca3af;cursor:not-allowed;opacity:1}
|
|
.modal-content input[type="file"]{margin-top:6px}
|
|
.assistance-combobox{position:relative}
|
|
.assistance-combobox .assist-source-input{width:100%;padding-right:34px;appearance:none;-webkit-appearance:none;-moz-appearance:none;cursor:text;color:#111827;font-weight:400}
|
|
.assistance-combobox .assist-source-toggle{position:absolute;right:10px;top:calc(50% + 10px);transform:translateY(-50%);width:18px;height:18px;border:0;background:transparent;color:#111827;cursor:pointer;padding:0;display:flex;align-items:center;justify-content:center;pointer-events:auto}
|
|
.assistance-fields.is-disabled .assist-source-toggle{color:#9ca3af}
|
|
.assistance-fields.is-enabled .assist-source-toggle{color:#111827}
|
|
.assistance-combobox .assist-source-toggle::before{content:'';display:block;width:8px;height:8px;border-right:3px solid currentColor;border-bottom:3px solid currentColor;transform:rotate(45deg);margin-top:-2px}
|
|
.assistance-combobox .assist-source-panel{position:absolute;left:0;right:0;top:100%;margin-top:6px;background:#fff;border:1px solid #d1d5db;border-radius:10px;box-shadow:0 14px 30px rgba(15,23,42,0.14);z-index:22000;max-height:260px;overflow:auto;display:none}
|
|
.assistance-combobox .assist-source-panel.show{display:block}
|
|
.assistance-combobox .assist-source-item{padding:9px 10px;cursor:pointer;font-size:13px;line-height:1.35;border-bottom:1px solid #f3f4f6}
|
|
.assistance-combobox .assist-source-item:last-child{border-bottom:0}
|
|
.assistance-combobox .assist-source-item:hover,.assistance-combobox .assist-source-item.active{background:#f8fafc}
|
|
.assistance-combobox .assist-source-empty{padding:10px;color:#6b7280;font-size:13px}
|
|
.assistance-fields.is-disabled .assist-source-input::placeholder{color:#9ca3af;opacity:1}
|
|
.assistance-fields.is-disabled .assist-source-input::-webkit-input-placeholder{color:#9ca3af;opacity:1}
|
|
.assistance-fields.is-disabled .assist-source-input::-moz-placeholder{color:#9ca3af;opacity:1}
|
|
.assistance-fields.is-disabled .assist-source-input:-ms-input-placeholder{color:#9ca3af;opacity:1}
|
|
.assistance-fields.is-enabled .assist-source-input{color:#111827;font-weight:400}
|
|
.assistance-fields.is-enabled .assist-source-input::placeholder{color:#111827 !important;opacity:1;font-weight:400}
|
|
.assistance-fields.is-enabled .assist-source-input::-webkit-input-placeholder{color:#111827 !important;opacity:1;font-weight:400}
|
|
.assistance-fields.is-enabled .assist-source-input::-moz-placeholder{color:#111827 !important;opacity:1;font-weight:400}
|
|
.assistance-fields.is-enabled .assist-source-input:-ms-input-placeholder{color:#111827 !important;opacity:1;font-weight:400}
|
|
.modal-content .label-title{display:inline-flex; align-items:center; gap:4px}
|
|
.modal-content .required-star{color:#dc2626; font-weight:700; line-height:1}
|
|
.layer-legend{display:inline-flex;align-items:center;gap:6px;line-height:1.2}
|
|
.layer-legend .legend-marker{display:inline-flex;align-items:flex-end;justify-content:center;width:16px;height:22px;flex:0 0 16px}
|
|
.layer-legend .legend-marker svg{display:block;width:16px;height:22px}
|
|
.layer-legend .legend-text{white-space:nowrap}
|
|
.btn-primary{background:#2563eb;color:#fff;padding:8px 12px;border:0;border-radius:8px;cursor:pointer}
|
|
.btn-secondary{background:#f3f4f6;color:#111;padding:8px 12px;border:0;border-radius:8px;cursor:pointer}
|
|
.modal-actions{margin-top:14px;text-align:right}
|
|
/* file name display next to file input */
|
|
.file-name{display:inline-block;margin-left:8px;color:#374151;font-size:13px}
|
|
/* Popup buttons style */
|
|
.leaflet-popup-content button{background:#111827;color:#fff;border:0;padding:6px 8px;border-radius:6px;margin-right:6px;cursor:pointer}
|
|
.leaflet-popup-content button:hover{opacity:0.95}
|
|
/* Debug overlay subtle */
|
|
#debug-overlay{position:absolute;left:10px;bottom:10px;z-index:12000;background:rgba(255,255,255,0.95);padding:8px;border-radius:8px;font-size:13px;color:#111;border:1px solid rgba(0,0,0,0.06);display:none}
|
|
/* Prevent double-click text selection on layer control while keeping it clickable */
|
|
.leaflet-control-layers label{ -webkit-user-select:none; -moz-user-select:none; -ms-user-select:none; user-select:none; cursor:pointer }
|
|
/* Toast messages */
|
|
#toast-container{position:fixed;right:16px;bottom:16px;z-index:14000;display:flex;flex-direction:column;gap:8px;align-items:flex-end}
|
|
.toast{background:#333;color:#fff;padding:10px 14px;border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,0.3);max-width:320px;font-size:13px;opacity:0;transform:translateY(-6px);transition:opacity .18s,transform .18s}
|
|
.toast.show{opacity:1;transform:translateY(0)}
|
|
.toast.error{background:#d32f2f}
|
|
.toast.success{background:#2e7d32}
|
|
.modal {position:fixed;left:0;top:0;right:0;bottom:0;background:rgba(0,0,0,0.45);display:flex;align-items:center;justify-content:center;z-index:9999}
|
|
.modal.hidden{display:none}
|
|
.modal-content{background:#fff;padding:18px;border-radius:10px;min-width:320px;max-width:520px;width:420px;max-height:80vh;overflow:auto}
|
|
.modal-content label{display:block;margin-top:8px}
|
|
.modal-actions{margin-top:12px;text-align:right}
|
|
.btn-primary{background:#1976d2;color:#fff;padding:8px 12px;border:0;border-radius:6px;cursor:pointer}
|
|
.btn-secondary{background:#eee;padding:8px 12px;border:0;border-radius:6px;cursor:pointer}
|
|
.btn-destructive{background:#d32f2f;color:#fff;padding:8px 12px;border:0;border-radius:6px;cursor:pointer}
|
|
.btn-destructive:hover{opacity:0.95}
|
|
.leaflet-popup-content{margin:14px 16px}
|
|
.leaflet-popup-content-wrapper{border-radius:16px}
|
|
.leaflet-popup-content .location-popup{min-width:380px;max-width:520px}
|
|
.leaflet-popup-content .location-popup-title{font-size:16px;font-weight:700;color:#0f172a;margin-bottom:6px;line-height:1.25}
|
|
.leaflet-popup-content .location-popup-meta{font-size:13px;color:#475569;margin-top:6px}
|
|
.leaflet-popup-content .location-popup-photo{margin-top:8px}
|
|
.leaflet-popup-content .location-popup-actions{display:flex;align-items:center;gap:8px;flex-wrap:nowrap;white-space:nowrap;margin-top:12px;justify-content:flex-start}
|
|
.leaflet-popup-content .location-popup-actions button{margin-right:0}
|
|
#masjid-radius-control{position:absolute;left:10px;bottom:20px;background:#fff;padding:8px;border-radius:6px;box-shadow:0 2px 6px rgba(0,0,0,0.2);z-index:10000}
|
|
/* Filter panel */
|
|
/* filter panel removed (moved into masjid control) */
|
|
/* nicer masjid control */
|
|
#masjid-radius-control{width:300px;padding:12px;border-radius:10px;border:1px solid rgba(0,0,0,0.06);box-shadow:0 12px 40px rgba(2,6,23,0.12);background:linear-gradient(180deg,#ffffff,#fbfbfe)}
|
|
#masjid-radius-control .mc-title{font-weight:700;margin-bottom:6px;color:#0f172a}
|
|
#masjid-radius-control .mc-address{font-size:13px;color:#475569;margin-bottom:8px}
|
|
#masjid-radius-control .mc-controls{display:flex;align-items:center;gap:8px}
|
|
#masjid-radius-control .mc-radius{flex:1}
|
|
#masjid-radius-control .mc-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
|
|
#masjid-radius-control .mc-actions button{background:#eef2ff;color:#0f172a;border:0;padding:6px 8px;border-radius:8px;cursor:pointer}
|
|
#masjid-radius-control .mc-actions button.destructive{background:#fee2e2}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="modal" class="modal hidden" role="dialog" aria-hidden="true">
|
|
<div class="modal-content">
|
|
<h3 id="modal-title">Form</h3>
|
|
<form id="modal-form">
|
|
<div id="modal-fields"></div>
|
|
<div class="modal-actions">
|
|
<button type="button" id="modal-cancel" class="btn-secondary">Batal</button>
|
|
<button type="submit" id="modal-submit" class="btn-primary">Simpan</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="app-shell">
|
|
<div id="top-right-controls" style="position:fixed;right:12px;top:12px;z-index:11500;display:flex;flex-direction:column;align-items:flex-end;gap:8px;max-width:calc(100vw - 24px)">
|
|
<div id="auth-bar" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:flex-end;max-width:calc(100vw - 24px)">
|
|
<span id="auth-user-label" style="font-size:13px;color:#0f172a;background:#fff;padding:8px 10px;border-radius:999px;box-shadow:0 6px 18px rgba(15,23,42,0.08)">Tamu</span>
|
|
<button type="button" id="auth-login-btn" style="background:#0f172a;color:#fff;border:0;padding:8px 12px;border-radius:8px;cursor:pointer;font-weight:600">Masuk</button>
|
|
<button type="button" id="auth-logout-btn" style="display:none;background:#fee2e2;color:#991b1b;border:0;padding:8px 12px;border-radius:8px;cursor:pointer;font-weight:600">Keluar</button>
|
|
<button type="button" id="admin-dashboard-btn" style="display:none;background:#1d4ed8;color:#fff;border:0;padding:8px 12px;border-radius:8px;cursor:pointer;font-weight:600">Admin</button>
|
|
</div>
|
|
</div>
|
|
<div style="position:fixed;left:50%;top:10px;transform:translateX(-50%);z-index:11000;display:flex;gap:8px">
|
|
<button id="view-map-btn" onclick="switchView('map')" style="background:#1e40af;color:#fff;border:0;padding:10px 16px;border-radius:8px;cursor:pointer;font-weight:600;font-size:15px;box-shadow:0 2px 8px rgba(30,64,175,0.3)">📍 Peta</button>
|
|
<button id="view-data-btn" onclick="switchView('data')" style="background:#e5e7eb;color:#374151;border:0;padding:10px 16px;border-radius:8px;cursor:pointer;font-weight:600;font-size:15px">📋 Daftar Data</button>
|
|
</div>
|
|
<main id="map-wrap" style="display:flex;flex:1;min-width:0;position:relative">
|
|
<div id="map" style="flex:1"></div>
|
|
</main>
|
|
<div id="data-panel" style="display:none;background:linear-gradient(180deg,#ffffff 0%,#f8fafc 100%);flex:1;min-width:0;flex-direction:column;overflow:hidden">
|
|
<div id="data-panel-header-mini" style="padding:18px 18px 14px;border-bottom:1px solid #e5e7eb;background:linear-gradient(180deg,#ffffff 0%,#f9fafb 100%)">
|
|
<div class="data-panel-kicker">📋 Data</div>
|
|
<div class="data-panel-title">Daftar Lokasi</div>
|
|
<div class="data-panel-subtitle">Pilih kategori untuk melihat data</div>
|
|
<div class="data-view-toggle">
|
|
<button type="button" id="data-view-worship" class="active" onclick="setDataPanelView('worship')">📍 Tempat Ibadah</button>
|
|
<button type="button" id="data-view-house" onclick="setDataPanelView('house')">🏠 Rumah</button>
|
|
</div>
|
|
</div>
|
|
<div id="data-panel-summary" style="padding:12px 18px 0;color:#475569;font-size:13px"></div>
|
|
<div class="data-table-wrap">
|
|
<table class="data-table">
|
|
<thead id="data-table-head"></thead>
|
|
<tbody id="data-table-body"></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="data-pagination" style="padding:14px 18px 18px;border-top:1px solid #e5e7eb;background:linear-gradient(180deg,rgba(255,255,255,0.65),#fff)">
|
|
<div class="data-pagination-inner" style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap">
|
|
<div id="data-pagination-info" class="data-pagination-info" style="color:#64748b;font-size:13px"></div>
|
|
<div id="data-pagination-controls" class="data-pagination-controls" style="display:flex;gap:6px;flex-wrap:wrap"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="auth-modal" class="modal hidden" role="dialog" aria-hidden="true">
|
|
<div class="modal-content">
|
|
<h3 id="auth-modal-title">Masuk</h3>
|
|
<form id="auth-modal-form">
|
|
<div id="auth-modal-fields"></div>
|
|
<div id="auth-modal-switch" style="margin-top:12px;font-size:13px;color:#475569;display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap"></div>
|
|
<div class="modal-actions">
|
|
<button type="button" id="auth-modal-cancel" class="btn-secondary">Batal</button>
|
|
<button type="submit" id="auth-modal-submit" class="btn-primary">Kirim</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="admin-modal" class="modal hidden" role="dialog" aria-hidden="true">
|
|
<div class="modal-content" style="max-width:760px;width:min(760px,92vw)">
|
|
<h3>Admin Dashboard</h3>
|
|
<div style="font-size:13px;color:#475569;margin-bottom:10px">Persetujuan akun pengelola yang menunggu aktivasi.</div>
|
|
<div id="admin-pending-list" style="max-height:60vh;overflow:auto"></div>
|
|
<div class="modal-actions">
|
|
<button type="button" id="admin-modal-close" class="btn-secondary">Tutup</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="admin-detail-modal" class="modal hidden" role="dialog" aria-hidden="true">
|
|
<div class="modal-content" style="max-width:860px;width:min(860px,94vw)">
|
|
<h3 id="admin-detail-title">Detail Pendaftaran</h3>
|
|
<div id="admin-detail-body" style="max-height:70vh;overflow:auto;font-size:14px;color:#334155"></div>
|
|
<div class="modal-actions">
|
|
<button type="button" id="admin-detail-close" class="btn-secondary">Tutup</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Email Verification Modal -->
|
|
|
|
<!-- Image viewer modal (separate from form modal) -->
|
|
<div id="image-viewer" style="display:none;position:fixed;left:0;top:0;right:0;bottom:0;z-index:13000;background:rgba(0,0,0,0.85);align-items:center;justify-content:center;padding:16px;box-sizing:border-box">
|
|
<div style="position:relative;width:min(92vw,960px);max-width:960px;max-height:88vh;margin:auto;display:flex;align-items:center;justify-content:center;overflow:hidden;border-radius:12px;background:rgba(15,23,42,0.1)">
|
|
<button id="image-viewer-close" style="position:absolute;right:12px;top:12px;background:#fff;border-radius:20px;border:0;padding:6px 8px;cursor:pointer;z-index:13001;box-shadow:0 2px 8px rgba(0,0,0,0.2)">✕</button>
|
|
<img id="image-viewer-img" src="" style="display:block;max-width:100%;max-height:88vh;width:auto;height:auto;object-fit:contain;border-radius:10px;box-shadow:0 6px 30px rgba(0,0,0,0.6)">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast container for non-blocking messages -->
|
|
<div id="toast-container"></div>
|
|
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
|
|
|
<script>
|
|
window.authToken = localStorage.getItem('webgis_jwt') || '';
|
|
try{
|
|
window.currentUser = JSON.parse(localStorage.getItem('webgis_user') || 'null');
|
|
}catch(e){
|
|
window.currentUser = null;
|
|
}
|
|
|
|
function syncAuthToken(token){
|
|
try{
|
|
window.authToken = token || '';
|
|
if(window.authToken){ localStorage.setItem('webgis_jwt', window.authToken); }
|
|
else { localStorage.removeItem('webgis_jwt'); }
|
|
}catch(e){}
|
|
}
|
|
|
|
function syncCurrentUser(user){
|
|
try{
|
|
window.currentUser = user || null;
|
|
if(window.currentUser){ localStorage.setItem('webgis_user', JSON.stringify(window.currentUser)); }
|
|
else { localStorage.removeItem('webgis_user'); }
|
|
}catch(e){
|
|
window.currentUser = user || null;
|
|
}
|
|
}
|
|
|
|
function buildHeaders(contentType = 'application/json'){
|
|
const headers = {};
|
|
if(contentType) headers['Content-Type'] = contentType;
|
|
if(window.authToken){ headers['Authorization'] = 'Bearer ' + window.authToken; }
|
|
return headers;
|
|
}
|
|
|
|
// increase allowed zoom: tiles provided by OSM go up to ~19, allow higher zoom levels for closer inspection
|
|
// make mouse-wheel zoom less aggressive so one scroll notch feels closer to one +/- button step
|
|
const map = L.map('map', {
|
|
maxZoom: 22,
|
|
minZoom: 2,
|
|
zoomControl: true,
|
|
zoomDelta: 1,
|
|
wheelPxPerZoomLevel: 240,
|
|
wheelDebounceTime: 80
|
|
}).setView([-0.0552, 109.35], 13);
|
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxNativeZoom: 19, maxZoom: 22, attribution: '© OpenStreetMap contributors' }).addTo(map);
|
|
|
|
const rumahBelumDibantuGroup = L.layerGroup().addTo(map);
|
|
const rumahSudahDibantuGroup = L.layerGroup().addTo(map);
|
|
const masjidGroup = L.layerGroup().addTo(map);
|
|
// lokasiGroup must be a FeatureGroup for Leaflet.Draw edit controls
|
|
const lokasiGroup = L.featureGroup().addTo(map);
|
|
const rumahMarkers = [];
|
|
|
|
let masjidCircle = null;
|
|
let masjidControlEl = null;
|
|
let currentEditingLayer = null; // used when creating/editing a marker to allow suggestion updates
|
|
|
|
// filter thresholds (null = disabled)
|
|
// masjid-specific filter intervals (defaults: min = 0, max = null/unbounded)
|
|
let masjidIncomeMin = 0;
|
|
let masjidIncomeMax = null;
|
|
let masjidMembersMin = 0;
|
|
let masjidMembersMax = null;
|
|
// whether worship filter UI is expanded (controls whether filters are applied)
|
|
let masjidFiltersExpanded = false;
|
|
|
|
const dataPanelState = {
|
|
view: 'worship',
|
|
page: 1,
|
|
pageSize: 10
|
|
};
|
|
let lokasiRecords = [];
|
|
let lokasiMarkerById = new Map();
|
|
|
|
function getRecordIdKey(value){
|
|
return String(value === undefined || value === null ? '' : value);
|
|
}
|
|
|
|
function formatMoney(value){
|
|
try{
|
|
if(value === undefined || value === null || value === '') return '-';
|
|
const numberValue = Number(value);
|
|
if(isNaN(numberValue)) return '-';
|
|
return 'Rp ' + new Intl.NumberFormat('id-ID').format(numberValue);
|
|
}catch(e){
|
|
return '-';
|
|
}
|
|
}
|
|
|
|
function formatCount(value, suffix){
|
|
try{
|
|
const numberValue = Number(value);
|
|
if(isNaN(numberValue)) return '0' + (suffix ? ' ' + suffix : '');
|
|
return String(numberValue) + (suffix ? ' ' + suffix : '');
|
|
}catch(e){
|
|
return '0' + (suffix ? ' ' + suffix : '');
|
|
}
|
|
}
|
|
|
|
function buildIconSvg(kind){
|
|
if(kind === 'eye'){
|
|
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1.5 12s3.5-6.5 10.5-6.5S22.5 12 22.5 12 19 18.5 12 18.5 1.5 12 1.5 12z"></path><circle cx="12" cy="12" r="3.5"></circle></svg>';
|
|
}
|
|
if(kind === 'edit'){
|
|
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"></path><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"></path></svg>';
|
|
}
|
|
if(kind === 'delete'){
|
|
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"></path><path d="M8 6V4h8v2"></path><path d="M19 6l-1 14H6L5 6"></path><path d="M10 11v6"></path><path d="M14 11v6"></path></svg>';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function buildActionButton(kind, label, handler, id){
|
|
return `<button type="button" class="data-action-btn" aria-label="${escapeHtml(label)}" title="${escapeHtml(label)}" onclick="${handler}('${escapeHtml(id)}')">${buildIconSvg(kind)}</button>`;
|
|
}
|
|
|
|
function getPanelRecords(){
|
|
const records = Array.isArray(lokasiRecords) ? lokasiRecords.slice() : [];
|
|
const filtered = records.filter(item => {
|
|
const jenis = String(item && item.jenis ? item.jenis : '').toLowerCase();
|
|
if(dataPanelState.view === 'house') return jenis === 'rumah';
|
|
return isWorshipType(jenis);
|
|
});
|
|
filtered.sort((left, right) => {
|
|
const leftPrimary = dataPanelState.view === 'house' ? String(left.alamat || '') : String(left.nama || '');
|
|
const rightPrimary = dataPanelState.view === 'house' ? String(right.alamat || '') : String(right.nama || '');
|
|
const primaryCompare = leftPrimary.localeCompare(rightPrimary, 'id', { sensitivity: 'base' });
|
|
if(primaryCompare !== 0) return primaryCompare;
|
|
const leftSecondary = dataPanelState.view === 'house' ? String(left.nama_kk || left.nama || '') : String(left.alamat || '');
|
|
const rightSecondary = dataPanelState.view === 'house' ? String(right.nama_kk || right.nama || '') : String(right.alamat || '');
|
|
return leftSecondary.localeCompare(rightSecondary, 'id', { sensitivity: 'base' });
|
|
});
|
|
return filtered;
|
|
}
|
|
|
|
function goToLocation(id){
|
|
switchView('map');
|
|
setTimeout(() => {
|
|
const marker = lokasiMarkerById.get(getRecordIdKey(id));
|
|
if(!marker){
|
|
showToast('Titik lokasi tidak ditemukan di peta', 'error', 3500);
|
|
return;
|
|
}
|
|
try{
|
|
const latlng = marker.getLatLng();
|
|
map.invalidateSize();
|
|
map.flyTo(latlng, Math.max(map.getZoom(), 19), { animate: true, duration: 0.55 });
|
|
if(typeof marker.openPopup === 'function'){
|
|
map.once('moveend', function(){
|
|
try{ marker.openPopup(); }catch(e){}
|
|
});
|
|
}
|
|
if(marker._jenis && isWorshipType(marker._jenis)){
|
|
try{ showMasjidRadius(marker); }catch(e){}
|
|
}
|
|
}catch(e){
|
|
console.error(e);
|
|
}
|
|
}, 50);
|
|
}
|
|
|
|
function closePopupById(id){
|
|
try{
|
|
const marker = lokasiMarkerById.get(getRecordIdKey(id));
|
|
if(marker && typeof marker.closePopup === 'function') marker.closePopup();
|
|
}catch(e){ console.error(e); }
|
|
}
|
|
|
|
window.currentUser = null;
|
|
|
|
function isCurrentUserAdmin(){
|
|
return !!(window.currentUser && window.currentUser.role === 'admin' && window.currentUser.status === 'active');
|
|
}
|
|
|
|
function isCurrentUserManager(){
|
|
return !!(window.currentUser && window.currentUser.role === 'manager' && window.currentUser.status === 'active');
|
|
}
|
|
|
|
function canEditRecord(item){
|
|
if(isCurrentUserAdmin()) return true;
|
|
if(!isCurrentUserManager()) return false;
|
|
return String(item && item.created_by ? item.created_by : '') === String(window.currentUser.id);
|
|
}
|
|
|
|
function updateAuthBar(){
|
|
const label = document.getElementById('auth-user-label');
|
|
const loginBtn = document.getElementById('auth-login-btn');
|
|
const logoutBtn = document.getElementById('auth-logout-btn');
|
|
const adminBtn = document.getElementById('admin-dashboard-btn');
|
|
const user = window.currentUser;
|
|
if(!label) return;
|
|
if(!user){
|
|
label.textContent = 'Tamu';
|
|
label.style.cursor = 'default';
|
|
label.onclick = null;
|
|
if(loginBtn) loginBtn.style.display = 'inline-block';
|
|
if(logoutBtn) logoutBtn.style.display = 'none';
|
|
if(adminBtn) adminBtn.style.display = 'none';
|
|
} else {
|
|
const nameOnly = user.name || user.email || 'Pengguna';
|
|
if(user.role === 'admin'){
|
|
label.innerHTML = '<span style="font-weight:700">' + escapeHtml(nameOnly) + '</span><span style="margin-left:8px;font-size:12px;color:#1d4ed8;font-weight:600">Verifikasi pengelola</span>';
|
|
label.style.cursor = 'pointer';
|
|
label.onclick = function(){ openAdminDashboard(); };
|
|
} else {
|
|
label.textContent = nameOnly;
|
|
label.style.cursor = 'default';
|
|
label.onclick = null;
|
|
}
|
|
if(loginBtn) loginBtn.style.display = 'none';
|
|
if(logoutBtn) logoutBtn.style.display = 'inline-block';
|
|
if(adminBtn) adminBtn.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function attachTopRightControls(){
|
|
try{
|
|
const cluster = document.getElementById('top-right-controls');
|
|
const mapWrap = document.getElementById('map-wrap');
|
|
if(!cluster || !mapWrap) return;
|
|
const controlContainer = mapWrap.querySelector('.leaflet-top.leaflet-right');
|
|
if(!controlContainer) return;
|
|
if(controlContainer.parentNode !== cluster){
|
|
cluster.appendChild(controlContainer);
|
|
}
|
|
try{
|
|
controlContainer.style.position = 'static';
|
|
controlContainer.style.top = 'auto';
|
|
controlContainer.style.right = 'auto';
|
|
controlContainer.style.margin = '0';
|
|
}catch(e){}
|
|
}catch(e){}
|
|
}
|
|
|
|
// State for registration verification code
|
|
let authRegisterState = {
|
|
code_sent: false,
|
|
code: null,
|
|
email: null
|
|
};
|
|
|
|
function openAuthModal(mode){
|
|
const title = document.getElementById('auth-modal-title');
|
|
const fields = document.getElementById('auth-modal-fields');
|
|
const switchArea = document.getElementById('auth-modal-switch');
|
|
const submitBtn = document.getElementById('auth-modal-submit');
|
|
const modal = document.getElementById('auth-modal');
|
|
|
|
// Reset registration verification state
|
|
if(mode === 'login'){
|
|
authRegisterState = { code_sent: false, code: null, email: null };
|
|
}
|
|
if(!title || !fields || !modal) return;
|
|
if(mode === 'register'){
|
|
title.textContent = 'Daftar Pengelola';
|
|
fields.innerHTML = '<div class="auth-field"><label><span class="label-title">Nama <span class="required-star">*</span></span><input name="name" type="text" required></label></div>'+
|
|
'<div class="auth-field"><label><span class="label-title">Email <span class="required-star">*</span></span><input name="email" type="email" required></label></div>'+
|
|
'<div class="auth-field"><label><span class="label-title">Password <span class="required-star">*</span></span><input name="password" type="password" required></label></div>'+
|
|
'<div class="auth-field"><label><span class="label-title">Konfirmasi Password <span class="required-star">*</span></span><input name="password_confirm" type="password" required></label></div>'+
|
|
'<div class="auth-field"><label><span class="label-title">Organisasi / Tempat Ibadah <span class="required-star">*</span></span><input name="organization" type="text" required></label></div>'+
|
|
'<div class="auth-field"><label><span class="label-title">Alamat Organisasi <span class="required-star">*</span></span><textarea name="org_address" required></textarea></label></div>'+
|
|
'<div class="auth-field"><label><span class="label-title">Nomor Telp Organisasi <span class="required-star">*</span></span><input name="org_phone" type="text" required></label></div>'+
|
|
'<div class="auth-field">' +
|
|
'<label><span class="label-title">Bukti Organisasi <span class="required-star">*</span></span></label>' +
|
|
'<div style="margin-top:6px"><input name="org_proof" id="org_proof_input" type="file" accept=".png,.jpg,.jpeg,.svg,.pdf" required></div>' +
|
|
'<div style="font-size:12px;color:#64748b;margin-top:6px">Hanya file: .png, .jpg, .jpeg, .svg, atau .pdf. Maks 5MB.</div>' +
|
|
'</div>'+
|
|
'<div style="display:flex;gap:8px;margin-top:12px">' +
|
|
'<button type="button" id="auth-send-code-btn" class="btn" style="flex:1;padding:10px;background:#3b82f6;color:white;border:none;border-radius:4px;cursor:pointer;font-size:14px">Kirim Kode Verifikasi</button>' +
|
|
'</div>' +
|
|
'<div class="auth-field"><label><span class="label-title">Kode Verifikasi <span class="required-star">*</span></span><input name="verify_code" type="text" placeholder="Masukkan 6 digit" maxlength="6" pattern="[0-9]{6}" required></label></div>'+
|
|
'<div style="font-size:12px;color:#64748b;margin-top:8px">Akun akan berstatus menunggu persetujuan admin setelah pendaftaran berhasil.</div>';
|
|
if(switchArea){ switchArea.innerHTML = '<span>Sudah punya akun? <button type="button" id="auth-switch-to-login" class="data-link" style="background:transparent;border:0;padding:0;cursor:pointer;margin-left:4px">Masuk</button></span>'; }
|
|
if(submitBtn) submitBtn.textContent = 'Daftar';
|
|
} else if(mode === 'login'){
|
|
title.textContent = 'Masuk';
|
|
fields.innerHTML = '<div class="auth-field"><label><span class="label-title">Email</span><input name="email" type="email" required></label></div>' +
|
|
'<div class="auth-field"><label><span class="label-title">Password</span><input name="password" type="password" required></label></div>' +
|
|
'<div style="text-align:right;margin-top:-6px;margin-bottom:12px">' +
|
|
'<button type="button" id="auth-forgot-password-link" style="background:transparent;border:0;padding:0;color:#3b82f6;cursor:pointer;font-size:13px">Lupa Password?</button>' +
|
|
'</div>';
|
|
if(switchArea){ switchArea.innerHTML = '<span>Ingin daftar pengelola? <button type="button" id="auth-switch-to-register" class="data-link" style="background:transparent;border:0;padding:0;cursor:pointer;margin-left:4px">Daftar di sini</button></span>'; }
|
|
if(submitBtn) submitBtn.textContent = 'Masuk';
|
|
} else if(mode === 'forgot_password'){
|
|
title.textContent = 'Lupa Password';
|
|
fields.innerHTML =
|
|
'<div class="auth-field"><label><span class="label-title">Email <span class="required-star">*</span></span><input name="email" type="email" required></label></div>'+
|
|
'<div style="display:flex;gap:8px;margin-top:12px;margin-bottom:12px">' +
|
|
'<button type="button" id="auth-send-forgot-code-btn" class="btn" style="flex:1;padding:10px;background:#3b82f6;color:white;border:none;border-radius:4px;cursor:pointer;font-size:14px">Kirim Kode Verifikasi</button>' +
|
|
'</div>' +
|
|
'<div class="auth-field"><label><span class="label-title">Kode Verifikasi <span class="required-star">*</span></span><input name="verify_code" type="text" placeholder="Masukkan 6 digit" maxlength="6" pattern="[0-9]{6}" required></label></div>'+
|
|
'<div class="auth-field"><label><span class="label-title">Password Baru <span class="required-star">*</span></span><input name="password" type="password" required></label></div>'+
|
|
'<div class="auth-field"><label><span class="label-title">Konfirmasi Password Baru <span class="required-star">*</span></span><input name="password_confirm" type="password" required></label></div>';
|
|
if(switchArea){ switchArea.innerHTML = '<span>Kembali ke <button type="button" id="auth-switch-to-login" class="data-link" style="background:transparent;border:0;padding:0;cursor:pointer;margin-left:4px">Masuk</button></span>'; }
|
|
if(submitBtn) submitBtn.textContent = 'Reset Password';
|
|
}
|
|
modal.classList.remove('hidden');
|
|
modal.setAttribute('aria-hidden','false');
|
|
try{ syncRequiredFieldMarkers(); }catch(e){}
|
|
try{
|
|
const switchToRegister = document.getElementById('auth-switch-to-register');
|
|
const switchToLogin = document.getElementById('auth-switch-to-login');
|
|
const forgotPasswordLink = document.getElementById('auth-forgot-password-link');
|
|
if(switchToRegister) switchToRegister.onclick = function(){ openAuthModal('register'); };
|
|
if(switchToLogin) switchToLogin.onclick = function(){ openAuthModal('login'); };
|
|
if(forgotPasswordLink) forgotPasswordLink.onclick = function(){ openAuthModal('forgot_password'); };
|
|
}catch(e){}
|
|
if(mode === 'register'){
|
|
const orgProof = document.getElementById('org_proof_input');
|
|
if(orgProof){
|
|
orgProof.addEventListener('change', function(){
|
|
const f = (orgProof.files && orgProof.files[0]) ? orgProof.files[0] : null;
|
|
if(!f) return;
|
|
const maxSize = 5 * 1024 * 1024;
|
|
const allowed = ['jpg','jpeg','png','svg','pdf'];
|
|
const ext = (f.name || '').split('.').pop().toLowerCase();
|
|
if(f.size > maxSize){
|
|
showToast('Ukuran file bukti organisasi maksimal 5MB', 'error', 5000);
|
|
orgProof.value = '';
|
|
return;
|
|
}
|
|
if(!allowed.includes(ext)){
|
|
showToast('Jenis file tidak diperbolehkan. Hanya .png, .jpg, .jpeg, .svg, atau .pdf.', 'error', 5000);
|
|
orgProof.value = '';
|
|
return;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
const form = document.getElementById('auth-modal-form');
|
|
if(form) form.onsubmit = async function(ev){
|
|
ev.preventDefault();
|
|
|
|
let endpoint = 'src/api/auth/login.php';
|
|
if(mode === 'register') endpoint = 'src/api/auth/register.php';
|
|
if(mode === 'forgot_password') endpoint = 'src/api/auth/reset_password.php';
|
|
|
|
try{
|
|
let resp, jr;
|
|
if(mode === 'register' || mode === 'forgot_password'){
|
|
// Validate password length and match
|
|
const passwordInput = form.querySelector('input[name="password"]');
|
|
const password = passwordInput ? passwordInput.value : '';
|
|
const passwordConfirmInput = form.querySelector('input[name="password_confirm"]');
|
|
const passwordConfirm = passwordConfirmInput ? passwordConfirmInput.value : '';
|
|
|
|
if(password.length < 8){
|
|
showToast('Password minimal harus 8 karakter', 'error', 5000);
|
|
return;
|
|
}
|
|
if(password !== passwordConfirm){
|
|
showToast('Konfirmasi password tidak cocok', 'error', 5000);
|
|
return;
|
|
}
|
|
|
|
if(mode === 'register'){
|
|
// client-side validation for org_proof: size <=5MB and allowed extensions
|
|
const fileInput = form.querySelector('input[name="org_proof"]') || document.getElementById('org_proof_input');
|
|
if(!fileInput){ showToast('Input bukti organisasi tidak ditemukan', 'error', 4000); return; }
|
|
if(!(fileInput.files && fileInput.files.length > 0)){ showToast('Mohon unggah bukti organisasi (foto/pdf).', 'error', 5000); return; }
|
|
const f = fileInput.files[0];
|
|
const maxSize = 5 * 1024 * 1024;
|
|
const allowed = ['jpg','jpeg','png','svg','pdf'];
|
|
const ext = (f.name || '').split('.').pop().toLowerCase();
|
|
if(f.size > maxSize){ showToast('Ukuran file bukti organisasi maksimal 5MB', 'error', 5000); return; }
|
|
if(!allowed.includes(ext)){ showToast('Jenis file tidak diperbolehkan. Hanya .png, .jpg, .jpeg, .svg, atau .pdf.', 'error', 5000); return; }
|
|
}
|
|
|
|
// Check verify code
|
|
const codeInput = form.querySelector('input[name="verify_code"]');
|
|
const code = codeInput ? codeInput.value.trim() : '';
|
|
if(!code || code.length !== 6 || !/^\d{6}$/.test(code)){
|
|
showToast('Masukkan kode verifikasi 6 digit yang valid', 'error', 4000);
|
|
return;
|
|
}
|
|
|
|
if(mode === 'register'){
|
|
const fd = new FormData(form);
|
|
resp = await fetch(endpoint, { method: 'POST', headers: buildHeaders(null), body: fd, credentials: 'same-origin' });
|
|
jr = await resp.json().catch(()=>null);
|
|
} else {
|
|
const fd = new FormData(form);
|
|
const payload = {};
|
|
for(const [k,v] of fd.entries()) payload[k] = v;
|
|
resp = await fetch(endpoint, { method: 'POST', headers: buildHeaders(), body: JSON.stringify(payload), credentials: 'same-origin' });
|
|
jr = await resp.json().catch(()=>null);
|
|
}
|
|
} else {
|
|
const fd = new FormData(form);
|
|
const payload = {};
|
|
for(const [k,v] of fd.entries()) payload[k] = v;
|
|
resp = await fetch(endpoint, { method: 'POST', headers: buildHeaders(), body: JSON.stringify(payload), credentials: 'same-origin' });
|
|
jr = await resp.json().catch(()=>null);
|
|
}
|
|
if(!resp.ok || !jr || jr.success === false){
|
|
showToast((jr && (jr.error || jr.status || jr.message)) ? (jr.error || jr.status || jr.message) : 'Gagal memproses respons dari server', 'error', 5000);
|
|
return;
|
|
}
|
|
if(mode === 'register'){
|
|
authRegisterState = { code_sent: false, code: null, email: null };
|
|
closeAuthModal();
|
|
showToast('Pendaftaran berhasil! Akun menunggu persetujuan admin.', 'success', 5000);
|
|
} else if(mode === 'forgot_password'){
|
|
authRegisterState = { code_sent: false, code: null, email: null };
|
|
closeAuthModal();
|
|
showToast('Reset password berhasil! Silakan masuk kembali.', 'success', 5000);
|
|
} else {
|
|
if(jr && jr.token){ syncAuthToken(jr.token); }
|
|
if(jr && jr.user){ syncCurrentUser(jr.user); }
|
|
await refreshAuthState();
|
|
closeAuthModal();
|
|
showToast('Berhasil masuk', 'success', 3000);
|
|
loadData();
|
|
}
|
|
}catch(e){ console.error(e); showToast('Gagal memproses autentikasi', 'error', 5000); }
|
|
};
|
|
|
|
// Setup event listeners for registration and forgot password "Send Verification Code" buttons
|
|
const wireVerificationBtn = function(btnId) {
|
|
const sendCodeBtn = document.getElementById(btnId);
|
|
if(sendCodeBtn){
|
|
sendCodeBtn.onclick = async function(ev){
|
|
ev.preventDefault();
|
|
const emailInput = form.querySelector('input[name="email"]');
|
|
const email = emailInput ? emailInput.value.trim().toLowerCase() : '';
|
|
|
|
if(!email || !email.includes('@')){
|
|
showToast('Masukkan email yang valid', 'error', 3000);
|
|
return;
|
|
}
|
|
|
|
// Client-side cooldown check
|
|
const limitKey = 'webgis_limit_' + btoa(email);
|
|
let limitInfo = { lastTime: 0 };
|
|
try{
|
|
const cached = localStorage.getItem(limitKey);
|
|
if(cached) limitInfo = JSON.parse(cached);
|
|
}catch(e){}
|
|
|
|
const now = Date.now();
|
|
if(limitInfo.lastTime && now - limitInfo.lastTime < 30 * 1000){
|
|
const remaining = Math.ceil((30 * 1000 - (now - limitInfo.lastTime)) / 1000);
|
|
showToast(`Silakan tunggu ${remaining} detik sebelum mengirim kembali.`, 'error', 3000);
|
|
return;
|
|
}
|
|
|
|
sendCodeBtn.disabled = true;
|
|
sendCodeBtn.textContent = 'Mengirim...';
|
|
|
|
try{
|
|
const resp = await fetch('src/api/auth/send_verification_code.php', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({email: email})
|
|
});
|
|
|
|
const jr = await resp.json().catch(()=>null);
|
|
|
|
if(!resp.ok || (jr && jr.success === false)){
|
|
const errMessage = jr && jr.message ? jr.message : (jr && jr.error ? jr.error : 'Gagal mengirim kode');
|
|
showToast(errMessage, 'error', 5000);
|
|
sendCodeBtn.disabled = false;
|
|
sendCodeBtn.textContent = 'Kirim Kode Verifikasi';
|
|
return;
|
|
}
|
|
|
|
// Update client side cooldown info on success
|
|
limitInfo.lastTime = now;
|
|
try{ localStorage.setItem(limitKey, JSON.stringify(limitInfo)); }catch(e){}
|
|
|
|
authRegisterState.code_sent = true;
|
|
authRegisterState.email = email;
|
|
if(jr.debug_code){
|
|
authRegisterState.code = jr.debug_code;
|
|
const codeInput = form.querySelector('input[name="verify_code"]');
|
|
if(codeInput){
|
|
codeInput.value = jr.debug_code;
|
|
showToast('Gagal mengirim email. Menggunakan kode pengujian: ' + jr.debug_code, 'warning', 8000);
|
|
}
|
|
} else {
|
|
showToast('Kode verifikasi terkirim ke email Anda! Silakan cek email Anda.', 'success', 6000);
|
|
}
|
|
|
|
// Cooldown countdown timer
|
|
let cooldown = 30;
|
|
sendCodeBtn.disabled = true;
|
|
sendCodeBtn.textContent = `Kirim Ulang (${cooldown}s)`;
|
|
const timer = setInterval(() => {
|
|
cooldown--;
|
|
if(cooldown <= 0){
|
|
clearInterval(timer);
|
|
sendCodeBtn.disabled = false;
|
|
sendCodeBtn.textContent = 'Kirim Kode Verifikasi';
|
|
} else {
|
|
sendCodeBtn.textContent = `Kirim Ulang (${cooldown}s)`;
|
|
}
|
|
}, 1000);
|
|
|
|
}catch(e){
|
|
console.error(e);
|
|
showToast('Gagal mengirim kode verifikasi', 'error', 5000);
|
|
sendCodeBtn.disabled = false;
|
|
sendCodeBtn.textContent = 'Kirim Kode Verifikasi';
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
if(mode === 'register'){
|
|
wireVerificationBtn('auth-send-code-btn');
|
|
} else if(mode === 'forgot_password'){
|
|
wireVerificationBtn('auth-send-forgot-code-btn');
|
|
}
|
|
}
|
|
|
|
function closeAuthModal(){
|
|
const modal = document.getElementById('auth-modal');
|
|
if(!modal) return;
|
|
modal.classList.add('hidden');
|
|
modal.setAttribute('aria-hidden','true');
|
|
// Reset form states
|
|
const form = document.getElementById('auth-modal-form');
|
|
if(form) form.reset();
|
|
authRegisterState = { code_sent: false, code: null, email: null };
|
|
}
|
|
|
|
// Email Verification Modal Functions
|
|
async function refreshAuthState(){
|
|
try{
|
|
const resp = await fetch('src/api/auth/me.php', { credentials: 'same-origin', headers: buildHeaders(null) });
|
|
const jr = await resp.json().catch(()=>null);
|
|
if(resp.ok && jr && jr.authenticated){
|
|
syncCurrentUser(jr.user);
|
|
} else if(resp.status === 401 || resp.status === 403){
|
|
syncAuthToken('');
|
|
syncCurrentUser(null);
|
|
} else {
|
|
try{
|
|
const cachedUser = JSON.parse(localStorage.getItem('webgis_user') || 'null');
|
|
if(cachedUser && window.authToken){
|
|
syncCurrentUser(cachedUser);
|
|
} else if(!window.currentUser && cachedUser){
|
|
syncCurrentUser(cachedUser);
|
|
}
|
|
}catch(err){}
|
|
}
|
|
}catch(e){
|
|
try{
|
|
const cachedUser = JSON.parse(localStorage.getItem('webgis_user') || 'null');
|
|
if(cachedUser && !window.currentUser){
|
|
syncCurrentUser(cachedUser);
|
|
}
|
|
}catch(err){
|
|
if(!window.currentUser){
|
|
window.currentUser = null;
|
|
}
|
|
}
|
|
}
|
|
updateAuthBar();
|
|
}
|
|
|
|
async function logoutUser(){
|
|
try{
|
|
await fetch('src/api/auth/logout.php', { method: 'POST', credentials: 'same-origin', headers: buildHeaders(null) });
|
|
}catch(e){}
|
|
syncAuthToken('');
|
|
syncCurrentUser(null);
|
|
updateAuthBar();
|
|
loadData();
|
|
}
|
|
|
|
async function openAdminDashboard(){
|
|
if(!isCurrentUserAdmin()) return;
|
|
const modal = document.getElementById('admin-modal');
|
|
const list = document.getElementById('admin-pending-list');
|
|
if(!modal || !list) return;
|
|
list.innerHTML = '<div style="padding:12px;color:#64748b">Memuat data...</div>';
|
|
modal.classList.remove('hidden');
|
|
modal.setAttribute('aria-hidden','false');
|
|
try{
|
|
const resp = await fetch('src/api/admin/list_pending_managers.php', { credentials: 'same-origin', headers: buildHeaders(null) });
|
|
const jr = await resp.json().catch(()=>null);
|
|
if(!resp.ok || !jr || !jr.success){
|
|
list.innerHTML = '<div class="data-empty">Gagal memuat data.</div>';
|
|
return;
|
|
}
|
|
const items = Array.isArray(jr.pending) ? jr.pending : [];
|
|
window.pendingManagerItems = items;
|
|
if(items.length === 0){
|
|
list.innerHTML = '<div class="data-empty">Tidak ada pendaftaran yang menunggu.</div>';
|
|
return;
|
|
}
|
|
list.innerHTML = items.map((item, index) => `
|
|
<div style="border:1px solid #e5e7eb;border-radius:12px;padding:12px;margin-bottom:10px;background:#fff">
|
|
<div style="font-weight:700;color:#0f172a">${escapeHtml(item.name || '-')}</div>
|
|
<div style="font-size:13px;color:#475569">${escapeHtml(item.email || '-')}</div>
|
|
<div style="font-size:12px;color:#64748b;margin-top:6px">${escapeHtml(item.created_at || '')}</div>
|
|
<div style="display:flex;gap:8px;margin-top:10px">
|
|
<button type="button" class="btn-secondary" onclick="openPendingManagerDetailByIndex(${index})">Detail</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}catch(e){
|
|
console.error(e);
|
|
list.innerHTML = '<div class="data-empty">Gagal memuat data.</div>';
|
|
}
|
|
}
|
|
|
|
async function approvePendingUser(id){
|
|
try{
|
|
const resp = await fetch('src/api/admin/approve_user.php', { method: 'POST', headers: buildHeaders(), body: JSON.stringify({ id: id }), credentials: 'same-origin' });
|
|
const jr = await resp.json().catch(()=>null);
|
|
if(!resp.ok || !jr || !jr.success){ showToast('Gagal menyetujui user', 'error', 4000); return; }
|
|
showToast('User disetujui', 'success', 3000);
|
|
closePendingManagerDetail();
|
|
openAdminDashboard();
|
|
}catch(e){ console.error(e); showToast('Gagal menyetujui user', 'error', 4000); }
|
|
}
|
|
|
|
async function rejectPendingUser(id){
|
|
const reason = prompt("Masukkan alasan penolakan:");
|
|
if (reason === null) return; // user cancelled
|
|
const trimmedReason = reason.trim();
|
|
if (!trimmedReason) {
|
|
showToast("Alasan penolakan wajib diisi!", "error", 4000);
|
|
return;
|
|
}
|
|
try{
|
|
const resp = await fetch('src/api/admin/reject_user.php', { method: 'POST', headers: buildHeaders(), body: JSON.stringify({ id: id, reason: trimmedReason }), credentials: 'same-origin' });
|
|
const jr = await resp.json().catch(()=>null);
|
|
if(!resp.ok || !jr || !jr.success){ showToast('Gagal menolak user', 'error', 4000); return; }
|
|
showToast('User ditolak', 'success', 3000);
|
|
closePendingManagerDetail();
|
|
openAdminDashboard();
|
|
}catch(e){ console.error(e); showToast('Gagal menolak user', 'error', 4000); }
|
|
}
|
|
|
|
function closeAdminDashboard(){
|
|
const modal = document.getElementById('admin-modal');
|
|
if(!modal) return;
|
|
modal.classList.add('hidden');
|
|
modal.setAttribute('aria-hidden','true');
|
|
}
|
|
|
|
function renderDataPanel(){
|
|
const headEl = document.getElementById('data-table-head');
|
|
const bodyEl = document.getElementById('data-table-body');
|
|
const summaryEl = document.getElementById('data-panel-summary');
|
|
const paginationInfoEl = document.getElementById('data-pagination-info');
|
|
const paginationControlsEl = document.getElementById('data-pagination-controls');
|
|
if(!headEl || !bodyEl || !summaryEl || !paginationInfoEl || !paginationControlsEl) return;
|
|
|
|
const rows = getPanelRecords();
|
|
const totalRows = rows.length;
|
|
const totalPages = Math.max(1, Math.ceil(totalRows / dataPanelState.pageSize));
|
|
if(dataPanelState.page > totalPages) dataPanelState.page = totalPages;
|
|
if(dataPanelState.page < 1) dataPanelState.page = 1;
|
|
const startIndex = (dataPanelState.page - 1) * dataPanelState.pageSize;
|
|
const pageRows = rows.slice(startIndex, startIndex + dataPanelState.pageSize);
|
|
|
|
const isHouseView = dataPanelState.view === 'house';
|
|
const isGuest = !window.currentUser;
|
|
|
|
if(isHouseView){
|
|
headEl.innerHTML = isGuest
|
|
? '<tr><th style="width:64px">No</th><th>Alamat</th><th>Status Bulan Ini</th><th>Pemberi Bantuan</th><th style="width:118px;text-align:right">Aksi</th></tr>'
|
|
: '<tr><th style="width:64px">No</th><th>Alamat</th><th>Nama Kepala Keluarga</th><th style="width:90px;text-align:center">Anggota</th><th>Penghasilan Bulanan</th><th>Status Bulan Ini</th><th>Pemberi Bantuan</th><th style="width:118px;text-align:right">Aksi</th></tr>';
|
|
} else {
|
|
headEl.innerHTML = '<tr><th style="width:64px">No</th><th style="width:120px">Rumah Ibadah</th><th style="width:150px">Nama Tempat Ibadah</th><th>Alamat</th><th>Bantuan Bulan Ini</th><th style="width:118px;text-align:right">Aksi</th></tr>';
|
|
}
|
|
|
|
summaryEl.textContent = isHouseView
|
|
? 'Menampilkan rumah yang aktif dengan urutan alamat.'
|
|
: 'Menampilkan tempat ibadah yang aktif dengan urutan nama.';
|
|
|
|
if(totalRows === 0){
|
|
bodyEl.innerHTML = `<tr><td colspan="${isHouseView ? (isGuest ? 5 : 8) : 6}"><div class="data-empty">Tidak ada data yang cocok untuk filter ini.</div></td></tr>`;
|
|
} else {
|
|
bodyEl.innerHTML = pageRows.map((item, index) => {
|
|
const id = getRecordIdKey(item.id);
|
|
const rowNumber = startIndex + index + 1;
|
|
if(isHouseView){
|
|
const assistedThisMonth = Number(item.bantuan_bulan_ini || 0) > 0 || (item.assisted && parseInt(item.assisted) === 1);
|
|
const statusBadge = assistedThisMonth
|
|
? '<span class="data-badge is-green">Sudah dibantu bulan ini</span>'
|
|
: '<span class="data-badge is-yellow">Belum dibantu bulan ini</span>';
|
|
const canEdit = canEditRecord(item);
|
|
const canAssistEdit = isCurrentUserManager() || isCurrentUserAdmin();
|
|
const providerName = item && item.sumber_bantuan_name ? item.sumber_bantuan_name : '';
|
|
const providerType = item && item.sumber_bantuan_jenis ? item.sumber_bantuan_jenis : '';
|
|
const providerPrefixed = providerName ? ((providerType ? (providerType.charAt(0).toUpperCase() + providerType.slice(1)) + ' - ' : '') + providerName) : '';
|
|
const providerCellContent = assistedThisMonth ? escapeHtml(providerPrefixed || '-') : '<span style="color:#9ca3af">-</span>';
|
|
if(isGuest){
|
|
return `<tr>
|
|
<td><span class="data-index">${rowNumber}</span></td>
|
|
<td><div class="data-muted">${escapeHtml(item.alamat || '-')}</div></td>
|
|
<td>${statusBadge}</td>
|
|
<td><div style="font-size:13px;color:#475569;white-space:nowrap">${providerCellContent}</div></td>
|
|
<td><div class="data-actions">${buildActionButton('eye', 'Lihat di peta', 'goToLocation', id)}</div></td>
|
|
</tr>`;
|
|
}
|
|
return `<tr>
|
|
<td><span class="data-index">${rowNumber}</span></td>
|
|
<td><div class="data-muted">${escapeHtml(item.alamat || '-')}</div></td>
|
|
<td><div class="data-name">${escapeHtml(item.nama_kk || item.nama || '-')}</div></td>
|
|
<td><div class="data-muted">${escapeHtml(formatCount(item.members, 'orang'))}</div></td>
|
|
<td><div class="data-muted">${escapeHtml(formatMoney(item.monthly_income))}</div></td>
|
|
<td>${statusBadge}</td>
|
|
<td><div style="font-size:13px;color:#475569;white-space:nowrap">${providerCellContent}</div></td>
|
|
<td><div class="data-actions">${buildActionButton('eye', 'Lihat di peta', 'goToLocation', id)}${(canEdit || canAssistEdit) ? buildActionButton('edit', 'Edit data', 'openEdit', id) : ''}${canEdit ? buildActionButton('delete', 'Hapus data', 'hapus', id) : ''}</div></td>
|
|
</tr>`;
|
|
}
|
|
const jenisLabel = (item.jenis || 'Tempat Ibadah').charAt(0).toUpperCase() + (item.jenis || 'Tempat Ibadah').slice(1);
|
|
const canEdit = canEditRecord(item);
|
|
const actionButtons = canEdit
|
|
? `${buildActionButton('eye', 'Lihat di peta', 'goToLocation', id)}${buildActionButton('edit', 'Edit data', 'openEdit', id)}${buildActionButton('delete', 'Hapus data', 'hapus', id)}`
|
|
: buildActionButton('eye', 'Lihat di peta', 'goToLocation', id);
|
|
return `<tr>
|
|
<td><span class="data-index">${rowNumber}</span></td>
|
|
<td><span class="data-pill" style="margin:0">${escapeHtml(jenisLabel)}</span></td>
|
|
<td><div class="data-name">${escapeHtml(item.nama || '-')}</div></td>
|
|
<td><div class="data-muted">${escapeHtml(item.alamat || '-')}</div></td>
|
|
<td><span class="data-badge is-green">${escapeHtml(formatCount(item.bantuan_bulan_ini, 'bantuan'))}</span></td>
|
|
<td><div class="data-actions">${actionButtons}</div></td>
|
|
</tr>`;
|
|
}).join('');
|
|
}
|
|
|
|
paginationInfoEl.textContent = totalRows === 0
|
|
? '0 data'
|
|
: `Halaman ${dataPanelState.page} dari ${totalPages} · ${totalRows} data`;
|
|
|
|
const pages = [];
|
|
if(totalPages > 1){
|
|
const pushPage = (page) => pages.push(`<button type="button" class="data-page-btn ${page === dataPanelState.page ? 'active' : ''}" onclick="setDataPanelPage(${page})">${page}</button>`);
|
|
const pushEllipsis = () => pages.push('<span class="data-page-ellipsis">…</span>');
|
|
pages.push(`<button type="button" class="data-page-btn" ${dataPanelState.page === 1 ? 'disabled' : ''} onclick="setDataPanelPage(${Math.max(1, dataPanelState.page - 1)})">Prev</button>`);
|
|
if(totalPages <= 7){
|
|
for(let page = 1; page <= totalPages; page++) pushPage(page);
|
|
} else {
|
|
pushPage(1);
|
|
const start = Math.max(2, dataPanelState.page - 1);
|
|
const end = Math.min(totalPages - 1, dataPanelState.page + 1);
|
|
if(start > 2) pushEllipsis();
|
|
for(let page = start; page <= end; page++) pushPage(page);
|
|
if(end < totalPages - 1) pushEllipsis();
|
|
pushPage(totalPages);
|
|
}
|
|
pages.push(`<button type="button" class="data-page-btn" ${dataPanelState.page === totalPages ? 'disabled' : ''} onclick="setDataPanelPage(${Math.min(totalPages, dataPanelState.page + 1)})">Next</button>`);
|
|
}
|
|
paginationControlsEl.innerHTML = pages.join('');
|
|
}
|
|
|
|
function switchView(viewType){
|
|
const mapWrap = document.getElementById('map-wrap');
|
|
const dataPanel = document.getElementById('data-panel');
|
|
const viewMapBtn = document.getElementById('view-map-btn');
|
|
const viewDataBtn = document.getElementById('view-data-btn');
|
|
const topRightControls = document.getElementById('top-right-controls');
|
|
|
|
if(viewType === 'map'){
|
|
if(mapWrap) mapWrap.style.display = 'flex';
|
|
if(dataPanel) dataPanel.style.display = 'none';
|
|
if(topRightControls) topRightControls.style.display = 'flex';
|
|
if(viewMapBtn) { viewMapBtn.style.background = '#1e40af'; viewMapBtn.style.color = '#fff'; viewMapBtn.style.boxShadow = '0 2px 8px rgba(30,64,175,0.3)'; }
|
|
if(viewDataBtn) { viewDataBtn.style.background = '#e5e7eb'; viewDataBtn.style.color = '#374151'; viewDataBtn.style.boxShadow = 'none'; }
|
|
} else if(viewType === 'data'){
|
|
if(mapWrap) mapWrap.style.display = 'none';
|
|
if(dataPanel) dataPanel.style.display = 'flex';
|
|
if(topRightControls) topRightControls.style.display = 'none';
|
|
if(viewMapBtn) { viewMapBtn.style.background = '#e5e7eb'; viewMapBtn.style.color = '#374151'; viewMapBtn.style.boxShadow = 'none'; }
|
|
if(viewDataBtn) { viewDataBtn.style.background = '#1e40af'; viewDataBtn.style.color = '#fff'; viewDataBtn.style.boxShadow = '0 2px 8px rgba(30,64,175,0.3)'; }
|
|
try{ renderDataPanel(); }catch(e){}
|
|
try{ hideMasjidRadius(); }catch(e){}
|
|
try{ const modalEl = document.getElementById('modal'); if(modalEl) modalEl.classList.add('hidden'); }catch(e){}
|
|
try{ currentEditingLayer = null; }catch(e){}
|
|
}
|
|
}
|
|
|
|
function setDataPanelView(view){
|
|
if(view !== 'house' && view !== 'worship') return;
|
|
dataPanelState.view = view;
|
|
dataPanelState.page = 1;
|
|
const worshipBtn = document.getElementById('data-view-worship');
|
|
const houseBtn = document.getElementById('data-view-house');
|
|
if(worshipBtn) worshipBtn.classList.toggle('active', view === 'worship');
|
|
if(houseBtn) houseBtn.classList.toggle('active', view === 'house');
|
|
renderDataPanel();
|
|
}
|
|
|
|
function setDataPanelPage(page){
|
|
const nextPage = Math.max(1, parseInt(page, 10) || 1);
|
|
dataPanelState.page = nextPage;
|
|
renderDataPanel();
|
|
}
|
|
|
|
// helper to check worship types in record
|
|
const worshipTypes = ['masjid','gereja','pura','vihara','klenteng'];
|
|
function isWorshipType(jenis){ try{ if(!jenis) return false; return worshipTypes.indexOf(String(jenis).toLowerCase()) !== -1; }catch(e){return false;} }
|
|
|
|
const assistanceSourceOptions = [
|
|
'Pemerintah Pusat',
|
|
'Pemerintah Daerah',
|
|
'Kementerian Sosial',
|
|
'Kementerian Kesehatan',
|
|
'Kementerian Pendidikan',
|
|
'Baznas',
|
|
'Lazis',
|
|
'CSR Perusahaan',
|
|
'Donatur Individu',
|
|
'Lainnya'
|
|
];
|
|
|
|
const assistanceShapeOptions = [
|
|
'Tunai',
|
|
'Pangan/Sembako',
|
|
'Pendidikan',
|
|
'Kesehatan',
|
|
'Program Keluarga Harapan',
|
|
'Lainnya'
|
|
];
|
|
|
|
function escapeHtml(value){
|
|
return String(value === undefined || value === null ? '' : value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function getDisplayName(rec){
|
|
if(!rec) return '';
|
|
const a = String(rec.nama || '').trim();
|
|
const b = String(rec.nama_kk || '').trim();
|
|
const isDash = s => !s || /^[-–—\s]+$/.test(s);
|
|
if(!isDash(a)) return a;
|
|
if(!isDash(b)) return b;
|
|
return '';
|
|
}
|
|
|
|
function toDateInputValue(value){
|
|
try{
|
|
if(!value) return '';
|
|
return String(value).split(' ')[0].split('T')[0];
|
|
}catch(e){
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function todayDateInputValue(){
|
|
try{
|
|
const now = new Date();
|
|
const year = now.getFullYear();
|
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
const day = String(now.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
}catch(e){
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function buildAssistanceFieldsHtml(record){
|
|
const assisted = !!(record && record.assisted && parseInt(record.assisted)===1);
|
|
const dateValue = assisted ? toDateInputValue(record.last_assisted_at || todayDateInputValue()) : '';
|
|
const notesValue = escapeHtml(record && record.assistance_notes ? record.assistance_notes : '');
|
|
const shapeValue = String(record && record.bentuk_bantuan ? record.bentuk_bantuan : '');
|
|
const sourceValue = String(record && record.sumber_bantuan ? record.sumber_bantuan : '');
|
|
const disabledAttr = assisted ? '' : 'disabled';
|
|
const shapeOptionsHtml = ['Tunai','Pangan/Sembako','Pendidikan','Kesehatan','Lainnya'].map(v =>
|
|
`<option value="${escapeHtml(v)}" ${String(v) === shapeValue ? 'selected' : ''}>${escapeHtml(v)}</option>`
|
|
).join('');
|
|
return `
|
|
<div id="assistance-fields" class="assistance-fields ${assisted ? 'is-enabled' : 'is-disabled'}" style="margin-top:12px">
|
|
<label><input name="assisted" type="checkbox" ${assisted ? 'checked' : ''}> Telah menerima bantuan</label>
|
|
<label>Tanggal Bantuan <input name="last_assisted_at" type="date" value="${escapeHtml(dateValue)}" ${disabledAttr} ${assisted ? 'required' : ''}></label>
|
|
<div class="assistance-combobox">
|
|
<label id="sumber-bantuan-label">Sumber Bantuan</label>
|
|
<input type="search" id="sumber-bantuan-search" class="assist-source-input" placeholder="Ketik nama tempat ibadah" value="${escapeHtml(sourceValue)}" ${disabledAttr} autocomplete="off">
|
|
<button type="button" class="assist-source-toggle" id="sumber-bantuan-toggle" ${disabledAttr} aria-label="Buka daftar sumber bantuan"></button>
|
|
<input type="hidden" name="sumber_bantuan" id="sumber-bantuan-value" value="${escapeHtml(sourceValue)}">
|
|
<div class="assist-source-panel" id="sumber-bantuan-panel" aria-label="Daftar sumber bantuan"></div>
|
|
</div>
|
|
<label>Bentuk Bantuan
|
|
<select name="bentuk_bantuan" ${disabledAttr} ${assisted ? 'required' : ''}>
|
|
<option value="">Pilih bentuk bantuan</option>
|
|
${shapeOptionsHtml}
|
|
</select>
|
|
</label>
|
|
<label>Catatan Bantuan <input name="assistance_notes" type="text" value="${notesValue}" ${disabledAttr}></label>
|
|
</div>`;
|
|
}
|
|
|
|
function createLocationPinIcon(color){
|
|
const fill = color || '#d32f2f';
|
|
const html = '<div class="location-pin" style="--pin-color:' + fill + '">' +
|
|
'<svg width="30" height="42" viewBox="0 0 30 42" aria-hidden="true" focusable="false">' +
|
|
'<path d="M15 1C8.4 1 3 6.4 3 13c0 8.4 12 28 12 28s12-19.6 12-28C27 6.4 21.6 1 15 1z" fill="' + fill + '" stroke="#ffffff" stroke-width="2"></path>' +
|
|
'<circle cx="15" cy="13" r="5.25" fill="#ffffff"></circle>' +
|
|
'</svg>' +
|
|
'</div>';
|
|
return L.divIcon({
|
|
html: html,
|
|
className: 'location-pin-icon',
|
|
iconSize: [30, 42],
|
|
iconAnchor: [15, 42],
|
|
popupAnchor: [0, -36]
|
|
});
|
|
}
|
|
|
|
function buildMarkerLegendSvg(color){
|
|
const fill = color || '#d32f2f';
|
|
return '<span class="legend-marker" aria-hidden="true">' +
|
|
'<svg viewBox="0 0 30 42" focusable="false" aria-hidden="true">' +
|
|
'<path d="M15 1C8.4 1 3 6.4 3 13c0 8.4 12 28 12 28s12-19.6 12-28C27 6.4 21.6 1 15 1z" fill="' + fill + '" stroke="#ffffff" stroke-width="2"></path>' +
|
|
'<circle cx="15" cy="13" r="5.25" fill="#ffffff"></circle>' +
|
|
'</svg>' +
|
|
'</span>';
|
|
}
|
|
|
|
function buildLayerLegendLabel(title, colors, description){
|
|
const colorList = Array.isArray(colors) ? colors : [colors];
|
|
const markers = colorList.filter(Boolean).map(color => buildMarkerLegendSvg(color)).join('');
|
|
return '<span class="layer-legend" aria-label="' + title + '">' + markers + '<span class="legend-text">' + title + '</span></span>';
|
|
}
|
|
|
|
function syncRequiredFieldMarkers(){
|
|
try{
|
|
const labels = document.querySelectorAll('#modal-fields label');
|
|
labels.forEach(label => {
|
|
if(!label || !label.querySelector) return;
|
|
const control = label.querySelector('input, select, textarea');
|
|
const shouldShow = !!control && !!control.required && !control.disabled;
|
|
let title = label.querySelector(':scope > .label-title');
|
|
if(shouldShow && !title){
|
|
title = document.createElement('span');
|
|
title.className = 'label-title';
|
|
const beforeNodes = [];
|
|
for(const node of Array.from(label.childNodes)){
|
|
if(node === control) break;
|
|
beforeNodes.push(node);
|
|
}
|
|
beforeNodes.forEach(node => title.appendChild(node));
|
|
label.insertBefore(title, control || label.firstChild);
|
|
}
|
|
let star = title ? title.querySelector('.required-star') : null;
|
|
if(shouldShow){
|
|
if(!star){
|
|
star = document.createElement('span');
|
|
star.className = 'required-star';
|
|
star.textContent = '*';
|
|
if(title){ title.appendChild(star); }
|
|
}
|
|
} else if(star){
|
|
star.remove();
|
|
if(title && !title.textContent.trim() && title.children.length === 0){
|
|
title.remove();
|
|
}
|
|
}
|
|
});
|
|
|
|
const sourceLabel = document.getElementById('sumber-bantuan-label');
|
|
const sourceInput = document.getElementById('sumber-bantuan-search');
|
|
if(sourceLabel){
|
|
const requiredSource = !!(sourceInput && sourceInput.required && !sourceInput.disabled);
|
|
let title = sourceLabel.querySelector(':scope > .label-title');
|
|
if(requiredSource && !title){
|
|
title = document.createElement('span');
|
|
title.className = 'label-title';
|
|
while(sourceLabel.firstChild){ title.appendChild(sourceLabel.firstChild); }
|
|
sourceLabel.appendChild(title);
|
|
}
|
|
if(title){
|
|
let star = title.querySelector('.required-star');
|
|
if(requiredSource && !star){
|
|
star = document.createElement('span');
|
|
star.className = 'required-star';
|
|
star.textContent = '*';
|
|
title.appendChild(star);
|
|
} else if(!requiredSource && star){
|
|
star.remove();
|
|
}
|
|
}
|
|
}
|
|
}catch(e){}
|
|
}
|
|
|
|
function setRequiredStarForControl(control, required){
|
|
try{
|
|
if(!control || !control.closest) return;
|
|
const label = control.closest('label');
|
|
if(!label) return;
|
|
let title = label.querySelector(':scope > .label-title');
|
|
if(required && !title){
|
|
title = document.createElement('span');
|
|
title.className = 'label-title';
|
|
while(label.firstChild && label.firstChild !== control){
|
|
title.appendChild(label.firstChild);
|
|
}
|
|
label.insertBefore(title, control);
|
|
}
|
|
if(!title) return;
|
|
let star = title.querySelector('.required-star');
|
|
if(required){
|
|
if(!star){
|
|
star = document.createElement('span');
|
|
star.className = 'required-star';
|
|
star.textContent = '*';
|
|
title.appendChild(star);
|
|
}
|
|
} else if(star){
|
|
star.remove();
|
|
if(!title.textContent.trim() && title.children.length === 0){ title.remove(); }
|
|
}
|
|
}catch(e){}
|
|
}
|
|
|
|
function populateAssistanceSourceOptions(){
|
|
const panel = document.getElementById('sumber-bantuan-panel');
|
|
const valueInput = document.getElementById('sumber-bantuan-value');
|
|
const searchInput = document.getElementById('sumber-bantuan-search');
|
|
if(!panel || !valueInput || !searchInput) return;
|
|
|
|
fetch('src/api/get_lokasi.php', { headers: buildHeaders(null), credentials: 'same-origin' })
|
|
.then(response => {
|
|
if(!response.ok){
|
|
throw new Error('get_lokasi_failed_' + response.status);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(allLoc => {
|
|
const filter = (window && window.assistanceSourceFilter) ? window.assistanceSourceFilter : null;
|
|
const items = (allLoc || [])
|
|
.filter(loc => {
|
|
if(!loc) return false;
|
|
const jenis = String(loc.jenis || '').toLowerCase();
|
|
if(filter && filter.worshipOnly){ if(!isWorshipType(jenis)) return false; }
|
|
else { if(jenis === 'rumah') return false; }
|
|
if(filter && (filter.createdBy !== undefined && filter.createdBy !== null)){
|
|
if(String(loc.created_by || '') !== String(filter.createdBy)) return false;
|
|
}
|
|
return true;
|
|
})
|
|
.map(loc => {
|
|
const rawName = String(loc && loc.nama ? loc.nama : '').trim();
|
|
const jenis = String(loc && loc.jenis ? loc.jenis : '').trim();
|
|
const label = rawName + (jenis ? ' — ' + jenis.charAt(0).toUpperCase() + jenis.slice(1) : '');
|
|
return { id: String(loc.id || ''), name: rawName, label: label };
|
|
})
|
|
.filter(item => item.name !== '')
|
|
.sort((a, b) => a.name.localeCompare(b.name, 'id', { sensitivity: 'base' }));
|
|
window.assistanceSourceItems = items;
|
|
window.assistanceSourceLoaded = true;
|
|
const currentValue = String(valueInput.value || '').trim();
|
|
if(currentValue){
|
|
const matched = items.find(item => item.id === currentValue || item.name.toLowerCase() === currentValue.toLowerCase() || item.label.toLowerCase() === currentValue.toLowerCase());
|
|
if(matched){ searchInput.value = matched.label; valueInput.value = matched.id; }
|
|
}
|
|
renderAssistanceSourceOptions(searchInput.value || '', false, false);
|
|
syncAssistanceSourceSearch();
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
function normalizeAssistanceQuery(query){
|
|
const q = String(query || '').trim();
|
|
if(!q) return { type: 'empty', term: '' };
|
|
const wildcard = q.indexOf('*') !== -1 || q.indexOf('?') !== -1;
|
|
if(wildcard){
|
|
const pattern = '^' + q.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$';
|
|
try{ return { type: 'regex', regex: new RegExp(pattern, 'i') }; }catch(e){ return { type: 'contains', term: q.toLowerCase() }; }
|
|
}
|
|
try{ return { type: 'regex', regex: new RegExp(q, 'i') }; }catch(e){ return { type: 'contains', term: q.toLowerCase() }; }
|
|
}
|
|
|
|
function getFilteredAssistanceSourceItems(query){
|
|
try{
|
|
const items = Array.isArray(window.assistanceSourceItems) ? window.assistanceSourceItems : [];
|
|
const q = normalizeAssistanceQuery(query);
|
|
if(q.type === 'empty') return items;
|
|
return items.filter(item => {
|
|
const text = item.label || '';
|
|
if(q.type === 'regex'){ return q.regex.test(text); }
|
|
return text.toLowerCase().indexOf(q.term) !== -1;
|
|
});
|
|
}catch(e){}
|
|
return [];
|
|
}
|
|
|
|
function renderAssistanceSourceOptions(query, append=false, showPanel=true){
|
|
try{
|
|
const panel = document.getElementById('sumber-bantuan-panel');
|
|
if(!panel) return;
|
|
const items = getFilteredAssistanceSourceItems(query);
|
|
const pageSize = 15;
|
|
const currentShown = append ? Array.from(panel.querySelectorAll('.assist-source-item')).length : 0;
|
|
const nextShown = Math.min(items.length, currentShown > 0 ? currentShown + pageSize : pageSize);
|
|
const visibleItems = items.slice(0, nextShown);
|
|
panel.innerHTML = '';
|
|
if(visibleItems.length === 0){
|
|
const empty = document.createElement('div');
|
|
empty.className = 'assist-source-empty';
|
|
empty.textContent = 'Tidak ditemukan';
|
|
panel.appendChild(empty);
|
|
if(showPanel) panel.classList.add('show');
|
|
return;
|
|
}
|
|
visibleItems.forEach(item => {
|
|
const row = document.createElement('div');
|
|
row.className = 'assist-source-item';
|
|
row.textContent = item.label;
|
|
row.dataset.value = item.id;
|
|
row.dataset.label = item.label;
|
|
row.addEventListener('mousedown', function(ev){ ev.preventDefault(); selectAssistanceSource(item); });
|
|
panel.appendChild(row);
|
|
});
|
|
if(showPanel) panel.classList.add('show');
|
|
}catch(e){}
|
|
}
|
|
|
|
function selectAssistanceSource(item){
|
|
try{
|
|
const search = document.getElementById('sumber-bantuan-search');
|
|
const valueInput = document.getElementById('sumber-bantuan-value');
|
|
const panel = document.getElementById('sumber-bantuan-panel');
|
|
if(search) search.value = item.label;
|
|
if(valueInput) valueInput.value = item.id;
|
|
if(panel) panel.classList.remove('show');
|
|
}catch(e){}
|
|
}
|
|
|
|
function syncAssistanceSourceSearch(){
|
|
try{
|
|
const search = document.getElementById('sumber-bantuan-search');
|
|
const valueInput = document.getElementById('sumber-bantuan-value');
|
|
if(!search || !valueInput) return;
|
|
const items = Array.isArray(window.assistanceSourceItems) ? window.assistanceSourceItems : [];
|
|
const selected = items.find(item => item.id === String(valueInput.value || ''));
|
|
search.value = selected ? selected.label : search.value || '';
|
|
}catch(e){}
|
|
}
|
|
|
|
// Geocoding helpers (Nominatim)
|
|
function debounce(fn, wait){ let t=null; return function(...args){ clearTimeout(t); t=setTimeout(()=>fn.apply(this,args), wait); }; }
|
|
async function geocodeSearch(q){
|
|
if(!q || q.length<2) return [];
|
|
const url = 'https://nominatim.openstreetmap.org/search?format=jsonv2&addressdetails=1&limit=6&q=' + encodeURIComponent(q);
|
|
try{
|
|
const r = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
|
if(r.ok){ const j = await r.json().catch(()=>null); return j || []; }
|
|
}catch(e){
|
|
console.error('geocodeSearch failed', e);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
async function reverseGeocode(lat, lon){
|
|
const url = `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${encodeURIComponent(lat)}&lon=${encodeURIComponent(lon)}`;
|
|
try{
|
|
const r = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
|
if(r.ok){ const j = await r.json().catch(()=>null); return j || null; }
|
|
}catch(e){
|
|
console.error('reverseGeocode failed', e);
|
|
}
|
|
return null;
|
|
}
|
|
function detectWorshipType(r){ try{
|
|
const dn = (r.display_name||'').toLowerCase();
|
|
const typ = (r.type||'').toLowerCase();
|
|
if(dn.indexOf('masjid')!==-1 || dn.indexOf('mosque')!==-1 || typ.indexOf('mosque')!==-1) return 'masjid';
|
|
if(dn.indexOf('church')!==-1 || dn.indexOf('gereja')!==-1) return 'gereja';
|
|
if(dn.indexOf('pura')!==-1) return 'pura';
|
|
if(dn.indexOf('vihara')!==-1 || dn.indexOf('buddhist')!==-1) return 'vihara';
|
|
if(dn.indexOf('klenteng')!==-1 || dn.indexOf('kong')!==-1 || dn.indexOf('temple')!==-1) return 'klenteng';
|
|
if(typ.indexOf('place_of_worship')!==-1) return 'masjid';
|
|
return null;
|
|
}catch(e){return null;} }
|
|
function extractNameFromResult(r){ try{ if(!r) return ''; if(r.display_name){ const parts = r.display_name.split(','); return parts[0].trim(); } return ''; }catch(e){ return ''; }
|
|
}
|
|
|
|
function formatDateToDMY(s){
|
|
try{
|
|
if(!s) return '';
|
|
// try Date parsing first (ISO or RFC)
|
|
const d = new Date(s);
|
|
if(!isNaN(d.getTime())){
|
|
const dd = ('0'+d.getDate()).slice(-2);
|
|
const mm = ('0'+(d.getMonth()+1)).slice(-2);
|
|
const yyyy = d.getFullYear();
|
|
return dd + '-' + mm + '-' + yyyy;
|
|
}
|
|
// fallback: accept 'YYYY-MM-DD [HH:MM:SS]' or 'YYYY-MM-DD'
|
|
const datePart = (''+s).split(' ')[0];
|
|
const parts = datePart.split('-');
|
|
if(parts.length===3){ return parts[2] + '-' + parts[1] + '-' + parts[0]; }
|
|
return s;
|
|
}catch(e){ return s; }
|
|
}
|
|
|
|
function openModal(title, html, onSubmit){
|
|
document.getElementById('modal-title').textContent = title;
|
|
document.getElementById('modal-fields').innerHTML = html;
|
|
syncRequiredFieldMarkers();
|
|
// enhance file inputs: add a filename span, a visible size-note and client-side size check (max 5 MB)
|
|
try{
|
|
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
const ALLOWED_TYPES = ['image/jpeg','image/png','image/webp'];
|
|
const fileInputs = document.querySelectorAll('#modal-fields input[type="file"]');
|
|
fileInputs.forEach(fi=>{
|
|
let s = fi.parentNode.querySelector('.file-name');
|
|
if(!s){ s = document.createElement('span'); s.className='file-name'; s.style.display='block'; s.style.fontSize='12px'; s.style.color='#333'; s.style.marginTop='6px'; fi.parentNode.appendChild(s); }
|
|
let note = fi.parentNode.querySelector('.file-note');
|
|
if(!note){ note = document.createElement('div'); note.className='file-note'; note.style.fontSize='12px'; note.style.color='#666'; note.style.marginTop='6px'; note.textContent = 'Maksimal 5 MB — Format: JPG, PNG, WEBP'; fi.parentNode.appendChild(note); }
|
|
fi.addEventListener('change', function(){
|
|
const f = (fi.files && fi.files[0]) ? fi.files[0] : null;
|
|
if(!f){ s.textContent = ''; return; }
|
|
if(f.size > MAX_FILE_SIZE){
|
|
try{ showToast('Ukuran file melebihi 5 MB', 'error', 5000); }catch(e){}
|
|
fi.value = '';
|
|
s.textContent = '';
|
|
return;
|
|
}
|
|
if(f.type && ALLOWED_TYPES.indexOf(f.type) === -1){
|
|
try{ showToast('Format file tidak didukung. Gunakan JPG, PNG, atau WEBP.', 'error', 6000); }catch(e){}
|
|
fi.value = '';
|
|
s.textContent = '';
|
|
return;
|
|
}
|
|
s.textContent = f.name + ' (' + Math.round(f.size/1024) + ' KB)';
|
|
});
|
|
});
|
|
}catch(e){}
|
|
// helpers to move phone label into household slot and back
|
|
function movePhoneToSlot(){ try{ const labels = Array.from(document.querySelectorAll('#modal-fields label')); const phoneLabel = labels.find(l=> l.querySelector && l.querySelector('input[name="no_telp"]')); const slot = document.querySelector('#modal-fields .hf-phone-slot'); if(phoneLabel && slot && phoneLabel.parentNode !== slot){ slot.appendChild(phoneLabel); } }catch(e){} }
|
|
function movePhoneBackBeforeAddress(){ try{ const slotLabel = document.querySelector('#modal-fields .hf-phone-slot > label'); const addrLabel = Array.from(document.querySelectorAll('#modal-fields label')).find(l=> l.querySelector && l.querySelector('input[name="alamat"]')); if(slotLabel && addrLabel && addrLabel.parentNode){ addrLabel.parentNode.insertBefore(slotLabel, addrLabel); } }catch(e){} }
|
|
window.movePhoneToSlot = movePhoneToSlot;
|
|
window.movePhoneBackBeforeAddress = movePhoneBackBeforeAddress;
|
|
// enhance address inputs: autocomplete + selection
|
|
try{
|
|
const addrInputs = document.querySelectorAll('#modal-fields input[name="alamat"]');
|
|
addrInputs.forEach(inp=>{
|
|
// create suggestion container
|
|
let box = inp.parentNode.querySelector('.suggestions');
|
|
if(!box){ box = document.createElement('div'); box.className='suggestions'; box.style.position='absolute'; box.style.background='#fff'; box.style.border='1px solid #e5e7eb'; box.style.borderRadius='6px'; box.style.boxShadow='0 6px 20px rgba(15,23,42,0.06)'; box.style.zIndex=20000; box.style.maxHeight='220px'; box.style.overflow='auto'; box.style.width = inp.offsetWidth + 'px'; box.style.display='none'; box.style.padding='6px'; box.style.marginTop='6px'; inp.parentNode.style.position='relative'; inp.parentNode.appendChild(box); }
|
|
const doSearch = debounce(async function(){ const q = inp.value; if(!q || q.length<2){ box.style.display='none'; box.innerHTML=''; return; } const res = await geocodeSearch(q); box.innerHTML=''; if(!res || res.length===0){ box.style.display='none'; return; } res.forEach(r=>{ const el = document.createElement('div'); el.style.padding='6px'; el.style.cursor='pointer'; el.style.borderRadius='6px'; el.style.fontSize='13px'; el.textContent = (r.display_name||r.display_name); el.addEventListener('click', function(){ inp.value = r.display_name || ''; box.style.display='none';
|
|
// if suggestion has lat/lon, update editing layer or store coordinates
|
|
if(r.lat && r.lon){ try{ if(currentEditingLayer && typeof currentEditingLayer.setLatLng === 'function'){ currentEditingLayer.setLatLng([parseFloat(r.lat), parseFloat(r.lon)]); map.panTo([parseFloat(r.lat), parseFloat(r.lon)]); } // store chosen coords on input dataset
|
|
inp.dataset.lat = r.lat; inp.dataset.lon = r.lon; }catch(e){}
|
|
}
|
|
// if suggestion indicates a place of worship, set jenis accordingly and mark required
|
|
try{ const sel = document.querySelector('#modal-fields select[name="jenis"]'); const wt = detectWorshipType(r); if(wt && sel){ sel.value=wt; sel.dispatchEvent(new Event('change')); } }catch(e){}
|
|
// auto-fill name if suggestion appears to be a place of worship
|
|
try{ const wt2 = detectWorshipType(r); if(wt2){ const nameInput = document.querySelector('#modal-fields input[name="nama"]'); if(nameInput && !nameInput.value) nameInput.value = extractNameFromResult(r); } }catch(e){}
|
|
}); el.addEventListener('mouseenter', ()=> el.style.background='#f8fafc'); el.addEventListener('mouseleave', ()=> el.style.background='transparent'); box.appendChild(el); }); box.style.display='block'; }, 300);
|
|
inp.addEventListener('input', doSearch);
|
|
inp.addEventListener('blur', function(){ setTimeout(()=>{ box.style.display='none'; },200); });
|
|
});
|
|
}catch(e){}
|
|
const modal = document.getElementById('modal'); modal.classList.remove('hidden'); modal.setAttribute('aria-hidden','false');
|
|
// after modal opens, ensure phone field is moved into household slot once DOM stabilizes
|
|
try{ requestAnimationFrame(()=>{ try{ movePhoneToSlot(); }catch(e){}; setTimeout(()=>{ try{ movePhoneToSlot(); }catch(e){} },10); }); }catch(e){}
|
|
// normalize monthly_income display: remove trailing decimals and show integer Rupiah
|
|
try{
|
|
const mi = document.querySelector('#modal-fields input[name="monthly_income"]');
|
|
if(mi){
|
|
try{ if(mi.value !== undefined && mi.value !== null && mi.value !== ''){ mi.value = String(Math.round(Number(mi.value))); } }catch(e){}
|
|
mi.addEventListener('blur', function(){ try{ if(this.value !== undefined && this.value !== null && this.value !== '') this.value = String(Math.round(Number(this.value))); }catch(e){} });
|
|
}
|
|
}catch(e){}
|
|
// ensure jenis change toggles requiredness for name and alamat and hides photo for masjid
|
|
try{
|
|
const jenisSel = document.querySelector('#modal-fields select[name="jenis"]');
|
|
const nameInp = document.querySelector('#modal-fields input[name="nama"]');
|
|
const addrInp = document.querySelector('#modal-fields input[name="alamat"]');
|
|
const photoField = document.querySelector('#modal-fields #photo-field, #modal-fields #photo-field-create');
|
|
if(jenisSel){
|
|
const toggleReq = function(){
|
|
if(isWorshipType(jenisSel.value)){
|
|
if(nameInp) nameInp.required = true;
|
|
if(addrInp) addrInp.required = true;
|
|
if(photoField) photoField.style.display = 'none';
|
|
} else if (jenisSel.value==='rumah'){
|
|
if(nameInp) nameInp.required = false;
|
|
if(addrInp) addrInp.required = true;
|
|
if(photoField) photoField.style.display = 'block';
|
|
// ensure nama_kk (when present) is required
|
|
try{ const namaKk = document.querySelector('#modal-fields input[name="nama_kk"]'); if(namaKk) namaKk.required = true; }catch(e){}
|
|
} else {
|
|
if(nameInp) nameInp.required = false;
|
|
if(addrInp) addrInp.required = false;
|
|
if(photoField) photoField.style.display = 'block';
|
|
}
|
|
setRequiredStarForControl(nameInp, !!(nameInp && nameInp.required));
|
|
setRequiredStarForControl(addrInp, !!(addrInp && addrInp.required));
|
|
try{ const namaKk = document.querySelector('#modal-fields input[name="nama_kk"]'); setRequiredStarForControl(namaKk, !!(namaKk && namaKk.required)); }catch(e){}
|
|
syncRequiredFieldMarkers();
|
|
};
|
|
jenisSel.addEventListener('change', toggleReq);
|
|
// initialize
|
|
toggleReq();
|
|
}
|
|
// ensure assisted checkbox toggles assistance fields and resets fields when unchecked
|
|
try{
|
|
const assistedChk = document.querySelector('#modal-fields input[name="assisted"]');
|
|
const assistedDate = document.querySelector('#modal-fields input[name="last_assisted_at"]');
|
|
const assistedShape = document.querySelector('#modal-fields select[name="bentuk_bantuan"]');
|
|
const assistedNotes = document.querySelector('#modal-fields input[name="assistance_notes"]');
|
|
const assistanceBlock = document.getElementById('assistance-fields');
|
|
const assistedSourceValue = document.getElementById('sumber-bantuan-value');
|
|
window.assistanceSourceLoaded = false;
|
|
window.assistanceSourceItems = [];
|
|
populateAssistanceSourceOptions();
|
|
const assistedSourceSearch = document.getElementById('sumber-bantuan-search');
|
|
const assistedSourcePanel = document.getElementById('sumber-bantuan-panel');
|
|
const assistedSourceToggle = document.getElementById('sumber-bantuan-toggle');
|
|
if(assistedSourceSearch){
|
|
assistedSourceSearch.addEventListener('input', function(){
|
|
if(assistedSourceValue) assistedSourceValue.value = '';
|
|
renderAssistanceSourceOptions(this.value, false);
|
|
});
|
|
assistedSourceSearch.addEventListener('focus', function(){ renderAssistanceSourceOptions(this.value, false); });
|
|
assistedSourceSearch.addEventListener('click', function(){ renderAssistanceSourceOptions(this.value, false); });
|
|
assistedSourceSearch.addEventListener('blur', function(){ setTimeout(()=>{ if(assistedSourcePanel) assistedSourcePanel.classList.remove('show'); }, 140); });
|
|
assistedSourceSearch.addEventListener('keydown', function(ev){ if(ev.key === 'Escape' && assistedSourcePanel){ assistedSourcePanel.classList.remove('show'); } });
|
|
}
|
|
if(assistedSourceToggle){
|
|
assistedSourceToggle.addEventListener('click', function(){
|
|
if(!assistedSourcePanel) return;
|
|
if(assistedSourcePanel.classList.contains('show')){ assistedSourcePanel.classList.remove('show'); }
|
|
else { renderAssistanceSourceOptions(assistedSourceSearch ? assistedSourceSearch.value : '', false); }
|
|
});
|
|
}
|
|
if(assistedSourcePanel){
|
|
assistedSourcePanel.addEventListener('scroll', function(){
|
|
const nearBottom = this.scrollTop + this.clientHeight >= this.scrollHeight - 24;
|
|
if(nearBottom) renderAssistanceSourceOptions(assistedSourceSearch ? assistedSourceSearch.value : '', true);
|
|
});
|
|
assistedSourcePanel.addEventListener('mouseleave', function(){ /* keep open until focus changes */ });
|
|
document.addEventListener('click', function closeSourcePanel(ev){
|
|
const root = document.getElementById('assistance-fields');
|
|
if(!root || !ev.target || !root.contains(ev.target)) return;
|
|
if(ev.target === assistedSourceSearch || ev.target === assistedSourceToggle || assistedSourcePanel.contains(ev.target)) return;
|
|
}, { once: true });
|
|
}
|
|
|
|
const setDisabled = function(el, disabled){ if(!el) return; el.disabled = disabled; };
|
|
const toggleAssistFields = function(forceDate){ try{
|
|
const enabled = !!(assistedChk && assistedChk.checked);
|
|
if(assistanceBlock){ assistanceBlock.classList.toggle('is-enabled', enabled); assistanceBlock.classList.toggle('is-disabled', !enabled); }
|
|
[assistedDate, assistedSourceSearch, assistedSourceToggle, assistedShape, assistedNotes].forEach(el => setDisabled(el, !enabled));
|
|
if(enabled){
|
|
if(assistedDate && (!assistedDate.value || forceDate)){ assistedDate.value = todayDateInputValue(); }
|
|
if(assistedDate) assistedDate.required = true;
|
|
if(assistedSourceSearch) assistedSourceSearch.required = true;
|
|
if(assistedShape) assistedShape.required = true;
|
|
} else {
|
|
// reset semua field ke kosong
|
|
if(assistedDate){ assistedDate.required = false; assistedDate.value = ''; }
|
|
if(assistedSourceValue){ assistedSourceValue.value = ''; }
|
|
if(assistedSourceSearch){ assistedSourceSearch.required = false; assistedSourceSearch.value = ''; }
|
|
if(assistedSourceSearch){ assistedSourceSearch.value = ''; }
|
|
if(assistedSourcePanel){ assistedSourcePanel.classList.remove('show'); }
|
|
if(assistedShape){ assistedShape.required = false; assistedShape.value = ''; }
|
|
if(assistedNotes){ assistedNotes.value = ''; }
|
|
}
|
|
syncRequiredFieldMarkers();
|
|
}catch(e){} };
|
|
if(assistedChk){ assistedChk.addEventListener('change', function(){ toggleAssistFields(true); }); toggleAssistFields(false); }
|
|
setTimeout(()=>{ try{ syncAssistanceSourceSearch(); }catch(e){} }, 30);
|
|
setTimeout(()=>{ try{ syncRequiredFieldMarkers(); }catch(e){} }, 30);
|
|
}catch(e){}
|
|
}catch(e){}
|
|
const form = document.getElementById('modal-form');
|
|
form.onsubmit = function(ev){
|
|
ev.preventDefault();
|
|
try{
|
|
const assistedInput = form.querySelector('input[name="assisted"]');
|
|
const assistedActive = !!(assistedInput && assistedInput.checked);
|
|
const sourceValue = form.querySelector('#sumber-bantuan-value');
|
|
if(sourceValue && assistedActive){
|
|
const candidate = String(sourceValue.value || '').trim();
|
|
if(!candidate){
|
|
const sourceSearch = form.querySelector('#sumber-bantuan-search');
|
|
if(sourceSearch){ sourceSearch.setCustomValidity('Pilih sumber bantuan dari daftar'); sourceSearch.reportValidity(); }
|
|
return;
|
|
}
|
|
}
|
|
const sourceSearch = form.querySelector('#sumber-bantuan-search');
|
|
if(sourceSearch) sourceSearch.setCustomValidity('');
|
|
}catch(e){}
|
|
if(!form.checkValidity()){ form.reportValidity(); return; }
|
|
const fd = new FormData(form);
|
|
const obj = {};
|
|
for(const [k,v] of fd.entries()) obj[k]=v;
|
|
const assistedField = form.querySelector('input[name="assisted"]');
|
|
const assistedActive = !!(assistedField && assistedField.checked);
|
|
obj.assisted = assistedActive ? 1 : 0;
|
|
if(assistedActive){
|
|
const sourceValue = form.querySelector('#sumber-bantuan-value');
|
|
const shapeInput = form.querySelector('select[name="bentuk_bantuan"]');
|
|
const notesInput = form.querySelector('input[name="assistance_notes"]');
|
|
const dateInput = form.querySelector('input[name="last_assisted_at"]');
|
|
if(sourceValue) obj.sumber_bantuan = sourceValue.value || '';
|
|
if(shapeInput) obj.bentuk_bantuan = shapeInput.value || '';
|
|
if(notesInput) obj.assistance_notes = notesInput.value || '';
|
|
if(dateInput) obj.last_assisted_at = dateInput.value || '';
|
|
} else {
|
|
obj.last_assisted_at = null;
|
|
obj.sumber_bantuan = null;
|
|
obj.bentuk_bantuan = null;
|
|
obj.assistance_notes = null;
|
|
}
|
|
// capture file input (if any) so callers can upload after modal closes
|
|
const fileInput = form.querySelector('input[name="photo"]');
|
|
const file = (fileInput && fileInput.files && fileInput.files[0]) ? fileInput.files[0] : null;
|
|
onSubmit(obj, file);
|
|
closeModal();
|
|
// clear current editing layer reference
|
|
currentEditingLayer = null;
|
|
};
|
|
document.getElementById('modal-cancel').onclick = closeModal;
|
|
}
|
|
function closeModal(){ const modal=document.getElementById('modal'); modal.classList.add('hidden'); modal.setAttribute('aria-hidden','true'); document.getElementById('modal-fields').innerHTML = ''; currentEditingLayer = null; try{ window.assistanceSourceFilter = null; }catch(e){} try{ const submit = document.getElementById('modal-submit'); if(submit){ submit.textContent = 'Simpan'; submit.className = 'btn-primary'; } }catch(e){} }
|
|
|
|
function showMasjidRadius(marker){
|
|
hideMasjidRadius();
|
|
const center = marker.getLatLng();
|
|
const defaultRadius = 200;
|
|
masjidCircle = L.circle(center, {radius: defaultRadius, color:'#1976d2', fillOpacity:0.08, interactive:false}).addTo(map);
|
|
|
|
masjidControlEl = document.createElement('div'); masjidControlEl.id='masjid-radius-control';
|
|
try{ masjidControlEl.style.position='absolute'; masjidControlEl.style.right='10px'; masjidControlEl.style.bottom='20px'; masjidControlEl.style.zIndex='14000'; masjidControlEl.style.width='320px'; }catch(e){}
|
|
const rawName = marker._data && marker._data.nama ? String(marker._data.nama).trim() : '';
|
|
const typeLabel = marker._data && marker._data.jenis ? String(marker._data.jenis).charAt(0).toUpperCase() + String(marker._data.jenis).slice(1) : 'Tempat Ibadah';
|
|
const name = rawName !== '' ? rawName : '';
|
|
const alamat = marker._data && marker._data.alamat ? marker._data.alamat : '';
|
|
const recId = marker._data && marker._data.id ? marker._data.id : '';
|
|
// title
|
|
const titleEl = document.createElement('div'); titleEl.className = 'mc-title'; titleEl.textContent = (name || typeLabel); masjidControlEl.appendChild(titleEl);
|
|
// address
|
|
const addrEl = document.createElement('div'); addrEl.className = 'mc-address'; addrEl.textContent = alamat; masjidControlEl.appendChild(addrEl);
|
|
// controls (radius display + slider)
|
|
const controls = document.createElement('div'); controls.className = 'mc-controls';
|
|
const radiusWrap = document.createElement('div'); radiusWrap.className = 'mc-radius';
|
|
radiusWrap.innerHTML = `Radius (m): <input id="masjid-radius-input" type="number" min="50" max="3000" value="${defaultRadius}" style="width:110px;margin-left:8px;padding:6px;border-radius:6px;border:1px solid #e5e7eb">`;
|
|
const slider = document.createElement('input'); slider.type='range'; slider.id='masjid-radius-slider'; slider.min='50'; slider.max='3000'; slider.value = String(defaultRadius); slider.style.width='100%';
|
|
controls.appendChild(radiusWrap); controls.appendChild(slider); masjidControlEl.appendChild(controls);
|
|
// actions
|
|
const actions = document.createElement('div'); actions.className = 'mc-actions';
|
|
const isGuest = !window.currentUser;
|
|
const editBtn = document.createElement('button'); editBtn.id='masjid-edit'; editBtn.textContent='Edit';
|
|
const delBtn = document.createElement('button'); delBtn.id='masjid-delete'; delBtn.textContent='Hapus'; delBtn.className='destructive';
|
|
const closeBtn = document.createElement('button'); closeBtn.id='masjid-radius-close'; closeBtn.textContent='Tutup';
|
|
if(!isGuest){ actions.appendChild(editBtn); actions.appendChild(delBtn); } actions.appendChild(closeBtn); masjidControlEl.appendChild(actions);
|
|
// add masjid filter controls (interval inputs) - only for non-guest users
|
|
if(!isGuest){
|
|
const filterWrap = document.createElement('div'); filterWrap.style.marginTop = '10px';
|
|
filterWrap.innerHTML = `<div style="font-weight:600;margin-bottom:6px">Filter untuk pewarnaan (interval)</div>`+
|
|
`<div style="display:flex;gap:8px;align-items:center;margin-bottom:6px"><label style="width:110px">Penghasilan (Rp)</label><input id="masjid-income-min" type="number" min="0" value="0" style="width:90px;padding:6px;border-radius:6px;border:1px solid #e5e7eb"> - <input id="masjid-income-max" type="number" min="0" placeholder="(max)" style="width:90px;padding:6px;border-radius:6px;border:1px solid #e5e7eb"></div>`+
|
|
`<div style="display:flex;gap:8px;align-items:center;margin-bottom:6px"><label style="width:110px">Jumlah Anggota</label><input id="masjid-members-min" type="number" min="0" value="0" style="width:90px;padding:6px;border-radius:6px;border:1px solid #e5e7eb"> - <input id="masjid-members-max" type="number" min="0" placeholder="(max)" style="width:90px;padding:6px;border-radius:6px;border:1px solid #e5e7eb"></div>`+
|
|
`<div style="text-align:right;margin-top:6px"><button id="masjid-filter-apply">Terapkan</button> <button id="masjid-filter-reset">Reset</button></div>`;
|
|
// create a toggle button to show/hide filter block
|
|
const filterToggle = document.createElement('button'); filterToggle.id='masjid-filter-toggle'; filterToggle.textContent = 'Filter ▾';
|
|
filterToggle.style.marginTop = '10px'; filterToggle.style.background = 'transparent'; filterToggle.style.border = '0'; filterToggle.style.color = '#0f172a'; filterToggle.style.cursor = 'pointer'; filterToggle.style.fontWeight = '600';
|
|
masjidControlEl.appendChild(filterToggle);
|
|
const filterContainer = document.createElement('div'); filterContainer.id = 'masjid-filter-container'; filterContainer.style.display = 'none';
|
|
try{ filterContainer.appendChild(filterWrap); masjidControlEl.appendChild(filterContainer); }catch(e){ console.error('failed to append filterWrap into container', e); }
|
|
// toggle behavior
|
|
try{
|
|
filterToggle.addEventListener('click', function(){
|
|
try{
|
|
if(filterContainer.style.display === 'none' || !filterContainer.style.display){
|
|
filterContainer.style.display = 'block';
|
|
filterToggle.textContent = 'Filter ▴';
|
|
masjidFiltersExpanded = true;
|
|
} else {
|
|
filterContainer.style.display = 'none';
|
|
filterToggle.textContent = 'Filter ▾';
|
|
masjidFiltersExpanded = false;
|
|
}
|
|
updateHomeColors(center, masjidCircle ? masjidCircle.getRadius() : defaultRadius);
|
|
}catch(e){ console.error('filter toggle failed', e); }
|
|
});
|
|
}catch(e){ console.error('filter toggle wiring failed', e); }
|
|
// wire filter controls (apply/reset)
|
|
try{
|
|
const miMin = document.getElementById('masjid-income-min');
|
|
const miMax = document.getElementById('masjid-income-max');
|
|
const mmMin = document.getElementById('masjid-members-min');
|
|
const mmMax = document.getElementById('masjid-members-max');
|
|
const applyBtn = document.getElementById('masjid-filter-apply');
|
|
const resetBtn = document.getElementById('masjid-filter-reset');
|
|
const applyFn = function(){ try{
|
|
const imin = miMin && miMin.value !== '' ? Number(miMin.value) : 0;
|
|
const imax = miMax && miMax.value !== '' ? Number(miMax.value) : null;
|
|
const mmin = mmMin && mmMin.value !== '' ? Number(mmMin.value) : 0;
|
|
const mmax = mmMax && mmMax.value !== '' ? Number(mmMax.value) : null;
|
|
masjidIncomeMin = isNaN(imin) ? 0 : imin;
|
|
masjidIncomeMax = (imax === null || isNaN(imax)) ? null : imax;
|
|
masjidMembersMin = isNaN(mmin) ? 0 : mmin;
|
|
masjidMembersMax = (mmax === null || isNaN(mmax)) ? null : mmax;
|
|
updateHomeColors(center, masjidCircle ? masjidCircle.getRadius() : defaultRadius);
|
|
}catch(e){console.error(e);} };
|
|
applyBtn && applyBtn.addEventListener('click', applyFn);
|
|
resetBtn && resetBtn.addEventListener('click', function(){ try{ if(miMin) miMin.value='0'; if(miMax) miMax.value=''; if(mmMin) mmMin.value='0'; if(mmMax) mmMax.value=''; masjidIncomeMin=0; masjidIncomeMax=null; masjidMembersMin=0; masjidMembersMax=null; updateHomeColors(center, masjidCircle ? masjidCircle.getRadius() : defaultRadius); }catch(e){} });
|
|
}catch(e){console.error('masjid filter wiring failed', e);}
|
|
}
|
|
try{ document.body.appendChild(masjidControlEl); }catch(e){ console.error('failed to append masjidControlEl', e); }
|
|
slider.addEventListener('input', function(e){ const r = parseInt(e.target.value,10); try{ masjidCircle.setRadius(r); }catch(e){}; const num = document.getElementById('masjid-radius-input'); if(num) num.value = r; updateHomeColors(center, r); });
|
|
// allow user to type radius number directly; only apply during input when within range, enforce bounds on blur/Enter
|
|
try{
|
|
const numInp = document.getElementById('masjid-radius-input');
|
|
if(numInp){
|
|
numInp.addEventListener('input', function(ev){
|
|
const raw = this.value;
|
|
const v = parseInt(raw,10);
|
|
if(isNaN(v)) return; // allow typing
|
|
if(v >= 50 && v <= 3000){
|
|
try{ slider.value = String(v); masjidCircle.setRadius(v); }catch(e){}
|
|
|
|
updateHomeColors(center, v);
|
|
}
|
|
});
|
|
// enforce/clamp on blur
|
|
numInp.addEventListener('blur', function(){
|
|
let v = parseInt(this.value,10);
|
|
if(isNaN(v)){
|
|
// reset to current slider value
|
|
this.value = slider.value;
|
|
return;
|
|
}
|
|
if(v < 50){ v = 50; this.value = v; try{ showToast('Radius minimal 50 m','error',2200); }catch(e){} }
|
|
if(v > 3000){ v = 3000; this.value = v; try{ showToast('Radius maksimal 3000 m','error',2200); }catch(e){} }
|
|
try{ slider.value = String(v); masjidCircle.setRadius(v); }catch(e){}
|
|
updateHomeColors(center, v);
|
|
});
|
|
// also apply on Enter
|
|
numInp.addEventListener('keydown', function(ev){ if(ev.key === 'Enter'){ this.blur(); } });
|
|
}
|
|
}catch(e){}
|
|
closeBtn.addEventListener('click', hideMasjidRadius);
|
|
if(!isGuest){
|
|
if(editBtn){ editBtn.addEventListener('click', function(){ hideMasjidRadius(); openEdit(recId); }); }
|
|
if(delBtn){ delBtn.addEventListener('click', function(){ hideMasjidRadius(); hapus(recId); }); }
|
|
}
|
|
updateHomeColors(center, defaultRadius);
|
|
}
|
|
function hideMasjidRadius(){
|
|
if(masjidCircle){ map.removeLayer(masjidCircle); masjidCircle=null; }
|
|
if(masjidControlEl){ masjidControlEl.remove(); masjidControlEl=null; }
|
|
// restore marker colors: assisted -> green, others -> red (clear masjid-filtering)
|
|
rumahMarkers.forEach(m=>{ try{ const assisted = m._data && (m._data.assisted && parseInt(m._data.assisted)===1); const color = assisted ? '#2e7d32' : '#d32f2f'; if(m && typeof m.setIcon === 'function'){ m.setIcon(createLocationPinIcon(color)); } }catch(e){} });
|
|
}
|
|
function updateHomeColors(centerLatLng, radiusMeters){
|
|
// If masjidCircle exists, prefer its center/radius for coloring
|
|
try{
|
|
if(masjidCircle){ centerLatLng = masjidCircle.getLatLng(); radiusMeters = masjidCircle.getRadius(); }
|
|
}catch(e){}
|
|
rumahMarkers.forEach(m=>{
|
|
try{
|
|
const latlng = m.getLatLng();
|
|
const dist = (centerLatLng && typeof centerLatLng.distanceTo === 'function') ? latlng.distanceTo(centerLatLng) : Infinity;
|
|
const assisted = m._data && (m._data.assisted && parseInt(m._data.assisted)===1);
|
|
// only apply masjid filters when masjidCircle is active AND the filter UI is expanded
|
|
const filtersActive = !!masjidCircle && !!masjidFiltersExpanded;
|
|
const incomeVal = (m._data && m._data.monthly_income !== undefined && m._data.monthly_income !== null && m._data.monthly_income !== '') ? Number(m._data.monthly_income) : null;
|
|
const membersVal = (m._data && m._data.members !== undefined && m._data.members !== null && m._data.members !== '') ? Number(m._data.members) : null;
|
|
const meetsIncome = filtersActive && (incomeVal !== null) && (Number(incomeVal) >= Number(masjidIncomeMin)) && (masjidIncomeMax === null || Number(incomeVal) <= Number(masjidIncomeMax));
|
|
const meetsMembers = filtersActive && (membersVal !== null) && (Number(membersVal) >= Number(masjidMembersMin)) && (masjidMembersMax === null || Number(membersVal) <= Number(masjidMembersMax));
|
|
// Use AND between filters: house must satisfy BOTH income AND members when filters are active
|
|
if(assisted || ((radiusMeters !== undefined && dist <= radiusMeters) && ( (!filtersActive) || (meetsIncome && meetsMembers) ))){
|
|
if(m && typeof m.setIcon === 'function'){ m.setIcon(createLocationPinIcon('#2e7d32')); }
|
|
} else {
|
|
if(m && typeof m.setIcon === 'function'){ m.setIcon(createLocationPinIcon('#d32f2f')); }
|
|
}
|
|
}catch(e){}
|
|
});
|
|
}
|
|
|
|
function loadData(){
|
|
try{ hideMasjidRadius(); }catch(e){}
|
|
fetch('src/api/get_lokasi.php', { credentials: 'same-origin', headers: buildHeaders(null) }).then(r=>r.json()).then(data=>{
|
|
lokasiGroup.clearLayers(); rumahBelumDibantuGroup.clearLayers(); rumahSudahDibantuGroup.clearLayers(); masjidGroup.clearLayers(); rumahMarkers.length=0; lokasiMarkerById = new Map();
|
|
lokasiRecords = Array.isArray(data) ? data : [];
|
|
let rumahCount=0, masjidCount=0;
|
|
lokasiRecords.forEach(item=>{
|
|
const jenis = (item.jenis||'').toLowerCase();
|
|
const alamatEsc = (item.alamat||'');
|
|
const color = isWorshipType(jenis) ? '#1976d2' : '#d32f2f';
|
|
if(jenis==='rumah' || isWorshipType(jenis)){
|
|
const marker = L.marker([item.latitude, item.longitude],{icon:createLocationPinIcon(color)});
|
|
marker._data = item; marker._jenis = jenis;
|
|
lokasiMarkerById.set(getRecordIdKey(item.id), marker);
|
|
let photoHtml = '';
|
|
if(item.photo_path){
|
|
const sep = item.photo_path.indexOf('?') !== -1 ? '&' : '?';
|
|
const photoUrl = item.photo_path + sep + 't=' + Date.now();
|
|
photoHtml = `<div style="margin-top:6px"><a class="popup-image-link" href="${photoUrl}"><img src="${photoUrl}" style="max-width:160px;max-height:120px;cursor:pointer"></a></div>`;
|
|
}
|
|
if(isWorshipType(jenis)){
|
|
masjidCount++;
|
|
masjidGroup.addLayer(marker);
|
|
marker.on('click', ()=>{ showMasjidRadius(marker); });
|
|
} else {
|
|
rumahCount++;
|
|
const assisted = item.assisted && parseInt(item.assisted)===1;
|
|
const statusHtml = assisted ? "<span style='color:green'>Telah menerima bantuan bulan ini</span>" : "<span style='color:orange'>Belum menerima bantuan bulan ini</span>";
|
|
const isGuest = !window.currentUser;
|
|
const canAssistEdit = isCurrentUserManager() || isCurrentUserAdmin();
|
|
const showEditBtn = canEditRecord(item) || (!isGuest && canAssistEdit);
|
|
const actionHtml = showEditBtn
|
|
? `${canEditRecord(item) ? `<button onclick="hapus('${item.id}')">Hapus</button> ` : ''}<button onclick="openEdit('${item.id}')">Edit</button> <button class="popup-close" onclick="closePopupById('${item.id}')">Tutup</button>`
|
|
: `<button class="popup-close" onclick="closePopupById('${item.id}')">Tutup</button>`;
|
|
const popupTitle = isGuest ? 'Lokasi Bantuan' : (getDisplayName(item) || 'Lokasi Bantuan');
|
|
const providerName = item && item.sumber_bantuan_name ? item.sumber_bantuan_name : '';
|
|
const providerType = item && item.sumber_bantuan_jenis ? item.sumber_bantuan_jenis : '';
|
|
const providerPrefixed = providerName ? ((providerType ? (providerType.charAt(0).toUpperCase() + providerType.slice(1)) + ' - ' : '') + providerName) : '';
|
|
const providerHtml = assisted ? `<div style="font-size:13px;color:#475569;margin-top:6px">Pemberi: ${escapeHtml(providerPrefixed || '-')}</div>` : `<div style="font-size:13px;color:#9ca3af;margin-top:6px">Pemberi: -</div>`;
|
|
const popupHtml = `<div class="location-popup location-popup-house"><div class="location-popup-title">${escapeHtml(popupTitle)}</div><div>${statusHtml}</div><div class="location-popup-meta">${escapeHtml(alamatEsc)}</div>${providerHtml}${!isGuest && photoHtml ? `<div class="location-popup-photo">${photoHtml}</div>` : ''}<div class="location-popup-actions">${actionHtml}</div></div>`;
|
|
marker.bindPopup(popupHtml, { closeButton: false, maxWidth: 640, minWidth: 420, autoPanPadding: [24, 24] });
|
|
rumahMarkers.push(marker);
|
|
if(assisted) {
|
|
marker.setIcon(createLocationPinIcon('#2e7d32'));
|
|
rumahSudahDibantuGroup.addLayer(marker);
|
|
} else {
|
|
marker.setIcon(createLocationPinIcon('#d32f2f'));
|
|
rumahBelumDibantuGroup.addLayer(marker);
|
|
}
|
|
}
|
|
lokasiGroup.addLayer(marker);
|
|
} else {
|
|
const marker = L.marker([item.latitude, item.longitude]);
|
|
marker._data = item; marker._jenis = jenis;
|
|
lokasiMarkerById.set(getRecordIdKey(item.id), marker);
|
|
const canAssistEdit = isCurrentUserManager() || isCurrentUserAdmin();
|
|
const showEditBtn2 = canEditRecord(item) || canAssistEdit;
|
|
const actionHtml = showEditBtn2 ? `<button onclick=\"hapus('${item.id}')\">Hapus</button> <button onclick=\"openEdit('${item.id}')\">Edit</button>` : '';
|
|
marker.bindPopup(`<div class="location-popup"><div class="location-popup-title">${escapeHtml(item.nama || '')}</div><div class="location-popup-meta">${escapeHtml(alamatEsc)}</div><div class="location-popup-actions">${actionHtml}<button class="popup-close" onclick=\"closePopupById('${item.id}')\">Tutup</button></div></div>`, { closeButton: false, maxWidth: 520, minWidth: 360, autoPanPadding: [24, 24] });
|
|
lokasiGroup.addLayer(marker);
|
|
}
|
|
});
|
|
try{ const dbg = document.getElementById('debug-overlay'); if(dbg){ dbg.textContent = `Loaded ${lokasiRecords.length} lokasi — Rumah: ${rumahCount}, Masjid: ${masjidCount}`; } }catch(e){}
|
|
try{ updateHomeColors(map.getCenter(), 0); }catch(e){}
|
|
renderDataPanel();
|
|
}).catch(err=>console.error(err));
|
|
}
|
|
|
|
function hapus(id){
|
|
// show confirmation modal with record name (if available)
|
|
fetch('src/api/get_lokasi.php', { credentials: 'same-origin', headers: buildHeaders(null) }).then(r=>r.json()).then(arr=>{
|
|
const rec = (arr||[]).find(x=>String(x.id)===String(id));
|
|
const displayName = rec ? (rec.nama || rec.nama_kk || 'lokasi ini') : 'lokasi ini';
|
|
const html = `<input type="hidden" name="id" value="${id}"><p>Anda yakin ingin menghapus <b>${displayName}</b>? Tindakan ini <b>tidak dapat dibatalkan</b>.</p>`;
|
|
openModal('Konfirmasi Hapus', html, function(values){
|
|
(async function(){
|
|
try{
|
|
const resp = await fetch('src/api/hapus_lokasi.php',{method:'POST',headers:buildHeaders(),body:JSON.stringify({id:id})});
|
|
const jr = await resp.json().catch(()=>null);
|
|
if(jr && jr.success===false){ showToast('Gagal menghapus: ' + (jr.error||'unknown'), 'error', 6000); }
|
|
}catch(e){ console.error('hapus failed', e); showToast('Gagal menghapus lokasi', 'error', 6000); }
|
|
loadData();
|
|
})();
|
|
});
|
|
// style the submit button as destructive for this confirmation
|
|
try{ const submit = document.getElementById('modal-submit'); if(submit){ submit.textContent = 'Hapus'; submit.className = 'btn-destructive'; } }catch(e){}
|
|
}).catch(err=>{ console.error(err); showToast('Gagal memuat data lokasi', 'error', 4000); });
|
|
}
|
|
|
|
function openEdit(id){
|
|
// fetch record then open modal
|
|
fetch('src/api/get_lokasi.php', { credentials: 'same-origin', headers: buildHeaders(null) }).then(r=>r.json()).then(arr=>{
|
|
const rec = (arr||[]).find(x=>String(x.id)===String(id));
|
|
if(!rec){ showToast('Record tidak ditemukan', 'error', 4200); return; }
|
|
const isAdmin = isCurrentUserAdmin();
|
|
const isManager = isCurrentUserManager();
|
|
const isOwner = String(rec && rec.created_by ? rec.created_by : '') === String(window.currentUser && window.currentUser.id ? window.currentUser.id : '');
|
|
const canFullEdit = isAdmin || (isManager && isOwner);
|
|
if(!canFullEdit && !isManager){
|
|
showToast('Anda hanya bisa mengedit data milik Anda sendiri', 'error', 5000);
|
|
return;
|
|
}
|
|
|
|
// If manager but not owner, allow assistance-only edit (restricted modal)
|
|
if(isManager && !isOwner && !isAdmin){
|
|
const hasNamaKk = rec && rec.nama_kk && !/^[-–—\s]+$/.test(String(rec.nama_kk || '').trim());
|
|
const kepalaKeluargaHtml = hasNamaKk ? `<div style="margin-top:8px"><strong>Nama Kepala Keluarga</strong><div style="margin-top:6px">${escapeHtml(rec.nama_kk)}</div></div>` : '';
|
|
const namaFallbackHtml = !hasNamaKk ? `<div><strong>Nama</strong><div style="margin-top:6px">${escapeHtml(getDisplayName(rec) || '')}</div></div>` : '';
|
|
const html = `<input type="hidden" name="id" value="${rec.id}">`+
|
|
`${namaFallbackHtml}`+
|
|
`${kepalaKeluargaHtml}`+
|
|
`<div style="margin-top:8px"><strong>Alamat</strong><div style="margin-top:6px">${escapeHtml(rec.alamat || '')}</div></div>`+
|
|
`${buildAssistanceFieldsHtml(rec)}`;
|
|
// restrict assistance source to worship places created by current manager
|
|
try{ window.assistanceSourceFilter = { createdBy: window.currentUser && window.currentUser.id ? window.currentUser.id : null, worshipOnly: true }; }catch(e){}
|
|
openModal('Edit Bantuan', html, function(values){
|
|
const payload = { id: rec.id };
|
|
// only submit assistance-related fields
|
|
payload.assisted = values.assisted ? 1 : 0;
|
|
payload.last_assisted_at = values.last_assisted_at || null;
|
|
payload.sumber_bantuan = values.sumber_bantuan || '';
|
|
payload.bentuk_bantuan = values.bentuk_bantuan || '';
|
|
payload.assistance_notes = values.assistance_notes || '';
|
|
(async function(){
|
|
try{
|
|
const resp = await fetch('src/api/update_lokasi.php',{method:'POST',headers:buildHeaders(),body:JSON.stringify(payload),credentials:'same-origin'});
|
|
const jr = await resp.json().catch(()=>null);
|
|
if(!resp.ok || (jr && jr.success===false)){
|
|
showToast('Gagal memperbarui data bantuan', 'error', 5000);
|
|
} else {
|
|
try{ showToast('Perubahan bantuan tersimpan', 'success', 3000); }catch(e){}
|
|
}
|
|
}catch(e){ console.error(e); showToast('Gagal memperbarui data bantuan', 'error', 5000); }
|
|
loadData();
|
|
})();
|
|
});
|
|
return;
|
|
}
|
|
|
|
const householdHtml = (rec.jenis === 'rumah') ?
|
|
`<div id="household-fields">`+
|
|
`<label>Nama Kepala Keluarga <input name="nama_kk" type="text" value="${rec.nama_kk||''}" required></label><div class="hf-phone-slot"></div><label>Status Rumah <input name="rumah_status" type="text" value="${rec.rumah_status||''}"></label><label>Jumlah Anggota <input name="members" type="number" value="${rec.members||''}" required></label><label>Penghasilan Bulanan (Rp) <input name="monthly_income" type="number" step="1" min="0" value="${(rec.monthly_income !== undefined && rec.monthly_income !== null && rec.monthly_income !== '') ? Math.round(Number(rec.monthly_income)) : ''}" required></label><div style="font-size:12px;color:#666;margin-top:4px">Masukkan nilai dalam Rupiah, tanpa simbol (contoh: 2000000)</div>`+
|
|
`</div>` : `<div id="household-fields"></div>`;
|
|
|
|
const html = `<input type=\"hidden\" name=\"id\" value=\"${rec.id}\">`+
|
|
`<label>Jenis <select name=\"jenis\" required><option value=\"\">Pilih jenis</option><option value=\"rumah\">Rumah</option><option value=\"masjid\">Masjid</option><option value=\"gereja\">Gereja</option><option value=\"pura\">Pura</option><option value=\"vihara\">Vihara</option><option value=\"klenteng\">Klenteng</option></select></label>`+
|
|
`<div id=\"nama-field\" style=\"display:block\"><label>Nama <input name=\"nama\" type=\"text\" value=\"${rec.nama||''}\"></label></div>`+
|
|
`${householdHtml}`+
|
|
`<label>No Telp <input name=\"no_telp\" type=\"text\" value=\"${rec.no_telp||''}\"></label>`+
|
|
`<label>Alamat <input name=\"alamat\" type=\"text\" value=\"${rec.alamat||''}\"></label>`+
|
|
`${buildAssistanceFieldsHtml(rec)}`+
|
|
`<div id=\"photo-field\" style=\"display:none\"><label>Foto Rumah <input name=\"photo\" type=\"file\" accept=\"image/*\"></label><div id=\"current-photo-name\" style=\"font-size:12px;color:#444;margin-top:6px\"></div></div>`;
|
|
// clear any assistance source filter for full edit
|
|
try{ window.assistanceSourceFilter = null; }catch(e){}
|
|
openModal('Edit Lokasi', html, function(values, file){
|
|
const payload = {id: id, nama: values.nama||'', jenis: values.jenis||'rumah', no_telp: values.no_telp||'', alamat: values.alamat||'', nama_kk: values.nama_kk||null, rumah_status: values.rumah_status||null, members: values.members||null, monthly_income: values.monthly_income||null };
|
|
const assistedOn = values.assisted ? 1 : 0;
|
|
payload.assisted = assistedOn;
|
|
if(assistedOn){
|
|
payload.last_assisted_at = values.last_assisted_at || '';
|
|
payload.sumber_bantuan = values.sumber_bantuan || '';
|
|
payload.bentuk_bantuan = values.bentuk_bantuan || '';
|
|
payload.assistance_notes = values.assistance_notes || '';
|
|
} else {
|
|
payload.last_assisted_at = null;
|
|
payload.sumber_bantuan = null;
|
|
payload.bentuk_bantuan = null;
|
|
payload.assistance_notes = null;
|
|
}
|
|
(async function(){
|
|
let updateSuccess = false;
|
|
try{
|
|
const respUpdate = await fetch('src/api/update_lokasi.php',{method:'POST',headers:buildHeaders(),body:JSON.stringify(payload),credentials:'same-origin'});
|
|
const jrUpdate = await respUpdate.json().catch(()=>null);
|
|
if(!respUpdate.ok || (jrUpdate && jrUpdate.success===false)){
|
|
const reason = jrUpdate && jrUpdate.error ? jrUpdate.error : 'unknown';
|
|
showToast('Gagal menyimpan perubahan: ' + reason, 'error', 6000);
|
|
} else {
|
|
updateSuccess = true;
|
|
// optimistically update marker on the map so UI reflects change immediately
|
|
try{
|
|
const idStr = String(id);
|
|
const m = rumahMarkers.find(x=> x && x._data && String(x._data.id)===idStr);
|
|
if(m){
|
|
try{ m._data = Object.assign({}, m._data || {}, payload); }catch(e){}
|
|
// Recompute colors using centralized logic (includes assisted, masjid radius and filters)
|
|
try{ updateHomeColors(map.getCenter(), 0); }catch(e){}
|
|
// update popup content if present
|
|
try{
|
|
const pd = m._data || {};
|
|
const statusHtml = (pd.assisted && parseInt(pd.assisted)===1) ? "<span style='color:green'>Telah menerima bantuan</span>" : "<span style='color:orange'>Belum menerima bantuan</span>";
|
|
const alamatEsc = pd.alamat || '';
|
|
let photoHtml = '';
|
|
if(pd.photo_path){ const sep = pd.photo_path.indexOf('?') !== -1 ? '&' : '?'; const photoUrl = pd.photo_path + sep + 't=' + Date.now(); photoHtml = `<div style="margin-top:6px"><a class="popup-image-link" href="${photoUrl}"><img src="${photoUrl}" style="max-width:160px;max-height:120px;cursor:pointer"></a></div>`; }
|
|
const nameLabel = pd.nama || pd.nama_kk || '';
|
|
const actionHtml = canEditRecord(pd) ? `<br><button onclick="hapus('${pd.id}')">Hapus</button> <button onclick="openEdit('${pd.id}')">Edit</button>` : '';
|
|
const newPopupHtml = `<b>${nameLabel}</b><br>${statusHtml}<div style="font-size:12px;color:#444;margin-top:6px">${alamatEsc}</div>${photoHtml}${actionHtml}`;
|
|
try{ const p = m.getPopup && m.getPopup(); if(p){ p.setContent(newPopupHtml); } }catch(e){}
|
|
}catch(e){}
|
|
}
|
|
}catch(e){}
|
|
}
|
|
}catch(e){ console.error('update failed', e); showToast('Gagal menyimpan perubahan: ' + (e && e.message ? e.message : e), 'error', 6000); }
|
|
|
|
if(updateSuccess){
|
|
let uploadSuccess = true;
|
|
if(file){
|
|
try{
|
|
const fdata = new FormData(); fdata.append('file', file); fdata.append('id', id);
|
|
const resp = await fetch('src/api/upload_photo.php',{method:'POST',headers:buildHeaders(null),body:fdata,credentials:'same-origin'});
|
|
const jr = await resp.json().catch(()=>null);
|
|
if(!jr || !jr.success){
|
|
uploadSuccess = false;
|
|
const reason = jr && (jr.error || (jr.last_error && jr.last_error.message)) ? (jr.error || jr.last_error.message) : 'unknown';
|
|
showToast('Gagal mengunggah foto: ' + reason, 'error', 7000);
|
|
} else {
|
|
showToast('Foto berhasil diunggah', 'success', 3000);
|
|
}
|
|
}catch(e){
|
|
uploadSuccess = false;
|
|
console.error('upload failed',e);
|
|
showToast('Gagal mengunggah foto: ' + (e && e.message ? e.message : e), 'error', 7000);
|
|
}
|
|
}
|
|
if(uploadSuccess){
|
|
showToast('Perubahan tersimpan', 'success', 2600);
|
|
}
|
|
}
|
|
loadData();
|
|
})();
|
|
});
|
|
// populate household fields
|
|
setTimeout(()=>{
|
|
const sel = document.querySelector('#modal-fields select[name="jenis"]'); if(sel) sel.value = rec.jenis||'rumah';
|
|
// if this record has a fixed jenis (rumah or masjid), prevent changing jenis in edit mode
|
|
try{
|
|
const recJenis = (rec.jenis||'').toLowerCase();
|
|
if(sel && (recJenis==='rumah' || isWorshipType(recJenis))){
|
|
sel.disabled = true;
|
|
// ensure a hidden input exists so the jenis value is submitted
|
|
let existing = document.querySelector('#modal-fields input[type="hidden"][name="jenis"]');
|
|
if(!existing){
|
|
const hin = document.createElement('input'); hin.type='hidden'; hin.name='jenis'; hin.value = rec.jenis||''; document.getElementById('modal-fields').appendChild(hin);
|
|
} else {
|
|
try{ existing.value = rec.jenis||''; }catch(e){}
|
|
}
|
|
} else if(sel){
|
|
sel.disabled = false;
|
|
const existing = document.querySelector('#modal-fields input[type="hidden"][name="jenis"]'); if(existing) try{ existing.remove(); }catch(e){}
|
|
}
|
|
}catch(e){}
|
|
const hf = document.getElementById('household-fields'); const namaField=document.getElementById('nama-field'); const assistanceFields=document.getElementById('assistance-fields'); const photoField=document.getElementById('photo-field');
|
|
function render(){
|
|
if(!hf) return;
|
|
if(sel && sel.value==='rumah'){
|
|
hf.innerHTML = `<label>Nama Kepala Keluarga <input name=\"nama_kk\" type=\"text\" value=\"${rec.nama_kk||''}\" required></label><div class=\"hf-phone-slot\"></div><label>Status Rumah <input name=\"rumah_status\" type=\"text\" value=\"${rec.rumah_status||''}\"></label><label>Jumlah Anggota <input name=\"members\" type=\"number\" value=\"${rec.members||''}\" required></label><label>Penghasilan Bulanan (Rp) <input name=\"monthly_income\" type=\"number\" step=\"1\" min=\"0\" value=\"${(rec.monthly_income !== undefined && rec.monthly_income !== null && rec.monthly_income !== '') ? Math.round(Number(rec.monthly_income)) : ''}\" required></label><div style=\"font-size:12px;color:#666;margin-top:4px\">Masukkan nilai dalam Rupiah, tanpa simbol (contoh: 2000000)</div>`;
|
|
if(namaField) namaField.style.display='none';
|
|
if(assistanceFields) assistanceFields.style.display='block';
|
|
if(photoField) photoField.style.display='block';
|
|
try{ const assistChk = document.querySelector('#modal-fields input[name="assisted"]'); if(assistChk && assistChk.checked === false){ assistChk.dispatchEvent(new Event('change')); } }catch(e){}
|
|
try{
|
|
const slot = hf.querySelector('.hf-phone-slot');
|
|
let phoneInput = document.querySelector('#modal-fields input[name="no_telp"]');
|
|
let phoneLabel = phoneInput ? phoneInput.closest('label') : null;
|
|
if(!phoneLabel){
|
|
// create a labeled phone input inside the slot
|
|
phoneLabel = document.createElement('label');
|
|
phoneLabel.innerHTML = 'No Telp <input name="no_telp" type="text">';
|
|
phoneInput = phoneLabel.querySelector('input[name="no_telp"]');
|
|
if(phoneInput && rec && rec.no_telp) phoneInput.value = rec.no_telp || '';
|
|
}
|
|
// append into slot (dedupe if another exists elsewhere)
|
|
if(slot && phoneLabel && slot.firstChild !== phoneLabel){
|
|
slot.appendChild(phoneLabel);
|
|
}
|
|
// remove any duplicate top-level phone labels (except the one we appended)
|
|
Array.from(document.querySelectorAll('#modal-fields label')).forEach(l=>{
|
|
if(l !== phoneLabel && l.querySelector && l.querySelector('input[name="no_telp"]')){
|
|
try{ l.remove(); }catch(e){}
|
|
}
|
|
});
|
|
}catch(e){}
|
|
} else {
|
|
hf.innerHTML='';
|
|
if(namaField) namaField.style.display='block';
|
|
if(assistanceFields) assistanceFields.style.display='none';
|
|
if(photoField) photoField.style.display='none';
|
|
try{ const assistChk = document.querySelector('#modal-fields input[name="assisted"]'); if(assistChk && assistChk.checked){ assistChk.checked = false; assistChk.dispatchEvent(new Event('change')); } }catch(e){}
|
|
try{
|
|
const moved = hf.querySelector('.hf-phone-slot > label');
|
|
if(moved){ const addr = document.querySelector('#modal-fields input[name=\"alamat\"]'); if(addr && addr.parentNode){ addr.parentNode.parentNode.insertBefore(moved, addr.parentNode); } }
|
|
}catch(e){}
|
|
}
|
|
}
|
|
if(sel) sel.addEventListener('change', render); render();
|
|
try{
|
|
const f = document.querySelector('#modal-fields input[name="nama_kk"]'); if(f) f.value = rec.nama_kk||'';
|
|
const rs = document.querySelector('#modal-fields input[name="rumah_status"]'); if(rs) rs.value = rec.rumah_status||'';
|
|
const mem = document.querySelector('#modal-fields input[name="members"]'); if(mem) mem.value = (rec.members !== undefined && rec.members !== null) ? rec.members : '';
|
|
const mi = document.querySelector('#modal-fields input[name="monthly_income"]');
|
|
if(mi){
|
|
if(rec.monthly_income !== undefined && rec.monthly_income !== null && rec.monthly_income !== ''){
|
|
try{ mi.value = String(Math.round(Number(rec.monthly_income))); }catch(e){ try{ const raw = String(rec.monthly_income); mi.value = raw.replace(/\.00$/,'').replace(/\.0$/,''); }catch(ee){ mi.value = rec.monthly_income; } }
|
|
} else { mi.value = ''; }
|
|
}
|
|
const p = document.querySelector('#modal-fields input[name="no_telp"]'); if(p) p.value = rec.no_telp||'';
|
|
const fa = document.querySelector('#modal-fields input[name="assisted"]'); if(fa) fa.checked = rec.assisted && parseInt(rec.assisted)===1;
|
|
const fd = document.querySelector('#modal-fields input[name="last_assisted_at"]'); if(fd) fd.value = rec.last_assisted_at ? rec.last_assisted_at.split(' ')[0] : '';
|
|
if(fa){ try{ fa.dispatchEvent(new Event('change')); }catch(e){} }
|
|
const fs = document.querySelector('#sumber-bantuan-value');
|
|
if(fs){
|
|
const currentValue = String(rec.sumber_bantuan || '');
|
|
fs.value = currentValue;
|
|
}
|
|
const fb = document.querySelector('#modal-fields select[name="bentuk_bantuan"]'); if(fb) fb.value = rec.bentuk_bantuan||'';
|
|
// bantuan_terakhir removed from schema; skip showing historical assistance
|
|
const fn = document.querySelector('#modal-fields input[name="assistance_notes"]'); if(fn) fn.value = rec.assistance_notes||'';
|
|
try{ setTimeout(()=>{ syncAssistanceSourceSearch(); }, 40); }catch(e){}
|
|
// ensure top-level name and alamat are set as well
|
|
const nameTop = document.querySelector('#modal-fields input[name="nama"]'); if(nameTop) nameTop.value = rec.nama||'';
|
|
const addrTop = document.querySelector('#modal-fields input[name="alamat"]'); if(addrTop) addrTop.value = rec.alamat||'';
|
|
// ensure phone moved into slot after values populated
|
|
requestAnimationFrame(()=>{ try{ movePhoneToSlot(); }catch(e){}; setTimeout(()=>{ try{ movePhoneToSlot(); }catch(e){} },10); });
|
|
// show current photo filename (instead of browser 'No file chosen')
|
|
const photoNameEl = document.getElementById('current-photo-name');
|
|
if(photoNameEl){
|
|
if(rec.photo_path){
|
|
let bn = '';
|
|
let urlRaw = rec.photo_path || '';
|
|
try{
|
|
// If the stored path is the serve_upload fallback, extract the `f` query value
|
|
const m = urlRaw.match(/[?&]f=([^&]+)/);
|
|
if(m && m[1]){
|
|
bn = decodeURIComponent(m[1]);
|
|
}
|
|
}catch(e){}
|
|
if(!bn){ bn = (urlRaw||'').split('/').pop().split('?')[0]; }
|
|
const url = urlRaw + (urlRaw.indexOf('?') !== -1 ? '&' : '?') + 't=' + Date.now();
|
|
photoNameEl.innerHTML = `Foto saat ini: <a href=\"${url}\" target=\"_blank\">${bn}</a>`;
|
|
} else {
|
|
photoNameEl.innerHTML = '';
|
|
}
|
|
}
|
|
// ensure jenis select reflects current value (or placeholder)
|
|
try{ const sel = document.querySelector('#modal-fields select[name="jenis"]'); if(sel){ sel.value = rec.jenis||''; sel.dispatchEvent(new Event('change')); } }catch(e){}
|
|
// after populating values, ensure phone moved into household for rumah
|
|
try{ requestAnimationFrame(()=>{ try{ movePhoneToSlot(); }catch(e){}; setTimeout(()=>{ try{ movePhoneToSlot(); }catch(e){} },10); }); }catch(e){}
|
|
}catch(e){}
|
|
// if this is a place of worship and name empty, attempt reverse geocode to suggest a name
|
|
try{
|
|
if(isWorshipType((rec.jenis||'').toLowerCase()) && (!rec.nama || rec.nama.trim()==='') && rec.latitude && rec.longitude){
|
|
(async function(){ const rev = await reverseGeocode(rec.latitude, rec.longitude); const wt = rev ? detectWorshipType(rev) : null; if(rev && wt){ const nameInp = document.querySelector('#modal-fields input[name="nama"]'); if(nameInp && !nameInp.value) nameInp.value = extractNameFromResult(rev); } })();
|
|
}
|
|
}catch(e){}
|
|
},50);
|
|
});
|
|
}
|
|
|
|
// Leaflet.draw: add draw control for adding markers
|
|
const drawControl = new L.Control.Draw({
|
|
draw: { marker: true, polyline: false, polygon: false, rectangle: false, circle: false, circlemarker: false },
|
|
edit: { featureGroup: lokasiGroup, edit: false, remove: false }
|
|
});
|
|
// image viewer: delegate clicks from popup thumbnails and filename links
|
|
function openImageViewer(url){
|
|
try{
|
|
const viewer = document.getElementById('image-viewer');
|
|
const img = document.getElementById('image-viewer-img');
|
|
if(!viewer || !img) return;
|
|
img.src = url;
|
|
viewer.style.display = 'flex';
|
|
}catch(e){ console.error(e); }
|
|
}
|
|
|
|
function isPreviewableImage(path){
|
|
if(!path) return false;
|
|
return /\.(png|jpe?g|gif|webp|svg)(\?|#|$)/i.test(String(path));
|
|
}
|
|
|
|
function openProofPreview(path){
|
|
if(!path) return;
|
|
if(isPreviewableImage(path)){
|
|
openImageViewer(path + (path.indexOf('?') !== -1 ? '&' : '?') + 't=' + Date.now());
|
|
return;
|
|
}
|
|
window.open(path, '_blank', 'noopener,noreferrer');
|
|
}
|
|
document.addEventListener('click', function(ev){
|
|
try{
|
|
const a = ev.target.closest && ev.target.closest('a');
|
|
if(!a) return;
|
|
const href = a.getAttribute('href') || '';
|
|
if(a.classList.contains('popup-image-link') || href.indexOf('/uploads/')!==-1){
|
|
ev.preventDefault();
|
|
openImageViewer(href + (href.indexOf('?')!==-1 ? '&' : '?') + 't=' + Date.now());
|
|
}
|
|
}catch(e){}
|
|
}, false);
|
|
const ivClose = document.getElementById('image-viewer-close'); if(ivClose){ ivClose.addEventListener('click', function(){ const v=document.getElementById('image-viewer'); const i=document.getElementById('image-viewer-img'); if(v) v.style.display='none'; if(i) i.src=''; }); }
|
|
|
|
// Non-blocking toast helper
|
|
function showToast(message, type='error', timeout=5000){
|
|
try{
|
|
const container = document.getElementById('toast-container'); if(!container) return;
|
|
const t = document.createElement('div'); t.className = 'toast ' + (type||'error'); t.textContent = message;
|
|
container.appendChild(t);
|
|
// show
|
|
requestAnimationFrame(()=> t.classList.add('show'));
|
|
setTimeout(()=>{ t.classList.remove('show'); setTimeout(()=>{ try{ t.remove(); }catch(e){} },200); }, timeout);
|
|
}catch(e){ console.error('toast error', e); }
|
|
}
|
|
|
|
map.addControl(drawControl);
|
|
|
|
map.on(L.Draw.Event.CREATED, function (e) {
|
|
const layer = e.layer;
|
|
if(e.layerType === 'marker'){
|
|
if(!window.currentUser){
|
|
showToast('Harus login terlebih dahulu untuk menambah data', 'error', 4000);
|
|
map.removeLayer(layer);
|
|
return;
|
|
}
|
|
const latlng = layer.getLatLng();
|
|
currentEditingLayer = layer;
|
|
const html = `<label>Jenis <select name="jenis" required><option value="">Pilih jenis</option><option value="rumah">Rumah</option><option value="masjid">Masjid</option><option value="gereja">Gereja</option><option value="pura">Pura</option><option value="vihara">Vihara</option><option value="klenteng">Klenteng</option></select></label>`+
|
|
`<div id="form-details-wrapper" style="display:none"><div id="nama-field"><label>Nama <input name="nama" type="text"></label></div>`+
|
|
`<div id="household-fields"></div>`+
|
|
`<label>No Telp <input name="no_telp" type="text"></label>`+
|
|
`<label>Alamat <input name="alamat" type="text"></label>`+
|
|
`${buildAssistanceFieldsHtml({assisted:false})}`+
|
|
`<div id="photo-field-create"><label>Foto <input name="photo" type="file" accept="image/*"></label></div></div>`;
|
|
openModal('Tambah Lokasi', html, function(values, file){
|
|
(async function(){
|
|
const usedLatLng = (currentEditingLayer && typeof currentEditingLayer.getLatLng === 'function') ? currentEditingLayer.getLatLng() : latlng;
|
|
const payload = { nama: values.nama||'', jenis: values.jenis||'rumah', no_telp: values.no_telp||'', alamat: values.alamat||'', nama_kk: values.nama_kk||null, rumah_status: values.rumah_status||null, members: values.members||null, monthly_income: values.monthly_income||null, latitude: usedLatLng.lat, longitude: usedLatLng.lng };
|
|
const assistedOn = values.assisted ? 1 : 0;
|
|
payload.assisted = assistedOn;
|
|
if(assistedOn){
|
|
payload.last_assisted_at = values.last_assisted_at || '';
|
|
payload.sumber_bantuan = values.sumber_bantuan || '';
|
|
payload.bentuk_bantuan = values.bentuk_bantuan || '';
|
|
payload.assistance_notes = values.assistance_notes || '';
|
|
}
|
|
try{
|
|
const res = await fetch('src/api/tambah_lokasi.php',{method:'POST',headers:buildHeaders(),body:JSON.stringify(payload),credentials:'same-origin'});
|
|
const j = await res.json().catch(()=>null);
|
|
const newId = j && j.id ? j.id : null;
|
|
if(newId && file){
|
|
try{
|
|
const fdata = new FormData(); fdata.append('file', file); fdata.append('id', newId);
|
|
const resp = await fetch('src/api/upload_photo.php',{method:'POST',headers:buildHeaders(null),body:fdata,credentials:'same-origin'});
|
|
const jr = await resp.json().catch(()=>null);
|
|
if(!jr || !jr.success){
|
|
const reason = jr && (jr.error || (jr.last_error && jr.last_error.message)) ? (jr.error || jr.last_error.message) : 'unknown';
|
|
showToast('Gagal mengunggah foto: ' + reason, 'error', 7000);
|
|
} else {
|
|
showToast('Foto berhasil diunggah', 'success', 3000);
|
|
}
|
|
}catch(e){ console.error('upload failed',e); showToast('Gagal mengunggah foto: ' + (e && e.message ? e.message : e), 'error', 7000); }
|
|
}
|
|
}catch(e){ console.error(e); }
|
|
loadData();
|
|
})();
|
|
});
|
|
// populate household fields and wire jenis change like edit modal (run for this modal instance)
|
|
setTimeout(()=>{
|
|
const sel = document.querySelector('#modal-fields select[name="jenis"]'); if(sel) sel.value = '';
|
|
const hf = document.getElementById('household-fields'); const namaField=document.getElementById('nama-field'); const assistanceFields=document.getElementById('assistance-fields'); const photoField=document.getElementById('photo-field-create');
|
|
const detailsWrapper = document.getElementById('form-details-wrapper');
|
|
const assistedSourceSearch = document.getElementById('sumber-bantuan-search');
|
|
function render(){ if(!hf) return; const v = (sel && sel.value)||'';
|
|
if(detailsWrapper) detailsWrapper.style.display = v ? 'block' : 'none';
|
|
|
|
if(sel && v==='rumah'){ hf.innerHTML = `<label>Nama Kepala Keluarga <input name="nama_kk" type="text" required></label><div class="hf-phone-slot"></div><label>Status Rumah <input name="rumah_status" type="text"></label><label>Jumlah Anggota <input name="members" type="number" required></label><label>Penghasilan Bulanan (Rp) <input name="monthly_income" type="number" step="1" min="0" required></label><div style="font-size:12px;color:#666;margin-top:4px">Masukkan nilai dalam Rupiah, tanpa simbol (contoh: 2000000)</div>`; if(namaField) namaField.style.display='none'; if(assistanceFields) assistanceFields.style.display='block'; if(photoField) photoField.style.display='block'; try{ const assistChk = document.querySelector('#modal-fields input[name="assisted"]'); if(assistChk && assistChk.checked === false){ assistChk.dispatchEvent(new Event('change')); } }catch(e){}
|
|
movePhoneToSlot();
|
|
} else if(isWorshipType(v)) { hf.innerHTML=''; if(namaField) namaField.style.display='block'; if(assistanceFields) assistanceFields.style.display='none'; if(photoField) photoField.style.display='none'; try{ const assistChk = document.querySelector('#modal-fields input[name="assisted"]'); if(assistChk && assistChk.checked){ assistChk.checked = false; assistChk.dispatchEvent(new Event('change')); } }catch(e){}
|
|
// require name for worship types
|
|
try{ const nameInput = document.querySelector('#modal-fields input[name="nama"]'); if(nameInput) nameInput.required = true; }catch(e){}
|
|
movePhoneBackBeforeAddress();
|
|
} else if(v) { hf.innerHTML=''; if(namaField) namaField.style.display='block'; if(assistanceFields) assistanceFields.style.display='none'; if(photoField) photoField.style.display='none'; try{ const assistChk = document.querySelector('#modal-fields input[name="assisted"]'); if(assistChk && assistChk.checked){ assistChk.checked = false; assistChk.dispatchEvent(new Event('change')); } }catch(e){}
|
|
// non-worship non-rumah
|
|
try{ const nameInput = document.querySelector('#modal-fields input[name="nama"]'); if(nameInput) nameInput.required = false; }catch(e){}
|
|
movePhoneBackBeforeAddress();
|
|
} }
|
|
syncRequiredFieldMarkers();
|
|
if(sel) sel.addEventListener('change', render); render();
|
|
// ensure phone positioned correctly after populating
|
|
try{ movePhoneToSlot(); }catch(e){}
|
|
},50);
|
|
// do a reverse geocode to prefill address/name where possible
|
|
(async function(){ try{ const rev = await reverseGeocode(latlng.lat, latlng.lng); if(rev && rev.display_name){ const addrInp = document.querySelector('#modal-fields input[name="alamat"]'); if(addrInp){ addrInp.value = rev.display_name; addrInp.dataset.lat = latlng.lat; addrInp.dataset.lon = latlng.lng; } const wt = detectWorshipType(rev); if(wt){ const nameInp = document.querySelector('#modal-fields input[name="nama"]'); if(nameInp && !nameInp.value){ nameInp.value = extractNameFromResult(rev); } } } }catch(e){} })();
|
|
}
|
|
});
|
|
// Layer control (ensure visible)
|
|
const overlays = {
|
|
[buildLayerLegendLabel('Rumah belum dibantu', '#d32f2f')]: rumahBelumDibantuGroup,
|
|
[buildLayerLegendLabel('Rumah sudah dibantu', '#2e7d32')]: rumahSudahDibantuGroup,
|
|
[buildLayerLegendLabel('Tempat Ibadah', '#1976d2', 'marker biru')]: masjidGroup,
|
|
'Lokasi': lokasiGroup
|
|
};
|
|
L.control.layers(null, overlays, { position: 'topright', collapsed: false }).addTo(map);
|
|
setTimeout(attachTopRightControls, 0);
|
|
setTimeout(attachTopRightControls, 50);
|
|
setTimeout(attachTopRightControls, 200);
|
|
|
|
// Tab switching removed - full screen map only
|
|
|
|
try{
|
|
const worshipBtn = document.getElementById('data-view-worship');
|
|
const houseBtn = document.getElementById('data-view-house');
|
|
if(worshipBtn) worshipBtn.addEventListener('click', function(){ setDataPanelView('worship'); });
|
|
if(houseBtn) houseBtn.addEventListener('click', function(){ setDataPanelView('house'); });
|
|
renderDataPanel();
|
|
}catch(e){}
|
|
|
|
try{
|
|
const loginBtn = document.getElementById('auth-login-btn');
|
|
const logoutBtn = document.getElementById('auth-logout-btn');
|
|
const adminBtn = document.getElementById('admin-dashboard-btn');
|
|
const authCancelBtn = document.getElementById('auth-modal-cancel');
|
|
const adminCloseBtn = document.getElementById('admin-modal-close');
|
|
const adminDetailCloseBtn = document.getElementById('admin-detail-close');
|
|
|
|
if(loginBtn) loginBtn.addEventListener('click', function(){ openAuthModal('login'); });
|
|
if(logoutBtn) logoutBtn.addEventListener('click', function(){ logoutUser(); });
|
|
if(adminBtn) adminBtn.addEventListener('click', function(){ openAdminDashboard(); });
|
|
if(authCancelBtn) authCancelBtn.addEventListener('click', function(){ closeAuthModal(); });
|
|
if(adminCloseBtn) adminCloseBtn.addEventListener('click', function(){ closeAdminDashboard(); });
|
|
if(adminDetailCloseBtn) adminDetailCloseBtn.addEventListener('click', function(){ closePendingManagerDetail(); });
|
|
|
|
document.getElementById('auth-modal').addEventListener('click', function(ev){ if(ev.target === this) closeAuthModal(); });
|
|
document.getElementById('admin-modal').addEventListener('click', function(ev){ if(ev.target === this) closeAdminDashboard(); });
|
|
document.getElementById('admin-detail-modal').addEventListener('click', function(ev){ if(ev.target === this) closePendingManagerDetail(); });
|
|
|
|
// Admin dashboard wiring is handled by openAdminDashboard() above.
|
|
}catch(e){}
|
|
|
|
if(window.authToken){
|
|
try{ refreshAuthState(); }catch(e){}
|
|
}
|
|
|
|
function openPendingManagerDetail(u){
|
|
const modal = document.getElementById('admin-detail-modal');
|
|
const title = document.getElementById('admin-detail-title');
|
|
const body = document.getElementById('admin-detail-body');
|
|
if(!modal || !title || !body) return;
|
|
|
|
title.textContent = 'Detail Pendaftaran';
|
|
const proofPath = u.org_proof_path || '';
|
|
const proofPreview = isPreviewableImage(proofPath)
|
|
? `<button type="button" class="btn-primary" onclick='openProofPreview(${JSON.stringify(proofPath)})'>Lihat Foto</button>`
|
|
: (proofPath ? `<a href="${escapeHtml(proofPath)}" target="_blank" class="data-link">Lihat file bukti</a>` : '-');
|
|
body.innerHTML = `
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px">
|
|
<div style="padding:12px;border:1px solid #e2e8f0;border-radius:12px;background:#fff"><div style="font-size:12px;color:#64748b">Nama</div><div style="font-weight:700;color:#0f172a">${escapeHtml(u.name || '-')}</div></div>
|
|
<div style="padding:12px;border:1px solid #e2e8f0;border-radius:12px;background:#fff"><div style="font-size:12px;color:#64748b">Email</div><div style="font-weight:700;color:#0f172a">${escapeHtml(u.email || '-')}</div></div>
|
|
<div style="padding:12px;border:1px solid #e2e8f0;border-radius:12px;background:#fff"><div style="font-size:12px;color:#64748b">Organisasi / Tempat Ibadah</div><div style="font-weight:700;color:#0f172a">${escapeHtml(u.organization || '-')}</div></div>
|
|
<div style="padding:12px;border:1px solid #e2e8f0;border-radius:12px;background:#fff"><div style="font-size:12px;color:#64748b">Alamat Organisasi</div><div style="font-weight:700;color:#0f172a">${escapeHtml(u.org_address || '-')}</div></div>
|
|
<div style="padding:12px;border:1px solid #e2e8f0;border-radius:12px;background:#fff"><div style="font-size:12px;color:#64748b">Nomor Telp Organisasi</div><div style="font-weight:700;color:#0f172a">${escapeHtml(u.org_phone || '-')}</div></div>
|
|
<div style="padding:12px;border:1px solid #e2e8f0;border-radius:12px;background:#fff"><div style="font-size:12px;color:#64748b">Tanggal Daftar</div><div style="font-weight:700;color:#0f172a">${u.created_at ? escapeHtml(formatDateToDMY(u.created_at)) : '-'}</div></div>
|
|
</div>
|
|
<div style="margin-top:14px;padding:12px;border:1px solid #e2e8f0;border-radius:12px;background:#fff">
|
|
<div style="font-size:12px;color:#64748b">Bukti Organisasi</div>
|
|
<div style="font-weight:700;color:#0f172a;margin-top:8px">${proofPreview}</div>
|
|
</div>
|
|
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:18px;padding-top:12px;border-top:1px solid #e2e8f0">
|
|
<button type="button" class="btn-destructive" style="padding:8px 16px;font-size:14px" onclick="rejectPendingUser('${escapeHtml(u.id)}')">Tolak</button>
|
|
<button type="button" class="btn-primary" style="padding:8px 16px;font-size:14px" onclick="approvePendingUser('${escapeHtml(u.id)}')">Setujui</button>
|
|
</div>
|
|
`;
|
|
|
|
modal.classList.remove('hidden');
|
|
modal.setAttribute('aria-hidden','false');
|
|
}
|
|
|
|
function openPendingManagerDetailByIndex(index){
|
|
const items = Array.isArray(window.pendingManagerItems) ? window.pendingManagerItems : [];
|
|
const item = items[index];
|
|
if(!item) return;
|
|
openPendingManagerDetail(item);
|
|
}
|
|
|
|
function closePendingManagerDetail(){
|
|
const modal = document.getElementById('admin-detail-modal');
|
|
if(!modal) return;
|
|
modal.classList.add('hidden');
|
|
modal.setAttribute('aria-hidden','true');
|
|
}
|
|
|
|
try{ setTimeout(()=>{ map.invalidateSize(); }, 0); }catch(e){}
|
|
|
|
// initial load
|
|
loadData();
|
|
|
|
// masjid-specific filters moved into masjid control; floating filter panel removed
|
|
|
|
</script>
|
|
<style>
|
|
@media (max-width: 1200px) {
|
|
#auth-bar {
|
|
right: 12px !important;
|
|
top: 64px !important;
|
|
max-width: calc(100vw - 24px) !important;
|
|
}
|
|
}
|
|
</style>
|
|
</body>
|
|
</html>
|