Initial commit
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
Options -Indexes
|
||||
RewriteEngine On
|
||||
RewriteBase /sinergi/
|
||||
|
||||
# Allow direct access to public assets
|
||||
RewriteCond %{REQUEST_URI} ^/sinergi/public/ [NC]
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# Route everything else to index.php
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.*)$ index.php [QSA,L]
|
||||
@@ -0,0 +1,418 @@
|
||||
<?php
|
||||
// app/controllers/AdminController.php
|
||||
|
||||
class AdminController extends Controller {
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
Auth::require('superadmin');
|
||||
}
|
||||
|
||||
public function dashboard(): void {
|
||||
$stats = $this->db->fetchOne("
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE status='aktif') as total_warga,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE status='aktif' AND status_kemiskinan='Miskin Ekstrem') as warga_ekstrem,
|
||||
(SELECT COUNT(*) FROM rumah_ibadah WHERE is_active=1) as total_ri,
|
||||
(SELECT COUNT(*) FROM laporan_masyarakat) as total_laporan,
|
||||
(SELECT COUNT(*) FROM laporan_masyarakat WHERE status='menunggu') as laporan_pending,
|
||||
(SELECT COUNT(*) FROM histori_bantuan WHERE status='disalurkan') as total_bantuan,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE is_blind_spot=1 AND status='aktif') as blind_spot,
|
||||
(SELECT COUNT(*) FROM users WHERE is_active=1) as total_user
|
||||
");
|
||||
|
||||
// Data per kecamatan
|
||||
$perKecamatan = $this->db->fetchAll("
|
||||
SELECT kec.nama,
|
||||
COUNT(w.id) as total,
|
||||
SUM(w.status_kemiskinan='Miskin Ekstrem') as ekstrem,
|
||||
SUM(w.status_kemiskinan='Miskin') as miskin,
|
||||
SUM(w.status_kemiskinan='Rentan Miskin') as rentan,
|
||||
SUM(w.is_blind_spot=1) as blind_spot
|
||||
FROM kecamatan kec
|
||||
LEFT JOIN kelurahan k ON k.id_kecamatan = kec.id
|
||||
LEFT JOIN warga_miskin w ON w.id_kelurahan = k.id AND w.status='aktif'
|
||||
GROUP BY kec.id, kec.nama
|
||||
ORDER BY total DESC
|
||||
");
|
||||
|
||||
// Tren bantuan 6 bulan terakhir
|
||||
$trenBantuan = $this->db->fetchAll("
|
||||
SELECT DATE_FORMAT(tanggal_penyaluran,'%Y-%m') as bulan, COUNT(*) as jumlah
|
||||
FROM histori_bantuan WHERE status='disalurkan'
|
||||
AND tanggal_penyaluran >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH)
|
||||
GROUP BY bulan ORDER BY bulan
|
||||
");
|
||||
|
||||
$aktivitas = $this->db->fetchAll("SELECT * FROM aktivitas_sistem ORDER BY created_at DESC LIMIT 15");
|
||||
|
||||
$this->view('admin.dashboard', [
|
||||
'title' => 'Dashboard Super Admin',
|
||||
'stats' => $stats,
|
||||
'perKecamatan'=> $perKecamatan,
|
||||
'trenBantuan' => $trenBantuan,
|
||||
'aktivitas' => $aktivitas,
|
||||
'user' => Auth::user()
|
||||
], 'admin');
|
||||
}
|
||||
|
||||
public function users(): void {
|
||||
$users = $this->db->fetchAll("
|
||||
SELECT u.*, r.nama as role_nama, r.slug as role_slug,
|
||||
k.nama as kelurahan_nama, ri.nama as rumah_ibadah_nama
|
||||
FROM users u
|
||||
JOIN roles r ON u.id_role = r.id
|
||||
LEFT JOIN kelurahan k ON u.id_kelurahan = k.id
|
||||
LEFT JOIN rumah_ibadah ri ON ri.id_user = u.id
|
||||
ORDER BY u.id_role, u.nama
|
||||
");
|
||||
$roles = $this->db->fetchAll("SELECT * FROM roles");
|
||||
$kelurahan = $this->db->fetchAll("SELECT k.id, CONCAT(kec.nama,' - ',k.nama) as nama FROM kelurahan k JOIN kecamatan kec ON k.id_kecamatan=kec.id ORDER BY kec.nama, k.nama");
|
||||
|
||||
$this->view('admin.users', [
|
||||
'title' => 'Manajemen User',
|
||||
'users' => $users,
|
||||
'roles' => $roles,
|
||||
'kelurahan' => $kelurahan,
|
||||
'user' => Auth::user()
|
||||
], 'admin');
|
||||
}
|
||||
|
||||
public function storeUser(): void {
|
||||
if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/admin/users');
|
||||
|
||||
$data = [
|
||||
'id_role' => (int)$_POST['id_role'],
|
||||
'id_kelurahan' => !empty($_POST['id_kelurahan']) ? (int)$_POST['id_kelurahan'] : null,
|
||||
'nama' => trim($_POST['nama']),
|
||||
'username' => trim($_POST['username']),
|
||||
'email' => trim($_POST['email']),
|
||||
'password' => password_hash($_POST['password'], PASSWORD_BCRYPT),
|
||||
'no_hp' => trim($_POST['no_hp'] ?? ''),
|
||||
'is_active' => 1
|
||||
];
|
||||
|
||||
try {
|
||||
$this->db->insert('users', $data);
|
||||
Auth::logActivity(Auth::id(), Auth::user()['nama'], 'superadmin', 'Tambah User', 'users', 'Menambah user: ' . $data['username']);
|
||||
$this->flash('success', 'User berhasil ditambahkan.');
|
||||
} catch (Exception $e) {
|
||||
$this->flash('error', 'Gagal menambah user: username/email sudah digunakan.');
|
||||
}
|
||||
$this->redirect(APP_URL . '/admin/users');
|
||||
}
|
||||
|
||||
public function updateUser(int $id): void {
|
||||
if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/admin/users');
|
||||
|
||||
$data = [
|
||||
'id_role' => (int)$_POST['id_role'],
|
||||
'id_kelurahan' => !empty($_POST['id_kelurahan']) ? (int)$_POST['id_kelurahan'] : null,
|
||||
'nama' => trim($_POST['nama']),
|
||||
'username' => trim($_POST['username']),
|
||||
'email' => trim($_POST['email']),
|
||||
'no_hp' => trim($_POST['no_hp'] ?? ''),
|
||||
'is_active' => (int)$_POST['is_active']
|
||||
];
|
||||
if (!empty($_POST['password'])) {
|
||||
$data['password'] = password_hash($_POST['password'], PASSWORD_BCRYPT);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->db->update('users', $data, 'id = ?', [$id]);
|
||||
Auth::logActivity(Auth::id(), Auth::user()['nama'], 'superadmin', 'Update User', 'users', "Update user ID $id");
|
||||
$this->flash('success', 'User berhasil diperbarui.');
|
||||
} catch (Exception $e) {
|
||||
$this->flash('error', 'Gagal memperbarui user: username atau email sudah digunakan.');
|
||||
}
|
||||
$this->redirect(APP_URL . '/admin/users');
|
||||
}
|
||||
|
||||
public function resetUserPassword(int $id): void {
|
||||
if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/admin/users');
|
||||
|
||||
if ($id === Auth::id()) {
|
||||
$this->flash('error', 'Tidak bisa reset password akun sendiri dari halaman ini.');
|
||||
$this->redirect(APP_URL . '/admin/users');
|
||||
}
|
||||
|
||||
$target = $this->db->fetchOne("
|
||||
SELECT u.id, u.nama, u.username, u.email
|
||||
FROM users u
|
||||
WHERE u.id = ?
|
||||
", [$id]);
|
||||
|
||||
if (!$target) {
|
||||
$this->flash('error', 'User tidak ditemukan.');
|
||||
$this->redirect(APP_URL . '/admin/users');
|
||||
}
|
||||
|
||||
$password = generatePasswordAkun();
|
||||
$this->db->update('users', [
|
||||
'password' => password_hash($password, PASSWORD_BCRYPT)
|
||||
], 'id = ?', [$id]);
|
||||
|
||||
$emailSent = kirimEmailAkun($target['email'], $target['nama'], $target['username'], $password, 'direset passwordnya');
|
||||
Auth::logActivity(Auth::id(), Auth::user()['nama'], 'superadmin', 'Reset Password User', 'users', "Reset password user ID $id");
|
||||
|
||||
if ($emailSent) {
|
||||
$this->flash('success', 'Password berhasil direset dan dikirim ke email pengguna.');
|
||||
} else {
|
||||
$this->flash('warning', 'Password berhasil direset, tetapi email belum terkirim. Password baru: <strong>' . $password . '</strong>');
|
||||
}
|
||||
|
||||
$this->redirect(APP_URL . '/admin/users');
|
||||
}
|
||||
|
||||
public function deleteUser(int $id): void {
|
||||
if ($id === Auth::id()) {
|
||||
$this->flash('error', 'Tidak bisa menghapus akun sendiri.');
|
||||
$this->redirect(APP_URL . '/admin/users');
|
||||
}
|
||||
$this->db->update('users', ['is_active' => 0], 'id = ?', [$id]);
|
||||
$this->db->update('rumah_ibadah', ['id_user' => null], 'id_user = ?', [$id]);
|
||||
Auth::logActivity(Auth::id(), Auth::user()['nama'], 'superadmin', 'Nonaktifkan User', 'users', "Nonaktifkan user ID $id");
|
||||
$this->flash('success', 'User berhasil dinonaktifkan.');
|
||||
$this->redirect(APP_URL . '/admin/users');
|
||||
}
|
||||
|
||||
public function rumahIbadah(): void {
|
||||
$data = $this->db->fetchAll("
|
||||
SELECT ri.*, ji.nama as jenis, k.nama as kelurahan, kec.nama as kecamatan,
|
||||
u.nama as pengurus_nama,
|
||||
(SELECT COUNT(*) FROM warga_miskin w WHERE w.id_rumah_ibadah=ri.id AND w.status='aktif') as jml_warga
|
||||
FROM rumah_ibadah ri
|
||||
JOIN jenis_rumah_ibadah ji ON ri.id_jenis = ji.id
|
||||
JOIN kelurahan k ON ri.id_kelurahan = k.id
|
||||
JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
LEFT JOIN users u ON ri.id_user = u.id
|
||||
ORDER BY kec.nama, k.nama, ri.nama
|
||||
");
|
||||
$jenis = $this->db->fetchAll("SELECT * FROM jenis_rumah_ibadah");
|
||||
$kelurahan = $this->db->fetchAll("SELECT k.id, CONCAT(kec.nama,' - ',k.nama) as nama FROM kelurahan k JOIN kecamatan kec ON k.id_kecamatan=kec.id ORDER BY kec.nama,k.nama");
|
||||
$pengurusAvail = $this->db->fetchAll("SELECT u.id, u.nama FROM users u LEFT JOIN rumah_ibadah ri ON ri.id_user=u.id WHERE u.id_role=3 AND ri.id IS NULL");
|
||||
|
||||
$this->view('admin.rumah_ibadah', [
|
||||
'title' => 'Manajemen Rumah Ibadah',
|
||||
'data' => $data,
|
||||
'jenis' => $jenis,
|
||||
'kelurahan' => $kelurahan,
|
||||
'pengurusAvail' => $pengurusAvail,
|
||||
'user' => Auth::user()
|
||||
], 'admin');
|
||||
}
|
||||
|
||||
public function storeRumahIbadah(): void {
|
||||
if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/admin/rumah-ibadah');
|
||||
|
||||
$foto = null;
|
||||
if (!empty($_FILES['foto']['name'])) {
|
||||
$foto = $this->uploadFile($_FILES['foto'], 'rumah_ibadah');
|
||||
}
|
||||
|
||||
$data = [
|
||||
'id_user' => !empty($_POST['id_user']) ? (int)$_POST['id_user'] : null,
|
||||
'id_kelurahan' => (int)$_POST['id_kelurahan'],
|
||||
'id_jenis' => (int)$_POST['id_jenis'],
|
||||
'nama' => trim($_POST['nama']),
|
||||
'alamat' => trim($_POST['alamat']),
|
||||
'lat' => (float)$_POST['lat'],
|
||||
'lng' => (float)$_POST['lng'],
|
||||
'radius_meter' => (int)($_POST['radius_meter'] ?? 500),
|
||||
'nama_ketua' => trim($_POST['nama_ketua'] ?? ''),
|
||||
'no_hp_pengurus'=> trim($_POST['no_hp_pengurus'] ?? ''),
|
||||
'foto' => $foto,
|
||||
'is_active' => 1
|
||||
];
|
||||
|
||||
$this->db->insert('rumah_ibadah', $data);
|
||||
Auth::logActivity(Auth::id(), Auth::user()['nama'], 'superadmin', 'Tambah Rumah Ibadah', 'rumah_ibadah', 'Menambah: ' . $data['nama']);
|
||||
$this->flash('success', 'Rumah ibadah berhasil ditambahkan.');
|
||||
$this->redirect(APP_URL . '/admin/rumah-ibadah');
|
||||
}
|
||||
|
||||
public function updateRumahIbadah(int $id): void {
|
||||
if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/admin/rumah-ibadah');
|
||||
|
||||
$data = [
|
||||
'id_user' => !empty($_POST['id_user']) ? (int)$_POST['id_user'] : null,
|
||||
'id_kelurahan' => (int)$_POST['id_kelurahan'],
|
||||
'id_jenis' => (int)$_POST['id_jenis'],
|
||||
'nama' => trim($_POST['nama']),
|
||||
'alamat' => trim($_POST['alamat']),
|
||||
'lat' => (float)$_POST['lat'],
|
||||
'lng' => (float)$_POST['lng'],
|
||||
'radius_meter' => (int)($_POST['radius_meter'] ?? 500),
|
||||
'nama_ketua' => trim($_POST['nama_ketua'] ?? ''),
|
||||
'no_hp_pengurus'=> trim($_POST['no_hp_pengurus'] ?? ''),
|
||||
'is_active' => (int)$_POST['is_active']
|
||||
];
|
||||
if (!empty($_FILES['foto']['name'])) {
|
||||
$data['foto'] = $this->uploadFile($_FILES['foto'], 'rumah_ibadah');
|
||||
}
|
||||
|
||||
$this->db->update('rumah_ibadah', $data, 'id = ?', [$id]);
|
||||
$this->flash('success', 'Rumah ibadah berhasil diperbarui.');
|
||||
$this->redirect(APP_URL . '/admin/rumah-ibadah');
|
||||
}
|
||||
|
||||
public function deleteRumahIbadah(int $id): void {
|
||||
$this->db->update('rumah_ibadah', ['is_active' => 0], 'id = ?', [$id]);
|
||||
$this->flash('success', 'Rumah ibadah berhasil dinonaktifkan.');
|
||||
$this->redirect(APP_URL . '/admin/rumah-ibadah');
|
||||
}
|
||||
|
||||
public function wilayah(): void {
|
||||
$kecamatan = $this->db->fetchAll("SELECT k.*, COUNT(kel.id) as jml_kel FROM kecamatan k LEFT JOIN kelurahan kel ON kel.id_kecamatan=k.id GROUP BY k.id ORDER BY k.nama");
|
||||
$kelurahan = $this->db->fetchAll("SELECT kel.*, kec.nama as kecamatan FROM kelurahan kel JOIN kecamatan kec ON kel.id_kecamatan=kec.id ORDER BY kec.nama, kel.nama");
|
||||
$this->view('admin.wilayah', [
|
||||
'title' => 'Manajemen Wilayah',
|
||||
'kecamatan' => $kecamatan,
|
||||
'kelurahan' => $kelurahan,
|
||||
'user' => Auth::user()
|
||||
], 'admin');
|
||||
}
|
||||
|
||||
public function storeKecamatan(): void {
|
||||
if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/admin/wilayah');
|
||||
$this->db->insert('kecamatan', [
|
||||
'nama' => trim($_POST['nama']),
|
||||
'kode' => trim($_POST['kode'] ?? ''),
|
||||
'lat' => (float)($_POST['lat'] ?? 0),
|
||||
'lng' => (float)($_POST['lng'] ?? 0)
|
||||
]);
|
||||
$this->flash('success', 'Kecamatan berhasil ditambahkan.');
|
||||
$this->redirect(APP_URL . '/admin/wilayah');
|
||||
}
|
||||
|
||||
public function storeKelurahan(): void {
|
||||
if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/admin/wilayah');
|
||||
$this->db->insert('kelurahan', [
|
||||
'id_kecamatan' => (int)$_POST['id_kecamatan'],
|
||||
'nama' => trim($_POST['nama']),
|
||||
'kode' => trim($_POST['kode'] ?? ''),
|
||||
'lat' => (float)($_POST['lat'] ?? 0),
|
||||
'lng' => (float)($_POST['lng'] ?? 0)
|
||||
]);
|
||||
$this->flash('success', 'Kelurahan berhasil ditambahkan.');
|
||||
$this->redirect(APP_URL . '/admin/wilayah');
|
||||
}
|
||||
|
||||
public function kategoriBantuan(): void {
|
||||
$data = $this->db->fetchAll("SELECT * FROM kategori_bantuan ORDER BY nama");
|
||||
$this->view('admin.kategori_bantuan', [
|
||||
'title' => 'Manajemen Kategori Bantuan',
|
||||
'data' => $data,
|
||||
'user' => Auth::user()
|
||||
], 'admin');
|
||||
}
|
||||
|
||||
public function storeKategoriBantuan(): void {
|
||||
if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/admin/kategori-bantuan');
|
||||
$this->db->insert('kategori_bantuan', [
|
||||
'nama' => trim($_POST['nama']),
|
||||
'deskripsi' => trim($_POST['deskripsi'] ?? ''),
|
||||
'satuan' => trim($_POST['satuan'] ?? ''),
|
||||
'is_active' => 1
|
||||
]);
|
||||
$this->flash('success', 'Kategori bantuan berhasil ditambahkan.');
|
||||
$this->redirect(APP_URL . '/admin/kategori-bantuan');
|
||||
}
|
||||
|
||||
public function updateKategoriBantuan(int $id): void {
|
||||
if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/admin/kategori-bantuan');
|
||||
$this->db->update('kategori_bantuan', [
|
||||
'nama' => trim($_POST['nama']),
|
||||
'deskripsi' => trim($_POST['deskripsi'] ?? ''),
|
||||
'satuan' => trim($_POST['satuan'] ?? ''),
|
||||
'is_active' => (int)$_POST['is_active']
|
||||
], 'id = ?', [$id]);
|
||||
$this->flash('success', 'Kategori bantuan berhasil diperbarui.');
|
||||
$this->redirect(APP_URL . '/admin/kategori-bantuan');
|
||||
}
|
||||
|
||||
public function monitoring(): void {
|
||||
$warga = $this->db->fetchAll("
|
||||
SELECT w.*, k.nama as kelurahan, kec.nama as kecamatan,
|
||||
ri.nama as rumah_ibadah_nama
|
||||
FROM warga_miskin w
|
||||
JOIN kelurahan k ON w.id_kelurahan = k.id
|
||||
JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
LEFT JOIN rumah_ibadah ri ON w.id_rumah_ibadah = ri.id
|
||||
WHERE w.status = 'aktif'
|
||||
ORDER BY w.level_prioritas DESC, w.skor_prioritas DESC
|
||||
");
|
||||
$this->view('admin.monitoring', [
|
||||
'title' => 'Monitoring Data Kemiskinan',
|
||||
'warga' => $warga,
|
||||
'user' => Auth::user()
|
||||
], 'admin');
|
||||
}
|
||||
|
||||
public function blindSpot(): void {
|
||||
$data = $this->db->fetchAll("
|
||||
SELECT w.*, k.nama as kelurahan, kec.nama as kecamatan
|
||||
FROM warga_miskin w
|
||||
JOIN kelurahan k ON w.id_kelurahan = k.id
|
||||
JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
WHERE w.is_blind_spot = 1 AND w.status = 'aktif'
|
||||
ORDER BY w.level_prioritas DESC
|
||||
");
|
||||
$this->view('admin.blind_spot', [
|
||||
'title' => 'Monitoring Blind Spot',
|
||||
'data' => $data,
|
||||
'user' => Auth::user()
|
||||
], 'admin');
|
||||
}
|
||||
|
||||
public function aktivitas(): void {
|
||||
$data = $this->db->fetchAll("
|
||||
SELECT * FROM aktivitas_sistem ORDER BY created_at DESC LIMIT 200
|
||||
");
|
||||
$this->view('admin.aktivitas', [
|
||||
'title' => 'Aktivitas Sistem',
|
||||
'data' => $data,
|
||||
'user' => Auth::user()
|
||||
], 'admin');
|
||||
}
|
||||
|
||||
// API: Stats untuk dashboard
|
||||
public function apiStats(): void {
|
||||
$data = $this->db->fetchOne("
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE status='aktif') as total_warga,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE status='aktif' AND level_prioritas='Tinggi') as prioritas_tinggi,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE is_blind_spot=1 AND status='aktif') as blind_spot,
|
||||
(SELECT COUNT(*) FROM rumah_ibadah WHERE is_active=1) as total_ri,
|
||||
(SELECT COUNT(*) FROM histori_bantuan WHERE status='disalurkan' AND MONTH(tanggal_penyaluran)=MONTH(CURDATE())) as bantuan_bulan_ini,
|
||||
(SELECT COUNT(*) FROM laporan_masyarakat WHERE status='menunggu') as laporan_pending
|
||||
");
|
||||
$this->json(['success' => true, 'data' => $data]);
|
||||
}
|
||||
|
||||
// API: Data peta
|
||||
public function apiPeta(): void {
|
||||
$warga = $this->db->fetchAll("
|
||||
SELECT w.id, w.nama, w.lat, w.lng, w.status_kemiskinan, w.level_prioritas,
|
||||
w.is_blind_spot, k.nama as kelurahan, ri.nama as rumah_ibadah
|
||||
FROM warga_miskin w
|
||||
JOIN kelurahan k ON w.id_kelurahan = k.id
|
||||
LEFT JOIN rumah_ibadah ri ON w.id_rumah_ibadah = ri.id
|
||||
WHERE w.status = 'aktif'
|
||||
");
|
||||
$ri = $this->db->fetchAll("
|
||||
SELECT ri.id, ri.nama, ri.lat, ri.lng, ri.radius_meter, ri.alamat,
|
||||
ji.nama as jenis, k.nama as kelurahan
|
||||
FROM rumah_ibadah ri
|
||||
JOIN jenis_rumah_ibadah ji ON ri.id_jenis = ji.id
|
||||
JOIN kelurahan k ON ri.id_kelurahan = k.id
|
||||
WHERE ri.is_active = 1
|
||||
");
|
||||
$laporan = $this->db->fetchAll("
|
||||
SELECT id, kode_laporan, nama_warga, lat, lng, status, created_at
|
||||
FROM laporan_masyarakat WHERE lat != 0 AND lng != 0
|
||||
AND status IN ('menunggu','diproses')
|
||||
");
|
||||
$this->json(['warga' => $warga, 'rumah_ibadah' => $ri, 'laporan' => $laporan]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
// app/controllers/AuthController.php
|
||||
|
||||
class AuthController extends Controller {
|
||||
|
||||
public function showLogin(): void {
|
||||
Auth::redirectIfLoggedIn();
|
||||
$this->view('auth.login', ['title' => 'Login - SINERGI'], null);
|
||||
}
|
||||
|
||||
public function doLogin(): void {
|
||||
if (!$this->verifyCsrf()) {
|
||||
$this->flash('error', 'Token tidak valid.');
|
||||
$this->redirect(APP_URL . '/login');
|
||||
}
|
||||
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
$this->flash('error', 'Username dan password wajib diisi.');
|
||||
$this->redirect(APP_URL . '/login');
|
||||
}
|
||||
|
||||
if (Auth::attempt($username, $password)) {
|
||||
$this->redirect(Auth::getDashboardUrl());
|
||||
} else {
|
||||
$this->flash('error', 'Username atau password salah.');
|
||||
$this->redirect(APP_URL . '/login');
|
||||
}
|
||||
}
|
||||
|
||||
public function logout(): void {
|
||||
Auth::logout();
|
||||
$this->redirect(APP_URL . '/login');
|
||||
}
|
||||
|
||||
public function forbidden(): void {
|
||||
http_response_code(403);
|
||||
$this->view('public.403', ['title' => 'Akses Ditolak'], null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,617 @@
|
||||
<?php
|
||||
// app/controllers/DinsosController.php
|
||||
|
||||
class DinsosController extends Controller {
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
Auth::require('dinsos');
|
||||
}
|
||||
|
||||
public function dashboard(): void {
|
||||
$stats = $this->db->fetchOne("
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE status='aktif') as total_warga,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE status='aktif' AND status_kemiskinan='Miskin Ekstrem') as ekstrem,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE status='aktif' AND status_kemiskinan='Miskin') as miskin,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE status='aktif' AND status_kemiskinan='Rentan Miskin') as rentan,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE is_blind_spot=1 AND status='aktif') as blind_spot,
|
||||
(SELECT COUNT(*) FROM rumah_ibadah WHERE is_active=1) as total_ri,
|
||||
(SELECT COUNT(*) FROM histori_bantuan WHERE status='disalurkan') as total_bantuan,
|
||||
(SELECT COUNT(*) FROM histori_bantuan WHERE status='disalurkan' AND MONTH(tanggal_penyaluran)=MONTH(CURDATE())) as bantuan_bulan_ini,
|
||||
(SELECT COUNT(*) FROM laporan_masyarakat WHERE status='menunggu') as laporan_pending
|
||||
");
|
||||
|
||||
$perKecamatan = $this->db->fetchAll("
|
||||
SELECT kec.id, kec.nama,
|
||||
COUNT(w.id) as total,
|
||||
SUM(w.status_kemiskinan='Miskin Ekstrem') as ekstrem,
|
||||
SUM(w.status_kemiskinan='Miskin') as miskin,
|
||||
SUM(w.status_kemiskinan='Rentan Miskin') as rentan,
|
||||
SUM(w.is_blind_spot) as blind_spot
|
||||
FROM kecamatan kec
|
||||
LEFT JOIN kelurahan k ON k.id_kecamatan = kec.id
|
||||
LEFT JOIN warga_miskin w ON w.id_kelurahan = k.id AND w.status='aktif'
|
||||
GROUP BY kec.id, kec.nama
|
||||
ORDER BY total DESC
|
||||
");
|
||||
|
||||
$trenBantuan = $this->db->fetchAll("
|
||||
SELECT DATE_FORMAT(tanggal_penyaluran,'%Y-%m') as bulan,
|
||||
COUNT(*) as jumlah,
|
||||
COUNT(DISTINCT id_warga) as warga_terbantu
|
||||
FROM histori_bantuan WHERE status='disalurkan'
|
||||
AND tanggal_penyaluran >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH)
|
||||
GROUP BY bulan ORDER BY bulan
|
||||
");
|
||||
|
||||
$topRi = $this->db->fetchAll("
|
||||
SELECT ri.nama, ji.nama as jenis, COUNT(hb.id) as jml_bantuan,
|
||||
COUNT(DISTINCT hb.id_warga) as jml_warga
|
||||
FROM rumah_ibadah ri
|
||||
JOIN jenis_rumah_ibadah ji ON ri.id_jenis = ji.id
|
||||
LEFT JOIN histori_bantuan hb ON hb.id_rumah_ibadah = ri.id AND hb.status='disalurkan'
|
||||
WHERE ri.is_active = 1
|
||||
GROUP BY ri.id, ri.nama, ji.nama
|
||||
ORDER BY jml_bantuan DESC LIMIT 5
|
||||
");
|
||||
|
||||
$this->view('dinsos.dashboard', [
|
||||
'title' => 'Dashboard Dinas Sosial',
|
||||
'stats' => $stats,
|
||||
'perKecamatan' => $perKecamatan,
|
||||
'trenBantuan' => $trenBantuan,
|
||||
'topRi' => $topRi,
|
||||
'user' => Auth::user()
|
||||
], 'dinsos');
|
||||
}
|
||||
|
||||
public function kemiskinan(): void {
|
||||
$filter_kec = $_GET['kecamatan'] ?? '';
|
||||
$filter_kel = $_GET['kelurahan'] ?? '';
|
||||
$filter_status = $_GET['status'] ?? '';
|
||||
$filter_prioritas = $_GET['prioritas'] ?? '';
|
||||
|
||||
$sql = "SELECT w.*, k.nama as kelurahan, kec.nama as kecamatan, ri.nama as rumah_ibadah
|
||||
FROM warga_miskin w
|
||||
JOIN kelurahan k ON w.id_kelurahan = k.id
|
||||
JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
LEFT JOIN rumah_ibadah ri ON w.id_rumah_ibadah = ri.id
|
||||
WHERE w.status = 'aktif'";
|
||||
$params = [];
|
||||
|
||||
if ($filter_kec) { $sql .= " AND kec.id = ?"; $params[] = $filter_kec; }
|
||||
if ($filter_kel) { $sql .= " AND k.id = ?"; $params[] = $filter_kel; }
|
||||
if ($filter_status) { $sql .= " AND w.status_kemiskinan = ?"; $params[] = $filter_status; }
|
||||
if ($filter_prioritas) { $sql .= " AND w.level_prioritas = ?"; $params[] = $filter_prioritas; }
|
||||
$sql .= " ORDER BY w.skor_prioritas DESC";
|
||||
|
||||
$data = $this->db->fetchAll($sql, $params);
|
||||
$kecamatan = $this->db->fetchAll("SELECT * FROM kecamatan ORDER BY nama");
|
||||
$kelurahan = !empty($filter_kec)
|
||||
? $this->db->fetchAll("SELECT * FROM kelurahan WHERE id_kecamatan = ? ORDER BY nama", [$filter_kec])
|
||||
: [];
|
||||
|
||||
$this->view('dinsos.kemiskinan', [
|
||||
'title' => 'Monitoring Kemiskinan',
|
||||
'data' => $data,
|
||||
'kecamatan' => $kecamatan,
|
||||
'kelurahan' => $kelurahan,
|
||||
'filter_kec' => $filter_kec,
|
||||
'filter_kel' => $filter_kel,
|
||||
'filter_status' => $filter_status,
|
||||
'filter_prioritas'=> $filter_prioritas,
|
||||
'user' => Auth::user()
|
||||
], 'dinsos');
|
||||
}
|
||||
|
||||
public function bantuan(): void {
|
||||
$bulan = $_GET['bulan'] ?? date('Y-m');
|
||||
[$tahun, $bln] = explode('-', $bulan);
|
||||
|
||||
$data = $this->db->fetchAll("
|
||||
SELECT hb.*, kb.nama as kategori, wm.nama as warga,
|
||||
ri.nama as rumah_ibadah, k.nama as kelurahan, kec.nama as kecamatan
|
||||
FROM histori_bantuan hb
|
||||
JOIN kategori_bantuan kb ON hb.id_kategori_bantuan = kb.id
|
||||
JOIN warga_miskin wm ON hb.id_warga = wm.id
|
||||
JOIN rumah_ibadah ri ON hb.id_rumah_ibadah = ri.id
|
||||
JOIN kelurahan k ON wm.id_kelurahan = k.id
|
||||
JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
WHERE hb.status = 'disalurkan'
|
||||
AND YEAR(hb.tanggal_penyaluran) = ? AND MONTH(hb.tanggal_penyaluran) = ?
|
||||
ORDER BY hb.tanggal_penyaluran DESC
|
||||
", [$tahun, $bln]);
|
||||
|
||||
$perKategori = $this->db->fetchAll("
|
||||
SELECT kb.nama, COUNT(hb.id) as jumlah
|
||||
FROM histori_bantuan hb
|
||||
JOIN kategori_bantuan kb ON hb.id_kategori_bantuan = kb.id
|
||||
WHERE hb.status = 'disalurkan'
|
||||
AND YEAR(hb.tanggal_penyaluran) = ? AND MONTH(hb.tanggal_penyaluran) = ?
|
||||
GROUP BY kb.id, kb.nama ORDER BY jumlah DESC
|
||||
", [$tahun, $bln]);
|
||||
|
||||
$this->view('dinsos.bantuan', [
|
||||
'title' => 'Monitoring Bantuan',
|
||||
'data' => $data,
|
||||
'perKategori' => $perKategori,
|
||||
'bulan' => $bulan,
|
||||
'user' => Auth::user()
|
||||
], 'dinsos');
|
||||
}
|
||||
|
||||
public function akun(): void {
|
||||
$users = $this->db->fetchAll("
|
||||
SELECT u.*, r.nama as role_nama, r.slug as role_slug,
|
||||
CONCAT(kec.nama, ' - ', k.nama) as kelurahan_nama,
|
||||
ri.id as rumah_ibadah_id, ri.nama as rumah_ibadah_nama
|
||||
FROM users u
|
||||
JOIN roles r ON u.id_role = r.id
|
||||
LEFT JOIN kelurahan k ON u.id_kelurahan = k.id
|
||||
LEFT JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
LEFT JOIN rumah_ibadah ri ON ri.id_user = u.id
|
||||
WHERE r.slug IN ('verifikator', 'rumah_ibadah')
|
||||
ORDER BY r.id, u.nama
|
||||
");
|
||||
|
||||
$roles = $this->db->fetchAll("
|
||||
SELECT id, nama, slug
|
||||
FROM roles
|
||||
WHERE slug IN ('verifikator', 'rumah_ibadah')
|
||||
ORDER BY id
|
||||
");
|
||||
|
||||
$kelurahan = $this->db->fetchAll("
|
||||
SELECT k.id, CONCAT(kec.nama, ' - ', k.nama) as nama
|
||||
FROM kelurahan k
|
||||
JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
ORDER BY kec.nama, k.nama
|
||||
");
|
||||
|
||||
$rumahIbadah = $this->db->fetchAll("
|
||||
SELECT ri.id, ri.nama, ri.id_user, CONCAT(kec.nama, ' - ', k.nama) as wilayah,
|
||||
ji.nama as jenis, u.nama as pengurus_nama
|
||||
FROM rumah_ibadah ri
|
||||
JOIN jenis_rumah_ibadah ji ON ri.id_jenis = ji.id
|
||||
JOIN kelurahan k ON ri.id_kelurahan = k.id
|
||||
JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
LEFT JOIN users u ON ri.id_user = u.id
|
||||
WHERE ri.is_active = 1
|
||||
ORDER BY kec.nama, k.nama, ri.nama
|
||||
");
|
||||
|
||||
$this->view('dinsos.akun', [
|
||||
'title' => 'Manajemen Akun Pengguna',
|
||||
'users' => $users,
|
||||
'roles' => $roles,
|
||||
'kelurahan' => $kelurahan,
|
||||
'rumahIbadah' => $rumahIbadah,
|
||||
'user' => Auth::user()
|
||||
], 'dinsos');
|
||||
}
|
||||
|
||||
public function storeAkun(): void {
|
||||
if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/dinsos/akun');
|
||||
|
||||
$roleSlug = $_POST['role_slug'] ?? '';
|
||||
if (!in_array($roleSlug, ['verifikator', 'rumah_ibadah'], true)) {
|
||||
$this->flash('error', 'Peran pengguna tidak valid.');
|
||||
$this->redirect(APP_URL . '/dinsos/akun');
|
||||
}
|
||||
|
||||
$role = $this->db->fetchOne("SELECT id, nama FROM roles WHERE slug = ?", [$roleSlug]);
|
||||
if (!$role) {
|
||||
$this->flash('error', 'Data peran tidak ditemukan.');
|
||||
$this->redirect(APP_URL . '/dinsos/akun');
|
||||
}
|
||||
|
||||
$idKelurahan = null;
|
||||
$idRumahIbadah = null;
|
||||
if ($roleSlug === 'verifikator') {
|
||||
$idKelurahan = (int)($_POST['id_kelurahan'] ?? 0);
|
||||
if ($idKelurahan <= 0) {
|
||||
$this->flash('error', 'Kelurahan tanggung jawab verifikator wajib dipilih.');
|
||||
$this->redirect(APP_URL . '/dinsos/akun');
|
||||
}
|
||||
} else {
|
||||
$idRumahIbadah = (int)($_POST['id_rumah_ibadah'] ?? 0);
|
||||
$ri = $this->db->fetchOne("SELECT id, id_kelurahan, id_user, nama FROM rumah_ibadah WHERE id = ? AND is_active = 1", [$idRumahIbadah]);
|
||||
if (!$ri) {
|
||||
$this->flash('error', 'Rumah ibadah tidak ditemukan atau tidak aktif.');
|
||||
$this->redirect(APP_URL . '/dinsos/akun');
|
||||
}
|
||||
if (!empty($ri['id_user'])) {
|
||||
$this->flash('error', 'Rumah ibadah tersebut sudah memiliki pengurus.');
|
||||
$this->redirect(APP_URL . '/dinsos/akun');
|
||||
}
|
||||
$idKelurahan = (int)$ri['id_kelurahan'];
|
||||
}
|
||||
|
||||
$password = generatePasswordAkun();
|
||||
$data = [
|
||||
'id_role' => (int)$role['id'],
|
||||
'id_kelurahan' => $idKelurahan,
|
||||
'nama' => trim($_POST['nama'] ?? ''),
|
||||
'username' => trim($_POST['username'] ?? ''),
|
||||
'email' => trim($_POST['email'] ?? ''),
|
||||
'password' => password_hash($password, PASSWORD_BCRYPT),
|
||||
'no_hp' => trim($_POST['no_hp'] ?? ''),
|
||||
'is_active' => 1
|
||||
];
|
||||
|
||||
if (!$data['nama'] || !$data['username'] || !$data['email']) {
|
||||
$this->flash('error', 'Nama lengkap, username, dan email wajib diisi.');
|
||||
$this->redirect(APP_URL . '/dinsos/akun');
|
||||
}
|
||||
|
||||
try {
|
||||
$idUser = $this->db->insert('users', $data);
|
||||
if ($roleSlug === 'rumah_ibadah') {
|
||||
$this->db->update('rumah_ibadah', ['id_user' => $idUser], 'id = ?', [$idRumahIbadah]);
|
||||
}
|
||||
|
||||
$emailSent = kirimEmailAkun($data['email'], $data['nama'], $data['username'], $password, 'dibuat');
|
||||
Auth::logActivity(Auth::id(), Auth::user()['nama'], 'dinsos', 'Tambah Akun Pengguna', 'users', 'Menambah user: ' . $data['username']);
|
||||
|
||||
if ($emailSent) {
|
||||
$this->flash('success', 'Akun berhasil disimpan dan kredensial dikirim ke email pengguna.');
|
||||
} else {
|
||||
$this->flash('warning', 'Akun berhasil disimpan, tetapi email belum terkirim. Password awal: <strong>' . $password . '</strong>');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->flash('error', 'Gagal menyimpan akun. Username atau email mungkin sudah digunakan.');
|
||||
}
|
||||
|
||||
$this->redirect(APP_URL . '/dinsos/akun');
|
||||
}
|
||||
|
||||
public function updateAkun(string $id): void {
|
||||
$id = (int)$id;
|
||||
if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/dinsos/akun');
|
||||
|
||||
$target = $this->managedUser($id);
|
||||
if (!$target) {
|
||||
$this->flash('error', 'Akun tidak ditemukan atau tidak dapat dikelola Dinsos.');
|
||||
$this->redirect(APP_URL . '/dinsos/akun');
|
||||
}
|
||||
|
||||
$roleSlug = $_POST['role_slug'] ?? '';
|
||||
if (!in_array($roleSlug, ['verifikator', 'rumah_ibadah'], true)) {
|
||||
$this->flash('error', 'Peran pengguna tidak valid.');
|
||||
$this->redirect(APP_URL . '/dinsos/akun');
|
||||
}
|
||||
|
||||
$role = $this->db->fetchOne("SELECT id FROM roles WHERE slug = ?", [$roleSlug]);
|
||||
$idKelurahan = null;
|
||||
$idRumahIbadah = null;
|
||||
|
||||
if ($roleSlug === 'verifikator') {
|
||||
$idKelurahan = (int)($_POST['id_kelurahan'] ?? 0);
|
||||
if ($idKelurahan <= 0) {
|
||||
$this->flash('error', 'Kelurahan tanggung jawab verifikator wajib dipilih.');
|
||||
$this->redirect(APP_URL . '/dinsos/akun');
|
||||
}
|
||||
} else {
|
||||
$idRumahIbadah = (int)($_POST['id_rumah_ibadah'] ?? 0);
|
||||
$ri = $this->db->fetchOne("SELECT id, id_kelurahan, id_user FROM rumah_ibadah WHERE id = ? AND is_active = 1", [$idRumahIbadah]);
|
||||
if (!$ri) {
|
||||
$this->flash('error', 'Rumah ibadah tidak ditemukan atau tidak aktif.');
|
||||
$this->redirect(APP_URL . '/dinsos/akun');
|
||||
}
|
||||
if (!empty($ri['id_user']) && (int)$ri['id_user'] !== $id) {
|
||||
$this->flash('error', 'Rumah ibadah tersebut sudah memiliki pengurus.');
|
||||
$this->redirect(APP_URL . '/dinsos/akun');
|
||||
}
|
||||
$idKelurahan = (int)$ri['id_kelurahan'];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'id_role' => (int)$role['id'],
|
||||
'id_kelurahan' => $idKelurahan,
|
||||
'nama' => trim($_POST['nama'] ?? ''),
|
||||
'username' => trim($_POST['username'] ?? ''),
|
||||
'email' => trim($_POST['email'] ?? ''),
|
||||
'no_hp' => trim($_POST['no_hp'] ?? ''),
|
||||
'is_active' => (int)($_POST['is_active'] ?? 1)
|
||||
];
|
||||
|
||||
try {
|
||||
$this->db->update('users', $data, 'id = ?', [$id]);
|
||||
$this->db->update('rumah_ibadah', ['id_user' => null], 'id_user = ?', [$id]);
|
||||
if ($roleSlug === 'rumah_ibadah' && $data['is_active']) {
|
||||
$this->db->update('rumah_ibadah', ['id_user' => $id], 'id = ?', [$idRumahIbadah]);
|
||||
}
|
||||
|
||||
Auth::logActivity(Auth::id(), Auth::user()['nama'], 'dinsos', 'Update Akun Pengguna', 'users', "Update user ID $id");
|
||||
$this->flash('success', 'Akun pengguna berhasil diperbarui.');
|
||||
} catch (Exception $e) {
|
||||
$this->flash('error', 'Gagal memperbarui akun. Username atau email mungkin sudah digunakan.');
|
||||
}
|
||||
|
||||
$this->redirect(APP_URL . '/dinsos/akun');
|
||||
}
|
||||
|
||||
public function resetAkunPassword(string $id): void {
|
||||
$id = (int)$id;
|
||||
if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/dinsos/akun');
|
||||
|
||||
$target = $this->managedUser($id);
|
||||
if (!$target) {
|
||||
$this->flash('error', 'Akun tidak ditemukan atau tidak dapat dikelola Dinsos.');
|
||||
$this->redirect(APP_URL . '/dinsos/akun');
|
||||
}
|
||||
|
||||
$password = generatePasswordAkun();
|
||||
$this->db->update('users', [
|
||||
'password' => password_hash($password, PASSWORD_BCRYPT)
|
||||
], 'id = ?', [$id]);
|
||||
|
||||
$emailSent = kirimEmailAkun($target['email'], $target['nama'], $target['username'], $password, 'direset passwordnya');
|
||||
Auth::logActivity(Auth::id(), Auth::user()['nama'], 'dinsos', 'Reset Password Akun', 'users', "Reset password user ID $id");
|
||||
|
||||
if ($emailSent) {
|
||||
$this->flash('success', 'Password berhasil direset dan dikirim ke email pengguna.');
|
||||
} else {
|
||||
$this->flash('warning', 'Password berhasil direset, tetapi email belum terkirim. Password baru: <strong>' . $password . '</strong>');
|
||||
}
|
||||
|
||||
$this->redirect(APP_URL . '/dinsos/akun');
|
||||
}
|
||||
|
||||
public function deactivateAkun(string $id): void {
|
||||
$id = (int)$id;
|
||||
if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/dinsos/akun');
|
||||
|
||||
$target = $this->managedUser($id);
|
||||
if (!$target) {
|
||||
$this->flash('error', 'Akun tidak ditemukan atau tidak dapat dikelola Dinsos.');
|
||||
$this->redirect(APP_URL . '/dinsos/akun');
|
||||
}
|
||||
|
||||
$this->db->update('users', ['is_active' => 0], 'id = ?', [$id]);
|
||||
$this->db->update('rumah_ibadah', ['id_user' => null], 'id_user = ?', [$id]);
|
||||
Auth::logActivity(Auth::id(), Auth::user()['nama'], 'dinsos', 'Nonaktifkan Akun', 'users', "Nonaktifkan user ID $id");
|
||||
$this->flash('success', 'Akun pengguna berhasil dinonaktifkan.');
|
||||
$this->redirect(APP_URL . '/dinsos/akun');
|
||||
}
|
||||
|
||||
private function managedUser(int $id): ?array {
|
||||
return $this->db->fetchOne("
|
||||
SELECT u.*, r.slug as role_slug
|
||||
FROM users u
|
||||
JOIN roles r ON u.id_role = r.id
|
||||
WHERE u.id = ? AND r.slug IN ('verifikator', 'rumah_ibadah')
|
||||
", [$id]);
|
||||
}
|
||||
|
||||
public function blindSpot(): void {
|
||||
$data = $this->db->fetchAll("
|
||||
SELECT w.*, k.nama as kelurahan, kec.nama as kecamatan,
|
||||
ri.id as assigned_ri_id, ri.nama as assigned_rumah_ibadah,
|
||||
assigned_ji.nama as assigned_jenis
|
||||
FROM warga_miskin w
|
||||
JOIN kelurahan k ON w.id_kelurahan = k.id
|
||||
JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
LEFT JOIN rumah_ibadah ri ON w.id_rumah_ibadah = ri.id
|
||||
LEFT JOIN jenis_rumah_ibadah assigned_ji ON ri.id_jenis = assigned_ji.id
|
||||
WHERE w.is_blind_spot = 1 AND w.status = 'aktif'
|
||||
ORDER BY CASE WHEN w.id_rumah_ibadah IS NULL THEN 0 ELSE 1 END,
|
||||
CASE w.level_prioritas WHEN 'Tinggi' THEN 1 WHEN 'Sedang' THEN 2 ELSE 3 END,
|
||||
w.skor_prioritas DESC
|
||||
");
|
||||
|
||||
$rumahIbadah = $this->db->fetchAll("
|
||||
SELECT ri.id, ri.nama, ri.alamat, ri.lat, ri.lng, ri.radius_meter,
|
||||
ji.nama as jenis, k.nama as kelurahan, kec.nama as kecamatan,
|
||||
COUNT(wm.id) as jml_warga
|
||||
FROM rumah_ibadah ri
|
||||
JOIN jenis_rumah_ibadah ji ON ri.id_jenis = ji.id
|
||||
JOIN kelurahan k ON ri.id_kelurahan = k.id
|
||||
JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
LEFT JOIN warga_miskin wm ON wm.id_rumah_ibadah = ri.id AND wm.status = 'aktif'
|
||||
WHERE ri.is_active = 1
|
||||
GROUP BY ri.id, ri.nama, ri.alamat, ri.lat, ri.lng, ri.radius_meter,
|
||||
ji.nama, k.nama, kec.nama
|
||||
ORDER BY kec.nama, k.nama, ri.nama
|
||||
");
|
||||
|
||||
$rekomendasi = $this->buildRekomendasiRumahIbadah($data, $rumahIbadah);
|
||||
$sudahDitugaskan = count(array_filter($data, fn($w) => !empty($w['id_rumah_ibadah'])));
|
||||
|
||||
$perKec = $this->db->fetchAll("
|
||||
SELECT kec.nama, COUNT(w.id) as jumlah
|
||||
FROM kecamatan kec
|
||||
JOIN kelurahan k ON k.id_kecamatan = kec.id
|
||||
JOIN warga_miskin w ON w.id_kelurahan = k.id
|
||||
WHERE w.is_blind_spot = 1 AND w.status = 'aktif'
|
||||
GROUP BY kec.id, kec.nama ORDER BY jumlah DESC
|
||||
");
|
||||
|
||||
$this->view('dinsos.blind_spot', [
|
||||
'title' => 'Monitoring Blind Spot',
|
||||
'data' => $data,
|
||||
'perKec' => $perKec,
|
||||
'rumahIbadah' => $rumahIbadah,
|
||||
'rekomendasi' => $rekomendasi,
|
||||
'ringkasanPenugasan' => [
|
||||
'sudah' => $sudahDitugaskan,
|
||||
'belum' => count($data) - $sudahDitugaskan,
|
||||
'total_ri' => count($rumahIbadah)
|
||||
],
|
||||
'user' => Auth::user()
|
||||
], 'dinsos');
|
||||
}
|
||||
|
||||
public function assignBlindSpot(string $id): void {
|
||||
$id = (int) $id;
|
||||
|
||||
if (!$this->verifyCsrf()) {
|
||||
$this->flash('error', 'Token tidak valid. Silakan coba lagi.');
|
||||
$this->redirect(APP_URL . '/dinsos/blind-spot');
|
||||
}
|
||||
|
||||
$idRumahIbadah = (int)($_POST['id_rumah_ibadah'] ?? 0);
|
||||
if ($id <= 0 || $idRumahIbadah <= 0) {
|
||||
$this->flash('error', 'Pilih rumah ibadah penanggung jawab terlebih dahulu.');
|
||||
$this->redirect(APP_URL . '/dinsos/blind-spot');
|
||||
}
|
||||
|
||||
$warga = $this->db->fetchOne("
|
||||
SELECT id, nama
|
||||
FROM warga_miskin
|
||||
WHERE id = ? AND is_blind_spot = 1 AND status = 'aktif'
|
||||
", [$id]);
|
||||
|
||||
if (!$warga) {
|
||||
$this->flash('error', 'Data warga blind spot tidak ditemukan.');
|
||||
$this->redirect(APP_URL . '/dinsos/blind-spot');
|
||||
}
|
||||
|
||||
$rumahIbadah = $this->db->fetchOne("
|
||||
SELECT id, nama
|
||||
FROM rumah_ibadah
|
||||
WHERE id = ? AND is_active = 1
|
||||
", [$idRumahIbadah]);
|
||||
|
||||
if (!$rumahIbadah) {
|
||||
$this->flash('error', 'Rumah ibadah tidak aktif atau tidak ditemukan.');
|
||||
$this->redirect(APP_URL . '/dinsos/blind-spot');
|
||||
}
|
||||
|
||||
$this->db->update('warga_miskin', [
|
||||
'id_rumah_ibadah' => $idRumahIbadah
|
||||
], 'id = ? AND is_blind_spot = 1 AND status = ?', [$id, 'aktif']);
|
||||
|
||||
Auth::logActivity(
|
||||
Auth::id(),
|
||||
Auth::user()['nama'],
|
||||
'dinsos',
|
||||
'Atur Penugasan Blind Spot',
|
||||
'blind_spot',
|
||||
'Menugaskan ' . $warga['nama'] . ' ke ' . $rumahIbadah['nama']
|
||||
);
|
||||
|
||||
$this->flash('success', 'Penugasan warga blind spot berhasil disimpan.');
|
||||
$this->redirect(APP_URL . '/dinsos/blind-spot');
|
||||
}
|
||||
|
||||
private function buildRekomendasiRumahIbadah(array $wargaList, array $rumahIbadahList): array {
|
||||
$hasil = [];
|
||||
|
||||
foreach ($wargaList as $warga) {
|
||||
$lat = (float)($warga['lat'] ?? 0);
|
||||
$lng = (float)($warga['lng'] ?? 0);
|
||||
$idWarga = (int)($warga['id'] ?? 0);
|
||||
$hasil[$idWarga] = [];
|
||||
|
||||
if (!$lat || !$lng) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$kandidat = [];
|
||||
foreach ($rumahIbadahList as $ri) {
|
||||
if (empty($ri['lat']) || empty($ri['lng'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$jarak = haversine($lat, $lng, (float)$ri['lat'], (float)$ri['lng']);
|
||||
$item = $ri;
|
||||
$item['jarak_meter'] = (int)round($jarak);
|
||||
$item['di_dalam_radius'] = $jarak <= (float)$ri['radius_meter'];
|
||||
$kandidat[] = $item;
|
||||
}
|
||||
|
||||
usort($kandidat, fn($a, $b) => $a['jarak_meter'] <=> $b['jarak_meter']);
|
||||
$hasil[$idWarga] = array_slice($kandidat, 0, 3);
|
||||
}
|
||||
|
||||
return $hasil;
|
||||
}
|
||||
|
||||
public function kinerja(): void {
|
||||
$data = $this->db->fetchAll("
|
||||
SELECT ri.id, ri.nama, ji.nama as jenis, k.nama as kelurahan, kec.nama as kecamatan,
|
||||
ri.radius_meter,
|
||||
COUNT(DISTINCT wm.id) as jml_warga,
|
||||
COUNT(DISTINCT hb.id) as jml_bantuan,
|
||||
COUNT(DISTINCT CASE WHEN wm.status_kemiskinan='Miskin Ekstrem' THEN wm.id END) as jml_ekstrem,
|
||||
MAX(hb.tanggal_penyaluran) as bantuan_terakhir
|
||||
FROM rumah_ibadah ri
|
||||
JOIN jenis_rumah_ibadah ji ON ri.id_jenis = ji.id
|
||||
JOIN kelurahan k ON ri.id_kelurahan = k.id
|
||||
JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
LEFT JOIN warga_miskin wm ON wm.id_rumah_ibadah = ri.id AND wm.status='aktif'
|
||||
LEFT JOIN histori_bantuan hb ON hb.id_rumah_ibadah = ri.id AND hb.status='disalurkan'
|
||||
WHERE ri.is_active = 1
|
||||
GROUP BY ri.id, ri.nama, ji.nama, k.nama, kec.nama, ri.radius_meter
|
||||
ORDER BY jml_bantuan DESC
|
||||
");
|
||||
|
||||
$this->view('dinsos.kinerja', [
|
||||
'title' => 'Kinerja Rumah Ibadah',
|
||||
'data' => $data,
|
||||
'user' => Auth::user()
|
||||
], 'dinsos');
|
||||
}
|
||||
|
||||
public function laporan(): void {
|
||||
$tahun = (int)($_GET['tahun'] ?? date('Y'));
|
||||
|
||||
$perBulan = $this->db->fetchAll("
|
||||
SELECT MONTH(tanggal_penyaluran) as bulan, COUNT(*) as bantuan,
|
||||
COUNT(DISTINCT id_warga) as warga_terbantu
|
||||
FROM histori_bantuan WHERE status='disalurkan' AND YEAR(tanggal_penyaluran) = ?
|
||||
GROUP BY MONTH(tanggal_penyaluran) ORDER BY bulan
|
||||
", [$tahun]);
|
||||
|
||||
$ringkasan = $this->db->fetchOne("
|
||||
SELECT COUNT(*) as total_bantuan,
|
||||
COUNT(DISTINCT id_warga) as total_warga_terbantu,
|
||||
COUNT(DISTINCT id_rumah_ibadah) as total_ri_aktif
|
||||
FROM histori_bantuan WHERE status='disalurkan' AND YEAR(tanggal_penyaluran) = ?
|
||||
", [$tahun]);
|
||||
|
||||
$this->view('dinsos.laporan', [
|
||||
'title' => 'Laporan Tahunan',
|
||||
'perBulan' => $perBulan,
|
||||
'ringkasan' => $ringkasan,
|
||||
'tahun' => $tahun,
|
||||
'user' => Auth::user()
|
||||
], 'dinsos');
|
||||
}
|
||||
|
||||
public function apiHeatmap(): void {
|
||||
$data = $this->db->fetchAll("
|
||||
SELECT lat, lng, status_kemiskinan,
|
||||
CASE status_kemiskinan
|
||||
WHEN 'Miskin Ekstrem' THEN 1.0
|
||||
WHEN 'Miskin' THEN 0.7
|
||||
WHEN 'Rentan Miskin' THEN 0.4
|
||||
ELSE 0.3
|
||||
END as intensity
|
||||
FROM warga_miskin WHERE status = 'aktif' AND lat != 0
|
||||
");
|
||||
$this->json($data);
|
||||
}
|
||||
|
||||
public function apiStats(): void {
|
||||
$perKec = $this->db->fetchAll("
|
||||
SELECT kec.nama,
|
||||
COUNT(w.id) as total,
|
||||
SUM(w.status_kemiskinan='Miskin Ekstrem') as ekstrem,
|
||||
SUM(w.is_blind_spot) as blind_spot
|
||||
FROM kecamatan kec
|
||||
LEFT JOIN kelurahan k ON k.id_kecamatan = kec.id
|
||||
LEFT JOIN warga_miskin w ON w.id_kelurahan = k.id AND w.status='aktif'
|
||||
GROUP BY kec.id, kec.nama
|
||||
");
|
||||
$trenBantuan = $this->db->fetchAll("
|
||||
SELECT DATE_FORMAT(tanggal_penyaluran,'%Y-%m') as bulan, COUNT(*) as jumlah
|
||||
FROM histori_bantuan WHERE status='disalurkan'
|
||||
AND tanggal_penyaluran >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
|
||||
GROUP BY bulan ORDER BY bulan
|
||||
");
|
||||
$this->json(['perKecamatan' => $perKec, 'trenBantuan' => $trenBantuan]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
// app/controllers/PublicController.php
|
||||
|
||||
class PublicController extends Controller {
|
||||
|
||||
public function beranda(): void {
|
||||
$stats = $this->db->fetchOne("
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE status = 'aktif') as total_warga,
|
||||
(SELECT COUNT(*) FROM rumah_ibadah WHERE is_active = 1) as total_ri,
|
||||
(SELECT COUNT(*) FROM laporan_masyarakat) as total_laporan,
|
||||
(SELECT COUNT(*) FROM histori_bantuan WHERE status = 'disalurkan') as total_bantuan,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE is_blind_spot = 1) as total_blind_spot
|
||||
");
|
||||
$this->view('public.beranda', ['title' => 'SINERGI - Sistem GIS Penanganan Kemiskinan', 'stats' => $stats], 'public');
|
||||
}
|
||||
|
||||
public function tentang(): void {
|
||||
$this->view('public.tentang', ['title' => 'Tentang Sistem - SINERGI'], 'public');
|
||||
}
|
||||
|
||||
public function kontak(): void {
|
||||
$this->view('public.kontak', ['title' => 'Kontak - SINERGI'], 'public');
|
||||
}
|
||||
|
||||
public function lapor(): void {
|
||||
$kecamatan = $this->db->fetchAll("SELECT * FROM kecamatan ORDER BY nama");
|
||||
$this->view('public.lapor', [
|
||||
'title' => 'Lapor Cepat - SINERGI',
|
||||
'kecamatan' => $kecamatan
|
||||
], 'public');
|
||||
}
|
||||
|
||||
public function kirimLaporan(): void {
|
||||
if (!$this->verifyCsrf()) {
|
||||
$this->flash('error', 'Token tidak valid.');
|
||||
$this->redirect(APP_URL . '/lapor');
|
||||
}
|
||||
|
||||
$namaWarga = trim($_POST['nama_warga'] ?? '');
|
||||
$namaPelapor = trim($_POST['nama_pelapor'] ?? '');
|
||||
$noHp = trim($_POST['no_hp_pelapor'] ?? '');
|
||||
$alamat = trim($_POST['alamat_warga'] ?? '');
|
||||
$idKelurahan = (int)($_POST['id_kelurahan'] ?? 0);
|
||||
$lat = (float)($_POST['lat'] ?? 0);
|
||||
$lng = (float)($_POST['lng'] ?? 0);
|
||||
$deskripsi = trim($_POST['deskripsi'] ?? '');
|
||||
|
||||
if (!$namaWarga || !$namaPelapor || !$alamat || !$idKelurahan || !$lat || !$lng) {
|
||||
$this->flash('error', 'Mohon lengkapi semua data yang wajib diisi.');
|
||||
$this->redirect(APP_URL . '/lapor');
|
||||
}
|
||||
|
||||
$foto = null;
|
||||
if (!empty($_FILES['foto']['name'])) {
|
||||
$foto = $this->uploadFile($_FILES['foto'], 'laporan');
|
||||
}
|
||||
|
||||
$kode = generateKodeLaporan();
|
||||
|
||||
$this->db->insert('laporan_masyarakat', [
|
||||
'kode_laporan' => $kode,
|
||||
'nama_pelapor' => $namaPelapor,
|
||||
'no_hp_pelapor' => $noHp,
|
||||
'nama_warga' => $namaWarga,
|
||||
'alamat_warga' => $alamat,
|
||||
'id_kelurahan' => $idKelurahan,
|
||||
'lat' => $lat,
|
||||
'lng' => $lng,
|
||||
'deskripsi' => $deskripsi,
|
||||
'foto' => $foto,
|
||||
'status' => 'menunggu'
|
||||
]);
|
||||
|
||||
$this->flash('success', 'Laporan berhasil dikirim! Simpan kode laporan Anda.', ['kode_laporan' => $kode]);
|
||||
$this->redirect(APP_URL . '/lapor');
|
||||
}
|
||||
|
||||
public function cekLaporan(): void {
|
||||
$this->view('public.cek_laporan', ['title' => 'Cek Status Laporan - SINERGI'], 'public');
|
||||
}
|
||||
|
||||
public function proseCekLaporan(): void {
|
||||
$kode = strtoupper(trim($_POST['kode_laporan'] ?? ''));
|
||||
$laporan = null;
|
||||
if ($kode) {
|
||||
$laporan = $this->db->fetchOne(
|
||||
"SELECT l.*, k.nama as kelurahan, kec.nama as kecamatan
|
||||
FROM laporan_masyarakat l
|
||||
JOIN kelurahan k ON l.id_kelurahan = k.id
|
||||
JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
WHERE l.kode_laporan = ?",
|
||||
[$kode]
|
||||
);
|
||||
}
|
||||
$this->view('public.cek_laporan', [
|
||||
'title' => 'Cek Status Laporan - SINERGI',
|
||||
'laporan' => $laporan,
|
||||
'kode' => $kode
|
||||
], 'public');
|
||||
}
|
||||
|
||||
// API: Ambil kelurahan berdasarkan kecamatan
|
||||
public function apiKelurahan(int $id): void {
|
||||
$data = $this->db->fetchAll("SELECT id, nama FROM kelurahan WHERE id_kecamatan = ? ORDER BY nama", [$id]);
|
||||
$this->json($data);
|
||||
}
|
||||
|
||||
// API: Data rumah ibadah untuk peta publik
|
||||
public function apiRumahIbadah(): void {
|
||||
$data = $this->db->fetchAll(
|
||||
"SELECT ri.id, ri.nama, ri.lat, ri.lng, ri.radius_meter, ri.alamat,
|
||||
ji.nama as jenis, k.nama as kelurahan
|
||||
FROM rumah_ibadah ri
|
||||
JOIN jenis_rumah_ibadah ji ON ri.id_jenis = ji.id
|
||||
JOIN kelurahan k ON ri.id_kelurahan = k.id
|
||||
WHERE ri.is_active = 1"
|
||||
);
|
||||
$this->json($data);
|
||||
}
|
||||
|
||||
// API: Data warga untuk peta publik (tanpa data sensitif)
|
||||
public function apiWargaPeta(): void {
|
||||
$data = $this->db->fetchAll(
|
||||
"SELECT w.lat, w.lng, w.status_kemiskinan, w.level_prioritas, w.is_blind_spot,
|
||||
k.nama as kelurahan, kec.nama as kecamatan
|
||||
FROM warga_miskin w
|
||||
JOIN kelurahan k ON w.id_kelurahan = k.id
|
||||
JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
WHERE w.status = 'aktif'"
|
||||
);
|
||||
$this->json($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
// app/controllers/RumahIbadahController.php
|
||||
|
||||
class RumahIbadahController extends Controller {
|
||||
|
||||
private array $ri;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
Auth::require('rumah_ibadah');
|
||||
$user = Auth::user();
|
||||
if (!$user['rumah_ibadah_id']) {
|
||||
// Belum terhubung ke rumah ibadah
|
||||
die('<div style="padding:40px;font-family:sans-serif;"><h2>Akun belum terhubung ke Rumah Ibadah</h2><p>Hubungi Super Admin untuk menautkan akun Anda.</p></div>');
|
||||
}
|
||||
$this->ri = $this->db->fetchOne(
|
||||
"SELECT ri.*, ji.nama as jenis, k.nama as kelurahan, kec.nama as kecamatan
|
||||
FROM rumah_ibadah ri
|
||||
JOIN jenis_rumah_ibadah ji ON ri.id_jenis = ji.id
|
||||
JOIN kelurahan k ON ri.id_kelurahan = k.id
|
||||
JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
WHERE ri.id = ?",
|
||||
[$user['rumah_ibadah_id']]
|
||||
);
|
||||
}
|
||||
|
||||
public function dashboard(): void {
|
||||
$riId = $this->ri['id'];
|
||||
|
||||
$stats = $this->db->fetchOne("
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE id_rumah_ibadah=? AND status='aktif') as total_warga,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE id_rumah_ibadah=? AND level_prioritas='Tinggi' AND status='aktif') as prioritas_tinggi,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE id_rumah_ibadah=? AND level_prioritas='Sedang' AND status='aktif') as prioritas_sedang,
|
||||
(SELECT COUNT(*) FROM histori_bantuan WHERE id_rumah_ibadah=? AND status='disalurkan') as total_bantuan,
|
||||
(SELECT COUNT(*) FROM histori_bantuan WHERE id_rumah_ibadah=? AND status='disalurkan' AND MONTH(tanggal_penyaluran)=MONTH(CURDATE())) as bantuan_bulan_ini,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE id_rumah_ibadah=? AND status_kemiskinan='Miskin Ekstrem' AND status='aktif') as ekstrem
|
||||
", array_fill(0, 6, $riId));
|
||||
|
||||
$wargaList = $this->db->fetchAll("
|
||||
SELECT w.id, w.nama, w.status_kemiskinan, w.level_prioritas, w.skor_prioritas,
|
||||
w.penghasilan_bulanan, w.jumlah_tanggungan, w.lat, w.lng,
|
||||
w.is_lansia, w.is_disabilitas, k.nama as kelurahan
|
||||
FROM warga_miskin w
|
||||
JOIN kelurahan k ON w.id_kelurahan = k.id
|
||||
WHERE w.id_rumah_ibadah = ? AND w.status = 'aktif'
|
||||
ORDER BY w.skor_prioritas DESC
|
||||
LIMIT 20
|
||||
", [$riId]);
|
||||
|
||||
$bantuanTerakhir = $this->db->fetchAll("
|
||||
SELECT hb.*, kb.nama as kategori, wm.nama as warga
|
||||
FROM histori_bantuan hb
|
||||
JOIN kategori_bantuan kb ON hb.id_kategori_bantuan = kb.id
|
||||
JOIN warga_miskin wm ON hb.id_warga = wm.id
|
||||
WHERE hb.id_rumah_ibadah = ?
|
||||
ORDER BY hb.tanggal_penyaluran DESC LIMIT 10
|
||||
", [$riId]);
|
||||
|
||||
$this->view('rumah_ibadah.dashboard', [
|
||||
'title' => 'Dashboard Rumah Ibadah',
|
||||
'ri' => $this->ri,
|
||||
'stats' => $stats,
|
||||
'wargaList' => $wargaList,
|
||||
'bantuanTerakhir'=> $bantuanTerakhir,
|
||||
'user' => Auth::user()
|
||||
], 'rumah_ibadah');
|
||||
}
|
||||
|
||||
public function warga(): void {
|
||||
$riId = $this->ri['id'];
|
||||
$level = $_GET['level'] ?? 'semua';
|
||||
$sql = "SELECT w.*, k.nama as kelurahan FROM warga_miskin w
|
||||
JOIN kelurahan k ON w.id_kelurahan = k.id
|
||||
WHERE w.id_rumah_ibadah = ? AND w.status = 'aktif'";
|
||||
$params = [$riId];
|
||||
if ($level !== 'semua') {
|
||||
$sql .= " AND w.level_prioritas = ?";
|
||||
$params[] = $level;
|
||||
}
|
||||
$sql .= " ORDER BY w.skor_prioritas DESC";
|
||||
|
||||
$data = $this->db->fetchAll($sql, $params);
|
||||
$this->view('rumah_ibadah.warga', [
|
||||
'title' => 'Daftar Warga Binaan',
|
||||
'data' => $data,
|
||||
'ri' => $this->ri,
|
||||
'level' => $level,
|
||||
'user' => Auth::user()
|
||||
], 'rumah_ibadah');
|
||||
}
|
||||
|
||||
public function detailWarga(int $id): void {
|
||||
$warga = $this->db->fetchOne("
|
||||
SELECT w.*, k.nama as kelurahan, kec.nama as kecamatan
|
||||
FROM warga_miskin w
|
||||
JOIN kelurahan k ON w.id_kelurahan = k.id
|
||||
JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
WHERE w.id = ? AND w.id_rumah_ibadah = ?
|
||||
", [$id, $this->ri['id']]);
|
||||
|
||||
if (!$warga) {
|
||||
$this->flash('error', 'Warga tidak ditemukan dalam binaan Anda.');
|
||||
$this->redirect(APP_URL . '/rumah-ibadah/warga');
|
||||
}
|
||||
|
||||
$bantuan = $this->db->fetchAll("
|
||||
SELECT hb.*, kb.nama as kategori
|
||||
FROM histori_bantuan hb
|
||||
JOIN kategori_bantuan kb ON hb.id_kategori_bantuan = kb.id
|
||||
WHERE hb.id_warga = ?
|
||||
ORDER BY hb.tanggal_penyaluran DESC
|
||||
", [$id]);
|
||||
|
||||
$this->view('rumah_ibadah.detail_warga', [
|
||||
'title' => 'Detail Warga',
|
||||
'warga' => $warga,
|
||||
'ri' => $this->ri,
|
||||
'bantuan' => $bantuan,
|
||||
'user' => Auth::user()
|
||||
], 'rumah_ibadah');
|
||||
}
|
||||
|
||||
public function prioritas(): void {
|
||||
$data = $this->db->fetchAll("
|
||||
SELECT w.*, k.nama as kelurahan,
|
||||
(SELECT COUNT(*) FROM histori_bantuan WHERE id_warga=w.id AND status='disalurkan') as jml_bantuan,
|
||||
(SELECT MAX(tanggal_penyaluran) FROM histori_bantuan WHERE id_warga=w.id AND status='disalurkan') as bantuan_terakhir
|
||||
FROM warga_miskin w
|
||||
JOIN kelurahan k ON w.id_kelurahan = k.id
|
||||
WHERE w.id_rumah_ibadah = ? AND w.status = 'aktif'
|
||||
ORDER BY w.skor_prioritas DESC, w.status_kemiskinan
|
||||
", [$this->ri['id']]);
|
||||
|
||||
$this->view('rumah_ibadah.prioritas', [
|
||||
'title' => 'Prioritas Bantuan',
|
||||
'data' => $data,
|
||||
'ri' => $this->ri,
|
||||
'user' => Auth::user()
|
||||
], 'rumah_ibadah');
|
||||
}
|
||||
|
||||
public function bantuan(): void {
|
||||
$data = $this->db->fetchAll("
|
||||
SELECT hb.*, kb.nama as kategori, wm.nama as warga
|
||||
FROM histori_bantuan hb
|
||||
JOIN kategori_bantuan kb ON hb.id_kategori_bantuan = kb.id
|
||||
JOIN warga_miskin wm ON hb.id_warga = wm.id
|
||||
WHERE hb.id_rumah_ibadah = ?
|
||||
ORDER BY hb.tanggal_penyaluran DESC
|
||||
", [$this->ri['id']]);
|
||||
|
||||
$this->view('rumah_ibadah.bantuan', [
|
||||
'title' => 'Histori Bantuan',
|
||||
'data' => $data,
|
||||
'ri' => $this->ri,
|
||||
'user' => Auth::user()
|
||||
], 'rumah_ibadah');
|
||||
}
|
||||
|
||||
public function formBantuan(): void {
|
||||
$wargaList = $this->db->fetchAll("
|
||||
SELECT id, nama, status_kemiskinan, level_prioritas, skor_prioritas
|
||||
FROM warga_miskin WHERE id_rumah_ibadah = ? AND status = 'aktif'
|
||||
ORDER BY skor_prioritas DESC
|
||||
", [$this->ri['id']]);
|
||||
|
||||
$kategori = $this->db->fetchAll("SELECT * FROM kategori_bantuan WHERE is_active = 1 ORDER BY nama");
|
||||
|
||||
$this->view('rumah_ibadah.form_bantuan', [
|
||||
'title' => 'Tambah Bantuan',
|
||||
'wargaList' => $wargaList,
|
||||
'kategori' => $kategori,
|
||||
'ri' => $this->ri,
|
||||
'user' => Auth::user()
|
||||
], 'rumah_ibadah');
|
||||
}
|
||||
|
||||
public function storeBantuan(): void {
|
||||
if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/rumah-ibadah/bantuan');
|
||||
|
||||
// Pastikan warga memang binaan rumah ibadah ini
|
||||
$idWarga = (int)$_POST['id_warga'];
|
||||
$warga = $this->db->fetchOne("SELECT id FROM warga_miskin WHERE id = ? AND id_rumah_ibadah = ?", [$idWarga, $this->ri['id']]);
|
||||
if (!$warga) {
|
||||
$this->flash('error', 'Warga tidak valid.');
|
||||
$this->redirect(APP_URL . '/rumah-ibadah/bantuan/tambah');
|
||||
}
|
||||
|
||||
$foto = null;
|
||||
if (!empty($_FILES['foto_bukti']['name'])) {
|
||||
$foto = $this->uploadFile($_FILES['foto_bukti'], 'bantuan');
|
||||
}
|
||||
|
||||
$this->db->insert('histori_bantuan', [
|
||||
'id_warga' => $idWarga,
|
||||
'id_rumah_ibadah' => $this->ri['id'],
|
||||
'id_kategori_bantuan' => (int)$_POST['id_kategori_bantuan'],
|
||||
'id_user_pengurus' => Auth::id(),
|
||||
'kode_bantuan' => generateKodeBantuan(),
|
||||
'tanggal_penyaluran' => $_POST['tanggal_penyaluran'],
|
||||
'jumlah' => (float)($_POST['jumlah'] ?? 0),
|
||||
'satuan' => trim($_POST['satuan'] ?? ''),
|
||||
'deskripsi' => trim($_POST['deskripsi'] ?? ''),
|
||||
'foto_bukti' => $foto,
|
||||
'status' => 'disalurkan'
|
||||
]);
|
||||
|
||||
Auth::logActivity(Auth::id(), Auth::user()['nama'], 'rumah_ibadah', 'Salurkan Bantuan', 'bantuan', "Bantuan untuk warga ID $idWarga");
|
||||
$this->flash('success', 'Bantuan berhasil dicatat.');
|
||||
$this->redirect(APP_URL . '/rumah-ibadah/bantuan');
|
||||
}
|
||||
|
||||
public function apiPeta(): void {
|
||||
$riId = $this->ri['id'];
|
||||
$warga = $this->db->fetchAll("
|
||||
SELECT w.id, w.nama, w.lat, w.lng, w.status_kemiskinan, w.level_prioritas, w.skor_prioritas
|
||||
FROM warga_miskin w WHERE w.id_rumah_ibadah = ? AND w.status = 'aktif'
|
||||
", [$riId]);
|
||||
$this->json([
|
||||
'rumah_ibadah' => $this->ri,
|
||||
'warga' => $warga
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
<?php
|
||||
// app/controllers/VerifikatorController.php
|
||||
|
||||
class VerifikatorController extends Controller {
|
||||
|
||||
private int $idKelurahan;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
Auth::require('verifikator');
|
||||
$this->idKelurahan = (int)($_SESSION['id_kelurahan'] ?? 0);
|
||||
}
|
||||
|
||||
public function dashboard(): void {
|
||||
$stats = $this->db->fetchOne("
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM laporan_masyarakat WHERE id_kelurahan=?) as total_laporan,
|
||||
(SELECT COUNT(*) FROM laporan_masyarakat WHERE id_kelurahan=? AND status='menunggu') as laporan_menunggu,
|
||||
(SELECT COUNT(*) FROM laporan_masyarakat WHERE id_kelurahan=? AND status='diproses') as laporan_diproses,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE id_kelurahan=? AND status='aktif') as total_warga,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE id_kelurahan=? AND level_prioritas='Tinggi' AND status='aktif') as prioritas_tinggi,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE id_kelurahan=? AND is_blind_spot=1 AND status='aktif') as blind_spot
|
||||
", array_fill(0, 6, $this->idKelurahan));
|
||||
|
||||
$laporanTerbaru = $this->db->fetchAll("
|
||||
SELECT l.*, k.nama as kelurahan FROM laporan_masyarakat l
|
||||
JOIN kelurahan k ON l.id_kelurahan = k.id
|
||||
WHERE l.id_kelurahan = ?
|
||||
ORDER BY l.created_at DESC LIMIT 10
|
||||
", [$this->idKelurahan]);
|
||||
|
||||
$this->view('verifikator.dashboard', [
|
||||
'title' => 'Dashboard Verifikator',
|
||||
'stats' => $stats,
|
||||
'laporanTerbaru'=> $laporanTerbaru,
|
||||
'user' => Auth::user()
|
||||
], 'verifikator');
|
||||
}
|
||||
|
||||
public function laporan(): void {
|
||||
$status = $_GET['status'] ?? 'semua';
|
||||
$sql = "SELECT l.*, k.nama as kelurahan, kec.nama as kecamatan
|
||||
FROM laporan_masyarakat l
|
||||
JOIN kelurahan k ON l.id_kelurahan = k.id
|
||||
JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
WHERE l.id_kelurahan = ?";
|
||||
$params = [$this->idKelurahan];
|
||||
if ($status !== 'semua') {
|
||||
$sql .= " AND l.status = ?";
|
||||
$params[] = $status;
|
||||
}
|
||||
$sql .= " ORDER BY l.created_at DESC";
|
||||
|
||||
$data = $this->db->fetchAll($sql, $params);
|
||||
$this->view('verifikator.laporan', [
|
||||
'title' => 'Daftar Laporan',
|
||||
'data' => $data,
|
||||
'status' => $status,
|
||||
'user' => Auth::user()
|
||||
], 'verifikator');
|
||||
}
|
||||
|
||||
// FIX: type hint string, cast to int internally (router always passes strings)
|
||||
public function detailLaporan(string $id): void {
|
||||
$id = (int) $id;
|
||||
|
||||
$laporan = $this->db->fetchOne("
|
||||
SELECT l.*, k.nama as kelurahan, kec.nama as kecamatan
|
||||
FROM laporan_masyarakat l
|
||||
JOIN kelurahan k ON l.id_kelurahan = k.id
|
||||
JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
WHERE l.id = ? AND l.id_kelurahan = ?
|
||||
", [$id, $this->idKelurahan]);
|
||||
|
||||
if (!$laporan) {
|
||||
$this->flash('error', 'Laporan tidak ditemukan.');
|
||||
$this->redirect(APP_URL . '/verifikator/laporan');
|
||||
}
|
||||
|
||||
$this->view('verifikator.detail_laporan', [
|
||||
'title' => 'Detail Laporan',
|
||||
'laporan' => $laporan,
|
||||
'user' => Auth::user()
|
||||
], 'verifikator');
|
||||
}
|
||||
|
||||
// FIX: type hint string, cast to int internally
|
||||
public function prosesLaporan(string $id): void {
|
||||
$id = (int) $id;
|
||||
|
||||
if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/verifikator/laporan');
|
||||
|
||||
$aksi = $_POST['aksi'] ?? '';
|
||||
$catatan = trim($_POST['catatan'] ?? '');
|
||||
|
||||
if ($aksi === 'proses') {
|
||||
$this->db->update('laporan_masyarakat', [
|
||||
'status' => 'diproses',
|
||||
'id_verifikator' => Auth::id(),
|
||||
'catatan_verifikator' => $catatan
|
||||
], 'id = ? AND id_kelurahan = ?', [$id, $this->idKelurahan]);
|
||||
$this->flash('success', 'Laporan ditandai sebagai diproses.');
|
||||
|
||||
} elseif ($aksi === 'tolak') {
|
||||
$this->db->update('laporan_masyarakat', [
|
||||
'status' => 'ditolak',
|
||||
'id_verifikator' => Auth::id(),
|
||||
'catatan_verifikator'=> $catatan,
|
||||
'tanggal_verifikasi' => date('Y-m-d H:i:s')
|
||||
], 'id = ? AND id_kelurahan = ?', [$id, $this->idKelurahan]);
|
||||
$this->flash('info', 'Laporan ditolak.');
|
||||
|
||||
} elseif ($aksi === 'verifikasi') {
|
||||
// Verifikasi = buat warga miskin baru dari laporan
|
||||
$laporan = $this->db->fetchOne("SELECT * FROM laporan_masyarakat WHERE id=?", [$id]);
|
||||
if ($laporan) {
|
||||
$this->db->update('laporan_masyarakat', [
|
||||
'status' => 'terverifikasi',
|
||||
'id_verifikator' => Auth::id(),
|
||||
'catatan_verifikator'=> $catatan,
|
||||
'tanggal_verifikasi' => date('Y-m-d H:i:s')
|
||||
], 'id = ?', [$id]);
|
||||
|
||||
// Cek apakah warga sudah ada
|
||||
$existing = $this->db->fetchOne("SELECT id FROM warga_miskin WHERE id_laporan = ?", [$id]);
|
||||
if (!$existing) {
|
||||
// Buat entry warga miskin baru
|
||||
$isBlindSpot = isInBlindSpot((float)$laporan['lat'], (float)$laporan['lng']) ? 1 : 0;
|
||||
$ri = findRumahIbadahTerdekat((float)$laporan['lat'], (float)$laporan['lng']);
|
||||
$idRi = ($ri && !$isBlindSpot) ? $ri['id'] : null;
|
||||
|
||||
// Jika dalam radius, gunakan yang terdekat dan dalam radius
|
||||
if ($ri && !$isBlindSpot) {
|
||||
$dist = haversine((float)$laporan['lat'], (float)$laporan['lng'], (float)$ri['lat'], (float)$ri['lng']);
|
||||
if ($dist > $ri['radius_meter']) {
|
||||
$idRi = null;
|
||||
$isBlindSpot = 1;
|
||||
}
|
||||
}
|
||||
|
||||
$wargaData = [
|
||||
'id_laporan' => $id,
|
||||
'id_kelurahan' => $laporan['id_kelurahan'],
|
||||
'id_rumah_ibadah' => $idRi,
|
||||
'id_verifikator' => Auth::id(),
|
||||
'nama' => $laporan['nama_warga'],
|
||||
'alamat' => $laporan['alamat_warga'],
|
||||
'lat' => $laporan['lat'],
|
||||
'lng' => $laporan['lng'],
|
||||
'is_blind_spot' => $isBlindSpot,
|
||||
'status' => 'aktif',
|
||||
'tanggal_verifikasi' => date('Y-m-d')
|
||||
];
|
||||
|
||||
// Hitung skor prioritas awal
|
||||
$skor = hitungSkorPrioritas($wargaData);
|
||||
$wargaData['skor_prioritas'] = $skor['skor'];
|
||||
$wargaData['level_prioritas'] = $skor['level'];
|
||||
|
||||
$this->db->insert('warga_miskin', $wargaData);
|
||||
}
|
||||
}
|
||||
Auth::logActivity(Auth::id(), Auth::user()['nama'], 'verifikator', 'Verifikasi Laporan', 'verifikasi', "Verifikasi laporan ID $id");
|
||||
$this->flash('success', 'Laporan berhasil diverifikasi. Data warga telah ditambahkan.');
|
||||
}
|
||||
|
||||
$this->redirect(APP_URL . '/verifikator/laporan/' . $id);
|
||||
}
|
||||
|
||||
public function warga(): void {
|
||||
$level = $_GET['level'] ?? 'semua';
|
||||
$sql = "SELECT w.*, k.nama as kelurahan, ri.nama as rumah_ibadah
|
||||
FROM warga_miskin w
|
||||
JOIN kelurahan k ON w.id_kelurahan = k.id
|
||||
LEFT JOIN rumah_ibadah ri ON w.id_rumah_ibadah = ri.id
|
||||
WHERE w.id_kelurahan = ? AND w.status = 'aktif'";
|
||||
$params = [$this->idKelurahan];
|
||||
if ($level !== 'semua') {
|
||||
$sql .= " AND w.level_prioritas = ?";
|
||||
$params[] = $level;
|
||||
}
|
||||
$sql .= " ORDER BY w.skor_prioritas DESC, w.nama";
|
||||
|
||||
$data = $this->db->fetchAll($sql, $params);
|
||||
$this->view('verifikator.warga', [
|
||||
'title' => 'Data Warga Miskin',
|
||||
'data' => $data,
|
||||
'level' => $level,
|
||||
'user' => Auth::user()
|
||||
], 'verifikator');
|
||||
}
|
||||
|
||||
// FIX: type hint string, cast to int internally
|
||||
public function detailWarga(string $id): void {
|
||||
$id = (int) $id;
|
||||
|
||||
$warga = $this->db->fetchOne("
|
||||
SELECT w.*, k.nama as kelurahan, kec.nama as kecamatan,
|
||||
ri.nama as rumah_ibadah_nama, ri.lat as ri_lat, ri.lng as ri_lng
|
||||
FROM warga_miskin w
|
||||
JOIN kelurahan k ON w.id_kelurahan = k.id
|
||||
JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
LEFT JOIN rumah_ibadah ri ON w.id_rumah_ibadah = ri.id
|
||||
WHERE w.id = ? AND w.id_kelurahan = ?
|
||||
", [$id, $this->idKelurahan]);
|
||||
|
||||
if (!$warga) {
|
||||
$this->flash('error', 'Data tidak ditemukan.');
|
||||
$this->redirect(APP_URL . '/verifikator/warga');
|
||||
}
|
||||
|
||||
$bantuan = $this->db->fetchAll("
|
||||
SELECT hb.*, kb.nama as kategori, ri.nama as rumah_ibadah
|
||||
FROM histori_bantuan hb
|
||||
JOIN kategori_bantuan kb ON hb.id_kategori_bantuan = kb.id
|
||||
JOIN rumah_ibadah ri ON hb.id_rumah_ibadah = ri.id
|
||||
WHERE hb.id_warga = ?
|
||||
ORDER BY hb.tanggal_penyaluran DESC
|
||||
", [$id]);
|
||||
|
||||
$this->view('verifikator.detail_warga', [
|
||||
'title' => 'Detail Warga',
|
||||
'warga' => $warga,
|
||||
'bantuan' => $bantuan,
|
||||
'user' => Auth::user()
|
||||
], 'verifikator');
|
||||
}
|
||||
|
||||
public function formTambahWarga(): void {
|
||||
$kelurahan = $this->db->fetchOne("SELECT k.id, k.nama, kec.nama as kecamatan FROM kelurahan k JOIN kecamatan kec ON k.id_kecamatan=kec.id WHERE k.id=?", [$this->idKelurahan]);
|
||||
$this->view('verifikator.form_warga', [
|
||||
'title' => 'Tambah Data Warga',
|
||||
'kelurahan' => $kelurahan,
|
||||
'user' => Auth::user()
|
||||
], 'verifikator');
|
||||
}
|
||||
|
||||
public function storeWarga(): void {
|
||||
if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/verifikator/warga');
|
||||
|
||||
$lat = (float)$_POST['lat'];
|
||||
$lng = (float)$_POST['lng'];
|
||||
|
||||
// Blind spot check
|
||||
$isBlindSpot = isInBlindSpot($lat, $lng) ? 1 : 0;
|
||||
|
||||
// Assignment rumah ibadah
|
||||
$ri = findRumahIbadahTerdekat($lat, $lng);
|
||||
$idRi = null;
|
||||
if ($ri && !$isBlindSpot) {
|
||||
$dist = haversine($lat, $lng, (float)$ri['lat'], (float)$ri['lng']);
|
||||
$idRi = ($dist <= $ri['radius_meter']) ? $ri['id'] : null;
|
||||
if (!$idRi) $isBlindSpot = 1;
|
||||
}
|
||||
|
||||
$penghasilan = (int)$_POST['penghasilan_bulanan'];
|
||||
$tanggungan = (int)$_POST['jumlah_tanggungan'];
|
||||
$statusKemiskinan = tentukanStatusKemiskinan($penghasilan, $tanggungan);
|
||||
|
||||
$tanggalLahir = $_POST['tanggal_lahir'] ?? '';
|
||||
$isLansia = 0;
|
||||
if ($tanggalLahir) {
|
||||
$umur = hitungUmur($tanggalLahir);
|
||||
if ($umur >= 60) $isLansia = 1;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'id_kelurahan' => $this->idKelurahan,
|
||||
'id_rumah_ibadah' => $idRi,
|
||||
'id_verifikator' => Auth::id(),
|
||||
'nik' => trim($_POST['nik'] ?? '') ?: null,
|
||||
'nama' => trim($_POST['nama']),
|
||||
'tempat_lahir' => trim($_POST['tempat_lahir'] ?? ''),
|
||||
'tanggal_lahir' => $tanggalLahir ?: null,
|
||||
'jenis_kelamin' => $_POST['jenis_kelamin'] ?? 'L',
|
||||
'agama' => $_POST['agama'] ?? 'Islam',
|
||||
'alamat' => trim($_POST['alamat']),
|
||||
'lat' => $lat,
|
||||
'lng' => $lng,
|
||||
'no_hp' => trim($_POST['no_hp'] ?? ''),
|
||||
'pendidikan_terakhir'=> $_POST['pendidikan_terakhir'] ?? 'SD',
|
||||
'pekerjaan' => trim($_POST['pekerjaan'] ?? ''),
|
||||
'penghasilan_bulanan'=> $penghasilan,
|
||||
'jumlah_tanggungan' => $tanggungan,
|
||||
'kondisi_rumah' => $_POST['kondisi_rumah'] ?? 'Tidak Layak',
|
||||
'riwayat_penyakit' => trim($_POST['riwayat_penyakit'] ?? ''),
|
||||
'is_lansia' => $isLansia,
|
||||
'is_disabilitas' => (int)($_POST['is_disabilitas'] ?? 0),
|
||||
'disabilitas_jenis' => trim($_POST['disabilitas_jenis'] ?? ''),
|
||||
'status_kemiskinan' => $statusKemiskinan,
|
||||
'is_blind_spot' => $isBlindSpot,
|
||||
'status' => 'aktif',
|
||||
'tanggal_verifikasi' => date('Y-m-d')
|
||||
];
|
||||
|
||||
$foto = null;
|
||||
if (!empty($_FILES['foto']['name'])) {
|
||||
$foto = $this->uploadFile($_FILES['foto'], 'warga');
|
||||
}
|
||||
$data['foto'] = $foto;
|
||||
|
||||
$skor = hitungSkorPrioritas($data);
|
||||
$data['skor_prioritas'] = $skor['skor'];
|
||||
$data['level_prioritas'] = $skor['level'];
|
||||
|
||||
try {
|
||||
$this->db->insert('warga_miskin', $data);
|
||||
Auth::logActivity(Auth::id(), Auth::user()['nama'], 'verifikator', 'Tambah Warga', 'warga_miskin', 'Tambah: ' . $data['nama']);
|
||||
$this->flash('success', 'Data warga berhasil ditambahkan.' . ($isBlindSpot ? ' <strong>PERHATIAN: Warga ini terdeteksi sebagai Blind Spot.</strong>' : ''));
|
||||
} catch (Exception $e) {
|
||||
$this->flash('error', 'Gagal menyimpan. NIK mungkin sudah terdaftar.');
|
||||
}
|
||||
|
||||
$this->redirect(APP_URL . '/verifikator/warga');
|
||||
}
|
||||
|
||||
// FIX: type hint string, cast to int internally
|
||||
public function updateWarga(string $id): void {
|
||||
$id = (int) $id;
|
||||
|
||||
if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/verifikator/warga');
|
||||
|
||||
$lat = (float)$_POST['lat'];
|
||||
$lng = (float)$_POST['lng'];
|
||||
$isBlindSpot = isInBlindSpot($lat, $lng) ? 1 : 0;
|
||||
$ri = findRumahIbadahTerdekat($lat, $lng);
|
||||
$idRi = null;
|
||||
if ($ri && !$isBlindSpot) {
|
||||
$dist = haversine($lat, $lng, (float)$ri['lat'], (float)$ri['lng']);
|
||||
$idRi = ($dist <= $ri['radius_meter']) ? $ri['id'] : null;
|
||||
if (!$idRi) $isBlindSpot = 1;
|
||||
}
|
||||
|
||||
$penghasilan = (int)$_POST['penghasilan_bulanan'];
|
||||
$tanggungan = (int)$_POST['jumlah_tanggungan'];
|
||||
$tanggalLahir = $_POST['tanggal_lahir'] ?? '';
|
||||
$isLansia = ($tanggalLahir && hitungUmur($tanggalLahir) >= 60) ? 1 : 0;
|
||||
|
||||
$data = [
|
||||
'id_rumah_ibadah' => $idRi,
|
||||
'nik' => trim($_POST['nik'] ?? '') ?: null,
|
||||
'nama' => trim($_POST['nama']),
|
||||
'tempat_lahir' => trim($_POST['tempat_lahir'] ?? ''),
|
||||
'tanggal_lahir' => $tanggalLahir ?: null,
|
||||
'jenis_kelamin' => $_POST['jenis_kelamin'] ?? 'L',
|
||||
'agama' => $_POST['agama'] ?? 'Islam',
|
||||
'alamat' => trim($_POST['alamat']),
|
||||
'lat' => $lat,
|
||||
'lng' => $lng,
|
||||
'pendidikan_terakhir'=> $_POST['pendidikan_terakhir'] ?? 'SD',
|
||||
'pekerjaan' => trim($_POST['pekerjaan'] ?? ''),
|
||||
'penghasilan_bulanan'=> $penghasilan,
|
||||
'jumlah_tanggungan' => $tanggungan,
|
||||
'kondisi_rumah' => $_POST['kondisi_rumah'] ?? 'Tidak Layak',
|
||||
'riwayat_penyakit' => trim($_POST['riwayat_penyakit'] ?? ''),
|
||||
'is_lansia' => $isLansia,
|
||||
'is_disabilitas' => (int)($_POST['is_disabilitas'] ?? 0),
|
||||
'disabilitas_jenis' => trim($_POST['disabilitas_jenis'] ?? ''),
|
||||
'status_kemiskinan' => tentukanStatusKemiskinan($penghasilan, $tanggungan),
|
||||
'is_blind_spot' => $isBlindSpot
|
||||
];
|
||||
|
||||
$skor = hitungSkorPrioritas($data);
|
||||
$data['skor_prioritas'] = $skor['skor'];
|
||||
$data['level_prioritas'] = $skor['level'];
|
||||
|
||||
if (!empty($_FILES['foto']['name'])) {
|
||||
$data['foto'] = $this->uploadFile($_FILES['foto'], 'warga');
|
||||
}
|
||||
|
||||
$this->db->update('warga_miskin', $data, 'id = ? AND id_kelurahan = ?', [$id, $this->idKelurahan]);
|
||||
Auth::logActivity(Auth::id(), Auth::user()['nama'], 'verifikator', 'Update Warga', 'warga_miskin', "Update warga ID $id");
|
||||
$this->flash('success', 'Data warga berhasil diperbarui.');
|
||||
$this->redirect(APP_URL . '/verifikator/warga/' . $id);
|
||||
}
|
||||
|
||||
public function apiStats(): void {
|
||||
$data = $this->db->fetchOne("
|
||||
SELECT
|
||||
COUNT(CASE WHEN status='menunggu' THEN 1 END) as menunggu,
|
||||
COUNT(CASE WHEN status='diproses' THEN 1 END) as diproses,
|
||||
COUNT(CASE WHEN status='terverifikasi' THEN 1 END) as terverifikasi,
|
||||
COUNT(CASE WHEN status='ditolak' THEN 1 END) as ditolak
|
||||
FROM laporan_masyarakat WHERE id_kelurahan = ?
|
||||
", [$this->idKelurahan]);
|
||||
$this->json(['success' => true, 'data' => $data]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
// app/controllers/WalikotaController.php
|
||||
|
||||
class WalikotaController extends Controller {
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
Auth::require('walikota');
|
||||
}
|
||||
|
||||
public function dashboard(): void {
|
||||
$stats = $this->db->fetchOne("
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE status='aktif') as total_warga,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE status='aktif' AND status_kemiskinan='Miskin Ekstrem') as ekstrem,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE status='aktif' AND status_kemiskinan='Miskin') as miskin,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE status='aktif' AND status_kemiskinan='Rentan Miskin') as rentan,
|
||||
(SELECT COUNT(*) FROM warga_miskin WHERE is_blind_spot=1 AND status='aktif') as blind_spot,
|
||||
(SELECT COUNT(*) FROM rumah_ibadah WHERE is_active=1) as total_ri,
|
||||
(SELECT COUNT(*) FROM histori_bantuan WHERE status='disalurkan') as total_bantuan,
|
||||
(SELECT COUNT(*) FROM histori_bantuan WHERE status='disalurkan' AND YEAR(tanggal_penyaluran)=YEAR(CURDATE())) as bantuan_tahun_ini,
|
||||
(SELECT COUNT(*) FROM laporan_masyarakat) as total_laporan,
|
||||
(SELECT COUNT(*) FROM laporan_masyarakat WHERE status='terverifikasi') as laporan_terverifikasi
|
||||
");
|
||||
|
||||
$perKecamatan = $this->db->fetchAll("
|
||||
SELECT kec.id, kec.nama,
|
||||
COUNT(w.id) as total,
|
||||
SUM(w.status_kemiskinan='Miskin Ekstrem') as ekstrem,
|
||||
SUM(w.status_kemiskinan='Miskin') as miskin,
|
||||
SUM(w.status_kemiskinan='Rentan Miskin') as rentan,
|
||||
SUM(w.is_blind_spot) as blind_spot,
|
||||
COUNT(DISTINCT ri.id) as total_ri
|
||||
FROM kecamatan kec
|
||||
LEFT JOIN kelurahan k ON k.id_kecamatan = kec.id
|
||||
LEFT JOIN warga_miskin w ON w.id_kelurahan = k.id AND w.status='aktif'
|
||||
LEFT JOIN rumah_ibadah ri ON ri.id_kelurahan = k.id AND ri.is_active=1
|
||||
GROUP BY kec.id, kec.nama
|
||||
ORDER BY total DESC
|
||||
");
|
||||
|
||||
$topRi = $this->db->fetchAll("
|
||||
SELECT ri.nama, ji.nama as jenis, kec.nama as kecamatan,
|
||||
COUNT(DISTINCT wm.id) as jml_warga,
|
||||
COUNT(DISTINCT hb.id) as jml_bantuan
|
||||
FROM rumah_ibadah ri
|
||||
JOIN jenis_rumah_ibadah ji ON ri.id_jenis = ji.id
|
||||
JOIN kelurahan k ON ri.id_kelurahan = k.id
|
||||
JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
LEFT JOIN warga_miskin wm ON wm.id_rumah_ibadah = ri.id AND wm.status='aktif'
|
||||
LEFT JOIN histori_bantuan hb ON hb.id_rumah_ibadah = ri.id AND hb.status='disalurkan'
|
||||
WHERE ri.is_active = 1
|
||||
GROUP BY ri.id, ri.nama, ji.nama, kec.nama
|
||||
ORDER BY jml_bantuan DESC LIMIT 5
|
||||
");
|
||||
|
||||
$trenTahunan = $this->db->fetchAll("
|
||||
SELECT YEAR(tanggal_penyaluran) as tahun, COUNT(*) as bantuan,
|
||||
COUNT(DISTINCT id_warga) as warga_terbantu
|
||||
FROM histori_bantuan WHERE status='disalurkan'
|
||||
GROUP BY tahun ORDER BY tahun
|
||||
");
|
||||
|
||||
$trenBulanan = $this->db->fetchAll("
|
||||
SELECT DATE_FORMAT(tanggal_penyaluran,'%Y-%m') as bulan, COUNT(*) as jumlah
|
||||
FROM histori_bantuan WHERE status='disalurkan'
|
||||
AND tanggal_penyaluran >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
|
||||
GROUP BY bulan ORDER BY bulan
|
||||
");
|
||||
|
||||
$this->view('walikota.dashboard', [
|
||||
'title' => 'Dashboard Eksekutif - Wali Kota',
|
||||
'stats' => $stats,
|
||||
'perKecamatan' => $perKecamatan,
|
||||
'topRi' => $topRi,
|
||||
'trenTahunan' => $trenTahunan,
|
||||
'trenBulanan' => $trenBulanan,
|
||||
'user' => Auth::user()
|
||||
], 'walikota');
|
||||
}
|
||||
|
||||
public function apiStats(): void {
|
||||
$perKec = $this->db->fetchAll("
|
||||
SELECT kec.nama,
|
||||
COUNT(w.id) as total,
|
||||
SUM(w.status_kemiskinan='Miskin Ekstrem') as ekstrem,
|
||||
SUM(w.is_blind_spot) as blind_spot
|
||||
FROM kecamatan kec
|
||||
LEFT JOIN kelurahan k ON k.id_kecamatan = kec.id
|
||||
LEFT JOIN warga_miskin w ON w.id_kelurahan = k.id AND w.status='aktif'
|
||||
GROUP BY kec.id, kec.nama
|
||||
");
|
||||
$this->json(['perKecamatan' => $perKec]);
|
||||
}
|
||||
|
||||
public function apiPeta(): void {
|
||||
$warga = $this->db->fetchAll("
|
||||
SELECT lat, lng, status_kemiskinan, is_blind_spot,
|
||||
CASE status_kemiskinan
|
||||
WHEN 'Miskin Ekstrem' THEN 1.0
|
||||
WHEN 'Miskin' THEN 0.7
|
||||
ELSE 0.4
|
||||
END as intensity
|
||||
FROM warga_miskin WHERE status='aktif' AND lat != 0
|
||||
");
|
||||
$ri = $this->db->fetchAll("
|
||||
SELECT ri.nama, ri.lat, ri.lng, ri.radius_meter, ji.nama as jenis
|
||||
FROM rumah_ibadah ri
|
||||
JOIN jenis_rumah_ibadah ji ON ri.id_jenis = ji.id
|
||||
WHERE ri.is_active = 1
|
||||
");
|
||||
$this->json(['warga' => $warga, 'rumah_ibadah' => $ri]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<div style="margin-bottom:20px">
|
||||
<h2 style="font-size:1.2rem;font-weight:700">📋 Log Aktivitas Sistem</h2>
|
||||
<p class="text-muted text-sm">200 aktivitas terbaru tercatat dalam sistem</p>
|
||||
</div>
|
||||
|
||||
<div class="filters-bar">
|
||||
<input type="text" id="search-akt" class="form-control" placeholder="🔍 Cari user, modul, aksi..." style="max-width:300px">
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body no-pad">
|
||||
<div class="table-wrap">
|
||||
<table id="table-akt">
|
||||
<thead>
|
||||
<tr><th>Waktu</th><th>User</th><th>Role</th><th>Aksi</th><th>Modul</th><th>Deskripsi</th><th>IP</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach($data as $a): ?>
|
||||
<tr>
|
||||
<td class="text-xs" style="font-family:monospace;white-space:nowrap"><?= $a['created_at'] ?></td>
|
||||
<td class="text-sm font-bold"><?= e($a['nama_user']??'System') ?></td>
|
||||
<td>
|
||||
<?php $rc=['superadmin'=>'badge-purple','verifikator'=>'badge-blue','rumah_ibadah'=>'badge-green','dinsos'=>'badge-teal','walikota'=>'badge-orange']; ?>
|
||||
<span class="badge <?= $rc[$a['role']??'']??'badge-gray' ?>"><?= e($a['role']??'—') ?></span>
|
||||
</td>
|
||||
<td class="text-sm font-bold"><?= e($a['aksi']) ?></td>
|
||||
<td><span class="badge badge-gray"><?= e($a['modul']??'—') ?></span></td>
|
||||
<td class="text-sm text-muted"><?= e($a['deskripsi']??'') ?></td>
|
||||
<td class="text-xs" style="font-family:monospace"><?= e($a['ip_address']??'') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if(empty($data)): ?>
|
||||
<div class="empty-state"><div class="empty-state-icon">📋</div><h3>Belum ada aktivitas</h3></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>document.addEventListener('DOMContentLoaded',()=>App.initTableSearch('search-akt','table-akt'));</script>
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
// blind_spot.php — Admin view
|
||||
?>
|
||||
<div style="margin-bottom:20px">
|
||||
<h2 style="font-size:1.2rem;font-weight:700">⚠️ Monitoring Blind Spot</h2>
|
||||
<p class="text-muted text-sm"><?= count($data) ?> warga tidak terjangkau radius rumah ibadah manapun</p>
|
||||
</div>
|
||||
|
||||
<?php if(empty($data)): ?>
|
||||
<div class="card"><div class="card-body" style="text-align:center;padding:48px">
|
||||
<div style="font-size:3rem">🎉</div>
|
||||
<h3 style="margin:12px 0 6px;font-size:1.1rem">Tidak Ada Blind Spot!</h3>
|
||||
<p style="color:var(--clr-text-muted)">Seluruh warga miskin sudah berada dalam radius pelayanan rumah ibadah.</p>
|
||||
</div></div>
|
||||
<?php else: ?>
|
||||
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<div class="card-body no-pad"><div id="bs-map" style="height:320px"></div></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">⚠️</span><h3>Daftar Warga Blind Spot</h3></div>
|
||||
<div class="card-body no-pad">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>#</th><th>Nama</th><th>Kecamatan</th><th>Kelurahan</th><th>Status Kemiskinan</th><th>Prioritas</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($data as $i=>$w): ?>
|
||||
<tr>
|
||||
<td class="text-xs text-muted"><?= $i+1 ?></td>
|
||||
<td style="font-weight:600"><?= e($w['nama']) ?></td>
|
||||
<td class="text-sm"><?= e($w['kecamatan']) ?></td>
|
||||
<td class="text-sm"><?= e($w['kelurahan']) ?></td>
|
||||
<td><?php $sc=['Miskin Ekstrem'=>'badge-red','Miskin'=>'badge-orange','Rentan Miskin'=>'badge-yellow']; ?>
|
||||
<span class="badge <?= $sc[$w['status_kemiskinan']]??'badge-gray' ?>"><?= e($w['status_kemiskinan']) ?></span></td>
|
||||
<td><?php $pc=['Tinggi'=>'badge-red','Sedang'=>'badge-yellow','Rendah'=>'badge-green']; ?>
|
||||
<span class="badge <?= $pc[$w['level_prioritas']]??'badge-gray' ?>"><?= e($w['level_prioritas']) ?></span></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const map = GISMap.init('bs-map', <?= MAP_DEFAULT_LAT ?>, <?= MAP_DEFAULT_LNG ?>, 12);
|
||||
const ri = await App.fetchJSON('<?= APP_URL ?>/api/rumah-ibadah');
|
||||
if(ri) GISMap.addRILayer(ri);
|
||||
const bs = <?= json_encode(array_map(fn($w)=>['lat'=>(float)$w['lat'],'lng'=>(float)$w['lng'],'nama'=>$w['nama'],'status_kemiskinan'=>$w['status_kemiskinan']], array_filter($data,fn($w)=>$w['lat']&&$w['lng']))) ?>;
|
||||
bs.forEach(w => L.circleMarker([w.lat,w.lng],{radius:10,fillColor:'#7c3aed',color:'#fff',weight:2,fillOpacity:0.9}).bindPopup(`<strong>⚠ ${w.nama}</strong><br>${w.status_kemiskinan}`).addTo(map));
|
||||
GISMap.autoFitBounds();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
// Ekstrak data chart di luar heredoc agar tidak ada konversi array-dalam-string
|
||||
$chartKec = [];
|
||||
$chartKecData = [];
|
||||
foreach ($perKecamatan ?? [] as $row) {
|
||||
$chartKec[] = (string)($row['nama'] ?? '');
|
||||
$chartKecData[] = (int)($row['total'] ?? 0);
|
||||
}
|
||||
|
||||
$chartBulan = [];
|
||||
$chartBantuanData = [];
|
||||
foreach ($trenBantuan ?? [] as $row) {
|
||||
$chartBulan[] = (string)($row['bulan'] ?? '');
|
||||
$chartBantuanData[] = (int)($row['jumlah'] ?? 0);
|
||||
}
|
||||
|
||||
$jsonKec = json_encode($chartKec);
|
||||
$jsonKecData = json_encode($chartKecData);
|
||||
$jsonBulan = json_encode($chartBulan);
|
||||
$jsonBantuanData = json_encode($chartBantuanData);
|
||||
$mapLat = MAP_DEFAULT_LAT;
|
||||
$mapLng = MAP_DEFAULT_LNG;
|
||||
$appUrl = APP_URL;
|
||||
|
||||
$extraJs = <<<JS
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Init peta
|
||||
const map = GISMap.init('admin-map', {$mapLat}, {$mapLng}, 13);
|
||||
|
||||
// Load data peta
|
||||
const data = await App.fetchJSON('{$appUrl}/admin/api/peta');
|
||||
if (data) {
|
||||
const wargaLayer = GISMap.addWargaLayer(data.warga || []);
|
||||
const riLayer = GISMap.addRILayer(data.rumah_ibadah || []);
|
||||
const lapLayer = GISMap.addLaporanLayer(data.laporan || []);
|
||||
|
||||
const overlays = {
|
||||
'👥 Warga Miskin': wargaLayer,
|
||||
'⛪ Rumah Ibadah': riLayer,
|
||||
'📋 Laporan Masuk': lapLayer
|
||||
};
|
||||
L.control.layers(GISMap.layers, overlays, {position:'topright', collapsed:true}).addTo(map);
|
||||
|
||||
GISMap.autoFitBounds();
|
||||
|
||||
const heat = await App.fetchJSON('{$appUrl}/dinsos/api/heatmap');
|
||||
if (heat) GISMap.addHeatmap(heat);
|
||||
} else {
|
||||
GISMap.autoFitBounds(true);
|
||||
}
|
||||
|
||||
// Charts — data sudah diekstrak bersih di PHP
|
||||
const kec = {$jsonKec};
|
||||
const kecData = {$jsonKecData};
|
||||
Charts.bar('chart-kecamatan', kec, [{ label: 'Warga Miskin', data: kecData }]);
|
||||
|
||||
const bulan = {$jsonBulan};
|
||||
const bantuanData = {$jsonBantuanData};
|
||||
Charts.line('chart-bantuan', bulan, [{ label: 'Bantuan Disalurkan', data: bantuanData }]);
|
||||
});
|
||||
</script>
|
||||
JS; ?>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon blue">👥</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format((int)($stats['total_warga'] ?? 0)) ?></div>
|
||||
<div class="stat-card-label">Total Warga Miskin</div>
|
||||
<div class="stat-card-sub"><?= number_format((int)($stats['warga_ekstrem'] ?? 0)) ?> miskin ekstrem</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon green">⛪</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format((int)($stats['total_ri'] ?? 0)) ?></div>
|
||||
<div class="stat-card-label">Rumah Ibadah Aktif</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon orange">📋</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format((int)($stats['total_laporan'] ?? 0)) ?></div>
|
||||
<div class="stat-card-label">Total Laporan</div>
|
||||
<div class="stat-card-sub"><?= (int)($stats['laporan_pending'] ?? 0) ?> menunggu</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon teal">📦</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format((int)($stats['total_bantuan'] ?? 0)) ?></div>
|
||||
<div class="stat-card-label">Total Bantuan</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon purple">⚠️</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format((int)($stats['blind_spot'] ?? 0)) ?></div>
|
||||
<div class="stat-card-label">Blind Spot</div>
|
||||
<div class="stat-card-sub">Belum terjangkau rumah ibadah</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon gray">👤</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format((int)($stats['total_user'] ?? 0)) ?></div>
|
||||
<div class="stat-card-label">Pengguna Sistem</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Grid: Map + Sidebar -->
|
||||
<div class="dashboard-grid" style="grid-template-columns:1fr 360px;gap:20px;margin-bottom:24px">
|
||||
<!-- PETA UTAMA -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">🗺</span>
|
||||
<h3>Peta Persebaran Kemiskinan Kota Pontianak</h3>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-ghost btn-sm" onclick="toggleHeatmap()">🌡 Heatmap</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body no-pad" style="position:relative">
|
||||
<div id="admin-map" style="height:480px"></div>
|
||||
<div class="map-legend">
|
||||
<h4>Legenda</h4>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-ekstrem"></div> Miskin Ekstrem</div>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-miskin"></div> Miskin</div>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-rentan"></div> Rentan Miskin</div>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-ri"></div> Rumah Ibadah</div>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-blind"></div> Blind Spot</div>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-laporan"></div> Laporan Masuk</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SIDEBAR: Aktivitas & Tabel Kec -->
|
||||
<div style="display:flex;flex-direction:column;gap:16px">
|
||||
<!-- Per Kecamatan -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">🗾</span><h3>Sebaran per Kecamatan</h3></div>
|
||||
<div class="card-body no-pad">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Kecamatan</th><th>Total</th><th>Ekstrem</th><th>Blind Spot</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($perKecamatan ?? [] as $k): ?>
|
||||
<tr>
|
||||
<td style="font-weight:500;font-size:0.82rem"><?= e($k['nama']) ?></td>
|
||||
<td><strong><?= number_format((int)($k['total'] ?? 0)) ?></strong></td>
|
||||
<td><span class="badge badge-red"><?= number_format((int)($k['ekstrem'] ?? 0)) ?></span></td>
|
||||
<td><span class="badge badge-purple"><?= number_format((int)($k['blind_spot'] ?? 0)) ?></span></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aktivitas Terbaru -->
|
||||
<div class="card" style="flex:1">
|
||||
<div class="card-header"><span class="card-icon">📋</span><h3>Aktivitas Terbaru</h3></div>
|
||||
<div class="card-body no-pad" style="max-height:240px;overflow-y:auto">
|
||||
<?php foreach ($aktivitas ?? [] as $a): ?>
|
||||
<div style="padding:10px 16px;border-bottom:1px solid #f1f5f9">
|
||||
<div style="font-size:0.8rem;font-weight:600"><?= e($a['aksi']) ?></div>
|
||||
<div style="font-size:0.72rem;color:var(--clr-text-muted)"><?= e($a['nama_user'] ?? '-') ?> · <?= e($a['modul'] ?? '') ?></div>
|
||||
<div style="font-size:0.7rem;color:var(--clr-text-muted)"><?= formatTanggal($a['created_at'], true) ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts Row -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📊</span><h3>Warga Miskin per Kecamatan</h3></div>
|
||||
<div class="card-body"><div class="chart-wrap"><canvas id="chart-kecamatan"></canvas></div></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📈</span><h3>Tren Bantuan 6 Bulan Terakhir</h3></div>
|
||||
<div class="card-body"><div class="chart-wrap"><canvas id="chart-bantuan"></canvas></div></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,89 @@
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
||||
<div><h2 style="font-size:1.2rem;font-weight:700">📦 Manajemen Kategori Bantuan</h2></div>
|
||||
<button class="btn btn-primary" onclick="App.showModal('modal-tambah')">➕ Tambah Kategori</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body no-pad">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>#</th><th>Nama</th><th>Deskripsi</th><th>Satuan</th><th>Status</th><th>Aksi</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($data as $i=>$k): ?>
|
||||
<tr>
|
||||
<td class="text-xs text-muted"><?= $i+1 ?></td>
|
||||
<td style="font-weight:600"><?= e($k['nama']) ?></td>
|
||||
<td class="text-sm text-muted"><?= e($k['deskripsi']??'') ?></td>
|
||||
<td><span class="badge badge-blue"><?= e($k['satuan']??'—') ?></span></td>
|
||||
<td><?= $k['is_active'] ? '<span class="badge badge-green">Aktif</span>' : '<span class="badge badge-gray">Nonaktif</span>' ?></td>
|
||||
<td>
|
||||
<button class="btn btn-ghost btn-sm" onclick='openEdit(<?= json_encode($k) ?>)'>✏️ Edit</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Tambah -->
|
||||
<div class="modal-overlay" id="modal-tambah">
|
||||
<div class="modal">
|
||||
<div class="modal-header"><h3>➕ Tambah Kategori Bantuan</h3><button class="modal-close" onclick="App.hideModal('modal-tambah')">✕</button></div>
|
||||
<form method="POST" action="<?= APP_URL ?>/admin/kategori-bantuan/store">
|
||||
<?= csrfField() ?>
|
||||
<div class="modal-body">
|
||||
<div class="form-group"><label class="form-label">Nama Kategori <span class="required">*</span></label>
|
||||
<input type="text" name="nama" class="form-control" required></div>
|
||||
<div class="form-group"><label class="form-label">Deskripsi</label>
|
||||
<textarea name="deskripsi" class="form-control" rows="2"></textarea></div>
|
||||
<div class="form-group"><label class="form-label">Satuan</label>
|
||||
<input type="text" name="satuan" class="form-control" placeholder="paket / kg / rupiah"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" onclick="App.hideModal('modal-tambah')">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">Simpan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Edit -->
|
||||
<div class="modal-overlay" id="modal-edit">
|
||||
<div class="modal">
|
||||
<div class="modal-header"><h3>✏️ Edit Kategori</h3><button class="modal-close" onclick="App.hideModal('modal-edit')">✕</button></div>
|
||||
<form method="POST" id="edit-form">
|
||||
<?= csrfField() ?>
|
||||
<div class="modal-body">
|
||||
<div class="form-group"><label class="form-label">Nama <span class="required">*</span></label>
|
||||
<input type="text" name="nama" id="edit-nama" class="form-control" required></div>
|
||||
<div class="form-group"><label class="form-label">Deskripsi</label>
|
||||
<textarea name="deskripsi" id="edit-deskripsi" class="form-control" rows="2"></textarea></div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group"><label class="form-label">Satuan</label>
|
||||
<input type="text" name="satuan" id="edit-satuan" class="form-control"></div>
|
||||
<div class="form-group"><label class="form-label">Status</label>
|
||||
<select name="is_active" id="edit-is_active" class="form-control">
|
||||
<option value="1">Aktif</option><option value="0">Nonaktif</option>
|
||||
</select></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" onclick="App.hideModal('modal-edit')">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">Simpan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openEdit(k) {
|
||||
document.getElementById('edit-nama').value = k.nama;
|
||||
document.getElementById('edit-deskripsi').value = k.deskripsi||'';
|
||||
document.getElementById('edit-satuan').value = k.satuan||'';
|
||||
document.getElementById('edit-is_active').value = k.is_active;
|
||||
document.getElementById('edit-form').action = '<?= APP_URL ?>/admin/kategori-bantuan/' + k.id + '/update';
|
||||
App.showModal('modal-edit');
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
// monitoring.php — Admin monitoring kemiskinan view
|
||||
?>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
||||
<div><h2 style="font-size:1.2rem;font-weight:700">👥 Monitoring Data Kemiskinan</h2>
|
||||
<p class="text-muted text-sm"><?= count($warga) ?> warga aktif terdata dalam sistem</p></div>
|
||||
</div>
|
||||
|
||||
<!-- Peta -->
|
||||
<div class="card" style="margin-bottom:20px">
|
||||
<div class="card-header"><span class="card-icon">🗺</span><h3>Peta Persebaran Seluruh Warga Miskin</h3></div>
|
||||
<div class="card-body no-pad" style="position:relative">
|
||||
<div id="mon-map" style="height:360px"></div>
|
||||
<div class="map-legend">
|
||||
<h4>Legenda</h4>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-ekstrem"></div> Miskin Ekstrem</div>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-miskin"></div> Miskin</div>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-rentan"></div> Rentan Miskin</div>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-blind"></div> Blind Spot</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search + Table -->
|
||||
<div class="filters-bar"><input type="text" id="search-mon" class="form-control" placeholder="🔍 Cari nama, NIK..." style="max-width:300px"></div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body no-pad">
|
||||
<div class="table-wrap">
|
||||
<table id="table-mon">
|
||||
<thead><tr><th>#</th><th>Nama</th><th>Wilayah</th><th>Status Kemiskinan</th><th>Prioritas</th><th>Penghasilan</th><th>Tgn</th><th>Rumah Ibadah</th><th>Ket</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($warga as $i=>$w): ?>
|
||||
<tr>
|
||||
<td class="text-xs text-muted"><?= $i+1 ?></td>
|
||||
<td>
|
||||
<div style="font-weight:600;font-size:0.875rem"><?= e($w['nama']) ?></div>
|
||||
<div class="text-xs text-muted"><?= $w['nik'] ? 'NIK: '.e($w['nik']) : '' ?></div>
|
||||
</td>
|
||||
<td class="text-sm"><?= e($w['kecamatan']) ?> › <?= e($w['kelurahan']) ?></td>
|
||||
<td><?php $sc=['Miskin Ekstrem'=>'badge-red','Miskin'=>'badge-orange','Rentan Miskin'=>'badge-yellow']; ?>
|
||||
<span class="badge <?= $sc[$w['status_kemiskinan']]??'badge-gray' ?>"><?= e($w['status_kemiskinan']) ?></span></td>
|
||||
<td><?php $pc=['Tinggi'=>'badge-red','Sedang'=>'badge-yellow','Rendah'=>'badge-green']; ?>
|
||||
<span class="badge <?= $pc[$w['level_prioritas']]??'badge-gray' ?>"><?= e($w['level_prioritas']) ?></span>
|
||||
<div class="text-xs text-muted"><?= $w['skor_prioritas'] ?>/100</div></td>
|
||||
<td class="text-sm"><?= formatRupiah($w['penghasilan_bulanan']??0) ?></td>
|
||||
<td class="text-sm text-center"><?= $w['jumlah_tanggungan'] ?></td>
|
||||
<td class="text-sm"><?= e($w['rumah_ibadah_nama']??'—') ?></td>
|
||||
<td>
|
||||
<?php if($w['is_blind_spot']): ?><span class="badge badge-purple" title="Blind Spot">⚠</span><?php endif; ?>
|
||||
<?php if($w['is_lansia']): ?><span class="badge badge-orange" title="Lansia">👴</span><?php endif; ?>
|
||||
<?php if($w['is_disabilitas']): ?><span class="badge badge-blue" title="Disabilitas">♿</span><?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
App.initTableSearch('search-mon','table-mon');
|
||||
const map = GISMap.init('mon-map', <?= MAP_DEFAULT_LAT ?>, <?= MAP_DEFAULT_LNG ?>, 12);
|
||||
const data = await App.fetchJSON('<?= APP_URL ?>/admin/api/peta');
|
||||
if(data) {
|
||||
GISMap.addRILayer(data.rumah_ibadah||[]);
|
||||
GISMap.addWargaLayer(data.warga||[]);
|
||||
GISMap.autoFitBounds();
|
||||
} else {
|
||||
GISMap.autoFitBounds(true);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,309 @@
|
||||
<?php $extraJs = <<<JS
|
||||
<script>
|
||||
let riMap = null;
|
||||
let riMarker = null;
|
||||
|
||||
function openTambahModal() {
|
||||
App.showModal('modal-tambah');
|
||||
setTimeout(() => {
|
||||
if (!riMap) {
|
||||
riMap = L.map('ri-map-tambah').setView([-0.0263, 109.3425], 13);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19}).addTo(riMap);
|
||||
riMap.on('click', e => placeRIMarker(e.latlng.lat, e.latlng.lng, 'tambah'));
|
||||
}
|
||||
riMap.invalidateSize();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function placeRIMarker(lat, lng, mode) {
|
||||
const m = mode === 'tambah' ? riMap : editMap;
|
||||
const prefix = mode === 'tambah' ? '' : 'edit-';
|
||||
if (riMarker) m.removeLayer(riMarker);
|
||||
riMarker = L.marker([lat, lng], { draggable: true }).addTo(m);
|
||||
document.getElementById(prefix + 'lat').value = lat.toFixed(7);
|
||||
document.getElementById(prefix + 'lng').value = lng.toFixed(7);
|
||||
riMarker.on('dragend', e => {
|
||||
const pos = e.target.getLatLng();
|
||||
document.getElementById(prefix + 'lat').value = pos.lat.toFixed(7);
|
||||
document.getElementById(prefix + 'lng').value = pos.lng.toFixed(7);
|
||||
});
|
||||
}
|
||||
|
||||
function openEditModal(ri) {
|
||||
document.getElementById('edit-nama').value = ri.nama;
|
||||
document.getElementById('edit-alamat').value = ri.alamat || '';
|
||||
document.getElementById('edit-id_jenis').value = ri.id_jenis;
|
||||
document.getElementById('edit-id_kelurahan').value = ri.id_kelurahan;
|
||||
document.getElementById('edit-radius_meter').value = ri.radius_meter;
|
||||
document.getElementById('edit-nama_ketua').value = ri.nama_ketua || '';
|
||||
document.getElementById('edit-no_hp_pengurus').value = ri.no_hp_pengurus || '';
|
||||
document.getElementById('edit-lat').value = ri.lat;
|
||||
document.getElementById('edit-lng').value = ri.lng;
|
||||
document.getElementById('edit-is_active').value = ri.is_active;
|
||||
document.getElementById('edit-form').action = '<?= APP_URL ?>/admin/rumah-ibadah/' + ri.id + '/update';
|
||||
App.showModal('modal-edit');
|
||||
}
|
||||
</script>
|
||||
JS; ?>
|
||||
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
||||
<div>
|
||||
<h2 style="font-size:1.2rem;font-weight:700">Manajemen Rumah Ibadah</h2>
|
||||
<p class="text-muted text-sm">Total <?= count($data) ?> rumah ibadah terdaftar</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="openTambahModal()">➕ Tambah Rumah Ibadah</button>
|
||||
</div>
|
||||
|
||||
<!-- PETA RUMAH IBADAH -->
|
||||
<div class="card" style="margin-bottom:20px">
|
||||
<div class="card-header"><span class="card-icon">🗺</span><h3>Peta Rumah Ibadah</h3></div>
|
||||
<div class="card-body no-pad">
|
||||
<div id="ri-overview-map" style="height:320px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SEARCH -->
|
||||
<div class="filters-bar">
|
||||
<input type="text" id="search-ri" class="form-control" placeholder="🔍 Cari nama, kelurahan..." style="max-width:300px">
|
||||
</div>
|
||||
|
||||
<!-- TABLE -->
|
||||
<div class="card">
|
||||
<div class="card-body no-pad">
|
||||
<div class="table-wrap">
|
||||
<table id="table-ri">
|
||||
<thead>
|
||||
<tr><th>#</th><th>Nama</th><th>Jenis</th><th>Wilayah</th><th>Ketua</th><th>Radius</th><th>Warga Binaan</th><th>Status</th><th>Aksi</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($data as $i => $r): ?>
|
||||
<tr>
|
||||
<td class="text-muted text-xs"><?= $i+1 ?></td>
|
||||
<td>
|
||||
<div style="font-weight:600;font-size:0.875rem"><?= e($r['nama']) ?></div>
|
||||
<div class="text-muted text-xs"><?= e($r['alamat'] ?? '') ?></div>
|
||||
</td>
|
||||
<td><span class="badge badge-green"><?= e($r['jenis']) ?></span></td>
|
||||
<td class="text-sm"><?= e($r['kecamatan']) ?> › <?= e($r['kelurahan']) ?></td>
|
||||
<td class="text-sm"><?= e($r['nama_ketua'] ?? '—') ?></td>
|
||||
<td class="text-sm"><?= number_format($r['radius_meter']) ?>m</td>
|
||||
<td><strong><?= number_format($r['jml_warga']) ?></strong> warga</td>
|
||||
<td><?= $r['is_active'] ? '<span class="badge badge-green">Aktif</span>' : '<span class="badge badge-gray">Nonaktif</span>' ?></td>
|
||||
<td>
|
||||
<div style="display:flex;gap:6px">
|
||||
<button class="btn btn-ghost btn-sm" onclick='openEditModal(<?= json_encode($r) ?>)'>✏️</button>
|
||||
<form method="POST" action="<?= APP_URL ?>/admin/rumah-ibadah/<?= $r['id'] ?>/delete"
|
||||
onsubmit="return confirm('Nonaktifkan <?= addslashes($r['nama']) ?>?')">
|
||||
<?= csrfField() ?>
|
||||
<button type="submit" class="btn btn-ghost btn-sm" style="color:var(--clr-danger)">🗑</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL TAMBAH -->
|
||||
<div class="modal-overlay" id="modal-tambah">
|
||||
<div class="modal" style="max-width:680px">
|
||||
<div class="modal-header">
|
||||
<h3>➕ Tambah Rumah Ibadah</h3>
|
||||
<button class="modal-close" onclick="App.hideModal('modal-tambah')">✕</button>
|
||||
</div>
|
||||
<form method="POST" action="<?= APP_URL ?>/admin/rumah-ibadah/store" enctype="multipart/form-data">
|
||||
<?= csrfField() ?>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nama Rumah Ibadah <span class="required">*</span></label>
|
||||
<input type="text" name="nama" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Jenis <span class="required">*</span></label>
|
||||
<select name="id_jenis" class="form-control" required>
|
||||
<?php foreach ($jenis as $j): ?>
|
||||
<option value="<?= $j['id'] ?>"><?= e($j['nama']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kelurahan <span class="required">*</span></label>
|
||||
<select name="id_kelurahan" class="form-control" required>
|
||||
<?php foreach ($kelurahan as $k): ?>
|
||||
<option value="<?= $k['id'] ?>"><?= e($k['nama']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Alamat</label>
|
||||
<input type="text" name="alamat" class="form-control">
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nama Ketua/Pengurus</label>
|
||||
<input type="text" name="nama_ketua" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">No HP Pengurus</label>
|
||||
<input type="text" name="no_hp_pengurus" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Akun Pengurus</label>
|
||||
<select name="id_user" class="form-control">
|
||||
<option value="">— Belum Ada —</option>
|
||||
<?php foreach ($pengurusAvail as $p): ?>
|
||||
<option value="<?= $p['id'] ?>"><?= e($p['nama']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Radius Pelayanan (meter) <span class="required">*</span></label>
|
||||
<input type="number" name="radius_meter" class="form-control" value="500" min="100" max="5000" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">📍 Pilih Lokasi pada Peta <span class="required">*</span></label>
|
||||
<div id="ri-map-tambah" style="height:220px;border-radius:8px;border:1px solid var(--clr-border)"></div>
|
||||
<div class="form-row cols-2" style="margin-top:8px">
|
||||
<input type="number" name="lat" id="lat" class="form-control" placeholder="Latitude" step="0.0000001" required>
|
||||
<input type="number" name="lng" id="lng" class="form-control" placeholder="Longitude" step="0.0000001" required>
|
||||
</div>
|
||||
<div class="form-hint">Klik pada peta untuk menandai lokasi</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Foto</label>
|
||||
<input type="file" name="foto" class="form-control" accept="image/*">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" onclick="App.hideModal('modal-tambah')">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">Simpan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL EDIT -->
|
||||
<div class="modal-overlay" id="modal-edit">
|
||||
<div class="modal" style="max-width:560px">
|
||||
<div class="modal-header">
|
||||
<h3>✏️ Edit Rumah Ibadah</h3>
|
||||
<button class="modal-close" onclick="App.hideModal('modal-edit')">✕</button>
|
||||
</div>
|
||||
<form method="POST" id="edit-form" enctype="multipart/form-data">
|
||||
<?= csrfField() ?>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nama <span class="required">*</span></label>
|
||||
<input type="text" name="nama" id="edit-nama" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Jenis</label>
|
||||
<select name="id_jenis" id="edit-id_jenis" class="form-control">
|
||||
<?php foreach ($jenis as $j): ?>
|
||||
<option value="<?= $j['id'] ?>"><?= e($j['nama']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kelurahan</label>
|
||||
<select name="id_kelurahan" id="edit-id_kelurahan" class="form-control">
|
||||
<?php foreach ($kelurahan as $k): ?>
|
||||
<option value="<?= $k['id'] ?>"><?= e($k['nama']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Alamat</label>
|
||||
<input type="text" name="alamat" id="edit-alamat" class="form-control">
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nama Ketua</label>
|
||||
<input type="text" name="nama_ketua" id="edit-nama_ketua" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">No HP</label>
|
||||
<input type="text" name="no_hp_pengurus" id="edit-no_hp_pengurus" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row cols-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Radius (m)</label>
|
||||
<input type="number" name="radius_meter" id="edit-radius_meter" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status</label>
|
||||
<select name="is_active" id="edit-is_active" class="form-control">
|
||||
<option value="1">Aktif</option><option value="0">Nonaktif</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Foto Baru</label>
|
||||
<input type="file" name="foto" class="form-control" accept="image/*">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Latitude</label>
|
||||
<input type="number" name="lat" id="edit-lat" class="form-control" step="0.0000001">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Longitude</label>
|
||||
<input type="number" name="lng" id="edit-lng" class="form-control" step="0.0000001">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" onclick="App.hideModal('modal-edit')">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">Simpan Perubahan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
App.initTableSearch('search-ri', 'table-ri');
|
||||
|
||||
// Overview map
|
||||
const ovMap = L.map('ri-overview-map').setView([-0.0263, 109.3425], 12);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19}).addTo(ovMap);
|
||||
|
||||
const riData = <?= json_encode(array_map(fn($r) => [
|
||||
'id'=>$r['id'],'nama'=>$r['nama'],'lat'=>(float)$r['lat'],'lng'=>(float)$r['lng'],
|
||||
'radius_meter'=>$r['radius_meter'],'jenis'=>$r['jenis'],'kelurahan'=>$r['kelurahan'],
|
||||
'jml_warga'=>$r['jml_warga'],'is_active'=>$r['is_active']
|
||||
], $data)) ?>;
|
||||
|
||||
const riBounds = [];
|
||||
riData.forEach(ri => {
|
||||
if (!ri.lat || !ri.lng) return;
|
||||
const lat = parseFloat(ri.lat), lng = parseFloat(ri.lng);
|
||||
if (isNaN(lat)||isNaN(lng)) return;
|
||||
const icon = L.divIcon({
|
||||
className:'',
|
||||
html:`<div style="background:${ri.is_active?'#16a34a':'#6b7280'};color:#fff;padding:4px 8px;border-radius:4px;font-size:11px;font-weight:700;white-space:nowrap;box-shadow:0 2px 4px rgba(0,0,0,0.2)">${ri.nama}</div>`,
|
||||
iconAnchor:[0,0]
|
||||
});
|
||||
L.marker([lat,lng],{icon}).bindPopup(`<strong>${ri.nama}</strong><br>${ri.jenis}<br>${ri.kelurahan}<br>Radius: ${ri.radius_meter}m<br>Warga: ${ri.jml_warga}`).addTo(ovMap);
|
||||
L.circle([lat,lng],{radius:ri.radius_meter,fillColor:'#16a34a',fillOpacity:0.08,color:'#16a34a',weight:1.5,dashArray:'4,4'}).addTo(ovMap);
|
||||
riBounds.push([lat,lng]);
|
||||
});
|
||||
// Auto-zoom ke seluruh rumah ibadah Kota Pontianak
|
||||
if (riBounds.length > 0) {
|
||||
const b = L.latLngBounds(riBounds);
|
||||
if (b.isValid()) ovMap.flyToBounds(b, {duration:0.6, padding:[30,30], maxZoom:14});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,221 @@
|
||||
<!-- HEADER ROW -->
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
||||
<div>
|
||||
<h2 style="font-size:1.2rem;font-weight:700">Manajemen User</h2>
|
||||
<p class="text-muted text-sm">Kelola seluruh pengguna sistem SINERGI</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="App.showModal('modal-tambah')">➕ Tambah User</button>
|
||||
</div>
|
||||
|
||||
<!-- SEARCH -->
|
||||
<div class="filters-bar">
|
||||
<input type="text" id="search-user" class="form-control" placeholder="🔍 Cari nama, username, email..." style="max-width:300px">
|
||||
</div>
|
||||
|
||||
<!-- TABLE -->
|
||||
<div class="card">
|
||||
<div class="card-body no-pad">
|
||||
<div class="table-wrap">
|
||||
<table id="table-users">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th><th>Nama</th><th>Username</th><th>Role</th>
|
||||
<th>Wilayah</th><th>No HP</th><th>Status</th><th>Login Terakhir</th><th>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $i => $u): ?>
|
||||
<tr>
|
||||
<td class="text-muted text-sm"><?= $i+1 ?></td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<div style="width:32px;height:32px;border-radius:50%;background:var(--clr-primary);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:0.85rem;flex-shrink:0">
|
||||
<?= strtoupper(substr($u['nama'],0,1)) ?>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:0.875rem"><?= e($u['nama']) ?></div>
|
||||
<div class="text-muted text-xs"><?= e($u['email']) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><code style="font-size:0.78rem;background:#f1f5f9;padding:2px 6px;border-radius:4px"><?= e($u['username']) ?></code></td>
|
||||
<td>
|
||||
<?php $roleColors = ['superadmin'=>'purple','verifikator'=>'blue','rumah_ibadah'=>'green','dinsos'=>'teal','walikota'=>'orange']; ?>
|
||||
<span class="badge badge-<?= $roleColors[$u['role_slug']] ?? 'gray' ?>"><?= e($u['role_nama']) ?></span>
|
||||
</td>
|
||||
<td class="text-sm"><?= e($u['kelurahan_nama'] ?? '—') ?></td>
|
||||
<td class="text-sm"><?= e($u['no_hp'] ?? '—') ?></td>
|
||||
<td><?= $u['is_active'] ? '<span class="badge badge-green">Aktif</span>' : '<span class="badge badge-gray">Nonaktif</span>' ?></td>
|
||||
<td class="text-xs text-muted"><?= $u['last_login'] ? formatTanggal($u['last_login'],true) : '—' ?></td>
|
||||
<td>
|
||||
<div style="display:flex;gap:6px">
|
||||
<?php if ($u['id'] != Auth::id()): ?>
|
||||
<button class="btn btn-ghost btn-sm" onclick='openEditModal(<?= json_encode($u) ?>)'>✏️</button>
|
||||
<form method="POST" action="<?= APP_URL ?>/admin/users/<?= $u['id'] ?>/delete" style="display:inline"
|
||||
onsubmit="return confirm('Nonaktifkan user <?= addslashes($u['nama']) ?>?')">
|
||||
<?= csrfField() ?>
|
||||
<button type="submit" class="btn btn-ghost btn-sm" style="color:var(--clr-danger)">🗑</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<span class="text-xs text-muted">Akun Anda</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL TAMBAH -->
|
||||
<div class="modal-overlay" id="modal-tambah">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>➕ Tambah User Baru</h3>
|
||||
<button class="modal-close" onclick="App.hideModal('modal-tambah')">✕</button>
|
||||
</div>
|
||||
<form method="POST" action="<?= APP_URL ?>/admin/users/store">
|
||||
<?= csrfField() ?>
|
||||
<div class="modal-body">
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nama Lengkap <span class="required">*</span></label>
|
||||
<input type="text" name="nama" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Username <span class="required">*</span></label>
|
||||
<input type="text" name="username" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Email <span class="required">*</span></label>
|
||||
<input type="email" name="email" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Password <span class="required">*</span></label>
|
||||
<input type="password" name="password" class="form-control" required minlength="6">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">No HP</label>
|
||||
<input type="text" name="no_hp" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Role <span class="required">*</span></label>
|
||||
<select name="id_role" class="form-control" required>
|
||||
<?php foreach ($roles as $r): ?>
|
||||
<option value="<?= $r['id'] ?>"><?= e($r['nama']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kelurahan Tugas</label>
|
||||
<select name="id_kelurahan" class="form-control">
|
||||
<option value="">— Semua Wilayah —</option>
|
||||
<?php foreach ($kelurahan as $k): ?>
|
||||
<option value="<?= $k['id'] ?>"><?= e($k['nama']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" onclick="App.hideModal('modal-tambah')">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">Simpan User</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL EDIT -->
|
||||
<div class="modal-overlay" id="modal-edit">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>✏️ Edit User</h3>
|
||||
<button class="modal-close" onclick="App.hideModal('modal-edit')">✕</button>
|
||||
</div>
|
||||
<form method="POST" id="edit-form">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="id" id="edit-id">
|
||||
<div class="modal-body">
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nama Lengkap <span class="required">*</span></label>
|
||||
<input type="text" name="nama" id="edit-nama" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Username <span class="required">*</span></label>
|
||||
<input type="text" name="username" id="edit-username" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Email <span class="required">*</span></label>
|
||||
<input type="email" name="email" id="edit-email" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Password Baru <span class="text-muted">(kosongkan jika tidak diubah)</span></label>
|
||||
<input type="password" name="password" class="form-control" minlength="6">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">No HP</label>
|
||||
<input type="text" name="no_hp" id="edit-no_hp" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Role</label>
|
||||
<select name="id_role" id="edit-id_role" class="form-control">
|
||||
<?php foreach ($roles as $r): ?>
|
||||
<option value="<?= $r['id'] ?>"><?= e($r['nama']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status</label>
|
||||
<select name="is_active" id="edit-is_active" class="form-control">
|
||||
<option value="1">Aktif</option>
|
||||
<option value="0">Nonaktif</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kelurahan Tugas</label>
|
||||
<select name="id_kelurahan" id="edit-id_kelurahan" class="form-control">
|
||||
<option value="">— Semua Wilayah —</option>
|
||||
<?php foreach ($kelurahan as $k): ?>
|
||||
<option value="<?= $k['id'] ?>"><?= e($k['nama']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" onclick="App.hideModal('modal-edit')">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">Simpan Perubahan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
App.initTableSearch('search-user', 'table-users');
|
||||
});
|
||||
|
||||
function openEditModal(u) {
|
||||
document.getElementById('edit-id').value = u.id;
|
||||
document.getElementById('edit-nama').value = u.nama;
|
||||
document.getElementById('edit-username').value = u.username;
|
||||
document.getElementById('edit-email').value = u.email;
|
||||
document.getElementById('edit-no_hp').value = u.no_hp || '';
|
||||
document.getElementById('edit-id_role').value = u.id_role;
|
||||
document.getElementById('edit-id_kelurahan').value = u.id_kelurahan || '';
|
||||
document.getElementById('edit-is_active').value = u.is_active;
|
||||
document.getElementById('edit-form').action = '<?= APP_URL ?>/admin/users/' + u.id + '/update';
|
||||
App.showModal('modal-edit');
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,119 @@
|
||||
<!-- ADMIN WILAYAH VIEW -->
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
||||
<div>
|
||||
<h2 style="font-size:1.2rem;font-weight:700">🗾 Manajemen Wilayah</h2>
|
||||
<p class="text-muted text-sm">Data kecamatan dan kelurahan Kota Pontianak</p>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<button class="btn btn-ghost" onclick="App.showModal('modal-kecamatan')">➕ Kecamatan</button>
|
||||
<button class="btn btn-primary" onclick="App.showModal('modal-kelurahan')">➕ Kelurahan</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
||||
<!-- Kecamatan -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">🏙</span><h3>Kecamatan (<?= count($kecamatan) ?>)</h3></div>
|
||||
<div class="card-body no-pad">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>#</th><th>Nama</th><th>Kode</th><th>Jumlah Kelurahan</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($kecamatan as $i=>$k): ?>
|
||||
<tr>
|
||||
<td class="text-xs text-muted"><?= $i+1 ?></td>
|
||||
<td style="font-weight:600"><?= e($k['nama']) ?></td>
|
||||
<td class="text-xs" style="font-family:monospace"><?= e($k['kode']??'') ?></td>
|
||||
<td class="text-sm text-center"><?= number_format($k['jml_kel']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kelurahan -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">🏘</span><h3>Kelurahan (<?= count($kelurahan) ?>)</h3></div>
|
||||
<div class="card-body no-pad" style="max-height:400px;overflow-y:auto">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>#</th><th>Nama</th><th>Kecamatan</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($kelurahan as $i=>$k): ?>
|
||||
<tr>
|
||||
<td class="text-xs text-muted"><?= $i+1 ?></td>
|
||||
<td style="font-weight:500;font-size:0.875rem"><?= e($k['nama']) ?></td>
|
||||
<td class="text-sm text-muted"><?= e($k['kecamatan']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Kecamatan -->
|
||||
<div class="modal-overlay" id="modal-kecamatan">
|
||||
<div class="modal">
|
||||
<div class="modal-header"><h3>➕ Tambah Kecamatan</h3><button class="modal-close" onclick="App.hideModal('modal-kecamatan')">✕</button></div>
|
||||
<form method="POST" action="<?= APP_URL ?>/admin/kecamatan/store">
|
||||
<?= csrfField() ?>
|
||||
<div class="modal-body">
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group"><label class="form-label">Nama Kecamatan <span class="required">*</span></label>
|
||||
<input type="text" name="nama" class="form-control" required></div>
|
||||
<div class="form-group"><label class="form-label">Kode Wilayah</label>
|
||||
<input type="text" name="kode" class="form-control"></div>
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group"><label class="form-label">Latitude</label>
|
||||
<input type="number" name="lat" class="form-control" step="0.0000001"></div>
|
||||
<div class="form-group"><label class="form-label">Longitude</label>
|
||||
<input type="number" name="lng" class="form-control" step="0.0000001"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" onclick="App.hideModal('modal-kecamatan')">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">Simpan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Kelurahan -->
|
||||
<div class="modal-overlay" id="modal-kelurahan">
|
||||
<div class="modal">
|
||||
<div class="modal-header"><h3>➕ Tambah Kelurahan</h3><button class="modal-close" onclick="App.hideModal('modal-kelurahan')">✕</button></div>
|
||||
<form method="POST" action="<?= APP_URL ?>/admin/kelurahan/store">
|
||||
<?= csrfField() ?>
|
||||
<div class="modal-body">
|
||||
<div class="form-group"><label class="form-label">Kecamatan <span class="required">*</span></label>
|
||||
<select name="id_kecamatan" class="form-control" required>
|
||||
<option value="">— Pilih Kecamatan —</option>
|
||||
<?php foreach($kecamatan as $k): ?>
|
||||
<option value="<?= $k['id'] ?>"><?= e($k['nama']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group"><label class="form-label">Nama Kelurahan <span class="required">*</span></label>
|
||||
<input type="text" name="nama" class="form-control" required></div>
|
||||
<div class="form-group"><label class="form-label">Kode Wilayah</label>
|
||||
<input type="text" name="kode" class="form-control"></div>
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group"><label class="form-label">Latitude</label>
|
||||
<input type="number" name="lat" class="form-control" step="0.0000001"></div>
|
||||
<div class="form-group"><label class="form-label">Longitude</label>
|
||||
<input type="number" name="lng" class="form-control" step="0.0000001"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" onclick="App.hideModal('modal-kelurahan')">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">Simpan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="app-url" content="<?= APP_URL ?>">
|
||||
<title>Login — SINERGI</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="<?= asset('css/app.css') ?>">
|
||||
</head>
|
||||
<body>
|
||||
<?php if (session_status()===PHP_SESSION_NONE){session_start();} ?>
|
||||
|
||||
<div class="login-page">
|
||||
<!-- LEFT PANEL -->
|
||||
<div class="login-left">
|
||||
<div class="login-logo">SINER<span>GI</span></div>
|
||||
<div class="login-tagline">
|
||||
Sistem Informasi Geografis Penanganan Kemiskinan<br>
|
||||
Berbasis Kolaborasi Rumah Ibadah, Masyarakat,<br>
|
||||
dan Pemerintah Kota Pontianak
|
||||
</div>
|
||||
|
||||
<div style="margin-top:48px;display:grid;grid-template-columns:1fr 1fr;gap:16px;max-width:380px;width:100%">
|
||||
<?php $items = [
|
||||
['🗺','Peta GIS Real-time'],['⛪','Kolaborasi Rumah Ibadah'],
|
||||
['📊','Dashboard Monitoring'],['⚡','Deteksi Blind Spot'],
|
||||
]; foreach ($items as [$icon,$label]): ?>
|
||||
<div style="background:rgba(255,255,255,0.08);border-radius:10px;padding:14px;display:flex;gap:10px;align-items:center;">
|
||||
<span style="font-size:1.4rem"><?= $icon ?></span>
|
||||
<span style="font-size:0.8rem;color:rgba(255,255,255,0.8);font-weight:500"><?= $label ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:48px;text-align:center">
|
||||
<a href="<?= APP_URL ?>/" style="color:rgba(255,255,255,0.5);font-size:0.82rem">← Kembali ke Halaman Publik</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT PANEL (Form) -->
|
||||
<div class="login-right">
|
||||
<div style="margin-bottom:32px;">
|
||||
<div style="font-size:2rem;font-weight:900;color:var(--clr-primary);">SINER<span style="color:var(--clr-accent)">GI</span></div>
|
||||
</div>
|
||||
|
||||
<div class="login-form-title">Selamat Datang</div>
|
||||
<div class="login-form-sub">Masuk ke sistem untuk melanjutkan</div>
|
||||
|
||||
<?= flashMessage() ?>
|
||||
|
||||
<form method="POST" action="<?= APP_URL ?>/login">
|
||||
<?= csrfField() ?>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="username">Username / Email <span class="required">*</span></label>
|
||||
<input type="text" id="username" name="username" class="form-control"
|
||||
placeholder="Masukkan username atau email" required autofocus
|
||||
value="<?= e($_POST['username'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">Password <span class="required">*</span></label>
|
||||
<div style="position:relative">
|
||||
<input type="password" id="password" name="password" class="form-control"
|
||||
placeholder="Masukkan password" required style="padding-right:44px">
|
||||
<button type="button" onclick="togglePwd()" style="position:absolute;right:12px;top:50%;transform:translateY(-50%);border:none;background:none;cursor:pointer;color:var(--clr-text-muted);font-size:1rem;" id="pwd-toggle">👁</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-full" style="margin-top:8px;padding:12px;font-size:1rem;justify-content:center;">
|
||||
Masuk ke Sistem
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div style="margin-top:20px;text-align:center">
|
||||
<a href="<?= APP_URL ?>/" style="font-size:0.82rem;color:var(--clr-text-muted)">← Kembali ke Beranda</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function togglePwd() {
|
||||
const i = document.getElementById('password');
|
||||
const b = document.getElementById('pwd-toggle');
|
||||
i.type = i.type === 'password' ? 'text' : 'password';
|
||||
b.textContent = i.type === 'password' ? '👁' : '🙈';
|
||||
}
|
||||
function fillLogin(u, p) {
|
||||
document.getElementById('username').value = u;
|
||||
document.getElementById('password').value = p;
|
||||
}
|
||||
</script>
|
||||
<script src="<?= asset('js/app.js') ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,89 @@
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
||||
<div>
|
||||
<h2 style="font-size:1.2rem;font-weight:700">📦 Monitoring Bantuan</h2>
|
||||
<p class="text-muted text-sm"><?= count($data) ?> penyaluran pada bulan <?= e($bulan) ?></p>
|
||||
</div>
|
||||
<form method="GET" style="display:flex;gap:8px;align-items:center">
|
||||
<label class="text-sm">Bulan:</label>
|
||||
<input type="month" name="bulan" class="form-control" value="<?= e($bulan) ?>" onchange="this.form.submit()">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Stats Bulan Ini -->
|
||||
<div class="stats-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:20px">
|
||||
<div class="stat-card"><div class="stat-card-icon green">📦</div>
|
||||
<div class="stat-card-body"><div class="stat-card-value"><?= count($data) ?></div><div class="stat-card-label">Total Penyaluran</div></div>
|
||||
</div>
|
||||
<div class="stat-card"><div class="stat-card-icon blue">👥</div>
|
||||
<div class="stat-card-body"><div class="stat-card-value"><?= count(array_unique(array_column($data,'id_warga'))) ?></div><div class="stat-card-label">Warga Terbantu</div></div>
|
||||
</div>
|
||||
<div class="stat-card"><div class="stat-card-icon orange">⛪</div>
|
||||
<div class="stat-card-body"><div class="stat-card-value"><?= count(array_unique(array_column($data,'id_rumah_ibadah'))) ?></div><div class="stat-card-label">Rumah Ibadah Aktif</div></div>
|
||||
</div>
|
||||
<div class="stat-card"><div class="stat-card-icon purple">📊</div>
|
||||
<div class="stat-card-body"><div class="stat-card-value"><?= count($perKategori) ?></div><div class="stat-card-label">Jenis Bantuan</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart + Tabel -->
|
||||
<div style="display:grid;grid-template-columns:1fr 280px;gap:20px;margin-bottom:20px">
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📋</span><h3>Daftar Penyaluran Bantuan</h3></div>
|
||||
<div class="card-body no-pad">
|
||||
<div class="filters-bar" style="margin:0;border:none;border-bottom:1px solid var(--clr-border);border-radius:0">
|
||||
<input type="text" id="search-bantuan" class="form-control" placeholder="🔍 Cari nama warga..." style="max-width:280px">
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table id="table-bantuan">
|
||||
<thead>
|
||||
<tr><th>Kode</th><th>Warga</th><th>Kategori</th><th>Jumlah</th><th>Rumah Ibadah</th><th>Wilayah</th><th>Tanggal</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach($data as $b): ?>
|
||||
<tr>
|
||||
<td><code style="font-size:0.72rem;background:#f1f5f9;padding:1px 5px;border-radius:3px"><?= e($b['kode_bantuan']) ?></code></td>
|
||||
<td style="font-weight:600;font-size:0.875rem"><?= e($b['warga']) ?></td>
|
||||
<td><span class="badge badge-green"><?= e($b['kategori']) ?></span></td>
|
||||
<td class="text-sm">
|
||||
<?= $b['jumlah'] ? ($b['satuan']==='rupiah' ? formatRupiah($b['jumlah']) : number_format($b['jumlah']).' '.$b['satuan']) : '—' ?>
|
||||
</td>
|
||||
<td class="text-sm"><?= e($b['rumah_ibadah']) ?></td>
|
||||
<td class="text-xs text-muted"><?= e($b['kecamatan']) ?></td>
|
||||
<td class="text-xs text-muted"><?= formatTanggal($b['tanggal_penyaluran']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if(empty($data)): ?>
|
||||
<div class="empty-state"><div class="empty-state-icon">📭</div><h3>Tidak ada penyaluran</h3><p>Belum ada bantuan yang disalurkan pada periode ini</p></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Donut per Kategori -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">🥧</span><h3>Per Kategori</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="chart-wrap" style="height:200px"><canvas id="chart-kategori"></canvas></div>
|
||||
<div style="margin-top:12px">
|
||||
<?php foreach($perKategori as $k): ?>
|
||||
<div style="display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid #f1f5f9;font-size:0.8rem">
|
||||
<span><?= e($k['nama']) ?></span>
|
||||
<strong><?= number_format($k['jumlah']) ?></strong>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
App.initTableSearch('search-bantuan','table-bantuan');
|
||||
Charts.donut('chart-kategori',
|
||||
<?= json_encode(array_column($perKategori,'nama')) ?>,
|
||||
<?= json_encode(array_column($perKategori,'jumlah')) ?>
|
||||
);
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,377 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
// Ekstrak semua data sebelum heredoc agar tidak terjadi Array to string conversion
|
||||
$mapLat = MAP_DEFAULT_LAT;
|
||||
$mapLng = MAP_DEFAULT_LNG;
|
||||
$appUrl = APP_URL;
|
||||
|
||||
$jsonKec = json_encode(array_column($perKecamatan ?? [], 'nama'));
|
||||
$jsonTotals = json_encode(array_column($perKecamatan ?? [], 'total'));
|
||||
$jsonEkstrems = json_encode(array_column($perKecamatan ?? [], 'ekstrem'));
|
||||
|
||||
$jsonBulan = json_encode(array_column($trenBantuan ?? [], 'bulan'));
|
||||
$jsonJmlBantuan = json_encode(array_column($trenBantuan ?? [], 'jumlah'));
|
||||
$jsonWargaTerbantu = json_encode(array_column($trenBantuan ?? [], 'warga_terbantu'));
|
||||
|
||||
$extraJs = <<<JS
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Peta dengan heatmap
|
||||
const map = GISMap.init('dinsos-map', {$mapLat}, {$mapLng}, 12);
|
||||
|
||||
// Load semua data
|
||||
const [heatData, petaData] = await Promise.all([
|
||||
App.fetchJSON('{$appUrl}/dinsos/api/heatmap'),
|
||||
App.fetchJSON('{$appUrl}/admin/api/peta')
|
||||
]);
|
||||
|
||||
if(petaData) {
|
||||
const riLayer = GISMap.addRILayer(petaData.rumah_ibadah||[]);
|
||||
const wLayer = GISMap.addWargaLayer(petaData.warga||[]);
|
||||
L.control.layers(GISMap.layers, {
|
||||
'⛪ Rumah Ibadah': riLayer,
|
||||
'👥 Warga Miskin': wLayer
|
||||
}, {position:'topright', collapsed:true}).addTo(map);
|
||||
}
|
||||
if(heatData) GISMap.addHeatmap(heatData);
|
||||
// Auto zoom ke seluruh Kota Pontianak
|
||||
GISMap.autoFitBounds();
|
||||
|
||||
// Chart per kecamatan
|
||||
const kec = {$jsonKec};
|
||||
const totals = {$jsonTotals};
|
||||
const ekstrems = {$jsonEkstrems};
|
||||
Charts.bar('chart-kecamatan', kec, [
|
||||
{label:'Total Warga', data:totals},
|
||||
{label:'Miskin Ekstrem', data:ekstrems}
|
||||
]);
|
||||
|
||||
// Chart tren bantuan
|
||||
const bulan = {$jsonBulan};
|
||||
const jmlBantuan = {$jsonJmlBantuan};
|
||||
const wargaTerbantu = {$jsonWargaTerbantu};
|
||||
Charts.line('chart-tren', bulan, [
|
||||
{label:'Bantuan Disalurkan', data:jmlBantuan},
|
||||
{label:'Warga Terbantu', data:wargaTerbantu}
|
||||
]);
|
||||
});
|
||||
</script>
|
||||
JS; ?>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon blue">👥</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format($stats['total_warga']??0) ?></div>
|
||||
<div class="stat-card-label">Total Warga Miskin</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($stats['ekstrem']??0) ?></div>
|
||||
<div class="stat-card-label">Miskin Ekstrem</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon orange">⚠</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format($stats['miskin']??0) ?></div>
|
||||
<div class="stat-card-label">Miskin</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon yellow">📊</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format($stats['rentan']??0) ?></div>
|
||||
<div class="stat-card-label">Rentan Miskin</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon purple">⚠️</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format($stats['blind_spot']??0) ?></div>
|
||||
<div class="stat-card-label">Blind Spot</div>
|
||||
<div class="stat-card-sub"><a href="<?= APP_URL ?>/dinsos/blind-spot" style="color:var(--clr-primary-l)">Detail →</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon green">📦</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format($stats['total_bantuan']??0) ?></div>
|
||||
<div class="stat-card-label">Total Bantuan</div>
|
||||
<div class="stat-card-sub"><?= number_format($stats['bantuan_bulan_ini']??0) ?> bulan ini</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid: Peta Besar + Info -->
|
||||
<div class="dashboard-grid" style="margin-bottom:20px">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">🌡</span><h3>Heatmap Kemiskinan Kota Pontianak</h3>
|
||||
<div class="card-actions">
|
||||
<a href="<?= APP_URL ?>/dinsos/kemiskinan" class="btn btn-ghost btn-sm">Detail Peta</a>
|
||||
<a href="<?= APP_URL ?>/dinsos/blind-spot" class="btn btn-danger btn-sm">⚠ Blind Spot</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body no-pad" style="position:relative">
|
||||
<div id="dinsos-map" style="height:460px"></div>
|
||||
<div class="map-legend">
|
||||
<h4>Intensitas Kemiskinan</h4>
|
||||
<div style="height:12px;border-radius:6px;background:linear-gradient(to right,#fef9c3,#fb923c,#ef4444,#7f1d1d);margin-bottom:6px"></div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:0.65rem;color:var(--clr-text-muted)">
|
||||
<span>Rendah</span><span>Tinggi</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar: Per kec + Top RI -->
|
||||
<div style="display:flex;flex-direction:column;gap:16px">
|
||||
<!-- Peringkat Kecamatan -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">🗾</span><h3>Kemiskinan per Kecamatan</h3></div>
|
||||
<div class="card-body no-pad">
|
||||
<?php foreach($perKecamatan as $i=>$k): ?>
|
||||
<div style="padding:10px 16px;border-bottom:1px solid #f1f5f9">
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
|
||||
<span style="font-weight:600;font-size:0.82rem"><?= $i+1 ?>. <?= e($k['nama']) ?></span>
|
||||
<span style="font-weight:700;color:var(--clr-primary)"><?= number_format($k['total']) ?></span>
|
||||
</div>
|
||||
<div style="height:5px;background:#f1f5f9;border-radius:3px;overflow:hidden">
|
||||
<?php $maxTotal = max(array_column($perKecamatan,'total')?:[1]); ?>
|
||||
<div style="height:100%;background:var(--clr-danger);width:<?= $maxTotal>0?round($k['total']/$maxTotal*100):0 ?>%;border-radius:3px"></div>
|
||||
</div>
|
||||
<div style="font-size:0.7rem;color:var(--clr-text-muted);margin-top:2px">
|
||||
Ekstrem: <?= $k['ekstrem'] ?> · Blind: <?= $k['blind_spot']??0 ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Rumah Ibadah Aktif -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">🏆</span><h3>Rumah Ibadah Paling Aktif</h3></div>
|
||||
<div class="card-body no-pad">
|
||||
<?php foreach($topRi as $i=>$r): ?>
|
||||
<div style="padding:10px 16px;border-bottom:1px solid #f1f5f9;display:flex;gap:12px;align-items:center">
|
||||
<div style="width:28px;height:28px;border-radius:50%;background:var(--clr-accent);color:var(--clr-primary-d);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:0.85rem;flex-shrink:0"><?= $i+1 ?></div>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-weight:600;font-size:0.82rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"><?= e($r['nama']) ?></div>
|
||||
<div style="font-size:0.72rem;color:var(--clr-text-muted)"><?= number_format($r['jml_bantuan']) ?> bantuan · <?= number_format($r['jml_warga']) ?> warga</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts Row -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📊</span><h3>Perbandingan per Kecamatan</h3></div>
|
||||
<div class="card-body"><div class="chart-wrap"><canvas id="chart-kecamatan"></canvas></div></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📈</span><h3>Tren Bantuan 6 Bulan</h3></div>
|
||||
<div class="card-body"><div class="chart-wrap"><canvas id="chart-tren"></canvas></div></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,137 @@
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
||||
<div>
|
||||
<h2 style="font-size:1.2rem;font-weight:700">🗺 Monitoring Kemiskinan</h2>
|
||||
<p class="text-muted text-sm"><?= count($data) ?> warga terfilter · Klik marker pada peta untuk detail</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<form method="GET" action="<?= APP_URL ?>/dinsos/kemiskinan">
|
||||
<div class="filters-bar" style="margin-bottom:16px">
|
||||
<select name="kecamatan" class="form-control" onchange="this.form.submit()">
|
||||
<option value="">— Semua Kecamatan —</option>
|
||||
<?php foreach($kecamatan as $k): ?>
|
||||
<option value="<?= $k['id'] ?>" <?= $filter_kec==$k['id']?'selected':'' ?>><?= e($k['nama']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if(!empty($kelurahan)): ?>
|
||||
<select name="kelurahan" class="form-control" onchange="this.form.submit()">
|
||||
<option value="">— Semua Kelurahan —</option>
|
||||
<?php foreach($kelurahan as $k): ?>
|
||||
<option value="<?= $k['id'] ?>" <?= $filter_kel==$k['id']?'selected':'' ?>><?= e($k['nama']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php endif; ?>
|
||||
<select name="status" class="form-control" onchange="this.form.submit()">
|
||||
<option value="">— Semua Status —</option>
|
||||
<?php foreach(['Miskin Ekstrem','Miskin','Rentan Miskin'] as $s): ?>
|
||||
<option value="<?= $s ?>" <?= $filter_status===$s?'selected':'' ?>><?= $s ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select name="prioritas" class="form-control" onchange="this.form.submit()">
|
||||
<option value="">— Semua Prioritas —</option>
|
||||
<?php foreach(['Tinggi','Sedang','Rendah'] as $p): ?>
|
||||
<option value="<?= $p ?>" <?= $filter_prioritas===$p?'selected':'' ?>><?= $p ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if($filter_kec||$filter_kel||$filter_status||$filter_prioritas): ?>
|
||||
<a href="<?= APP_URL ?>/dinsos/kemiskinan" class="btn btn-ghost btn-sm">✕ Reset</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Grid: Peta + Daftar -->
|
||||
<div style="display:grid;grid-template-columns:1fr 360px;gap:16px">
|
||||
<!-- PETA -->
|
||||
<div class="card">
|
||||
<div class="card-body no-pad" style="position:relative">
|
||||
<div id="kemiskinan-map" style="height:600px"></div>
|
||||
<div class="map-legend">
|
||||
<h4>Legenda</h4>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-ekstrem"></div> Miskin Ekstrem</div>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-miskin"></div> Miskin</div>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-rentan"></div> Rentan Miskin</div>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-ri"></div> Rumah Ibadah</div>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-blind"></div> Blind Spot</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DAFTAR WARGA -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">👥</span>
|
||||
<h3>Daftar Warga (<?= count($data) ?>)</h3>
|
||||
</div>
|
||||
<div style="overflow-y:auto;max-height:560px">
|
||||
<?php foreach($data as $w): ?>
|
||||
<div class="sidebar-panel-item" id="warga-item-<?= $w['id'] ?>"
|
||||
onclick="highlightWarga(<?= $w['id'] ?>,<?= (float)$w['lat'] ?>,<?= (float)$w['lng'] ?>)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<h4><?= e($w['nama']) ?></h4>
|
||||
<div style="display:flex;gap:4px;align-items:center">
|
||||
<?php if($w['is_blind_spot']): ?><span class="badge badge-purple" style="font-size:0.62rem">Blind</span><?php endif; ?>
|
||||
<?php $pc=['Tinggi'=>'badge-red','Sedang'=>'badge-yellow','Rendah'=>'badge-green']; ?>
|
||||
<span class="badge <?= $pc[$w['level_prioritas']]??'badge-gray' ?>" style="font-size:0.62rem"><?= $w['level_prioritas'] ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<p><?= e($w['kecamatan']) ?> › <?= e($w['kelurahan']) ?></p>
|
||||
<div style="font-size:0.72rem;color:var(--clr-text-muted);margin-top:2px">
|
||||
<?= e($w['status_kemiskinan']) ?> · <?= formatRupiah($w['penghasilan_bulanan']??0) ?>/bln
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php if(empty($data)): ?>
|
||||
<div class="empty-state"><div class="empty-state-icon">👥</div><h3>Tidak ada data</h3></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let wargaMarkers = {};
|
||||
|
||||
function highlightWarga(id, lat, lng) {
|
||||
document.querySelectorAll('.sidebar-panel-item').forEach(el => el.classList.remove('active'));
|
||||
document.getElementById('warga-item-'+id)?.classList.add('active');
|
||||
if (wargaMarkers[id]) {
|
||||
wargaMarkers[id].openPopup();
|
||||
GISMap.map.setView([lat,lng],16);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const map = GISMap.init('kemiskinan-map', <?= MAP_DEFAULT_LAT ?>, <?= MAP_DEFAULT_LNG ?>, 12);
|
||||
|
||||
const riData = await App.fetchJSON('<?= APP_URL ?>/api/rumah-ibadah');
|
||||
if(riData) GISMap.addRILayer(riData);
|
||||
// Zoom ke Pontianak terlebih dahulu
|
||||
GISMap.autoFitBounds();
|
||||
|
||||
const wData = <?= json_encode(array_map(fn($w) => [
|
||||
'id'=>$w['id'],'nama'=>$w['nama'],'lat'=>(float)$w['lat'],'lng'=>(float)$w['lng'],
|
||||
'status_kemiskinan'=>$w['status_kemiskinan'],'level_prioritas'=>$w['level_prioritas'],
|
||||
'is_blind_spot'=>$w['is_blind_spot'],'kelurahan'=>$w['kelurahan']??'',
|
||||
'kecamatan'=>$w['kecamatan']??'','penghasilan_bulanan'=>$w['penghasilan_bulanan']??0
|
||||
], array_filter($data, fn($w)=>$w['lat']&&$w['lng']))) ?>;
|
||||
|
||||
const colors = {'Miskin Ekstrem':'#dc2626','Miskin':'#ea580c','Rentan Miskin':'#ca8a04'};
|
||||
|
||||
wData.forEach(w => {
|
||||
const color = w.is_blind_spot ? '#7c3aed' : (colors[w.status_kemiskinan]||'#6b7280');
|
||||
const m = L.circleMarker([w.lat,w.lng],{radius:9,fillColor:color,color:'#fff',weight:2,fillOpacity:0.9})
|
||||
.bindPopup(`<div style="min-width:180px"><strong>${w.nama}</strong><br>
|
||||
<span style="font-size:12px;color:#6b7280">${w.kecamatan} › ${w.kelurahan}</span><br>
|
||||
<span style="color:${color};font-weight:700;font-size:11px">${w.status_kemiskinan}</span>
|
||||
${w.is_blind_spot?'<br><span style="color:#7c3aed;font-weight:700;font-size:11px">⚠ Blind Spot</span>':''}
|
||||
</div>`)
|
||||
.addTo(map);
|
||||
wargaMarkers[w.id] = m;
|
||||
m.on('click', () => {
|
||||
document.querySelectorAll('.sidebar-panel-item').forEach(el=>el.classList.remove('active'));
|
||||
document.getElementById('warga-item-'+w.id)?.classList.add('active');
|
||||
document.getElementById('warga-item-'+w.id)?.scrollIntoView({behavior:'smooth',block:'nearest'});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,123 @@
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
||||
<div>
|
||||
<h2 style="font-size:1.2rem;font-weight:700">🏆 Kinerja Rumah Ibadah</h2>
|
||||
<p class="text-muted text-sm">Monitoring efektivitas penyaluran bantuan oleh seluruh rumah ibadah</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="stats-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:20px">
|
||||
<?php
|
||||
$totalBantuan = array_sum(array_column($data,'jml_bantuan'));
|
||||
$totalWarga = array_sum(array_column($data,'jml_warga'));
|
||||
$aktif = count(array_filter($data, fn($d)=>$d['jml_bantuan']>0));
|
||||
?>
|
||||
<div class="stat-card"><div class="stat-card-icon green">⛪</div>
|
||||
<div class="stat-card-body"><div class="stat-card-value"><?= count($data) ?></div><div class="stat-card-label">Total Rumah Ibadah</div></div>
|
||||
</div>
|
||||
<div class="stat-card"><div class="stat-card-icon blue">✅</div>
|
||||
<div class="stat-card-body"><div class="stat-card-value"><?= $aktif ?></div><div class="stat-card-label">Aktif Menyalurkan</div></div>
|
||||
</div>
|
||||
<div class="stat-card"><div class="stat-card-icon teal">📦</div>
|
||||
<div class="stat-card-body"><div class="stat-card-value"><?= number_format($totalBantuan) ?></div><div class="stat-card-label">Total Bantuan</div></div>
|
||||
</div>
|
||||
<div class="stat-card"><div class="stat-card-icon orange">👥</div>
|
||||
<div class="stat-card-body"><div class="stat-card-value"><?= number_format($totalWarga) ?></div><div class="stat-card-label">Total Warga Binaan</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts + Table -->
|
||||
<div style="display:grid;grid-template-columns:1fr 300px;gap:20px;margin-bottom:20px">
|
||||
<!-- Bar chart -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📊</span><h3>Ranking Bantuan per Rumah Ibadah (Top 10)</h3></div>
|
||||
<div class="card-body"><div class="chart-wrap" style="height:300px"><canvas id="chart-kinerja"></canvas></div></div>
|
||||
</div>
|
||||
|
||||
<!-- Donut: aktif vs tidak -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">🥧</span><h3>Status Partisipasi</h3></div>
|
||||
<div class="card-body"><div class="chart-wrap" style="height:200px"><canvas id="chart-partisipasi"></canvas></div>
|
||||
<div style="text-align:center;margin-top:12px">
|
||||
<div style="font-size:0.8rem;color:var(--clr-text-muted)">
|
||||
<span style="color:#16a34a;font-weight:700"><?= $aktif ?></span> aktif |
|
||||
<span style="color:#dc2626;font-weight:700"><?= count($data)-$aktif ?></span> belum aktif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabel lengkap -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📋</span><h3>Detail Kinerja Semua Rumah Ibadah</h3></div>
|
||||
<div class="card-body no-pad">
|
||||
<div class="filters-bar" style="margin:0;border:none;border-bottom:1px solid var(--clr-border);border-radius:0">
|
||||
<input type="text" id="search-kinerja" class="form-control" placeholder="🔍 Cari nama rumah ibadah..." style="max-width:280px">
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table id="table-kinerja">
|
||||
<thead>
|
||||
<tr><th>#</th><th>Nama</th><th>Jenis</th><th>Wilayah</th><th>Warga Binaan</th><th>Bantuan Disalurkan</th><th>Miskin Ekstrem</th><th>Bantuan Terakhir</th><th>Status</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach($data as $i=>$r): ?>
|
||||
<tr>
|
||||
<td class="text-xs text-muted"><?= $i+1 ?></td>
|
||||
<td>
|
||||
<div style="font-weight:600;font-size:0.875rem"><?= e($r['nama']) ?></div>
|
||||
<div class="text-xs text-muted">Radius: <?= number_format($r['radius_meter']) ?>m</div>
|
||||
</td>
|
||||
<td><span class="badge badge-green"><?= e($r['jenis']) ?></span></td>
|
||||
<td class="text-sm"><?= e($r['kecamatan']) ?> › <?= e($r['kelurahan']) ?></td>
|
||||
<td class="text-sm text-center"><strong><?= number_format($r['jml_warga']) ?></strong></td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<strong style="color:var(--clr-success)"><?= number_format($r['jml_bantuan']) ?></strong>
|
||||
<div class="priority-bar" style="width:60px">
|
||||
<?php $mx=max(array_column($data,'jml_bantuan')?:[1]); ?>
|
||||
<div class="priority-bar-fill low" style="width:<?= $mx>0?round($r['jml_bantuan']/$mx*100):0 ?>%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-sm text-center">
|
||||
<?php if($r['jml_ekstrem']>0): ?>
|
||||
<span class="badge badge-red"><?= number_format($r['jml_ekstrem']) ?></span>
|
||||
<?php else: ?><span class="text-muted">—</span><?php endif; ?>
|
||||
</td>
|
||||
<td class="text-xs text-muted"><?= $r['bantuan_terakhir'] ? formatTanggal($r['bantuan_terakhir']) : '—' ?></td>
|
||||
<td>
|
||||
<?php if($r['jml_bantuan']>0): ?>
|
||||
<span class="badge badge-green">✅ Aktif</span>
|
||||
<?php elseif($r['jml_warga']>0): ?>
|
||||
<span class="badge badge-yellow">⏳ Ada Binaan</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-gray">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
App.initTableSearch('search-kinerja','table-kinerja');
|
||||
|
||||
const top10 = <?= json_encode(array_slice($data,0,10)) ?>;
|
||||
Charts.bar('chart-kinerja',
|
||||
top10.map(r=>r.nama.length>20?r.nama.slice(0,20)+'...':r.nama),
|
||||
[{label:'Bantuan Disalurkan', data:top10.map(r=>r.jml_bantuan)},
|
||||
{label:'Warga Binaan', data:top10.map(r=>r.jml_warga)}]
|
||||
);
|
||||
|
||||
Charts.donut('chart-partisipasi',
|
||||
['Aktif Menyalurkan','Belum Aktif'],
|
||||
[<?= $aktif ?>, <?= count($data)-$aktif ?>],
|
||||
['#16a34a','#dc2626']
|
||||
);
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,85 @@
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
||||
<div>
|
||||
<h2 style="font-size:1.2rem;font-weight:700">📄 Laporan Tahunan</h2>
|
||||
<p class="text-muted text-sm">Rekap penyaluran bantuan tahun <?= $tahun ?></p>
|
||||
</div>
|
||||
<form method="GET" style="display:flex;gap:8px;align-items:center">
|
||||
<label class="text-sm">Tahun:</label>
|
||||
<select name="tahun" class="form-control" onchange="this.form.submit()">
|
||||
<?php for($y=date('Y');$y>=2020;$y--): ?>
|
||||
<option value="<?= $y ?>" <?= $tahun==$y?'selected':'' ?>><?= $y ?></option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
<button type="button" class="btn btn-ghost btn-sm" onclick="window.print()">🖨 Cetak</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Ringkasan -->
|
||||
<div class="stats-grid" style="grid-template-columns:repeat(3,1fr);margin-bottom:20px">
|
||||
<div class="stat-card"><div class="stat-card-icon green">📦</div>
|
||||
<div class="stat-card-body"><div class="stat-card-value"><?= number_format($ringkasan['total_bantuan']??0) ?></div><div class="stat-card-label">Total Penyaluran Bantuan</div></div>
|
||||
</div>
|
||||
<div class="stat-card"><div class="stat-card-icon blue">👥</div>
|
||||
<div class="stat-card-body"><div class="stat-card-value"><?= number_format($ringkasan['total_warga_terbantu']??0) ?></div><div class="stat-card-label">Warga Mendapat Bantuan</div></div>
|
||||
</div>
|
||||
<div class="stat-card"><div class="stat-card-icon orange">⛪</div>
|
||||
<div class="stat-card-body"><div class="stat-card-value"><?= number_format($ringkasan['total_ri_aktif']??0) ?></div><div class="stat-card-label">Rumah Ibadah Aktif</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart bulanan -->
|
||||
<div class="card" style="margin-bottom:20px">
|
||||
<div class="card-header"><span class="card-icon">📊</span><h3>Grafik Penyaluran Bantuan Bulan per Bulan Tahun <?= $tahun ?></h3></div>
|
||||
<div class="card-body"><div class="chart-wrap" style="height:280px"><canvas id="chart-bulanan"></canvas></div></div>
|
||||
</div>
|
||||
|
||||
<!-- Tabel per bulan -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📋</span><h3>Rekap per Bulan</h3></div>
|
||||
<div class="card-body no-pad">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Bulan</th><th>Bantuan Disalurkan</th><th>Warga Terbantu</th><th>Rata-rata/Warga</th></tr></thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$namaBulan=['','Januari','Februari','Maret','April','Mei','Juni','Juli','Agustus','September','Oktober','November','Desember'];
|
||||
$totalBulanan=0; $totalWarga=0;
|
||||
foreach($perBulan as $b):
|
||||
$totalBulanan+=$b['bantuan'];
|
||||
$totalWarga+=$b['warga_terbantu'];
|
||||
$avg=$b['warga_terbantu']>0?round($b['bantuan']/$b['warga_terbantu'],1):0;
|
||||
?>
|
||||
<tr>
|
||||
<td style="font-weight:600"><?= $namaBulan[(int)$b['bulan']] ?></td>
|
||||
<td><strong><?= number_format($b['bantuan']) ?></strong></td>
|
||||
<td><?= number_format($b['warga_terbantu']) ?> warga</td>
|
||||
<td class="text-sm text-muted"><?= $avg ?> kali/warga</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<!-- Total -->
|
||||
<tr style="background:#f0f9ff;font-weight:700">
|
||||
<td>TOTAL</td>
|
||||
<td><?= number_format($totalBulanan) ?></td>
|
||||
<td><?= number_format($totalWarga) ?> (unik)</td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if(empty($perBulan)): ?>
|
||||
<div class="empty-state"><div class="empty-state-icon">📭</div><h3>Tidak ada data</h3><p>Belum ada penyaluran pada tahun <?= $tahun ?></p></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const bulanData = <?= json_encode($perBulan) ?>;
|
||||
const namaBulan = ['','Januari','Februari','Maret','April','Mei','Juni','Juli','Agustus','September','Oktober','November','Desember'];
|
||||
Charts.bar('chart-bulanan',
|
||||
bulanData.map(b=>namaBulan[parseInt(b.bulan)]),
|
||||
[{label:'Bantuan Disalurkan', data:bulanData.map(b=>b.bantuan)},
|
||||
{label:'Warga Terbantu', data:bulanData.map(b=>b.warga_terbantu)}]
|
||||
);
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="app-url" content="<?= APP_URL ?>">
|
||||
<meta name="csrf-token" content="<?= $_SESSION['csrf_token'] ?? '' ?>">
|
||||
<title><?= e($title ?? 'SINERGI') ?> — SINERGI</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||
<link rel="stylesheet" href="<?= asset('css/app.css') ?>">
|
||||
<?php if (isset($extraCss)) echo $extraCss; ?>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="app-shell">
|
||||
<!-- SIDEBAR -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<div class="sidebar-brand-icon">🗺</div>
|
||||
<div class="sidebar-brand-text">
|
||||
<h1>SINERGI</h1>
|
||||
<p>GIS Kemiskinan Pontianak</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-title">Menu Utama</div>
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="<?= APP_URL ?>/admin/dashboard">
|
||||
<span class="nav-icon">📊</span> Dashboard
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-title">Data Master</div>
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="<?= APP_URL ?>/admin/users">
|
||||
<span class="nav-icon">👤</span> Manajemen User
|
||||
</a></li>
|
||||
<li><a href="<?= APP_URL ?>/admin/rumah-ibadah">
|
||||
<span class="nav-icon">⛪</span> Rumah Ibadah
|
||||
</a></li>
|
||||
<li><a href="<?= APP_URL ?>/admin/wilayah">
|
||||
<span class="nav-icon">🗾</span> Wilayah
|
||||
</a></li>
|
||||
<li><a href="<?= APP_URL ?>/admin/kategori-bantuan">
|
||||
<span class="nav-icon">📦</span> Kategori Bantuan
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-title">Monitoring</div>
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="<?= APP_URL ?>/admin/monitoring">
|
||||
<span class="nav-icon">👥</span> Data Kemiskinan
|
||||
</a></li>
|
||||
<li><a href="<?= APP_URL ?>/admin/blind-spot">
|
||||
<span class="nav-icon">⚠️</span> Blind Spot
|
||||
</a></li>
|
||||
<li><a href="<?= APP_URL ?>/admin/aktivitas">
|
||||
<span class="nav-icon">📋</span> Log Aktivitas
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div class="sidebar-user">
|
||||
<div class="sidebar-user-avatar"><?= strtoupper(substr($user['nama'] ?? 'A', 0, 1)) ?></div>
|
||||
<div class="sidebar-user-info">
|
||||
<div class="sidebar-user-name"><?= e($user['nama'] ?? '') ?></div>
|
||||
<div class="sidebar-user-role">Super Admin</div>
|
||||
</div>
|
||||
<a href="<?= APP_URL ?>/logout" class="sidebar-logout" title="Logout">⏻</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- TOPBAR -->
|
||||
<header class="topbar">
|
||||
<div class="topbar-title"><?= e($title ?? '') ?></div>
|
||||
<div class="topbar-actions">
|
||||
<span class="topbar-time" id="live-clock"></span>
|
||||
<a href="<?= APP_URL ?>" target="_blank" class="btn btn-ghost btn-sm">🌐 Publik</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- MAIN CONTENT -->
|
||||
<main class="main-content">
|
||||
<?= flashMessage() ?>
|
||||
<?= $content ?>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script src="<?= asset('js/app.js') ?>"></script>
|
||||
<?php if (isset($extraJs)) echo $extraJs; ?>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="app-url" content="<?= APP_URL ?>">
|
||||
<meta name="csrf-token" content="<?= $_SESSION['csrf_token'] ?? '' ?>">
|
||||
<title><?= e($title ?? 'SINERGI') ?> — Dinas Sosial</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||
<link rel="stylesheet" href="<?= asset('css/app.css') ?>">
|
||||
<?php if (isset($extraCss)) echo $extraCss; ?>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<div class="sidebar-brand-icon" style="background:#0284c7">📈</div>
|
||||
<div class="sidebar-brand-text">
|
||||
<h1>SINERGI</h1>
|
||||
<p>Dinas Sosial</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-title">Monitoring</div>
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="<?= APP_URL ?>/dinsos/dashboard"><span class="nav-icon">📊</span> Dashboard</a></li>
|
||||
<li><a href="<?= APP_URL ?>/dinsos/kemiskinan"><span class="nav-icon">🗺</span> Peta Kemiskinan</a></li>
|
||||
<li><a href="<?= APP_URL ?>/dinsos/bantuan"><span class="nav-icon">📦</span> Monitoring Bantuan</a></li>
|
||||
<li><a href="<?= APP_URL ?>/dinsos/blind-spot"><span class="nav-icon">⚠️</span> Blind Spot</a></li>
|
||||
<li><a href="<?= APP_URL ?>/dinsos/kinerja"><span class="nav-icon">🏆</span> Kinerja Rumah Ibadah</a></li>
|
||||
<li><a href="<?= APP_URL ?>/dinsos/laporan"><span class="nav-icon">📄</span> Laporan Tahunan</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div class="sidebar-user">
|
||||
<div class="sidebar-user-avatar" style="background:#0284c7"><?= strtoupper(substr($user['nama'] ?? 'D', 0, 1)) ?></div>
|
||||
<div class="sidebar-user-info">
|
||||
<div class="sidebar-user-name"><?= e($user['nama'] ?? '') ?></div>
|
||||
<div class="sidebar-user-role">Dinas Sosial</div>
|
||||
</div>
|
||||
<a href="<?= APP_URL ?>/logout" class="sidebar-logout" title="Logout">⏻</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<header class="topbar">
|
||||
<div class="topbar-title"><?= e($title ?? '') ?></div>
|
||||
<div class="topbar-actions">
|
||||
<span class="topbar-time" id="live-clock"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<?= flashMessage() ?>
|
||||
<?= $content ?>
|
||||
</main>
|
||||
</div>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script src="<?= asset('js/app.js') ?>"></script>
|
||||
<?php if (isset($extraJs)) echo $extraJs; ?>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="app-url" content="<?= APP_URL ?>">
|
||||
<meta name="csrf-token" content="<?= $_SESSION['csrf_token'] ?? '' ?>">
|
||||
<title><?= e($title ?? 'SINERGI') ?></title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||
<link rel="stylesheet" href="<?= asset('css/app.css') ?>">
|
||||
<style>
|
||||
body { background: #f8fafc; }
|
||||
.public-hero {
|
||||
background: linear-gradient(135deg, var(--clr-primary-d) 0%, var(--clr-primary) 100%);
|
||||
color: #fff; padding: 80px 32px; text-align: center;
|
||||
}
|
||||
.public-hero h1 { font-size: clamp(1.8rem, 4vw, 3rem); font-weight: 900; }
|
||||
.public-hero p { font-size: 1rem; color: rgba(255,255,255,0.75); max-width: 600px; margin: 12px auto 0; }
|
||||
.public-container { max-width: 1100px; margin: 0 auto; padding: 0 24px; }
|
||||
</style>
|
||||
<?php if (isset($extraCss)) echo $extraCss; ?>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="public-nav">
|
||||
<a href="<?= APP_URL ?>/" class="public-nav-brand">
|
||||
<span style="background:var(--clr-accent);width:32px;height:32px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:16px;">🗺</span>
|
||||
<span>SINER<span>GI</span></span>
|
||||
</a>
|
||||
<div class="public-nav-links">
|
||||
<a href="<?= APP_URL ?>/">Beranda</a>
|
||||
<a href="<?= APP_URL ?>/tentang">Tentang</a>
|
||||
<a href="<?= APP_URL ?>/kontak">Kontak</a>
|
||||
<a href="<?= APP_URL ?>/lapor" style="background:rgba(255,255,255,0.1);padding:7px 14px;border-radius:6px;">📝 Lapor</a>
|
||||
<a href="<?= APP_URL ?>/cek-laporan">🔍 Cek Status</a>
|
||||
<a href="<?= APP_URL ?>/login" class="btn-login" style="background:var(--clr-accent);color:var(--clr-primary-d);padding:7px 18px;border-radius:6px;font-weight:700;">Login</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php
|
||||
// Expose kode_laporan dari flash ke GLOBALS sebelum flashMessage() mengonsumsinya
|
||||
// flashMessage() memanggil getFlash() yang menghapus session — jadi kita ambil dulu
|
||||
if (isset($_SESSION['flash']['kode_laporan'])) {
|
||||
$GLOBALS['_flash_kode_laporan'] = $_SESSION['flash']['kode_laporan'];
|
||||
}
|
||||
?>
|
||||
<?= flashMessage() ?>
|
||||
<?= $content ?>
|
||||
|
||||
<footer class="public-footer">
|
||||
<div style="margin-bottom:8px;">
|
||||
<strong style="color:rgba(255,255,255,0.8);">SINERGI</strong> — Sistem Informasi Geografis Penanganan Kemiskinan<br>
|
||||
Berbasis Kolaborasi Rumah Ibadah, Masyarakat, dan Pemerintah Kota Pontianak
|
||||
</div>
|
||||
<div>© <?= date('Y') ?> Pemerintah Kota Pontianak — Dinas Sosial</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="<?= asset('js/app.js') ?>"></script>
|
||||
<?php if (isset($extraJs)) echo $extraJs; ?>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="app-url" content="<?= APP_URL ?>">
|
||||
<meta name="csrf-token" content="<?= $_SESSION['csrf_token'] ?? '' ?>">
|
||||
<title><?= e($title ?? 'SINERGI') ?> — SINERGI Rumah Ibadah</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||
<link rel="stylesheet" href="<?= asset('css/app.css') ?>">
|
||||
<?php if (isset($extraCss)) echo $extraCss; ?>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<div class="sidebar-brand-icon">⛪</div>
|
||||
<div class="sidebar-brand-text">
|
||||
<h1>SINERGI</h1>
|
||||
<p>Pengurus Rumah Ibadah</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($ri)): ?>
|
||||
<div style="padding:14px 16px;background:rgba(255,255,255,0.06);margin:12px;border-radius:8px;">
|
||||
<div style="font-size:0.7rem;color:rgba(255,255,255,0.45);text-transform:uppercase;letter-spacing:0.08em;margin-bottom:4px;">Rumah Ibadah</div>
|
||||
<div style="font-size:0.88rem;color:#fff;font-weight:600;"><?= e($ri['nama']) ?></div>
|
||||
<div style="font-size:0.75rem;color:rgba(255,255,255,0.5);"><?= e($ri['jenis'] ?? '') ?> • <?= e($ri['kelurahan'] ?? '') ?></div>
|
||||
<div style="font-size:0.72rem;color:#16a34a;margin-top:3px;">Radius: <?= number_format($ri['radius_meter'] ?? 500) ?>m</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-title">Menu</div>
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="<?= APP_URL ?>/rumah-ibadah/dashboard"><span class="nav-icon">📊</span> Dashboard</a></li>
|
||||
<li><a href="<?= APP_URL ?>/rumah-ibadah/warga"><span class="nav-icon">👥</span> Warga Binaan</a></li>
|
||||
<li><a href="<?= APP_URL ?>/rumah-ibadah/prioritas"><span class="nav-icon">🔥</span> Prioritas Bantuan</a></li>
|
||||
<li><a href="<?= APP_URL ?>/rumah-ibadah/bantuan"><span class="nav-icon">📦</span> Histori Bantuan</a></li>
|
||||
<li><a href="<?= APP_URL ?>/rumah-ibadah/bantuan/tambah"><span class="nav-icon">➕</span> Catat Bantuan</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div class="sidebar-user">
|
||||
<div class="sidebar-user-avatar"><?= strtoupper(substr($user['nama'] ?? 'R', 0, 1)) ?></div>
|
||||
<div class="sidebar-user-info">
|
||||
<div class="sidebar-user-name"><?= e($user['nama'] ?? '') ?></div>
|
||||
<div class="sidebar-user-role">Pengurus</div>
|
||||
</div>
|
||||
<a href="<?= APP_URL ?>/logout" class="sidebar-logout" title="Logout">⏻</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<header class="topbar">
|
||||
<div class="topbar-title"><?= e($title ?? '') ?></div>
|
||||
<div class="topbar-actions">
|
||||
<span class="topbar-time" id="live-clock"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<?= flashMessage() ?>
|
||||
<?= $content ?>
|
||||
</main>
|
||||
</div>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script src="<?= asset('js/app.js') ?>"></script>
|
||||
<?php if (isset($extraJs)) echo $extraJs; ?>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="app-url" content="<?= APP_URL ?>">
|
||||
<meta name="csrf-token" content="<?= $_SESSION['csrf_token'] ?? '' ?>">
|
||||
<title><?= e($title ?? 'SINERGI') ?> — SINERGI Verifikator</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||
<link rel="stylesheet" href="<?= asset('css/app.css') ?>">
|
||||
<?php if (isset($extraCss)) echo $extraCss; ?>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<div class="sidebar-brand-icon">✅</div>
|
||||
<div class="sidebar-brand-text">
|
||||
<h1>SINERGI</h1>
|
||||
<p>Verifikator Kelurahan</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php $u = Auth::user(); ?>
|
||||
<div style="padding:14px 16px;background:rgba(255,255,255,0.05);margin:12px;border-radius:8px;">
|
||||
<div style="font-size:0.7rem;color:rgba(255,255,255,0.45);text-transform:uppercase;letter-spacing:0.08em;margin-bottom:4px;">Wilayah Tugas</div>
|
||||
<div style="font-size:0.88rem;color:#fff;font-weight:600;"><?= e($u['kelurahan_nama'] ?? '-') ?></div>
|
||||
<div style="font-size:0.75rem;color:rgba(255,255,255,0.5);"><?= e($u['kecamatan_nama'] ?? '') ?></div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-title">Menu</div>
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="<?= APP_URL ?>/verifikator/dashboard"><span class="nav-icon">📊</span> Dashboard</a></li>
|
||||
<li><a href="<?= APP_URL ?>/verifikator/laporan"><span class="nav-icon">📋</span> Daftar Laporan</a></li>
|
||||
<li><a href="<?= APP_URL ?>/verifikator/warga"><span class="nav-icon">👥</span> Data Warga</a></li>
|
||||
<li><a href="<?= APP_URL ?>/verifikator/warga/tambah"><span class="nav-icon">➕</span> Tambah Warga</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div class="sidebar-user">
|
||||
<div class="sidebar-user-avatar"><?= strtoupper(substr($user['nama'] ?? 'V', 0, 1)) ?></div>
|
||||
<div class="sidebar-user-info">
|
||||
<div class="sidebar-user-name"><?= e($user['nama'] ?? '') ?></div>
|
||||
<div class="sidebar-user-role">Verifikator</div>
|
||||
</div>
|
||||
<a href="<?= APP_URL ?>/logout" class="sidebar-logout" title="Logout">⏻</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<header class="topbar">
|
||||
<div class="topbar-title"><?= e($title ?? '') ?></div>
|
||||
<div class="topbar-actions">
|
||||
<span class="topbar-time" id="live-clock"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<?= flashMessage() ?>
|
||||
<?= $content ?>
|
||||
</main>
|
||||
</div>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script src="<?= asset('js/app.js') ?>"></script>
|
||||
<?php if (isset($extraJs)) echo $extraJs; ?>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="app-url" content="<?= APP_URL ?>">
|
||||
<title><?= e($title ?? 'SINERGI') ?> — Dashboard Eksekutif</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||
<link rel="stylesheet" href="<?= asset('css/app.css') ?>">
|
||||
<style>
|
||||
:root { --clr-sidebar-bg: #1a0533; }
|
||||
.sidebar-brand-icon { background: #7c3aed !important; }
|
||||
</style>
|
||||
<?php if (isset($extraCss)) echo $extraCss; ?>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<div class="sidebar-brand-icon">🏛</div>
|
||||
<div class="sidebar-brand-text">
|
||||
<h1>SINERGI</h1>
|
||||
<p>Dashboard Eksekutif</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding:14px 16px;background:rgba(255,255,255,0.06);margin:12px;border-radius:8px;text-align:center;">
|
||||
<div style="font-size:1.8rem;margin-bottom:4px;">🏛</div>
|
||||
<div style="font-size:0.88rem;color:#fff;font-weight:700;">Wali Kota Pontianak</div>
|
||||
<div style="font-size:0.75rem;color:rgba(255,255,255,0.5);">Dashboard Hanya Baca</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-title">Menu</div>
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="<?= APP_URL ?>/walikota/dashboard" class="active"><span class="nav-icon">📊</span> Dashboard Eksekutif</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="padding:16px;margin:0 12px;background:rgba(124,58,237,0.12);border-radius:8px;border:1px solid rgba(124,58,237,0.2);">
|
||||
<div style="font-size:0.72rem;color:rgba(255,255,255,0.5);margin-bottom:4px;">ℹ️ Mode Akses</div>
|
||||
<div style="font-size:0.78rem;color:rgba(255,255,255,0.7);">Hanya dapat melihat data. Tidak dapat mengubah data apapun.</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div class="sidebar-user">
|
||||
<div class="sidebar-user-avatar" style="background:#7c3aed"><?= strtoupper(substr($user['nama'] ?? 'W', 0, 1)) ?></div>
|
||||
<div class="sidebar-user-info">
|
||||
<div class="sidebar-user-name"><?= e($user['nama'] ?? '') ?></div>
|
||||
<div class="sidebar-user-role">Wali Kota</div>
|
||||
</div>
|
||||
<a href="<?= APP_URL ?>/logout" class="sidebar-logout" title="Logout">⏻</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<header class="topbar">
|
||||
<div class="topbar-title"><?= e($title ?? '') ?></div>
|
||||
<div class="topbar-actions">
|
||||
<span style="font-size:0.78rem;color:#7c3aed;font-weight:600;background:#f3e8ff;padding:4px 10px;border-radius:20px;">🏛 Wali Kota — Read Only</span>
|
||||
<span class="topbar-time" id="live-clock"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<?= $content ?>
|
||||
</main>
|
||||
</div>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script src="<?= asset('js/app.js') ?>"></script>
|
||||
<?php if (isset($extraJs)) echo $extraJs; ?>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8"><title>403 Akses Ditolak — SINERGI</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="<?= APP_URL ?>/public/css/app.css">
|
||||
</head>
|
||||
<body style="background:var(--clr-bg);display:flex;align-items:center;justify-content:center;min-height:100vh">
|
||||
<div style="text-align:center;padding:40px">
|
||||
<div style="font-size:5rem">🚫</div>
|
||||
<h1 style="font-size:4rem;font-weight:900;color:var(--clr-danger);margin:8px 0">403</h1>
|
||||
<h2 style="font-size:1.3rem;font-weight:700;margin-bottom:8px">Akses Ditolak</h2>
|
||||
<p style="color:var(--clr-text-muted);margin-bottom:24px">Anda tidak memiliki izin untuk mengakses halaman ini.</p>
|
||||
<div style="display:flex;gap:12px;justify-content:center">
|
||||
<a href="<?= APP_URL ?>/login" class="btn btn-primary">Login dengan Akun Lain</a>
|
||||
<a href="<?= APP_URL ?>/" class="btn btn-ghost">Ke Beranda</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8"><title>404 — SINERGI</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="<?= APP_URL ?>/public/css/app.css">
|
||||
</head>
|
||||
<body style="background:var(--clr-bg);display:flex;align-items:center;justify-content:center;min-height:100vh">
|
||||
<div style="text-align:center;padding:40px">
|
||||
<div style="font-size:5rem">🗺</div>
|
||||
<h1 style="font-size:4rem;font-weight:900;color:var(--clr-primary);margin:8px 0">404</h1>
|
||||
<h2 style="font-size:1.3rem;font-weight:700;margin-bottom:8px">Halaman Tidak Ditemukan</h2>
|
||||
<p style="color:var(--clr-text-muted);margin-bottom:24px">Halaman yang Anda cari tidak ada atau sudah dipindahkan.</p>
|
||||
<a href="<?= APP_URL ?>/" class="btn btn-primary">← Kembali ke Beranda</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,91 @@
|
||||
<!-- HERO -->
|
||||
<div class="public-hero">
|
||||
<div style="font-size:3.5rem;margin-bottom:12px">🗺</div>
|
||||
<h1>SINERGI</h1>
|
||||
<p>Sistem Informasi Geografis Penanganan Kemiskinan Berbasis Kolaborasi Rumah Ibadah, Masyarakat, dan Pemerintah Kota Pontianak</p>
|
||||
<div style="margin-top:28px;display:flex;gap:12px;justify-content:center;flex-wrap:wrap">
|
||||
<a href="<?= APP_URL ?>/lapor" style="background:var(--clr-accent);color:var(--clr-primary-d);padding:12px 28px;border-radius:8px;font-weight:700;font-size:1rem;text-decoration:none">📝 Lapor Warga Membutuhkan</a>
|
||||
<a href="<?= APP_URL ?>/cek-laporan" style="background:rgba(255,255,255,0.12);color:#fff;padding:12px 28px;border-radius:8px;font-weight:600;font-size:1rem;text-decoration:none;border:1px solid rgba(255,255,255,0.2)">🔍 Cek Status Laporan</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STATS -->
|
||||
<div class="public-container" style="margin-top:-40px">
|
||||
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:16px;margin-bottom:40px">
|
||||
<?php $sItems = [
|
||||
['👥', number_format($stats['total_warga']??0), 'Warga Terdata'],
|
||||
['⛪', number_format($stats['total_ri']??0), 'Rumah Ibadah'],
|
||||
['📋', number_format($stats['total_laporan']??0), 'Total Laporan'],
|
||||
['📦', number_format($stats['total_bantuan']??0), 'Bantuan Disalurkan'],
|
||||
]; foreach($sItems as [$icon,$val,$label]): ?>
|
||||
<div style="background:#fff;border-radius:12px;padding:20px;text-align:center;box-shadow:0 4px 16px rgba(0,0,0,0.08);border:1px solid var(--clr-border)">
|
||||
<div style="font-size:2rem"><?= $icon ?></div>
|
||||
<div style="font-size:1.6rem;font-weight:800;color:var(--clr-primary);margin-top:4px"><?= $val ?></div>
|
||||
<div style="font-size:0.78rem;color:var(--clr-text-muted)"><?= $label ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<div style="background:var(--clr-primary);border-radius:12px;padding:20px;text-align:center;box-shadow:0 4px 16px rgba(0,0,0,0.15)">
|
||||
<div style="font-size:2rem">🏛</div>
|
||||
<div style="font-size:0.9rem;font-weight:700;color:#fff;margin-top:8px">Kota Pontianak</div>
|
||||
<div style="font-size:0.72rem;color:rgba(255,255,255,0.6)">6 Kecamatan</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PETA PUBLIK -->
|
||||
<div class="public-container" style="margin-bottom:48px">
|
||||
<h2 style="font-size:1.4rem;font-weight:800;color:var(--clr-text);margin-bottom:8px">Peta Persebaran Kemiskinan</h2>
|
||||
<p style="color:var(--clr-text-muted);margin-bottom:16px;font-size:0.9rem">Data real-time persebaran warga miskin dan rumah ibadah di Kota Pontianak</p>
|
||||
<div style="border-radius:16px;overflow:hidden;border:1px solid var(--clr-border);box-shadow:0 8px 32px rgba(0,0,0,0.1);position:relative">
|
||||
<div id="public-map" style="height:460px"></div>
|
||||
<div class="map-legend">
|
||||
<h4>Keterangan</h4>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-ekstrem"></div> Miskin Ekstrem</div>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-miskin"></div> Miskin</div>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-rentan"></div> Rentan Miskin</div>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-ri"></div> Rumah Ibadah</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CARA MELAPOR -->
|
||||
<div style="background:#f8fafc;padding:60px 0">
|
||||
<div class="public-container">
|
||||
<h2 style="text-align:center;font-size:1.4rem;font-weight:800;margin-bottom:8px">Cara Melapor</h2>
|
||||
<p style="text-align:center;color:var(--clr-text-muted);margin-bottom:36px">Mudah dan cepat, tanpa perlu daftar atau login</p>
|
||||
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:24px">
|
||||
<?php $steps = [
|
||||
['1','📝','Isi Formulir','Isi nama warga, alamat, dan kondisi yang dialami'],
|
||||
['2','📷','Upload Foto','Unggah foto kondisi tempat tinggal (opsional)'],
|
||||
['3','📍','Tandai Lokasi','Pilih lokasi tepat pada peta interaktif'],
|
||||
['4','✅','Kirim Laporan','Simpan kode laporan untuk memantau status'],
|
||||
]; foreach($steps as [$num,$icon,$title,$desc]): ?>
|
||||
<div style="text-align:center;padding:24px 16px">
|
||||
<div style="width:52px;height:52px;border-radius:50%;background:var(--clr-primary);color:#fff;display:flex;align-items:center;justify-content:center;font-size:1.3rem;margin:0 auto 12px">
|
||||
<?= $icon ?>
|
||||
</div>
|
||||
<div style="font-size:0.72rem;font-weight:700;color:var(--clr-accent);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:4px">Langkah <?= $num ?></div>
|
||||
<div style="font-weight:700;margin-bottom:6px"><?= $title ?></div>
|
||||
<div style="font-size:0.82rem;color:var(--clr-text-muted)"><?= $desc ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div style="text-align:center;margin-top:28px">
|
||||
<a href="<?= APP_URL ?>/lapor" style="background:var(--clr-primary);color:#fff;padding:13px 32px;border-radius:8px;font-weight:700;font-size:1rem;text-decoration:none;display:inline-block">📝 Mulai Melapor Sekarang</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const map = GISMap.init('public-map', <?= MAP_DEFAULT_LAT ?>, <?= MAP_DEFAULT_LNG ?>, 12);
|
||||
const [wArgaData, riData] = await Promise.all([
|
||||
App.fetchJSON('<?= APP_URL ?>/api/warga-peta'),
|
||||
App.fetchJSON('<?= APP_URL ?>/api/rumah-ibadah')
|
||||
]);
|
||||
if(wArgaData) GISMap.addWargaLayer(wArgaData);
|
||||
if(riData) GISMap.addRILayer(riData);
|
||||
// Auto zoom ke seluruh Kota Pontianak setelah marker dimuat
|
||||
GISMap.autoFitBounds();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,102 @@
|
||||
<!-- CEK STATUS LAPORAN -->
|
||||
<div style="background:linear-gradient(135deg,var(--clr-primary-d),var(--clr-primary));color:#fff;padding:40px 32px;text-align:center">
|
||||
<h1 style="font-size:1.8rem;font-weight:800">🔍 Cek Status Laporan</h1>
|
||||
<p style="color:rgba(255,255,255,0.75)">Masukkan kode laporan yang Anda terima saat mengirim laporan</p>
|
||||
</div>
|
||||
|
||||
<div class="public-container" style="padding-top:36px;padding-bottom:60px;max-width:600px">
|
||||
<?= flashMessage() ?>
|
||||
|
||||
<div class="card" style="margin-bottom:24px">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="<?= APP_URL ?>/cek-laporan">
|
||||
<?= csrfField() ?>
|
||||
<div style="display:flex;gap:12px">
|
||||
<input type="text" name="kode_laporan" class="form-control"
|
||||
placeholder="Contoh: LPR-20240501-0001" required
|
||||
value="<?= e($kode ?? '') ?>" style="font-family:monospace;font-weight:600;letter-spacing:0.05em">
|
||||
<button type="submit" class="btn btn-primary" style="white-space:nowrap">🔍 Cek</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (isset($laporan) && !$laporan && isset($kode) && $kode): ?>
|
||||
<div class="card">
|
||||
<div class="card-body" style="text-align:center;padding:40px">
|
||||
<div style="font-size:3rem">❓</div>
|
||||
<h3 style="margin:12px 0 6px">Laporan Tidak Ditemukan</h3>
|
||||
<p style="color:var(--clr-text-muted)">Kode laporan <strong><?= e($kode) ?></strong> tidak ditemukan. Pastikan kode sudah benar.</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php elseif (isset($laporan) && $laporan): ?>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📋</span><h3>Hasil Pencarian</h3></div>
|
||||
<div class="card-body">
|
||||
<!-- Status Bar -->
|
||||
<?php
|
||||
$steps = ['menunggu'=>1,'diproses'=>2,'terverifikasi'=>3,'ditolak'=>-1];
|
||||
$cur = $steps[$laporan['status']] ?? 1;
|
||||
$colors = ['menunggu'=>'#d97706','diproses'=>'#0284c7','terverifikasi'=>'#16a34a','ditolak'=>'#dc2626'];
|
||||
$icons = ['menunggu'=>'⏳','diproses'=>'🔄','terverifikasi'=>'✅','ditolak'=>'❌'];
|
||||
?>
|
||||
<div style="text-align:center;margin-bottom:24px">
|
||||
<div style="font-size:2.5rem;margin-bottom:8px"><?= $icons[$laporan['status']] ?? '❓' ?></div>
|
||||
<div style="font-size:1.1rem;font-weight:800;color:<?= $colors[$laporan['status']] ?? '#6b7280' ?>">
|
||||
<?= ucfirst($laporan['status']) ?>
|
||||
</div>
|
||||
<div style="font-size:0.82rem;color:var(--clr-text-muted);margin-top:4px">
|
||||
Kode: <strong style="font-family:monospace"><?= e($laporan['kode_laporan']) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($laporan['status'] !== 'ditolak'): ?>
|
||||
<!-- Progress Steps -->
|
||||
<div style="display:flex;align-items:center;gap:0;margin-bottom:24px">
|
||||
<?php $stepLabels = ['Laporan Dikirim','Sedang Diproses','Terverifikasi']; ?>
|
||||
<?php foreach ($stepLabels as $i => $sl): ?>
|
||||
<div style="flex:1;text-align:center">
|
||||
<div style="width:32px;height:32px;border-radius:50%;margin:0 auto 4px;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:0.85rem;
|
||||
background:<?= $i+1<=$cur?'var(--clr-success)':'#e2e8f0' ?>;
|
||||
color:<?= $i+1<=$cur?'#fff':'#94a3b8' ?>">
|
||||
<?= $i+1<=$cur?'✓':($i+1) ?>
|
||||
</div>
|
||||
<div style="font-size:0.72rem;color:<?= $i+1<=$cur?'var(--clr-success)':'#94a3b8' ?>;font-weight:<?= $i+1<=$cur?'600':'400' ?>">
|
||||
<?= $sl ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($i < 2): ?>
|
||||
<div style="flex:1;height:2px;background:<?= $i+1<$cur?'var(--clr-success)':'#e2e8f0' ?>;margin-bottom:20px"></div>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Detail -->
|
||||
<div style="display:grid;gap:10px">
|
||||
<?php $rows = [
|
||||
['Nama Warga', $laporan['nama_warga']],
|
||||
['Wilayah', ($laporan['kelurahan']??'').", ".($laporan['kecamatan']??'')],
|
||||
['Tanggal Laporan', formatTanggal($laporan['created_at'],true)],
|
||||
['Tanggal Verifikasi', $laporan['tanggal_verifikasi'] ? formatTanggal($laporan['tanggal_verifikasi'],true) : '—'],
|
||||
]; foreach($rows as [$l,$v]): ?>
|
||||
<div style="display:flex;gap:12px;padding:8px 0;border-bottom:1px solid #f1f5f9">
|
||||
<div style="font-size:0.8rem;color:var(--clr-text-muted);min-width:130px"><?= $l ?></div>
|
||||
<div style="font-size:0.875rem;font-weight:500"><?= e($v) ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($laporan['catatan_verifikator']): ?>
|
||||
<div style="margin-top:8px;padding:12px;background:#f8fafc;border-radius:8px;border-left:3px solid var(--clr-primary)">
|
||||
<div style="font-size:0.75rem;color:var(--clr-text-muted);margin-bottom:4px">Catatan Petugas:</div>
|
||||
<div style="font-size:0.875rem"><?= nl2br(e($laporan['catatan_verifikator'])) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="margin-top:20px;text-align:center">
|
||||
<a href="<?= APP_URL ?>/lapor" style="color:var(--clr-text-muted);font-size:0.85rem">Buat laporan baru →</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
<div class="public-hero" style="padding:50px 32px">
|
||||
<h1 style="font-size:1.8rem;font-weight:800">📞 Kontak</h1>
|
||||
<p style="color:rgba(255,255,255,0.75)">Hubungi kami untuk informasi lebih lanjut tentang SINERGI</p>
|
||||
</div>
|
||||
<div class="public-container" style="padding:48px 24px">
|
||||
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:20px;max-width:800px;margin:0 auto">
|
||||
<div class="card"><div class="card-body">
|
||||
<div style="font-size:2rem;margin-bottom:10px">🏛</div>
|
||||
<h3 style="font-weight:700;margin-bottom:8px">Dinas Sosial Kota Pontianak</h3>
|
||||
<p style="font-size:0.85rem;color:var(--clr-text-muted)">📍 Jl. Gusti Sulung Lelanang No. 1B Pontianak Selatan, Darat Sekip, Kec. Pontianak Kota, Kota Pontianak, Kalimantan Barat 78116</p>
|
||||
<p style="font-size:0.85rem;color:var(--clr-text-muted)">📞 (0561) 8108 969</p>
|
||||
<p style="font-size:0.85rem;color:var(--clr-primary-l)">✉ dinsos@pontianak.go.id</p>
|
||||
</div></div>
|
||||
<div class="card"><div class="card-body">
|
||||
<div style="font-size:2rem;margin-bottom:10px">🌐</div>
|
||||
<h3 style="font-weight:700;margin-bottom:8px">Pemerintah Kota Pontianak</h3>
|
||||
<p style="font-size:0.85rem;color:var(--clr-text-muted)">📞 (0561) 8181771</p>
|
||||
<p style="font-size:0.85rem;color:var(--clr-primary-l)">🌐 www.pontianak.go.id</p>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
$_mapLat = MAP_DEFAULT_LAT;
|
||||
$_mapLng = MAP_DEFAULT_LNG;
|
||||
$_appUrl = APP_URL;
|
||||
$extraJs = <<<JS
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
LocationPicker.init('lapor-map','lat','lng', {$_mapLat}, {$_mapLng});
|
||||
|
||||
// Load kecamatan → kelurahan
|
||||
document.getElementById('kecamatan').addEventListener('change', async function() {
|
||||
const id = this.value;
|
||||
const sel = document.getElementById('id_kelurahan');
|
||||
sel.innerHTML = '<option value="">— Memuat... —</option>';
|
||||
if (!id) { sel.innerHTML = '<option value="">— Pilih Kelurahan —</option>'; return; }
|
||||
const data = await App.fetchJSON('{$_appUrl}/api/kelurahan/' + id);
|
||||
sel.innerHTML = '<option value="">— Pilih Kelurahan —</option>';
|
||||
if (data) data.forEach(k => {
|
||||
sel.innerHTML += '<option value="' + k.id + '">' + k.nama + '</option>';
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
JS;
|
||||
?>
|
||||
|
||||
<!-- Hero kecil -->
|
||||
<div style="background:linear-gradient(135deg,var(--clr-primary-d),var(--clr-primary));color:#fff;padding:40px 32px;text-align:center">
|
||||
<h1 style="font-size:1.8rem;font-weight:800">📝 Lapor Cepat</h1>
|
||||
<p style="color:rgba(255,255,255,0.75);margin-top:6px">Laporkan warga yang membutuhkan bantuan — gratis, mudah, tanpa login</p>
|
||||
</div>
|
||||
|
||||
<div class="public-container" style="padding-top:36px;padding-bottom:60px">
|
||||
<?= flashMessage() ?>
|
||||
|
||||
<?php
|
||||
// Kode laporan dari flash — ditampilkan sebagai card permanen (tidak auto-dismiss)
|
||||
$kodeLaporan = $GLOBALS['_flash_kode_laporan'] ?? null;
|
||||
?>
|
||||
<?php if ($kodeLaporan): ?>
|
||||
<div style="background:#f0fdf4;border:2px solid #22c55e;border-radius:12px;padding:28px;margin-bottom:28px;text-align:center">
|
||||
<div style="font-size:1rem;color:#15803d;font-weight:600;margin-bottom:10px">✅ Laporan Berhasil Dikirim!</div>
|
||||
<div style="font-size:0.85rem;color:#166534;margin-bottom:14px">Simpan kode berikut untuk memantau status laporan Anda</div>
|
||||
<div style="display:inline-block;background:#fff;border:2px dashed #22c55e;border-radius:10px;padding:14px 36px;margin-bottom:16px">
|
||||
<div style="font-size:0.75rem;color:#6b7280;margin-bottom:4px;letter-spacing:1px;text-transform:uppercase">Kode Laporan</div>
|
||||
<div style="font-size:2rem;font-weight:900;letter-spacing:6px;color:#15803d;font-family:monospace"><?= e($kodeLaporan) ?></div>
|
||||
</div>
|
||||
<br>
|
||||
<button type="button"
|
||||
onclick="navigator.clipboard.writeText('<?= e($kodeLaporan) ?>').then(()=>{this.textContent='✅ Tersalin!';this.style.background='#15803d';setTimeout(()=>{this.textContent='📋 Salin Kode';this.style.background='';},2000);})"
|
||||
class="btn btn-primary btn-sm" style="margin-right:8px">📋 Salin Kode</button>
|
||||
<a href="<?= APP_URL ?>/cek-laporan?kode=<?= urlencode($kodeLaporan) ?>" class="btn btn-secondary btn-sm">🔍 Cek Status</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 380px;gap:28px">
|
||||
<!-- FORM -->
|
||||
<div>
|
||||
<form method="POST" action="<?= APP_URL ?>/lapor" enctype="multipart/form-data">
|
||||
<?= csrfField() ?>
|
||||
|
||||
<div class="card" style="margin-bottom:20px">
|
||||
<div class="card-header"><span class="card-icon">👤</span><h3>Data Pelapor</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nama Pelapor <span class="required">*</span></label>
|
||||
<input type="text" name="nama_pelapor" class="form-control" required placeholder="Nama lengkap Anda">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">No. HP (WhatsApp)</label>
|
||||
<input type="text" name="no_hp_pelapor" class="form-control" placeholder="08xxxxxxxxxx">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom:20px">
|
||||
<div class="card-header"><span class="card-icon">🏠</span><h3>Data Warga yang Dilaporkan</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nama Warga <span class="required">*</span></label>
|
||||
<input type="text" name="nama_warga" class="form-control" required placeholder="Nama lengkap warga yang membutuhkan bantuan">
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kecamatan <span class="required">*</span></label>
|
||||
<select id="kecamatan" class="form-control" required>
|
||||
<option value="">— Pilih Kecamatan —</option>
|
||||
<?php foreach($kecamatan as $k): ?>
|
||||
<option value="<?= $k['id'] ?>"><?= e($k['nama']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kelurahan <span class="required">*</span></label>
|
||||
<select name="id_kelurahan" id="id_kelurahan" class="form-control" required>
|
||||
<option value="">— Pilih Kelurahan —</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Alamat Lengkap <span class="required">*</span></label>
|
||||
<textarea name="alamat_warga" class="form-control" rows="2" required placeholder="Nama jalan, nomor rumah, RT/RW"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Deskripsi Kondisi <span class="required">*</span></label>
|
||||
<textarea name="deskripsi" class="form-control" rows="3" required placeholder="Ceritakan kondisi warga, mengapa membutuhkan bantuan, kondisi rumah, dll."></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Foto Kondisi (Opsional)</label>
|
||||
<input type="file" name="foto" class="form-control" accept="image/*">
|
||||
<div class="form-hint">Foto rumah atau kondisi warga membantu proses verifikasi</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom:20px">
|
||||
<div class="card-header"><span class="card-icon">📍</span><h3>Lokasi Rumah Warga</h3></div>
|
||||
<div class="card-body">
|
||||
<div id="lapor-map" style="height:300px;border-radius:8px;border:1px solid var(--clr-border);margin-bottom:12px"></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Latitude <span class="required">*</span></label>
|
||||
<input type="number" name="lat" id="lat" class="form-control" step="0.0000001" required placeholder="-0.0263">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Longitude <span class="required">*</span></label>
|
||||
<input type="number" name="lng" id="lng" class="form-control" step="0.0000001" required placeholder="109.3425">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-hint">📌 Klik pada peta atau drag marker untuk menentukan lokasi rumah warga secara tepat</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-full" style="padding:13px;font-size:1rem;justify-content:center">
|
||||
📤 Kirim Laporan
|
||||
</button>
|
||||
<div style="margin-top:10px;text-align:center;font-size:0.8rem;color:var(--clr-text-muted)">
|
||||
Laporan akan diverifikasi oleh petugas. Simpan kode laporan untuk memantau status.
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- SIDEBAR INFO -->
|
||||
<div>
|
||||
<div class="card" style="position:sticky;top:80px">
|
||||
<div class="card-header"><span class="card-icon">ℹ️</span><h3>Informasi Penting</h3></div>
|
||||
<div class="card-body" style="display:flex;flex-direction:column;gap:16px">
|
||||
<div style="padding:14px;background:#f0fdf4;border-radius:8px;border:1px solid #bbf7d0">
|
||||
<div style="font-weight:700;color:var(--clr-success);margin-bottom:6px">✅ Apa yang terjadi setelah laporan?</div>
|
||||
<ol style="margin:0;padding-left:18px;font-size:0.82rem;color:#166534;line-height:2">
|
||||
<li>Laporan diterima dan diberi kode unik</li>
|
||||
<li>Verifikator kelurahan melakukan survei</li>
|
||||
<li>Data masuk sistem pemetaan GIS</li>
|
||||
<li>Rumah ibadah terdekat mendapat notifikasi</li>
|
||||
<li>Bantuan disalurkan sesuai prioritas</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div style="padding:14px;background:#eff6ff;border-radius:8px;border:1px solid #bfdbfe">
|
||||
<div style="font-weight:700;color:var(--clr-info);margin-bottom:6px">📱 Cek Status Laporan</div>
|
||||
<div style="font-size:0.82rem;color:#1e40af">Simpan kode laporan yang diberikan setelah mengirim untuk memantau status verifikasi.</div>
|
||||
<a href="<?= APP_URL ?>/cek-laporan" class="btn btn-primary btn-sm" style="margin-top:10px">🔍 Cek Status</a>
|
||||
</div>
|
||||
|
||||
<div style="padding:14px;background:#fef9c3;border-radius:8px;border:1px solid #fde047">
|
||||
<div style="font-weight:700;color:#a16207;margin-bottom:6px">⚠️ Peringatan</div>
|
||||
<div style="font-size:0.82rem;color:#854d0e">Laporan yang tidak akurat atau laporan palsu akan ditolak. Isi data dengan benar dan jujur.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
<!-- TENTANG SISTEM -->
|
||||
<div style="background:linear-gradient(135deg,var(--clr-primary-d),var(--clr-primary));color:#fff;padding:60px 32px;text-align:center">
|
||||
<h1 style="font-size:2rem;font-weight:900">Tentang SINERGI</h1>
|
||||
<p style="color:rgba(255,255,255,0.75);max-width:600px;margin:12px auto 0">Sistem Informasi Geografis Penanganan Kemiskinan Berbasis Kolaborasi</p>
|
||||
</div>
|
||||
<div class="public-container" style="padding:48px 24px">
|
||||
<div style="max-width:800px;margin:0 auto">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-bottom:40px">
|
||||
<div class="card"><div class="card-body">
|
||||
<div style="font-size:2rem;margin-bottom:12px">🎯</div>
|
||||
<h3 style="font-weight:700;margin-bottom:8px">Tujuan Sistem</h3>
|
||||
<p style="color:var(--clr-text-muted);font-size:0.875rem;line-height:1.7">SINERGI dirancang untuk memetakan, memverifikasi, dan menangani kemiskinan di Kota Pontianak melalui kolaborasi antara rumah ibadah, masyarakat, dan pemerintah.</p>
|
||||
</div></div>
|
||||
<div class="card"><div class="card-body">
|
||||
<div style="font-size:2rem;margin-bottom:12px">⛪</div>
|
||||
<h3 style="font-weight:700;margin-bottom:8px">Kolaborasi Rumah Ibadah</h3>
|
||||
<p style="color:var(--clr-text-muted);font-size:0.875rem;line-height:1.7">Setiap warga miskin yang terverifikasi akan ditugaskan ke rumah ibadah terdekat menggunakan Haversine Formula untuk memastikan penyaluran bantuan tepat sasaran.</p>
|
||||
</div></div>
|
||||
</div>
|
||||
<div class="card"><div class="card-body">
|
||||
<h3 style="font-weight:700;margin-bottom:16px">Fitur Unggulan Sistem</h3>
|
||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px">
|
||||
<?php $fitur = [
|
||||
['🗺','Peta GIS Real-time','Visualisasi persebaran kemiskinan secara real-time menggunakan Leaflet.js'],
|
||||
['🔥','Heatmap Kemiskinan','Identifikasi daerah dengan konsentrasi kemiskinan tertinggi'],
|
||||
['⚡','Blind Spot Detection','Deteksi otomatis warga yang tidak terjangkau radius rumah ibadah'],
|
||||
['📊','Skor Prioritas','Algoritma penilaian prioritas bantuan berdasarkan 7 indikator'],
|
||||
['📱','Lapor Tanpa Login','Masyarakat dapat melaporkan warga tanpa perlu mendaftar'],
|
||||
['🏛','Dashboard Eksekutif','Monitoring real-time untuk Wali Kota dan Dinas Sosial'],
|
||||
]; foreach($fitur as [$icon,$title,$desc]): ?>
|
||||
<div style="padding:16px;background:#f8fafc;border-radius:8px;border:1px solid var(--clr-border)">
|
||||
<div style="font-size:1.5rem;margin-bottom:8px"><?= $icon ?></div>
|
||||
<div style="font-weight:700;font-size:0.875rem;margin-bottom:4px"><?= $title ?></div>
|
||||
<div style="font-size:0.78rem;color:var(--clr-text-muted);line-height:1.5"><?= $desc ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,46 @@
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
||||
<div>
|
||||
<h2 style="font-size:1.2rem;font-weight:700">📦 Histori Bantuan</h2>
|
||||
<p class="text-muted text-sm"><?= e($ri['nama']) ?> · <?= count($data) ?> catatan penyaluran</p>
|
||||
</div>
|
||||
<a href="<?= APP_URL ?>/rumah-ibadah/bantuan/tambah" class="btn btn-success">➕ Catat Bantuan Baru</a>
|
||||
</div>
|
||||
|
||||
<div class="filters-bar">
|
||||
<input type="text" id="search-b" class="form-control" placeholder="🔍 Cari nama warga, kategori..." style="max-width:280px">
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body no-pad">
|
||||
<div class="table-wrap">
|
||||
<table id="tbl-b">
|
||||
<thead>
|
||||
<tr><th>Kode</th><th>Warga</th><th>Kategori</th><th>Jumlah</th><th>Tanggal</th><th>Status</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach($data as $b): ?>
|
||||
<tr>
|
||||
<td><code style="font-size:0.72rem;background:#f1f5f9;padding:1px 5px;border-radius:3px"><?= e($b['kode_bantuan']) ?></code></td>
|
||||
<td style="font-weight:600"><?= e($b['warga']) ?></td>
|
||||
<td><span class="badge badge-green"><?= e($b['kategori']) ?></span></td>
|
||||
<td class="text-sm">
|
||||
<?= $b['jumlah'] ? ($b['satuan']==='rupiah' ? formatRupiah($b['jumlah']) : number_format($b['jumlah']).' '.$b['satuan']) : '—' ?>
|
||||
</td>
|
||||
<td class="text-sm"><?= formatTanggal($b['tanggal_penyaluran']) ?></td>
|
||||
<td><span class="badge badge-green">✅ <?= ucfirst($b['status']) ?></span></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if(empty($data)): ?>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📭</div>
|
||||
<h3>Belum ada penyaluran</h3>
|
||||
<p>Mulai catat penyaluran bantuan pertama Anda</p>
|
||||
<a href="<?= APP_URL ?>/rumah-ibadah/bantuan/tambah" class="btn btn-success" style="margin-top:12px">➕ Catat Bantuan</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<script>document.addEventListener('DOMContentLoaded',()=>App.initTableSearch('search-b','tbl-b'));</script>
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php ob_start(); ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const ri = <?= json_encode(['lat'=>(float)$ri['lat'],'lng'=>(float)$ri['lng'],'nama'=>$ri['nama'],'radius_meter'=>(int)$ri['radius_meter']]) ?>;
|
||||
|
||||
// Fokus ke lokasi rumah ibadah
|
||||
const map = GISMap.init('ri-dashboard-map', ri.lat, ri.lng, 15);
|
||||
|
||||
// Marker rumah ibadah
|
||||
L.circle([ri.lat, ri.lng], {
|
||||
radius: ri.radius_meter, fillColor:'#16a34a', fillOpacity:0.08,
|
||||
color:'#16a34a', weight:2, dashArray:'6,4'
|
||||
}).addTo(map).bindPopup('<strong>Radius Pelayanan: '+ri.radius_meter+'m</strong>');
|
||||
|
||||
GISMap.createRIMarker(ri).bindPopup('<strong>'+ri.nama+'</strong>').addTo(map).openPopup();
|
||||
|
||||
// Warga binaan
|
||||
const wargaData = await App.fetchJSON('<?= APP_URL ?>/rumah-ibadah/api/peta');
|
||||
if (wargaData?.warga && wargaData.warga.length > 0) {
|
||||
GISMap.addWargaLayer(wargaData.warga, w => {
|
||||
window.location.href = '<?= APP_URL ?>/rumah-ibadah/warga/' + w.id;
|
||||
});
|
||||
// Zoom ke area RI + warga binaan (sudah fokus ke RI dari init)
|
||||
// Tambahkan koordinat RI ke bounds agar tetap kelihatan
|
||||
GISMap._allMarkerBounds.push([ri.lat, ri.lng]);
|
||||
GISMap.autoFitBounds();
|
||||
}
|
||||
// Jika tidak ada warga, tetap fokus ke lokasi RI (sudah di-setView dari init)
|
||||
|
||||
// Chart prioritas
|
||||
const pData = <?= json_encode([
|
||||
($stats['prioritas_tinggi']??0),
|
||||
($stats['prioritas_sedang']??0),
|
||||
(($stats['total_warga']??0)-($stats['prioritas_tinggi']??0)-($stats['prioritas_sedang']??0))
|
||||
]) ?>;
|
||||
Charts.donut('chart-prioritas',
|
||||
['Prioritas Tinggi','Prioritas Sedang','Prioritas Rendah'],
|
||||
pData, ['#dc2626','#d97706','#16a34a']
|
||||
);
|
||||
});
|
||||
</script>
|
||||
<?php $extraJs = ob_get_clean(); ?>
|
||||
|
||||
<!-- Header RI Info -->
|
||||
<div style="background:var(--clr-primary);color:#fff;border-radius:var(--radius-md);padding:20px 24px;margin-bottom:20px;display:flex;align-items:center;gap:20px">
|
||||
<div style="font-size:3rem">⛪</div>
|
||||
<div>
|
||||
<div style="font-size:1.2rem;font-weight:800"><?= e($ri['nama']) ?></div>
|
||||
<div style="font-size:0.85rem;color:rgba(255,255,255,0.7)"><?= e($ri['jenis']??'') ?> · <?= e($ri['kelurahan']??'') ?>, <?= e($ri['kecamatan']??'') ?></div>
|
||||
<div style="font-size:0.8rem;color:rgba(255,255,255,0.6);margin-top:4px">📍 <?= e($ri['alamat']??'') ?> | Radius: <?= number_format($ri['radius_meter']??500) ?>m</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon blue">👥</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format($stats['total_warga']??0) ?></div>
|
||||
<div class="stat-card-label">Total Warga Binaan</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($stats['prioritas_tinggi']??0) ?></div>
|
||||
<div class="stat-card-label">Prioritas Tinggi</div>
|
||||
<div class="stat-card-sub"><a href="<?= APP_URL ?>/rumah-ibadah/prioritas" style="color:var(--clr-primary-l)">Lihat →</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon orange">⚠</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format($stats['ekstrem']??0) ?></div>
|
||||
<div class="stat-card-label">Miskin Ekstrem</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon green">📦</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format($stats['total_bantuan']??0) ?></div>
|
||||
<div class="stat-card-label">Total Bantuan Disalurkan</div>
|
||||
<div class="stat-card-sub"><?= number_format($stats['bantuan_bulan_ini']??0) ?> bulan ini</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Grid: Peta + Sidebar -->
|
||||
<div class="dashboard-grid" style="margin-bottom:20px">
|
||||
<!-- PETA: fokus ke rumah ibadah + radius + warga -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">🗺</span>
|
||||
<h3>Peta Wilayah Pelayanan & Warga Binaan</h3>
|
||||
<div class="card-actions">
|
||||
<a href="<?= APP_URL ?>/rumah-ibadah/bantuan/tambah" class="btn btn-accent btn-sm">➕ Catat Bantuan</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body no-pad" style="position:relative">
|
||||
<div id="ri-dashboard-map" style="height:460px"></div>
|
||||
<div class="map-legend">
|
||||
<h4>Legenda</h4>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-ri"></div> Rumah Ibadah Anda</div>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-ekstrem"></div> Miskin Ekstrem</div>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-miskin"></div> Miskin</div>
|
||||
<div class="map-legend-item"><div class="map-legend-dot legend-dot-rentan"></div> Rentan Miskin</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SIDEBAR -->
|
||||
<div style="display:flex;flex-direction:column;gap:16px">
|
||||
<!-- Chart Prioritas -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📊</span><h3>Distribusi Prioritas</h3></div>
|
||||
<div class="card-body"><div class="chart-wrap" style="height:180px"><canvas id="chart-prioritas"></canvas></div></div>
|
||||
</div>
|
||||
|
||||
<!-- Warga Prioritas Tinggi -->
|
||||
<div class="card" style="flex:1">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">🔥</span><h3>Warga Prioritas Tinggi</h3>
|
||||
<div class="card-actions"><a href="<?= APP_URL ?>/rumah-ibadah/prioritas" class="btn btn-ghost btn-sm">Semua</a></div>
|
||||
</div>
|
||||
<div class="card-body no-pad" style="max-height:280px;overflow-y:auto">
|
||||
<?php
|
||||
$topWarga = array_filter($wargaList, fn($w) => $w['level_prioritas']==='Tinggi');
|
||||
foreach(array_slice($topWarga,0,8) as $w):
|
||||
?>
|
||||
<a href="<?= APP_URL ?>/rumah-ibadah/warga/<?= $w['id'] ?>" style="display:block;text-decoration:none;color:inherit">
|
||||
<div class="sidebar-panel-item">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<h4 style="font-size:0.82rem"><?= e($w['nama']) ?></h4>
|
||||
<span style="font-size:0.78rem;font-weight:700;color:var(--clr-danger)"><?= $w['skor_prioritas'] ?>/100</span>
|
||||
</div>
|
||||
<p><?= e($w['status_kemiskinan']) ?> · Tgn: <?= $w['jumlah_tanggungan'] ?></p>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<?php if(empty($topWarga)): ?>
|
||||
<div class="empty-state" style="padding:20px"><p>Tidak ada warga prioritas tinggi</p></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bantuan Terakhir -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">📦</span><h3>Bantuan Terakhir Disalurkan</h3>
|
||||
<div class="card-actions"><a href="<?= APP_URL ?>/rumah-ibadah/bantuan" class="btn btn-ghost btn-sm">Histori Lengkap</a></div>
|
||||
</div>
|
||||
<div class="card-body no-pad">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Kode</th><th>Warga</th><th>Kategori</th><th>Jumlah</th><th>Tanggal</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($bantuanTerakhir as $b): ?>
|
||||
<tr>
|
||||
<td><code style="font-size:0.72rem;background:#f1f5f9;padding:1px 5px;border-radius:3px"><?= e($b['kode_bantuan']) ?></code></td>
|
||||
<td style="font-weight:600;font-size:0.875rem"><?= e($b['warga']) ?></td>
|
||||
<td><span class="badge badge-green"><?= e($b['kategori']) ?></span></td>
|
||||
<td class="text-sm"><?= $b['jumlah'] ? ($b['satuan']==='rupiah'?formatRupiah($b['jumlah']):(number_format($b['jumlah']).' '.$b['satuan'])) : '—' ?></td>
|
||||
<td class="text-sm text-muted"><?= formatTanggal($b['tanggal_penyaluran']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if(empty($bantuanTerakhir)): ?>
|
||||
<div class="empty-state" style="padding:24px"><div class="empty-state-icon">📭</div><p>Belum ada bantuan disalurkan</p></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php ob_start(); ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const lat = <?= (float)$warga['lat'] ?>;
|
||||
const lng = <?= (float)$warga['lng'] ?>;
|
||||
if(lat && lng) {
|
||||
const map = L.map('ri-warga-map').setView([lat,lng],16);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19}).addTo(map);
|
||||
const color = '<?= warnaMiskin($warga['status_kemiskinan']) ?>';
|
||||
L.circleMarker([lat,lng],{radius:12,fillColor:color,color:'#fff',weight:3,fillOpacity:0.9})
|
||||
.bindPopup('<strong><?= addslashes($warga['nama']) ?></strong>').addTo(map).openPopup();
|
||||
// Rumah ibadah marker
|
||||
const ri = <?= json_encode(['lat'=>(float)$ri['lat'],'lng'=>(float)$ri['lng'],'nama'=>$ri['nama'],'radius_meter'=>(int)$ri['radius_meter']]) ?>;
|
||||
GISMap.map = map;
|
||||
GISMap.createRIMarker(ri).bindPopup(ri.nama).addTo(map);
|
||||
L.circle([ri.lat,ri.lng],{radius:ri.radius_meter,fillColor:'#16a34a',fillOpacity:0.06,color:'#16a34a',weight:1.5,dashArray:'5,4'}).addTo(map);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php $extraJs = ob_get_clean(); ?>
|
||||
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px">
|
||||
<a href="<?= APP_URL ?>/rumah-ibadah/warga" class="btn btn-ghost btn-sm">← Kembali</a>
|
||||
<h2 style="font-size:1.1rem;font-weight:700"><?= e($warga['nama']) ?></h2>
|
||||
<?php $sc=['Miskin Ekstrem'=>'badge-red','Miskin'=>'badge-orange','Rentan Miskin'=>'badge-yellow']; ?>
|
||||
<span class="badge <?= $sc[$warga['status_kemiskinan']]??'badge-gray' ?>"><?= e($warga['status_kemiskinan']) ?></span>
|
||||
<div style="margin-left:auto">
|
||||
<a href="<?= APP_URL ?>/rumah-ibadah/bantuan/tambah" class="btn btn-success btn-sm">📦 Catat Bantuan</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 320px;gap:20px">
|
||||
<div style="display:flex;flex-direction:column;gap:16px">
|
||||
<!-- Skor -->
|
||||
<div class="card" style="border-left:4px solid <?= ['Tinggi'=>'#dc2626','Sedang'=>'#d97706','Rendah'=>'#16a34a'][$warga['level_prioritas']]??'#6b7280' ?>">
|
||||
<div class="card-body">
|
||||
<div style="display:flex;align-items:center;gap:20px">
|
||||
<div style="text-align:center;min-width:80px">
|
||||
<div style="font-size:2.5rem;font-weight:900;color:<?= ['Tinggi'=>'#dc2626','Sedang'=>'#d97706','Rendah'=>'#16a34a'][$warga['level_prioritas']]??'#6b7280' ?>"><?= $warga['skor_prioritas'] ?></div>
|
||||
<div class="text-xs text-muted">Skor Prioritas</div>
|
||||
</div>
|
||||
<div style="flex:1">
|
||||
<div class="priority-bar" style="height:10px;margin-bottom:8px">
|
||||
<div class="priority-bar-fill <?= strtolower($warga['level_prioritas'])==='tinggi'?'high':(strtolower($warga['level_prioritas'])==='sedang'?'medium':'low') ?>"
|
||||
style="width:<?= min(100,$warga['skor_prioritas']) ?>%"></div>
|
||||
</div>
|
||||
<div style="font-weight:700">Prioritas <?= e($warga['level_prioritas']) ?></div>
|
||||
<div class="text-sm text-muted"><?= e($warga['status_kemiskinan']) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Pribadi -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">👤</span><h3>Data Pribadi</h3></div>
|
||||
<div class="card-body">
|
||||
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:14px">
|
||||
<?php $items = [
|
||||
['Nama Lengkap',$warga['nama']],
|
||||
['NIK',$warga['nik']??'—'],
|
||||
['Kelurahan',$warga['kelurahan']??'—'],
|
||||
['Kecamatan',$warga['kecamatan']??'—'],
|
||||
['Pendidikan',$warga['pendidikan_terakhir']??'—'],
|
||||
['Pekerjaan',$warga['pekerjaan']??'—'],
|
||||
['Penghasilan',formatRupiah($warga['penghasilan_bulanan']??0).'/bln'],
|
||||
['Tanggungan',($warga['jumlah_tanggungan']??0).' orang'],
|
||||
['Kondisi Rumah',$warga['kondisi_rumah']??'—'],
|
||||
['Riwayat Penyakit',$warga['riwayat_penyakit']??'—'],
|
||||
]; foreach($items as [$l,$v]): ?>
|
||||
<div>
|
||||
<div class="text-xs text-muted"><?= $l ?></div>
|
||||
<div style="font-weight:500;font-size:0.875rem;margin-top:2px"><?= e($v) ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap">
|
||||
<?php if($warga['is_lansia']): ?><span class="badge badge-orange">👴 Lansia</span><?php endif; ?>
|
||||
<?php if($warga['is_disabilitas']): ?><span class="badge badge-blue">♿ Disabilitas</span><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Histori Bantuan -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📦</span><h3>Histori Bantuan Diterima (<?= count($bantuan) ?>)</h3></div>
|
||||
<div class="card-body no-pad">
|
||||
<?php if($bantuan): ?>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Tanggal</th><th>Kategori</th><th>Jumlah</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($bantuan as $b): ?>
|
||||
<tr>
|
||||
<td class="text-sm"><?= formatTanggal($b['tanggal_penyaluran']) ?></td>
|
||||
<td><span class="badge badge-green"><?= e($b['kategori']) ?></span></td>
|
||||
<td class="text-sm"><?= $b['jumlah'] ? ($b['satuan']==='rupiah'?formatRupiah($b['jumlah']):(number_format($b['jumlah']).' '.$b['satuan'])) : '—' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="empty-state" style="padding:20px"><p>Belum menerima bantuan</p></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KANAN: Peta -->
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📍</span><h3>Lokasi Warga</h3></div>
|
||||
<div class="card-body no-pad">
|
||||
<div id="ri-warga-map" style="height:300px"></div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-xs text-muted">Koordinat</div>
|
||||
<div style="font-family:monospace;font-size:0.78rem"><?= $warga['lat'] ?>, <?= $warga['lng'] ?></div>
|
||||
<?php if($warga['alamat']): ?>
|
||||
<div class="text-xs text-muted" style="margin-top:8px">Alamat</div>
|
||||
<div style="font-size:0.8rem"><?= e($warga['alamat']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,133 @@
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px">
|
||||
<a href="<?= APP_URL ?>/rumah-ibadah/bantuan" class="btn btn-ghost btn-sm">← Kembali</a>
|
||||
<h2 style="font-size:1.1rem;font-weight:700">Catat Penyaluran Bantuan</h2>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 340px;gap:20px">
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📦</span><h3>Form Penyaluran Bantuan</h3></div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="<?= APP_URL ?>/rumah-ibadah/bantuan/store" enctype="multipart/form-data">
|
||||
<?= csrfField() ?>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Warga Penerima <span class="required">*</span></label>
|
||||
<select name="id_warga" id="sel-warga" class="form-control" required onchange="showWargaInfo(this)">
|
||||
<option value="">— Pilih Warga —</option>
|
||||
<?php foreach($wargaList as $w): ?>
|
||||
<option value="<?= $w['id'] ?>"
|
||||
data-status="<?= e($w['status_kemiskinan']) ?>"
|
||||
data-level="<?= e($w['level_prioritas']) ?>">
|
||||
<?= e($w['nama']) ?> [<?= e($w['level_prioritas']) ?>]
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Info warga terpilih -->
|
||||
<div id="warga-info" style="display:none;margin-bottom:16px;padding:12px;background:#eff6ff;border-radius:8px;border:1px solid #bfdbfe">
|
||||
<div style="font-size:0.8rem;font-weight:600;color:var(--clr-primary)">ℹ️ Info Warga Terpilih</div>
|
||||
<div style="font-size:0.82rem;margin-top:4px">
|
||||
Status: <span id="info-status" style="font-weight:700"></span> ·
|
||||
Prioritas: <span id="info-level" style="font-weight:700"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kategori Bantuan <span class="required">*</span></label>
|
||||
<select name="id_kategori_bantuan" class="form-control" required onchange="setSatuan(this)">
|
||||
<option value="">— Pilih Kategori —</option>
|
||||
<?php foreach($kategori as $k): ?>
|
||||
<option value="<?= $k['id'] ?>" data-satuan="<?= e($k['satuan']??'paket') ?>"><?= e($k['nama']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tanggal Penyaluran <span class="required">*</span></label>
|
||||
<input type="date" name="tanggal_penyaluran" class="form-control" value="<?= date('Y-m-d') ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Jumlah <span class="required">*</span></label>
|
||||
<input type="number" name="jumlah" id="inp-jumlah" class="form-control" value="1" min="0.01" step="0.01" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Satuan</label>
|
||||
<input type="text" name="satuan" id="inp-satuan" class="form-control" value="paket" placeholder="paket/kg/rupiah">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Deskripsi Bantuan</label>
|
||||
<textarea name="deskripsi" class="form-control" rows="3" placeholder="Contoh: Sembako bulanan terdiri dari beras 5kg, minyak, gula..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Foto Bukti Penyaluran</label>
|
||||
<input type="file" name="foto_bukti" class="form-control" accept="image/*">
|
||||
<div class="form-hint">Upload foto saat penyerahan bantuan sebagai bukti dokumentasi</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:flex-end;gap:12px;margin-top:8px">
|
||||
<a href="<?= APP_URL ?>/rumah-ibadah/bantuan" class="btn btn-ghost">Batal</a>
|
||||
<button type="submit" class="btn btn-success" style="padding:10px 24px">✅ Simpan Penyaluran</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SIDEBAR: Daftar warga prioritas -->
|
||||
<div>
|
||||
<div class="card" style="position:sticky;top:80px">
|
||||
<div class="card-header"><span class="card-icon">🔥</span><h3>Warga Prioritas Tinggi</h3></div>
|
||||
<div class="card-body no-pad" style="max-height:500px;overflow-y:auto">
|
||||
<?php
|
||||
$topWarga = array_filter($wargaList, fn($w) => $w['level_prioritas']==='Tinggi');
|
||||
foreach($topWarga as $w):
|
||||
?>
|
||||
<div class="sidebar-panel-item" onclick="document.getElementById('sel-warga').value='<?= $w['id'] ?>';showWargaInfo(document.getElementById('sel-warga'))" style="cursor:pointer">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<h4><?= e($w['nama']) ?></h4>
|
||||
<span style="font-weight:700;font-size:0.78rem;color:var(--clr-danger)"><?= isset($w['skor_prioritas']) ? $w['skor_prioritas'] . '/100' : 'N/A' ?></span>
|
||||
</div>
|
||||
<p><?= e($w['status_kemiskinan']) ?></p>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php if(empty($topWarga)): ?>
|
||||
<div class="empty-state" style="padding:20px"><p>Tidak ada prioritas tinggi</p></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showWargaInfo(sel) {
|
||||
const opt = sel.options[sel.selectedIndex];
|
||||
const info = document.getElementById('warga-info');
|
||||
if(sel.value) {
|
||||
info.style.display='block';
|
||||
document.getElementById('info-status').textContent = opt.dataset.status||'—';
|
||||
document.getElementById('info-level').textContent = opt.dataset.level||'—';
|
||||
} else {
|
||||
info.style.display='none';
|
||||
}
|
||||
}
|
||||
function setSatuan(sel) {
|
||||
const opt = sel.options[sel.selectedIndex];
|
||||
const satuan = opt.dataset.satuan||'paket';
|
||||
document.getElementById('inp-satuan').value = satuan;
|
||||
if(satuan==='rupiah'){
|
||||
document.getElementById('inp-jumlah').step='1000';
|
||||
document.getElementById('inp-jumlah').placeholder='mis: 300000';
|
||||
} else {
|
||||
document.getElementById('inp-jumlah').step='0.01';
|
||||
document.getElementById('inp-jumlah').placeholder='';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
<div style="margin-bottom:20px">
|
||||
<h2 style="font-size:1.2rem;font-weight:700">🔥 Daftar Prioritas Bantuan</h2>
|
||||
<p class="text-muted text-sm"><?= e($ri['nama']) ?> · Diurutkan berdasarkan skor prioritas tertinggi</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body no-pad">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Rank</th><th>Nama</th><th>Skor</th><th>Status</th><th>Penghasilan</th><th>Tanggungan</th><th>Bantuan Terakhir</th><th>Total</th><th>Aksi</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($data as $i=>$w): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="width:28px;height:28px;border-radius:50%;background:<?= $i<3?['#dc2626','#d97706','#0284c7'][$i]:'#e2e8f0' ?>;color:<?= $i<3?'#fff':'var(--clr-text)' ?>;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:0.8rem"><?= $i+1 ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="font-weight:600"><?= e($w['nama']) ?></div>
|
||||
<?php if($w['is_lansia']): ?><span class="badge badge-orange" style="font-size:0.62rem">Lansia</span><?php endif; ?>
|
||||
<?php if($w['is_disabilitas']): ?><span class="badge badge-blue" style="font-size:0.62rem">Disabilitas</span><?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<div style="font-size:1.2rem;font-weight:800;color:<?= ['Tinggi'=>'#dc2626','Sedang'=>'#d97706','Rendah'=>'#16a34a'][$w['level_prioritas']]??'#6b7280' ?>"><?= $w['skor_prioritas'] ?></div>
|
||||
<?php $pc=['Tinggi'=>'badge-red','Sedang'=>'badge-yellow','Rendah'=>'badge-green']; ?>
|
||||
<span class="badge <?= $pc[$w['level_prioritas']]??'badge-gray' ?>" style="font-size:0.62rem"><?= $w['level_prioritas'] ?></span>
|
||||
</td>
|
||||
<td><?php $sc=['Miskin Ekstrem'=>'badge-red','Miskin'=>'badge-orange','Rentan Miskin'=>'badge-yellow']; ?>
|
||||
<span class="badge <?= $sc[$w['status_kemiskinan']]??'badge-gray' ?>"><?= e($w['status_kemiskinan']) ?></span></td>
|
||||
<td class="text-sm"><?= formatRupiah($w['penghasilan_bulanan']??0) ?>/bln</td>
|
||||
<td class="text-sm text-center"><?= $w['jumlah_tanggungan'] ?> orang</td>
|
||||
<td class="text-xs text-muted"><?= $w['bantuan_terakhir'] ? formatTanggal($w['bantuan_terakhir']) : '<span style="color:#dc2626;font-weight:600">Belum pernah</span>' ?></td>
|
||||
<td class="text-sm text-center"><?= number_format($w['jml_bantuan']) ?>×</td>
|
||||
<td>
|
||||
<a href="<?= APP_URL ?>/rumah-ibadah/warga/<?= $w['id'] ?>" class="btn btn-ghost btn-sm">Detail</a>
|
||||
<a href="<?= APP_URL ?>/rumah-ibadah/bantuan/tambah" class="btn btn-success btn-sm">📦 Bantu</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if(empty($data)): ?>
|
||||
<div class="empty-state"><div class="empty-state-icon">👥</div><h3>Tidak ada warga binaan</h3></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,62 @@
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
||||
<div>
|
||||
<h2 style="font-size:1.2rem;font-weight:700">👥 Daftar Warga Binaan</h2>
|
||||
<p class="text-muted text-sm"><?= e($ri['nama']) ?> · <?= count($data) ?> warga</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<?php foreach(['semua'=>'Semua','Tinggi'=>'🔥 Prioritas Tinggi','Sedang'=>'⚡ Sedang','Rendah'=>'✅ Rendah'] as $l=>$label): ?>
|
||||
<a href="?level=<?= $l ?>" class="tab-btn <?= $level===$l?'active':'' ?>"><?= $label ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="filters-bar">
|
||||
<input type="text" id="search-w" class="form-control" placeholder="🔍 Cari nama warga..." style="max-width:280px">
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body no-pad">
|
||||
<div class="table-wrap">
|
||||
<table id="tbl-w">
|
||||
<thead>
|
||||
<tr><th>#</th><th>Nama</th><th>Status Kemiskinan</th><th>Prioritas</th><th>Penghasilan</th><th>Tanggungan</th><th>Aksi</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach($data as $i=>$w): ?>
|
||||
<tr>
|
||||
<td class="text-xs text-muted"><?= $i+1 ?></td>
|
||||
<td>
|
||||
<div style="font-weight:600"><?= e($w['nama']) ?></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']; ?>
|
||||
<span class="badge <?= $sc[$w['status_kemiskinan']]??'badge-gray' ?>"><?= e($w['status_kemiskinan']) ?></span>
|
||||
</td>
|
||||
<td>
|
||||
<?php $pc=['Tinggi'=>'badge-red','Sedang'=>'badge-yellow','Rendah'=>'badge-green']; ?>
|
||||
<span class="badge <?= $pc[$w['level_prioritas']]??'badge-gray' ?>"><?= e($w['level_prioritas']) ?></span>
|
||||
<div class="text-xs text-muted"><?= $w['skor_prioritas'] ?>/100</div>
|
||||
</td>
|
||||
<td class="text-sm"><?= formatRupiah($w['penghasilan_bulanan']??0) ?></td>
|
||||
<td class="text-sm text-center"><?= $w['jumlah_tanggungan'] ?> orang</td>
|
||||
<td>
|
||||
<a href="<?= APP_URL ?>/rumah-ibadah/warga/<?= $w['id'] ?>" class="btn btn-ghost btn-sm">Detail</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if(empty($data)): ?>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">👥</div>
|
||||
<h3>Tidak ada warga binaan</h3>
|
||||
<p>Warga binaan akan otomatis ditugaskan berdasarkan radius pelayanan</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>document.addEventListener('DOMContentLoaded',()=>App.initTableSearch('search-w','tbl-w'));</script>
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php ob_start(); ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const map = GISMap.init('verif-map', <?= MAP_DEFAULT_LAT ?>, <?= MAP_DEFAULT_LNG ?>, 14);
|
||||
|
||||
const laporan = <?= json_encode(array_map(fn($l) => [
|
||||
'lat' => (float)$l['lat'], 'lng' => (float)$l['lng'],
|
||||
'nama_warga' => $l['nama_warga'], 'kode_laporan' => $l['kode_laporan'],
|
||||
'status' => $l['status']
|
||||
], array_filter($laporanTerbaru, fn($l) => $l['lat'] && $l['lng']))) ?>;
|
||||
|
||||
laporan.forEach(l => {
|
||||
const color = l.status === 'terverifikasi' ? '#16a34a' : l.status === 'ditolak' ? '#dc2626' : '#0284c7';
|
||||
L.circleMarker([l.lat, l.lng], {radius:9,fillColor:color,color:'#fff',weight:2,fillOpacity:0.9})
|
||||
.bindPopup(`<strong>${l.nama_warga}</strong><br><code>${l.kode_laporan}</code>`)
|
||||
.addTo(map);
|
||||
GISMap._allMarkerBounds.push([l.lat, l.lng]);
|
||||
});
|
||||
GISMap.autoFitBounds(laporan.length === 0);
|
||||
|
||||
const statsData = await App.fetchJSON('<?= APP_URL ?>/verifikator/api/stats');
|
||||
if (statsData?.data) {
|
||||
const d = statsData.data;
|
||||
Charts.donut('chart-status',
|
||||
['Menunggu', 'Diproses', 'Terverifikasi', 'Ditolak'],
|
||||
[d.menunggu, d.diproses, d.terverifikasi, d.ditolak],
|
||||
['#d97706','#0284c7','#16a34a','#dc2626']
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php $extraJs = ob_get_clean(); ?>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon orange">📋</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format($stats['laporan_menunggu'] ?? 0) ?></div>
|
||||
<div class="stat-card-label">Laporan Menunggu</div>
|
||||
<div class="stat-card-sub"><a href="<?= APP_URL ?>/verifikator/laporan?status=menunggu" style="color:var(--clr-primary-l)">Lihat semua →</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon blue">🔄</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format($stats['laporan_diproses'] ?? 0) ?></div>
|
||||
<div class="stat-card-label">Sedang Diproses</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon green">✅</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format($stats['total_warga'] ?? 0) ?></div>
|
||||
<div class="stat-card-label">Warga Terverifikasi</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($stats['prioritas_tinggi'] ?? 0) ?></div>
|
||||
<div class="stat-card-label">Prioritas Tinggi</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon purple">⚠️</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format($stats['blind_spot'] ?? 0) ?></div>
|
||||
<div class="stat-card-label">Blind Spot</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid: Peta + Chart -->
|
||||
<div class="dashboard-grid" style="margin-bottom:20px">
|
||||
<!-- PETA -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">🗺</span><h3>Peta Laporan Kelurahan Saya</h3></div>
|
||||
<div class="card-body no-pad">
|
||||
<div id="verif-map" style="height:380px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SIDEBAR: Donut + Laporan List -->
|
||||
<div style="display:flex;flex-direction:column;gap:16px">
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📊</span><h3>Status Laporan</h3></div>
|
||||
<div class="card-body"><div class="chart-wrap" style="height:180px"><canvas id="chart-status"></canvas></div></div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="flex:1">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">📋</span><h3>Laporan Terbaru</h3>
|
||||
<div class="card-actions"><a href="<?= APP_URL ?>/verifikator/laporan" class="btn btn-ghost btn-sm">Semua</a></div>
|
||||
</div>
|
||||
<div class="card-body no-pad" style="max-height:300px;overflow-y:auto">
|
||||
<?php foreach ($laporanTerbaru as $l): ?>
|
||||
<a href="<?= APP_URL ?>/verifikator/laporan/<?= $l['id'] ?>" style="display:block;text-decoration:none;color:inherit">
|
||||
<div class="sidebar-panel-item">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<h4><?= e($l['nama_warga']) ?></h4>
|
||||
<?php
|
||||
$badge = match($l['status']) {
|
||||
'menunggu' => 'badge-yellow', 'diproses' => 'badge-blue',
|
||||
'terverifikasi' => 'badge-green', 'ditolak' => 'badge-red', default => 'badge-gray'
|
||||
};
|
||||
$label = match($l['status']) {
|
||||
'menunggu'=>'Menunggu','diproses'=>'Diproses',
|
||||
'terverifikasi'=>'Terverifikasi','ditolak'=>'Ditolak', default=>$l['status']
|
||||
};
|
||||
?>
|
||||
<span class="badge <?= $badge ?>"><?= $label ?></span>
|
||||
</div>
|
||||
<p><?= e($l['kode_laporan']) ?> · <?= formatTanggal($l['created_at']) ?></p>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($laporanTerbaru)): ?>
|
||||
<div class="empty-state" style="padding:24px">
|
||||
<div class="empty-state-icon">📭</div>
|
||||
<p>Belum ada laporan masuk</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">⚡</span><h3>Aksi Cepat</h3></div>
|
||||
<div class="card-body">
|
||||
<div style="display:flex;gap:12px;flex-wrap:wrap">
|
||||
<a href="<?= APP_URL ?>/verifikator/laporan?status=menunggu" class="btn btn-accent">
|
||||
📋 Verifikasi Laporan (<?= $stats['laporan_menunggu'] ?? 0 ?>)
|
||||
</a>
|
||||
<a href="<?= APP_URL ?>/verifikator/warga/tambah" class="btn btn-primary">➕ Tambah Data Warga</a>
|
||||
<a href="<?= APP_URL ?>/verifikator/warga?level=Tinggi" class="btn btn-danger">🔥 Warga Prioritas Tinggi</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php ob_start(); ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const lat = <?= (float)$laporan['lat'] ?>;
|
||||
const lng = <?= (float)$laporan['lng'] ?>;
|
||||
if (lat && lng) {
|
||||
const map = L.map('laporan-map').setView([lat, lng], 16);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19}).addTo(map);
|
||||
L.marker([lat, lng]).bindPopup('<strong><?= addslashes($laporan['nama_warga']) ?></strong>').addTo(map).openPopup();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php $extraJs = ob_get_clean(); ?>
|
||||
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px">
|
||||
<a href="<?= APP_URL ?>/verifikator/laporan" class="btn btn-ghost btn-sm">← Kembali</a>
|
||||
<h2 style="font-size:1.1rem;font-weight:700">Detail Laporan: <?= e($laporan['kode_laporan']) ?></h2>
|
||||
<?php $badge = ['menunggu'=>'badge-yellow','diproses'=>'badge-blue','terverifikasi'=>'badge-green','ditolak'=>'badge-red']; ?>
|
||||
<span class="badge <?= $badge[$laporan['status']] ?? 'badge-gray' ?>" style="font-size:0.8rem"><?= ucfirst($laporan['status']) ?></span>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 380px;gap:20px">
|
||||
<!-- KIRI: Info Detail -->
|
||||
<div style="display:flex;flex-direction:column;gap:16px">
|
||||
|
||||
<!-- Info Warga Dilaporkan -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">👤</span><h3>Data Warga Dilaporkan</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="form-row cols-2">
|
||||
<div>
|
||||
<div class="text-xs text-muted" style="margin-bottom:2px">Nama Warga</div>
|
||||
<div style="font-weight:700;font-size:1.05rem"><?= e($laporan['nama_warga']) ?></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-muted" style="margin-bottom:2px">Wilayah</div>
|
||||
<div style="font-weight:500"><?= e($laporan['kelurahan'] ?? '') ?>, <?= e($laporan['kecamatan'] ?? '') ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="divider">
|
||||
<div class="form-group">
|
||||
<div class="text-xs text-muted">Alamat Lengkap</div>
|
||||
<div style="font-weight:500"><?= e($laporan['alamat_warga']) ?></div>
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div>
|
||||
<div class="text-xs text-muted">Koordinat</div>
|
||||
<div class="text-sm" style="font-family:monospace"><?= $laporan['lat'] ?>, <?= $laporan['lng'] ?></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-muted">Tanggal Laporan</div>
|
||||
<div class="text-sm"><?= formatTanggal($laporan['created_at'], true) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($laporan['deskripsi']): ?>
|
||||
<hr class="divider">
|
||||
<div class="form-group">
|
||||
<div class="text-xs text-muted">Deskripsi Kondisi</div>
|
||||
<div style="font-size:0.875rem;line-height:1.6;background:#f8fafc;padding:12px;border-radius:6px;margin-top:4px"><?= nl2br(e($laporan['deskripsi'])) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Pelapor -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📝</span><h3>Data Pelapor</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="form-row cols-2">
|
||||
<div>
|
||||
<div class="text-xs text-muted">Nama Pelapor</div>
|
||||
<div style="font-weight:600"><?= e($laporan['nama_pelapor']) ?></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-muted">No HP</div>
|
||||
<div><?= e($laporan['no_hp_pelapor'] ?? '—') ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Foto -->
|
||||
<?php if ($laporan['foto']): ?>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📷</span><h3>Foto Bukti</h3></div>
|
||||
<div class="card-body">
|
||||
<img src="<?= uploadUrl($laporan['foto']) ?>" style="max-width:100%;border-radius:8px;border:1px solid var(--clr-border)">
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Catatan Verifikator -->
|
||||
<?php if ($laporan['catatan_verifikator']): ?>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">💬</span><h3>Catatan Verifikator</h3></div>
|
||||
<div class="card-body">
|
||||
<div style="background:#f8fafc;padding:12px;border-radius:6px;font-size:0.875rem"><?= nl2br(e($laporan['catatan_verifikator'])) ?></div>
|
||||
<?php if ($laporan['tanggal_verifikasi']): ?>
|
||||
<div class="text-xs text-muted" style="margin-top:8px">Diverifikasi: <?= formatTanggal($laporan['tanggal_verifikasi'], true) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- KANAN: Peta + Aksi -->
|
||||
<div style="display:flex;flex-direction:column;gap:16px">
|
||||
<!-- PETA LOKASI -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📍</span><h3>Lokasi Warga</h3></div>
|
||||
<div class="card-body no-pad">
|
||||
<div id="laporan-map" style="height:240px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PANEL AKSI VERIFIKASI -->
|
||||
<?php if (in_array($laporan['status'], ['menunggu','diproses'])): ?>
|
||||
<div class="card" style="border:2px solid var(--clr-primary-l)">
|
||||
<div class="card-header" style="background:var(--clr-primary);color:#fff">
|
||||
<span class="card-icon">⚡</span>
|
||||
<h3 style="color:#fff">Tindakan Verifikasi</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<!-- Tandai Diproses -->
|
||||
<?php if ($laporan['status'] === 'menunggu'): ?>
|
||||
<form method="POST" action="<?= APP_URL ?>/verifikator/laporan/<?= $laporan['id'] ?>/proses" style="margin-bottom:12px">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="aksi" value="proses">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Catatan (opsional)</label>
|
||||
<textarea name="catatan" class="form-control" rows="2" placeholder="Catatan survei awal..."></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-full">🔄 Tandai Sedang Diproses</button>
|
||||
</form>
|
||||
<hr class="divider">
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- VERIFIKASI - Buat data warga -->
|
||||
<form method="POST" action="<?= APP_URL ?>/verifikator/laporan/<?= $laporan['id'] ?>/proses" style="margin-bottom:12px">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="aksi" value="verifikasi">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Catatan Verifikasi</label>
|
||||
<textarea name="catatan" class="form-control" rows="2" placeholder="Hasil survei lapangan..."></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-full">✅ Verifikasi & Buat Data Warga</button>
|
||||
<div class="form-hint" style="margin-top:4px">Akan otomatis membuat data warga miskin dan menetapkan rumah ibadah penanggung jawab</div>
|
||||
</form>
|
||||
|
||||
<!-- TOLAK -->
|
||||
<form method="POST" action="<?= APP_URL ?>/verifikator/laporan/<?= $laporan['id'] ?>/proses">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="aksi" value="tolak">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Alasan Penolakan <span class="required">*</span></label>
|
||||
<textarea name="catatan" class="form-control" rows="2" placeholder="Jelaskan alasan penolakan..." required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-danger w-full" onclick="return confirm('Tolak laporan ini?')">❌ Tolak Laporan</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="card">
|
||||
<div class="card-body" style="text-align:center;padding:28px">
|
||||
<?php if ($laporan['status'] === 'terverifikasi'): ?>
|
||||
<div style="font-size:2.5rem">✅</div>
|
||||
<div style="font-weight:700;color:var(--clr-success);margin-top:8px">Laporan Terverifikasi</div>
|
||||
<div class="text-sm text-muted" style="margin-top:4px">Data warga telah ditambahkan ke sistem</div>
|
||||
<a href="<?= APP_URL ?>/verifikator/warga" class="btn btn-success" style="margin-top:12px">Lihat Data Warga</a>
|
||||
<?php else: ?>
|
||||
<div style="font-size:2.5rem">❌</div>
|
||||
<div style="font-weight:700;color:var(--clr-danger);margin-top:8px">Laporan Ditolak</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,277 @@
|
||||
<?php $umur = $warga['tanggal_lahir'] ? hitungUmur($warga['tanggal_lahir']) : 0; ?>
|
||||
<?php ob_start(); ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const lat = <?= (float)$warga['lat'] ?>;
|
||||
const lng = <?= (float)$warga['lng'] ?>;
|
||||
if (lat && lng) {
|
||||
const map = L.map('warga-detail-map').setView([lat,lng],16);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19}).addTo(map);
|
||||
const color = '<?= warnaMiskin($warga['status_kemiskinan']) ?>';
|
||||
L.circleMarker([lat,lng],{radius:12,fillColor:color,color:'#fff',weight:3,fillOpacity:0.9})
|
||||
.bindPopup('<strong><?= addslashes($warga['nama']) ?></strong>').addTo(map).openPopup();
|
||||
<?php if($warga['ri_lat'] && $warga['ri_lng']): ?>
|
||||
GISMap.map = map;
|
||||
GISMap.createRIMarker({lat:<?= (float)$warga['ri_lat'] ?>,lng:<?= (float)$warga['ri_lng'] ?>})
|
||||
.bindPopup('<?= addslashes($warga['rumah_ibadah_nama']??'Rumah Ibadah') ?>').addTo(map);
|
||||
<?php endif; ?>
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php $extraJs = ob_get_clean(); ?>
|
||||
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px">
|
||||
<a href="<?= APP_URL ?>/verifikator/warga" class="btn btn-ghost btn-sm">← Kembali</a>
|
||||
<h2 style="font-size:1.1rem;font-weight:700"><?= e($warga['nama']) ?></h2>
|
||||
<?php $sc=['Miskin Ekstrem'=>'badge-red','Miskin'=>'badge-orange','Rentan Miskin'=>'badge-yellow']; ?>
|
||||
<span class="badge <?= $sc[$warga['status_kemiskinan']]??'badge-gray' ?>"><?= e($warga['status_kemiskinan']) ?></span>
|
||||
<?php if($warga['is_blind_spot']): ?><span class="badge badge-purple">⚠ Blind Spot</span><?php endif; ?>
|
||||
<div style="margin-left:auto;display:flex;gap:8px">
|
||||
<button class="btn btn-primary btn-sm" onclick="App.showModal('modal-edit')">✏️ Edit Data</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 340px;gap:20px">
|
||||
<!-- KIRI -->
|
||||
<div style="display:flex;flex-direction:column;gap:16px">
|
||||
|
||||
<!-- Skor Prioritas Card -->
|
||||
<div class="card" style="border-left:4px solid <?= ['Tinggi'=>'#dc2626','Sedang'=>'#d97706','Rendah'=>'#16a34a'][$warga['level_prioritas']]??'#6b7280' ?>">
|
||||
<div class="card-body">
|
||||
<div style="display:flex;align-items:center;gap:20px">
|
||||
<div style="text-align:center;min-width:90px">
|
||||
<div style="font-size:2.5rem;font-weight:900;color:<?= ['Tinggi'=>'#dc2626','Sedang'=>'#d97706','Rendah'=>'#16a34a'][$warga['level_prioritas']]??'#6b7280' ?>"><?= $warga['skor_prioritas'] ?></div>
|
||||
<div class="text-xs text-muted">Skor / 100</div>
|
||||
</div>
|
||||
<div style="flex:1">
|
||||
<div class="priority-bar" style="height:10px;margin-bottom:8px">
|
||||
<div class="priority-bar-fill <?= strtolower($warga['level_prioritas'])==='tinggi'?'high':(strtolower($warga['level_prioritas'])==='sedang'?'medium':'low') ?>"
|
||||
style="width:<?= min(100,$warga['skor_prioritas']) ?>%"></div>
|
||||
</div>
|
||||
<div style="font-size:1rem;font-weight:700">Prioritas <?= e($warga['level_prioritas']) ?></div>
|
||||
<div class="text-sm text-muted">Diperbarui otomatis berdasarkan data sosial ekonomi</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Pribadi -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">👤</span><h3>Data Pribadi</h3></div>
|
||||
<div class="card-body">
|
||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px">
|
||||
<?php $items = [
|
||||
['NIK', $warga['nik']??'—'],
|
||||
['Tempat Lahir', $warga['tempat_lahir']??'—'],
|
||||
['Tanggal Lahir', $warga['tanggal_lahir'] ? formatTanggal($warga['tanggal_lahir'])." ($umur thn)" : '—'],
|
||||
['Jenis Kelamin', $warga['jenis_kelamin']==='L'?'Laki-laki':'Perempuan'],
|
||||
['Agama', $warga['agama']??'—'],
|
||||
['No HP', $warga['no_hp']??'—'],
|
||||
]; foreach($items as [$l,$v]): ?>
|
||||
<div>
|
||||
<div class="text-xs text-muted"><?= $l ?></div>
|
||||
<div style="font-weight:500;font-size:0.875rem;margin-top:2px"><?= e($v) ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<hr class="divider">
|
||||
<div>
|
||||
<div class="text-xs text-muted">Alamat</div>
|
||||
<div style="font-weight:500;margin-top:2px"><?= e($warga['alamat']) ?></div>
|
||||
<div class="text-xs text-muted" style="margin-top:2px"><?= e($warga['kelurahan']) ?>, <?= e($warga['kecamatan']) ?></div>
|
||||
</div>
|
||||
<?php if($warga['foto']): ?>
|
||||
<hr class="divider">
|
||||
<img src="<?= uploadUrl($warga['foto']) ?>" style="width:100px;height:100px;object-fit:cover;border-radius:8px;border:1px solid var(--clr-border)">
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sosial Ekonomi -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">💼</span><h3>Data Sosial Ekonomi</h3></div>
|
||||
<div class="card-body">
|
||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px">
|
||||
<?php $items2 = [
|
||||
['Pendidikan', $warga['pendidikan_terakhir']??'—'],
|
||||
['Pekerjaan', $warga['pekerjaan']??'—'],
|
||||
['Penghasilan', formatRupiah($warga['penghasilan_bulanan']??0).'/bln'],
|
||||
['Tanggungan', ($warga['jumlah_tanggungan']??0).' orang'],
|
||||
['Kondisi Rumah', $warga['kondisi_rumah']??'—'],
|
||||
['Riwayat Penyakit', $warga['riwayat_penyakit']??'—'],
|
||||
]; foreach($items2 as [$l,$v]): ?>
|
||||
<div>
|
||||
<div class="text-xs text-muted"><?= $l ?></div>
|
||||
<div style="font-weight:500;font-size:0.875rem;margin-top:2px"><?= e($v) ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<hr class="divider">
|
||||
<div style="display:flex;gap:12px;flex-wrap:wrap">
|
||||
<?php if($warga['is_lansia']): ?>
|
||||
<span class="badge badge-orange" style="font-size:0.8rem;padding:4px 10px">👴 Lansia</span>
|
||||
<?php endif; ?>
|
||||
<?php if($warga['is_disabilitas']): ?>
|
||||
<span class="badge badge-blue" style="font-size:0.8rem;padding:4px 10px">♿ Disabilitas: <?= e($warga['disabilitas_jenis']??'') ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if($warga['is_blind_spot']): ?>
|
||||
<span class="badge badge-purple" style="font-size:0.8rem;padding:4px 10px">⚠ Blind Spot</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Histori Bantuan -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📦</span><h3>Histori Bantuan (<?= count($bantuan) ?>)</h3></div>
|
||||
<div class="card-body no-pad">
|
||||
<?php if($bantuan): ?>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Tanggal</th><th>Kategori</th><th>Jumlah</th><th>Rumah Ibadah</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($bantuan as $b): ?>
|
||||
<tr>
|
||||
<td class="text-sm"><?= formatTanggal($b['tanggal_penyaluran']) ?></td>
|
||||
<td><span class="badge badge-green"><?= e($b['kategori']) ?></span></td>
|
||||
<td class="text-sm"><?= $b['jumlah'] ? ($b['satuan']==='rupiah'?formatRupiah($b['jumlah']):(number_format($b['jumlah']).' '.$b['satuan'])) : '—' ?></td>
|
||||
<td class="text-sm"><?= e($b['rumah_ibadah']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="empty-state" style="padding:24px"><div class="empty-state-icon">📭</div><p>Belum ada bantuan</p></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KANAN: Peta + RI Info -->
|
||||
<div style="display:flex;flex-direction:column;gap:16px">
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📍</span><h3>Lokasi</h3></div>
|
||||
<div class="card-body no-pad">
|
||||
<div id="warga-detail-map" style="height:260px"></div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-xs text-muted">Koordinat</div>
|
||||
<div style="font-family:monospace;font-size:0.8rem"><?= $warga['lat'] ?>, <?= $warga['lng'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if($warga['rumah_ibadah_nama']): ?>
|
||||
<div class="card" style="border:1px solid #bbf7d0">
|
||||
<div class="card-header" style="background:#f0fdf4">
|
||||
<span class="card-icon">⛪</span><h3>Rumah Ibadah Penanggung Jawab</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div style="font-weight:700;font-size:1rem;color:var(--clr-success)"><?= e($warga['rumah_ibadah_nama']) ?></div>
|
||||
<div class="text-sm text-muted" style="margin-top:4px">Ditetapkan otomatis berdasarkan Haversine Formula</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">ℹ️</span><h3>Info Verifikasi</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="text-xs text-muted">Tanggal Verifikasi</div>
|
||||
<div class="text-sm"><?= $warga['tanggal_verifikasi'] ? formatTanggal($warga['tanggal_verifikasi']) : '—' ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL EDIT (sederhana) -->
|
||||
<div class="modal-overlay" id="modal-edit">
|
||||
<div class="modal" style="max-width:680px">
|
||||
<div class="modal-header">
|
||||
<h3>✏️ Edit Data Warga</h3>
|
||||
<button class="modal-close" onclick="App.hideModal('modal-edit')">✕</button>
|
||||
</div>
|
||||
<form method="POST" action="<?= APP_URL ?>/verifikator/warga/<?= $warga['id'] ?>/update" enctype="multipart/form-data">
|
||||
<?= csrfField() ?>
|
||||
<div class="modal-body">
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group"><label class="form-label">Nama <span class="required">*</span></label>
|
||||
<input type="text" name="nama" class="form-control" value="<?= e($warga['nama']) ?>" required></div>
|
||||
<div class="form-group"><label class="form-label">NIK</label>
|
||||
<input type="text" name="nik" class="form-control" value="<?= e($warga['nik']??'') ?>" maxlength="16"></div>
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group"><label class="form-label">Tempat Lahir</label>
|
||||
<input type="text" name="tempat_lahir" class="form-control" value="<?= e($warga['tempat_lahir']??'') ?>"></div>
|
||||
<div class="form-group"><label class="form-label">Tgl Lahir</label>
|
||||
<input type="date" name="tanggal_lahir" class="form-control" value="<?= $warga['tanggal_lahir']??'' ?>"></div>
|
||||
</div>
|
||||
<div class="form-row cols-3">
|
||||
<div class="form-group"><label class="form-label">JK</label>
|
||||
<select name="jenis_kelamin" class="form-control">
|
||||
<option value="L" <?= $warga['jenis_kelamin']==='L'?'selected':'' ?>>Laki-laki</option>
|
||||
<option value="P" <?= $warga['jenis_kelamin']==='P'?'selected':'' ?>>Perempuan</option>
|
||||
</select></div>
|
||||
<div class="form-group"><label class="form-label">Agama</label>
|
||||
<select name="agama" class="form-control">
|
||||
<?php foreach(['Islam','Kristen','Katolik','Hindu','Buddha','Konghucu','Lainnya'] as $ag): ?>
|
||||
<option value="<?= $ag ?>" <?= ($warga['agama']??'')===$ag?'selected':'' ?>><?= $ag ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="form-group"><label class="form-label">No HP</label>
|
||||
<input type="text" name="no_hp" class="form-control" value="<?= e($warga['no_hp']??'') ?>"></div>
|
||||
</div>
|
||||
<div class="form-group"><label class="form-label">Alamat</label>
|
||||
<input type="text" name="alamat" class="form-control" value="<?= e($warga['alamat']) ?>"></div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group"><label class="form-label">Penghasilan/Bln</label>
|
||||
<input type="number" name="penghasilan_bulanan" class="form-control" value="<?= $warga['penghasilan_bulanan']??0 ?>"></div>
|
||||
<div class="form-group"><label class="form-label">Tanggungan</label>
|
||||
<input type="number" name="jumlah_tanggungan" class="form-control" value="<?= $warga['jumlah_tanggungan']??0 ?>"></div>
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group"><label class="form-label">Pendidikan</label>
|
||||
<select name="pendidikan_terakhir" class="form-control">
|
||||
<?php foreach(['Tidak Sekolah','SD','SMP','SMA','D3','S1','S2','S3'] as $p): ?>
|
||||
<option value="<?= $p ?>" <?= $warga['pendidikan_terakhir']===$p?'selected':'' ?>><?= $p ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="form-group"><label class="form-label">Pekerjaan</label>
|
||||
<input type="text" name="pekerjaan" class="form-control" value="<?= e($warga['pekerjaan']??'') ?>" placeholder="Buruh, Petani, dll"></div>
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group"><label class="form-label">Kondisi Rumah</label>
|
||||
<select name="kondisi_rumah" class="form-control">
|
||||
<?php foreach(['Sangat Tidak Layak','Tidak Layak','Cukup Layak','Layak'] as $k): ?>
|
||||
<option value="<?= $k ?>" <?= $warga['kondisi_rumah']===$k?'selected':'' ?>><?= $k ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select></div>
|
||||
</div>
|
||||
<div class="form-group"><label class="form-label">Riwayat Penyakit</label>
|
||||
<input type="text" name="riwayat_penyakit" class="form-control" value="<?= e($warga['riwayat_penyakit']??'') ?>"></div>
|
||||
<div style="display:flex;gap:20px">
|
||||
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
|
||||
<input type="checkbox" name="is_lansia" value="1" <?= $warga['is_lansia']?'checked':'' ?>> Lansia</label>
|
||||
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
|
||||
<input type="checkbox" name="is_disabilitas" id="edit-is-dis" value="1" <?= $warga['is_disabilitas']?'checked':'' ?>
|
||||
onchange="document.getElementById('edit-jenis-dis').style.display=this.checked?'block':'none'"> Disabilitas</label>
|
||||
</div>
|
||||
<div id="edit-jenis-dis" style="margin-top:10px;<?= $warga['is_disabilitas']?'':'display:none' ?>">
|
||||
<div class="form-group"><label class="form-label">Jenis Disabilitas</label>
|
||||
<input type="text" name="disabilitas_jenis" class="form-control" value="<?= e($warga['disabilitas_jenis']??'') ?>" placeholder="Tuna netra, tuna daksa, dll"></div>
|
||||
</div>
|
||||
<div class="form-row cols-2" style="margin-top:12px">
|
||||
<div class="form-group"><label class="form-label">Latitude</label>
|
||||
<input type="number" name="lat" class="form-control" value="<?= $warga['lat'] ?>" step="0.0000001"></div>
|
||||
<div class="form-group"><label class="form-label">Longitude</label>
|
||||
<input type="number" name="lng" class="form-control" value="<?= $warga['lng'] ?>" step="0.0000001"></div>
|
||||
</div>
|
||||
<div class="form-group"><label class="form-label">Foto Baru</label>
|
||||
<input type="file" name="foto" class="form-control" accept="image/*"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" onclick="App.hideModal('modal-edit')">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">Simpan Perubahan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
$_mapLat = MAP_DEFAULT_LAT;
|
||||
$_mapLng = MAP_DEFAULT_LNG;
|
||||
$extraJs = <<<JS
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
LocationPicker.init('picker-map','lat','lng', {$_mapLat}, {$_mapLng});
|
||||
|
||||
// Auto-detect lansia
|
||||
document.getElementById('tanggal_lahir').addEventListener('change', function() {
|
||||
const d = new Date(this.value);
|
||||
const age = Math.floor((Date.now() - d.getTime()) / (365.25*24*60*60*1000));
|
||||
document.getElementById('is_lansia').checked = age >= 60;
|
||||
});
|
||||
|
||||
// Auto hitung skor
|
||||
const calcBtn = document.getElementById('calc-skor');
|
||||
if (calcBtn) {
|
||||
calcBtn.addEventListener('click', function() {
|
||||
const p = parseInt(document.getElementById('penghasilan').value) || 0;
|
||||
const t = parseInt(document.getElementById('tanggungan').value) || 0;
|
||||
const kapita = t > 0 ? p/t : p;
|
||||
const st = kapita <= 350000 ? 'Miskin Ekstrem' : kapita <= 550000 ? 'Miskin' : 'Rentan Miskin';
|
||||
document.getElementById('status_kem_preview').textContent = st;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
JS; ?>
|
||||
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px">
|
||||
<a href="<?= APP_URL ?>/verifikator/warga" class="btn btn-ghost btn-sm">← Kembali</a>
|
||||
<h2 style="font-size:1.1rem;font-weight:700">Input Data Warga Baru</h2>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="<?= APP_URL ?>/verifikator/warga/store" enctype="multipart/form-data">
|
||||
<?= csrfField() ?>
|
||||
<div style="display:grid;grid-template-columns:1fr 360px;gap:20px">
|
||||
|
||||
<!-- KIRI: Form Data -->
|
||||
<div style="display:flex;flex-direction:column;gap:16px">
|
||||
<!-- Data Pribadi -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">👤</span><h3>Data Pribadi</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nama Lengkap <span class="required">*</span></label>
|
||||
<input type="text" name="nama" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">NIK (KTP)</label>
|
||||
<input type="text" name="nik" class="form-control" maxlength="16" placeholder="16 digit NIK">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tempat Lahir</label>
|
||||
<input type="text" name="tempat_lahir" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tanggal Lahir</label>
|
||||
<input type="date" name="tanggal_lahir" id="tanggal_lahir" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row cols-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Jenis Kelamin</label>
|
||||
<select name="jenis_kelamin" class="form-control">
|
||||
<option value="L">Laki-laki</option>
|
||||
<option value="P">Perempuan</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Agama</label>
|
||||
<select name="agama" class="form-control">
|
||||
<?php foreach (['Islam','Kristen','Katolik','Hindu','Buddha','Konghucu','Lainnya'] as $ag): ?>
|
||||
<option value="<?= $ag ?>"><?= $ag ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">No HP</label>
|
||||
<input type="text" name="no_hp" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Alamat Lengkap <span class="required">*</span></label>
|
||||
<textarea name="alamat" class="form-control" rows="2" required></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Foto</label>
|
||||
<input type="file" name="foto" class="form-control" accept="image/*">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sosial Ekonomi -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">💼</span><h3>Data Sosial Ekonomi</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Pendidikan Terakhir</label>
|
||||
<select name="pendidikan_terakhir" class="form-control">
|
||||
<?php foreach (['Tidak Sekolah','SD','SMP','SMA','D3','S1','S2','S3'] as $p): ?>
|
||||
<option value="<?= $p ?>"><?= $p ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Pekerjaan</label>
|
||||
<input type="text" name="pekerjaan" class="form-control" placeholder="Buruh, Petani, dll">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Penghasilan/Bulan (Rp) <span class="required">*</span></label>
|
||||
<input type="number" name="penghasilan_bulanan" id="penghasilan" class="form-control" value="0" min="0" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Jumlah Tanggungan <span class="required">*</span></label>
|
||||
<input type="number" name="jumlah_tanggungan" id="tanggungan" class="form-control" value="0" min="0" max="20" required>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:12px;align-items:center;margin-bottom:12px">
|
||||
<button type="button" id="calc-skor" class="btn btn-ghost btn-sm">⚡ Hitung Otomatis</button>
|
||||
<span class="text-sm">Status Kemiskinan: <strong id="status_kem_preview" style="color:var(--clr-danger)">—</strong></span>
|
||||
</div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kondisi Rumah <span class="required">*</span></label>
|
||||
<select name="kondisi_rumah" class="form-control" required>
|
||||
<?php foreach (['Sangat Tidak Layak','Tidak Layak','Cukup Layak','Layak'] as $k): ?>
|
||||
<option value="<?= $k ?>"><?= $k ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Riwayat Penyakit</label>
|
||||
<input type="text" name="riwayat_penyakit" class="form-control" placeholder="Diabetes, Hipertensi, dll">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kondisi Khusus -->
|
||||
<div style="display:flex;gap:24px;flex-wrap:wrap">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" name="is_lansia" id="is_lansia" value="1"> Lansia (≥60 tahun)
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" name="is_disabilitas" id="is_disabilitas" value="1"
|
||||
onchange="document.getElementById('jenis-dis').style.display=this.checked?'block':'none'">
|
||||
Penyandang Disabilitas
|
||||
</label>
|
||||
</div>
|
||||
<div id="jenis-dis" style="display:none;margin-top:10px">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Jenis Disabilitas</label>
|
||||
<input type="text" name="disabilitas_jenis" class="form-control" placeholder="Tuna netra, tuna daksa, dll">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KANAN: Peta Lokasi -->
|
||||
<div style="display:flex;flex-direction:column;gap:16px">
|
||||
<div class="card" style="position:sticky;top:80px">
|
||||
<div class="card-header"><span class="card-icon">📍</span><h3>Lokasi Rumah</h3></div>
|
||||
<div class="card-body">
|
||||
<div id="picker-map" style="height:340px;border-radius:8px;border:1px solid var(--clr-border);margin-bottom:12px"></div>
|
||||
<div class="form-row cols-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Latitude <span class="required">*</span></label>
|
||||
<input type="number" name="lat" id="lat" class="form-control" step="0.0000001" required placeholder="-0.0263">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Longitude <span class="required">*</span></label>
|
||||
<input type="number" name="lng" id="lng" class="form-control" step="0.0000001" required placeholder="109.3425">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-hint">Klik peta atau drag marker untuk menentukan lokasi rumah</div>
|
||||
|
||||
<!-- Wilayah -->
|
||||
<div class="form-group" style="margin-top:12px">
|
||||
<label class="form-label">Kelurahan</label>
|
||||
<input type="text" class="form-control" value="<?= e($kelurahan['nama'] ?? '') ?> — <?= e($kelurahan['kecamatan'] ?? '') ?>" readonly style="background:#f8fafc">
|
||||
</div>
|
||||
|
||||
<div style="margin-top:16px;padding:12px;background:#f0fdf4;border-radius:8px;border:1px solid #bbf7d0">
|
||||
<div style="font-size:0.78rem;color:var(--clr-success);font-weight:600;margin-bottom:4px">ℹ️ Assignment Otomatis</div>
|
||||
<div style="font-size:0.75rem;color:#166534">Setelah disimpan, sistem akan otomatis:<br>
|
||||
✓ Menghitung rumah ibadah terdekat (Haversine)<br>
|
||||
✓ Mendeteksi blind spot<br>
|
||||
✓ Menghitung skor prioritas bantuan
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ACTION BUTTONS -->
|
||||
<div style="display:flex;justify-content:flex-end;gap:12px;margin-top:20px">
|
||||
<a href="<?= APP_URL ?>/verifikator/warga" class="btn btn-ghost">Batal</a>
|
||||
<button type="submit" class="btn btn-primary" style="padding:11px 28px">💾 Simpan Data Warga</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,64 @@
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
||||
<div>
|
||||
<h2 style="font-size:1.2rem;font-weight:700">Daftar Laporan Masyarakat</h2>
|
||||
<p class="text-muted text-sm">Kelurahan: <strong><?= e(Auth::user()['kelurahan_nama'] ?? '-') ?></strong></p>
|
||||
</div>
|
||||
<a href="<?= APP_URL ?>/verifikator/warga/tambah" class="btn btn-primary">➕ Input Manual Warga</a>
|
||||
</div>
|
||||
|
||||
<!-- Filter Status -->
|
||||
<div class="tabs">
|
||||
<?php $statuses = ['semua'=>'Semua','menunggu'=>'⏳ Menunggu','diproses'=>'🔄 Diproses','terverifikasi'=>'✅ Terverifikasi','ditolak'=>'❌ Ditolak']; ?>
|
||||
<?php foreach ($statuses as $s => $l): ?>
|
||||
<a href="?status=<?= $s ?>" class="tab-btn <?= ($status === $s) ? 'active' : '' ?>"><?= $l ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- SEARCH -->
|
||||
<div class="filters-bar">
|
||||
<input type="text" id="search-lap" class="form-control" placeholder="🔍 Cari nama warga, kode laporan..." style="max-width:320px">
|
||||
</div>
|
||||
|
||||
<!-- TABLE -->
|
||||
<div class="card">
|
||||
<div class="card-body no-pad">
|
||||
<div class="table-wrap">
|
||||
<table id="table-laporan">
|
||||
<thead>
|
||||
<tr><th>Kode</th><th>Nama Warga</th><th>Pelapor</th><th>Alamat</th><th>Status</th><th>Tanggal</th><th>Aksi</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($data as $l): ?>
|
||||
<tr>
|
||||
<td><code style="font-size:0.78rem;background:#f1f5f9;padding:2px 6px;border-radius:4px"><?= e($l['kode_laporan']) ?></code></td>
|
||||
<td>
|
||||
<div style="font-weight:600"><?= e($l['nama_warga']) ?></div>
|
||||
<div class="text-xs text-muted"><?= e($l['alamat_warga']) ?></div>
|
||||
</td>
|
||||
<td class="text-sm">
|
||||
<?= e($l['nama_pelapor']) ?>
|
||||
<?php if ($l['no_hp_pelapor']): ?>
|
||||
<div class="text-xs text-muted"><?= e($l['no_hp_pelapor']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-sm"><?= e($l['kelurahan'] ?? '') ?></td>
|
||||
<td>
|
||||
<?php $badgeMap = ['menunggu'=>'badge-yellow','diproses'=>'badge-blue','terverifikasi'=>'badge-green','ditolak'=>'badge-red']; ?>
|
||||
<span class="badge <?= $badgeMap[$l['status']] ?? 'badge-gray' ?>"><?= ucfirst($l['status']) ?></span>
|
||||
</td>
|
||||
<td class="text-xs text-muted"><?= formatTanggal($l['created_at']) ?></td>
|
||||
<td>
|
||||
<a href="<?= APP_URL ?>/verifikator/laporan/<?= $l['id'] ?>" class="btn btn-primary btn-sm">Detail</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (empty($data)): ?>
|
||||
<div class="empty-state"><div class="empty-state-icon">📭</div><h3>Tidak ada laporan</h3><p>Belum ada laporan dengan status ini</p></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>document.addEventListener('DOMContentLoaded', () => App.initTableSearch('search-lap', 'table-laporan'));</script>
|
||||
@@ -0,0 +1,111 @@
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
||||
<div>
|
||||
<h2 style="font-size:1.2rem;font-weight:700">Data Warga Miskin</h2>
|
||||
<p class="text-muted text-sm">Kelurahan: <strong><?= e(Auth::user()['kelurahan_nama'] ?? '-') ?></strong> · <?= count($data) ?> warga terdata</p>
|
||||
</div>
|
||||
<a href="<?= APP_URL ?>/verifikator/warga/tambah" class="btn btn-primary">➕ Tambah Warga</a>
|
||||
</div>
|
||||
|
||||
<!-- Filter -->
|
||||
<div class="tabs">
|
||||
<?php $levels = ['semua'=>'Semua','Tinggi'=>'🔥 Prioritas Tinggi','Sedang'=>'⚡ Sedang','Rendah'=>'✅ Rendah']; ?>
|
||||
<?php foreach ($levels as $l => $label): ?>
|
||||
<a href="?level=<?= $l ?>" class="tab-btn <?= $level===$l?'active':'' ?>"><?= $label ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="filters-bar">
|
||||
<input type="text" id="search-warga" class="form-control" placeholder="🔍 Cari nama, NIK..." style="max-width:300px">
|
||||
</div>
|
||||
|
||||
<!-- PETA -->
|
||||
<div class="card" style="margin-bottom:20px">
|
||||
<div class="card-header"><span class="card-icon">🗺</span><h3>Peta Warga Kelurahan</h3></div>
|
||||
<div class="card-body no-pad">
|
||||
<div id="warga-map" style="height:280px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TABLE -->
|
||||
<div class="card">
|
||||
<div class="card-body no-pad">
|
||||
<div class="table-wrap">
|
||||
<table id="table-warga">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th><th>Nama / NIK</th><th>Status Kemiskinan</th><th>Prioritas</th>
|
||||
<th>Penghasilan</th><th>Tanggungan</th><th>Rumah Ibadah</th><th>Ket.</th><th>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($data as $i => $w): ?>
|
||||
<tr>
|
||||
<td class="text-xs text-muted"><?= $i+1 ?></td>
|
||||
<td>
|
||||
<div style="font-weight:600"><?= e($w['nama']) ?></div>
|
||||
<div class="text-xs text-muted"><?= $w['nik'] ? 'NIK: '.e($w['nik']) : '<em>NIK belum diisi</em>' ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<?php $sc = ['Miskin Ekstrem'=>'badge-red','Miskin'=>'badge-orange','Rentan Miskin'=>'badge-yellow']; ?>
|
||||
<span class="badge <?= $sc[$w['status_kemiskinan']] ?? 'badge-gray' ?>"><?= e($w['status_kemiskinan']) ?></span>
|
||||
</td>
|
||||
<td>
|
||||
<?php $pc = ['Tinggi'=>'badge-red','Sedang'=>'badge-yellow','Rendah'=>'badge-green']; ?>
|
||||
<span class="badge <?= $pc[$w['level_prioritas']] ?? 'badge-gray' ?>"><?= e($w['level_prioritas']) ?></span>
|
||||
<div style="margin-top:4px">
|
||||
<div class="priority-bar" style="width:80px">
|
||||
<div class="priority-bar-fill <?= strtolower($w['level_prioritas'])==='tinggi'?'high':(strtolower($w['level_prioritas'])==='sedang'?'medium':'low') ?>"
|
||||
style="width:<?= min(100,$w['skor_prioritas']) ?>%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-muted"><?= $w['skor_prioritas'] ?>/100</div>
|
||||
</td>
|
||||
<td class="text-sm"><?= formatRupiah($w['penghasilan_bulanan'] ?? 0) ?></td>
|
||||
<td class="text-sm text-center"><?= $w['jumlah_tanggungan'] ?> orang</td>
|
||||
<td class="text-sm"><?= e($w['rumah_ibadah'] ?? '—') ?></td>
|
||||
<td>
|
||||
<?php if ($w['is_blind_spot']): ?>
|
||||
<span class="badge badge-purple" title="Tidak dalam radius rumah ibadah">⚠ Blind</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($w['is_lansia']): ?>
|
||||
<span class="badge badge-orange" title="Lansia">👴</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($w['is_disabilitas']): ?>
|
||||
<span class="badge badge-blue" title="Disabilitas">♿</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<a href="<?= APP_URL ?>/verifikator/warga/<?= $w['id'] ?>" class="btn btn-ghost btn-sm">Detail</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (empty($data)): ?>
|
||||
<div class="empty-state"><div class="empty-state-icon">👥</div><h3>Belum ada data warga</h3></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
App.initTableSearch('search-warga','table-warga');
|
||||
|
||||
const map = GISMap.init('warga-map', <?= MAP_DEFAULT_LAT ?>, <?= MAP_DEFAULT_LNG ?>, 14);
|
||||
const warga = <?= json_encode(array_map(fn($w) => [
|
||||
'id'=>$w['id'],'nama'=>$w['nama'],'lat'=>(float)$w['lat'],'lng'=>(float)$w['lng'],
|
||||
'status_kemiskinan'=>$w['status_kemiskinan'],'level_prioritas'=>$w['level_prioritas'],
|
||||
'is_blind_spot'=>$w['is_blind_spot'],'kelurahan'=>$w['kelurahan']??''
|
||||
], array_filter($data, fn($w) => $w['lat'] && $w['lng']))) ?>;
|
||||
|
||||
if (warga.length) {
|
||||
GISMap.addWargaLayer(warga, w => {
|
||||
window.location.href = '<?= APP_URL ?>/verifikator/warga/' + w.id;
|
||||
});
|
||||
GISMap.autoFitBounds();
|
||||
} else {
|
||||
GISMap.autoFitBounds(true);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,205 @@
|
||||
<?php ob_start(); ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Peta eksekutif dengan heatmap
|
||||
const map = GISMap.init('wk-map', <?= MAP_DEFAULT_LAT ?>, <?= MAP_DEFAULT_LNG ?>, 12);
|
||||
|
||||
const [heatData, petaData] = await Promise.all([
|
||||
App.fetchJSON('<?= APP_URL ?>/dinsos/api/heatmap'),
|
||||
App.fetchJSON('<?= APP_URL ?>/walikota/api/peta')
|
||||
]);
|
||||
|
||||
if(petaData) {
|
||||
GISMap.addRILayer(petaData.rumah_ibadah||[]);
|
||||
}
|
||||
if(heatData) GISMap.addHeatmap(heatData);
|
||||
// Auto zoom ke seluruh Kota Pontianak
|
||||
GISMap.autoFitBounds();
|
||||
|
||||
// Chart per kecamatan (horizontal bar)
|
||||
const kec = <?= json_encode(array_column($perKecamatan,'nama')) ?>;
|
||||
const totals = <?= json_encode(array_column($perKecamatan,'total')) ?>;
|
||||
const ekstrems = <?= json_encode(array_column($perKecamatan,'ekstrem')) ?>;
|
||||
new Chart(document.getElementById('chart-kec'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: kec,
|
||||
datasets: [
|
||||
{label:'Total', data:totals, backgroundColor:'#1a3a5ccc', borderRadius:4},
|
||||
{label:'Ekstrem', data:ekstrems, backgroundColor:'#dc2626cc', borderRadius:4}
|
||||
]
|
||||
},
|
||||
options:{responsive:true,maintainAspectRatio:false,
|
||||
indexAxis:'y',
|
||||
scales:{x:{beginAtZero:true,grid:{color:'#f1f5f9'}},y:{grid:{display:false}}}}
|
||||
});
|
||||
|
||||
// Chart tren bulanan
|
||||
const bulan = <?= json_encode(array_column($trenBulanan,'bulan')) ?>;
|
||||
const bantuanData = <?= json_encode(array_column($trenBulanan,'jumlah')) ?>;
|
||||
Charts.line('chart-tren-wk', bulan, [{label:'Bantuan Tersalurkan', data:bantuanData}]);
|
||||
|
||||
// Donut komposisi kemiskinan
|
||||
Charts.donut('chart-komposisi',
|
||||
['Miskin Ekstrem','Miskin','Rentan Miskin'],
|
||||
[<?= ($stats['ekstrem']??0).','.(($stats['miskin']??0)).','.(($stats['rentan']??0)) ?>],
|
||||
['#dc2626','#ea580c','#ca8a04']
|
||||
);
|
||||
});
|
||||
</script>
|
||||
<?php $extraJs = ob_get_clean(); ?>
|
||||
|
||||
<!-- Executive Header Banner -->
|
||||
<div style="background:linear-gradient(135deg,#1a0533 0%,#2d1b69 50%,#4c1d95 100%);color:#fff;border-radius:var(--radius-lg);padding:28px 32px;margin-bottom:24px;display:flex;align-items:center;gap:24px">
|
||||
<div style="font-size:4rem">🏛</div>
|
||||
<div>
|
||||
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:0.12em;color:rgba(255,255,255,0.55);margin-bottom:6px">Dashboard Eksekutif</div>
|
||||
<div style="font-size:1.5rem;font-weight:900;letter-spacing:-0.01em">Penanganan Kemiskinan Kota Pontianak</div>
|
||||
<div style="font-size:0.85rem;color:rgba(255,255,255,0.65);margin-top:6px">Data real-time sistem SINERGI · <?= date('d F Y') ?></div>
|
||||
</div>
|
||||
<div style="margin-left:auto;text-align:right">
|
||||
<div style="font-size:0.72rem;color:rgba(255,255,255,0.45);text-transform:uppercase;letter-spacing:0.08em">Status Sistem</div>
|
||||
<div style="font-size:1rem;font-weight:700;color:#4ade80;margin-top:4px">● Aktif & Online</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI Cards Row 1 -->
|
||||
<div class="stats-grid" style="grid-template-columns:repeat(5,1fr)">
|
||||
<div class="stat-card" style="border-top:3px solid #dc2626">
|
||||
<div class="stat-card-icon red">👥</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format($stats['total_warga']??0) ?></div>
|
||||
<div class="stat-card-label">Total Warga Miskin</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card" style="border-top:3px solid #dc2626">
|
||||
<div class="stat-card-icon red">💀</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format($stats['ekstrem']??0) ?></div>
|
||||
<div class="stat-card-label">Miskin Ekstrem</div>
|
||||
<div class="stat-card-sub"><?= $stats['total_warga']>0?round($stats['ekstrem']/$stats['total_warga']*100,1):0 ?>% dari total</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card" style="border-top:3px solid #7c3aed">
|
||||
<div class="stat-card-icon purple">⚠️</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format($stats['blind_spot']??0) ?></div>
|
||||
<div class="stat-card-label">Warga Blind Spot</div>
|
||||
<div class="stat-card-sub">Belum terjangkau</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card" style="border-top:3px solid #16a34a">
|
||||
<div class="stat-card-icon green">⛪</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format($stats['total_ri']??0) ?></div>
|
||||
<div class="stat-card-label">Rumah Ibadah Aktif</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card" style="border-top:3px solid #0284c7">
|
||||
<div class="stat-card-icon teal">📦</div>
|
||||
<div class="stat-card-body">
|
||||
<div class="stat-card-value"><?= number_format($stats['bantuan_tahun_ini']??0) ?></div>
|
||||
<div class="stat-card-label">Bantuan Tahun Ini</div>
|
||||
<div class="stat-card-sub">Total: <?= number_format($stats['total_bantuan']??0) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid: Peta Besar + Sidebar Charts -->
|
||||
<div style="display:grid;grid-template-columns:1fr 380px;gap:20px;margin-bottom:20px">
|
||||
<!-- PETA UTAMA HEATMAP -->
|
||||
<div class="card">
|
||||
<div class="card-header" style="background:linear-gradient(to right,#1a0533,#2d1b69);color:#fff;border-radius:0">
|
||||
<span class="card-icon">🌡</span>
|
||||
<h3 style="color:#fff">Heatmap Kemiskinan Kota Pontianak</h3>
|
||||
</div>
|
||||
<div class="card-body no-pad" style="position:relative">
|
||||
<div id="wk-map" style="height:440px"></div>
|
||||
<div class="map-legend">
|
||||
<h4>Intensitas</h4>
|
||||
<div style="height:10px;border-radius:5px;background:linear-gradient(to right,#fef9c3,#fb923c,#ef4444,#7f1d1d);margin:4px 0 6px"></div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:0.6rem;color:var(--clr-text-muted)"><span>Rendah</span><span>Tinggi</span></div>
|
||||
<div class="map-legend-item" style="margin-top:8px"><div class="map-legend-dot legend-dot-ri"></div> Rumah Ibadah</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SIDEBAR CHARTS -->
|
||||
<div style="display:flex;flex-direction:column;gap:16px">
|
||||
<!-- Komposisi -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">🥧</span><h3>Komposisi Kemiskinan</h3></div>
|
||||
<div class="card-body"><div class="chart-wrap" style="height:160px"><canvas id="chart-komposisi"></canvas></div></div>
|
||||
</div>
|
||||
|
||||
<!-- Peringkat Kecamatan -->
|
||||
<div class="card" style="flex:1">
|
||||
<div class="card-header"><span class="card-icon">🏆</span><h3>Ranking Kecamatan</h3></div>
|
||||
<div class="card-body no-pad">
|
||||
<?php foreach($perKecamatan as $i=>$k): ?>
|
||||
<div style="padding:10px 16px;border-bottom:1px solid #f1f5f9">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<div style="width:22px;height:22px;border-radius:50%;background:<?= $i===0?'#dc2626':($i===1?'#d97706':($i===2?'#65a30d':'#94a3b8')) ?>;color:#fff;display:flex;align-items:center;justify-content:center;font-size:0.72rem;font-weight:800;flex-shrink:0"><?= $i+1 ?></div>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:0.82rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"><?= e($k['nama']) ?></div>
|
||||
<div style="height:4px;background:#f1f5f9;border-radius:2px;margin-top:3px;overflow:hidden">
|
||||
<?php $mx=max(array_column($perKecamatan,'total')?:[1]); ?>
|
||||
<div style="height:100%;background:<?= $i===0?'#dc2626':($i===1?'#d97706':'#1a3a5c') ?>;width:<?= $mx>0?round($k['total']/$mx*100):0 ?>%;border-radius:2px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<strong style="font-size:0.9rem;color:var(--clr-text)"><?= number_format($k['total']) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Charts Row -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px">
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📊</span><h3>Perbandingan Kemiskinan per Kecamatan</h3></div>
|
||||
<div class="card-body"><div class="chart-wrap" style="height:260px"><canvas id="chart-kec"></canvas></div></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">📈</span><h3>Tren Bantuan Tersalurkan (12 Bulan)</h3></div>
|
||||
<div class="card-body"><div class="chart-wrap" style="height:260px"><canvas id="chart-tren-wk"></canvas></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rumah Ibadah Paling Aktif -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-icon">🏆</span><h3>5 Rumah Ibadah Paling Aktif</h3></div>
|
||||
<div class="card-body no-pad">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Peringkat</th><th>Nama</th><th>Jenis</th><th>Kecamatan</th><th>Warga Binaan</th><th>Bantuan Disalurkan</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($topRi as $i=>$r): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="width:28px;height:28px;border-radius:50%;background:<?= $i===0?'#f59e0b':($i===1?'#94a3b8':($i===2?'#c2410c':'#e2e8f0')) ?>;color:<?= $i<3?'#fff':'var(--clr-text)' ?>;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:0.85rem"><?= $i+1 ?></div>
|
||||
</td>
|
||||
<td style="font-weight:600"><?= e($r['nama']) ?></td>
|
||||
<td><span class="badge badge-green"><?= e($r['jenis']) ?></span></td>
|
||||
<td class="text-sm"><?= e($r['kecamatan']) ?></td>
|
||||
<td class="text-sm"><strong><?= number_format($r['jml_warga']) ?></strong> warga</td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<strong style="color:var(--clr-success)"><?= number_format($r['jml_bantuan']) ?></strong>
|
||||
<div style="flex:1;max-width:80px">
|
||||
<div class="priority-bar">
|
||||
<?php $mx2=max(array_column($topRi,'jml_bantuan')?:[1]); ?>
|
||||
<div class="priority-bar-fill low" style="width:<?= $mx2>0?round($r['jml_bantuan']/$mx2*100):0 ?>%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
// config/app.php
|
||||
|
||||
define('APP_NAME', 'SINERGI');
|
||||
define('APP_FULL_NAME', 'Sistem Informasi Geografis Penanganan Kemiskinan Berbasis Kolaborasi Rumah Ibadah, Masyarakat, dan Pemerintah Kota Pontianak');
|
||||
define('APP_VERSION', '1.0.0');
|
||||
define('APP_URL', 'http://localhost/sinergi');
|
||||
define('APP_ROOT', dirname(__DIR__));
|
||||
define('APP_UPLOAD_DIR', APP_ROOT . '/public/images/uploads/');
|
||||
define('APP_UPLOAD_URL', APP_URL . '/public/images/uploads/');
|
||||
define('APP_TIMEZONE', 'Asia/Pontianak');
|
||||
|
||||
// ── Koordinat Kota Pontianak ──────────────────────────────────
|
||||
// Pusat kota: Tugu Khatulistiwa area
|
||||
define('MAP_DEFAULT_LAT', -0.0263);
|
||||
define('MAP_DEFAULT_LNG', 109.3425);
|
||||
define('MAP_DEFAULT_ZOOM', 13); // 13 = tampak seluruh kota
|
||||
|
||||
// Bounding box seluruh Kota Pontianak
|
||||
// SW: (-0.115, 109.280) NE: (0.055, 109.420)
|
||||
define('MAP_BOUNDS_SW_LAT', -0.115);
|
||||
define('MAP_BOUNDS_SW_LNG', 109.280);
|
||||
define('MAP_BOUNDS_NE_LAT', 0.055);
|
||||
define('MAP_BOUNDS_NE_LNG', 109.420);
|
||||
|
||||
// ── Konfigurasi Sistem ────────────────────────────────────────
|
||||
define('BLIND_SPOT_RADIUS', 500); // meter
|
||||
|
||||
// Session
|
||||
define('SESSION_NAME', 'sinergi_session');
|
||||
define('SESSION_LIFETIME', 7200); // 2 jam
|
||||
|
||||
date_default_timezone_set(APP_TIMEZONE);
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
// config/database.php
|
||||
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_NAME', 'sinergi_db');
|
||||
define('DB_USER', 'root');
|
||||
define('DB_PASS', '');
|
||||
define('DB_CHARSET', 'utf8mb4');
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
// core/Auth.php
|
||||
|
||||
class Auth {
|
||||
private static ?array $user = null;
|
||||
|
||||
public static function init(): void {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_name(SESSION_NAME);
|
||||
session_set_cookie_params([
|
||||
'lifetime' => SESSION_LIFETIME,
|
||||
'path' => '/',
|
||||
'secure' => false,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax'
|
||||
]);
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
|
||||
public static function attempt(string $username, string $password): bool {
|
||||
$db = Database::getInstance();
|
||||
$user = $db->fetchOne(
|
||||
"SELECT u.*, r.slug as role_slug, r.nama as role_nama
|
||||
FROM users u
|
||||
JOIN roles r ON u.id_role = r.id
|
||||
WHERE (u.username = ? OR u.email = ?) AND u.is_active = 1",
|
||||
[$username, $username]
|
||||
);
|
||||
|
||||
if ($user && password_verify($password, $user['password'])) {
|
||||
// Update last login
|
||||
$db->update('users', ['last_login' => date('Y-m-d H:i:s')], 'id = ?', [$user['id']]);
|
||||
|
||||
// Store in session
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['user_name'] = $user['nama'];
|
||||
$_SESSION['user_role'] = $user['role_slug'];
|
||||
$_SESSION['user_email'] = $user['email'];
|
||||
$_SESSION['id_kelurahan'] = $user['id_kelurahan'];
|
||||
$_SESSION['login_time'] = time();
|
||||
|
||||
// Log activity
|
||||
self::logActivity($user['id'], $user['nama'], $user['role_slug'], 'Login', 'auth', 'Login ke sistem');
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function check(): bool {
|
||||
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
|
||||
}
|
||||
|
||||
public static function user(): ?array {
|
||||
if (!self::check()) return null;
|
||||
if (self::$user !== null) return self::$user;
|
||||
|
||||
$db = Database::getInstance();
|
||||
self::$user = $db->fetchOne(
|
||||
"SELECT u.*, r.slug as role_slug, r.nama as role_nama,
|
||||
k.nama as kelurahan_nama, kec.nama as kecamatan_nama,
|
||||
ri.id as rumah_ibadah_id, ri.nama as rumah_ibadah_nama,
|
||||
ri.lat as ri_lat, ri.lng as ri_lng, ri.radius_meter as ri_radius
|
||||
FROM users u
|
||||
JOIN roles r ON u.id_role = r.id
|
||||
LEFT JOIN kelurahan k ON u.id_kelurahan = k.id
|
||||
LEFT JOIN kecamatan kec ON k.id_kecamatan = kec.id
|
||||
LEFT JOIN rumah_ibadah ri ON ri.id_user = u.id
|
||||
WHERE u.id = ?",
|
||||
[$_SESSION['user_id']]
|
||||
);
|
||||
return self::$user;
|
||||
}
|
||||
|
||||
public static function id(): ?int {
|
||||
return self::check() ? (int)$_SESSION['user_id'] : null;
|
||||
}
|
||||
|
||||
public static function role(): ?string {
|
||||
return $_SESSION['user_role'] ?? null;
|
||||
}
|
||||
|
||||
public static function is(string ...$roles): bool {
|
||||
return in_array(self::role(), $roles);
|
||||
}
|
||||
|
||||
public static function logout(): void {
|
||||
if (self::check()) {
|
||||
self::logActivity(
|
||||
$_SESSION['user_id'],
|
||||
$_SESSION['user_name'],
|
||||
$_SESSION['user_role'],
|
||||
'Logout', 'auth', 'Logout dari sistem'
|
||||
);
|
||||
}
|
||||
$_SESSION = [];
|
||||
session_destroy();
|
||||
self::$user = null;
|
||||
}
|
||||
|
||||
public static function require(string ...$roles): void {
|
||||
self::init();
|
||||
if (!self::check()) {
|
||||
header('Location: ' . APP_URL . '/login');
|
||||
exit;
|
||||
}
|
||||
if (!empty($roles) && !self::is(...$roles)) {
|
||||
header('Location: ' . APP_URL . '/forbidden');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
public static function redirectIfLoggedIn(): void {
|
||||
if (self::check()) {
|
||||
header('Location: ' . self::getDashboardUrl());
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getDashboardUrl(): string {
|
||||
return match(self::role()) {
|
||||
'superadmin' => APP_URL . '/admin/dashboard',
|
||||
'verifikator' => APP_URL . '/verifikator/dashboard',
|
||||
'rumah_ibadah' => APP_URL . '/rumah-ibadah/dashboard',
|
||||
'dinsos' => APP_URL . '/dinsos/dashboard',
|
||||
'walikota' => APP_URL . '/walikota/dashboard',
|
||||
default => APP_URL . '/'
|
||||
};
|
||||
}
|
||||
|
||||
public static function logActivity(int $userId, string $namaUser, string $role, string $aksi, string $modul, string $deskripsi = ''): void {
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$db->insert('aktivitas_sistem', [
|
||||
'id_user' => $userId,
|
||||
'nama_user' => $namaUser,
|
||||
'role' => $role,
|
||||
'aksi' => $aksi,
|
||||
'modul' => $modul,
|
||||
'deskripsi' => $deskripsi,
|
||||
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '',
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? ''
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
// Silent fail for logging
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
// core/Controller.php
|
||||
|
||||
class Controller {
|
||||
protected Database $db;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = Database::getInstance();
|
||||
}
|
||||
|
||||
protected function view(string $view, array $data = [], ?string $layout = 'main'): void {
|
||||
extract($data);
|
||||
$viewFile = APP_ROOT . '/app/views/' . str_replace('.', '/', $view) . '.php';
|
||||
|
||||
if (!file_exists($viewFile)) {
|
||||
die("View not found: $viewFile");
|
||||
}
|
||||
|
||||
if ($layout) {
|
||||
$layoutFile = APP_ROOT . '/app/views/layouts/' . $layout . '.php';
|
||||
if (file_exists($layoutFile)) {
|
||||
// Capture view content
|
||||
ob_start();
|
||||
require $viewFile;
|
||||
$content = ob_get_clean();
|
||||
require $layoutFile;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
require $viewFile;
|
||||
}
|
||||
|
||||
protected function json(mixed $data, int $code = 200): void {
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function redirect(string $url): void {
|
||||
// Flush session ke disk sebelum redirect agar data tidak hilang
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
session_write_close();
|
||||
}
|
||||
header("Location: $url");
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function back(): void {
|
||||
$referer = $_SERVER['HTTP_REFERER'] ?? APP_URL;
|
||||
$this->redirect($referer);
|
||||
}
|
||||
|
||||
protected function csrf(): string {
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return $_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
protected function verifyCsrf(): bool {
|
||||
// Pastikan session aktif sebelum membaca token
|
||||
Auth::init();
|
||||
$token = $_POST['_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
return hash_equals($_SESSION['csrf_token'] ?? '', $token);
|
||||
}
|
||||
|
||||
protected function input(string $key, mixed $default = null): mixed {
|
||||
return $_POST[$key] ?? $_GET[$key] ?? $default;
|
||||
}
|
||||
|
||||
protected function sanitize(string $value): string {
|
||||
return htmlspecialchars(trim($value), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
protected function flash(string $type, string $message, array $extra = []): void {
|
||||
$_SESSION['flash'] = ['type' => $type, 'message' => $message] + $extra;
|
||||
}
|
||||
|
||||
protected function getFlash(): ?array {
|
||||
if (isset($_SESSION['flash'])) {
|
||||
$flash = $_SESSION['flash'];
|
||||
unset($_SESSION['flash']);
|
||||
return $flash;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function uploadFile(array $file, string $dir = 'uploads'): ?string {
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) return null;
|
||||
|
||||
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||
$allow = ['jpg', 'jpeg', 'png', 'webp'];
|
||||
if (!in_array($ext, $allow)) return null;
|
||||
|
||||
$maxSize = 5 * 1024 * 1024; // 5MB
|
||||
if ($file['size'] > $maxSize) return null;
|
||||
|
||||
$filename = uniqid('img_', true) . '.' . $ext;
|
||||
$destDir = APP_ROOT . '/public/images/' . $dir . '/';
|
||||
if (!is_dir($destDir)) mkdir($destDir, 0755, true);
|
||||
|
||||
if (move_uploaded_file($file['tmp_name'], $destDir . $filename)) {
|
||||
return $filename;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function paginate(int $total, int $perPage = 20): array {
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$offset = ($page - 1) * $perPage;
|
||||
$pages = (int)ceil($total / $perPage);
|
||||
return compact('page', 'offset', 'perPage', 'pages', 'total');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
// core/Database.php
|
||||
|
||||
class Database {
|
||||
private static ?Database $instance = null;
|
||||
private PDO $pdo;
|
||||
|
||||
private function __construct() {
|
||||
$dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=' . DB_CHARSET;
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4"
|
||||
];
|
||||
try {
|
||||
$this->pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
|
||||
} catch (PDOException $e) {
|
||||
die(json_encode(['error' => 'Database connection failed: ' . $e->getMessage()]));
|
||||
}
|
||||
}
|
||||
|
||||
public static function getInstance(): Database {
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new Database();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function getConnection(): PDO {
|
||||
return $this->pdo;
|
||||
}
|
||||
|
||||
// Shorthand query
|
||||
public function query(string $sql, array $params = []): PDOStatement {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
public function fetchAll(string $sql, array $params = []): array {
|
||||
return $this->query($sql, $params)->fetchAll();
|
||||
}
|
||||
|
||||
public function fetchOne(string $sql, array $params = []): ?array {
|
||||
$row = $this->query($sql, $params)->fetch();
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
public function insert(string $table, array $data): int {
|
||||
$cols = implode(', ', array_keys($data));
|
||||
$placeholders = implode(', ', array_fill(0, count($data), '?'));
|
||||
$sql = "INSERT INTO `$table` ($cols) VALUES ($placeholders)";
|
||||
$this->query($sql, array_values($data));
|
||||
return (int)$this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
public function update(string $table, array $data, string $where, array $whereParams = []): int {
|
||||
$sets = implode(', ', array_map(fn($k) => "`$k` = ?", array_keys($data)));
|
||||
$sql = "UPDATE `$table` SET $sets WHERE $where";
|
||||
$stmt = $this->query($sql, array_merge(array_values($data), $whereParams));
|
||||
return $stmt->rowCount();
|
||||
}
|
||||
|
||||
public function delete(string $table, string $where, array $params = []): int {
|
||||
$stmt = $this->query("DELETE FROM `$table` WHERE $where", $params);
|
||||
return $stmt->rowCount();
|
||||
}
|
||||
|
||||
public function lastInsertId(): string {
|
||||
return $this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
// Prevent cloning
|
||||
private function __clone() {}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
// core/Router.php
|
||||
|
||||
class Router {
|
||||
private array $routes = [];
|
||||
|
||||
public function get(string $path, callable|array $handler): void {
|
||||
$this->routes['GET'][$path] = $handler;
|
||||
}
|
||||
|
||||
public function post(string $path, callable|array $handler): void {
|
||||
$this->routes['POST'][$path] = $handler;
|
||||
}
|
||||
|
||||
public function dispatch(): void {
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||
|
||||
// Strip base path
|
||||
$basePath = parse_url(APP_URL, PHP_URL_PATH);
|
||||
if ($basePath && str_starts_with($uri, $basePath)) {
|
||||
$uri = substr($uri, strlen($basePath));
|
||||
}
|
||||
$uri = '/' . ltrim($uri, '/');
|
||||
if ($uri !== '/' && str_ends_with($uri, '/')) {
|
||||
$uri = rtrim($uri, '/');
|
||||
}
|
||||
|
||||
$routes = $this->routes[$method] ?? [];
|
||||
|
||||
// Two-pass: static routes (no {param}) matched first, dynamic routes second.
|
||||
// This ensures /verifikator/warga/tambah is never swallowed by /verifikator/warga/{id}.
|
||||
$staticRoutes = array_filter($routes, fn($p) => !str_contains($p, '{'), ARRAY_FILTER_USE_KEY);
|
||||
$dynamicRoutes = array_filter($routes, fn($p) => str_contains($p, '{'), ARRAY_FILTER_USE_KEY);
|
||||
|
||||
foreach ([$staticRoutes, $dynamicRoutes] as $group) {
|
||||
foreach ($group as $pattern => $handler) {
|
||||
$regex = $this->buildRegex($pattern);
|
||||
if (preg_match($regex, $uri, $matches)) {
|
||||
array_shift($matches);
|
||||
$params = array_values($matches);
|
||||
|
||||
if (is_array($handler)) {
|
||||
[$controllerName, $action] = $handler;
|
||||
$controller = new $controllerName();
|
||||
$controller->$action(...$params);
|
||||
} else {
|
||||
$handler(...$params);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 404
|
||||
http_response_code(404);
|
||||
require APP_ROOT . '/app/views/public/404.php';
|
||||
}
|
||||
|
||||
private function buildRegex(string $pattern): string {
|
||||
$pattern = preg_replace('/\{(\w+)\}/', '([^/]+)', $pattern);
|
||||
return '#^' . $pattern . '$#';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
<?php
|
||||
// core/helpers.php
|
||||
|
||||
/**
|
||||
* Haversine Formula - menghitung jarak antara dua koordinat (meter)
|
||||
*/
|
||||
function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float {
|
||||
$R = 6371000; // radius bumi meter
|
||||
$phi1 = deg2rad($lat1);
|
||||
$phi2 = deg2rad($lat2);
|
||||
$dphi = deg2rad($lat2 - $lat1);
|
||||
$dlam = deg2rad($lng2 - $lng1);
|
||||
|
||||
$a = sin($dphi / 2) ** 2 + cos($phi1) * cos($phi2) * sin($dlam / 2) ** 2;
|
||||
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||
|
||||
return $R * $c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Algoritma Skor Prioritas Bantuan (0-100)
|
||||
* Semakin tinggi skor = semakin prioritas
|
||||
*/
|
||||
function hitungSkorPrioritas(array $warga): array {
|
||||
$skor = 0;
|
||||
|
||||
// 1. Penghasilan (30 poin)
|
||||
$penghasilan = (int)($warga['penghasilan_bulanan'] ?? 0);
|
||||
if ($penghasilan === 0) $skor += 30;
|
||||
elseif ($penghasilan <= 300000) $skor += 25;
|
||||
elseif ($penghasilan <= 600000) $skor += 18;
|
||||
elseif ($penghasilan <= 1000000) $skor += 10;
|
||||
else $skor += 3;
|
||||
|
||||
// 2. Jumlah tanggungan (20 poin)
|
||||
$tanggungan = (int)($warga['jumlah_tanggungan'] ?? 0);
|
||||
if ($tanggungan >= 6) $skor += 20;
|
||||
elseif ($tanggungan >= 4) $skor += 15;
|
||||
elseif ($tanggungan >= 2) $skor += 10;
|
||||
else $skor += 4;
|
||||
|
||||
// 3. Kondisi rumah (15 poin)
|
||||
$skor += match($warga['kondisi_rumah'] ?? '') {
|
||||
'Sangat Tidak Layak' => 15,
|
||||
'Tidak Layak' => 10,
|
||||
'Cukup Layak' => 5,
|
||||
'Layak' => 1,
|
||||
default => 5
|
||||
};
|
||||
|
||||
// 4. Pendidikan (10 poin)
|
||||
$skor += match($warga['pendidikan_terakhir'] ?? '') {
|
||||
'Tidak Sekolah' => 10,
|
||||
'SD' => 8,
|
||||
'SMP' => 6,
|
||||
'SMA' => 3,
|
||||
default => 1
|
||||
};
|
||||
|
||||
// 5. Riwayat penyakit (10 poin)
|
||||
if (!empty($warga['riwayat_penyakit'])) {
|
||||
$penyakit = strtolower($warga['riwayat_penyakit']);
|
||||
if (str_contains($penyakit, 'kanker') || str_contains($penyakit, 'gagal ginjal') || str_contains($penyakit, 'stroke')) {
|
||||
$skor += 10;
|
||||
} elseif (str_contains($penyakit, 'diabetes') || str_contains($penyakit, 'jantung') || str_contains($penyakit, 'tbc')) {
|
||||
$skor += 8;
|
||||
} else {
|
||||
$skor += 5;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Lansia (8 poin)
|
||||
if (!empty($warga['is_lansia'])) $skor += 8;
|
||||
|
||||
// 7. Disabilitas (7 poin)
|
||||
if (!empty($warga['is_disabilitas'])) $skor += 7;
|
||||
|
||||
$skor = min(100, $skor);
|
||||
|
||||
$level = match(true) {
|
||||
$skor >= 70 => 'Tinggi',
|
||||
$skor >= 40 => 'Sedang',
|
||||
default => 'Rendah'
|
||||
};
|
||||
|
||||
return ['skor' => $skor, 'level' => $level];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan status kemiskinan berdasarkan penghasilan dan tanggungan
|
||||
*/
|
||||
function tentukanStatusKemiskinan(int $penghasilan, int $tanggungan): string {
|
||||
$kapita = $tanggungan > 0 ? $penghasilan / $tanggungan : $penghasilan;
|
||||
if ($kapita <= 350000) return 'Miskin Ekstrem';
|
||||
if ($kapita <= 550000) return 'Miskin';
|
||||
return 'Rentan Miskin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate kode laporan: LPR-YYYYMMDD-XXXX
|
||||
*/
|
||||
function generateKodeLaporan(): string {
|
||||
$db = Database::getInstance();
|
||||
$today = date('Ymd');
|
||||
$count = $db->fetchOne("SELECT COUNT(*)+1 as n FROM laporan_masyarakat WHERE DATE(created_at) = CURDATE()")['n'];
|
||||
return 'LPR-' . $today . '-' . str_pad($count, 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate kode bantuan: BNT-YYYYMMDD-XXXX
|
||||
*/
|
||||
function generateKodeBantuan(): string {
|
||||
$db = Database::getInstance();
|
||||
$today = date('Ymd');
|
||||
$count = $db->fetchOne("SELECT COUNT(*)+1 as n FROM histori_bantuan WHERE DATE(created_at) = CURDATE()")['n'];
|
||||
return 'BNT-' . $today . '-' . str_pad($count, 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate password awal/reset yang mudah diketik tetapi tetap acak.
|
||||
*/
|
||||
function generatePasswordAkun(): string {
|
||||
return 'Sinergi@' . random_int(100000, 999999);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kirim kredensial akun via email jika mail server tersedia.
|
||||
* Mengembalikan false di instalasi lokal yang belum punya konfigurasi SMTP/sendmail.
|
||||
*/
|
||||
function kirimEmailAkun(string $email, string $nama, string $username, string $password, string $aksi = 'dibuat'): bool {
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$subject = 'Informasi Akun SINERGI';
|
||||
$message = "Yth. {$nama},\n\n"
|
||||
. "Akun SINERGI Anda telah {$aksi}.\n\n"
|
||||
. "URL Login: " . APP_URL . "/login\n"
|
||||
. "Username: {$username}\n"
|
||||
. "Password: {$password}\n\n"
|
||||
. "Silakan masuk ke sistem dan simpan kredensial ini dengan aman.\n\n"
|
||||
. "Salam,\nAdmin SINERGI";
|
||||
|
||||
$headers = [
|
||||
'From: SINERGI <noreply@sinergi.local>',
|
||||
'Content-Type: text/plain; charset=UTF-8'
|
||||
];
|
||||
|
||||
return @mail($email, $subject, $message, implode("\r\n", $headers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Temukan rumah ibadah terdekat berdasarkan Haversine
|
||||
*/
|
||||
function findRumahIbadahTerdekat(float $lat, float $lng): ?array {
|
||||
$db = Database::getInstance();
|
||||
$rumahIbadahs = $db->fetchAll(
|
||||
"SELECT ri.*, k.nama as kelurahan FROM rumah_ibadah ri
|
||||
JOIN kelurahan k ON ri.id_kelurahan = k.id
|
||||
WHERE ri.is_active = 1"
|
||||
);
|
||||
|
||||
$nearest = null;
|
||||
$minDist = PHP_FLOAT_MAX;
|
||||
|
||||
foreach ($rumahIbadahs as $ri) {
|
||||
$dist = haversine($lat, $lng, (float)$ri['lat'], (float)$ri['lng']);
|
||||
if ($dist < $minDist) {
|
||||
$minDist = $dist;
|
||||
$nearest = $ri;
|
||||
$nearest['jarak_meter'] = round($dist);
|
||||
}
|
||||
}
|
||||
return $nearest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah titik berada dalam radius salah satu rumah ibadah
|
||||
*/
|
||||
function isInBlindSpot(float $lat, float $lng): bool {
|
||||
$db = Database::getInstance();
|
||||
$rumahIbadahs = $db->fetchAll("SELECT lat, lng, radius_meter FROM rumah_ibadah WHERE is_active = 1");
|
||||
foreach ($rumahIbadahs as $ri) {
|
||||
$dist = haversine($lat, $lng, (float)$ri['lat'], (float)$ri['lng']);
|
||||
if ($dist <= (float)$ri['radius_meter']) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format rupiah
|
||||
*/
|
||||
function formatRupiah(int $amount): string {
|
||||
return 'Rp ' . number_format($amount, 0, ',', '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format tanggal Indonesia
|
||||
*/
|
||||
function formatTanggal(string $date, bool $withTime = false): string {
|
||||
if (empty($date) || $date === '0000-00-00') return '-';
|
||||
$bulan = ['','Januari','Februari','Maret','April','Mei','Juni','Juli','Agustus','September','Oktober','November','Desember'];
|
||||
$d = new DateTime($date);
|
||||
$str = $d->format('j') . ' ' . $bulan[(int)$d->format('n')] . ' ' . $d->format('Y');
|
||||
if ($withTime) $str .= ' ' . $d->format('H:i');
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hitung umur
|
||||
*/
|
||||
function hitungUmur(string $tanggalLahir): int {
|
||||
if (empty($tanggalLahir)) return 0;
|
||||
return (new DateTime($tanggalLahir))->diff(new DateTime())->y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Warna marker berdasarkan status kemiskinan
|
||||
*/
|
||||
function warnaMiskin(string $status): string {
|
||||
return match($status) {
|
||||
'Miskin Ekstrem' => '#dc2626',
|
||||
'Miskin' => '#ea580c',
|
||||
'Rentan Miskin' => '#ca8a04',
|
||||
default => '#6b7280'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Badge HTML status
|
||||
*/
|
||||
function statusBadge(string $status): string {
|
||||
$map = [
|
||||
'menunggu' => ['bg-yellow-100 text-yellow-800', 'Menunggu'],
|
||||
'diproses' => ['bg-blue-100 text-blue-800', 'Diproses'],
|
||||
'terverifikasi'=> ['bg-green-100 text-green-800', 'Terverifikasi'],
|
||||
'ditolak' => ['bg-red-100 text-red-800', 'Ditolak'],
|
||||
'aktif' => ['bg-green-100 text-green-800', 'Aktif'],
|
||||
'nonaktif' => ['bg-gray-100 text-gray-600', 'Nonaktif'],
|
||||
'Tinggi' => ['bg-red-100 text-red-800', 'Tinggi'],
|
||||
'Sedang' => ['bg-yellow-100 text-yellow-800', 'Sedang'],
|
||||
'Rendah' => ['bg-green-100 text-green-800', 'Rendah'],
|
||||
'disalurkan' => ['bg-green-100 text-green-800', 'Disalurkan'],
|
||||
'rencana' => ['bg-blue-100 text-blue-800', 'Rencana'],
|
||||
'dibatalkan' => ['bg-red-100 text-red-800', 'Dibatalkan'],
|
||||
];
|
||||
[$cls, $label] = $map[$status] ?? ['bg-gray-100 text-gray-600', $status];
|
||||
return "<span class=\"badge $cls\">$label</span>";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSRF hidden input
|
||||
*/
|
||||
function csrfField(): string {
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return '<input type="hidden" name="_token" value="' . htmlspecialchars($_SESSION['csrf_token']) . '">';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set flash message ke session.
|
||||
* Gunakan fungsi ini (bukan tulis langsung ke $_SESSION['flash']) agar
|
||||
* kode_laporan / kode_bantuan disimpan sebagai key terpisah — bukan
|
||||
* ditempel ke dalam string pesan — sehingga tidak ikut terhapus saat
|
||||
* flashMessage() dipanggil.
|
||||
*
|
||||
* Contoh penggunaan:
|
||||
* setFlash('success', 'Laporan berhasil disimpan.', ['kode_laporan' => $kode]);
|
||||
* setFlash('error', 'Terjadi kesalahan.');
|
||||
*/
|
||||
function setFlash(string $type, string $message, array $extra = []): void {
|
||||
$_SESSION['flash'] = array_merge(['type' => $type, 'message' => $message], $extra);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flash message display.
|
||||
*
|
||||
* FIX Bug 1 — unset dilakukan SETELAH semua data flash dibaca, bukan di
|
||||
* tengah-tengah, sehingga kode_laporan/kode_bantuan yang
|
||||
* mungkin dipakai lapor.php masih tersedia selama request ini.
|
||||
*
|
||||
* FIX Bug 2 — pesan flash boleh mengandung <strong> dan <br> (supaya kode
|
||||
* laporan bisa ditebalkan), tapi semua tag lain tetap di-escape
|
||||
* agar aman dari XSS.
|
||||
*
|
||||
* Jika flash mengandung 'kode_laporan' atau 'kode_bantuan', nilainya
|
||||
* otomatis disisipkan ke baris kedua pesan dalam tag <strong>.
|
||||
*/
|
||||
function flashMessage(): string {
|
||||
if (empty($_SESSION['flash'])) return '';
|
||||
|
||||
// Baca dulu seluruh data — JANGAN unset sebelum semua field diambil
|
||||
$flash = $_SESSION['flash'];
|
||||
unset($_SESSION['flash']); // hapus sekarang, setelah semua field tersalin ke $flash
|
||||
|
||||
$type = $flash['type'] ?? 'info';
|
||||
$message = $flash['message'] ?? '';
|
||||
|
||||
// Sisipkan kode laporan / bantuan sebagai baris ke-2 jika ada
|
||||
if (!empty($flash['kode_laporan'])) {
|
||||
$kode = htmlspecialchars($flash['kode_laporan'], ENT_QUOTES, 'UTF-8');
|
||||
$message .= '<br>Kode Laporan: <strong>' . $kode . '</strong>';
|
||||
} elseif (!empty($flash['kode_bantuan'])) {
|
||||
$kode = htmlspecialchars($flash['kode_bantuan'], ENT_QUOTES, 'UTF-8');
|
||||
$message .= '<br>Kode Bantuan: <strong>' . $kode . '</strong>';
|
||||
}
|
||||
|
||||
// Escape pesan utama — lalu kembalikan <strong> dan <br> yang sudah kita sisipkan
|
||||
// Alur: escape semua → unescape hanya tag yang kita izinkan
|
||||
$safe = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
|
||||
$safe = str_replace(
|
||||
['<strong>', '</strong>', '<br>'],
|
||||
['<strong>', '</strong>', '<br>'],
|
||||
$safe
|
||||
);
|
||||
|
||||
$icon = match($type) {
|
||||
'success' => '✓',
|
||||
'error' => '✕',
|
||||
'warning' => '⚠',
|
||||
default => 'ℹ',
|
||||
};
|
||||
$cls = match($type) {
|
||||
'success' => 'flash-success',
|
||||
'error' => 'flash-error',
|
||||
'warning' => 'flash-warning',
|
||||
default => 'flash-info',
|
||||
};
|
||||
|
||||
return "<div class=\"flash-message $cls\"><span>$icon</span><span>$safe</span></div>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML
|
||||
*/
|
||||
function e(string $str): string {
|
||||
return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset URL
|
||||
*/
|
||||
function asset(string $path): string {
|
||||
return APP_URL . '/public/' . ltrim($path, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload URL
|
||||
*/
|
||||
function uploadUrl(string $filename): string {
|
||||
if (empty($filename)) return asset('images/placeholder.png');
|
||||
return APP_URL . '/public/images/uploads/' . $filename;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
-- ============================================================
|
||||
-- SINERGI - Data Dummy untuk Pengujian
|
||||
-- Jalankan SETELAH sinergi.sql
|
||||
-- ============================================================
|
||||
|
||||
USE `sinergi_db`;
|
||||
|
||||
-- ============================================================
|
||||
-- Rumah Ibadah (30 data)
|
||||
-- ============================================================
|
||||
INSERT INTO `users` (`id_role`,`id_kelurahan`,`nama`,`username`,`email`,`password`,`no_hp`,`is_active`) VALUES
|
||||
(3,1,'Ustadz Hamid Ismail','ri_masjid_darussalam','hamid@sinergi.id','$2y$12$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi','081200000010',1),
|
||||
(3,2,'Pdt. Yohannes Kalvin','ri_gereja_bethel','yohannes@sinergi.id','$2y$12$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi','081200000011',1),
|
||||
(3,5,'Ustadz Ridwan','ri_masjid_alfalah','ridwan@sinergi.id','$2y$12$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi','081200000012',1),
|
||||
(3,9,'Ustadz Mukhlis','ri_masjid_almuhajirin','mukhlis@sinergi.id','$2y$12$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi','081200000013',1),
|
||||
(3,13,'Bhikkhu Suriyavamsa','ri_vihara_bodhi','suriyavamsa@sinergi.id','$2y$12$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi','081200000014',1),
|
||||
(3,17,'Ustadz Zainul','ri_masjid_taqwa','zainul@sinergi.id','$2y$12$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi','081200000015',1),
|
||||
(3,21,'Ustadz Hasan','ri_masjid_siantan','hasan@sinergi.id','$2y$12$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi','081200000016',1);
|
||||
|
||||
INSERT INTO `rumah_ibadah` (`id_user`,`id_kelurahan`,`id_jenis`,`nama`,`alamat`,`lat`,`lng`,`radius_meter`,`nama_ketua`,`is_active`) VALUES
|
||||
-- Pontianak Kota
|
||||
(6,1,1,'Masjid Darussalam','Jl. Darussalam No.1, Darat Sekip',-0.0170,109.3220,600,'Ustadz Hamid Ismail',1),
|
||||
(7,2,2,'Gereja Bethel Indonesia','Jl. Nusantara No.5, Sungai Bangkong',-0.0295,109.3315,500,'Pdt. Yohannes Kalvin',1),
|
||||
(NULL,3,1,'Masjid Al-Ikhlas','Jl. Tengah No.10',-0.0215,109.3295,550,NULL,1),
|
||||
(NULL,4,3,'Vihara Saddharma','Jl. Tanjung No.8, Mariana',-0.0158,109.3358,450,NULL,1),
|
||||
-- Pontianak Barat
|
||||
(8,5,1,'Masjid Al-Falah','Jl. Sungai Jawi No.22',-0.0462,109.3055,600,'Ustadz Ridwan',1),
|
||||
(NULL,6,2,'Gereja HKBP Sungai Jawi','Jl. Pendidikan No.4, Sungai Jawi Luar',-0.0535,109.3010,500,NULL,1),
|
||||
(NULL,7,1,'Masjid Nurul Huda','Jl. Pal Lima No.15',-0.0598,109.2908,550,NULL,1),
|
||||
(NULL,8,5,'Klenteng Kwan Ti Miao','Jl. Gajah Mada No.100, Sungai Beliung',-0.0448,109.3158,400,NULL,1),
|
||||
-- Pontianak Selatan
|
||||
(9,9,1,'Masjid Al-Muhajirin','Jl. Akcaya No.30',-0.0648,109.3382,600,'Ustadz Mukhlis',1),
|
||||
(NULL,10,2,'Gereja Katolik St. Yosep','Jl. Bangka No.12',-0.0752,109.3425,500,NULL,1),
|
||||
(NULL,11,1,'Masjid Baitul Muttaqin','Jl. Parit No.7, Bangka Belitung Laut',-0.0818,109.3352,550,NULL,1),
|
||||
(NULL,12,3,'Vihara Singa Raja','Jl. Tokaya No.3',-0.0702,109.3305,450,NULL,1),
|
||||
-- Pontianak Tenggara
|
||||
(10,13,3,'Vihara Bodhi Dharma','Jl. Bansir No.5',-0.0872,109.3552,500,'Bhikkhu Suriyavamsa',1),
|
||||
(NULL,14,1,'Masjid Al-Muttaqin','Jl. Bansir Laut No.18',-0.0952,109.3602,600,NULL,1),
|
||||
(NULL,15,2,'Gereja Pentakosta Tanjung Hulu','Jl. Tanjung No.20',-0.0918,109.3705,500,NULL,1),
|
||||
(NULL,16,1,'Masjid Nurul Iman','Jl. Tanjung Hilir No.9',-0.0852,109.3652,550,NULL,1),
|
||||
-- Pontianak Timur
|
||||
(11,17,1,'Masjid At-Taqwa','Jl. Saigon No.11',-0.0382,109.3702,600,'Ustadz Zainul',1),
|
||||
(NULL,18,2,'Gereja GKE Tambelan','Jl. Tambelan No.6',-0.0448,109.3752,500,NULL,1),
|
||||
(NULL,19,1,'Masjid Nurul Hidayah','Jl. Tanjung Raya No.33',-0.0318,109.3802,550,NULL,1),
|
||||
(NULL,20,5,'Klenteng Tri Dharma','Jl. Parit Mayor No.2',-0.0498,109.3682,400,NULL,1),
|
||||
-- Pontianak Utara
|
||||
(12,21,1,'Masjid Raya Siantan','Jl. Siantan No.1',-0.0198,109.3252,700,'Ustadz Hasan',1),
|
||||
(NULL,22,2,'Gereja Sidang Jemaat Allah','Jl. Siantan Tengah No.8',0.0102,109.3205,500,NULL,1),
|
||||
(NULL,23,1,'Masjid Al-Amin','Jl. Siantan Hilir No.15',0.0052,109.3302,600,NULL,1),
|
||||
(NULL,24,3,'Vihara Metta Karuna','Jl. Batu Layang No.20',0.0298,109.3352,450,NULL,1);
|
||||
|
||||
-- ============================================================
|
||||
-- Warga Miskin (50 data dummy)
|
||||
-- ============================================================
|
||||
INSERT INTO `warga_miskin`
|
||||
(`id_kelurahan`,`id_rumah_ibadah`,`id_verifikator`,`nik`,`nama`,`tempat_lahir`,`tanggal_lahir`,`jenis_kelamin`,`agama`,`alamat`,`lat`,`lng`,`pendidikan_terakhir`,`pekerjaan`,`penghasilan_bulanan`,`jumlah_tanggungan`,`kondisi_rumah`,`riwayat_penyakit`,`is_lansia`,`is_disabilitas`,`status_kemiskinan`,`skor_prioritas`,`level_prioritas`,`is_blind_spot`,`status`,`tanggal_verifikasi`)
|
||||
VALUES
|
||||
(1,1,2,'6271010101850001','Ahmad Firdaus','Pontianak','1985-01-15','L','Islam','Jl. Darussalam Gg. 3 No. 12',-0.0180,109.3225,'SD','Buruh Harian',400000,4,'Tidak Layak','Hipertensi',0,0,'Miskin',72,'Tinggi',0,'aktif','2024-01-10'),
|
||||
(1,1,2,'6271010101900002','Siti Rahmah','Pontianak','1990-05-20','P','Islam','Jl. Darussalam Gg. 5 No. 8',-0.0175,109.3218,'SMP','Pembantu RT',500000,3,'Tidak Layak',NULL,0,0,'Miskin',58,'Sedang',0,'aktif','2024-01-12'),
|
||||
(2,2,2,'6271010201750003','Maria Lestari','Pontianak','1975-08-30','P','Kristen','Jl. Nusantara No. 45',-0.0288,109.3320,'SD','Pedagang Kecil',350000,5,'Sangat Tidak Layak','Diabetes, Hipertensi',1,0,'Miskin Ekstrem',95,'Tinggi',0,'aktif','2024-01-08'),
|
||||
(2,2,2,'6271010201550004','Yohanes Subur','Pontianak','1955-03-12','L','Kristen','Jl. Bangkong No. 7',-0.0298,109.3308,'Tidak Sekolah',NULL,0,6,'Sangat Tidak Layak','Stroke',1,1,'Miskin Ekstrem',98,'Tinggi',0,'aktif','2024-01-08'),
|
||||
(3,3,2,'6271010301880005','Heri Susanto','Bandung','1988-11-22','L','Islam','Jl. Tengah Gg. 1 No. 3',-0.0220,109.3290,'SMP','Ojek Online',800000,3,'Cukup Layak',NULL,0,0,'Rentan Miskin',35,'Rendah',0,'aktif','2024-01-15'),
|
||||
(5,5,3,'6271020501920006','Nurhalimah','Pontianak','1992-07-04','P','Islam','Jl. Sungai Jawi No. 66',-0.0468,109.3058,'SMA','Tidak Bekerja',200000,4,'Tidak Layak','TBC',0,0,'Miskin Ekstrem',88,'Tinggi',0,'aktif','2024-02-01'),
|
||||
(5,5,3,'6271020501800007','Bambang Hartono','Pontianak','1980-09-15','L','Islam','Jl. Sungai Jawi No. 70',-0.0470,109.3060,'SD','Pemulung',300000,5,'Sangat Tidak Layak',NULL,0,1,'Miskin Ekstrem',92,'Tinggi',0,'aktif','2024-02-01'),
|
||||
(6,6,3,'6271020601850008','Agustina Br Sirait','Medan','1985-04-20','P','Kristen','Jl. Pendidikan No. 22',-0.0538,109.3012,'SMP','Buruh Cuci',450000,3,'Tidak Layak',NULL,0,0,'Miskin',55,'Sedang',0,'aktif','2024-02-05'),
|
||||
(7,7,3,'6271020701960009','Rohani','Pontianak','1996-12-01','P','Islam','Jl. Pal Lima No. 18',-0.0600,109.2912,'SMA','Tidak Bekerja',150000,2,'Tidak Layak','Gangguan Jiwa',0,1,'Miskin Ekstrem',90,'Tinggi',0,'aktif','2024-02-10'),
|
||||
(9,9,2,'6271030901780010','Musa Kadir','Pontianak','1978-06-08','L','Islam','Jl. Akcaya No. 15',-0.0652,109.3378,'SD','Kuli Bangunan',600000,4,'Tidak Layak',NULL,0,0,'Miskin',62,'Sedang',0,'aktif','2024-02-12'),
|
||||
(9,9,2,'6271030901720011','Fatimah Zahra','Pontianak','1972-02-14','P','Islam','Jl. Akcaya No. 17',-0.0655,109.3380,'Tidak Sekolah',NULL,0,3,'Sangat Tidak Layak','Jantung',1,0,'Miskin Ekstrem',93,'Tinggi',0,'aktif','2024-02-12'),
|
||||
(10,10,2,'6271031001820012','Antonius Jaya','Pontianak','1982-10-30','L','Katolik','Jl. Bangka No. 28',-0.0755,109.3428,'SMP','Nelayan',500000,4,'Tidak Layak',NULL,0,0,'Miskin',60,'Sedang',0,'aktif','2024-02-15'),
|
||||
(11,11,2,'6271031101900013','Rahma Wati','Pontianak','1990-03-25','P','Islam','Jl. Parit Laut No. 5',-0.0820,109.3355,'SD','Penjual Gorengan',350000,5,'Tidak Layak',NULL,0,0,'Miskin',70,'Tinggi',0,'aktif','2024-02-18'),
|
||||
(13,13,2,'6271041301750014','Lim Ah Kow','Pontianak','1975-07-07','L','Buddha','Jl. Bansir No. 12',-0.0875,109.3555,'SD','Tukang Becak',400000,4,'Tidak Layak','Asma',0,0,'Miskin',68,'Sedang',0,'aktif','2024-03-01'),
|
||||
(14,14,2,'6271041401850015','Abdullah Sani','Pontianak','1985-11-18','L','Islam','Jl. Bansir Laut No. 25',-0.0955,109.3605,'SMP','Tidak Bekerja',0,5,'Sangat Tidak Layak',NULL,0,1,'Miskin Ekstrem',95,'Tinggi',0,'aktif','2024-03-05'),
|
||||
(17,17,2,'6271051701880016','Yuliana','Pontianak','1988-08-12','P','Islam','Jl. Saigon No. 20',-0.0385,109.3705,'SMP','Buruh Pabrik',600000,3,'Cukup Layak',NULL,0,0,'Rentan Miskin',42,'Rendah',0,'aktif','2024-03-08'),
|
||||
(17,17,2,'6271051701600017','Pak Suwarno','Pontianak','1960-01-01','L','Islam','Jl. Saigon No. 25',-0.0388,109.3708,'SD','Tidak Bekerja',0,2,'Sangat Tidak Layak','Diabetes, Gagal Ginjal',1,0,'Miskin Ekstrem',97,'Tinggi',0,'aktif','2024-03-08'),
|
||||
(21,21,2,'6271061501700018','Ibrahim','Pontianak','1970-05-05','L','Islam','Jl. Siantan No. 40',0.0202,109.3255,'SD','Tukang Kayu',500000,4,'Tidak Layak',NULL,0,0,'Miskin',58,'Sedang',0,'aktif','2024-03-10'),
|
||||
(22,22,2,'6271062201800019','Dorkas Br Manurung','Medan','1980-12-25','P','Kristen','Jl. Siantan Tengah No. 15',0.0105,109.3208,'SMP','Pedagang',400000,3,'Tidak Layak',NULL,0,0,'Miskin',55,'Sedang',0,'aktif','2024-03-12'),
|
||||
-- Blind Spot samples (tidak masuk dalam radius manapun)
|
||||
(3,NULL,2,'6271010301850020','Surya Pratama','Pontianak','1985-07-17','L','Islam','Jl. Pinggiran Sungai No. 5',-0.0250,109.3400,'SMP','Nelayan',350000,4,'Tidak Layak',NULL,0,0,'Miskin',65,'Sedang',1,'aktif','2024-04-01'),
|
||||
(8,NULL,3,'6271020801920021','Tan Siu Liang','Pontianak','1992-03-08','L','Buddha','Jl. Beliung Bawah No. 2',-0.0455,109.3200,'SD','Pemulung',250000,3,'Sangat Tidak Layak',NULL,0,0,'Miskin Ekstrem',82,'Tinggi',1,'aktif','2024-04-05'),
|
||||
-- More regular entries
|
||||
(4,4,2,'6271010401960022','Dewi Kartika','Pontianak','1996-06-14','P','Buddha','Jl. Mariana No. 5',-0.0160,109.3360,'SMA','Tidak Bekerja',0,2,'Tidak Layak',NULL,0,0,'Miskin',72,'Tinggi',0,'aktif','2024-04-08'),
|
||||
(6,6,3,'6271020601900023','Agus Salim','Pontianak','1990-09-09','L','Islam','Jl. Jawi Luar No. 30',-0.0532,109.3005,'SMP','Buruh Harian',450000,3,'Tidak Layak',NULL,0,0,'Miskin',60,'Sedang',0,'aktif','2024-04-10'),
|
||||
(12,12,2,'6271031201830024','Tan Ah Moi','Pontianak','1983-02-28','P','Buddha','Jl. Tokaya No. 10',-0.0705,109.3308,'SD','Pedagang Sayur',300000,4,'Tidak Layak','Reumatik',0,0,'Miskin',68,'Sedang',0,'aktif','2024-04-12'),
|
||||
(15,15,2,'6271041501780025','Hendrikus','Pontianak','1978-11-11','L','Katolik','Jl. Tanjung No. 35',-0.0920,109.3708,'SD','Nelayan',400000,5,'Tidak Layak',NULL,0,0,'Miskin',65,'Sedang',0,'aktif','2024-04-15'),
|
||||
(16,16,2,'6271041601920026','Aminah','Pontianak','1992-04-04','P','Islam','Jl. Hilir No. 12',-0.0855,109.3655,'SMP','Pembantu RT',400000,3,'Tidak Layak',NULL,0,0,'Miskin',55,'Sedang',0,'aktif','2024-04-18'),
|
||||
(18,18,2,'6271051801850027','Tarigan Boru','Pontianak','1985-08-08','P','Kristen','Jl. Tambelan No. 18',-0.0450,109.3755,'SMP','Pembantu RT',350000,2,'Tidak Layak',NULL,0,0,'Miskin',52,'Sedang',0,'aktif','2024-04-20'),
|
||||
(19,19,2,'6271051901770028','Joko Widodo','Pontianak','1977-06-21','L','Islam','Jl. Tanjung Raya No. 44',-0.0320,109.3805,'SD','Kuli Angkut',450000,4,'Tidak Layak',NULL,0,0,'Miskin',62,'Sedang',0,'aktif','2024-04-22'),
|
||||
(20,20,2,'6271052001880029','Halim Wijaya','Pontianak','1988-03-15','L','Buddha','Jl. Mayor No. 8',-0.0500,109.3685,'SMP','Tukang Parkir',300000,3,'Tidak Layak',NULL,0,0,'Miskin',58,'Sedang',0,'aktif','2024-04-25'),
|
||||
(23,23,2,'6271062301600030','Pak Hamdan','Pontianak','1960-07-07','L','Islam','Jl. Hilir Siantan No. 22',0.0055,109.3305,'Tidak Sekolah',NULL,0,3,'Sangat Tidak Layak','TBC, Hipertensi',1,0,'Miskin Ekstrem',96,'Tinggi',0,'aktif','2024-04-28');
|
||||
|
||||
-- ============================================================
|
||||
-- Histori Bantuan (30 data dummy)
|
||||
-- ============================================================
|
||||
INSERT INTO `histori_bantuan`
|
||||
(`id_warga`,`id_rumah_ibadah`,`id_kategori_bantuan`,`id_user_pengurus`,`kode_bantuan`,`tanggal_penyaluran`,`jumlah`,`satuan`,`deskripsi`,`status`)
|
||||
VALUES
|
||||
(1,1,1,6,'BNT-20240115-0001','2024-01-15',1,'paket','Sembako bulanan Jan 2024','disalurkan'),
|
||||
(2,1,1,6,'BNT-20240115-0002','2024-01-15',1,'paket','Sembako bulanan Jan 2024','disalurkan'),
|
||||
(3,2,1,7,'BNT-20240120-0003','2024-01-20',1,'paket','Bantuan sembako','disalurkan'),
|
||||
(3,2,4,7,'BNT-20240120-0004','2024-01-20',1,'paket','Obat diabetes dan hipertensi','disalurkan'),
|
||||
(4,2,3,7,'BNT-20240125-0005','2024-01-25',300000,'rupiah','Bantuan tunai bulanan','disalurkan'),
|
||||
(6,5,1,8,'BNT-20240201-0006','2024-02-01',1,'paket','Sembako bulanan Feb 2024','disalurkan'),
|
||||
(6,5,4,8,'BNT-20240201-0007','2024-02-01',1,'paket','Obat TBC','disalurkan'),
|
||||
(7,5,1,8,'BNT-20240201-0008','2024-02-01',1,'paket','Sembako bulanan','disalurkan'),
|
||||
(7,5,3,8,'BNT-20240210-0009','2024-02-10',500000,'rupiah','Bantuan tunai mendesak','disalurkan'),
|
||||
(9,7,1,8,'BNT-20240215-0010','2024-02-15',1,'paket','Sembako untuk keluarga Rohani','disalurkan'),
|
||||
(10,9,1,9,'BNT-20240220-0011','2024-02-20',1,'paket','Sembako Feb 2024','disalurkan'),
|
||||
(11,9,3,9,'BNT-20240220-0012','2024-02-20',400000,'rupiah','Bantuan tunai lansia','disalurkan'),
|
||||
(11,9,4,9,'BNT-20240225-0013','2024-02-25',1,'paket','Obat jantung','disalurkan'),
|
||||
(14,13,1,10,'BNT-20240301-0014','2024-03-01',1,'paket','Sembako Maret 2024','disalurkan'),
|
||||
(15,14,1,9,'BNT-20240305-0015','2024-03-05',1,'paket','Sembako penyandang disabilitas','disalurkan'),
|
||||
(15,14,3,9,'BNT-20240305-0016','2024-03-05',500000,'rupiah','Bantuan tunai mendesak','disalurkan'),
|
||||
(17,17,4,11,'BNT-20240310-0017','2024-03-10',1,'paket','Obat diabetes dan ginjal','disalurkan'),
|
||||
(17,17,3,11,'BNT-20240310-0018','2024-03-10',500000,'rupiah','Bantuan tunai pak Suwarno','disalurkan'),
|
||||
(1,1,1,6,'BNT-20240215-0019','2024-02-15',1,'paket','Sembako bulanan Feb 2024','disalurkan'),
|
||||
(2,1,2,6,'BNT-20240215-0020','2024-02-15',1,'paket','Bantuan pakaian','disalurkan'),
|
||||
(3,2,1,7,'BNT-20240220-0021','2024-02-20',1,'paket','Sembako bulanan Feb 2024','disalurkan'),
|
||||
(6,5,1,8,'BNT-20240301-0022','2024-03-01',1,'paket','Sembako Maret 2024','disalurkan'),
|
||||
(10,9,2,9,'BNT-20240310-0023','2024-03-10',1,'paket','Bantuan pakaian anak','disalurkan'),
|
||||
(13,11,1,9,'BNT-20240315-0024','2024-03-15',1,'paket','Sembako untuk Rahma','disalurkan'),
|
||||
(18,21,1,12,'BNT-20240315-0025','2024-03-15',1,'paket','Sembako Ibrahim','disalurkan'),
|
||||
(30,23,3,12,'BNT-20240430-0026','2024-04-30',400000,'rupiah','Bantuan tunai pak Hamdan','disalurkan'),
|
||||
(1,1,5,6,'BNT-20240401-0027','2024-04-01',1,'paket','Peralatan sekolah anak Ahmad','disalurkan'),
|
||||
(4,2,1,7,'BNT-20240415-0028','2024-04-15',1,'paket','Sembako April 2024','disalurkan'),
|
||||
(7,5,1,8,'BNT-20240401-0029','2024-04-01',1,'paket','Sembako April Bambang','disalurkan'),
|
||||
(17,17,1,11,'BNT-20240401-0030','2024-04-01',1,'paket','Sembako April Pak Suwarno','disalurkan');
|
||||
|
||||
-- ============================================================
|
||||
-- Laporan Masyarakat (10 data dummy)
|
||||
-- ============================================================
|
||||
INSERT INTO `laporan_masyarakat`
|
||||
(`kode_laporan`,`nama_pelapor`,`no_hp_pelapor`,`nama_warga`,`alamat_warga`,`id_kelurahan`,`lat`,`lng`,`deskripsi`,`status`,`catatan_verifikator`,`id_verifikator`,`tanggal_verifikasi`)
|
||||
VALUES
|
||||
('LPR-20240501-0001','Rizky Pratama','08190001111','Pak Udin','Jl. Darussalam Gg. 7 No. 3',1,-0.0183,109.3228,'Bapak ini sudah lama tidak bekerja, memiliki 3 anak yang masih kecil dan tinggal di rumah yang hampir roboh.','terverifikasi','Data valid, sudah survei lapangan',2,'2024-05-03'),
|
||||
('LPR-20240502-0002','Susi Lestari','08190002222','Ibu Sari','Jl. Bangkong No. 32',2,-0.0300,109.3312,'Ibu ini janda dengan 4 anak, hidup dari berjualan kue keliling. Penghasilan sangat minim.','terverifikasi','Terverifikasi, perlu prioritas',2,'2024-05-05'),
|
||||
('LPR-20240503-0003','Andi Wijaya','08190003333','Pak Badu','Jl. Pal Lima No. 40',7,-0.0602,109.2915,'Lansia sebatang kara, tidak ada yang merawat.','diproses',NULL,NULL,NULL),
|
||||
('LPR-20240504-0004','Tono Hartono','08190004444','Keluarga Rudi','Jl. Sungai Jawi No. 85',5,-0.0472,109.3062,'Keluarga ini sangat membutuhkan bantuan, rumah bocor dan tidak layak.','menunggu',NULL,NULL,NULL),
|
||||
('LPR-20240505-0005','Linda Susanti','08190005555','Pak Wahid','Jl. Akcaya No. 45',9,-0.0660,109.3385,'Suami sakit stroke, istri harus menghidupi 3 anak sendiri.','terverifikasi','Valid, prioritas tinggi',2,'2024-05-08'),
|
||||
('LPR-20240506-0006','Hendra','08190006666','Bu Aminah','Jl. Bansir No. 30',13,-0.0878,109.3558,'Keluarga miskin ekstrem, anak-anak putus sekolah.','menunggu',NULL,NULL,NULL),
|
||||
('LPR-20240507-0007','Ratna Sari','08190007777','Pak Syarif','Jl. Saigon No. 40',17,-0.0390,109.3710,'Penyandang disabilitas, tidak bisa bekerja, tidak ada keluarga.','terverifikasi','Penyandang disabilitas berat',2,'2024-05-10'),
|
||||
('LPR-20240508-0008','Irwan','08190008888','Bu Marsini','Jl. Siantan No. 55',21,0.0205,109.3260,'Ibu tua sebatang kara, hidup dari belas kasihan tetangga.','menunggu',NULL,NULL,NULL),
|
||||
('LPR-20240509-0009','Yayuk','08190009999','Pak Karso','Jl. Pendidikan No. 30',6,-0.0540,109.3015,'Buruh harian yang terkena PHK, punya 5 anak.','ditolak','Data tidak lengkap, perlu verifikasi ulang',3,'2024-05-12'),
|
||||
('LPR-20240510-0010','Bambang S','08190000000','Nenek Sari','Jl. Siantan Tengah No. 25',22,0.0108,109.3212,'Nenek berusia 80 tahun, hidup sendirian, tidak ada penghasilan.','diproses',NULL,NULL,NULL);
|
||||
|
||||
-- ============================================================
|
||||
-- Aktivitas Sistem
|
||||
-- ============================================================
|
||||
INSERT INTO `aktivitas_sistem` (`id_user`,`nama_user`,`role`,`aksi`,`modul`,`deskripsi`,`ip_address`) VALUES
|
||||
(2,'Budi Santoso','verifikator','Verifikasi Laporan','verifikasi','Verifikasi laporan LPR-20240501-0001','192.168.1.10'),
|
||||
(2,'Budi Santoso','verifikator','Tambah Warga','warga_miskin','Menambah data warga Ahmad Firdaus','192.168.1.10'),
|
||||
(6,'Ustadz Hamid Ismail','rumah_ibadah','Salurkan Bantuan','bantuan','Menyalurkan sembako ke Ahmad Firdaus','192.168.1.20'),
|
||||
(1,'Super Administrator','superadmin','Login','auth','Login ke sistem','192.168.1.1'),
|
||||
(4,'Ahmad Fauzi','dinsos','Lihat Dashboard','dashboard','Akses dashboard monitoring','192.168.1.30'),
|
||||
(5,'H. Edi Kamtono','walikota','Lihat Dashboard','dashboard','Akses dashboard eksekutif','192.168.1.40');
|
||||
@@ -0,0 +1,357 @@
|
||||
-- ============================================================
|
||||
-- SINERGI - Sistem Informasi Geografis Penanganan Kemiskinan
|
||||
-- Database Schema v1.0
|
||||
-- Kota Pontianak
|
||||
-- ============================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS `sinergi_db`
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE `sinergi_db`;
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: roles
|
||||
-- ============================================================
|
||||
CREATE TABLE `roles` (
|
||||
`id` TINYINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`nama` VARCHAR(50) NOT NULL,
|
||||
`slug` VARCHAR(30) NOT NULL,
|
||||
`deskripsi` VARCHAR(255),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uq_roles_slug` (`slug`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT INTO `roles` (`id`,`nama`,`slug`,`deskripsi`) VALUES
|
||||
(1,'Super Admin','superadmin','Administrator sistem, mengelola seluruh data master'),
|
||||
(2,'Verifikator Kelurahan','verifikator','Petugas verifikasi laporan di tingkat kelurahan'),
|
||||
(3,'Pengurus Rumah Ibadah','rumah_ibadah','Pengurus rumah ibadah yang menangani warga binaan'),
|
||||
(4,'Dinas Sosial','dinsos','Petugas Dinas Sosial untuk monitoring kemiskinan'),
|
||||
(5,'Wali Kota','walikota','Wali Kota untuk dashboard eksekutif');
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: kecamatan
|
||||
-- ============================================================
|
||||
CREATE TABLE `kecamatan` (
|
||||
`id` SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`nama` VARCHAR(100) NOT NULL,
|
||||
`kode` VARCHAR(20),
|
||||
`lat` DECIMAL(10,7),
|
||||
`lng` DECIMAL(10,7),
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 6 Kecamatan Kota Pontianak
|
||||
INSERT INTO `kecamatan` (`id`,`nama`,`kode`,`lat`,`lng`) VALUES
|
||||
(1,'Pontianak Kota','62.71.01',-0.0233,109.3294),
|
||||
(2,'Pontianak Barat','62.71.02',-0.0500,109.3100),
|
||||
(3,'Pontianak Selatan','62.71.03',-0.0700,109.3400),
|
||||
(4,'Pontianak Tenggara','62.71.04',-0.0900,109.3600),
|
||||
(5,'Pontianak Timur','62.71.05',-0.0400,109.3700),
|
||||
(6,'Pontianak Utara','62.71.06',0.0100,109.3300);
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: kelurahan
|
||||
-- ============================================================
|
||||
CREATE TABLE `kelurahan` (
|
||||
`id` SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`id_kecamatan` SMALLINT UNSIGNED NOT NULL,
|
||||
`nama` VARCHAR(100) NOT NULL,
|
||||
`kode` VARCHAR(20),
|
||||
`lat` DECIMAL(10,7),
|
||||
`lng` DECIMAL(10,7),
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `fk_kel_kec` (`id_kecamatan`),
|
||||
CONSTRAINT `fk_kel_kec` FOREIGN KEY (`id_kecamatan`) REFERENCES `kecamatan` (`id`) ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Kelurahan Kota Pontianak (sample per kecamatan)
|
||||
INSERT INTO `kelurahan` (`id`,`id_kecamatan`,`nama`,`kode`,`lat`,`lng`) VALUES
|
||||
-- Pontianak Kota
|
||||
(1,1,'Darat Sekip','62.71.01.1001',-0.0175,109.3230),
|
||||
(2,1,'Sungai Bangkong','62.71.01.1002',-0.0290,109.3310),
|
||||
(3,1,'Tengah','62.71.01.1003',-0.0210,109.3290),
|
||||
(4,1,'Mariana','62.71.01.1004',-0.0155,109.3350),
|
||||
-- Pontianak Barat
|
||||
(5,2,'Sungai Jawi Dalam','62.71.02.1001',-0.0467,109.3050),
|
||||
(6,2,'Sungai Jawi Luar','62.71.02.1002',-0.0530,109.3000),
|
||||
(7,2,'Pal Lima','62.71.02.1003',-0.0600,109.2900),
|
||||
(8,2,'Sungai Beliung','62.71.02.1004',-0.0450,109.3150),
|
||||
-- Pontianak Selatan
|
||||
(9,3,'Akcaya','62.71.03.1001',-0.0650,109.3380),
|
||||
(10,3,'Bangka Belitung Darat','62.71.03.1002',-0.0750,109.3420),
|
||||
(11,3,'Bangka Belitung Laut','62.71.03.1003',-0.0820,109.3350),
|
||||
(12,3,'Parit Tokaya','62.71.03.1004',-0.0700,109.3300),
|
||||
-- Pontianak Tenggara
|
||||
(13,4,'Bansir Darat','62.71.04.1001',-0.0870,109.3550),
|
||||
(14,4,'Bansir Laut','62.71.04.1002',-0.0950,109.3600),
|
||||
(15,4,'Tanjung Hulu','62.71.04.1003',-0.0920,109.3700),
|
||||
(16,4,'Tanjung Hilir','62.71.04.1004',-0.0850,109.3650),
|
||||
-- Pontianak Timur
|
||||
(17,5,'Saigon','62.71.05.1001',-0.0380,109.3700),
|
||||
(18,5,'Tambelan Sampit','62.71.05.1002',-0.0450,109.3750),
|
||||
(19,5,'Tanjung Raya','62.71.05.1003',-0.0320,109.3800),
|
||||
(20,5,'Parit Mayor','62.71.05.1004',-0.0500,109.3680),
|
||||
-- Pontianak Utara
|
||||
(21,6,'Siantan Hulu','62.71.06.1001',0.0200,109.3250),
|
||||
(22,6,'Siantan Tengah','62.71.06.1002',0.0100,109.3200),
|
||||
(23,6,'Siantan Hilir','62.71.06.1003',0.0050,109.3300),
|
||||
(24,6,'Batu Layang','62.71.06.1004',0.0300,109.3350);
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: users
|
||||
-- ============================================================
|
||||
CREATE TABLE `users` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`id_role` TINYINT UNSIGNED NOT NULL,
|
||||
`id_kelurahan` SMALLINT UNSIGNED DEFAULT NULL,
|
||||
`nama` VARCHAR(100) NOT NULL,
|
||||
`username` VARCHAR(50) NOT NULL,
|
||||
`email` VARCHAR(100) NOT NULL,
|
||||
`password` VARCHAR(255) NOT NULL,
|
||||
`no_hp` VARCHAR(20),
|
||||
`foto` VARCHAR(255),
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`last_login` DATETIME,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uq_users_username` (`username`),
|
||||
UNIQUE KEY `uq_users_email` (`email`),
|
||||
KEY `fk_users_role` (`id_role`),
|
||||
KEY `fk_users_kel` (`id_kelurahan`),
|
||||
CONSTRAINT `fk_users_role` FOREIGN KEY (`id_role`) REFERENCES `roles` (`id`) ON DELETE RESTRICT,
|
||||
CONSTRAINT `fk_users_kel` FOREIGN KEY (`id_kelurahan`) REFERENCES `kelurahan` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Password: Admin@123 (bcrypt)
|
||||
INSERT INTO `users` (`id`,`id_role`,`id_kelurahan`,`nama`,`username`,`email`,`password`,`no_hp`,`is_active`) VALUES
|
||||
(1,1,NULL,'Super Administrator','superadmin','superadmin@sinergi.id','$2y$12$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi','081200000001',1),
|
||||
(2,2,1,'Budi Santoso','verif_darat','budi.santoso@pontianak.go.id','$2y$12$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi','081200000002',1),
|
||||
(3,2,5,'Sari Dewi','verif_jawi','sari.dewi@pontianak.go.id','$2y$12$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi','081200000003',1),
|
||||
(4,4,NULL,'Ahmad Fauzi','dinsos_ahmad','ahmad.fauzi@dinsos.id','$2y$12$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi','081200000004',1),
|
||||
(5,5,NULL,'H. Edi Kamtono','walikota','walikota@pontianak.go.id','$2y$12$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi','081200000005',1);
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: jenis_rumah_ibadah
|
||||
-- ============================================================
|
||||
CREATE TABLE `jenis_rumah_ibadah` (
|
||||
`id` TINYINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`nama` VARCHAR(50) NOT NULL,
|
||||
`ikon` VARCHAR(100),
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT INTO `jenis_rumah_ibadah` VALUES
|
||||
(1,'Masjid','masjid.png'),
|
||||
(2,'Gereja','gereja.png'),
|
||||
(3,'Vihara','vihara.png'),
|
||||
(4,'Pura','pura.png'),
|
||||
(5,'Klenteng','klenteng.png');
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: rumah_ibadah
|
||||
-- ============================================================
|
||||
CREATE TABLE `rumah_ibadah` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`id_user` INT UNSIGNED DEFAULT NULL COMMENT 'User pengurus',
|
||||
`id_kelurahan` SMALLINT UNSIGNED NOT NULL,
|
||||
`id_jenis` TINYINT UNSIGNED NOT NULL,
|
||||
`nama` VARCHAR(150) NOT NULL,
|
||||
`alamat` TEXT,
|
||||
`lat` DECIMAL(10,7) NOT NULL,
|
||||
`lng` DECIMAL(10,7) NOT NULL,
|
||||
`radius_meter` INT UNSIGNED NOT NULL DEFAULT 500 COMMENT 'Radius pelayanan dalam meter',
|
||||
`no_hp_pengurus` VARCHAR(20),
|
||||
`nama_ketua` VARCHAR(100),
|
||||
`foto` VARCHAR(255),
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `fk_ri_user` (`id_user`),
|
||||
KEY `fk_ri_kel` (`id_kelurahan`),
|
||||
KEY `fk_ri_jenis` (`id_jenis`),
|
||||
CONSTRAINT `fk_ri_user` FOREIGN KEY (`id_user`) REFERENCES `users` (`id`) ON DELETE SET NULL,
|
||||
CONSTRAINT `fk_ri_kel` FOREIGN KEY (`id_kelurahan`) REFERENCES `kelurahan` (`id`) ON DELETE RESTRICT,
|
||||
CONSTRAINT `fk_ri_jenis` FOREIGN KEY (`id_jenis`) REFERENCES `jenis_rumah_ibadah` (`id`) ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: kategori_bantuan
|
||||
-- ============================================================
|
||||
CREATE TABLE `kategori_bantuan` (
|
||||
`id` TINYINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`nama` VARCHAR(100) NOT NULL,
|
||||
`deskripsi` TEXT,
|
||||
`satuan` VARCHAR(50) COMMENT 'kg, paket, rupiah, dll',
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT INTO `kategori_bantuan` (`id`,`nama`,`deskripsi`,`satuan`) VALUES
|
||||
(1,'Sembako','Bantuan bahan pokok makanan','paket'),
|
||||
(2,'Sandang','Bantuan pakaian dan kebutuhan sandang','paket'),
|
||||
(3,'Uang Tunai','Bantuan uang tunai langsung','rupiah'),
|
||||
(4,'Obat-obatan','Bantuan obat dan layanan kesehatan','paket'),
|
||||
(5,'Peralatan Sekolah','Bantuan perlengkapan pendidikan anak','paket'),
|
||||
(6,'Renovasi Rumah','Bantuan material renovasi rumah tidak layak huni','paket'),
|
||||
(7,'Modal Usaha','Bantuan modal untuk usaha kecil','rupiah'),
|
||||
(8,'Gas & Listrik','Bantuan biaya gas dan listrik','paket');
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: laporan_masyarakat
|
||||
-- ============================================================
|
||||
CREATE TABLE `laporan_masyarakat` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`kode_laporan` VARCHAR(20) NOT NULL UNIQUE COMMENT 'LPR-YYYYMMDD-XXXX',
|
||||
`nama_pelapor` VARCHAR(100) NOT NULL,
|
||||
`no_hp_pelapor` VARCHAR(20),
|
||||
`nama_warga` VARCHAR(100) NOT NULL,
|
||||
`alamat_warga` TEXT NOT NULL,
|
||||
`id_kelurahan` SMALLINT UNSIGNED NOT NULL,
|
||||
`lat` DECIMAL(10,7) NOT NULL,
|
||||
`lng` DECIMAL(10,7) NOT NULL,
|
||||
`deskripsi` TEXT,
|
||||
`foto` VARCHAR(255),
|
||||
`status` ENUM('menunggu','diproses','terverifikasi','ditolak') NOT NULL DEFAULT 'menunggu',
|
||||
`catatan_verifikator` TEXT,
|
||||
`id_verifikator` INT UNSIGNED DEFAULT NULL,
|
||||
`tanggal_verifikasi` DATETIME,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `fk_lap_kel` (`id_kelurahan`),
|
||||
KEY `fk_lap_verif` (`id_verifikator`),
|
||||
CONSTRAINT `fk_lap_kel` FOREIGN KEY (`id_kelurahan`) REFERENCES `kelurahan` (`id`) ON DELETE RESTRICT,
|
||||
CONSTRAINT `fk_lap_verif` FOREIGN KEY (`id_verifikator`) REFERENCES `users` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: warga_miskin
|
||||
-- ============================================================
|
||||
CREATE TABLE `warga_miskin` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`id_laporan` INT UNSIGNED DEFAULT NULL,
|
||||
`id_kelurahan` SMALLINT UNSIGNED NOT NULL,
|
||||
`id_rumah_ibadah` INT UNSIGNED DEFAULT NULL COMMENT 'Rumah ibadah penanggung jawab',
|
||||
`id_verifikator` INT UNSIGNED DEFAULT NULL,
|
||||
-- Data Pribadi
|
||||
`nik` VARCHAR(16) UNIQUE,
|
||||
`nama` VARCHAR(100) NOT NULL,
|
||||
`tempat_lahir` VARCHAR(100),
|
||||
`tanggal_lahir` DATE,
|
||||
`jenis_kelamin` ENUM('L','P') DEFAULT 'L',
|
||||
`agama` ENUM('Islam','Kristen','Katolik','Hindu','Buddha','Konghucu','Lainnya') DEFAULT 'Islam',
|
||||
`alamat` TEXT NOT NULL,
|
||||
`lat` DECIMAL(10,7) NOT NULL,
|
||||
`lng` DECIMAL(10,7) NOT NULL,
|
||||
`no_hp` VARCHAR(20),
|
||||
`foto` VARCHAR(255),
|
||||
-- Data Sosial Ekonomi
|
||||
`pendidikan_terakhir` ENUM('Tidak Sekolah','SD','SMP','SMA','D3','S1','S2','S3') DEFAULT 'SD',
|
||||
`pekerjaan` VARCHAR(100),
|
||||
`penghasilan_bulanan` INT UNSIGNED DEFAULT 0 COMMENT 'Rupiah per bulan',
|
||||
`jumlah_tanggungan` TINYINT UNSIGNED DEFAULT 0,
|
||||
-- Kondisi Khusus
|
||||
`kondisi_rumah` ENUM('Sangat Tidak Layak','Tidak Layak','Cukup Layak','Layak') DEFAULT 'Tidak Layak',
|
||||
`riwayat_penyakit` TEXT COMMENT 'Daftar penyakit yang diderita',
|
||||
`is_lansia` TINYINT(1) DEFAULT 0,
|
||||
`is_disabilitas` TINYINT(1) DEFAULT 0,
|
||||
`disabilitas_jenis` VARCHAR(100),
|
||||
-- Status & Prioritas
|
||||
`status_kemiskinan` ENUM('Miskin Ekstrem','Miskin','Rentan Miskin') DEFAULT 'Miskin',
|
||||
`skor_prioritas` TINYINT UNSIGNED DEFAULT 0 COMMENT '0-100',
|
||||
`level_prioritas` ENUM('Tinggi','Sedang','Rendah') DEFAULT 'Sedang',
|
||||
`is_blind_spot` TINYINT(1) DEFAULT 0,
|
||||
`status` ENUM('aktif','nonaktif','meninggal','pindah') DEFAULT 'aktif',
|
||||
-- Timestamps
|
||||
`tanggal_verifikasi` DATE,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `fk_wm_laporan` (`id_laporan`),
|
||||
KEY `fk_wm_kel` (`id_kelurahan`),
|
||||
KEY `fk_wm_ri` (`id_rumah_ibadah`),
|
||||
KEY `fk_wm_verif` (`id_verifikator`),
|
||||
KEY `idx_wm_blind_spot` (`is_blind_spot`),
|
||||
KEY `idx_wm_prioritas` (`level_prioritas`),
|
||||
KEY `idx_wm_status_kem` (`status_kemiskinan`),
|
||||
CONSTRAINT `fk_wm_laporan` FOREIGN KEY (`id_laporan`) REFERENCES `laporan_masyarakat` (`id`) ON DELETE SET NULL,
|
||||
CONSTRAINT `fk_wm_kel` FOREIGN KEY (`id_kelurahan`) REFERENCES `kelurahan` (`id`) ON DELETE RESTRICT,
|
||||
CONSTRAINT `fk_wm_ri` FOREIGN KEY (`id_rumah_ibadah`) REFERENCES `rumah_ibadah` (`id`) ON DELETE SET NULL,
|
||||
CONSTRAINT `fk_wm_verif` FOREIGN KEY (`id_verifikator`) REFERENCES `users` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: histori_bantuan
|
||||
-- ============================================================
|
||||
CREATE TABLE `histori_bantuan` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`id_warga` INT UNSIGNED NOT NULL,
|
||||
`id_rumah_ibadah` INT UNSIGNED NOT NULL,
|
||||
`id_kategori_bantuan` TINYINT UNSIGNED NOT NULL,
|
||||
`id_user_pengurus` INT UNSIGNED NOT NULL,
|
||||
`kode_bantuan` VARCHAR(20) NOT NULL UNIQUE COMMENT 'BNT-YYYYMMDD-XXXX',
|
||||
`tanggal_penyaluran` DATE NOT NULL,
|
||||
`jumlah` DECIMAL(12,2),
|
||||
`satuan` VARCHAR(50),
|
||||
`deskripsi` TEXT,
|
||||
`foto_bukti` VARCHAR(255),
|
||||
`status` ENUM('rencana','disalurkan','dibatalkan') DEFAULT 'disalurkan',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `fk_hb_warga` (`id_warga`),
|
||||
KEY `fk_hb_ri` (`id_rumah_ibadah`),
|
||||
KEY `fk_hb_kat` (`id_kategori_bantuan`),
|
||||
KEY `fk_hb_user` (`id_user_pengurus`),
|
||||
CONSTRAINT `fk_hb_warga` FOREIGN KEY (`id_warga`) REFERENCES `warga_miskin` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_hb_ri` FOREIGN KEY (`id_rumah_ibadah`) REFERENCES `rumah_ibadah` (`id`) ON DELETE RESTRICT,
|
||||
CONSTRAINT `fk_hb_kat` FOREIGN KEY (`id_kategori_bantuan`) REFERENCES `kategori_bantuan` (`id`) ON DELETE RESTRICT,
|
||||
CONSTRAINT `fk_hb_user` FOREIGN KEY (`id_user_pengurus`) REFERENCES `users` (`id`) ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: aktivitas_sistem
|
||||
-- ============================================================
|
||||
CREATE TABLE `aktivitas_sistem` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`id_user` INT UNSIGNED DEFAULT NULL,
|
||||
`nama_user` VARCHAR(100) COMMENT 'Snapshot nama saat aksi',
|
||||
`role` VARCHAR(30),
|
||||
`aksi` VARCHAR(100) NOT NULL,
|
||||
`modul` VARCHAR(50),
|
||||
`deskripsi` TEXT,
|
||||
`ip_address` VARCHAR(45),
|
||||
`user_agent` VARCHAR(255),
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `fk_as_user` (`id_user`),
|
||||
KEY `idx_as_modul` (`modul`),
|
||||
KEY `idx_as_created` (`created_at`),
|
||||
CONSTRAINT `fk_as_user` FOREIGN KEY (`id_user`) REFERENCES `users` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: sessions (untuk autentikasi manual)
|
||||
-- ============================================================
|
||||
CREATE TABLE `sessions` (
|
||||
`id` VARCHAR(128) NOT NULL,
|
||||
`id_user` INT UNSIGNED NOT NULL,
|
||||
`ip_address` VARCHAR(45),
|
||||
`user_agent` VARCHAR(255),
|
||||
`payload` TEXT,
|
||||
`last_activity` INT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `fk_sess_user` (`id_user`),
|
||||
CONSTRAINT `fk_sess_user` FOREIGN KEY (`id_user`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
@@ -0,0 +1,130 @@
|
||||
-- ============================================================
|
||||
-- SINERGI — Entity Relationship Diagram (ERD) Documentation
|
||||
-- ============================================================
|
||||
--
|
||||
-- DIAGRAM RELASI TABEL:
|
||||
--
|
||||
-- roles ─────────────────────────────┐
|
||||
-- id, nama, slug, deskripsi │ 1
|
||||
-- │
|
||||
-- users ────────────────────────────-┤ N (setiap user punya 1 role)
|
||||
-- id, id_role(FK→roles), id_kelurahan(FK→kelurahan)
|
||||
-- nama, username, email, password, no_hp, foto, is_active
|
||||
-- last_login, created_at
|
||||
--
|
||||
-- kecamatan ─────────────────────────┐
|
||||
-- id, nama, kode, lat, lng │ 1
|
||||
-- │
|
||||
-- kelurahan ─────────────────────────┤ N (banyak kelurahan per kecamatan)
|
||||
-- id, id_kecamatan(FK→kecamatan) │ 1
|
||||
-- nama, kode, lat, lng │
|
||||
-- │ N
|
||||
-- rumah_ibadah ──────────────────────┤ (banyak RI per kelurahan)
|
||||
-- id, id_user(FK→users) │
|
||||
-- id_kelurahan(FK→kelurahan) │
|
||||
-- id_jenis(FK→jenis_rumah_ibadah) │
|
||||
-- nama, alamat, lat, lng │
|
||||
-- radius_meter, nama_ketua, foto │ 1
|
||||
-- │
|
||||
-- warga_miskin ──────────────────────┘ N (banyak warga per RI)
|
||||
-- id, id_laporan(FK→laporan_masyarakat)
|
||||
-- id_kelurahan(FK→kelurahan)
|
||||
-- id_rumah_ibadah(FK→rumah_ibadah)
|
||||
-- id_verifikator(FK→users)
|
||||
-- nik, nama, tgl_lahir, jk, agama
|
||||
-- alamat, lat, lng, foto
|
||||
-- pendidikan_terakhir, pekerjaan
|
||||
-- penghasilan_bulanan, jumlah_tanggungan
|
||||
-- kondisi_rumah, riwayat_penyakit
|
||||
-- is_lansia, is_disabilitas
|
||||
-- status_kemiskinan, skor_prioritas
|
||||
-- level_prioritas, is_blind_spot
|
||||
-- status, tanggal_verifikasi
|
||||
--
|
||||
-- laporan_masyarakat ────────────────┐
|
||||
-- id, kode_laporan │ 1
|
||||
-- nama_pelapor, no_hp_pelapor │
|
||||
-- nama_warga, alamat_warga │
|
||||
-- id_kelurahan(FK→kelurahan) │
|
||||
-- lat, lng, deskripsi, foto │
|
||||
-- status, catatan_verifikator │
|
||||
-- id_verifikator(FK→users) │
|
||||
-- tanggal_verifikasi │
|
||||
-- │ (1 laporan → 1 warga)
|
||||
-- histori_bantuan ─────────────────────────────────
|
||||
-- id, id_warga(FK→warga_miskin)
|
||||
-- id_rumah_ibadah(FK→rumah_ibadah)
|
||||
-- id_kategori_bantuan(FK→kategori_bantuan)
|
||||
-- id_user_pengurus(FK→users)
|
||||
-- kode_bantuan, tanggal_penyaluran
|
||||
-- jumlah, satuan, deskripsi, foto_bukti
|
||||
-- status
|
||||
--
|
||||
-- kategori_bantuan ──────────────────
|
||||
-- id, nama, deskripsi, satuan, is_active
|
||||
--
|
||||
-- jenis_rumah_ibadah ────────────────
|
||||
-- id, nama, ikon
|
||||
--
|
||||
-- aktivitas_sistem ──────────────────
|
||||
-- id, id_user(FK→users), nama_user
|
||||
-- role, aksi, modul, deskripsi
|
||||
-- ip_address, user_agent, created_at
|
||||
--
|
||||
-- sessions ──────────────────────────
|
||||
-- id, id_user(FK→users)
|
||||
-- ip_address, user_agent
|
||||
-- payload, last_activity
|
||||
--
|
||||
-- ============================================================
|
||||
-- RELASI KARDINALITAS:
|
||||
-- ============================================================
|
||||
--
|
||||
-- roles 1 ──── N users
|
||||
-- kecamatan 1 ──── N kelurahan
|
||||
-- kelurahan 1 ──── N rumah_ibadah
|
||||
-- kelurahan 1 ──── N users (verifikator)
|
||||
-- kelurahan 1 ──── N warga_miskin
|
||||
-- kelurahan 1 ──── N laporan_masyarakat
|
||||
-- rumah_ibadah 1 ──── N warga_miskin (binaan)
|
||||
-- rumah_ibadah 1 ──── N histori_bantuan
|
||||
-- warga_miskin 1 ──── N histori_bantuan
|
||||
-- laporan 1 ──── 1 warga_miskin (opsional)
|
||||
-- kategori 1 ──── N histori_bantuan
|
||||
-- users 1 ──── 1 rumah_ibadah (pengurus)
|
||||
-- users 1 ──── N aktivitas_sistem
|
||||
-- users 1 ──── N laporan (verifikator)
|
||||
-- users 1 ──── N warga_miskin (verifikator)
|
||||
-- jenis_ri 1 ──── N rumah_ibadah
|
||||
--
|
||||
-- ============================================================
|
||||
-- PROSES BISNIS UTAMA:
|
||||
-- ============================================================
|
||||
--
|
||||
-- [1] PELAPORAN:
|
||||
-- Masyarakat → POST /lapor → laporan_masyarakat (status=menunggu)
|
||||
--
|
||||
-- [2] VERIFIKASI:
|
||||
-- Verifikator → verifikasi laporan →
|
||||
-- UPDATE laporan status=terverifikasi +
|
||||
-- INSERT warga_miskin
|
||||
--
|
||||
-- [3] ASSIGNMENT RI (Haversine):
|
||||
-- Saat INSERT warga_miskin:
|
||||
-- - Loop semua rumah_ibadah aktif
|
||||
-- - Hitung jarak Haversine(warga.lat,warga.lng, ri.lat,ri.lng)
|
||||
-- - Jika jarak <= radius_meter → assign id_rumah_ibadah
|
||||
-- - Jika tidak ada → is_blind_spot = 1
|
||||
--
|
||||
-- [4] SKOR PRIORITAS (0-100):
|
||||
-- penghasilan(30) + tanggungan(20) + kondisi_rumah(15) +
|
||||
-- pendidikan(10) + penyakit(10) + lansia(8) + disabilitas(7)
|
||||
-- → level: Tinggi(≥70), Sedang(≥40), Rendah(<40)
|
||||
--
|
||||
-- [5] PENYALURAN BANTUAN:
|
||||
-- Pengurus RI → INSERT histori_bantuan
|
||||
--
|
||||
-- [6] MONITORING:
|
||||
-- Dinsos/Wali Kota → READ ONLY semua data
|
||||
-- GIS Heatmap dari koordinat warga_miskin
|
||||
-- ============================================================
|
||||
@@ -0,0 +1,298 @@
|
||||
# PANDUAN INSTALASI SINERGI
|
||||
## Sistem Informasi Geografis Penanganan Kemiskinan — Kota Pontianak
|
||||
|
||||
---
|
||||
|
||||
## PRASYARAT SISTEM
|
||||
|
||||
Pastikan software berikut sudah terinstal:
|
||||
|
||||
| Software | Versi | Keterangan |
|
||||
|----------|-------|------------|
|
||||
| Laragon | 6.0+ | Full / Lite |
|
||||
| PHP | 8.1+ | Sudah termasuk dalam Laragon |
|
||||
| MySQL | 8.0+ | Sudah termasuk dalam Laragon |
|
||||
| Apache | 2.4+ | Sudah termasuk dalam Laragon |
|
||||
|
||||
---
|
||||
|
||||
## LANGKAH INSTALASI
|
||||
|
||||
### Step 1: Copy Folder Proyek
|
||||
|
||||
```
|
||||
Salin folder `sinergi/` ke:
|
||||
C:\laragon\www\sinergi\
|
||||
```
|
||||
|
||||
Pastikan struktur folder seperti ini:
|
||||
```
|
||||
C:\laragon\www\sinergi\
|
||||
├── app\
|
||||
│ ├── controllers\
|
||||
│ ├── models\
|
||||
│ └── views\
|
||||
├── config\
|
||||
├── core\
|
||||
├── public\
|
||||
├── database\
|
||||
├── index.php
|
||||
└── .htaccess
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Aktifkan mod_rewrite Apache
|
||||
|
||||
1. Buka **Laragon** → klik kanan tray icon
|
||||
2. Pilih **Apache** → **httpd.conf**
|
||||
3. Cari baris: `#LoadModule rewrite_module modules/mod_rewrite.so`
|
||||
4. Hapus tanda `#` di depannya
|
||||
5. Simpan file
|
||||
6. Restart Laragon
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Import Database
|
||||
|
||||
#### Cara A — Melalui phpMyAdmin (Disarankan)
|
||||
1. Buka browser: `http://localhost/phpmyadmin`
|
||||
2. Klik **"Import"** di menu atas
|
||||
3. Pilih file `database/sinergi.sql` → klik **Go**
|
||||
4. Setelah selesai, pilih file `database/dummy_data.sql` → klik **Go**
|
||||
|
||||
#### Cara B — Melalui Command Line
|
||||
```bash
|
||||
# Buka Command Prompt di folder sinergi
|
||||
cd C:\laragon\www\sinergi
|
||||
|
||||
# Import schema
|
||||
mysql -u root -p < database\sinergi.sql
|
||||
|
||||
# Import dummy data
|
||||
mysql -u root -p sinergi_db < database\dummy_data.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Konfigurasi Aplikasi
|
||||
|
||||
Edit file `config/database.php`:
|
||||
```php
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_NAME', 'sinergi_db');
|
||||
define('DB_USER', 'root');
|
||||
define('DB_PASS', ''); // Sesuaikan password MySQL Anda
|
||||
define('DB_CHARSET', 'utf8mb4');
|
||||
```
|
||||
|
||||
Edit file `config/app.php` jika nama folder berbeda:
|
||||
```php
|
||||
define('APP_URL', 'http://localhost/sinergi'); // Sesuaikan
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 5: Buat Folder Upload
|
||||
|
||||
```bash
|
||||
# Pastikan folder ini ada dan bisa ditulis
|
||||
C:\laragon\www\sinergi\public\images\uploads\
|
||||
C:\laragon\www\sinergi\public\images\laporan\
|
||||
C:\laragon\www\sinergi\public\images\warga\
|
||||
C:\laragon\www\sinergi\public\images\bantuan\
|
||||
C:\laragon\www\sinergi\public\images\rumah_ibadah\
|
||||
```
|
||||
|
||||
Atau jalankan script ini di Command Prompt:
|
||||
```batch
|
||||
mkdir C:\laragon\www\sinergi\public\images\uploads
|
||||
mkdir C:\laragon\www\sinergi\public\images\laporan
|
||||
mkdir C:\laragon\www\sinergi\public\images\warga
|
||||
mkdir C:\laragon\www\sinergi\public\images\bantuan
|
||||
mkdir C:\laragon\www\sinergi\public\images\rumah_ibadah
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 6: Jalankan Aplikasi
|
||||
|
||||
1. **Start Laragon** (klik tombol Start All)
|
||||
2. Buka browser: `http://localhost/sinergi`
|
||||
3. Halaman beranda publik akan tampil
|
||||
|
||||
---
|
||||
|
||||
## AKUN LOGIN DEMO
|
||||
|
||||
Semua akun menggunakan password: **Admin@123**
|
||||
|
||||
| Role | Username | Password | Akses |
|
||||
|------|----------|----------|-------|
|
||||
| Super Admin | `superadmin` | Admin@123 | Semua fitur admin |
|
||||
| Verifikator (Kel. Darat Sekip) | `verif_darat` | Admin@123 | Verifikasi laporan |
|
||||
| Verifikator (Kel. Sungai Jawi) | `verif_jawi` | Admin@123 | Verifikasi laporan |
|
||||
| Pengurus Masjid Darussalam | `ri_masjid_darussalam` | Admin@123 | Penyaluran bantuan |
|
||||
| Pengurus Gereja Bethel | `ri_gereja_bethel` | Admin@123 | Penyaluran bantuan |
|
||||
| Pengurus Masjid Al-Falah | `ri_masjid_alfalah` | Admin@123 | Penyaluran bantuan |
|
||||
| Dinas Sosial | `dinsos_ahmad` | Admin@123 | Monitoring |
|
||||
| Wali Kota | `walikota` | Admin@123 | Dashboard Eksekutif |
|
||||
|
||||
---
|
||||
|
||||
## URL AKSES
|
||||
|
||||
| Halaman | URL |
|
||||
|---------|-----|
|
||||
| Beranda Publik | `http://localhost/sinergi/` |
|
||||
| Form Laporan | `http://localhost/sinergi/lapor` |
|
||||
| Cek Laporan | `http://localhost/sinergi/cek-laporan` |
|
||||
| Login | `http://localhost/sinergi/login` |
|
||||
| Dashboard Admin | `http://localhost/sinergi/admin/dashboard` |
|
||||
| Dashboard Verifikator | `http://localhost/sinergi/verifikator/dashboard` |
|
||||
| Dashboard Rumah Ibadah | `http://localhost/sinergi/rumah-ibadah/dashboard` |
|
||||
| Dashboard Dinsos | `http://localhost/sinergi/dinsos/dashboard` |
|
||||
| Dashboard Wali Kota | `http://localhost/sinergi/walikota/dashboard` |
|
||||
|
||||
---
|
||||
|
||||
## TROUBLESHOOTING
|
||||
|
||||
### Error 404 (Halaman tidak ditemukan)
|
||||
- Pastikan `mod_rewrite` sudah aktif di Apache
|
||||
- Cek file `.htaccess` ada di root folder `sinergi/`
|
||||
- Pastikan `AllowOverride All` aktif di `httpd.conf`:
|
||||
```
|
||||
<Directory "C:/laragon/www">
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
```
|
||||
|
||||
### Error database connection
|
||||
- Pastikan MySQL running di Laragon
|
||||
- Cek konfigurasi di `config/database.php`
|
||||
- Pastikan database `sinergi_db` sudah diimport
|
||||
|
||||
### Peta Leaflet tidak muncul
|
||||
- Pastikan koneksi internet aktif (Leaflet CDN)
|
||||
- Cek console browser (F12) untuk error JavaScript
|
||||
|
||||
### Upload foto gagal
|
||||
- Pastikan folder `public/images/uploads/` ada dan writable
|
||||
- Cek konfigurasi `upload_max_filesize` di `php.ini` (minimal 5M)
|
||||
|
||||
### Halaman putih kosong
|
||||
- Aktifkan error reporting: tambah di awal `index.php`:
|
||||
```php
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## STRUKTUR FOLDER LENGKAP
|
||||
|
||||
```
|
||||
sinergi/
|
||||
├── .htaccess # URL Rewriting
|
||||
├── index.php # Entry point & Router
|
||||
│
|
||||
├── config/
|
||||
│ ├── app.php # Konfigurasi aplikasi
|
||||
│ └── database.php # Konfigurasi database
|
||||
│
|
||||
├── core/
|
||||
│ ├── Auth.php # Autentikasi & session
|
||||
│ ├── Controller.php # Base controller
|
||||
│ ├── Database.php # Koneksi database (PDO)
|
||||
│ ├── Router.php # URL Router
|
||||
│ └── helpers.php # Helper functions (Haversine, skor, dll)
|
||||
│
|
||||
├── app/
|
||||
│ ├── controllers/
|
||||
│ │ ├── AdminController.php
|
||||
│ │ ├── AuthController.php
|
||||
│ │ ├── DinsosController.php
|
||||
│ │ ├── PublicController.php
|
||||
│ │ ├── RumahIbadahController.php
|
||||
│ │ ├── VerifikatorController.php
|
||||
│ │ └── WalikotaController.php
|
||||
│ │
|
||||
│ └── views/
|
||||
│ ├── layouts/
|
||||
│ │ ├── admin.php
|
||||
│ │ ├── dinsos.php
|
||||
│ │ ├── public.php
|
||||
│ │ ├── rumah_ibadah.php
|
||||
│ │ ├── verifikator.php
|
||||
│ │ └── walikota.php
|
||||
│ ├── admin/ # View Super Admin
|
||||
│ ├── auth/ # Login
|
||||
│ ├── dinsos/ # View Dinsos
|
||||
│ ├── public/ # View Publik
|
||||
│ ├── rumah_ibadah/ # View Pengurus RI
|
||||
│ ├── verifikator/ # View Verifikator
|
||||
│ └── walikota/ # View Wali Kota
|
||||
│
|
||||
├── public/
|
||||
│ ├── css/app.css # Stylesheet utama
|
||||
│ ├── js/app.js # JavaScript utama (GIS, Charts)
|
||||
│ └── images/ # Gambar & upload
|
||||
│
|
||||
└── database/
|
||||
├── sinergi.sql # Schema + data roles/kecamatan
|
||||
└── dummy_data.sql # Data dummy untuk testing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TEKNOLOGI YANG DIGUNAKAN
|
||||
|
||||
| Komponen | Teknologi | Versi |
|
||||
|----------|-----------|-------|
|
||||
| Frontend | HTML5, CSS3 Native, JavaScript | — |
|
||||
| Peta GIS | Leaflet.js | 1.9.4 |
|
||||
| Heatmap | Leaflet.heat | 0.2.0 |
|
||||
| Grafik | Chart.js | 4.4.0 |
|
||||
| Font | Inter (Google Fonts) | — |
|
||||
| Backend | PHP Native (tanpa framework) | 8.1+ |
|
||||
| Database | MySQL | 8.0+ |
|
||||
| Arsitektur | MVC Sederhana + RBAC | — |
|
||||
| Environment | Laragon | 6.0+ |
|
||||
|
||||
---
|
||||
|
||||
## FITUR UTAMA
|
||||
|
||||
### 🗺 Sistem GIS
|
||||
- Peta interaktif Leaflet.js
|
||||
- Marker warga miskin (merah=ekstrem, oranye=miskin, kuning=rentan)
|
||||
- Marker & radius rumah ibadah (hijau)
|
||||
- Heatmap persebaran kemiskinan
|
||||
- Layer control (beralih antara tampilan)
|
||||
- Popup informasi detail
|
||||
|
||||
### 📊 Algoritma Prioritas Bantuan
|
||||
Skor 0-100 berdasarkan 7 faktor:
|
||||
1. **Penghasilan** (30 poin) — semakin rendah = skor lebih tinggi
|
||||
2. **Jumlah tanggungan** (20 poin)
|
||||
3. **Kondisi rumah** (15 poin)
|
||||
4. **Pendidikan** (10 poin)
|
||||
5. **Riwayat penyakit** (10 poin)
|
||||
6. **Status lansia** (8 poin)
|
||||
7. **Status disabilitas** (7 poin)
|
||||
|
||||
### 🔍 Deteksi Blind Spot
|
||||
- Menggunakan **Haversine Formula** untuk menghitung jarak
|
||||
- Jika warga tidak berada dalam radius rumah ibadah manapun → **Blind Spot**
|
||||
- Blind spot otomatis muncul di dashboard Dinsos & Admin
|
||||
|
||||
### ⛪ Assignment Rumah Ibadah
|
||||
- Otomatis dihitung saat verifikasi warga
|
||||
- Menggunakan Haversine Formula mencari rumah ibadah dalam radius terdekat
|
||||
- 1 warga = 1 rumah ibadah penanggung jawab
|
||||
|
||||
---
|
||||
|
||||
*SINERGI v1.0.0 — Sistem Informasi Geografis Penanganan Kemiskinan*
|
||||
*Kota Pontianak — © 2024 Dinas Sosial Kota Pontianak*
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
// index.php - Entry point SINERGI
|
||||
|
||||
// Load config
|
||||
require_once __DIR__ . '/config/app.php';
|
||||
require_once __DIR__ . '/config/database.php';
|
||||
|
||||
// Autoload core
|
||||
require_once __DIR__ . '/core/Database.php';
|
||||
require_once __DIR__ . '/core/Auth.php';
|
||||
require_once __DIR__ . '/core/Controller.php';
|
||||
require_once __DIR__ . '/core/Router.php';
|
||||
require_once __DIR__ . '/core/helpers.php';
|
||||
|
||||
// Autoload controllers
|
||||
foreach (glob(__DIR__ . '/app/controllers/*.php') as $file) {
|
||||
require_once $file;
|
||||
}
|
||||
|
||||
// Init session
|
||||
Auth::init();
|
||||
|
||||
// Init router
|
||||
$router = new Router();
|
||||
|
||||
// ============================================================
|
||||
// PUBLIC ROUTES (tanpa login)
|
||||
// ============================================================
|
||||
$router->get('/', [PublicController::class, 'beranda']);
|
||||
$router->get('/tentang', [PublicController::class, 'tentang']);
|
||||
$router->get('/kontak', [PublicController::class, 'kontak']);
|
||||
$router->get('/lapor', [PublicController::class, 'lapor']);
|
||||
$router->post('/lapor', [PublicController::class, 'kirimLaporan']);
|
||||
$router->get('/cek-laporan', [PublicController::class, 'cekLaporan']);
|
||||
$router->post('/cek-laporan', [PublicController::class, 'proseCekLaporan']);
|
||||
|
||||
// API publik (JSON)
|
||||
$router->get('/api/kelurahan/{id}', [PublicController::class, 'apiKelurahan']);
|
||||
$router->get('/api/rumah-ibadah', [PublicController::class, 'apiRumahIbadah']);
|
||||
$router->get('/api/warga-peta', [PublicController::class, 'apiWargaPeta']);
|
||||
|
||||
// ============================================================
|
||||
// AUTH ROUTES
|
||||
// ============================================================
|
||||
$router->get('/login', [AuthController::class, 'showLogin']);
|
||||
$router->post('/login', [AuthController::class, 'doLogin']);
|
||||
$router->get('/logout', [AuthController::class, 'logout']);
|
||||
$router->get('/forbidden', [AuthController::class, 'forbidden']);
|
||||
|
||||
// ============================================================
|
||||
// SUPER ADMIN ROUTES
|
||||
// ============================================================
|
||||
$router->get('/admin/dashboard', [AdminController::class, 'dashboard']);
|
||||
$router->get('/admin/users', [AdminController::class, 'users']);
|
||||
$router->post('/admin/users/store', [AdminController::class, 'storeUser']);
|
||||
$router->post('/admin/users/{id}/update', [AdminController::class, 'updateUser']);
|
||||
$router->post('/admin/users/{id}/reset-password', [AdminController::class, 'resetUserPassword']);
|
||||
$router->post('/admin/users/{id}/delete', [AdminController::class, 'deleteUser']);
|
||||
$router->get('/admin/rumah-ibadah', [AdminController::class, 'rumahIbadah']);
|
||||
$router->post('/admin/rumah-ibadah/store', [AdminController::class, 'storeRumahIbadah']);
|
||||
$router->post('/admin/rumah-ibadah/{id}/update', [AdminController::class, 'updateRumahIbadah']);
|
||||
$router->post('/admin/rumah-ibadah/{id}/delete', [AdminController::class, 'deleteRumahIbadah']);
|
||||
$router->get('/admin/wilayah', [AdminController::class, 'wilayah']);
|
||||
$router->post('/admin/kecamatan/store', [AdminController::class, 'storeKecamatan']);
|
||||
$router->post('/admin/kelurahan/store', [AdminController::class, 'storeKelurahan']);
|
||||
$router->get('/admin/kategori-bantuan', [AdminController::class, 'kategoriBantuan']);
|
||||
$router->post('/admin/kategori-bantuan/store', [AdminController::class, 'storeKategoriBantuan']);
|
||||
$router->post('/admin/kategori-bantuan/{id}/update', [AdminController::class, 'updateKategoriBantuan']);
|
||||
$router->get('/admin/monitoring', [AdminController::class, 'monitoring']);
|
||||
$router->get('/admin/blind-spot', [AdminController::class, 'blindSpot']);
|
||||
$router->get('/admin/aktivitas', [AdminController::class, 'aktivitas']);
|
||||
$router->get('/admin/api/stats', [AdminController::class, 'apiStats']);
|
||||
$router->get('/admin/api/peta', [AdminController::class, 'apiPeta']);
|
||||
|
||||
// ============================================================
|
||||
// VERIFIKATOR ROUTES
|
||||
// ============================================================
|
||||
$router->get('/verifikator/dashboard', [VerifikatorController::class, 'dashboard']);
|
||||
$router->get('/verifikator/laporan', [VerifikatorController::class, 'laporan']);
|
||||
$router->get('/verifikator/laporan/{id}', [VerifikatorController::class, 'detailLaporan']);
|
||||
$router->post('/verifikator/laporan/{id}/proses', [VerifikatorController::class, 'prosesLaporan']);
|
||||
$router->get('/verifikator/warga', [VerifikatorController::class, 'warga']);
|
||||
$router->get('/verifikator/warga/tambah', [VerifikatorController::class, 'formTambahWarga']);
|
||||
$router->get('/verifikator/warga/{id}', [VerifikatorController::class, 'detailWarga']);
|
||||
$router->post('/verifikator/warga/store', [VerifikatorController::class, 'storeWarga']);
|
||||
$router->post('/verifikator/warga/{id}/update',[VerifikatorController::class, 'updateWarga']);
|
||||
$router->get('/verifikator/api/stats', [VerifikatorController::class, 'apiStats']);
|
||||
|
||||
// ============================================================
|
||||
// RUMAH IBADAH ROUTES
|
||||
// ============================================================
|
||||
$router->get('/rumah-ibadah/dashboard', [RumahIbadahController::class, 'dashboard']);
|
||||
$router->get('/rumah-ibadah/warga', [RumahIbadahController::class, 'warga']);
|
||||
$router->get('/rumah-ibadah/warga/{id}', [RumahIbadahController::class, 'detailWarga']);
|
||||
$router->get('/rumah-ibadah/prioritas', [RumahIbadahController::class, 'prioritas']);
|
||||
$router->get('/rumah-ibadah/bantuan', [RumahIbadahController::class, 'bantuan']);
|
||||
$router->get('/rumah-ibadah/bantuan/tambah', [RumahIbadahController::class, 'formBantuan']);
|
||||
$router->post('/rumah-ibadah/bantuan/store', [RumahIbadahController::class, 'storeBantuan']);
|
||||
$router->get('/rumah-ibadah/api/peta', [RumahIbadahController::class, 'apiPeta']);
|
||||
|
||||
// ============================================================
|
||||
// DINSOS ROUTES
|
||||
// ============================================================
|
||||
$router->get('/dinsos/dashboard', [DinsosController::class, 'dashboard']);
|
||||
$router->get('/dinsos/kemiskinan', [DinsosController::class, 'kemiskinan']);
|
||||
$router->get('/dinsos/bantuan', [DinsosController::class, 'bantuan']);
|
||||
$router->get('/dinsos/blind-spot', [DinsosController::class, 'blindSpot']);
|
||||
$router->post('/dinsos/blind-spot/{id}/assign', [DinsosController::class, 'assignBlindSpot']);
|
||||
$router->get('/dinsos/kinerja', [DinsosController::class, 'kinerja']);
|
||||
$router->get('/dinsos/laporan', [DinsosController::class, 'laporan']);
|
||||
$router->get('/dinsos/akun', [DinsosController::class, 'akun']);
|
||||
$router->post('/dinsos/akun/store', [DinsosController::class, 'storeAkun']);
|
||||
$router->post('/dinsos/akun/{id}/update', [DinsosController::class, 'updateAkun']);
|
||||
$router->post('/dinsos/akun/{id}/reset-password', [DinsosController::class, 'resetAkunPassword']);
|
||||
$router->post('/dinsos/akun/{id}/deactivate', [DinsosController::class, 'deactivateAkun']);
|
||||
$router->get('/dinsos/api/heatmap', [DinsosController::class, 'apiHeatmap']);
|
||||
$router->get('/dinsos/api/stats', [DinsosController::class, 'apiStats']);
|
||||
|
||||
// ============================================================
|
||||
// WALIKOTA ROUTES
|
||||
// ============================================================
|
||||
$router->get('/walikota/dashboard', [WalikotaController::class, 'dashboard']);
|
||||
$router->get('/walikota/api/stats', [WalikotaController::class, 'apiStats']);
|
||||
$router->get('/walikota/api/peta', [WalikotaController::class, 'apiPeta']);
|
||||
|
||||
// Dispatch
|
||||
$router->dispatch();
|
||||
@@ -0,0 +1,510 @@
|
||||
/* ============================================================
|
||||
SINERGI — Main Stylesheet
|
||||
Sistem Informasi Geografis Penanganan Kemiskinan
|
||||
Kota Pontianak
|
||||
============================================================ */
|
||||
|
||||
/* --- CSS Variables --- */
|
||||
:root {
|
||||
--clr-primary: #1a3a5c;
|
||||
--clr-primary-d: #112845;
|
||||
--clr-primary-l: #2a5c8e;
|
||||
--clr-accent: #e8a020;
|
||||
--clr-accent-d: #c87d10;
|
||||
--clr-success: #16a34a;
|
||||
--clr-danger: #dc2626;
|
||||
--clr-warning: #d97706;
|
||||
--clr-info: #0284c7;
|
||||
--clr-bg: #f0f4f8;
|
||||
--clr-surface: #ffffff;
|
||||
--clr-border: #d1dce8;
|
||||
--clr-text: #1e2d3d;
|
||||
--clr-text-muted: #5a6e82;
|
||||
--clr-sidebar-bg: #112845;
|
||||
--clr-sidebar-hover: rgba(255,255,255,0.08);
|
||||
--clr-sidebar-active: rgba(232,160,32,0.18);
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 16px;
|
||||
--shadow-sm: 0 1px 4px rgba(0,0,0,0.08);
|
||||
--shadow-md: 0 4px 16px rgba(0,0,0,0.10);
|
||||
--shadow-lg: 0 8px 32px rgba(0,0,0,0.14);
|
||||
--sidebar-w: 260px;
|
||||
--topbar-h: 62px;
|
||||
--font-main: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
--transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* --- Reset & Base --- */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html { font-size: 15px; scroll-behavior: smooth; }
|
||||
body {
|
||||
font-family: var(--font-main);
|
||||
background: var(--clr-bg);
|
||||
color: var(--clr-text);
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
a { color: var(--clr-primary-l); text-decoration: none; }
|
||||
a:hover { color: var(--clr-accent); }
|
||||
img { max-width: 100%; display: block; }
|
||||
button, input, select, textarea { font-family: inherit; font-size: inherit; }
|
||||
|
||||
/* --- Layout: App Shell --- */
|
||||
.app-shell {
|
||||
display: grid;
|
||||
grid-template-columns: var(--sidebar-w) 1fr;
|
||||
grid-template-rows: var(--topbar-h) 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* --- Sidebar --- */
|
||||
.sidebar {
|
||||
grid-row: 1 / -1;
|
||||
background: var(--clr-sidebar-bg);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
z-index: 100;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.sidebar-brand {
|
||||
padding: 20px 20px 16px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.sidebar-brand-icon {
|
||||
width: 40px; height: 40px;
|
||||
background: var(--clr-accent);
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 20px; flex-shrink: 0;
|
||||
}
|
||||
.sidebar-brand-text h1 {
|
||||
font-size: 1.1rem; font-weight: 700; color: #fff; line-height: 1.2;
|
||||
}
|
||||
.sidebar-brand-text p { font-size: 0.68rem; color: rgba(255,255,255,0.5); line-height: 1.3; }
|
||||
|
||||
.sidebar-section { padding: 20px 12px 4px; }
|
||||
.sidebar-section-title {
|
||||
font-size: 0.65rem; font-weight: 700;
|
||||
text-transform: uppercase; letter-spacing: 0.1em;
|
||||
color: rgba(255,255,255,0.35);
|
||||
padding: 0 8px; margin-bottom: 4px;
|
||||
}
|
||||
.sidebar-nav { list-style: none; }
|
||||
.sidebar-nav li { margin-bottom: 2px; }
|
||||
.sidebar-nav a {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 9px 12px; border-radius: var(--radius-sm);
|
||||
color: rgba(255,255,255,0.78); font-size: 0.875rem; font-weight: 500;
|
||||
transition: var(--transition); cursor: pointer;
|
||||
}
|
||||
.sidebar-nav a:hover { background: var(--clr-sidebar-hover); color: #fff; }
|
||||
.sidebar-nav a.active {
|
||||
background: var(--clr-sidebar-active);
|
||||
color: var(--clr-accent); font-weight: 600;
|
||||
border-left: 3px solid var(--clr-accent);
|
||||
}
|
||||
.sidebar-nav a .nav-icon { width: 18px; text-align: center; flex-shrink: 0; font-size: 1rem; }
|
||||
.sidebar-nav a .nav-badge {
|
||||
margin-left: auto; background: var(--clr-danger);
|
||||
color: #fff; font-size: 0.65rem; font-weight: 700;
|
||||
padding: 1px 6px; border-radius: 20px; min-width: 18px; text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
margin-top: auto; padding: 16px 12px;
|
||||
border-top: 1px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
.sidebar-user {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 8px 10px; border-radius: var(--radius-sm);
|
||||
background: rgba(255,255,255,0.06);
|
||||
}
|
||||
.sidebar-user-avatar {
|
||||
width: 34px; height: 34px; border-radius: 50%;
|
||||
background: var(--clr-primary-l);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 700; font-size: 0.85rem; color: #fff; flex-shrink: 0;
|
||||
}
|
||||
.sidebar-user-info { flex: 1; min-width: 0; }
|
||||
.sidebar-user-name { font-size: 0.8rem; font-weight: 600; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.sidebar-user-role { font-size: 0.68rem; color: rgba(255,255,255,0.45); }
|
||||
.sidebar-logout { color: rgba(255,255,255,0.5); font-size: 1rem; cursor: pointer; transition: var(--transition); }
|
||||
.sidebar-logout:hover { color: var(--clr-danger); }
|
||||
|
||||
/* --- Topbar --- */
|
||||
.topbar {
|
||||
grid-column: 2;
|
||||
background: var(--clr-surface);
|
||||
border-bottom: 1px solid var(--clr-border);
|
||||
display: flex; align-items: center; gap: 16px;
|
||||
padding: 0 28px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
position: sticky; top: 0; z-index: 50;
|
||||
}
|
||||
.topbar-title { font-size: 1rem; font-weight: 600; color: var(--clr-text); flex: 1; }
|
||||
.topbar-breadcrumb { font-size: 0.8rem; color: var(--clr-text-muted); }
|
||||
.topbar-actions { display: flex; align-items: center; gap: 10px; margin-left: auto; }
|
||||
.topbar-time { font-size: 0.8rem; color: var(--clr-text-muted); font-family: var(--font-mono); }
|
||||
|
||||
/* --- Main Content --- */
|
||||
.main-content {
|
||||
grid-column: 2;
|
||||
padding: 24px 28px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* --- Cards --- */
|
||||
.card {
|
||||
background: var(--clr-surface);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--clr-border);
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--clr-border);
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
}
|
||||
.card-header h3 { font-size: 0.95rem; font-weight: 600; color: var(--clr-text); }
|
||||
.card-header .card-icon { font-size: 1.1rem; }
|
||||
.card-header .card-actions { margin-left: auto; display: flex; gap: 8px; }
|
||||
.card-body { padding: 20px; }
|
||||
.card-body.no-pad { padding: 0; }
|
||||
|
||||
/* --- Stat Cards --- */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.stat-card {
|
||||
background: var(--clr-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 18px 20px;
|
||||
border: 1px solid var(--clr-border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: flex; align-items: flex-start; gap: 14px;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.stat-card:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); }
|
||||
.stat-card-icon {
|
||||
width: 44px; height: 44px; border-radius: var(--radius-sm);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.3rem; flex-shrink: 0;
|
||||
}
|
||||
.stat-card-icon.blue { background: #dbeafe; color: #1d4ed8; }
|
||||
.stat-card-icon.red { background: #fee2e2; color: #dc2626; }
|
||||
.stat-card-icon.orange { background: #ffedd5; color: #c2410c; }
|
||||
.stat-card-icon.yellow { background: #fef9c3; color: #a16207; }
|
||||
.stat-card-icon.green { background: #dcfce7; color: #15803d; }
|
||||
.stat-card-icon.purple { background: #f3e8ff; color: #7e22ce; }
|
||||
.stat-card-icon.teal { background: #ccfbf1; color: #0f766e; }
|
||||
.stat-card-icon.gray { background: #f1f5f9; color: #475569; }
|
||||
.stat-card-body { flex: 1; min-width: 0; }
|
||||
.stat-card-value { font-size: 1.75rem; font-weight: 700; line-height: 1; color: var(--clr-text); }
|
||||
.stat-card-label { font-size: 0.78rem; color: var(--clr-text-muted); margin-top: 3px; }
|
||||
.stat-card-sub { font-size: 0.72rem; color: var(--clr-text-muted); margin-top: 6px; }
|
||||
|
||||
/* --- Map Container --- */
|
||||
.map-container {
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--clr-border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
position: relative;
|
||||
}
|
||||
.map-container #map, .map-container .map-inner {
|
||||
width: 100%; border-radius: 0;
|
||||
}
|
||||
|
||||
/* --- Dashboard Grid --- */
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 340px;
|
||||
gap: 20px;
|
||||
}
|
||||
.dashboard-grid.full { grid-template-columns: 1fr; }
|
||||
@media (max-width: 1100px) {
|
||||
.dashboard-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* --- Tables --- */
|
||||
.table-wrap { overflow-x: auto; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
|
||||
thead th {
|
||||
background: #f8fafc; color: var(--clr-text-muted);
|
||||
font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
|
||||
letter-spacing: 0.05em; padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--clr-border); white-space: nowrap;
|
||||
}
|
||||
tbody td { padding: 11px 14px; border-bottom: 1px solid #f1f5f9; vertical-align: middle; }
|
||||
tbody tr:last-child td { border-bottom: none; }
|
||||
tbody tr:hover { background: #f8fafc; }
|
||||
|
||||
/* --- Badges --- */
|
||||
.badge {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
padding: 2px 9px; border-radius: 20px;
|
||||
font-size: 0.7rem; font-weight: 700; letter-spacing: 0.02em; white-space: nowrap;
|
||||
}
|
||||
.badge-red { background: #fee2e2; color: #dc2626; }
|
||||
.badge-orange { background: #ffedd5; color: #c2410c; }
|
||||
.badge-yellow { background: #fef9c3; color: #a16207; }
|
||||
.badge-green { background: #dcfce7; color: #15803d; }
|
||||
.badge-blue { background: #dbeafe; color: #1d4ed8; }
|
||||
.badge-gray { background: #f1f5f9; color: #475569; }
|
||||
.badge-purple { background: #f3e8ff; color: #7e22ce; }
|
||||
|
||||
/* --- Buttons --- */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 8px 16px; border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem; font-weight: 600; cursor: pointer;
|
||||
border: none; transition: var(--transition); white-space: nowrap;
|
||||
}
|
||||
.btn-primary { background: var(--clr-primary); color: #fff; }
|
||||
.btn-primary:hover { background: var(--clr-primary-l); }
|
||||
.btn-accent { background: var(--clr-accent); color: var(--clr-primary-d); }
|
||||
.btn-accent:hover { background: var(--clr-accent-d); }
|
||||
.btn-success { background: var(--clr-success); color: #fff; }
|
||||
.btn-success:hover { background: #15803d; }
|
||||
.btn-danger { background: var(--clr-danger); color: #fff; }
|
||||
.btn-danger:hover { background: #b91c1c; }
|
||||
.btn-ghost { background: transparent; color: var(--clr-text-muted); border: 1px solid var(--clr-border); }
|
||||
.btn-ghost:hover { background: var(--clr-bg); color: var(--clr-text); }
|
||||
.btn-sm { padding: 5px 11px; font-size: 0.78rem; }
|
||||
.btn-icon { padding: 7px; }
|
||||
|
||||
/* --- Forms --- */
|
||||
.form-group { margin-bottom: 16px; }
|
||||
.form-label { display: block; font-size: 0.82rem; font-weight: 600; color: var(--clr-text); margin-bottom: 5px; }
|
||||
.form-label .required { color: var(--clr-danger); margin-left: 2px; }
|
||||
.form-control {
|
||||
width: 100%; padding: 9px 13px;
|
||||
border: 1px solid var(--clr-border); border-radius: var(--radius-sm);
|
||||
background: var(--clr-surface); color: var(--clr-text); font-size: 0.875rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.form-control:focus { outline: none; border-color: var(--clr-primary-l); box-shadow: 0 0 0 3px rgba(42,92,142,0.12); }
|
||||
.form-hint { font-size: 0.75rem; color: var(--clr-text-muted); margin-top: 4px; }
|
||||
.form-row { display: grid; gap: 16px; }
|
||||
.form-row.cols-2 { grid-template-columns: 1fr 1fr; }
|
||||
.form-row.cols-3 { grid-template-columns: 1fr 1fr 1fr; }
|
||||
|
||||
/* --- Flash Messages --- */
|
||||
.flash-message {
|
||||
display: flex; align-items: flex-start; gap: 10px;
|
||||
padding: 12px 16px; border-radius: var(--radius-sm);
|
||||
margin-bottom: 20px; font-size: 0.875rem; font-weight: 500;
|
||||
}
|
||||
.flash-success { background: #dcfce7; color: #15803d; border-left: 4px solid #16a34a; }
|
||||
.flash-error { background: #fee2e2; color: #dc2626; border-left: 4px solid #dc2626; }
|
||||
.flash-warning { background: #fef9c3; color: #a16207; border-left: 4px solid #d97706; }
|
||||
.flash-info { background: #dbeafe; color: #1d4ed8; border-left: 4px solid #0284c7; }
|
||||
|
||||
/* --- Priority Score Bar --- */
|
||||
.priority-bar { position: relative; height: 6px; background: #e2e8f0; border-radius: 3px; overflow: hidden; }
|
||||
.priority-bar-fill { height: 100%; border-radius: 3px; transition: width 0.4s ease; }
|
||||
.priority-bar-fill.high { background: var(--clr-danger); }
|
||||
.priority-bar-fill.medium { background: var(--clr-warning); }
|
||||
.priority-bar-fill.low { background: var(--clr-success); }
|
||||
|
||||
/* --- Map Legend --- */
|
||||
.map-legend {
|
||||
position: absolute; bottom: 28px; left: 12px; z-index: 1000;
|
||||
background: var(--clr-surface); border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-md); padding: 12px 14px;
|
||||
border: 1px solid var(--clr-border); min-width: 160px;
|
||||
}
|
||||
.map-legend h4 { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--clr-text-muted); margin-bottom: 8px; }
|
||||
.map-legend-item { display: flex; align-items: center; gap: 8px; font-size: 0.78rem; margin-bottom: 5px; }
|
||||
.map-legend-dot { width: 11px; height: 11px; border-radius: 50%; flex-shrink: 0; }
|
||||
.legend-dot-ekstrem { background: #dc2626; }
|
||||
.legend-dot-miskin { background: #ea580c; }
|
||||
.legend-dot-rentan { background: #ca8a04; }
|
||||
.legend-dot-ri { background: #16a34a; }
|
||||
.legend-dot-blind { background: #7c3aed; }
|
||||
.legend-dot-laporan { background: #0284c7; }
|
||||
|
||||
/* --- Tabs --- */
|
||||
.tabs { display: flex; gap: 2px; border-bottom: 2px solid var(--clr-border); margin-bottom: 20px; }
|
||||
.tab-btn {
|
||||
padding: 9px 18px; font-size: 0.875rem; font-weight: 600;
|
||||
color: var(--clr-text-muted); border: none; background: none; cursor: pointer;
|
||||
border-bottom: 2px solid transparent; margin-bottom: -2px; transition: var(--transition);
|
||||
}
|
||||
.tab-btn.active { color: var(--clr-primary); border-bottom-color: var(--clr-primary); }
|
||||
.tab-btn:hover:not(.active) { color: var(--clr-text); }
|
||||
|
||||
/* --- Filters --- */
|
||||
.filters-bar {
|
||||
display: flex; gap: 10px; flex-wrap: wrap; align-items: center;
|
||||
margin-bottom: 18px; padding: 14px 16px;
|
||||
background: var(--clr-surface); border: 1px solid var(--clr-border);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.filters-bar select, .filters-bar input {
|
||||
padding: 7px 11px; border: 1px solid var(--clr-border); border-radius: var(--radius-sm);
|
||||
background: var(--clr-surface); font-size: 0.85rem; color: var(--clr-text);
|
||||
}
|
||||
|
||||
/* --- Modal --- */
|
||||
.modal-overlay {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,0.48);
|
||||
z-index: 1000; display: flex; align-items: center; justify-content: center;
|
||||
padding: 20px; opacity: 0; pointer-events: none; transition: opacity 0.2s;
|
||||
}
|
||||
.modal-overlay.active { opacity: 1; pointer-events: all; }
|
||||
.modal {
|
||||
background: var(--clr-surface); border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg); width: 100%; max-width: 560px;
|
||||
max-height: 90vh; overflow-y: auto;
|
||||
transform: translateY(20px); transition: transform 0.2s;
|
||||
}
|
||||
.modal-overlay.active .modal { transform: translateY(0); }
|
||||
.modal-header {
|
||||
padding: 20px 24px; border-bottom: 1px solid var(--clr-border);
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
.modal-header h3 { font-size: 1rem; font-weight: 700; }
|
||||
.modal-close { cursor: pointer; color: var(--clr-text-muted); font-size: 1.3rem; border: none; background: none; }
|
||||
.modal-body { padding: 24px; }
|
||||
.modal-footer { padding: 16px 24px; border-top: 1px solid var(--clr-border); display: flex; justify-content: flex-end; gap: 10px; }
|
||||
|
||||
/* --- Empty State --- */
|
||||
.empty-state {
|
||||
text-align: center; padding: 48px 24px; color: var(--clr-text-muted);
|
||||
}
|
||||
.empty-state-icon { font-size: 3rem; margin-bottom: 12px; opacity: 0.4; }
|
||||
.empty-state h3 { font-size: 1rem; font-weight: 600; margin-bottom: 6px; }
|
||||
.empty-state p { font-size: 0.875rem; }
|
||||
|
||||
/* --- Pagination --- */
|
||||
.pagination { display: flex; gap: 4px; align-items: center; justify-content: center; margin-top: 20px; }
|
||||
.page-btn {
|
||||
padding: 6px 11px; border-radius: var(--radius-sm);
|
||||
font-size: 0.82rem; font-weight: 600; cursor: pointer;
|
||||
border: 1px solid var(--clr-border); background: var(--clr-surface); color: var(--clr-text-muted);
|
||||
transition: var(--transition);
|
||||
}
|
||||
.page-btn:hover, .page-btn.active { background: var(--clr-primary); color: #fff; border-color: var(--clr-primary); }
|
||||
|
||||
/* --- Chart Container --- */
|
||||
.chart-wrap { position: relative; height: 240px; }
|
||||
|
||||
/* --- Sidebar list panel --- */
|
||||
.sidebar-panel {
|
||||
height: 100%; overflow-y: auto;
|
||||
border-left: 1px solid var(--clr-border);
|
||||
background: var(--clr-surface);
|
||||
}
|
||||
.sidebar-panel-header {
|
||||
padding: 16px 20px; border-bottom: 1px solid var(--clr-border);
|
||||
position: sticky; top: 0; background: var(--clr-surface); z-index: 10;
|
||||
}
|
||||
.sidebar-panel-item {
|
||||
padding: 12px 20px; border-bottom: 1px solid #f1f5f9; cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.sidebar-panel-item:hover { background: #f8fafc; }
|
||||
.sidebar-panel-item.active { background: #eff6ff; border-left: 3px solid var(--clr-primary); }
|
||||
.sidebar-panel-item h4 { font-size: 0.85rem; font-weight: 600; }
|
||||
.sidebar-panel-item p { font-size: 0.75rem; color: var(--clr-text-muted); margin-top: 2px; }
|
||||
|
||||
/* --- Public layout --- */
|
||||
.public-nav {
|
||||
position: sticky; top: 0; z-index: 200;
|
||||
background: var(--clr-primary-d); box-shadow: var(--shadow-md);
|
||||
padding: 0 32px; height: 64px;
|
||||
display: flex; align-items: center; gap: 24px;
|
||||
}
|
||||
.public-nav-brand {
|
||||
display: flex; align-items: center; gap: 10px; color: #fff;
|
||||
font-weight: 800; font-size: 1.15rem; text-decoration: none;
|
||||
}
|
||||
.public-nav-brand span { color: var(--clr-accent); }
|
||||
.public-nav-links { display: flex; gap: 4px; margin-left: auto; }
|
||||
.public-nav-links a {
|
||||
color: rgba(255,255,255,0.78); padding: 7px 14px; border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem; font-weight: 500; transition: var(--transition);
|
||||
}
|
||||
.public-nav-links a:hover { background: rgba(255,255,255,0.1); color: #fff; }
|
||||
.public-nav-links a.btn-login {
|
||||
background: var(--clr-accent); color: var(--clr-primary-d);
|
||||
font-weight: 700; padding: 7px 18px;
|
||||
}
|
||||
.public-footer {
|
||||
background: var(--clr-primary-d); color: rgba(255,255,255,0.55);
|
||||
padding: 32px; text-align: center; font-size: 0.82rem; margin-top: 60px;
|
||||
}
|
||||
|
||||
/* --- Login Page --- */
|
||||
.login-page {
|
||||
min-height: 100vh; display: flex;
|
||||
background: linear-gradient(135deg, var(--clr-primary-d) 0%, var(--clr-primary) 60%, var(--clr-primary-l) 100%);
|
||||
}
|
||||
.login-left {
|
||||
flex: 1; display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center; padding: 48px;
|
||||
color: #fff; display: none;
|
||||
}
|
||||
@media (min-width: 900px) { .login-left { display: flex; } }
|
||||
.login-right {
|
||||
width: 100%; max-width: 440px; background: #fff;
|
||||
display: flex; flex-direction: column; justify-content: center;
|
||||
padding: 48px; box-shadow: var(--shadow-lg);
|
||||
}
|
||||
.login-logo { font-size: 3rem; font-weight: 900; letter-spacing: -0.02em; }
|
||||
.login-logo span { color: var(--clr-accent); }
|
||||
.login-tagline { font-size: 0.875rem; color: rgba(255,255,255,0.65); margin-top: 8px; max-width: 360px; text-align: center; line-height: 1.5; }
|
||||
.login-form-title { font-size: 1.5rem; font-weight: 800; color: var(--clr-text); margin-bottom: 6px; }
|
||||
.login-form-sub { font-size: 0.85rem; color: var(--clr-text-muted); margin-bottom: 28px; }
|
||||
|
||||
/* --- Misc --- */
|
||||
.text-muted { color: var(--clr-text-muted); }
|
||||
.text-sm { font-size: 0.82rem; }
|
||||
.text-xs { font-size: 0.72rem; }
|
||||
.font-bold { font-weight: 700; }
|
||||
.text-right { text-align: right; }
|
||||
.text-center { text-align: center; }
|
||||
.mt-auto { margin-top: auto; }
|
||||
.w-full { width: 100%; }
|
||||
.flex { display: flex; }
|
||||
.items-center { align-items: center; }
|
||||
.gap-2 { gap: 8px; }
|
||||
.gap-3 { gap: 12px; }
|
||||
.section-title { font-size: 1.05rem; font-weight: 700; color: var(--clr-text); margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
|
||||
.divider { border: none; border-top: 1px solid var(--clr-border); margin: 20px 0; }
|
||||
|
||||
/* --- Scrollbar --- */
|
||||
::-webkit-scrollbar { width: 5px; height: 5px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 99px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
|
||||
|
||||
/* --- Responsive --- */
|
||||
@media (max-width: 900px) {
|
||||
.app-shell { grid-template-columns: 1fr; }
|
||||
.sidebar { display: none; }
|
||||
.topbar, .main-content { grid-column: 1; }
|
||||
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.form-row.cols-3 { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.main-content { padding: 16px; }
|
||||
.topbar { padding: 0 16px; }
|
||||
.stats-grid { grid-template-columns: 1fr 1fr; }
|
||||
.form-row.cols-2, .form-row.cols-3 { grid-template-columns: 1fr; }
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 714 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 714 KiB |
@@ -0,0 +1,656 @@
|
||||
// public/js/app.js — SINERGI Core JavaScript v1.1
|
||||
// Fix: Auto-zoom ke Kota Pontianak + auto-fitBounds setelah data dimuat
|
||||
|
||||
/* ============================================================
|
||||
KONSTANTA KOTA PONTIANAK
|
||||
============================================================ */
|
||||
const PONTIANAK = {
|
||||
lat: -0.0263,
|
||||
lng: 109.3425,
|
||||
zoom: 13,
|
||||
// Bounding box seluruh Kota Pontianak (untuk fitBounds fallback)
|
||||
bounds: [[-0.115, 109.280], [0.055, 109.420]]
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
UTILITY FUNCTIONS
|
||||
============================================================ */
|
||||
const App = {
|
||||
url: document.querySelector('meta[name="app-url"]')?.content || '',
|
||||
|
||||
formatNumber(n) {
|
||||
return Number(n || 0).toLocaleString('id-ID');
|
||||
},
|
||||
|
||||
formatRupiah(n) {
|
||||
return 'Rp ' + Number(n || 0).toLocaleString('id-ID');
|
||||
},
|
||||
|
||||
formatTanggal(str) {
|
||||
if (!str) return '-';
|
||||
const bulan = ['','Januari','Februari','Maret','April','Mei','Juni',
|
||||
'Juli','Agustus','September','Oktober','November','Desember'];
|
||||
const d = new Date(str);
|
||||
return `${d.getDate()} ${bulan[d.getMonth() + 1]} ${d.getFullYear()}`;
|
||||
},
|
||||
|
||||
async fetchJSON(url) {
|
||||
try {
|
||||
const r = await fetch(url);
|
||||
return await r.json();
|
||||
} catch (e) {
|
||||
console.error('Fetch error:', url, e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
showModal(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) { el.classList.add('active'); document.body.style.overflow = 'hidden'; }
|
||||
},
|
||||
hideModal(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) { el.classList.remove('active'); document.body.style.overflow = ''; }
|
||||
},
|
||||
|
||||
confirmDelete(url, msg = 'Apakah Anda yakin ingin menghapus data ini?') {
|
||||
if (!confirm(msg)) return false;
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = url;
|
||||
const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
form.innerHTML = `<input type="hidden" name="_token" value="${csrf}">`;
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
},
|
||||
|
||||
startClock() {
|
||||
const el = document.getElementById('live-clock');
|
||||
if (!el) return;
|
||||
const update = () => {
|
||||
const now = new Date();
|
||||
el.textContent = now.toLocaleTimeString('id-ID', {
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||||
});
|
||||
};
|
||||
update();
|
||||
setInterval(update, 1000);
|
||||
},
|
||||
|
||||
initFlash() {
|
||||
document.querySelectorAll('.flash-message').forEach(el => {
|
||||
setTimeout(() => {
|
||||
el.style.transition = 'opacity 0.5s, max-height 0.5s';
|
||||
el.style.opacity = '0';
|
||||
el.style.maxHeight = '0';
|
||||
el.style.overflow = 'hidden';
|
||||
setTimeout(() => el.remove(), 500);
|
||||
}, 4500);
|
||||
});
|
||||
},
|
||||
|
||||
initTableSearch(inputId, tableId) {
|
||||
const input = document.getElementById(inputId);
|
||||
const table = document.getElementById(tableId);
|
||||
if (!input || !table) return;
|
||||
input.addEventListener('input', () => {
|
||||
const q = input.value.toLowerCase();
|
||||
table.querySelectorAll('tbody tr').forEach(row => {
|
||||
row.style.display = row.textContent.toLowerCase().includes(q) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
init() {
|
||||
this.startClock();
|
||||
this.initFlash();
|
||||
|
||||
// Close modal on overlay click
|
||||
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
||||
overlay.addEventListener('click', e => {
|
||||
if (e.target === overlay) overlay.classList.remove('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Close modal on ESC
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') {
|
||||
document.querySelectorAll('.modal-overlay.active')
|
||||
.forEach(m => m.classList.remove('active'));
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Sidebar active link highlight
|
||||
const path = window.location.pathname;
|
||||
document.querySelectorAll('.sidebar-nav a').forEach(a => {
|
||||
const href = a.getAttribute('href');
|
||||
if (href && href !== '/' && path.startsWith(href)) {
|
||||
a.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
GIS MAP MODULE — dengan auto-zoom ke Pontianak
|
||||
============================================================ */
|
||||
const GISMap = {
|
||||
map: null,
|
||||
layers: {},
|
||||
overlayLayers: {},
|
||||
heatLayer: null,
|
||||
_allMarkerBounds: [], // Kumpulkan semua koordinat untuk fitBounds akhir
|
||||
|
||||
warnaMiskin: {
|
||||
'Miskin Ekstrem': '#dc2626',
|
||||
'Miskin': '#ea580c',
|
||||
'Rentan Miskin': '#ca8a04',
|
||||
},
|
||||
|
||||
createMarker(lat, lng, color, size = 10) {
|
||||
return L.circleMarker([lat, lng], {
|
||||
radius: size,
|
||||
fillColor: color,
|
||||
color: '#fff',
|
||||
weight: 2,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.88
|
||||
});
|
||||
},
|
||||
|
||||
createRIMarker(ri) {
|
||||
const icon = L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="
|
||||
background:#16a34a;color:#fff;width:32px;height:32px;
|
||||
border-radius:50% 50% 50% 0;transform:rotate(-45deg);
|
||||
border:2.5px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,0.35);
|
||||
display:flex;align-items:center;justify-content:center;font-size:14px;
|
||||
"><span style="transform:rotate(45deg)">⛪</span></div>`,
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 32],
|
||||
popupAnchor: [0, -34]
|
||||
});
|
||||
return L.marker([parseFloat(ri.lat), parseFloat(ri.lng)], { icon });
|
||||
},
|
||||
|
||||
// ─── INISIALISASI PETA ─────────────────────────────────────
|
||||
// Selalu mulai dari Kota Pontianak, zoom sesuai konteks
|
||||
init(containerId, lat, lng, zoom) {
|
||||
// Reset state untuk peta baru
|
||||
this._allMarkerBounds = [];
|
||||
|
||||
// Gunakan koordinat Pontianak sebagai default yang kuat
|
||||
const centerLat = (lat && !isNaN(lat)) ? lat : PONTIANAK.lat;
|
||||
const centerLng = (lng && !isNaN(lng)) ? lng : PONTIANAK.lng;
|
||||
const initZoom = (zoom && !isNaN(zoom)) ? zoom : PONTIANAK.zoom;
|
||||
|
||||
this.map = L.map(containerId, {
|
||||
zoomControl: true,
|
||||
attributionControl: true,
|
||||
// Batasi pan agar tidak keluar wilayah Kalimantan Barat
|
||||
maxBounds: [[-2.0, 107.5], [2.0, 112.0]],
|
||||
maxBoundsViscosity: 0.8
|
||||
});
|
||||
|
||||
// ── Langsung set view ke Pontianak dulu sebelum tile load ──
|
||||
this.map.setView([centerLat, centerLng], initZoom);
|
||||
|
||||
// Tile layers
|
||||
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
maxZoom: 19,
|
||||
// Tambahkan subdomains untuk load lebih cepat
|
||||
subdomains: ['a','b','c']
|
||||
});
|
||||
|
||||
const satellite = L.tileLayer(
|
||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
attribution: 'Tiles © Esri',
|
||||
maxZoom: 19
|
||||
}
|
||||
);
|
||||
|
||||
const topo = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenTopoMap',
|
||||
maxZoom: 17
|
||||
});
|
||||
|
||||
osm.addTo(this.map);
|
||||
|
||||
this.layers = {
|
||||
'🗺 Peta Jalan': osm,
|
||||
'🛰 Satelit': satellite,
|
||||
'🏔 Topografi': topo
|
||||
};
|
||||
|
||||
// Layer control dasar
|
||||
this._layerControl = L.control.layers(this.layers, {}, {
|
||||
position: 'topright',
|
||||
collapsed: true
|
||||
}).addTo(this.map);
|
||||
|
||||
// Tombol reset zoom ke Pontianak
|
||||
this._addResetButton();
|
||||
|
||||
return this.map;
|
||||
},
|
||||
|
||||
// Tombol reset zoom ke seluruh Kota Pontianak
|
||||
_addResetButton() {
|
||||
const ResetControl = L.Control.extend({
|
||||
options: { position: 'topleft' },
|
||||
onAdd: () => {
|
||||
const btn = L.DomUtil.create('button', '');
|
||||
btn.innerHTML = '🏙';
|
||||
btn.title = 'Kembali ke Kota Pontianak';
|
||||
btn.style.cssText = `
|
||||
width:30px;height:30px;border:2px solid rgba(0,0,0,0.2);
|
||||
border-radius:4px;background:#fff;cursor:pointer;
|
||||
font-size:14px;display:flex;align-items:center;
|
||||
justify-content:center;box-shadow:0 1px 5px rgba(0,0,0,0.2);
|
||||
`;
|
||||
L.DomEvent.on(btn, 'click', L.DomEvent.stopPropagation)
|
||||
.on(btn, 'click', L.DomEvent.preventDefault)
|
||||
.on(btn, 'click', () => {
|
||||
this.map.flyToBounds(PONTIANAK.bounds, {
|
||||
duration: 0.8,
|
||||
padding: [20, 20]
|
||||
});
|
||||
});
|
||||
return btn;
|
||||
}
|
||||
});
|
||||
new ResetControl().addTo(this.map);
|
||||
},
|
||||
|
||||
// ─── LAYER WARGA MISKIN ────────────────────────────────────
|
||||
addWargaLayer(wargaData, onClickCb) {
|
||||
const group = L.featureGroup(); // featureGroup mendukung getBounds()
|
||||
wargaData.forEach(w => {
|
||||
if (!w.lat || !w.lng) return;
|
||||
const lat = parseFloat(w.lat);
|
||||
const lng = parseFloat(w.lng);
|
||||
if (isNaN(lat) || isNaN(lng)) return;
|
||||
|
||||
const color = w.is_blind_spot
|
||||
? '#7c3aed'
|
||||
: (this.warnaMiskin[w.status_kemiskinan] || '#6b7280');
|
||||
const size = w.status_kemiskinan === 'Miskin Ekstrem' ? 11 : 9;
|
||||
|
||||
const m = this.createMarker(lat, lng, color, size);
|
||||
|
||||
const popup = `
|
||||
<div style="min-width:200px;font-family:Inter,system-ui,sans-serif">
|
||||
<strong style="font-size:13px">${w.nama || 'Data laporan'}</strong><br>
|
||||
<span style="font-size:11px;color:#6b7280">${w.kelurahan || ''}</span><br>
|
||||
<span style="display:inline-block;margin-top:5px;padding:2px 9px;border-radius:10px;
|
||||
font-size:11px;font-weight:700;background:${color}22;color:${color}">
|
||||
${w.status_kemiskinan || ''}
|
||||
</span>
|
||||
${w.level_prioritas ? `<br><span style="font-size:11px;margin-top:3px;display:block">
|
||||
Prioritas: <strong>${w.level_prioritas}</strong>
|
||||
${w.skor_prioritas ? `(${w.skor_prioritas}/100)` : ''}</span>` : ''}
|
||||
${w.penghasilan_bulanan !== undefined ? `<span style="font-size:11px;color:#6b7280">
|
||||
Penghasilan: Rp ${Number(w.penghasilan_bulanan||0).toLocaleString('id-ID')}/bln</span>` : ''}
|
||||
${w.is_blind_spot ? '<br><span style="color:#7c3aed;font-weight:700;font-size:11px;display:block;margin-top:3px">⚠ Blind Spot — Belum terjangkau RI</span>' : ''}
|
||||
</div>`;
|
||||
|
||||
m.bindPopup(popup, { maxWidth: 240 });
|
||||
if (onClickCb) m.on('click', () => onClickCb(w));
|
||||
group.addLayer(m);
|
||||
this._allMarkerBounds.push([lat, lng]);
|
||||
});
|
||||
group.addTo(this.map);
|
||||
return group;
|
||||
},
|
||||
|
||||
// ─── LAYER RUMAH IBADAH ────────────────────────────────────
|
||||
addRILayer(riData, onClickCb) {
|
||||
const group = L.featureGroup();
|
||||
riData.forEach(ri => {
|
||||
if (!ri.lat || !ri.lng) return;
|
||||
const lat = parseFloat(ri.lat);
|
||||
const lng = parseFloat(ri.lng);
|
||||
if (isNaN(lat) || isNaN(lng)) return;
|
||||
|
||||
const m = this.createRIMarker(ri);
|
||||
|
||||
// Radius lingkaran pelayanan
|
||||
const radius = parseInt(ri.radius_meter) || 500;
|
||||
const circle = L.circle([lat, lng], {
|
||||
radius,
|
||||
fillColor: '#16a34a',
|
||||
fillOpacity: 0.07,
|
||||
color: '#16a34a',
|
||||
weight: 1.5,
|
||||
dashArray: '6,5',
|
||||
interactive: false
|
||||
});
|
||||
|
||||
const popup = `
|
||||
<div style="min-width:200px;font-family:Inter,system-ui,sans-serif">
|
||||
<strong style="font-size:13px">⛪ ${ri.nama}</strong><br>
|
||||
<span style="font-size:11px;color:#6b7280">${ri.jenis || ''} • ${ri.kelurahan || ''}</span>
|
||||
${ri.alamat ? `<br><span style="font-size:11px;color:#374151">${ri.alamat}</span>` : ''}
|
||||
<br><span style="font-size:11px;color:#16a34a;font-weight:600;display:block;margin-top:4px">
|
||||
📏 Radius pelayanan: ${radius.toLocaleString()}m</span>
|
||||
${ri.nama_ketua ? `<span style="font-size:11px;color:#6b7280">Ketua: ${ri.nama_ketua}</span>` : ''}
|
||||
</div>`;
|
||||
|
||||
m.bindPopup(popup, { maxWidth: 240 });
|
||||
if (onClickCb) m.on('click', () => onClickCb(ri));
|
||||
|
||||
group.addLayer(circle);
|
||||
group.addLayer(m);
|
||||
this._allMarkerBounds.push([lat, lng]);
|
||||
});
|
||||
group.addTo(this.map);
|
||||
return group;
|
||||
},
|
||||
|
||||
// ─── LAYER LAPORAN PUBLIK ──────────────────────────────────
|
||||
addLaporanLayer(laporanData) {
|
||||
const group = L.featureGroup();
|
||||
laporanData.forEach(lap => {
|
||||
if (!lap.lat || !lap.lng) return;
|
||||
const lat = parseFloat(lap.lat);
|
||||
const lng = parseFloat(lap.lng);
|
||||
if (isNaN(lat) || isNaN(lng)) return;
|
||||
|
||||
const m = this.createMarker(lat, lng, '#0284c7', 8);
|
||||
m.bindPopup(`
|
||||
<div style="min-width:180px;font-family:Inter,system-ui,sans-serif">
|
||||
<strong style="font-size:13px">📋 Laporan Masuk</strong><br>
|
||||
<span style="font-size:12px">${lap.nama_warga || ''}</span><br>
|
||||
<code style="font-size:10px;background:#f1f5f9;padding:1px 5px;border-radius:3px">
|
||||
${lap.kode_laporan || ''}</code>
|
||||
</div>`, { maxWidth: 200 });
|
||||
group.addLayer(m);
|
||||
this._allMarkerBounds.push([lat, lng]);
|
||||
});
|
||||
group.addTo(this.map);
|
||||
return group;
|
||||
},
|
||||
|
||||
// ─── HEATMAP LAYER ─────────────────────────────────────────
|
||||
addHeatmap(points) {
|
||||
if (typeof L.heatLayer === 'undefined') {
|
||||
console.warn('Leaflet.heat tidak tersedia');
|
||||
return;
|
||||
}
|
||||
const data = points
|
||||
.map(p => [parseFloat(p.lat), parseFloat(p.lng), parseFloat(p.intensity || 0.5)])
|
||||
.filter(p => !isNaN(p[0]) && !isNaN(p[1]) && p[0] !== 0);
|
||||
|
||||
if (this.heatLayer) this.map.removeLayer(this.heatLayer);
|
||||
this.heatLayer = L.heatLayer(data, {
|
||||
radius: 28,
|
||||
blur: 22,
|
||||
maxZoom: 16,
|
||||
max: 1.0,
|
||||
gradient: {
|
||||
0.0: 'transparent',
|
||||
0.3: '#fef9c3',
|
||||
0.55: '#fb923c',
|
||||
0.75: '#ef4444',
|
||||
1.0: '#7f1d1d'
|
||||
}
|
||||
}).addTo(this.map);
|
||||
},
|
||||
|
||||
// ─── AUTO FIT BOUNDS ───────────────────────────────────────
|
||||
// Panggil ini SETELAH semua layer dimuat untuk zoom ke data
|
||||
autoFitBounds(forceKotaPontianak = false) {
|
||||
if (forceKotaPontianak || this._allMarkerBounds.length === 0) {
|
||||
// Tidak ada data → tampilkan seluruh Kota Pontianak
|
||||
this.map.flyToBounds(PONTIANAK.bounds, {
|
||||
duration: 0.6,
|
||||
padding: [20, 20],
|
||||
maxZoom: 14
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const bounds = L.latLngBounds(this._allMarkerBounds);
|
||||
if (bounds.isValid()) {
|
||||
// Jika data sangat menyebar → fit ke batas data
|
||||
// Jika data sangat mepet (1-2 titik) → zoom level wajar
|
||||
const boundsSize = bounds.getNorthEast().distanceTo(bounds.getSouthWest());
|
||||
if (boundsSize < 500) {
|
||||
// Data terlalu dekat → zoom ke tengah dengan zoom 15
|
||||
this.map.flyTo(bounds.getCenter(), 15, { duration: 0.6 });
|
||||
} else {
|
||||
this.map.flyToBounds(bounds, {
|
||||
duration: 0.6,
|
||||
padding: [40, 40],
|
||||
maxZoom: 15 // Jangan zoom terlalu dekat
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.map.flyToBounds(PONTIANAK.bounds, { duration: 0.5 });
|
||||
}
|
||||
} catch (e) {
|
||||
this.map.flyToBounds(PONTIANAK.bounds, { duration: 0.5 });
|
||||
}
|
||||
},
|
||||
|
||||
// fitBounds dari layerGroup (kompatibel dengan kode lama)
|
||||
fitBounds(layerGroup) {
|
||||
try {
|
||||
const bounds = layerGroup.getBounds();
|
||||
if (bounds.isValid()) {
|
||||
this.map.flyToBounds(bounds, { duration: 0.5, padding: [30, 30] });
|
||||
}
|
||||
} catch (e) {
|
||||
this.autoFitBounds(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
CHART MODULE (Chart.js wrapper)
|
||||
============================================================ */
|
||||
const Charts = {
|
||||
defaults: {
|
||||
fontFamily: "'Inter', system-ui, sans-serif",
|
||||
colors: ['#1a3a5c', '#e8a020', '#16a34a', '#dc2626', '#0284c7', '#7c3aed', '#ea580c', '#0d9488']
|
||||
},
|
||||
|
||||
bar(canvasId, labels, datasets, opts = {}) {
|
||||
const ctx = document.getElementById(canvasId);
|
||||
if (!ctx) return null;
|
||||
return new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels,
|
||||
datasets: datasets.map((d, i) => ({
|
||||
backgroundColor: this.defaults.colors[i % this.defaults.colors.length] + 'cc',
|
||||
borderColor: this.defaults.colors[i % this.defaults.colors.length],
|
||||
borderRadius: 4,
|
||||
borderWidth: 1,
|
||||
...d
|
||||
}))
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: datasets.length > 1 },
|
||||
tooltip: { callbacks: { label: ctx => ' ' + Number(ctx.raw).toLocaleString('id-ID') } }
|
||||
},
|
||||
scales: {
|
||||
y: { beginAtZero: true, grid: { color: '#f1f5f9' }, ticks: { font: { size: 11 } } },
|
||||
x: { grid: { display: false }, ticks: { font: { size: 11 } } }
|
||||
},
|
||||
...opts
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
line(canvasId, labels, datasets, opts = {}) {
|
||||
const ctx = document.getElementById(canvasId);
|
||||
if (!ctx) return null;
|
||||
return new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels,
|
||||
datasets: datasets.map((d, i) => ({
|
||||
borderColor: this.defaults.colors[i % this.defaults.colors.length],
|
||||
backgroundColor: this.defaults.colors[i % this.defaults.colors.length] + '18',
|
||||
tension: 0.4, fill: true, borderWidth: 2.5,
|
||||
pointRadius: 4, pointHoverRadius: 6,
|
||||
...d
|
||||
}))
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: datasets.length > 1 } },
|
||||
scales: {
|
||||
y: { beginAtZero: true, grid: { color: '#f1f5f9' }, ticks: { font: { size: 11 } } },
|
||||
x: { grid: { display: false }, ticks: { font: { size: 11 } } }
|
||||
},
|
||||
...opts
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
donut(canvasId, labels, data, colors) {
|
||||
const ctx = document.getElementById(canvasId);
|
||||
if (!ctx) return null;
|
||||
return new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
data,
|
||||
backgroundColor: colors || this.defaults.colors,
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: { font: { size: 11 }, padding: 14, usePointStyle: true }
|
||||
}
|
||||
},
|
||||
cutout: '65%'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
LOCATION PICKER — untuk form laporan & input warga
|
||||
============================================================ */
|
||||
const LocationPicker = {
|
||||
map: null,
|
||||
marker: null,
|
||||
|
||||
init(mapId, latInputId, lngInputId, initLat, initLng) {
|
||||
// Selalu mulai dari Pontianak
|
||||
const startLat = (initLat && !isNaN(initLat)) ? initLat : PONTIANAK.lat;
|
||||
const startLng = (initLng && !isNaN(initLng)) ? initLng : PONTIANAK.lng;
|
||||
|
||||
this.map = L.map(mapId, { zoomControl: true }).setView([startLat, startLng], 14);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap',
|
||||
maxZoom: 19,
|
||||
subdomains: ['a','b','c']
|
||||
}).addTo(this.map);
|
||||
|
||||
// Jika ada koordinat awal, langsung pasang marker
|
||||
if (initLat && initLng && !isNaN(initLat)) {
|
||||
this.placeMarker(initLat, initLng, latInputId, lngInputId);
|
||||
}
|
||||
|
||||
// Klik peta = pasang / pindah marker
|
||||
this.map.on('click', e => {
|
||||
this.placeMarker(e.latlng.lat, e.latlng.lng, latInputId, lngInputId);
|
||||
});
|
||||
|
||||
// Kontrol tombol lokasi saya
|
||||
this._addGeolocateButton(latInputId, lngInputId);
|
||||
|
||||
// Instruksi klik
|
||||
const hint = L.control({ position: 'bottomleft' });
|
||||
hint.onAdd = () => {
|
||||
const d = L.DomUtil.create('div');
|
||||
d.style.cssText = 'background:rgba(255,255,255,0.9);padding:6px 10px;border-radius:6px;font-size:11px;color:#374151;box-shadow:0 1px 4px rgba(0,0,0,0.15)';
|
||||
d.innerHTML = '📍 Klik peta atau drag marker untuk menentukan lokasi';
|
||||
return d;
|
||||
};
|
||||
hint.addTo(this.map);
|
||||
},
|
||||
|
||||
placeMarker(lat, lng, latInputId, lngInputId) {
|
||||
if (this.marker) this.map.removeLayer(this.marker);
|
||||
this.marker = L.marker([lat, lng], {
|
||||
draggable: true,
|
||||
autoPan: true
|
||||
}).addTo(this.map);
|
||||
this.marker.bindPopup('📍 Lokasi dipilih').openPopup();
|
||||
|
||||
document.getElementById(latInputId).value = lat.toFixed(7);
|
||||
document.getElementById(lngInputId).value = lng.toFixed(7);
|
||||
|
||||
this.marker.on('dragend', e => {
|
||||
const pos = e.target.getLatLng();
|
||||
document.getElementById(latInputId).value = pos.lat.toFixed(7);
|
||||
document.getElementById(lngInputId).value = pos.lng.toFixed(7);
|
||||
});
|
||||
},
|
||||
|
||||
_addGeolocateButton(latInputId, lngInputId) {
|
||||
const GeoBtn = L.Control.extend({
|
||||
options: { position: 'topright' },
|
||||
onAdd: () => {
|
||||
const btn = L.DomUtil.create('button', '');
|
||||
btn.innerHTML = '📍 Lokasi Saya';
|
||||
btn.title = 'Gunakan lokasi GPS saya';
|
||||
btn.style.cssText = `
|
||||
padding:6px 10px;border:2px solid rgba(0,0,0,0.2);border-radius:4px;
|
||||
background:#1a3a5c;color:#fff;cursor:pointer;font-size:11px;font-weight:600;
|
||||
box-shadow:0 1px 5px rgba(0,0,0,0.2);white-space:nowrap;
|
||||
`;
|
||||
L.DomEvent.disableClickPropagation(btn);
|
||||
btn.onclick = () => {
|
||||
if (!navigator.geolocation) {
|
||||
alert('GPS tidak tersedia di browser Anda');
|
||||
return;
|
||||
}
|
||||
btn.innerHTML = '⌛ Mencari...';
|
||||
btn.disabled = true;
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
pos => {
|
||||
const { latitude, longitude } = pos.coords;
|
||||
this.placeMarker(latitude, longitude, latInputId, lngInputId);
|
||||
this.map.flyTo([latitude, longitude], 17, { duration: 0.8 });
|
||||
btn.innerHTML = '📍 Lokasi Saya';
|
||||
btn.disabled = false;
|
||||
},
|
||||
err => {
|
||||
alert('Gagal mendapatkan lokasi: ' + err.message);
|
||||
btn.innerHTML = '📍 Lokasi Saya';
|
||||
btn.disabled = false;
|
||||
},
|
||||
{ enableHighAccuracy: true, timeout: 10000 }
|
||||
);
|
||||
};
|
||||
return btn;
|
||||
}
|
||||
});
|
||||
new GeoBtn().addTo(this.map);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Bootstrap ──
|
||||
document.addEventListener('DOMContentLoaded', () => App.init());
|
||||
Reference in New Issue
Block a user