378 lines
16 KiB
PHP
378 lines
16 KiB
PHP
<?php
|
|
$formatJarak = function ($meter): string {
|
|
if ($meter === null || $meter === '') return '-';
|
|
$meter = (float)$meter;
|
|
if ($meter >= 1000) {
|
|
return number_format($meter / 1000, 1, ',', '.') . ' km';
|
|
}
|
|
return number_format($meter, 0, ',', '.') . ' m';
|
|
};
|
|
|
|
$jsonFlags = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
|
|
$totalBlind = count($data);
|
|
$sudahDitugaskan = (int)($ringkasanPenugasan['sudah'] ?? 0);
|
|
$belumDitugaskan = (int)($ringkasanPenugasan['belum'] ?? 0);
|
|
$totalRumahIbadah = (int)($ringkasanPenugasan['total_ri'] ?? 0);
|
|
?>
|
|
|
|
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:20px;margin-bottom:20px">
|
|
<div>
|
|
<h2 style="font-size:1.2rem;font-weight:700">Monitoring Blind Spot</h2>
|
|
<p class="text-muted text-sm">Atur penugasan rumah ibadah untuk warga yang berada di luar radius pelayanan.</p>
|
|
</div>
|
|
<?php if (!empty($perKec)): ?>
|
|
<div style="display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end">
|
|
<?php foreach ($perKec as $k): ?>
|
|
<div style="text-align:center;padding:8px 14px;background:#f3e8ff;border-radius:8px;border:1px solid #d8b4fe;min-width:110px">
|
|
<div style="font-size:1.05rem;font-weight:800;color:#7c3aed"><?= number_format((int)$k['jumlah']) ?></div>
|
|
<div style="font-size:0.7rem;color:#6d28d9"><?= e($k['nama']) ?></div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="stats-grid" style="grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin-bottom:20px">
|
|
<div class="stat-card">
|
|
<div class="stat-card-icon purple">BS</div>
|
|
<div class="stat-card-body">
|
|
<div class="stat-card-value"><?= number_format($totalBlind) ?></div>
|
|
<div class="stat-card-label">Total blind spot</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-card-icon red">!</div>
|
|
<div class="stat-card-body">
|
|
<div class="stat-card-value"><?= number_format($belumDitugaskan) ?></div>
|
|
<div class="stat-card-label">Belum ditugaskan</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-card-icon green">OK</div>
|
|
<div class="stat-card-body">
|
|
<div class="stat-card-value"><?= number_format($sudahDitugaskan) ?></div>
|
|
<div class="stat-card-label">Sudah ditugaskan</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-card-icon blue">RI</div>
|
|
<div class="stat-card-body">
|
|
<div class="stat-card-value"><?= number_format($totalRumahIbadah) ?></div>
|
|
<div class="stat-card-label">Rumah ibadah aktif</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-bottom:20px">
|
|
<div class="card-header">
|
|
<span class="card-icon">MAP</span>
|
|
<h3>Peta Blind Spot dan Rumah Ibadah</h3>
|
|
</div>
|
|
<div class="card-body no-pad">
|
|
<div id="blind-map" style="height:360px"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<span class="card-icon">BS</span>
|
|
<h3>Daftar Warga Blind Spot (<?= number_format($totalBlind) ?>)</h3>
|
|
<div class="card-actions">
|
|
<input type="text" id="search-blind" class="form-control" placeholder="Cari warga atau rumah ibadah..." style="width:260px">
|
|
</div>
|
|
</div>
|
|
<div class="card-body no-pad">
|
|
<div class="table-wrap">
|
|
<table id="table-blind">
|
|
<thead>
|
|
<tr>
|
|
<th>#</th>
|
|
<th>Warga</th>
|
|
<th>Lokasi</th>
|
|
<th>Prioritas</th>
|
|
<th>Rekomendasi Terdekat</th>
|
|
<th>Penugasan</th>
|
|
<th>Aksi</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data as $i => $w): ?>
|
|
<?php
|
|
$rowRekomendasi = $rekomendasi[(int)$w['id']] ?? [];
|
|
$top = $rowRekomendasi[0] ?? null;
|
|
$currentRiId = (int)($w['id_rumah_ibadah'] ?? 0);
|
|
$isTopAssigned = $top && $currentRiId === (int)$top['id'];
|
|
$modalPayload = [
|
|
'id' => (int)$w['id'],
|
|
'nama' => $w['nama'],
|
|
'alamat' => $w['alamat'] ?? '',
|
|
'lokasi' => trim(($w['kecamatan'] ?? '') . ' / ' . ($w['kelurahan'] ?? ''), ' /'),
|
|
'current_ri_id' => $currentRiId
|
|
];
|
|
?>
|
|
<tr id="warga-row-<?= (int)$w['id'] ?>">
|
|
<td class="text-xs text-muted"><?= $i + 1 ?></td>
|
|
<td>
|
|
<div style="font-weight:700"><?= e($w['nama']) ?></div>
|
|
<div class="text-xs text-muted" style="max-width:220px"><?= e($w['alamat'] ?? '') ?></div>
|
|
<div class="text-xs text-muted" style="font-family:var(--font-mono);margin-top:3px">
|
|
<?= e((string)$w['lat']) ?>, <?= e((string)$w['lng']) ?>
|
|
</div>
|
|
</td>
|
|
<td class="text-sm">
|
|
<div><?= e($w['kecamatan'] ?? '') ?></div>
|
|
<div class="text-xs text-muted"><?= e($w['kelurahan'] ?? '') ?></div>
|
|
</td>
|
|
<td>
|
|
<?php $sc = ['Miskin Ekstrem' => 'badge-red', 'Miskin' => 'badge-orange', 'Rentan Miskin' => 'badge-yellow']; ?>
|
|
<?php $pc = ['Tinggi' => 'badge-red', 'Sedang' => 'badge-yellow', 'Rendah' => 'badge-green']; ?>
|
|
<div style="display:flex;flex-direction:column;gap:5px;align-items:flex-start">
|
|
<span class="badge <?= $sc[$w['status_kemiskinan']] ?? 'badge-gray' ?>"><?= e($w['status_kemiskinan']) ?></span>
|
|
<span class="badge <?= $pc[$w['level_prioritas']] ?? 'badge-gray' ?>">Prioritas <?= e($w['level_prioritas']) ?></span>
|
|
<span class="text-xs text-muted">Skor <?= number_format((int)$w['skor_prioritas']) ?></span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<?php if ($top): ?>
|
|
<div style="min-width:260px">
|
|
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
|
<strong><?= e($top['nama']) ?></strong>
|
|
<span class="badge badge-blue"><?= e($formatJarak($top['jarak_meter'])) ?></span>
|
|
</div>
|
|
<div class="text-xs text-muted" style="margin-top:3px">
|
|
<?= e($top['jenis']) ?> · <?= e($top['kelurahan']) ?>, <?= e($top['kecamatan']) ?>
|
|
</div>
|
|
<div class="text-xs text-muted" style="margin-top:3px">
|
|
Radius <?= e($formatJarak($top['radius_meter'])) ?> · <?= number_format((int)$top['jml_warga']) ?> warga binaan
|
|
</div>
|
|
<?php if (count($rowRekomendasi) > 1): ?>
|
|
<div class="text-xs text-muted" style="margin-top:6px">
|
|
Alternatif:
|
|
<?php foreach (array_slice($rowRekomendasi, 1) as $alt): ?>
|
|
<span><?= e($alt['nama']) ?> (<?= e($formatJarak($alt['jarak_meter'])) ?>)</span>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php else: ?>
|
|
<span class="text-sm text-muted">Koordinat warga atau rumah ibadah belum lengkap.</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<?php if (!empty($w['assigned_rumah_ibadah'])): ?>
|
|
<div style="min-width:210px">
|
|
<span class="badge badge-green">Sudah ditugaskan</span>
|
|
<div style="font-weight:700;margin-top:6px"><?= e($w['assigned_rumah_ibadah']) ?></div>
|
|
<div class="text-xs text-muted"><?= e($w['assigned_jenis'] ?? 'Rumah Ibadah') ?></div>
|
|
</div>
|
|
<?php else: ?>
|
|
<div style="min-width:180px">
|
|
<span class="badge badge-red">Belum ditugaskan</span>
|
|
<div class="text-xs text-muted" style="margin-top:6px">Gunakan rekomendasi atau pilih manual.</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<div style="display:flex;flex-direction:column;gap:8px;min-width:175px">
|
|
<?php if ($top && !$isTopAssigned): ?>
|
|
<form method="POST" action="<?= APP_URL ?>/dinsos/blind-spot/<?= (int)$w['id'] ?>/assign" onsubmit="return confirm('Gunakan rekomendasi rumah ibadah terdekat untuk warga ini?')">
|
|
<?= csrfField() ?>
|
|
<input type="hidden" name="id_rumah_ibadah" value="<?= (int)$top['id'] ?>">
|
|
<button type="submit" class="btn btn-success btn-sm" style="width:100%;justify-content:center">Gunakan rekomendasi</button>
|
|
</form>
|
|
<?php elseif ($top): ?>
|
|
<span class="badge badge-green" style="justify-content:center;padding:6px 10px">Rekomendasi aktif</span>
|
|
<?php endif; ?>
|
|
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary btn-sm"
|
|
style="width:100%;justify-content:center"
|
|
onclick='openAssignModal(<?= json_encode($modalPayload, $jsonFlags) ?>)'
|
|
<?= empty($rumahIbadah) ? 'disabled' : '' ?>>
|
|
Pilih manual
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php if (empty($data)): ?>
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">OK</div>
|
|
<h3>Tidak Ada Blind Spot</h3>
|
|
<p>Semua warga sudah berada dalam radius pelayanan rumah ibadah.</p>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-overlay" id="modal-assign">
|
|
<div class="modal" style="max-width:620px">
|
|
<div class="modal-header">
|
|
<h3>Atur Penugasan Blind Spot</h3>
|
|
<button class="modal-close" onclick="App.hideModal('modal-assign')">×</button>
|
|
</div>
|
|
<form method="POST" id="assign-form">
|
|
<?= csrfField() ?>
|
|
<div class="modal-body">
|
|
<div class="form-group">
|
|
<label class="form-label">Warga</label>
|
|
<div style="padding:12px 14px;border:1px solid var(--clr-border);border-radius:var(--radius-sm);background:#f8fafc">
|
|
<div id="assign-warga-name" style="font-weight:700"></div>
|
|
<div id="assign-warga-location" class="text-xs text-muted" style="margin-top:3px"></div>
|
|
<div id="assign-warga-address" class="text-xs text-muted" style="margin-top:3px"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Rekomendasi terdekat</label>
|
|
<div id="assign-recommendations" style="display:flex;flex-direction:column;gap:8px"></div>
|
|
<div class="form-hint">Klik salah satu rekomendasi untuk mengisi pilihan manual di bawah.</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Pilih rumah ibadah <span class="required">*</span></label>
|
|
<select name="id_rumah_ibadah" id="assign-ri" class="form-control" required>
|
|
<option value="">-- Pilih rumah ibadah --</option>
|
|
<?php foreach ($rumahIbadah as $ri): ?>
|
|
<option value="<?= (int)$ri['id'] ?>">
|
|
<?= e($ri['nama']) ?> - <?= e($ri['jenis']) ?>, <?= e($ri['kelurahan']) ?> (<?= number_format((int)$ri['jml_warga']) ?> warga)
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<div class="form-hint">Pilihan manual akan mengganti penugasan sebelumnya jika warga sudah pernah ditugaskan.</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-ghost" onclick="App.hideModal('modal-assign')">Batal</button>
|
|
<button type="submit" class="btn btn-primary">Simpan Penugasan</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const recommendationsByWarga = <?= json_encode($rekomendasi, $jsonFlags) ?>;
|
|
const blindSpotAppUrl = <?= json_encode(APP_URL, $jsonFlags) ?>;
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? '').replace(/[&<>"']/g, ch => ({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
}[ch]));
|
|
}
|
|
|
|
function formatJarakClient(meter) {
|
|
const value = Number(meter || 0);
|
|
if (!value) return '-';
|
|
if (value >= 1000) return (value / 1000).toLocaleString('id-ID', { maximumFractionDigits: 1 }) + ' km';
|
|
return value.toLocaleString('id-ID') + ' m';
|
|
}
|
|
|
|
function openAssignModal(warga) {
|
|
const form = document.getElementById('assign-form');
|
|
const select = document.getElementById('assign-ri');
|
|
const list = document.getElementById('assign-recommendations');
|
|
|
|
form.action = `${blindSpotAppUrl}/dinsos/blind-spot/${warga.id}/assign`;
|
|
document.getElementById('assign-warga-name').textContent = warga.nama || '-';
|
|
document.getElementById('assign-warga-location').textContent = warga.lokasi || '-';
|
|
document.getElementById('assign-warga-address').textContent = warga.alamat || '-';
|
|
|
|
const rekomendasi = recommendationsByWarga[warga.id] || [];
|
|
select.value = warga.current_ri_id ? String(warga.current_ri_id) : '';
|
|
if (!select.value && rekomendasi[0]) {
|
|
select.value = String(rekomendasi[0].id);
|
|
}
|
|
|
|
list.innerHTML = '';
|
|
if (!rekomendasi.length) {
|
|
const empty = document.createElement('div');
|
|
empty.className = 'text-sm text-muted';
|
|
empty.textContent = 'Belum ada rekomendasi karena koordinat belum lengkap.';
|
|
list.appendChild(empty);
|
|
} else {
|
|
rekomendasi.forEach((ri, index) => {
|
|
const item = document.createElement('button');
|
|
item.type = 'button';
|
|
item.className = 'btn btn-ghost btn-sm';
|
|
item.style.justifyContent = 'space-between';
|
|
item.style.width = '100%';
|
|
item.innerHTML = `<span>${index + 1}. ${escapeHtml(ri.nama)}</span><span>${formatJarakClient(ri.jarak_meter)}</span>`;
|
|
item.addEventListener('click', () => {
|
|
select.value = String(ri.id);
|
|
});
|
|
list.appendChild(item);
|
|
});
|
|
}
|
|
|
|
App.showModal('modal-assign');
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
App.initTableSearch('search-blind', 'table-blind');
|
|
|
|
const map = GISMap.init('blind-map', <?= MAP_DEFAULT_LAT ?>, <?= MAP_DEFAULT_LNG ?>, 12);
|
|
const riData = await App.fetchJSON('<?= APP_URL ?>/api/rumah-ibadah');
|
|
if (riData) GISMap.addRILayer(riData);
|
|
|
|
const blindData = <?= json_encode(array_map(function ($w) use ($rekomendasi) {
|
|
$top = $rekomendasi[(int)$w['id']][0] ?? null;
|
|
return [
|
|
'id' => (int)$w['id'],
|
|
'lat' => (float)$w['lat'],
|
|
'lng' => (float)$w['lng'],
|
|
'nama' => $w['nama'],
|
|
'status_kemiskinan' => $w['status_kemiskinan'],
|
|
'level_prioritas' => $w['level_prioritas'],
|
|
'assigned' => $w['assigned_rumah_ibadah'] ?? '',
|
|
'rekomendasi' => $top ? [
|
|
'nama' => $top['nama'],
|
|
'lat' => (float)$top['lat'],
|
|
'lng' => (float)$top['lng'],
|
|
'jarak_meter' => (int)$top['jarak_meter']
|
|
] : null
|
|
];
|
|
}, array_filter($data, fn($w) => !empty($w['lat']) && !empty($w['lng']))), $jsonFlags) ?>;
|
|
|
|
blindData.forEach(w => {
|
|
const marker = L.circleMarker([w.lat, w.lng], {
|
|
radius: 10,
|
|
fillColor: '#7c3aed',
|
|
color: '#fff',
|
|
weight: 2,
|
|
fillOpacity: 0.9
|
|
});
|
|
|
|
const popup = `
|
|
<div style="min-width:210px;font-family:Inter,system-ui,sans-serif">
|
|
<strong>Blind Spot</strong><br>
|
|
${escapeHtml(w.nama)}<br>
|
|
<span style="font-size:11px;color:#6b7280">${escapeHtml(w.status_kemiskinan)} - Prioritas ${escapeHtml(w.level_prioritas)}</span>
|
|
${w.assigned ? `<br><span style="font-size:11px;color:#15803d;font-weight:700">Ditugaskan: ${escapeHtml(w.assigned)}</span>` : '<br><span style="font-size:11px;color:#dc2626;font-weight:700">Belum ditugaskan</span>'}
|
|
${w.rekomendasi ? `<br><span style="font-size:11px;color:#0284c7">Rekomendasi: ${escapeHtml(w.rekomendasi.nama)} (${formatJarakClient(w.rekomendasi.jarak_meter)})</span>` : ''}
|
|
</div>`;
|
|
|
|
marker.bindPopup(popup).addTo(map);
|
|
GISMap._allMarkerBounds.push([w.lat, w.lng]);
|
|
|
|
if (w.rekomendasi && w.rekomendasi.lat && w.rekomendasi.lng) {
|
|
L.polyline([[w.lat, w.lng], [w.rekomendasi.lat, w.rekomendasi.lng]], {
|
|
color: '#0284c7',
|
|
weight: 2,
|
|
opacity: 0.5,
|
|
dashArray: '5,7'
|
|
}).addTo(map);
|
|
}
|
|
});
|
|
|
|
GISMap.autoFitBounds();
|
|
});
|
|
</script>
|