Files
2026-06-10 19:48:00 +07:00

218 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const Penduduk = (() => {
let all = [], filtered = [], editNIK = null;
const DEMO = [
{ nik:'6171010101010001', nama_lengkap:'Ahmad Suryana', umur:44, status_keluarga:'kepala', pekerjaan:'Buruh Harian', penghasilan:900000, status_ekonomi:'miskin', kondisi_kesehatan:'sehat', pendidikan_terakhir:'SMP', no_kk:'6171010101010001', kecamatan:'Pontianak Timur' },
{ nik:'6171010101010002', nama_lengkap:'Siti Rahma', umur:39, status_keluarga:'istri', pekerjaan:'Ibu RT', penghasilan:0, status_ekonomi:'miskin', kondisi_kesehatan:'sakit_ringan',pendidikan_terakhir:'SD', no_kk:'6171010101010001', kecamatan:'Pontianak Timur' },
{ nik:'6171010101010003', nama_lengkap:'Budi Suryana', umur:12, status_keluarga:'anak', pekerjaan:'Pelajar', penghasilan:0, status_ekonomi:'miskin', kondisi_kesehatan:'sehat', pendidikan_terakhir:'SMP', no_kk:'6171010101010001', kecamatan:'Pontianak Timur' },
{ nik:'6171010101010004', nama_lengkap:'Samsul Bahri', umur:49, status_keluarga:'kepala', pekerjaan:'Pedagang Kaki Lima',penghasilan:1200000,status_ekonomi:'rentan', kondisi_kesehatan:'sehat', pendidikan_terakhir:'SMA_SMK', no_kk:'6171010101010002', kecamatan:'Pontianak Kota' },
{ nik:'6171010101010005', nama_lengkap:'Dewi Kartika', umur:46, status_keluarga:'istri', pekerjaan:'Buruh Cuci', penghasilan:500000, status_ekonomi:'rentan', kondisi_kesehatan:'sehat', pendidikan_terakhir:'SD', no_kk:'6171010101010002', kecamatan:'Pontianak Kota' },
{ nik:'6171010101010006', nama_lengkap:'Maria Magdalena', umur:54, status_keluarga:'kepala', pekerjaan:'Penjual Gorengan',penghasilan:800000, status_ekonomi:'miskin', kondisi_kesehatan:'sakit_berat', pendidikan_terakhir:'SD', no_kk:'6171010101010003', kecamatan:'Pontianak Selatan' },
{ nik:'6171010101010007', nama_lengkap:'Petrus Yulius', umur:19, status_keluarga:'anak', pekerjaan:'Pelajar', penghasilan:0, status_ekonomi:'miskin', kondisi_kesehatan:'sehat', pendidikan_terakhir:'SMA_SMK', no_kk:'6171010101010003', kecamatan:'Pontianak Selatan' },
{ nik:'6171010101010010', nama_lengkap:'Lim Ah Kow', umur:59, status_keluarga:'kepala', pekerjaan:'Tidak Bekerja', penghasilan:0, status_ekonomi:'miskin', kondisi_kesehatan:'sakit_berat', pendidikan_terakhir:'SD', no_kk:'6171010101010005', kecamatan:'Pontianak Kota' },
{ nik:'6171010101010012', nama_lengkap:'Mulyadi', umur:45, status_keluarga:'kepala', pekerjaan:'PNS Gol. III', penghasilan:4500000, status_ekonomi:'mampu', kondisi_kesehatan:'sehat', pendidikan_terakhir:'S1', no_kk:'6171010101010006', kecamatan:'Pontianak Timur' },
{ nik:'6171010101010014', nama_lengkap:'Tan Eng Huat', umur:65, status_keluarga:'kepala', pekerjaan:'Pemulung', penghasilan:600000, status_ekonomi:'miskin', kondisi_kesehatan:'disabilitas', pendidikan_terakhir:'SD', no_kk:'6171010101010008', kecamatan:'Pontianak Kota' },
];
async function load() {
try { all = await Api.get('/penduduk'); } catch { all = DEMO; }
filtered = [...all];
render(filtered);
ge('pf-count').textContent = `${all.length} total`;
}
function search(q) { applyFilter(q); }
function applyFilter(qOverride) {
const q = qOverride ?? gv('pf-search');
const ek = gv('pf-ekonomi');
const ke = gv('pf-kesehatan');
filtered = all.filter(p =>
(!q || p.nama_lengkap?.toLowerCase().includes(q.toLowerCase()) || p.nik?.includes(q)) &&
(!ek || p.status_ekonomi === ek) &&
(!ke || p.kondisi_kesehatan === ke)
);
render(filtered);
ge('pf-count').textContent = `${filtered.length} / ${all.length}`;
}
const pendidikanLabel = { tidak_sekolah:'Tdk Sekolah', SD:'SD', SMP:'SMP', SMA_SMK:'SMA/SMK', D1_D3:'D3', S1:'S1', S2_S3:'S2/S3' };
function render(data) {
const tb = ge('tbody-penduduk');
if (!data.length) { tb.innerHTML = '<tr><td colspan="10"><div class="empty-state"><div class="es-icon">🔍</div><h3>Tidak ada data</h3><p>Coba ubah filter pencarian</p></div></td></tr>'; return; }
tb.innerHTML = data.map(p => `
<tr class="row-${p.status_ekonomi}">
<td class="mono">${p.nik}</td>
<td><strong>${p.nama_lengkap}</strong><div style="font-size:10px;color:var(--muted)">${p.kecamatan||''}</div></td>
<td>${p.umur||'?'} th</td>
<td>${cap(p.status_keluarga)}</td>
<td>${p.pekerjaan||'—'}</td>
<td style="white-space:nowrap;font-family:'JetBrains Mono',monospace;font-size:11px">${rp(p.penghasilan)}</td>
<td>${bdg(p.status_ekonomi,'')}</td>
<td>${bdg(p.kondisi_kesehatan,'')}</td>
<td>${pendidikanLabel[p.pendidikan_terakhir]||p.pendidikan_terakhir||'—'}</td>
<td>
<div style="display:flex;gap:4px">
<button class="btn btn-secondary btn-xs" onclick="Penduduk.showDetail('${p.nik}')" title="Detail">👁</button>
<button class="btn btn-secondary btn-xs" onclick="Penduduk.showForm('${p.nik}')" title="Edit" ${!App.canEdit()?'style="display:none"':''}>✏️</button>
<button class="btn btn-danger btn-xs" onclick="Penduduk.hapus('${p.nik}')" title="Hapus" ${!App.canEdit()?'style="display:none"':''}>🗑</button>
</div>
</td>
</tr>`).join('');
}
async function showForm(nik = null) {
editNIK = nik;
ge('mp-title').textContent = nik ? `✏️ Edit — ${nik}` : ' Tambah Penduduk Baru';
ge('fp-nik').readOnly = !!nik;
await loadKKDropdown();
if (nik) {
try {
const p = await Api.get(`/penduduk/${nik}`);
fillForm(p);
} catch {
const p = all.find(x => x.nik === nik);
if (p) fillForm(p);
}
} else {
['fp-nik','fp-nama','fp-tempat','fp-pekerjaan','fp-ket-sehat'].forEach(id => sv(id,''));
sv('fp-penghasilan','0'); sv('fp-umur','');
ge('fp-warn').classList.add('hidden');
}
App.showModal('modal-penduduk');
}
function fillForm(p) {
sv('fp-nik', p.nik); sv('fp-nama', p.nama_lengkap);
sv('fp-jkel', p.jenis_kelamin); sv('fp-tgl', p.tanggal_lahir?.substring(0,10));
sv('fp-umur', p.umur ? `${p.umur} tahun` : '');
sv('fp-tempat', p.tempat_lahir); sv('fp-agama', p.agama);
sv('fp-status-kk', p.status_keluarga); sv('fp-kawin', p.status_perkawinan);
sv('fp-hidup', p.status_hidup); sv('fp-ekonomi', p.status_ekonomi);
sv('fp-pekerjaan', p.pekerjaan); sv('fp-penghasilan', p.penghasilan || 0);
sv('fp-pendidikan', p.pendidikan_terakhir); sv('fp-stat-pend', p.status_pendidikan);
sv('fp-kesehatan', p.kondisi_kesehatan); sv('fp-ket-sehat', p.keterangan_kesehatan);
if (p.id_keluarga) sv('fp-kk', p.id_keluarga);
}
async function loadKKDropdown() {
const sel = ge('fp-kk');
try {
const list = await Api.get('/keluarga');
sel.innerHTML = list.map(k => `<option value="${k.id}">${k.no_kk}${k.nama_kepala}</option>`).join('');
} catch {
sel.innerHTML = '<option value="1">Demo — Ahmad Suryana</option>';
}
}
function hitungUmur() {
const tgl = gv('fp-tgl');
if (!tgl) return;
const umur = Math.floor((Date.now() - new Date(tgl)) / (365.25*24*3600*1000));
sv('fp-umur', `${umur} tahun`);
// Validasi
const pend = gv('fp-pendidikan'), stat = gv('fp-stat-pend');
const warns = [];
if (umur >= 7 && umur <= 15 && stat === 'tidak_sekolah')
warns.push(`⚠️ Usia ${umur} tahun — seharusnya masih wajib belajar (SD/SMP)`);
if (umur >= 16 && umur <= 18 && stat === 'tidak_sekolah' && ['tidak_sekolah','SD'].includes(pend))
warns.push(`⚠️ Usia ${umur} tahun — disarankan melanjutkan ke SMA/SMK`);
const wEl = ge('fp-warn');
if (warns.length) { wEl.innerHTML = warns.join('<br>'); wEl.classList.remove('hidden'); }
else wEl.classList.add('hidden');
}
async function save() {
const nik = gv('fp-nik');
if (!nik || nik.length !== 16 || !/^\d+$/.test(nik)) { App.toast('NIK harus 16 digit angka','error'); return; }
if (!gv('fp-nama')) { App.toast('Nama lengkap wajib diisi','error'); return; }
if (!gv('fp-tgl')) { App.toast('Tanggal lahir wajib diisi','error'); return; }
const body = {
nik, id_keluarga: gv('fp-kk'),
nama_lengkap: gv('fp-nama'), jenis_kelamin: gv('fp-jkel'),
tanggal_lahir: gv('fp-tgl'), tempat_lahir: gv('fp-tempat'),
agama: gv('fp-agama'), status_keluarga: gv('fp-status-kk'),
status_perkawinan: gv('fp-kawin'), status_hidup: gv('fp-hidup'),
pekerjaan: gv('fp-pekerjaan'), penghasilan: +gv('fp-penghasilan')||0,
status_ekonomi: gv('fp-ekonomi'), pendidikan_terakhir: gv('fp-pendidikan'),
status_pendidikan: gv('fp-stat-pend'), kondisi_kesehatan: gv('fp-kesehatan'),
keterangan_kesehatan: gv('fp-ket-sehat'),
};
try {
if (editNIK) { await Api.put(`/penduduk/${editNIK}`, body); App.toast('Data diperbarui ✅','success'); }
else { await Api.post('/penduduk', body); App.toast('Penduduk ditambahkan ✅','success'); }
App.closeModal('modal-penduduk');
await load();
App.updateHeaderStats();
} catch(e) { App.toast(e.message,'error'); }
}
async function hapus(nik) {
if (!confirm(`Hapus data penduduk NIK ${nik}?\nTindakan ini tidak dapat dibatalkan.`)) return;
try {
await Api.delete(`/penduduk/${nik}`);
App.toast('Data dihapus','success');
load(); App.updateHeaderStats();
} catch(e) { App.toast(e.message,'error'); }
}
async function showDetail(nik) {
ge('dp-title').textContent = 'Memuat…';
ge('detail-panel').classList.add('open');
let p;
try { p = await Api.get(`/penduduk/${nik}`); }
catch { p = all.find(x => x.nik === nik); }
if (!p) { App.toast('Gagal memuat detail','error'); return; }
ge('dp-title').textContent = p.nama_lengkap;
const histori = p.histori_bantuan || [];
ge('dp-body').innerHTML = `
<div class="dp-section">
<div class="dp-section-title">Identitas</div>
${field('NIK', `<span class="mono">${p.nik}</span>`)}
${field('Jenis Kelamin', p.jenis_kelamin === 'L' ? 'Laki-laki' : 'Perempuan')}
${field('Umur', `${p.umur||'?'} tahun`)}
${field('Tempat Lahir', p.tempat_lahir||'—')}
${field('Agama', p.agama||'—')}
${field('Status KK', cap(p.status_keluarga))}
${field('Perkawinan', cap(p.status_perkawinan))}
</div>
<div class="dp-section">
<div class="dp-section-title">Sosial Ekonomi</div>
${field('Pekerjaan', p.pekerjaan||'—')}
${field('Penghasilan', rp(p.penghasilan))}
${field('Status Ekonomi', bdg(p.status_ekonomi,''))}
${field('Pendidikan', p.pendidikan_terakhir?.replace('_','/')||'—')}
${field('Kesehatan', bdg(p.kondisi_kesehatan,''))}
</div>
<div class="dp-section">
<div class="dp-section-title">Lokasi</div>
${field('Kelurahan', p.kelurahan||'—')}
${field('Kecamatan', p.kecamatan||'—')}
${field('Alamat', p.alamat||'—')}
${field('Rumah Ibadah', p.nama_ri||'—')}
</div>
<div class="dp-section">
<div class="dp-section-title">Riwayat Bantuan (${histori.length}x)</div>
${histori.length ? histori.map(h=>`
<div style="background:var(--bg4);border-radius:8px;padding:8px 10px;margin-bottom:6px;font-size:12px">
<strong>${h.nama_bantuan}</strong><span style="color:var(--muted);font-size:11px;float:right">${h.tanggal?.substring(0,10)}</span>
<div style="color:var(--muted);font-size:11px">${h.nama_ri||''} · ${h.jumlah} ${h.satuan||''}</div>
</div>`).join('') : '<p style="color:var(--muted);font-size:12px">Belum pernah menerima bantuan</p>'}
</div>
${App.canEdit() ? `
<div style="display:flex;gap:8px;margin-top:8px">
<button class="btn btn-primary" style="flex:1" onclick="Bantuan.quickSalur('${p.nik}','${p.nama_lengkap}')">📦 Salurkan Bantuan</button>
<button class="btn btn-secondary" onclick="Penduduk.showForm('${p.nik}')">✏️ Edit</button>
</div>` : ''}
`;
}
function field(lbl, val) {
return `<div class="dp-field"><span class="dp-field-lbl">${lbl}</span><span class="dp-field-val">${val}</span></div>`;
}
function closeDetail() { ge('detail-panel').classList.remove('open'); }
return { load, search, applyFilter, showForm, hitungUmur, save, hapus, showDetail, closeDetail };
})();