Start
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
12. DASHBOARD ANALITIK
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
const Dashboard = {
|
||||
toggle(){
|
||||
document.getElementById('dash-panel').classList.toggle('hidden');
|
||||
if(!document.getElementById('dash-panel').classList.contains('hidden')) this.render();
|
||||
},
|
||||
render(){
|
||||
const db=Store.allDb();
|
||||
const warga=db.warga||[], ri=db.rumahIbadah||[], jalan=db.jalan||[], parsil=db.parsil||[];
|
||||
const totalWarga=warga.reduce((s,r)=>s+(parseInt(r.jml_anggota)||1),0);
|
||||
const miskinBerat=warga.filter(r=>r.tingkat_kemiskinan==='Sangat Miskin'||r.tingkat_kemiskinan==='Miskin').length;
|
||||
const blmBansos=warga.filter(r=>r.bansos==='Belum Ada').length;
|
||||
const blmSertif=parsil.filter(r=>r.status==='Belum Bersertifikat').length;
|
||||
const jalanRusak=jalan.filter(r=>r.kondisi==='Rusak Berat'||r.kondisi==='Rusak Ringan').length;
|
||||
const totalRI=ri.length;
|
||||
|
||||
// Radius coverage for Warga layer
|
||||
let coveredKK=0, uncoveredKK=0;
|
||||
const coverageHubs = ri.map(hub=>({ ...hub, count:0 }));
|
||||
warga.forEach(b => {
|
||||
if(!b.lat) return;
|
||||
let nearestDist=Infinity, nearestHub=null;
|
||||
ri.forEach(hub=>{
|
||||
if(!hub.lat) return;
|
||||
const d=L.latLng(b.lat,b.lng).distanceTo(L.latLng(hub.lat,hub.lng));
|
||||
if(d<nearestDist){nearestDist=d;nearestHub=hub;}
|
||||
});
|
||||
if(nearestHub && nearestDist<=(parseFloat(nearestHub.radius)||300)){
|
||||
coveredKK++;
|
||||
const h = coverageHubs.find(h=>h.id===nearestHub.id);
|
||||
if(h) h.count++;
|
||||
} else {
|
||||
uncoveredKK++;
|
||||
}
|
||||
});
|
||||
|
||||
// tingkat kemiskinan breakdown
|
||||
const tkBreak={};
|
||||
CFG.warga.tingkat.forEach(t=>{ tkBreak[t]=warga.filter(r=>r.tingkat_kemiskinan===t).length; });
|
||||
const maxTK=Math.max(...Object.values(tkBreak),1);
|
||||
|
||||
// bansos breakdown
|
||||
const bansosBreak={};
|
||||
CFG.warga.bantuan.forEach(b=>{ const c=warga.filter(r=>r.bansos===b).length; if(c>0) bansosBreak[b]=c; });
|
||||
|
||||
// jalan status breakdown
|
||||
const jalanBreak={};
|
||||
CFG.jalan.statuses.forEach(s=>{ jalanBreak[s]=jalan.filter(r=>r.status===s).length; });
|
||||
|
||||
document.getElementById('dash-body').innerHTML=`
|
||||
<div class="dash-tab-bar">
|
||||
<div class="dash-tab active" onclick="Dashboard.switchTab('ringkasan')">Ringkasan Utama</div>
|
||||
<div class="dash-tab" onclick="Dashboard.switchTab('distribusi')">Distribusi Bansos</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB RINGKASAN -->
|
||||
<div id="dash-panel-ringkasan">
|
||||
<div class="stat-grid">
|
||||
<div class="stat-card c-red"><div class="sc-val">${H.fmNum(warga.length)}</div><div class="sc-label">KK Warga Miskin Terdata</div><div class="sc-sub">${H.fmNum(totalWarga)} jiwa total</div></div>
|
||||
<div class="stat-card c-amber"><div class="sc-val">${H.fmNum(miskinBerat)}</div><div class="sc-label">Miskin & Sangat Miskin</div><div class="sc-sub">${warga.length?Math.round(miskinBerat/warga.length*100):0}% dari total KK</div></div>
|
||||
<div class="stat-card c-accent"><div class="sc-val">${H.fmNum(blmBansos)}</div><div class="sc-label">KK Belum Terima Bansos</div><div class="sc-sub">Perlu intervensi segera</div></div>
|
||||
<div class="stat-card c-teal"><div class="sc-val">${H.fmNum(totalRI)}</div><div class="sc-label">Rumah Ibadah Aktif</div><div class="sc-sub">Agen lapangan terdaftar</div></div>
|
||||
<div class="stat-card c-purple"><div class="sc-val">${H.fmNum(blmSertif)}</div><div class="sc-label">Parsil Belum Bersertifikat</div><div class="sc-sub">Kandidat program PTSL</div></div>
|
||||
<div class="stat-card c-green"><div class="sc-val">${H.fmNum(jalanRusak)}</div><div class="sc-label">Ruas Jalan Rusak</div><div class="sc-sub">Dari ${H.fmNum(jalan.length)} ruas terdata</div></div>
|
||||
</div>
|
||||
|
||||
<div class="dash-sect">
|
||||
<div class="dash-sect-title">Sebaran Tingkat Kemiskinan</div>
|
||||
<table class="risk-table"><thead><tr><th>Kategori</th><th>Jumlah KK</th><th>Proporsi</th></tr></thead><tbody>
|
||||
${CFG.warga.tingkat.map(t=>{
|
||||
const n=tkBreak[t]||0, pct=warga.length?Math.round(n/warga.length*100):0;
|
||||
const clr={'Sangat Miskin':'#f05c5c','Miskin':'#f0a030','Hampir Miskin':'#f0c040','Rentan':'#5b9cf6'}[t];
|
||||
return \`<tr><td>\${t}</td><td>\${H.fmNum(n)} KK</td><td><div class="risk-bar-wrap"><div class="risk-bar"><div class="risk-bar-fill" style="width:\${Math.round(n/maxTK*100)}%;background:\${clr}"></div></div>\${pct}%</div></td></tr>\`;
|
||||
}).join('')}
|
||||
</tbody></table>
|
||||
</div>
|
||||
|
||||
<div class="dash-sect">
|
||||
<div class="dash-sect-title">Penerima Program Bantuan Sosial</div>
|
||||
${Object.entries(bansosBreak).length?Object.entries(bansosBreak).sort((a,b)=>b[1]-a[1]).map(([b,n])=>`
|
||||
<div class="prog-row"><span class="prog-name">${b}</span><span class="prog-count">${n} KK</span></div>`).join('')
|
||||
:'<div style="color:var(--text4);font-size:12px">Belum ada data warga tercatat.</div>'}
|
||||
</div>
|
||||
|
||||
<div class="dash-sect">
|
||||
<div class="dash-sect-title">Status Ruas Jalan</div>
|
||||
<table class="risk-table"><thead><tr><th>Status</th><th>Jumlah Ruas</th><th>Total Panjang</th></tr></thead><tbody>
|
||||
${CFG.jalan.statuses.map(s=>{
|
||||
const ruas=jalan.filter(r=>r.status===s);
|
||||
const pj=ruas.reduce((t,r)=>t+(r.panjang||0),0);
|
||||
return \`<tr><td><span class="badge \${CFG.jalan.badgeClass[s]}">\${s}</span></td><td>\${ruas.length} ruas</td><td>\${H.fmDist(pj)}</td></tr>\`;
|
||||
}).join('')}
|
||||
</tbody></table>
|
||||
</div>
|
||||
|
||||
<div class="dash-sect">
|
||||
<div class="dash-sect-title">Rekomendasi Intervensi Kebijakan</div>
|
||||
<div class="intervensi-grid">
|
||||
<div class="intervensi-card"><div class="ic-label">🏠 Prioritas Bansos</div>
|
||||
<div class="ic-val">${H.fmNum(blmBansos)} KK</div>
|
||||
<div class="ic-sub">Warga belum menerima bantuan apapun</div></div>
|
||||
<div class="intervensi-card"><div class="ic-label">📜 Prioritas PTSL</div>
|
||||
<div class="ic-val">${H.fmNum(blmSertif)} Parsil</div>
|
||||
<div class="ic-sub">Lahan tanpa sertifikat, akses kredit terbatas</div></div>
|
||||
<div class="intervensi-card"><div class="ic-label">🛣️ Prioritas Perbaikan Jalan</div>
|
||||
<div class="ic-val">${H.fmNum(jalanRusak)} Ruas</div>
|
||||
<div class="ic-sub">Jalan rusak ringan-berat perlu perbaikan</div></div>
|
||||
<div class="intervensi-card"><div class="ic-label">🕌 Coverage Rumah Ibadah</div>
|
||||
<div class="ic-val">${H.fmNum(totalRI)} RI Aktif</div>
|
||||
<div class="ic-sub">Total dana sosial: Rp ${H.fmNum(ri.reduce((t,r)=>t+(parseInt(r.dana_sosial)||0),0))}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB DISTRIBUSI BANSOS -->
|
||||
<div id="dash-panel-distribusi" style="display:none">
|
||||
<div class="coverage-grid">
|
||||
<div class="coverage-card">
|
||||
<div class="cc-icon green">✓</div>
|
||||
<div class="cc-info">
|
||||
<div class="cc-val">${coveredKK} KK</div>
|
||||
<div class="cc-label">Terjangkau Hub (≤300m)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="coverage-card">
|
||||
<div class="cc-icon red">✕</div>
|
||||
<div class="cc-info">
|
||||
<div class="cc-val">${uncoveredKK} KK</div>
|
||||
<div class="cc-label">Di Luar Jangkauan Hub</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="coverage-card" style="grid-column:1/3;padding:16px">
|
||||
<div class="cc-icon teal" style="width:40px;height:40px">🕌</div>
|
||||
<div class="cc-info">
|
||||
<div class="cc-val">${totalRI} Rumah Ibadah</div>
|
||||
<div class="cc-label">Agen Distribusi Lapangan Terdaftar</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dash-sect">
|
||||
<div class="dash-sect-title">Beban Distribusi per Rumah Ibadah</div>
|
||||
<table class="hub-table">
|
||||
<thead><tr><th>Nama Hub</th><th>Kapasitas</th><th>Beban KK</th></tr></thead>
|
||||
<tbody>
|
||||
${coverageHubs.sort((a,b)=>b.count-a.count).map(h=>{
|
||||
const clr=CFG.rumahIbadah.color(h);
|
||||
const max = Math.max(...coverageHubs.map(x=>x.count), 1);
|
||||
const pct = Math.round(h.count/max*100);
|
||||
return \`<tr>
|
||||
<td><div class="hub-name-cell"><span class="hub-dot" style="background:\${clr}"></span>\${H.esc(h.nama)}</div></td>
|
||||
<td>\${h.jml_binaan||'-'} KK</td>
|
||||
<td>
|
||||
<div class="hub-count-bar">
|
||||
<div class="hub-bar"><div class="hub-bar-fill" style="width:\${pct}%;background:\${pct>80?'var(--amber)':clr}"></div></div>
|
||||
<span>\${h.count} KK</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>\`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
${coverageHubs.length===0?'<div style="text-align:center;color:var(--text4);font-size:11px;padding:20px">Belum ada data Rumah Ibadah.</div>':''}
|
||||
</div>
|
||||
</div>`;
|
||||
},
|
||||
|
||||
switchTab(tabId){
|
||||
document.querySelectorAll('.dash-tab').forEach(t=>t.classList.remove('active'));
|
||||
document.querySelectorAll('[id^="dash-panel-"]').forEach(p=>p.style.display='none');
|
||||
|
||||
if(tabId === 'ringkasan'){
|
||||
document.querySelectorAll('.dash-tab')[0].classList.add('active');
|
||||
document.getElementById('dash-panel-ringkasan').style.display='block';
|
||||
} else {
|
||||
document.querySelectorAll('.dash-tab')[1].classList.add('active');
|
||||
document.getElementById('dash-panel-distribusi').style.display='block';
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
8. DRAW MANAGER
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
const Draw = (() => {
|
||||
let _map, _drawn, _pending, _handler, _active=false;
|
||||
const BAR=()=>document.getElementById('draw-bar');
|
||||
const TXT=()=>document.getElementById('draw-indicator-text');
|
||||
|
||||
function init(m){
|
||||
_map=m; _drawn=new L.FeatureGroup().addTo(_map);
|
||||
_map.on(L.Draw.Event.CREATED, e=>{
|
||||
_drawn.clearLayers(); _drawn.addLayer(e.layer);
|
||||
_pending=e.layer; stop(); Modal.openAdd(App.state.activeLayer, e.layer);
|
||||
});
|
||||
_map.on('click', e=>{
|
||||
if(!_active||App.state.activeLayer!=='spbu'&&App.state.activeLayer!=='warga'&&App.state.activeLayer!=='rumahIbadah') return;
|
||||
const mk=L.marker(e.latlng);
|
||||
_drawn.clearLayers(); _drawn.addLayer(mk);
|
||||
_pending=mk; stop(); Modal.openAdd(App.state.activeLayer, mk);
|
||||
});
|
||||
document.getElementById('btn-cancel').addEventListener('click', cancel);
|
||||
}
|
||||
|
||||
function start(ln){
|
||||
_active=true;
|
||||
const msgs={spbu:'Klik peta untuk menandai lokasi SPBU',jalan:'Klik titik-titik ruas jalan — klik ganda untuk selesai',parsil:'Klik titik-titik batas kavling — klik ganda untuk selesai',warga:'Klik peta untuk menandai lokasi rumah warga miskin',rumahIbadah:'Klik peta untuk menandai lokasi rumah ibadah'};
|
||||
document.getElementById('draw-text').textContent=msgs[ln]||'Mode gambar aktif';
|
||||
BAR().classList.remove('hidden');
|
||||
if(ln==='jalan'){ _handler=new L.Draw.Polyline(_map,{shapeOptions:{color:'#f0a030',weight:4}}); _handler.enable(); }
|
||||
else if(ln==='parsil'){ _handler=new L.Draw.Polygon(_map,{shapeOptions:{color:'#9b7bfa',weight:2,fillOpacity:.2}}); _handler.enable(); }
|
||||
}
|
||||
|
||||
function stop(){ _active=false; BAR().classList.add('hidden'); if(_handler){_handler.disable();_handler=null;} }
|
||||
function cancel(){ stop(); _drawn.clearLayers(); _pending=null; }
|
||||
function clearPending(){ _drawn.clearLayers(); _pending=null; }
|
||||
|
||||
return {init, start, stop, cancel, clearPending};
|
||||
})();
|
||||
@@ -0,0 +1,63 @@
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
13. MAP RENDER
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
function renderMap(){
|
||||
const db=Store.allDb();
|
||||
// SPBU
|
||||
App._groups.spbu.clearLayers();
|
||||
db.spbu.forEach(r=>{
|
||||
const m=L.circleMarker([r.lat,r.lng],{radius:9,fillColor:CFG.spbu.color(r),color:'#fff',weight:2,fillOpacity:1});
|
||||
m.bindPopup(Popup.spbu(r),{maxWidth:320});
|
||||
m.on('click',()=>highlightCard(r.id));
|
||||
App._groups.spbu.addLayer(m);
|
||||
});
|
||||
// Warga
|
||||
App._groups.warga.clearLayers();
|
||||
db.warga.forEach(r=>{
|
||||
const clr=CFG.warga.color(r);
|
||||
// Check if in any RI radius
|
||||
const inRadius=db.rumahIbadah.some(hub=>{
|
||||
if(!hub.lat) return false;
|
||||
const d=L.latLng(r.lat,r.lng).distanceTo(L.latLng(hub.lat,hub.lng));
|
||||
return d<=(parseFloat(hub.radius)||300);
|
||||
});
|
||||
const iconHtml=`<div style="width:22px;height:22px;background:${clr};border:2px solid #fff;border-radius:50%;
|
||||
display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(0,0,0,.4);
|
||||
font-size:11px;line-height:1">👨👩👧</div>${inRadius?'':'<div style="position:absolute;top:-4px;right:-4px;width:10px;height:10px;background:#f05c5c;border-radius:50%;border:1px solid #fff"></div>'}`;
|
||||
const icon=L.divIcon({html:`<div style="position:relative">${iconHtml}</div>`,className:'',iconSize:[22,22],iconAnchor:[11,11]});
|
||||
const m=L.marker([r.lat,r.lng],{icon});
|
||||
m.bindPopup(Popup.warga(r),{maxWidth:360});
|
||||
m.on('click',()=>highlightCard(r.id));
|
||||
App._groups.warga.addLayer(m);
|
||||
});
|
||||
// Rumah Ibadah
|
||||
App._groups.rumahIbadah.clearLayers();
|
||||
db.rumahIbadah.forEach(r=>{
|
||||
const clr=CFG.rumahIbadah.color(r);
|
||||
const m=L.circleMarker([r.lat,r.lng],{radius:10,fillColor:clr,color:'#fff',weight:2.5,fillOpacity:1});
|
||||
m.bindPopup(Popup.rumahIbadah(r),{maxWidth:320});
|
||||
m.on('click',()=>highlightCard(r.id));
|
||||
App._groups.rumahIbadah.addLayer(m);
|
||||
// Always draw radius 300m coverage circle
|
||||
const radiusM=parseFloat(r.radius)||300;
|
||||
const ring=L.circle([r.lat,r.lng],{radius:radiusM,color:clr,weight:1.5,fillColor:clr,fillOpacity:.06,dashArray:'6 4'});
|
||||
App._groups.rumahIbadah.addLayer(ring);
|
||||
});
|
||||
// Jalan
|
||||
App._groups.jalan.clearLayers();
|
||||
db.jalan.forEach(r=>{
|
||||
const l=L.polyline(r.coords,{color:CFG.jalan.color(r),weight:4,opacity:.9});
|
||||
l.bindPopup(Popup.jalan(r),{maxWidth:300});
|
||||
l.on('click',()=>highlightCard(r.id));
|
||||
App._groups.jalan.addLayer(l);
|
||||
});
|
||||
// Parsil
|
||||
App._groups.parsil.clearLayers();
|
||||
db.parsil.forEach(r=>{
|
||||
const clr=CFG.parsil.color(r);
|
||||
const p=L.polygon(r.coords,{color:clr,weight:2,fillColor:clr,fillOpacity:.2});
|
||||
p.bindPopup(Popup.parsil(r),{maxWidth:300});
|
||||
p.on('click',()=>highlightCard(r.id));
|
||||
App._groups.parsil.addLayer(p);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
9. MODAL MANAGER
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
const Modal = (() => {
|
||||
let _ln=null, _editId=null, _geo=null;
|
||||
const ov=()=>document.getElementById('modal-ov');
|
||||
|
||||
function openAdd(ln, leafletLayer){
|
||||
_ln=ln; _editId=null; _geo=leafletLayer;
|
||||
const cfg=CFG[ln];
|
||||
document.getElementById('modal-icon').textContent=cfg.icon;
|
||||
document.getElementById('modal-title').textContent='Tambah '+cfg.label;
|
||||
document.getElementById('modal-sub').textContent='Isi atribut data baru';
|
||||
let html='';
|
||||
if(ln==='spbu') html=Forms.spbu(null, leafletLayer.getLatLng());
|
||||
else if(ln==='jalan') html=Forms.jalan(null, H.geoLen(leafletLayer.getLatLngs()));
|
||||
else if(ln==='parsil') html=Forms.parsil(null, H.geoArea(leafletLayer.getLatLngs()[0]));
|
||||
else if(ln==='warga') html=Forms.warga(null, leafletLayer.getLatLng());
|
||||
else if(ln==='rumahIbadah') html=Forms.rumahIbadah(null, leafletLayer.getLatLng());
|
||||
document.getElementById('modal-body').innerHTML=html;
|
||||
_bindRadio(); _bindFormTabs(); _bindHistBtn(); ov().classList.remove('hidden');
|
||||
setTimeout(()=>document.getElementById('f-nama')?.focus(),60);
|
||||
}
|
||||
|
||||
function openEdit(ln, id){
|
||||
_ln=ln; _editId=id; _geo=null;
|
||||
const rec=Store.find(ln,id); if(!rec) return;
|
||||
const cfg=CFG[ln];
|
||||
document.getElementById('modal-icon').textContent=cfg.icon;
|
||||
document.getElementById('modal-title').textContent='Edit '+cfg.label;
|
||||
document.getElementById('modal-sub').textContent=H.esc(rec.nama);
|
||||
let html='';
|
||||
if(ln==='spbu') html=Forms.spbu(rec,null);
|
||||
else if(ln==='jalan') html=Forms.jalan(rec,null);
|
||||
else if(ln==='parsil') html=Forms.parsil(rec,null);
|
||||
else if(ln==='warga') html=Forms.warga(rec,null);
|
||||
else if(ln==='rumahIbadah') html=Forms.rumahIbadah(rec,null);
|
||||
document.getElementById('modal-body').innerHTML=html;
|
||||
_bindRadio(); _bindFormTabs(); _bindHistBtn(); ov().classList.remove('hidden');
|
||||
setTimeout(()=>document.getElementById('f-nama')?.focus(),60);
|
||||
}
|
||||
|
||||
function close(){ ov().classList.add('hidden'); Draw.clearPending(); }
|
||||
|
||||
function submit(){
|
||||
const data=Forms.readAll(_ln); if(!data) return;
|
||||
if(_editId){
|
||||
data.updatedAt=H.today();
|
||||
Store.update(_ln,_editId,data);
|
||||
Toast.success('Data berhasil diperbarui');
|
||||
} else {
|
||||
const geo=_geo;
|
||||
if(_ln==='spbu'||_ln==='warga'||_ln==='rumahIbadah'){
|
||||
const ll=geo.getLatLng(); data.lat=ll.lat; data.lng=ll.lng;
|
||||
} else if(_ln==='jalan'){
|
||||
data.coords=geo.getLatLngs().map(l=>[l.lat,l.lng]);
|
||||
} else if(_ln==='parsil'){
|
||||
data.coords=geo.getLatLngs()[0].map(l=>[l.lat,l.lng]);
|
||||
}
|
||||
data.id=H.uid(); data.createdAt=H.today(); data.updatedAt=H.today();
|
||||
Store.add(_ln,data);
|
||||
Toast.success('Data berhasil disimpan');
|
||||
}
|
||||
close(); App.refresh();
|
||||
}
|
||||
|
||||
function _bindRadio(){
|
||||
document.querySelectorAll('.ropt').forEach(el=>{
|
||||
el.addEventListener('click',()=>{
|
||||
const parent=el.closest('.rtoggle'); if(!parent) return;
|
||||
parent.querySelectorAll('.ropt').forEach(r=>r.classList.remove('yes','no'));
|
||||
const v=el.dataset.v; el.classList.add(v==='true'?'yes':'no');
|
||||
const inp=el.querySelector('input'); if(inp) inp.checked=true;
|
||||
});
|
||||
});
|
||||
// Bind penyakit checkboxes
|
||||
document.querySelectorAll('.penyakit-check input').forEach(inp=>{
|
||||
inp.addEventListener('change', e=>{
|
||||
if(e.target.checked) e.target.parentElement.classList.add('checked');
|
||||
else e.target.parentElement.classList.remove('checked');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _bindFormTabs(){
|
||||
const tabs=document.querySelectorAll('.form-tab');
|
||||
if(!tabs.length) return;
|
||||
tabs.forEach(tab=>{
|
||||
tab.addEventListener('click',()=>{
|
||||
tabs.forEach(t=>t.classList.remove('active'));
|
||||
document.querySelectorAll('.form-section-panel').forEach(p=>p.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
const panelId='panel-'+tab.dataset.panel;
|
||||
document.getElementById(panelId)?.classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _bindHistBtn(){
|
||||
const btn=document.getElementById('btn-add-hist');
|
||||
if(!btn) return;
|
||||
btn.addEventListener('click',()=>{
|
||||
const tbody=document.getElementById('hist-tbody');
|
||||
if(!tbody) return;
|
||||
const tr=document.createElement('tr');
|
||||
tr.innerHTML=`
|
||||
<td><input class="finput hist-program" style="padding:3px 6px;font-size:10px" placeholder="cth: PKH"/></td>
|
||||
<td><input class="finput hist-tahun" style="padding:3px 6px;font-size:10px;width:70px" type="number" min="2000" max="2030" placeholder="${new Date().getFullYear()}"/></td>
|
||||
<td><select class="finput hist-status" style="padding:3px 6px;font-size:10px">
|
||||
<option>Aktif</option><option>Berhenti</option><option>Dalam Proses</option>
|
||||
</select></td>
|
||||
<td><input class="finput hist-nominal" style="padding:3px 6px;font-size:10px" type="number" placeholder="Rp"/></td>
|
||||
<td><button class="btn-del-row" type="button" onclick="this.closest('tr').remove()">✕</button></td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
return {openAdd, openEdit, close, submit};
|
||||
})();
|
||||
@@ -0,0 +1,31 @@
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
11. SIDEBAR
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
function renderSidebar(ln){
|
||||
const cfg=CFG[ln];
|
||||
document.getElementById('sb-title').textContent=cfg.label;
|
||||
document.getElementById('sb-sub').textContent=cfg.sub;
|
||||
const data=Store.all(ln);
|
||||
const list=document.getElementById('sb-list');
|
||||
if(!data.length){
|
||||
list.innerHTML=`<div class="empty"><svg width="44" height="44" viewBox="0 0 24 24" fill="currentColor"><path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg><p>Belum ada data.<br/>Klik <strong>Tambah</strong> untuk mulai digitasi.</p></div>`;
|
||||
return;
|
||||
}
|
||||
list.innerHTML=data.map(r=>Card[ln](r)).join('');
|
||||
}
|
||||
|
||||
function updateBadges(){
|
||||
const db=Store.allDb();
|
||||
['spbu','jalan','parsil','warga','rumahIbadah'].forEach(k=>{
|
||||
const el=document.getElementById('badge-'+k);
|
||||
if(el) el.textContent=db[k]?.length||0;
|
||||
});
|
||||
const total=Object.values(db).reduce((s,a)=>s+a.length,0);
|
||||
document.getElementById('total-count').textContent=total+' fitur';
|
||||
}
|
||||
|
||||
function highlightCard(id){
|
||||
document.querySelectorAll('.dcard').forEach(c=>c.classList.remove('active'));
|
||||
const c=document.getElementById('card-'+id);
|
||||
if(c){ c.classList.add('active'); c.scrollIntoView({behavior:'smooth',block:'nearest'}); }
|
||||
}
|
||||
Reference in New Issue
Block a user