Deploy WebGIS 01-05 dengan Docker

This commit is contained in:
2026-06-12 17:58:06 +07:00
commit 1bf000979c
76 changed files with 12301 additions and 0 deletions
+373
View File
@@ -0,0 +1,373 @@
-- phpMyAdmin SQL Dump
-- version 5.2.3
-- https://www.phpmyadmin.net/
--
-- Host: localhost:3306
-- Generation Time: Jun 12, 2026 at 10:30 AM
-- Server version: 8.4.3
-- PHP Version: 8.3.30
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Database: `05_povertydb`
--
-- --------------------------------------------------------
--
-- Table structure for table `penerima_bantuan`
--
CREATE TABLE `penerima_bantuan` (
`id` int NOT NULL,
`user_id` int DEFAULT NULL,
`rumah_ibadah_id` int DEFAULT NULL,
`nama_penerima` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`nik` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`no_hp` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
`alamat` text COLLATE utf8mb4_unicode_ci NOT NULL,
`pekerjaan` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`penghasilan` int NOT NULL,
`jumlah_tanggungan` int NOT NULL,
`kondisi_rumah` enum('baik','sedang','rusak') COLLATE utf8mb4_unicode_ci NOT NULL,
`jenis_bantuan` enum('zakat','sembako','uang_tunai','lainnya') COLLATE utf8mb4_unicode_ci NOT NULL,
`latitude` double NOT NULL,
`longitude` double NOT NULL,
`jarak_meter` double DEFAULT NULL,
`status_pengajuan` enum('menunggu','diterima','ditolak','dibatalkan') COLLATE utf8mb4_unicode_ci DEFAULT 'menunggu',
`status_hapus` enum('normal','menunggu','disetujui','ditolak') COLLATE utf8mb4_unicode_ci DEFAULT 'normal',
`alasan_hapus` text COLLATE utf8mb4_unicode_ci,
`alasan_status` text COLLATE utf8mb4_unicode_ci,
`sumber_pengajuan` enum('mandiri','admin_rumah_ibadah') COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `penerima_bantuan`
--
INSERT INTO `penerima_bantuan` (`id`, `user_id`, `rumah_ibadah_id`, `nama_penerima`, `nik`, `no_hp`, `alamat`, `pekerjaan`, `penghasilan`, `jumlah_tanggungan`, `kondisi_rumah`, `jenis_bantuan`, `latitude`, `longitude`, `jarak_meter`, `status_pengajuan`, `status_hapus`, `alasan_hapus`, `alasan_status`, `sumber_pengajuan`, `created_at`, `updated_at`) VALUES
(1, 4, 1, 'Jotok', '60103011704940005', '08567891325', 'Jl. Bujang Malaka, RT/RW 024/008, Beringin, Kapuas, Sanggau', 'Tukang Antar Koran', 500000, 2, 'sedang', 'sembako', -0.07088082929060342, 109.36113953590394, 160.17, 'diterima', 'normal', NULL, '', 'mandiri', '2026-06-09 10:10:05', '2026-06-09 11:56:27'),
(2, 6, 5, 'caca', '124854', '410154', 'Gg. Kasturi Raja, Jl. Kenari', 'Mahasiswa', 0, 0, 'baik', 'uang_tunai', -0.020854501391015294, 109.33250427246095, 259.51, 'diterima', 'normal', NULL, '', 'mandiri', '2026-06-09 10:57:26', '2026-06-09 14:53:12'),
(4, NULL, 2, 'angel1', '123456', '123456', '1234', 'wwwwww', 0, 0, 'sedang', 'uang_tunai', -0.025105475576032914, 109.33623790740968, 128.27, 'diterima', 'normal', NULL, '', 'admin_rumah_ibadah', '2026-06-09 11:27:12', '2026-06-09 11:28:31'),
(5, NULL, 2, 'angel2', '123', '123', 'ssss', 'sssss', 0, 0, 'sedang', 'uang_tunai', -0.025245714451433383, 109.33630228042604, 122.41, 'diterima', 'normal', NULL, '', 'admin_rumah_ibadah', '2026-06-09 11:28:22', '2026-06-09 11:28:29'),
(6, 4, 1, 'k', 'g', 'u', 'k', 'i', 5, 7, 'baik', 'zakat', -0.06864918884517689, 109.3591332435608, 218.94, 'dibatalkan', 'disetujui', 'h', 'h', 'admin_rumah_ibadah', '2026-06-09 11:57:59', '2026-06-09 12:19:58'),
(7, 9, 5, 'penerima03', '124854', '123', 'Gang Merak II, Mariana, Pontianak Kota, Pontianak, Kalimantan Barat, Kalimantan, 78112, Indonesia', 'Mahasiswa', 50000, 1, 'sedang', 'sembako', -0.02033240043514327, 109.33441936969759, 136.81, 'diterima', 'normal', NULL, '', 'admin_rumah_ibadah', '2026-06-09 14:52:40', '2026-06-09 14:53:12'),
(8, 10, 1, 'penerima04', '222223432', '13334341', 'Komplek Mitra Indah Utama I, Bangka Belitung Darat, Pontianak Tenggara, Pontianak, Kalimantan Barat, Kalimantan, 78124, Indonesia', 'wiraswasta', 1000000, 3, 'baik', 'sembako', -0.06962094072669782, 109.35720741748811, 299.36, 'diterima', 'normal', NULL, '', 'admin_rumah_ibadah', '2026-06-10 01:22:26', '2026-06-10 01:22:34'),
(9, 11, 1, 'penerima05', '111111111111111', '11111111111111111', 'Nilam Permai, Sungai Raya, Kubu Raya, Kalimantan Barat, Kalimantan, 78124, Indonesia', 'disabilitas', 0, 1, 'rusak', 'uang_tunai', -0.07270815709759908, 109.3613098561764, 299.07, 'diterima', 'normal', NULL, '', 'admin_rumah_ibadah', '2026-06-10 01:25:25', '2026-06-10 01:25:37');
-- --------------------------------------------------------
--
-- Table structure for table `penyaluran_bantuan`
--
CREATE TABLE `penyaluran_bantuan` (
`id` int NOT NULL,
`program_id` int DEFAULT NULL,
`penerima_id` int NOT NULL,
`rumah_ibadah_id` int NOT NULL,
`admin_id` int NOT NULL,
`jenis_bantuan` enum('zakat','sembako','uang_tunai','lainnya') COLLATE utf8mb4_unicode_ci NOT NULL,
`nominal` int DEFAULT NULL,
`jumlah_item` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`bantuan_ke` int DEFAULT NULL,
`keterangan` text COLLATE utf8mb4_unicode_ci,
`tanggal_penyaluran` date NOT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `penyaluran_bantuan`
--
INSERT INTO `penyaluran_bantuan` (`id`, `program_id`, `penerima_id`, `rumah_ibadah_id`, `admin_id`, `jenis_bantuan`, `nominal`, `jumlah_item`, `bantuan_ke`, `keterangan`, `tanggal_penyaluran`, `created_at`) VALUES
(1, 5, 5, 2, 5, 'zakat', 10000, NULL, 1, 'gg', '2026-06-09', '2026-06-09 11:33:36');
-- --------------------------------------------------------
--
-- Table structure for table `program_bantuan`
--
CREATE TABLE `program_bantuan` (
`id` int NOT NULL,
`rumah_ibadah_id` int DEFAULT NULL,
`nama_program` varchar(150) COLLATE utf8mb4_unicode_ci NOT NULL,
`jenis_bantuan` enum('zakat','sembako','uang_tunai','lainnya') COLLATE utf8mb4_unicode_ci NOT NULL,
`deskripsi` text COLLATE utf8mb4_unicode_ci,
`bulan` int NOT NULL,
`tahun` int NOT NULL,
`target_frekuensi` int DEFAULT '1',
`nominal` int DEFAULT NULL,
`jumlah_item` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`status_program` enum('aktif','selesai','dibatalkan') COLLATE utf8mb4_unicode_ci DEFAULT 'aktif',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `program_bantuan`
--
INSERT INTO `program_bantuan` (`id`, `rumah_ibadah_id`, `nama_program`, `jenis_bantuan`, `deskripsi`, `bulan`, `tahun`, `target_frekuensi`, `nominal`, `jumlah_item`, `status_program`, `created_at`, `updated_at`) VALUES
(3, 2, 'a', 'uang_tunai', 'a', 6, 2026, 1, NULL, NULL, 'aktif', '2026-06-09 11:29:28', '2026-06-09 11:29:28'),
(4, 2, 'b', 'uang_tunai', 'b', 6, 2026, 1, NULL, NULL, 'aktif', '2026-06-09 11:29:47', '2026-06-09 11:29:47'),
(5, 2, 'c', 'sembako', 'c', 6, 2026, 1, NULL, NULL, 'aktif', '2026-06-09 11:30:54', '2026-06-09 11:30:54');
-- --------------------------------------------------------
--
-- Table structure for table `riwayat_status`
--
CREATE TABLE `riwayat_status` (
`id` int NOT NULL,
`penerima_id` int NOT NULL,
`user_id` int NOT NULL,
`status_lama` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`status_baru` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`catatan` text COLLATE utf8mb4_unicode_ci,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `riwayat_status`
--
INSERT INTO `riwayat_status` (`id`, `penerima_id`, `user_id`, `status_lama`, `status_baru`, `catatan`, `created_at`) VALUES
(1, 1, 4, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-09 10:10:05'),
(2, 1, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-09 10:48:16'),
(3, 2, 6, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-09 10:57:26'),
(4, 2, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-09 10:59:04'),
(7, 4, 5, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-09 11:27:12'),
(8, 5, 1, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-09 11:28:22'),
(9, 5, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-09 11:28:29'),
(10, 4, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-09 11:28:31'),
(11, 6, 1, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-09 11:57:59'),
(12, 6, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-09 11:58:09'),
(13, 6, 1, 'diterima', 'dibatalkan', 'jotok 2 rmh', '2026-06-09 11:58:22'),
(14, 6, 1, 'dibatalkan', 'hapus_disetujui', 'h', '2026-06-09 12:19:58'),
(15, 7, 1, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-09 14:52:40'),
(16, 7, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-09 14:52:49'),
(17, 8, 1, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-10 01:22:26'),
(18, 8, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-10 01:22:34'),
(19, 9, 2, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-10 01:25:25'),
(20, 9, 2, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-10 01:25:37');
-- --------------------------------------------------------
--
-- Table structure for table `rumah_ibadah`
--
CREATE TABLE `rumah_ibadah` (
`id` int NOT NULL,
`user_id` int DEFAULT NULL,
`nama_rumah_ibadah` varchar(150) COLLATE utf8mb4_unicode_ci NOT NULL,
`agama` enum('islam','kristen','katolik','hindu','buddha','konghucu') COLLATE utf8mb4_unicode_ci NOT NULL,
`alamat` text COLLATE utf8mb4_unicode_ci NOT NULL,
`nomor_hp` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
`latitude` double NOT NULL,
`longitude` double NOT NULL,
`status_pengajuan` enum('menunggu','diterima','ditolak') COLLATE utf8mb4_unicode_ci DEFAULT 'menunggu',
`status_hapus` enum('normal','menunggu','disetujui','ditolak') COLLATE utf8mb4_unicode_ci DEFAULT 'normal',
`alasan_hapus` text COLLATE utf8mb4_unicode_ci,
`keterangan` text COLLATE utf8mb4_unicode_ci,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `rumah_ibadah`
--
INSERT INTO `rumah_ibadah` (`id`, `user_id`, `nama_rumah_ibadah`, `agama`, `alamat`, `nomor_hp`, `latitude`, `longitude`, `status_pengajuan`, `status_hapus`, `alasan_hapus`, `keterangan`, `created_at`, `updated_at`) VALUES
(1, 2, 'Gereja Katolik Paroki St. Sesilia', 'katolik', 'Jl. Sungai Raya Dalam', '0812345678913', -0.07052077848657445, 109.3597447872162, 'diterima', 'normal', NULL, '', '2026-06-09 10:11:48', '2026-06-09 10:48:18'),
(2, 5, 'GPIB', 'kristen', 'Jl. Kartini, Pontianak', '0812345678913', -0.02508443513561694, 109.33739125728609, 'diterima', 'normal', NULL, '', '2026-06-09 10:54:13', '2026-06-09 10:59:11'),
(4, 8, 'Gereja Elim', 'kristen', 'Jalan Rajawali, Tengah, Pontianak Kota, Pontianak, Kalimantan Barat, Kalimantan, 78112, Indonesia', '0812345678913', -0.02152069970142388, 109.33472514152527, 'ditolak', 'disetujui', 's', 's', '2026-06-09 14:50:50', '2026-06-09 14:51:16'),
(5, 8, 'Gereja Elim', 'kristen', 'Jalan Rajawali, Tengah, Pontianak Kota, Pontianak, Kalimantan Barat, Kalimantan, 78112, Indonesia', '0812345678913', -0.02151988492191236, 109.33474123477937, 'diterima', 'normal', NULL, 'Ditambahkan langsung oleh super admin.', '2026-06-09 14:53:12', '2026-06-09 14:53:12');
-- --------------------------------------------------------
--
-- Table structure for table `users`
--
CREATE TABLE `users` (
`id` int NOT NULL,
`nama` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`username` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`role` enum('super_admin','admin_rumah_ibadah','penerima') COLLATE utf8mb4_unicode_ci NOT NULL,
`status_akun` enum('aktif','nonaktif') COLLATE utf8mb4_unicode_ci DEFAULT 'aktif',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `users`
--
INSERT INTO `users` (`id`, `nama`, `username`, `password`, `role`, `status_akun`, `created_at`, `updated_at`) VALUES
(1, 'Super Admin Dinas Sosial', 'superadmin', '$2y$12$79Vc.m66x5uIIIt4rifL0OIY3qrzzXmgktTW3Y1QEwWB9GMaeLFni', 'super_admin', 'aktif', '2026-06-09 10:04:24', '2026-06-09 14:48:23'),
(2, 'admingerejasesilia', 'admingerejasesilia', '$2y$10$Pt4VTj7yDLt1xabM5WwGJuBo.gat807fkV0Q.KiGKv.V0tRbo1SHW', 'admin_rumah_ibadah', 'aktif', '2026-06-09 10:06:38', '2026-06-10 01:23:46'),
(4, 'penerima01', 'penerima01', '$2y$12$79Vc.m66x5uIIIt4rifL0OIY3qrzzXmgktTW3Y1QEwWB9GMaeLFni', 'penerima', 'nonaktif', '2026-06-09 10:08:28', '2026-06-10 01:18:35'),
(5, 'admingpib', 'admingpib', '$2y$12$79Vc.m66x5uIIIt4rifL0OIY3qrzzXmgktTW3Y1QEwWB9GMaeLFni', 'admin_rumah_ibadah', 'nonaktif', '2026-06-09 10:52:35', '2026-06-09 14:47:58'),
(6, 'penerima02', 'penerima02', '$2y$12$79Vc.m66x5uIIIt4rifL0OIY3qrzzXmgktTW3Y1QEwWB9GMaeLFni', 'penerima', 'aktif', '2026-06-09 10:55:15', '2026-06-09 14:47:48'),
(8, 'Admin Gereja Elim', 'admingerejaelim', '$2y$10$hiPcHEFAb2lPq3WKLra1MuZ8220s/UY.3PG24ZIrmbxrbp2imboGO', 'admin_rumah_ibadah', 'aktif', '2026-06-09 14:49:55', '2026-06-09 14:49:55'),
(9, 'penerima03', 'penerima03', '$2y$10$.50kERk9OSPvJhe.9IFxd.3qi2j5spTxtM36Kx1mI7Q3HkeVlyzvm', 'penerima', 'nonaktif', '2026-06-09 14:51:50', '2026-06-10 01:18:20'),
(10, 'penerima04', 'penerima04', '$2y$10$Q0yfgX7/KnCa4sxnz/f5YupK2aVyKWLDPO8CD/mdQlCSOTRbzLBli', 'penerima', 'aktif', '2026-06-10 01:20:04', '2026-06-10 01:20:04'),
(11, 'penerima05', 'penerima05', '$2y$10$0UJ97IYsDsS/K/qJbHJsiO938TFSU2AorU0fqxAawnbpwhJN/qBqi', 'penerima', 'aktif', '2026-06-10 01:21:01', '2026-06-10 01:21:01');
--
-- Indexes for dumped tables
--
--
-- Indexes for table `penerima_bantuan`
--
ALTER TABLE `penerima_bantuan`
ADD PRIMARY KEY (`id`),
ADD KEY `idx_pb_user` (`user_id`),
ADD KEY `idx_pb_ri` (`rumah_ibadah_id`),
ADD KEY `idx_pb_status` (`status_pengajuan`),
ADD KEY `idx_pb_jenis` (`jenis_bantuan`);
--
-- Indexes for table `penyaluran_bantuan`
--
ALTER TABLE `penyaluran_bantuan`
ADD PRIMARY KEY (`id`),
ADD KEY `idx_pen_program` (`program_id`),
ADD KEY `idx_pen_penerima` (`penerima_id`),
ADD KEY `idx_pen_ri` (`rumah_ibadah_id`),
ADD KEY `idx_pen_admin` (`admin_id`),
ADD KEY `idx_pen_tanggal` (`tanggal_penyaluran`);
--
-- Indexes for table `program_bantuan`
--
ALTER TABLE `program_bantuan`
ADD PRIMARY KEY (`id`),
ADD KEY `idx_prog_ri` (`rumah_ibadah_id`),
ADD KEY `idx_prog_periode` (`bulan`,`tahun`),
ADD KEY `idx_prog_status` (`status_program`);
--
-- Indexes for table `riwayat_status`
--
ALTER TABLE `riwayat_status`
ADD PRIMARY KEY (`id`),
ADD KEY `idx_rw_penerima` (`penerima_id`),
ADD KEY `idx_rw_user` (`user_id`);
--
-- Indexes for table `rumah_ibadah`
--
ALTER TABLE `rumah_ibadah`
ADD PRIMARY KEY (`id`),
ADD KEY `idx_ri_user` (`user_id`),
ADD KEY `idx_ri_status` (`status_pengajuan`),
ADD KEY `idx_ri_agama` (`agama`);
--
-- Indexes for table `users`
--
ALTER TABLE `users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `username` (`username`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `penerima_bantuan`
--
ALTER TABLE `penerima_bantuan`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=10;
--
-- AUTO_INCREMENT for table `penyaluran_bantuan`
--
ALTER TABLE `penyaluran_bantuan`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT for table `program_bantuan`
--
ALTER TABLE `program_bantuan`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
--
-- AUTO_INCREMENT for table `riwayat_status`
--
ALTER TABLE `riwayat_status`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=21;
--
-- AUTO_INCREMENT for table `rumah_ibadah`
--
ALTER TABLE `rumah_ibadah`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
--
-- AUTO_INCREMENT for table `users`
--
ALTER TABLE `users`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=12;
--
-- Constraints for dumped tables
--
--
-- Constraints for table `penerima_bantuan`
--
ALTER TABLE `penerima_bantuan`
ADD CONSTRAINT `fk_pb_ri` FOREIGN KEY (`rumah_ibadah_id`) REFERENCES `rumah_ibadah` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,
ADD CONSTRAINT `fk_pb_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
--
-- Constraints for table `penyaluran_bantuan`
--
ALTER TABLE `penyaluran_bantuan`
ADD CONSTRAINT `fk_pen_admin` FOREIGN KEY (`admin_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `fk_pen_penerima` FOREIGN KEY (`penerima_id`) REFERENCES `penerima_bantuan` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `fk_pen_program` FOREIGN KEY (`program_id`) REFERENCES `program_bantuan` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,
ADD CONSTRAINT `fk_pen_ri` FOREIGN KEY (`rumah_ibadah_id`) REFERENCES `rumah_ibadah` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
--
-- Constraints for table `program_bantuan`
--
ALTER TABLE `program_bantuan`
ADD CONSTRAINT `fk_prog_ri` FOREIGN KEY (`rumah_ibadah_id`) REFERENCES `rumah_ibadah` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
--
-- Constraints for table `riwayat_status`
--
ALTER TABLE `riwayat_status`
ADD CONSTRAINT `fk_rw_penerima` FOREIGN KEY (`penerima_id`) REFERENCES `penerima_bantuan` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `fk_rw_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
--
-- Constraints for table `rumah_ibadah`
--
ALTER TABLE `rumah_ibadah`
ADD CONSTRAINT `fk_ri_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
@@ -0,0 +1,723 @@
# PANDUAN INSTALASI LOCALHOST SAMPAI HOSTING
**Project:** 05 - WebGIS Poverty Mapping
**Teknologi:** PHP Native, MySQL/MariaDB, Leaflet.js, HTML, CSS, JavaScript Vanilla
**Folder lokal:** `webgis-poverty/05/`
**URL lokal:** `http://localhost/webgis-poverty/05/`
---
## 1. Ringkasan Aplikasi
Aplikasi ini adalah WebGIS Poverty Mapping untuk memetakan penerima bantuan dan rumah ibadah sebagai pemberi/penyalur bantuan. Sistem memiliki tiga role utama:
1. **Super Admin**
- Mengelola seluruh data.
- Menyetujui/menolak rumah ibadah.
- Menyetujui/menolak penerima bantuan.
- Mengelola akun admin rumah ibadah dan penerima.
- Mengelola program bantuan dan laporan.
- Menyetujui/menolak pengajuan hapus marker/pin.
2. **Admin Rumah Ibadah**
- Mengelola rumah ibadah miliknya.
- Mengelola akun penerima.
- Mengajukan/mengelola penerima dalam radius rumah ibadahnya.
- Membuat program bantuan.
- Mencatat penyaluran bantuan.
- Melihat laporan dan riwayat sesuai rumah ibadah yang dikelola.
3. **Penerima**
- Registrasi akun.
- Mengajukan titik lokasi rumah melalui peta.
- Melihat status pengajuan.
- Melihat riwayat bantuan.
- Mengajukan hapus pin lokasi miliknya.
Aturan utama sistem:
- Radius tanggung jawab rumah ibadah adalah **300 meter**.
- Penerima otomatis dikaitkan ke **rumah ibadah terdekat** dalam radius 300 meter.
- Satu akun penerima hanya boleh memiliki **satu pin aktif**.
- Admin rumah ibadah dapat mengelola lebih dari satu rumah ibadah.
- Data yang sudah punya relasi tidak langsung dihapus permanen, tetapi dinonaktifkan atau diberi status hapus agar riwayat audit tetap aman.
---
## 2. Struktur File Project
Struktur project terbaru:
```text
05/
├── index.php
├── login.php
├── logout.php
├── register.php
├── dashboard.php
├── koneksi.php
├── auth.php
├── ambil_data_peta.php
├── ambil_laporan_penyaluran.php
├── ambil_program_bantuan.php
├── ambil_riwayat.php
├── ambil_user.php
├── ajukan_rumah_ibadah.php
├── hapus_rumah_ibadah.php
├── update_status_rumah_ibadah.php
├── tambah_penerima.php
├── edit_penerima.php
├── hapus_penerima.php
├── update_status_penerima.php
├── tambah_program_bantuan.php
├── edit_program_bantuan.php
├── hapus_program_bantuan.php
├── tambah_penyaluran.php
├── tambah_user.php
├── edit_user.php
├── hapus_user.php
├── backup_sql/
│ ├── schema.sql
│ ├── migration_fix_multi_ri_pin.sql
│ ├── migration_revisi_persetujuan_hapus.sql
│ └── migration_revisi_sinkronisasi.sql
├── RANGKUMAN_PENTING_PROJECT_05.md
└── PANDUAN_INSTALASI_LOCALHOST_SAMPAI_HOSTING.md
```
---
## 3. Persiapan Localhost dengan Laragon
1. Buka Laragon.
2. Klik **Start All** agar Apache dan MySQL/MariaDB aktif.
3. Buka folder Laragon:
```text
C:\laragon\www\
```
4. Buat folder project:
```text
C:\laragon\www\webgis-poverty\05\
```
5. Ekstrak isi ZIP project ke folder `05`.
6. Pastikan file `index.php`, `login.php`, `dashboard.php`, dan `koneksi.php` berada langsung di dalam folder `05`, bukan di dalam folder tambahan.
Struktur yang benar:
```text
C:\laragon\www\webgis-poverty\05\index.php
C:\laragon\www\webgis-poverty\05\login.php
C:\laragon\www\webgis-poverty\05\dashboard.php
C:\laragon\www\webgis-poverty\05\koneksi.php
```
Struktur yang salah:
```text
C:\laragon\www\webgis-poverty\05\05\index.php
```
Jika terjadi struktur salah, pindahkan semua file dari folder `05\05\` ke folder `05\` utama.
---
## 4. Import Database di Localhost
### 4.1 Buka phpMyAdmin
1. Jalankan Laragon.
2. Klik **Database** atau buka browser:
```text
http://localhost/phpmyadmin
```
3. Login phpMyAdmin Laragon:
```text
Username : root
Password : kosongkan
```
### 4.2 Pilihan File SQL
Ada dua pilihan import database:
#### Opsi A - Database Demo Lengkap
Gunakan file:
```text
05_povertydb.sql
```
Opsi ini cocok jika ingin langsung ada data contoh, akun demo, rumah ibadah, penerima, program, dan laporan.
#### Opsi B - Struktur Kosong dari ZIP
Gunakan file:
```text
backup_sql/schema.sql
```
Opsi ini cocok jika ingin membuat database dari awal. Setelah import, data contoh mungkin belum lengkap.
Jika ada file `05_povertydb.sql`, disarankan memakai file tersebut untuk demo karena akun-akun contoh sudah tersedia.
### 4.3 Cara Import
1. Buka phpMyAdmin.
2. Klik tab **Import**.
3. Pilih file SQL.
4. Klik **Go**.
5. Pastikan database `05_povertydb` berhasil dibuat.
6. Pastikan tabel berikut muncul:
```text
users
rumah_ibadah
penerima_bantuan
program_bantuan
penyaluran_bantuan
riwayat_status
```
---
## 5. Konfigurasi Database Localhost
Buka file:
```text
koneksi.php
```
Untuk Laragon, gunakan konfigurasi berikut:
```php
$host = "localhost";
$user = "root";
$pass = "";
$db = "05_povertydb";
```
Simpan file tersebut.
---
## 6. Akun Login Default
Semua password akun demo adalah:
```text
password
```
Akun utama:
```text
Role : Super Admin
Username : superadmin
Password : password
```
Contoh akun admin rumah ibadah jika memakai database demo:
```text
Username : admingerejasesilia
Password : password
Role : admin_rumah_ibadah
```
```text
Username : admingerejaelim
Password : password
Role : admin_rumah_ibadah
```
Contoh akun penerima jika memakai database demo:
```text
Username : penerima02
Password : password
Role : penerima
```
```text
Username : penerima04
Password : password
Role : penerima
```
```text
Username : penerima05
Password : password
Role : penerima
```
Catatan:
- Akun dengan status `nonaktif` tidak bisa login.
- Password disimpan menggunakan `password_hash()` dan diverifikasi menggunakan `password_verify()`.
- Jangan menulis password plain text langsung di database.
---
## 7. Menjalankan Aplikasi di Localhost
1. Pastikan Laragon sudah **Start All**.
2. Buka browser.
3. Akses:
```text
http://localhost/webgis-poverty/05/
```
4. Jika diarahkan ke login, masuk menggunakan:
```text
Username : superadmin
Password : password
```
5. Setelah login, menu utama:
```text
index.php = halaman peta WebGIS
dashboard.php = dashboard, akun, program, penyaluran, laporan, dan riwayat
logout.php = keluar dari sistem
```
---
## 8. Pengujian Fitur Localhost
### 8.1 Login Super Admin
1. Buka halaman login.
2. Masukkan username `superadmin`.
3. Masukkan password `password`.
4. Pastikan masuk ke dashboard.
### 8.2 Kelola Akun
1. Login sebagai super admin.
2. Buka **Dashboard**.
3. Cari bagian **Kelola Akun Pengguna**.
4. Tambah akun admin rumah ibadah atau penerima.
5. Gunakan password minimal 6 karakter. Untuk demo, isi `password`.
6. Simpan.
7. Coba login menggunakan akun baru.
### 8.3 Tambah Rumah Ibadah
1. Login sebagai super admin.
2. Buka **Peta**.
3. Klik lokasi pada peta.
4. Pilih **Tambah Rumah Ibadah**.
5. Isi data rumah ibadah.
6. Masukkan username admin rumah ibadah.
7. Simpan.
8. Jika super admin yang menambah, status rumah ibadah langsung dapat diterima.
### 8.4 Pengajuan Rumah Ibadah oleh Admin
1. Login sebagai admin rumah ibadah.
2. Buka peta.
3. Klik lokasi rumah ibadah.
4. Pilih **Ajukan Rumah Ibadah**.
5. Isi data.
6. Pengajuan akan berstatus `menunggu`.
7. Super admin harus menyetujui agar rumah ibadah aktif.
### 8.5 Pengajuan Penerima
1. Login sebagai penerima.
2. Buka peta.
3. Klik lokasi rumah.
4. Pilih **Simpan Pengajuan**.
5. Isi data penerima.
6. Sistem mencari rumah ibadah terdekat dalam radius 300 meter.
7. Status awal penerima adalah `menunggu`.
8. Super admin atau admin rumah ibadah terkait dapat menyetujui/menolak.
### 8.6 Program Bantuan
1. Login sebagai super admin atau admin rumah ibadah.
2. Buka dashboard.
3. Isi form **Buat Program Bantuan**.
4. Pilih rumah ibadah, jenis bantuan, bulan, tahun, target frekuensi, nominal/item, dan deskripsi.
5. Simpan.
### 8.7 Penyaluran Bantuan
1. Pastikan penerima sudah berstatus `diterima`.
2. Pastikan penerima berada dalam rumah ibadah yang sama dengan program.
3. Buka form **Catat Penyaluran Bantuan**.
4. Pilih program.
5. Pilih penerima.
6. Isi tanggal dan keterangan.
7. Simpan.
8. Sistem otomatis menghitung bantuan ke-berapa pada program tersebut.
### 8.8 Laporan Bulanan
1. Buka dashboard.
2. Cari bagian **Monitoring Penyaluran Per Periode**.
3. Pilih bulan dan tahun.
4. Gunakan filter rumah ibadah, agama, program, jenis bantuan, dan status penyaluran.
5. Klik tampilkan/muat laporan.
6. Laporan menampilkan penerima yang belum diberi, sudah diberi, belum memenuhi target, sesuai target, atau melebihi target.
### 8.9 Pengajuan Hapus Marker/Pin
1. Super admin dapat menghapus marker langsung.
2. Admin rumah ibadah hanya dapat mengajukan hapus rumah ibadah miliknya atau pin penerima yang dikelola.
3. Penerima dapat mengajukan hapus pin miliknya sendiri.
4. Pengajuan hapus akan masuk ke panel persetujuan super admin.
5. Jika disetujui, marker/pin hilang dari peta aktif.
---
## 9. Persiapan Sebelum Hosting
Sebelum upload ke hosting, pastikan:
```text
[ ] Aplikasi berjalan di localhost.
[ ] Database berhasil diimport.
[ ] Login superadmin berhasil.
[ ] Peta muncul.
[ ] Dashboard muncul.
[ ] Tambah akun berhasil.
[ ] Tambah rumah ibadah berhasil.
[ ] Tambah penerima berhasil.
[ ] Program bantuan berhasil dibuat.
[ ] Penyaluran bantuan berhasil dicatat.
[ ] Laporan berhasil tampil.
```
File yang wajib diupload ke hosting:
```text
index.php
login.php
logout.php
register.php
dashboard.php
koneksi.php
auth.php
ambil_data_peta.php
ambil_laporan_penyaluran.php
ambil_program_bantuan.php
ambil_riwayat.php
ambil_user.php
ajukan_rumah_ibadah.php
hapus_rumah_ibadah.php
update_status_rumah_ibadah.php
tambah_penerima.php
edit_penerima.php
hapus_penerima.php
update_status_penerima.php
tambah_program_bantuan.php
edit_program_bantuan.php
hapus_program_bantuan.php
tambah_penyaluran.php
tambah_user.php
edit_user.php
hapus_user.php
```
File yang tidak wajib diupload ke hosting setelah database berhasil diimport:
```text
backup_sql/
05_povertydb.sql
RANGKUMAN_PENTING_PROJECT_05.md
PANDUAN_INSTALASI_LOCALHOST_SAMPAI_HOSTING.md
file ZIP backup
```
Untuk keamanan, file SQL dan ZIP backup sebaiknya tidak dibiarkan terbuka di `public_html`.
---
## 10. Upload ke Hosting cPanel
1. Login ke cPanel hosting.
2. Buka **File Manager**.
3. Masuk ke folder:
```text
public_html
```
4. Buat folder:
```text
webgis-poverty
```
5. Di dalamnya buat folder:
```text
05
```
6. Upload file aplikasi ke:
```text
public_html/webgis-poverty/05/
```
7. Jika upload berupa ZIP, klik kanan ZIP lalu pilih **Extract**.
8. Pastikan `index.php` berada langsung di folder `05`.
URL setelah upload:
```text
https://namadomain.com/webgis-poverty/05/
```
---
## 11. Membuat Database Hosting
1. Buka menu **MySQL Databases** di cPanel.
2. Buat database baru, contoh:
```text
cpaneluser_05_povertydb
```
3. Buat user database baru, contoh:
```text
cpaneluser_userdb
```
4. Buat password database.
5. Tambahkan user ke database.
6. Berikan akses **ALL PRIVILEGES**.
7. Catat data berikut:
```text
Host database : localhost
Nama database : cpaneluser_05_povertydb
Username database : cpaneluser_userdb
Password database : password database hosting
```
---
## 12. Import Database di Hosting
1. Buka **phpMyAdmin** dari cPanel.
2. Pilih database hosting yang sudah dibuat.
3. Klik tab **Import**.
4. Upload file SQL.
5. Klik **Go**.
6. Pastikan semua tabel muncul.
Jika hosting menolak perintah:
```sql
CREATE DATABASE IF NOT EXISTS `05_povertydb`;
USE `05_povertydb`;
```
maka hapus atau komentari dua bagian tersebut dari file SQL, lalu import ulang pada database yang sudah dipilih di phpMyAdmin.
---
## 13. Mengubah koneksi.php untuk Hosting
Buka `koneksi.php`, lalu ubah konfigurasi menjadi data hosting.
Contoh:
```php
$host = "localhost";
$user = "cpaneluser_userdb";
$pass = "PasswordDatabaseHosting";
$db = "cpaneluser_05_povertydb";
```
Simpan file.
---
## 14. Akses Aplikasi Online
Jika foldernya:
```text
public_html/webgis-poverty/05/
```
maka URL biasanya:
```text
https://namadomain.com/webgis-poverty/05/
```
Jika langsung diletakkan di `public_html`, URL menjadi:
```text
https://namadomain.com/
```
Jika memakai subdomain, misalnya `gis.namadomain.com`, arahkan document root subdomain ke folder aplikasi.
---
## 15. Checklist Setelah Hosting
```text
[ ] Halaman login terbuka.
[ ] Login superadmin berhasil.
[ ] Dashboard terbuka.
[ ] Peta Leaflet muncul.
[ ] Marker rumah ibadah muncul.
[ ] Marker penerima muncul.
[ ] Tambah akun berhasil.
[ ] Tambah/ajukan rumah ibadah berhasil.
[ ] Setujui/tolak rumah ibadah berhasil.
[ ] Tambah/ajukan penerima berhasil.
[ ] Setujui/tolak penerima berhasil.
[ ] Buat program bantuan berhasil.
[ ] Catat penyaluran bantuan berhasil.
[ ] Laporan bulanan tampil.
[ ] Riwayat tampil.
[ ] Logout berhasil.
[ ] Aplikasi bisa dibuka dari HP.
```
---
## 16. Troubleshooting
### 16.1 Halaman Blank Putih
Penyebab umum:
- Error PHP.
- Koneksi database gagal.
- File belum lengkap.
- Ada output sebelum `session_start()`.
Solusi:
- Cek file `error_log` di hosting.
- Pastikan `koneksi.php` benar.
- Pastikan semua file PHP terupload.
### 16.2 Database Connection Failed
Solusi:
- Cek nama database.
- Cek username database.
- Cek password database.
- Cek host database, biasanya `localhost`.
- Pastikan user database sudah diberi **ALL PRIVILEGES**.
### 16.3 Login Gagal
Solusi:
- Pastikan username benar.
- Pastikan akun berstatus `aktif`.
- Pastikan password adalah `password` untuk akun demo.
- Pastikan password di database dibuat memakai `password_hash()`.
### 16.4 Peta Tidak Muncul
Solusi:
- Pastikan internet aktif karena Leaflet memakai CDN.
- Pastikan elemen peta memiliki tinggi.
- Buka Console browser untuk melihat error JavaScript.
### 16.5 Marker Tidak Muncul
Solusi:
- Cek latitude dan longitude.
- Cek status pengajuan.
- Data `ditolak` atau `status_hapus='disetujui'` memang tidak tampil sebagai marker aktif.
- Cek response `ambil_data_peta.php` melalui tab Network browser.
### 16.6 Import SQL Gagal
Solusi:
- Pastikan database sudah dipilih sebelum import.
- Jika error `CREATE DATABASE`, hapus baris `CREATE DATABASE` dan `USE`.
- Jika error `IF NOT EXISTS` pada `ALTER TABLE`, gunakan file migration versi kompatibel.
- Jika ada pin aktif ganda, rapikan data penerima terlebih dahulu sebelum membuat unique key satu pin aktif.
### 16.7 Fetch Error
Solusi:
- Pastikan file PHP tujuan ada.
- Pastikan path fetch memakai path relatif, misalnya:
```javascript
fetch('ambil_data_peta.php')
```
Jangan memakai path localhost saat sudah hosting:
```javascript
fetch('http://localhost/webgis-poverty/05/ambil_data_peta.php')
```
---
## 17. Keamanan Dasar Setelah Online
Setelah aplikasi online:
1. Ganti password akun penting jika sudah masuk tahap produksi.
2. Jangan biarkan file `.sql` dan `.zip` di folder publik.
3. Jangan tampilkan detail error kepada pengguna umum.
4. Gunakan HTTPS.
5. Pastikan semua akun admin menggunakan password yang kuat.
6. Nonaktifkan akun yang tidak digunakan.
7. Backup database secara berkala.
---
## 18. Ringkasan Akhir
Urutan instalasi yang benar:
```text
1. Ekstrak project ke C:\laragon\www\webgis-poverty\05\
2. Start All di Laragon.
3. Import database melalui phpMyAdmin.
4. Cek koneksi.php.
5. Buka http://localhost/webgis-poverty/05/
6. Login superadmin / password.
7. Uji peta, dashboard, akun, rumah ibadah, penerima, program, penyaluran, dan laporan.
8. Upload file PHP ke hosting.
9. Buat database hosting.
10. Import SQL di hosting.
11. Ubah koneksi.php sesuai hosting.
12. Hapus file SQL/ZIP dari public_html setelah selesai.
```
+230
View File
@@ -0,0 +1,230 @@
# RANGKUMAN PENTING PROJECT 05 - WebGIS Poverty Mapping
File ini merangkum catatan penting saja dari beberapa file catatan revisi/migrasi.
---
## 1. File SQL yang Penting Disimpan
Simpan file SQL berikut sebagai backup database dan migrasi:
1. `schema.sql`
- Struktur database dasar.
- Dipakai jika ingin instal ulang database dari awal.
2. `migration_fix_multi_ri_pin.sql`
- Memperbaiki masalah admin rumah ibadah yang punya lebih dari satu rumah ibadah.
- Menambahkan aturan satu akun penerima hanya boleh punya satu pin aktif.
3. `migration_revisi_persetujuan_hapus.sql`
- Menambahkan fitur pengajuan/persetujuan hapus marker rumah ibadah dan pin penerima.
4. `migration_revisi_sinkronisasi.sql`
- Memperbaiki sinkronisasi penerima dengan rumah ibadah terdekat.
Rekomendasi:
- Jangan letakkan file SQL di folder publik saat hosting.
- Simpan di folder backup lokal, misalnya `backup_sql/`.
---
## 2. Perbaikan Paling Penting pada Sistem
### A. Admin Rumah Ibadah Multi-RI
Masalah lama:
- Admin rumah ibadah hanya terbaca punya 1 rumah ibadah karena query memakai `LIMIT 1`.
- Akibatnya, pin/marker dari rumah ibadah lain tidak muncul.
Perbaikan:
- Sistem sekarang mendukung admin yang mengelola lebih dari satu rumah ibadah.
- Query memakai daftar ID rumah ibadah, bukan hanya 1 ID.
Dampak:
- Pin penerima dan rumah ibadah milik admin tampil lebih lengkap.
- Laporan, riwayat, program bantuan, dan penyaluran mengikuti semua rumah ibadah milik admin.
---
### B. Satu Akun Penerima Hanya Boleh Satu Pin Aktif
Masalah lama:
- Satu akun penerima bisa punya lebih dari satu pin aktif.
- Ini menyebabkan data tidak sinkron dan penerima bisa muncul di beberapa rumah ibadah.
Perbaikan:
- Sebelum menambah pin, sistem mengecek apakah user penerima sudah punya pin aktif.
- Pin aktif yang dimaksud adalah status `menunggu` atau `diterima`, dan belum disetujui hapus.
- Database juga ditambah unique key agar satu user tidak bisa punya lebih dari satu pin aktif.
---
### C. Sinkronisasi Radius 300 Meter
Masalah lama:
- Penerima bisa terhubung ke rumah ibadah yang salah.
- Jika radius beberapa rumah ibadah saling beririsan, sistem belum selalu memilih yang paling dekat.
Perbaikan:
- Penerima dihitung ulang berdasarkan rumah ibadah aktif terdekat dalam radius 300 meter.
- Jika rumah ibadah dihapus, penerima dilepas lalu dicari ulang ke rumah ibadah aktif terdekat lain.
---
### D. Persetujuan Hapus Marker/Pin
Alur baru:
- Super admin bisa menghapus marker rumah ibadah dan pin penerima secara langsung.
- Admin rumah ibadah hanya bisa mengajukan hapus rumah ibadah/penerima.
- Penerima bisa mengajukan hapus pin miliknya sendiri.
- Pengajuan hapus harus disetujui super admin.
Dampak:
- Data tidak langsung hilang sembarangan.
- Ada kontrol dari super admin.
- Marker yang sudah disetujui hapus tidak tampil lagi di peta dan laporan aktif.
---
### E. CRUD Akun
Super admin dapat mengelola:
- Akun admin rumah ibadah.
- Akun penerima.
Admin rumah ibadah dapat mengelola:
- Akun penerima saja.
Fitur:
- Tambah akun.
- Lihat daftar akun.
- Filter role/status.
- Cari nama/username.
- Edit akun.
- Hapus akun jika belum punya relasi data.
- Nonaktifkan akun jika sudah punya relasi agar audit data tidak rusak.
---
### F. Validasi Program dan Penyaluran Bantuan
Perbaikan penting:
- Program bantuan tidak otomatis mengambil rumah ibadah pertama.
- Admin yang punya banyak rumah ibadah harus memilih rumah ibadah.
- Penyaluran hanya bisa dicatat jika program dan penerima berada di rumah ibadah yang sama.
- Tombol "Catat" tidak muncul lagi jika program sudah selesai.
- Dropdown penerima difilter sesuai rumah ibadah dari program yang dipilih.
---
### G. Perbaikan Logika NULL
Masalah lama:
- Nilai kosong seperti nominal, jarak meter, atau rumah_ibadah_id bisa tersimpan sebagai `0`, bukan `NULL`.
Perbaikan:
- Query INSERT/UPDATE dipisah agar nilai kosong benar-benar masuk sebagai `NULL`.
Dampak:
- Data lebih bersih.
- Tidak muncul rumah_ibadah_id = 0 atau jarak_meter = 0 yang menyesatkan.
---
### H. Validasi Client-side
Ditambahkan validasi sebelum data dikirim:
- Program wajib dipilih.
- Penerima wajib dipilih.
- Nama program wajib diisi.
- Bulan harus 1-12.
- Tahun minimal 2000.
- Nama, username, dan password wajib saat membuat akun baru.
- Password minimal 6 karakter.
---
## 3. Catatan Error SQL
### Error #1044 information_schema
Penyebab:
- Akun MySQL tidak punya akses membaca `INFORMATION_SCHEMA`.
Solusi:
- Gunakan file migrasi versi manual.
- Atau jalankan `jalankan_migrasi_fix.php` jika file itu tersedia.
- Setelah selesai, hapus `jalankan_migrasi_fix.php`.
---
### Error #1064 IF NOT EXISTS
Penyebab:
- Versi MySQL/MariaDB tidak mendukung syntax:
`ALTER TABLE ... ADD COLUMN IF NOT EXISTS`
Solusi:
- Gunakan file `migration_fix_multi_ri_pin.sql` versi kompatibel.
- Jika ada penerima dengan pin aktif ganda, rapikan dulu lewat dashboard super admin.
- Setelah itu jalankan ulang migration.
---
## 4. File Catatan yang Bisa Digabung/Hapus
Setelah rangkuman ini dibuat, file catatan berikut boleh dipindahkan ke backup:
- `CATATAN_FIXED_MULTI_RI.md`
- `CATATAN_PERBAIKAN_LOGIKA.md`
- `CATATAN_REVISI_CRUD_AKUN.md`
- `CATATAN_REVISI_PERSETUJUAN_HAPUS.md`
- `CATATAN_SQL_ERROR_1044.md`
- `CATATAN_SQL_ERROR_1064.md`
Jangan langsung buang permanen jika proyek masih diperiksa dosen/penguji.
---
## 5. Rekomendasi Struktur Folder
Gunakan struktur seperti ini:
```text
05/
├── index.php
├── login.php
├── register.php
├── koneksi.php
├── auth.php
├── tambah_penerima.php
├── tambah_penyaluran.php
├── tambah_program_bantuan.php
├── edit_user.php
├── hapus_user.php
├── ...
└── backup_dokumentasi/
├── RANGKUMAN_PENTING_PROJECT_05.md
└── backup_sql/
├── schema.sql
├── migration_fix_multi_ri_pin.sql
├── migration_revisi_persetujuan_hapus.sql
└── migration_revisi_sinkronisasi.sql
```
---
## 6. Kesimpulan
Yang paling penting disimpan:
1. `schema.sql`
2. Semua file `migration_*.sql`
3. Satu file rangkuman ini
4. File PHP aplikasi utama
Yang tidak wajib ada di folder utama aplikasi:
- File catatan `.md`
- File SQL backup
- File migrasi yang sudah selesai dijalankan
Untuk hosting, file SQL dan catatan revisi sebaiknya tidak ikut berada di folder publik.
@@ -0,0 +1,632 @@
# SKPL / SRS WEBGIS POVERTY MAPPING PROJECT 05
**Nama Dokumen:** Spesifikasi Kebutuhan Perangkat Lunak / Software Requirements Specification
**Nama Sistem:** WebGIS Poverty Mapping
**Versi:** 1.0
**Teknologi:** PHP Native, MySQL/MariaDB, Leaflet.js, HTML, CSS, JavaScript Vanilla
**Lingkungan Pengembangan:** Laragon Localhost dan Hosting cPanel
---
## 1. Pendahuluan
### 1.1 Tujuan Dokumen
Dokumen ini menjelaskan kebutuhan perangkat lunak untuk aplikasi **WebGIS Poverty Mapping Project 05**. Dokumen ini dapat digunakan sebagai acuan pengembangan, pengujian, instalasi, dan evaluasi aplikasi.
### 1.2 Ruang Lingkup Sistem
WebGIS Poverty Mapping adalah aplikasi berbasis web untuk memetakan rumah ibadah dan penerima bantuan. Sistem digunakan untuk membantu proses pendataan, validasi, pengelolaan program bantuan, penyaluran bantuan, dan pelaporan penerima yang sudah atau belum memperoleh bantuan.
Sistem memiliki fitur utama:
1. Login dan manajemen role pengguna.
2. Registrasi akun penerima.
3. Pengelolaan akun pengguna.
4. Pemetaan rumah ibadah dan penerima bantuan menggunakan Leaflet.
5. Pengajuan dan persetujuan rumah ibadah.
6. Pengajuan dan persetujuan penerima bantuan.
7. Sinkronisasi penerima ke rumah ibadah terdekat dalam radius 300 meter.
8. Pengelolaan program bantuan.
9. Pencatatan penyaluran bantuan.
10. Laporan penyaluran bantuan per periode.
11. Riwayat status dan penyaluran.
12. Pengajuan dan persetujuan hapus marker/pin.
### 1.3 Definisi Istilah
| Istilah | Definisi |
|---|---|
| WebGIS | Sistem informasi geografis berbasis web. |
| Rumah Ibadah | Entitas pemberi/penyalur bantuan yang memiliki lokasi pada peta. |
| Penerima | Pengguna atau warga yang mengajukan bantuan dan memiliki titik lokasi. |
| Super Admin | Pengguna dengan akses tertinggi. |
| Admin Rumah Ibadah | Pengguna yang mengelola satu atau lebih rumah ibadah. |
| Radius 300 Meter | Jarak maksimal penerima agar dapat dikaitkan dengan rumah ibadah. |
| Marker | Titik rumah ibadah pada peta. |
| Pin | Titik penerima pada peta. |
| Program Bantuan | Program penyaluran bantuan pada periode tertentu. |
| Penyaluran Bantuan | Catatan pemberian bantuan kepada penerima. |
| SKPL | Spesifikasi Kebutuhan Perangkat Lunak. |
| SRS | Software Requirements Specification. |
### 1.4 Referensi Teknologi
Sistem dibangun menggunakan:
1. PHP Native tanpa framework.
2. MySQL/MariaDB sebagai basis data.
3. Leaflet.js sebagai library peta.
4. HTML, CSS, dan JavaScript Vanilla.
5. AJAX/fetch API untuk pertukaran data dengan file PHP backend.
6. Laragon sebagai lingkungan localhost.
7. cPanel sebagai contoh lingkungan hosting.
---
## 2. Deskripsi Umum Sistem
### 2.1 Perspektif Produk
Aplikasi ini merupakan sistem mandiri berbasis web yang berjalan pada server PHP dan database MySQL/MariaDB. Pengguna mengakses aplikasi melalui browser. Data spasial berupa latitude dan longitude ditampilkan menggunakan Leaflet.js.
### 2.2 Fungsi Umum Produk
Sistem menyediakan fungsi:
1. Autentikasi pengguna.
2. Otorisasi berdasarkan role.
3. Pengelolaan data pengguna.
4. Pengelolaan titik rumah ibadah.
5. Pengelolaan titik penerima bantuan.
6. Sinkronisasi area penerima dengan rumah ibadah.
7. Pengelolaan program bantuan.
8. Pencatatan penyaluran bantuan.
9. Pelaporan penerima sudah/belum diberi bantuan.
10. Riwayat perubahan status.
### 2.3 Kelas Pengguna
#### 2.3.1 Super Admin
Super Admin adalah pengguna dengan hak akses penuh. Super Admin dapat mengelola semua akun, rumah ibadah, penerima, program bantuan, penyaluran bantuan, laporan, dan persetujuan hapus marker/pin.
#### 2.3.2 Admin Rumah Ibadah
Admin Rumah Ibadah adalah pengguna yang mengelola satu atau lebih rumah ibadah. Admin dapat mengelola penerima yang masuk dalam wilayah rumah ibadahnya, membuat program bantuan, mencatat penyaluran, dan melihat laporan sesuai data miliknya.
#### 2.3.3 Penerima
Penerima adalah pengguna yang dapat registrasi, login, mengajukan pin lokasi rumah, melihat status pengajuan, melihat riwayat bantuan, dan mengajukan hapus pin miliknya.
### 2.4 Batasan Sistem
1. Sistem menggunakan PHP Native tanpa framework.
2. Sistem menggunakan MySQL/MariaDB.
3. Sistem menggunakan Leaflet CDN sehingga peta membutuhkan koneksi internet.
4. Radius tanggung jawab rumah ibadah ditetapkan 300 meter.
5. Sistem tidak memakai API geocoding berbayar.
6. Sistem tidak menyediakan upload dokumen identitas.
7. Sistem tidak menyediakan notifikasi email/WhatsApp otomatis.
8. Sistem tidak menyediakan pencetakan laporan PDF otomatis.
### 2.5 Asumsi dan Ketergantungan
1. Browser pengguna mendukung JavaScript.
2. Server mendukung PHP dan MySQL/MariaDB.
3. Database sudah diimport sebelum aplikasi dijalankan.
4. File `koneksi.php` sudah disesuaikan dengan konfigurasi database.
5. Leaflet CDN dapat diakses dari jaringan pengguna.
---
## 3. Aktor Sistem
| Kode | Aktor | Deskripsi |
|---|---|---|
| A-01 | Super Admin | Pengelola utama sistem. |
| A-02 | Admin Rumah Ibadah | Pengelola rumah ibadah dan penyaluran bantuan. |
| A-03 | Penerima | Pengguna yang mengajukan dan menerima bantuan. |
---
## 4. Kebutuhan Fungsional
### 4.1 Autentikasi dan Akun
| Kode | Kebutuhan | Aktor | Prioritas |
|---|---|---|---|
| FR-01 | Sistem harus menyediakan login menggunakan username dan password. | Semua aktor | Tinggi |
| FR-02 | Sistem harus menolak login akun yang tidak aktif. | Semua aktor | Tinggi |
| FR-03 | Sistem harus menyediakan logout. | Semua aktor | Tinggi |
| FR-04 | Sistem harus menyimpan password menggunakan hash. | Sistem | Tinggi |
| FR-05 | Sistem harus menyediakan registrasi akun penerima. | Penerima | Tinggi |
| FR-06 | Sistem harus mengizinkan super admin membuat akun admin rumah ibadah dan penerima. | Super Admin | Tinggi |
| FR-07 | Sistem harus mengizinkan admin rumah ibadah membuat akun penerima. | Admin Rumah Ibadah | Sedang |
| FR-08 | Sistem harus menyediakan fitur edit akun. | Super Admin, Admin Rumah Ibadah | Sedang |
| FR-09 | Sistem harus menonaktifkan akun yang sudah punya relasi data jika tidak dapat dihapus permanen. | Super Admin, Admin Rumah Ibadah | Tinggi |
### 4.2 Peta WebGIS
| Kode | Kebutuhan | Aktor | Prioritas |
|---|---|---|---|
| FR-10 | Sistem harus menampilkan peta menggunakan Leaflet. | Semua aktor | Tinggi |
| FR-11 | Sistem harus menampilkan marker rumah ibadah sesuai hak akses. | Semua aktor | Tinggi |
| FR-12 | Sistem harus menampilkan pin penerima sesuai hak akses. | Semua aktor | Tinggi |
| FR-13 | Sistem harus menampilkan lingkaran radius 300 meter untuk rumah ibadah aktif. | Semua aktor | Tinggi |
| FR-14 | Sistem harus menyediakan popup detail pada marker/pin. | Semua aktor | Sedang |
| FR-15 | Sistem harus memungkinkan input lokasi berdasarkan klik pada peta. | Semua aktor sesuai role | Tinggi |
### 4.3 Pengelolaan Rumah Ibadah
| Kode | Kebutuhan | Aktor | Prioritas |
|---|---|---|---|
| FR-16 | Sistem harus mengizinkan super admin menambah rumah ibadah. | Super Admin | Tinggi |
| FR-17 | Sistem harus mengizinkan admin rumah ibadah mengajukan rumah ibadah. | Admin Rumah Ibadah | Tinggi |
| FR-18 | Sistem harus menyediakan status rumah ibadah: menunggu, diterima, ditolak. | Sistem | Tinggi |
| FR-19 | Sistem harus mengizinkan super admin menyetujui/menolak rumah ibadah. | Super Admin | Tinggi |
| FR-20 | Sistem harus menghubungkan rumah ibadah dengan akun admin rumah ibadah. | Super Admin | Tinggi |
| FR-21 | Sistem harus mendukung satu admin mengelola lebih dari satu rumah ibadah. | Admin Rumah Ibadah | Tinggi |
| FR-22 | Sistem harus melepas atau menyinkronkan ulang penerima jika rumah ibadah ditolak atau dihapus. | Sistem | Tinggi |
### 4.4 Pengelolaan Penerima Bantuan
| Kode | Kebutuhan | Aktor | Prioritas |
|---|---|---|---|
| FR-23 | Sistem harus mengizinkan penerima mengajukan lokasi dirinya sendiri. | Penerima | Tinggi |
| FR-24 | Sistem harus mengizinkan super admin/admin rumah ibadah menambahkan penerima berdasarkan username penerima. | Super Admin, Admin Rumah Ibadah | Tinggi |
| FR-25 | Sistem harus menyimpan data nama, NIK, nomor HP, alamat, pekerjaan, penghasilan, tanggungan, kondisi rumah, jenis bantuan, latitude, dan longitude. | Sistem | Tinggi |
| FR-26 | Sistem harus menyediakan status penerima: menunggu, diterima, ditolak, dibatalkan. | Sistem | Tinggi |
| FR-27 | Sistem harus mengizinkan super admin menyetujui, menolak, mereset, atau membatalkan penerima. | Super Admin | Tinggi |
| FR-28 | Sistem harus mengizinkan admin rumah ibadah menyetujui/menolak penerima dalam wilayahnya. | Admin Rumah Ibadah | Tinggi |
| FR-29 | Sistem harus mencegah admin rumah ibadah mengembalikan status ke menunggu. | Admin Rumah Ibadah | Tinggi |
| FR-30 | Sistem harus mencegah admin rumah ibadah membatalkan penerima yang sudah diterima. | Admin Rumah Ibadah | Tinggi |
| FR-31 | Sistem harus membatasi satu akun penerima hanya memiliki satu pin aktif. | Sistem | Tinggi |
### 4.5 Sinkronisasi Radius
| Kode | Kebutuhan | Aktor | Prioritas |
|---|---|---|---|
| FR-32 | Sistem harus menghitung jarak penerima ke rumah ibadah menggunakan koordinat. | Sistem | Tinggi |
| FR-33 | Sistem harus mengaitkan penerima ke rumah ibadah terdekat dalam radius 300 meter. | Sistem | Tinggi |
| FR-34 | Sistem harus membiarkan penerima belum terhubung jika tidak ada rumah ibadah dalam radius 300 meter. | Sistem | Sedang |
| FR-35 | Sistem harus menyinkronkan ulang penerima saat rumah ibadah disetujui. | Sistem | Tinggi |
| FR-36 | Sistem harus menyinkronkan ulang penerima saat lokasi penerima berubah. | Sistem | Tinggi |
| FR-37 | Sistem harus memilih rumah ibadah paling dekat jika radius beberapa rumah ibadah saling beririsan. | Sistem | Tinggi |
### 4.6 Pengajuan dan Persetujuan Hapus Marker/Pin
| Kode | Kebutuhan | Aktor | Prioritas |
|---|---|---|---|
| FR-38 | Sistem harus mengizinkan super admin menghapus marker rumah ibadah secara langsung dari peta aktif. | Super Admin | Tinggi |
| FR-39 | Sistem harus mengizinkan super admin menghapus pin penerima secara langsung dari peta aktif. | Super Admin | Tinggi |
| FR-40 | Sistem harus mengizinkan admin rumah ibadah mengajukan hapus rumah ibadah miliknya. | Admin Rumah Ibadah | Sedang |
| FR-41 | Sistem harus mengizinkan admin rumah ibadah mengajukan hapus pin penerima yang dikelola. | Admin Rumah Ibadah | Sedang |
| FR-42 | Sistem harus mengizinkan penerima mengajukan hapus pin miliknya sendiri. | Penerima | Sedang |
| FR-43 | Sistem harus menyediakan panel persetujuan hapus marker/pin untuk super admin. | Super Admin | Tinggi |
| FR-44 | Sistem harus menyembunyikan marker/pin yang sudah disetujui penghapusannya. | Sistem | Tinggi |
### 4.7 Program Bantuan
| Kode | Kebutuhan | Aktor | Prioritas |
|---|---|---|---|
| FR-45 | Sistem harus mengizinkan super admin/admin rumah ibadah membuat program bantuan. | Super Admin, Admin Rumah Ibadah | Tinggi |
| FR-46 | Program bantuan harus memiliki rumah ibadah, nama program, jenis bantuan, bulan, tahun, target frekuensi, nominal/item, deskripsi, dan status. | Sistem | Tinggi |
| FR-47 | Sistem harus mengizinkan penghapusan/pembatalan program bantuan. | Super Admin, Admin Rumah Ibadah | Sedang |
| FR-48 | Sistem harus mencegah penyaluran baru pada program berstatus selesai atau dibatalkan. | Sistem | Tinggi |
| FR-49 | Sistem harus menampilkan daftar program bantuan sesuai hak akses. | Super Admin, Admin Rumah Ibadah, Penerima | Sedang |
### 4.8 Penyaluran Bantuan
| Kode | Kebutuhan | Aktor | Prioritas |
|---|---|---|---|
| FR-50 | Sistem harus mengizinkan super admin/admin rumah ibadah mencatat penyaluran bantuan. | Super Admin, Admin Rumah Ibadah | Tinggi |
| FR-51 | Sistem harus memastikan program dan penerima berada pada rumah ibadah yang sama. | Sistem | Tinggi |
| FR-52 | Sistem harus memastikan penerima berstatus diterima sebelum bantuan disalurkan. | Sistem | Tinggi |
| FR-53 | Sistem harus memastikan penerima masih berada dalam radius 300 meter rumah ibadah program. | Sistem | Tinggi |
| FR-54 | Sistem harus mengambil jenis bantuan, nominal, dan jumlah item dari program. | Sistem | Sedang |
| FR-55 | Sistem harus menghitung bantuan_ke secara otomatis. | Sistem | Tinggi |
| FR-56 | Sistem harus memberi peringatan jika penyaluran melebihi target frekuensi program. | Sistem | Sedang |
### 4.9 Laporan dan Riwayat
| Kode | Kebutuhan | Aktor | Prioritas |
|---|---|---|---|
| FR-57 | Sistem harus menyediakan laporan penyaluran berdasarkan bulan dan tahun. | Semua aktor sesuai hak akses | Tinggi |
| FR-58 | Sistem harus menyediakan filter rumah ibadah, agama, program, jenis bantuan, dan status penyaluran. | Semua aktor sesuai hak akses | Sedang |
| FR-59 | Sistem harus menampilkan status belum_diberi, sudah_diberi, belum_memenuhi_target, sesuai_target, dan melebihi_target. | Sistem | Tinggi |
| FR-60 | Sistem harus menyediakan rekap laporan. | Semua aktor sesuai hak akses | Sedang |
| FR-61 | Sistem harus menyimpan riwayat perubahan status penerima. | Sistem | Tinggi |
| FR-62 | Sistem harus menampilkan riwayat pengajuan, status, dan penyaluran sesuai hak akses. | Semua aktor | Sedang |
---
## 5. Kebutuhan Non-Fungsional
| Kode | Kebutuhan | Keterangan | Prioritas |
|---|---|---|---|
| NFR-01 | Keamanan Password | Password harus disimpan dengan `password_hash()` dan diverifikasi dengan `password_verify()`. | Tinggi |
| NFR-02 | Otorisasi Role | Akses fitur harus dibatasi berdasarkan role. | Tinggi |
| NFR-03 | Validasi Input | Input wajib divalidasi di sisi server. | Tinggi |
| NFR-04 | Prepared Statement | Query yang menerima input pengguna harus memakai prepared statement. | Tinggi |
| NFR-05 | Kompatibilitas Browser | Sistem dapat berjalan pada browser modern seperti Chrome, Edge, dan Firefox. | Sedang |
| NFR-06 | Responsivitas | Tampilan harus dapat digunakan pada laptop dan perangkat mobile. | Sedang |
| NFR-07 | Ketersediaan | Sistem dapat berjalan pada server PHP dan MySQL/MariaDB standar. | Tinggi |
| NFR-08 | Maintainability | Struktur file sederhana dan mudah dipelihara. | Sedang |
| NFR-09 | Usability | Antarmuka harus mudah dipahami oleh admin dan penerima. | Sedang |
| NFR-10 | Auditability | Perubahan status penting harus tercatat dalam riwayat. | Tinggi |
---
## 6. Kebutuhan Data
### 6.1 Tabel `users`
Menyimpan data akun pengguna.
Kolom utama:
- id
- nama
- username
- password
- role
- status_akun
- created_at
- updated_at
Role yang tersedia:
- super_admin
- admin_rumah_ibadah
- penerima
Status akun:
- aktif
- nonaktif
### 6.2 Tabel `rumah_ibadah`
Menyimpan data rumah ibadah.
Kolom utama:
- id
- user_id
- nama_rumah_ibadah
- agama
- alamat
- nomor_hp
- latitude
- longitude
- status_pengajuan
- status_hapus
- alasan_hapus
- keterangan
- created_at
- updated_at
Status pengajuan:
- menunggu
- diterima
- ditolak
Status hapus:
- normal
- menunggu
- disetujui
- ditolak
### 6.3 Tabel `penerima_bantuan`
Menyimpan data penerima bantuan.
Kolom utama:
- id
- user_id
- rumah_ibadah_id
- nama_penerima
- nik
- no_hp
- alamat
- pekerjaan
- penghasilan
- jumlah_tanggungan
- kondisi_rumah
- jenis_bantuan
- latitude
- longitude
- jarak_meter
- status_pengajuan
- status_hapus
- pin_aktif_user_id
- alasan_hapus
- alasan_status
- sumber_pengajuan
- created_at
- updated_at
Status pengajuan:
- menunggu
- diterima
- ditolak
- dibatalkan
Aturan penting:
- `pin_aktif_user_id` digunakan untuk mencegah satu akun penerima memiliki lebih dari satu pin aktif.
### 6.4 Tabel `program_bantuan`
Menyimpan program bantuan.
Kolom utama:
- id
- rumah_ibadah_id
- nama_program
- jenis_bantuan
- deskripsi
- bulan
- tahun
- target_frekuensi
- nominal
- jumlah_item
- status_program
- created_at
- updated_at
Status program:
- aktif
- selesai
- dibatalkan
### 6.5 Tabel `penyaluran_bantuan`
Menyimpan catatan penyaluran bantuan.
Kolom utama:
- id
- program_id
- penerima_id
- rumah_ibadah_id
- admin_id
- jenis_bantuan
- nominal
- jumlah_item
- bantuan_ke
- keterangan
- tanggal_penyaluran
- created_at
### 6.6 Tabel `riwayat_status`
Menyimpan riwayat perubahan status penerima.
Kolom utama:
- id
- penerima_id
- user_id
- status_lama
- status_baru
- catatan
- created_at
---
## 7. Aturan Bisnis
| Kode | Aturan Bisnis |
|---|---|
| BR-01 | Setiap pengguna harus login untuk mengakses sistem. |
| BR-02 | Akun nonaktif tidak boleh login. |
| BR-03 | Registrasi umum hanya untuk role penerima. |
| BR-04 | Admin rumah ibadah hanya boleh membuat akun penerima. |
| BR-05 | Super admin dapat membuat akun admin rumah ibadah dan penerima. |
| BR-06 | Satu akun penerima hanya boleh memiliki satu pin aktif. |
| BR-07 | Penerima dikaitkan ke rumah ibadah terdekat dalam radius 300 meter. |
| BR-08 | Jika tidak ada rumah ibadah dalam radius 300 meter, penerima belum memiliki rumah ibadah peninjau. |
| BR-09 | Admin rumah ibadah hanya boleh mengelola rumah ibadah miliknya. |
| BR-10 | Admin rumah ibadah tidak boleh membatalkan penerima yang sudah diterima. |
| BR-11 | Penghapusan marker/pin oleh admin rumah ibadah dan penerima harus disetujui super admin. |
| BR-12 | Marker/pin yang status hapusnya disetujui tidak tampil sebagai data aktif. |
| BR-13 | Program dan penerima harus berada pada rumah ibadah yang sama agar penyaluran dapat dicatat. |
| BR-14 | Program berstatus selesai atau dibatalkan tidak dapat menerima penyaluran baru. |
| BR-15 | Target frekuensi menjadi acuan laporan dan peringatan, bukan penghapus data riwayat. |
| BR-16 | Data yang sudah punya relasi tidak boleh dihapus sembarangan agar audit tetap aman. |
---
## 8. Use Case Sistem
### 8.1 Daftar Use Case
| Kode | Use Case | Aktor |
|---|---|---|
| UC-01 | Login | Semua aktor |
| UC-02 | Logout | Semua aktor |
| UC-03 | Registrasi Akun Penerima | Penerima |
| UC-04 | Kelola Akun | Super Admin, Admin Rumah Ibadah |
| UC-05 | Lihat Peta | Semua aktor |
| UC-06 | Tambah/Ajukan Rumah Ibadah | Super Admin, Admin Rumah Ibadah |
| UC-07 | Setujui/Tolak Rumah Ibadah | Super Admin |
| UC-08 | Tambah/Ajukan Penerima | Semua aktor sesuai hak akses |
| UC-09 | Setujui/Tolak Penerima | Super Admin, Admin Rumah Ibadah |
| UC-10 | Ajukan Hapus Marker/Pin | Admin Rumah Ibadah, Penerima |
| UC-11 | Setujui/Tolak Hapus Marker/Pin | Super Admin |
| UC-12 | Buat Program Bantuan | Super Admin, Admin Rumah Ibadah |
| UC-13 | Hapus/Batalkan Program Bantuan | Super Admin, Admin Rumah Ibadah |
| UC-14 | Catat Penyaluran Bantuan | Super Admin, Admin Rumah Ibadah |
| UC-15 | Lihat Laporan Penyaluran | Semua aktor sesuai hak akses |
| UC-16 | Lihat Riwayat | Semua aktor sesuai hak akses |
### 8.2 Deskripsi Use Case Utama
#### UC-01 Login
Aktor memasukkan username dan password. Sistem memeriksa akun pada tabel `users`. Jika akun aktif dan password benar, sistem membuat session dan mengarahkan pengguna ke dashboard.
#### UC-04 Kelola Akun
Super Admin dapat membuat, mengedit, menonaktifkan, dan menghapus akun admin rumah ibadah atau penerima. Admin Rumah Ibadah hanya dapat mengelola akun penerima.
#### UC-06 Tambah/Ajukan Rumah Ibadah
Super Admin atau Admin Rumah Ibadah memilih titik pada peta dan mengisi data rumah ibadah. Data disimpan dengan status pengajuan sesuai role. Super Admin dapat menyetujui data agar rumah ibadah aktif.
#### UC-08 Tambah/Ajukan Penerima
Pengguna memilih lokasi rumah penerima pada peta dan mengisi data penerima. Sistem mengecek satu pin aktif dan menghitung rumah ibadah terdekat dalam radius 300 meter.
#### UC-14 Catat Penyaluran Bantuan
Admin memilih program dan penerima yang sudah diterima. Sistem memastikan program dan penerima berada pada rumah ibadah yang sama, lalu menyimpan catatan penyaluran.
---
## 9. Matriks Hak Akses
| Fitur | Super Admin | Admin Rumah Ibadah | Penerima |
|---|---:|---:|---:|
| Login | Ya | Ya | Ya |
| Registrasi akun sendiri | Tidak | Tidak | Ya |
| Kelola akun admin rumah ibadah | Ya | Tidak | Tidak |
| Kelola akun penerima | Ya | Ya | Tidak |
| Lihat peta | Ya | Ya | Ya |
| Tambah rumah ibadah | Ya | Ajukan | Tidak |
| Setujui rumah ibadah | Ya | Tidak | Tidak |
| Tambah penerima | Ya | Ya, sesuai radius | Ajukan diri sendiri |
| Setujui penerima | Ya | Ya, sesuai wilayah | Tidak |
| Reset/batalkan penerima diterima | Ya | Tidak | Tidak |
| Buat program bantuan | Ya | Ya, miliknya | Tidak |
| Catat penyaluran bantuan | Ya | Ya, miliknya | Tidak |
| Lihat laporan | Ya, semua | Ya, miliknya | Ya, miliknya |
| Hapus marker/pin langsung | Ya | Tidak | Tidak |
| Ajukan hapus marker/pin | Tidak perlu | Ya | Ya, pin sendiri |
| Setujui hapus marker/pin | Ya | Tidak | Tidak |
---
## 10. Antarmuka Sistem
### 10.1 Halaman Login
Komponen:
- Input username.
- Input password.
- Tombol Login.
- Link Registrasi Penerima.
- Informasi akun demo.
### 10.2 Halaman Register
Komponen:
- Nama lengkap.
- Username.
- Password.
- Konfirmasi password.
- Tombol Daftar.
### 10.3 Halaman Peta
Komponen:
- Peta Leaflet.
- Marker rumah ibadah.
- Pin penerima.
- Popup detail.
- Form tambah/ajukan rumah ibadah.
- Form tambah/ajukan penerima.
- Tombol setujui/tolak.
- Tombol hapus/ajukan hapus.
### 10.4 Halaman Dashboard
Komponen:
- Statistik data.
- Panel persetujuan hapus marker/pin.
- Form kelola akun.
- Daftar akun.
- Monitoring penyaluran per periode.
- Rekap penyaluran.
- Form program bantuan.
- Daftar program bantuan.
- Form penyaluran bantuan.
- Tabel laporan penerima.
- Riwayat pengajuan dan bantuan.
---
## 11. Kebutuhan Lingkungan
### 11.1 Localhost
| Komponen | Kebutuhan |
|---|---|
| Web server | Apache dari Laragon |
| Database | MySQL/MariaDB |
| PHP | PHP versi yang mendukung MySQLi dan password_hash |
| Browser | Chrome, Edge, Firefox, atau browser modern lain |
| Internet | Dibutuhkan untuk Leaflet CDN |
### 11.2 Hosting
| Komponen | Kebutuhan |
|---|---|
| Hosting | cPanel atau hosting PHP sejenis |
| PHP | Mendukung MySQLi |
| Database | MySQL/MariaDB |
| SSL | Disarankan menggunakan HTTPS |
| File Manager/FTP | Untuk upload file aplikasi |
---
## 12. Kriteria Penerimaan Sistem
Sistem dianggap berhasil jika:
1. Pengguna dapat login sesuai role.
2. Akun nonaktif tidak dapat login.
3. Super admin dapat mengelola akun.
4. Admin rumah ibadah dapat mengelola akun penerima.
5. Peta dapat menampilkan rumah ibadah dan penerima.
6. Rumah ibadah dapat diajukan dan disetujui.
7. Penerima dapat diajukan dan disetujui.
8. Sistem mencegah satu akun penerima memiliki lebih dari satu pin aktif.
9. Sistem menghubungkan penerima ke rumah ibadah terdekat dalam radius 300 meter.
10. Program bantuan dapat dibuat.
11. Penyaluran bantuan dapat dicatat.
12. Laporan bulanan dapat menampilkan status bantuan.
13. Riwayat perubahan status tersimpan.
14. Pengajuan hapus marker/pin dapat diproses oleh super admin.
15. Aplikasi dapat berjalan di localhost dan hosting.
---
## 13. Risiko dan Mitigasi
| Risiko | Dampak | Mitigasi |
|---|---|---|
| File SQL gagal diimport | Sistem tidak dapat berjalan | Sediakan panduan import dan troubleshooting. |
| Password akun salah | Pengguna tidak bisa login | Gunakan akun demo dengan password `password`. |
| Leaflet CDN tidak bisa diakses | Peta tidak muncul | Pastikan koneksi internet aktif atau siapkan Leaflet lokal jika diperlukan. |
| Data penerima ganda | Laporan tidak akurat | Terapkan aturan satu pin aktif per akun penerima. |
| Admin salah mengelola data RI lain | Data tidak valid | Terapkan pembatasan akses berdasarkan user_id rumah ibadah. |
| Hapus data merusak audit | Riwayat hilang | Gunakan status hapus/nonaktif, bukan hapus permanen untuk data berelasi. |
---
## 14. Penutup
Dokumen SKPL/SRS ini menjadi acuan kebutuhan aplikasi WebGIS Poverty Mapping Project 05. Sistem dirancang untuk membantu pendataan penerima bantuan berbasis lokasi, pengelolaan rumah ibadah, pencatatan program bantuan, penyaluran bantuan, dan pelaporan penerima yang sudah atau belum menerima bantuan.
@@ -0,0 +1,510 @@
TUTORIAL CARA PAKAI APLIKASI WEBGIS POVERTY MAPPING PROJECT 05
=================================================================
A. AKUN LOGIN DEMO
------------------
Semua password akun demo adalah:
password
Akun utama:
1. Super Admin
Username : superadmin
Password : password
Contoh akun admin rumah ibadah jika memakai database demo:
2. Admin Gereja Sesilia
Username : admingerejasesilia
Password : password
3. Admin Gereja Elim
Username : admingerejaelim
Password : password
Contoh akun penerima jika memakai database demo:
4. Penerima 02
Username : penerima02
Password : password
5. Penerima 04
Username : penerima04
Password : password
6. Penerima 05
Username : penerima05
Password : password
Catatan:
- Akun nonaktif tidak bisa login.
- Jika membuat akun baru, password minimal 6 karakter.
=================================================================
B. ALUR UMUM APLIKASI
---------------------
Aplikasi ini dipakai untuk:
1. Memetakan rumah ibadah.
2. Memetakan calon penerima bantuan.
3. Menghubungkan penerima dengan rumah ibadah terdekat dalam radius 300 meter.
4. Menyetujui atau menolak pengajuan penerima.
5. Membuat program bantuan.
6. Mencatat penyaluran bantuan.
7. Melihat laporan penerima yang sudah/belum diberi bantuan per bulan.
Role pengguna:
1. Super Admin
Mengelola semua data.
2. Admin Rumah Ibadah
Mengelola rumah ibadah, penerima, program, dan penyaluran sesuai rumah ibadahnya.
3. Penerima
Mengajukan titik lokasi rumah dan melihat riwayat bantuan.
=================================================================
C. CARA LOGIN
-------------
1. Buka aplikasi:
http://localhost/webgis-poverty/05/
2. Masukkan username.
3. Masukkan password.
4. Klik Login.
5. Setelah berhasil, pengguna masuk ke dashboard.
=================================================================
D. CARA LOGOUT
--------------
1. Klik tombol Logout di bagian atas halaman.
2. Sistem akan keluar dari akun.
3. Pengguna diarahkan kembali ke halaman login.
=================================================================
E. CARA PAKAI SEBAGAI SUPER ADMIN
---------------------------------
Super Admin adalah role tertinggi.
Super Admin dapat mengelola seluruh data aplikasi.
1. Login Super Admin
Username : superadmin
Password : password
2. Melihat Dashboard
Setelah login, dashboard menampilkan statistik seperti:
- Rumah ibadah menunggu.
- Rumah ibadah diterima.
- Penerima menunggu.
- Penerima diterima.
- Penerima ditolak.
- Program aktif.
3. Mengelola Akun
Langkah:
- Buka Dashboard.
- Cari bagian Kelola Akun Pengguna.
- Isi nama, username, password, role, dan status akun.
- Pilih role: Admin Rumah Ibadah atau Penerima.
- Klik Buat Akun.
Super Admin dapat:
- membuat akun admin rumah ibadah;
- membuat akun penerima;
- mengedit akun;
- menonaktifkan akun;
- menghapus akun jika belum punya relasi data.
Jika akun sudah punya relasi data, sistem tidak menghapus permanen, tetapi menonaktifkan akun agar data riwayat tetap aman.
4. Menambah Rumah Ibadah
Langkah:
- Klik Buka Peta.
- Klik titik lokasi rumah ibadah di peta.
- Klik Tambah Rumah Ibadah.
- Isi username admin rumah ibadah.
- Isi nama rumah ibadah.
- Pilih agama.
- Isi alamat dan nomor HP.
- Klik Ajukan/Tambah.
Jika Super Admin yang menambah, data dapat langsung aktif/diterima.
5. Menyetujui Rumah Ibadah
Langkah:
- Buka peta.
- Klik marker rumah ibadah yang berstatus menunggu.
- Klik Setujui jika data benar.
- Klik Tolak jika data tidak valid.
Jika rumah ibadah disetujui, sistem akan menghitung ulang penerima yang berada dalam radius 300 meter.
6. Menambah Penerima
Langkah:
- Buka peta.
- Klik lokasi rumah penerima.
- Klik Tambah Penerima.
- Isi username akun penerima.
- Isi nama, NIK, nomor HP, alamat, pekerjaan, penghasilan, jumlah tanggungan, kondisi rumah, dan jenis bantuan.
- Klik Simpan.
Sistem akan mencari rumah ibadah terdekat dalam radius 300 meter.
7. Menyetujui Penerima
Langkah:
- Buka peta.
- Klik marker penerima berstatus menunggu.
- Klik Setujui untuk menerima.
- Klik Tolak untuk menolak.
Penerima yang diterima dapat masuk ke laporan dan penyaluran bantuan.
8. Menghapus Marker/Pin
Super Admin dapat langsung menghapus marker rumah ibadah atau pin penerima dari peta aktif.
Langkah:
- Klik marker/pin.
- Klik Hapus Marker atau Hapus Pin.
- Isi alasan jika diminta.
- Konfirmasi.
Data tidak hilang permanen dari database, tetapi tidak tampil lagi sebagai data aktif.
9. Memproses Pengajuan Hapus Marker/Pin
Langkah:
- Buka Dashboard.
- Cari bagian Persetujuan Hapus Marker/Pin.
- Lihat pengajuan hapus rumah ibadah atau penerima.
- Klik Setujui Hapus atau Tolak Hapus.
10. Membuat Program Bantuan
Langkah:
- Buka Dashboard.
- Cari bagian Buat Program Bantuan.
- Pilih rumah ibadah.
- Isi nama program.
- Pilih jenis bantuan.
- Isi bulan dan tahun.
- Isi target frekuensi.
- Isi nominal atau jumlah item jika ada.
- Isi deskripsi.
- Klik Simpan.
11. Menghapus Program Bantuan
Langkah:
- Buka bagian Daftar Program.
- Pilih program.
- Klik Hapus.
Jika program sudah punya data penyaluran, sistem dapat membatalkan program, bukan menghapus permanen.
12. Mencatat Penyaluran Bantuan
Langkah:
- Buka Dashboard.
- Cari bagian Catat Penyaluran Bantuan.
- Pilih program.
- Pilih penerima diterima.
- Jenis bantuan, nominal, dan item akan mengikuti program.
- Isi tanggal.
- Isi keterangan jika perlu.
- Klik Simpan.
Syarat:
- Penerima harus berstatus diterima.
- Penerima harus berada pada rumah ibadah yang sama dengan program.
- Penerima harus berada dalam radius 300 meter.
- Program tidak boleh berstatus selesai atau dibatalkan.
13. Melihat Laporan Bulanan
Langkah:
- Buka Dashboard.
- Cari bagian Monitoring Penyaluran Per Periode.
- Pilih bulan dan tahun.
- Gunakan filter rumah ibadah, agama, program, jenis bantuan, dan status.
- Klik tampilkan/muat laporan.
Status laporan:
- belum_diberi
- sudah_diberi
- belum_memenuhi_target
- sesuai_target
- melebihi_target
14. Melihat Riwayat
Langkah:
- Buka Dashboard.
- Cari bagian Riwayat Pengajuan dan Bantuan.
- Lihat daftar pengajuan, perubahan status, dan penyaluran.
=================================================================
F. CARA PAKAI SEBAGAI ADMIN RUMAH IBADAH
----------------------------------------
Admin Rumah Ibadah mengelola data sesuai rumah ibadah miliknya.
Satu admin dapat mengelola lebih dari satu rumah ibadah.
1. Login Admin Rumah Ibadah
Contoh:
Username : admingerejasesilia
Password : password
2. Mengajukan Rumah Ibadah
Langkah:
- Buka peta.
- Klik lokasi rumah ibadah.
- Klik Ajukan Rumah Ibadah.
- Isi nama rumah ibadah, agama, alamat, dan nomor HP.
- Klik Ajukan.
Status awal adalah menunggu.
Super Admin harus menyetujui pengajuan tersebut.
3. Membuat Akun Penerima
Langkah:
- Buka Dashboard.
- Cari Kelola Akun Pengguna.
- Isi nama, username, dan password penerima.
- Role otomatis/terbatas sebagai penerima.
- Klik Buat Akun.
4. Menambah Penerima
Langkah:
- Buka peta.
- Klik lokasi rumah penerima.
- Klik Tambah Penerima.
- Isi username penerima.
- Isi data penerima.
- Klik Simpan.
Catatan:
- Penerima harus berada dalam radius 300 meter dari rumah ibadah milik admin.
- Jika lokasi lebih dekat ke rumah ibadah lain, sistem akan menolak agar data tidak salah sinkron.
5. Menyetujui atau Menolak Penerima
Langkah:
- Buka peta.
- Klik marker penerima menunggu.
- Klik Setujui atau Tolak.
Batasan:
- Admin rumah ibadah tidak boleh mengembalikan status penerima ke menunggu.
- Admin rumah ibadah tidak boleh membatalkan penerima yang sudah diterima.
- Untuk reset/batal penerima diterima, hubungi Super Admin.
6. Membuat Program Bantuan
Langkah:
- Buka Dashboard.
- Pilih rumah ibadah milik admin.
- Isi nama program, jenis bantuan, bulan, tahun, target, nominal/item, dan deskripsi.
- Klik Simpan.
7. Mencatat Penyaluran Bantuan
Langkah:
- Buka Dashboard.
- Pilih program.
- Pilih penerima yang sudah diterima.
- Isi tanggal dan keterangan.
- Klik Simpan.
Sistem hanya menampilkan/menerima penerima yang sesuai dengan rumah ibadah program.
8. Melihat Laporan
Langkah:
- Buka Dashboard.
- Pilih bulan dan tahun.
- Gunakan filter yang tersedia.
- Lihat data penerima yang sudah atau belum diberi bantuan.
9. Mengajukan Hapus Marker/Pin
Langkah:
- Buka peta.
- Klik marker rumah ibadah milik admin atau pin penerima yang dikelola.
- Klik Ajukan Hapus.
- Isi alasan.
- Kirim.
Super Admin akan memutuskan setuju atau tolak.
=================================================================
G. CARA PAKAI SEBAGAI PENERIMA
------------------------------
Penerima dapat membuat akun sendiri melalui registrasi.
1. Registrasi Akun Penerima
Langkah:
- Buka halaman login.
- Klik Registrasi.
- Isi nama lengkap.
- Isi username.
- Isi password minimal 6 karakter.
- Isi konfirmasi password.
- Klik Daftar.
2. Login Penerima
Langkah:
- Buka halaman login.
- Masukkan username dan password.
- Klik Login.
3. Mengajukan Titik Lokasi Rumah
Langkah:
- Buka peta.
- Klik lokasi rumah pada peta.
- Isi data penerima.
- Klik Simpan Pengajuan.
Sistem akan memeriksa apakah lokasi masuk radius 300 meter dari rumah ibadah diterima.
4. Melihat Status Pengajuan
Langkah:
- Buka peta atau dashboard.
- Lihat status pengajuan.
Status yang mungkin:
- menunggu
- diterima
- ditolak
- dibatalkan
5. Melihat Riwayat Bantuan
Langkah:
- Buka Dashboard.
- Lihat bagian riwayat atau laporan yang tersedia untuk akun penerima.
6. Mengajukan Hapus Pin
Langkah:
- Buka peta.
- Klik pin milik sendiri.
- Klik Ajukan Hapus Pin.
- Isi alasan.
- Kirim.
Pengajuan hapus harus disetujui Super Admin.
=================================================================
H. ATURAN PENTING SISTEM
------------------------
1. Satu akun penerima hanya boleh punya satu pin aktif.
2. Rumah ibadah aktif adalah rumah ibadah yang statusnya diterima dan tidak dihapus.
3. Penerima aktif adalah penerima yang statusnya menunggu/diterima dan tidak dihapus.
4. Radius tanggung jawab rumah ibadah adalah 300 meter.
5. Jika ada beberapa rumah ibadah dalam radius 300 meter, penerima dikaitkan ke rumah ibadah terdekat.
6. Admin rumah ibadah hanya boleh mengelola data rumah ibadahnya sendiri.
7. Super Admin memiliki akses paling luas.
8. Password akun demo adalah password.
9. File SQL dan ZIP backup jangan dibiarkan di public_html setelah hosting.
=================================================================
I. TIPS SAAT PRESENTASI / DEMO
------------------------------
Urutan demo yang rapi:
1. Login sebagai Super Admin.
2. Tunjukkan dashboard.
3. Buka peta.
4. Tunjukkan marker rumah ibadah dan penerima.
5. Tambahkan akun penerima baru.
6. Tambahkan atau ajukan rumah ibadah.
7. Tambahkan penerima dari peta.
8. Setujui penerima.
9. Buat program bantuan.
10. Catat penyaluran bantuan.
11. Buka laporan bulanan.
12. Tunjukkan status sudah/belum diberi bantuan.
13. Tunjukkan pengajuan hapus marker/pin.
14. Logout.
=================================================================
J. MASALAH UMUM DAN SOLUSI CEPAT
--------------------------------
1. Tidak bisa login
- Cek username.
- Cek password: password.
- Cek status akun harus aktif.
2. Peta tidak muncul
- Pastikan internet aktif.
- Leaflet diambil dari CDN.
3. Marker tidak muncul
- Cek status data.
- Data ditolak atau dihapus memang tidak tampil aktif.
4. Penerima tidak bisa ditambah
- Cek apakah akun penerima sudah punya pin aktif.
- Cek apakah koordinat valid.
- Cek apakah username penerima benar dan aktif.
5. Admin rumah ibadah tidak bisa menambah penerima
- Cek apakah lokasi penerima masuk radius 300 meter rumah ibadah miliknya.
- Cek apakah penerima lebih dekat ke rumah ibadah lain.
6. Tidak bisa mencatat bantuan
- Cek status program.
- Cek status penerima.
- Cek apakah program dan penerima berada pada rumah ibadah yang sama.
=================================================================
SELESAI
=================================================================
+49
View File
@@ -0,0 +1,49 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
require_role(['super_admin','admin_rumah_ibadah'], true);
$allowedAgama = ['islam','kristen','katolik','hindu','buddha','konghucu'];
$nama = clean_text(post('nama_rumah_ibadah'), 150);
$agama = post('agama');
$alamat = clean_text(post('alamat'), 1000);
$nomor_hp = clean_text(post('nomor_hp'), 20);
$username_admin = clean_text(post('username_admin'), 50);
$lat = post('latitude');
$lng = post('longitude');
if ($nama==='' || $alamat==='' || $nomor_hp==='' || !valid_latlng($lat, $lng)) json_response('err', 'Data rumah ibadah belum lengkap atau koordinat tidak valid.');
if (!valid_enum($agama, $allowedAgama)) json_response('err', 'Agama tidak valid.');
if ($_SESSION['role'] === 'admin_rumah_ibadah') {
$user_id = (int) $_SESSION['user_id'];
$status = 'menunggu';
$ket = 'Menunggu persetujuan super admin.';
} else {
// Super admin wajib mengaitkan titik rumah ibadah ke username admin rumah ibadah.
if ($username_admin === '') json_response('err', 'Username admin rumah ibadah wajib diisi agar titik rumah ibadah punya pengelola.', [], 422);
$akunAdmin = get_user_by_username_role($username_admin, 'admin_rumah_ibadah');
if (!$akunAdmin) json_response('err', 'Username admin rumah ibadah tidak ditemukan, bukan role admin_rumah_ibadah, atau akun tidak aktif.', [], 404);
$user_id = (int) $akunAdmin['id'];
$status = 'diterima';
$ket = 'Ditambahkan langsung oleh super admin.';
}
$stmt = $koneksi->prepare("INSERT INTO rumah_ibadah (user_id, nama_rumah_ibadah, agama, alamat, nomor_hp, latitude, longitude, status_pengajuan, keterangan) VALUES (?,?,?,?,?,?,?,?,?)");
$stmt->bind_param('issssddss', $user_id, $nama, $agama, $alamat, $nomor_hp, $lat, $lng, $status, $ket);
if (!$stmt->execute()) json_response('err', 'Gagal mengajukan rumah ibadah: ' . $koneksi->error);
$id = $stmt->insert_id;
$jumlahSinkron = 0;
if ($status === 'diterima') {
// Jika rumah ibadah langsung diterima, penerima lama yang berada dalam radius otomatis ditautkan.
$jumlahSinkron = sync_penerima_area_by_rumah_ibadah($id);
}
$stmt = $koneksi->prepare("SELECT ri.*, u.nama AS nama_admin, u.username AS username_admin FROM rumah_ibadah ri LEFT JOIN users u ON u.id=ri.user_id WHERE ri.id=?");
$stmt->bind_param('i', $id);
$row = fetch_all_stmt($stmt)[0] ?? null;
if ($row) $row['jumlah_penerima_tersinkron'] = $jumlahSinkron;
json_response('ok', 'Rumah ibadah berhasil diajukan.' . ($jumlahSinkron ? " $jumlahSinkron penerima lama otomatis masuk area tanggung jawab." : ''), $row);
?>
+55
View File
@@ -0,0 +1,55 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
$user = current_user();
$role = $user['role'];
$data = ['rumah_ibadah' => [], 'penerima' => []];
// Ambil rumah ibadah sesuai role.
if ($role === 'super_admin') {
$sql = "SELECT ri.*, u.nama AS nama_admin FROM rumah_ibadah ri LEFT JOIN users u ON u.id=ri.user_id WHERE ri.status_pengajuan IN ('diterima','menunggu') AND COALESCE(ri.status_hapus,'normal')<>'disetujui' ORDER BY ri.id DESC";
$res = $koneksi->query($sql);
while ($row = $res->fetch_assoc()) $data['rumah_ibadah'][] = $row;
} elseif ($role === 'admin_rumah_ibadah') {
$uid = (int) $user['id'];
// Admin hanya melihat rumah ibadah miliknya, termasuk yang masih menunggu.
$stmt = $koneksi->prepare("SELECT ri.*, u.nama AS nama_admin FROM rumah_ibadah ri LEFT JOIN users u ON u.id=ri.user_id WHERE ri.user_id=? AND ri.status_pengajuan IN ('diterima','menunggu') AND COALESCE(ri.status_hapus,'normal')<>'disetujui' ORDER BY ri.id DESC");
$stmt->bind_param('i', $uid);
$data['rumah_ibadah'] = fetch_all_stmt($stmt);
} else {
$res = $koneksi->query("SELECT ri.*, NULL AS nama_admin FROM rumah_ibadah ri WHERE ri.status_pengajuan='diterima' AND COALESCE(ri.status_hapus,'normal')<>'disetujui' ORDER BY ri.id DESC");
while ($row = $res->fetch_assoc()) $data['rumah_ibadah'][] = $row;
}
// Ambil penerima sesuai role. Marker ditolak tidak dikirim.
if ($role === 'super_admin') {
$sql = "SELECT pb.*, ri.nama_rumah_ibadah, ri.agama FROM penerima_bantuan pb LEFT JOIN rumah_ibadah ri ON ri.id=pb.rumah_ibadah_id WHERE pb.status_pengajuan IN ('diterima','menunggu','dibatalkan') AND COALESCE(pb.status_hapus,'normal')<>'disetujui' ORDER BY pb.id DESC";
$res = $koneksi->query($sql);
while ($row = $res->fetch_assoc()) $data['penerima'][] = $row;
} elseif ($role === 'admin_rumah_ibadah') {
$ids = get_admin_rumah_ibadah_ids((int)$user['id'], true);
if (!empty($ids)) {
$types = '';
$params = [];
$in = build_in_condition('pb.rumah_ibadah_id', $ids, $types, $params);
$sql = "SELECT pb.*, ri.nama_rumah_ibadah, ri.agama
FROM penerima_bantuan pb
LEFT JOIN rumah_ibadah ri ON ri.id=pb.rumah_ibadah_id
WHERE $in
AND pb.status_pengajuan IN ('diterima','menunggu')
AND COALESCE(pb.status_hapus,'normal')<>'disetujui'
ORDER BY pb.id DESC";
$stmt = $koneksi->prepare($sql);
stmt_bind($stmt, $types, $params);
$data['penerima'] = fetch_all_stmt($stmt);
}
} else {
$uid = (int) $user['id'];
$stmt = $koneksi->prepare("SELECT pb.*, ri.nama_rumah_ibadah, ri.agama FROM penerima_bantuan pb LEFT JOIN rumah_ibadah ri ON ri.id=pb.rumah_ibadah_id WHERE pb.user_id=? AND pb.status_pengajuan IN ('diterima','menunggu','dibatalkan') AND COALESCE(pb.status_hapus,'normal')<>'disetujui' ORDER BY pb.id DESC");
$stmt->bind_param('i', $uid);
$data['penerima'] = fetch_all_stmt($stmt);
}
json_response('ok', 'Data peta berhasil diambil.', $data);
?>
+138
View File
@@ -0,0 +1,138 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
$bulan = (int) getv('bulan', date('n'));
$tahun = (int) getv('tahun', date('Y'));
$rumah_filter = getv('rumah_ibadah_id', '');
$agama_filter = getv('agama', '');
$program_filter = getv('program_id', '');
$status_filter = getv('status_penyaluran', 'semua');
$jenis_filter = getv('jenis_bantuan', '');
if ($bulan < 1 || $bulan > 12 || $tahun < 2000) json_response('err', 'Filter bulan/tahun tidak valid.');
if (!valid_enum($status_filter, ['semua','belum_diberi','sudah_diberi','belum_memenuhi_target','sesuai_target','melebihi_target'])) json_response('err', 'Status penyaluran tidak valid.');
$where = ["pb.status_pengajuan='diterima'", "COALESCE(pb.status_hapus,'normal')<>'disetujui'", "COALESCE(ri.status_hapus,'normal')<>'disetujui'", "prog.bulan=?", "prog.tahun=?", "prog.status_program<>'dibatalkan'"];
$types = 'ii';
$params = [$bulan, $tahun];
if ($program_filter !== '') {
$where[] = 'prog.id=?';
$types .= 'i';
$params[] = (int)$program_filter;
}
if ($jenis_filter !== '') {
$where[] = 'prog.jenis_bantuan=?';
$types .= 's';
$params[] = $jenis_filter;
}
if ($agama_filter !== '') {
$where[] = 'ri.agama=?';
$types .= 's';
$params[] = $agama_filter;
}
if ($rumah_filter !== '') {
$where[] = 'ri.id=?';
$types .= 'i';
$params[] = (int)$rumah_filter;
}
if ($_SESSION['role'] === 'admin_rumah_ibadah') {
$ids = get_admin_rumah_ibadah_ids((int)$_SESSION['user_id'], true);
if (empty($ids)) json_response('ok', 'Admin belum memiliki rumah ibadah diterima.', ['rows'=>[], 'rekap'=>[]]);
$where[] = build_in_condition('ri.id', $ids, $types, $params);
} elseif ($_SESSION['role'] === 'penerima') {
$where[] = 'pb.user_id=?';
$types .= 'i';
$params[] = (int)$_SESSION['user_id'];
}
$whereSql = implode(' AND ', $where);
$sql = "
SELECT
pb.id AS penerima_id,
pb.nama_penerima,
pb.no_hp,
pb.alamat,
pb.jenis_bantuan AS jenis_bantuan_diminta,
ri.id AS rumah_ibadah_id,
ri.nama_rumah_ibadah,
ri.agama,
prog.id AS program_id,
prog.nama_program,
prog.status_program,
prog.jenis_bantuan,
prog.bulan,
prog.tahun,
prog.target_frekuensi,
prog.nominal,
prog.jumlah_item,
COALESCE(cnt.jumlah_penyaluran, 0) AS jumlah_penyaluran,
cnt.tanggal_terakhir,
cnt.riwayat_penyaluran
FROM penerima_bantuan pb
JOIN rumah_ibadah ri ON ri.id = pb.rumah_ibadah_id
JOIN program_bantuan prog ON prog.rumah_ibadah_id = ri.id
LEFT JOIN (
SELECT penerima_id, program_id,
COUNT(id) AS jumlah_penyaluran,
MAX(tanggal_penyaluran) AS tanggal_terakhir,
GROUP_CONCAT(CONCAT(tanggal_penyaluran, ' | ', jenis_bantuan, ' | Rp', COALESCE(nominal,0), ' | Item: ', COALESCE(jumlah_item,''), ' | ke-', COALESCE(bantuan_ke,0), ' | ', COALESCE(keterangan,'')) ORDER BY tanggal_penyaluran SEPARATOR ';;') AS riwayat_penyaluran
FROM penyaluran_bantuan
WHERE MONTH(tanggal_penyaluran)=? AND YEAR(tanggal_penyaluran)=?
GROUP BY penerima_id, program_id
) cnt ON cnt.penerima_id = pb.id AND cnt.program_id = prog.id
WHERE $whereSql
ORDER BY ri.nama_rumah_ibadah ASC, pb.nama_penerima ASC, prog.nama_program ASC
";
// Dua parameter pertama untuk subquery count bulan/tahun, lalu parameter where.
$bindTypes = 'ii' . $types;
$bindParams = array_merge([$bulan, $tahun], $params);
$stmt = $koneksi->prepare($sql);
if (!$stmt) json_response('err', 'Query laporan gagal disiapkan: ' . $koneksi->error);
stmt_bind($stmt, $bindTypes, $bindParams);
$stmt->execute();
$res = $stmt->get_result();
$rows = [];
$rekap = [
'total' => 0,
'belum_diberi' => 0,
'sudah_diberi' => 0,
'belum_memenuhi_target' => 0,
'sesuai_target' => 0,
'melebihi_target' => 0
];
while ($row = $res->fetch_assoc()) {
$jumlah = (int)$row['jumlah_penyaluran'];
$target = max(1, (int)$row['target_frekuensi']);
if ($jumlah === 0) $status = 'belum_diberi';
elseif ($jumlah > 0 && $jumlah < $target) $status = 'belum_memenuhi_target';
elseif ($jumlah === $target) $status = 'sesuai_target';
else $status = 'melebihi_target';
$row['status_penyaluran'] = $status;
$row['riwayat'] = $row['riwayat_penyaluran'] ? explode(';;', $row['riwayat_penyaluran']) : [];
$include = false;
if ($status_filter === 'semua') $include = true;
elseif ($status_filter === 'sudah_diberi') $include = $jumlah > 0;
else $include = ($status_filter === $status);
if ($include) {
$rows[] = $row;
$rekap['total']++;
if ($jumlah > 0) $rekap['sudah_diberi']++;
else $rekap['belum_diberi']++;
if ($status === 'belum_memenuhi_target') $rekap['belum_memenuhi_target']++;
if ($status === 'sesuai_target') $rekap['sesuai_target']++;
if ($status === 'melebihi_target') $rekap['melebihi_target']++;
}
}
json_response('ok', 'Laporan penyaluran berhasil diambil.', ['rows' => $rows, 'rekap' => $rekap]);
?>
+28
View File
@@ -0,0 +1,28 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
$role = $_SESSION['role'];
$rows = [];
if ($role === 'super_admin') {
$sql = "SELECT p.*, ri.nama_rumah_ibadah FROM program_bantuan p LEFT JOIN rumah_ibadah ri ON ri.id=p.rumah_ibadah_id ORDER BY p.tahun DESC, p.bulan DESC, p.id DESC";
$res = $koneksi->query($sql);
while ($row = $res->fetch_assoc()) $rows[] = $row;
} elseif ($role === 'admin_rumah_ibadah') {
$ids = get_admin_rumah_ibadah_ids((int)$_SESSION['user_id'], true);
if (!empty($ids)) {
$types = '';
$params = [];
$in = build_in_condition('p.rumah_ibadah_id', $ids, $types, $params);
$stmt = $koneksi->prepare("SELECT p.*, ri.nama_rumah_ibadah FROM program_bantuan p LEFT JOIN rumah_ibadah ri ON ri.id=p.rumah_ibadah_id WHERE $in ORDER BY p.tahun DESC, p.bulan DESC, p.id DESC");
stmt_bind($stmt, $types, $params);
$rows = fetch_all_stmt($stmt);
}
} else {
$uid = (int) $_SESSION['user_id'];
$stmt = $koneksi->prepare("SELECT DISTINCT p.*, ri.nama_rumah_ibadah FROM program_bantuan p INNER JOIN penyaluran_bantuan pb ON pb.program_id=p.id INNER JOIN penerima_bantuan pr ON pr.id=pb.penerima_id LEFT JOIN rumah_ibadah ri ON ri.id=p.rumah_ibadah_id WHERE pr.user_id=? ORDER BY p.tahun DESC, p.bulan DESC, p.id DESC");
$stmt->bind_param('i', $uid);
$rows = fetch_all_stmt($stmt);
}
json_response('ok', 'Program bantuan berhasil diambil.', $rows);
?>
+44
View File
@@ -0,0 +1,44 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
$role = $_SESSION['role'];
$data = ['pengajuan' => [], 'status' => [], 'penyaluran' => []];
if ($role === 'super_admin') {
$sql = "SELECT pb.*, ri.nama_rumah_ibadah FROM penerima_bantuan pb LEFT JOIN rumah_ibadah ri ON ri.id=pb.rumah_ibadah_id ORDER BY pb.created_at DESC LIMIT 100";
$res = $koneksi->query($sql); while($row=$res->fetch_assoc()) $data['pengajuan'][]=$row;
$sql = "SELECT rw.*, pb.nama_penerima, u.nama AS nama_user FROM riwayat_status rw JOIN penerima_bantuan pb ON pb.id=rw.penerima_id JOIN users u ON u.id=rw.user_id ORDER BY rw.created_at DESC LIMIT 100";
$res = $koneksi->query($sql); while($row=$res->fetch_assoc()) $data['status'][]=$row;
$sql = "SELECT pn.*, pb.nama_penerima, ri.nama_rumah_ibadah, pr.nama_program FROM penyaluran_bantuan pn JOIN penerima_bantuan pb ON pb.id=pn.penerima_id JOIN rumah_ibadah ri ON ri.id=pn.rumah_ibadah_id LEFT JOIN program_bantuan pr ON pr.id=pn.program_id ORDER BY pn.tanggal_penyaluran DESC, pn.id DESC LIMIT 100";
$res = $koneksi->query($sql); while($row=$res->fetch_assoc()) $data['penyaluran'][]=$row;
} elseif ($role === 'admin_rumah_ibadah') {
$ids = get_admin_rumah_ibadah_ids((int)$_SESSION['user_id'], true);
if (!empty($ids)) {
$types = ''; $params = [];
$inPb = build_in_condition('pb.rumah_ibadah_id', $ids, $types, $params);
$stmt = $koneksi->prepare("SELECT pb.*, ri.nama_rumah_ibadah FROM penerima_bantuan pb LEFT JOIN rumah_ibadah ri ON ri.id=pb.rumah_ibadah_id WHERE $inPb ORDER BY pb.created_at DESC LIMIT 100");
stmt_bind($stmt, $types, $params); $data['pengajuan'] = fetch_all_stmt($stmt);
$types = ''; $params = [];
$inPb = build_in_condition('pb.rumah_ibadah_id', $ids, $types, $params);
$stmt = $koneksi->prepare("SELECT rw.*, pb.nama_penerima, u.nama AS nama_user FROM riwayat_status rw JOIN penerima_bantuan pb ON pb.id=rw.penerima_id JOIN users u ON u.id=rw.user_id WHERE $inPb ORDER BY rw.created_at DESC LIMIT 100");
stmt_bind($stmt, $types, $params); $data['status'] = fetch_all_stmt($stmt);
$types = ''; $params = [];
$inPn = build_in_condition('pn.rumah_ibadah_id', $ids, $types, $params);
$stmt = $koneksi->prepare("SELECT pn.*, pb.nama_penerima, ri.nama_rumah_ibadah, pr.nama_program FROM penyaluran_bantuan pn JOIN penerima_bantuan pb ON pb.id=pn.penerima_id JOIN rumah_ibadah ri ON ri.id=pn.rumah_ibadah_id LEFT JOIN program_bantuan pr ON pr.id=pn.program_id WHERE $inPn ORDER BY pn.tanggal_penyaluran DESC, pn.id DESC LIMIT 100");
stmt_bind($stmt, $types, $params); $data['penyaluran'] = fetch_all_stmt($stmt);
}
} else {
$uid = (int)$_SESSION['user_id'];
$stmt = $koneksi->prepare("SELECT pb.*, ri.nama_rumah_ibadah FROM penerima_bantuan pb LEFT JOIN rumah_ibadah ri ON ri.id=pb.rumah_ibadah_id WHERE pb.user_id=? ORDER BY pb.created_at DESC LIMIT 100");
$stmt->bind_param('i', $uid); $data['pengajuan'] = fetch_all_stmt($stmt);
$stmt = $koneksi->prepare("SELECT rw.*, pb.nama_penerima, u.nama AS nama_user FROM riwayat_status rw JOIN penerima_bantuan pb ON pb.id=rw.penerima_id JOIN users u ON u.id=rw.user_id WHERE pb.user_id=? ORDER BY rw.created_at DESC LIMIT 100");
$stmt->bind_param('i', $uid); $data['status'] = fetch_all_stmt($stmt);
$stmt = $koneksi->prepare("SELECT pn.*, pb.nama_penerima, ri.nama_rumah_ibadah, pr.nama_program FROM penyaluran_bantuan pn JOIN penerima_bantuan pb ON pb.id=pn.penerima_id JOIN rumah_ibadah ri ON ri.id=pn.rumah_ibadah_id LEFT JOIN program_bantuan pr ON pr.id=pn.program_id WHERE pb.user_id=? ORDER BY pn.tanggal_penyaluran DESC, pn.id DESC LIMIT 100");
$stmt->bind_param('i', $uid); $data['penyaluran'] = fetch_all_stmt($stmt);
}
json_response('ok', 'Riwayat berhasil diambil.', $data);
?>
+76
View File
@@ -0,0 +1,76 @@
<?php
/**
* ambil_user.php
* Mengambil daftar akun untuk tabel kelola akun di dashboard.
* Filter: role, status_akun, keyword.
*/
require_once __DIR__ . '/auth.php';
require_login(true);
require_role(['super_admin', 'admin_rumah_ibadah'], true);
$actor = current_user();
$actorRole = $actor['role'];
$filterRole = getv('role', 'semua');
$status = getv('status_akun', 'semua');
$keyword = clean_text(getv('keyword', ''), 100);
$where = [];
$types = '';
$params = [];
if ($actorRole === 'super_admin') {
// Super admin mengelola akun admin rumah ibadah dan penerima saja.
$where[] = "u.role IN ('admin_rumah_ibadah','penerima')";
if (in_array($filterRole, ['admin_rumah_ibadah', 'penerima'], true)) {
$where[] = "u.role=?";
$types .= 's';
$params[] = $filterRole;
}
} else {
// Admin rumah ibadah hanya mengelola akun penerima.
$where[] = "u.role='penerima'";
// Jika admin punya lebih dari satu rumah ibadah, semua ID rumah ibadah miliknya harus ikut terbaca.
// Akun penerima tanpa titik tetap ditampilkan agar bisa dipakai saat tambah penerima.
$ids = get_admin_rumah_ibadah_ids((int)$actor['id'], true);
if (!empty($ids)) {
$in = build_in_condition('pb.rumah_ibadah_id', $ids, $types, $params);
$where[] = "(pb.id IS NULL OR $in)";
} else {
$where[] = "pb.id IS NULL";
}
}
if (in_array($status, ['aktif', 'nonaktif'], true)) {
$where[] = "u.status_akun=?";
$types .= 's';
$params[] = $status;
}
if ($keyword !== '') {
$where[] = "(u.nama LIKE ? OR u.username LIKE ?)";
$kw = '%' . $keyword . '%';
$types .= 'ss';
$params[] = $kw;
$params[] = $kw;
}
$sql = "SELECT
u.id, u.nama, u.username, u.role, u.status_akun, u.created_at, u.updated_at,
COUNT(DISTINCT pb.id) AS jumlah_titik_penerima,
COUNT(DISTINCT ri.id) AS jumlah_rumah_ibadah,
GROUP_CONCAT(DISTINCT ri.nama_rumah_ibadah SEPARATOR ', ') AS rumah_ibadah_dikelola,
GROUP_CONCAT(DISTINCT rpb.nama_rumah_ibadah SEPARATOR ', ') AS rumah_ibadah_penerima
FROM users u
LEFT JOIN penerima_bantuan pb ON pb.user_id=u.id AND COALESCE(pb.status_hapus,'normal')<>'disetujui'
LEFT JOIN rumah_ibadah rpb ON rpb.id=pb.rumah_ibadah_id
LEFT JOIN rumah_ibadah ri ON ri.user_id=u.id AND COALESCE(ri.status_hapus,'normal')<>'disetujui'
WHERE " . implode(' AND ', $where) . "
GROUP BY u.id
ORDER BY u.updated_at DESC, u.id DESC";
$stmt = $koneksi->prepare($sql);
if (!$stmt) json_response('err', 'Query daftar akun gagal: ' . $koneksi->error, [], 500);
if ($types) stmt_bind($stmt, $types, $params);
$rows = fetch_all_stmt($stmt);
json_response('ok', 'Daftar akun berhasil diambil.', $rows);
?>
+314
View File
@@ -0,0 +1,314 @@
<?php
/**
* auth.php
* Fungsi session, role pengguna, response JSON, validasi umum,
* prepared statement helper, perhitungan Haversine, dan sinkronisasi area.
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/koneksi.php';
function json_response($status = 'ok', $message = '', $data = [], $http_code = 200) {
http_response_code($http_code);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'status' => $status,
'message' => $message,
'data' => $data
], JSON_UNESCAPED_UNICODE);
exit;
}
function require_post() {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
json_response('err', 'Method harus POST.', [], 405);
}
}
function is_logged_in() {
return isset($_SESSION['user_id'], $_SESSION['role']);
}
function require_login($json = false) {
if (!is_logged_in()) {
if ($json) json_response('err', 'Silakan login terlebih dahulu.', [], 401);
header('Location: login.php');
exit;
}
}
function current_user() {
global $koneksi;
if (!is_logged_in()) return null;
$id = (int) $_SESSION['user_id'];
$stmt = $koneksi->prepare("SELECT id, nama, username, role, status_akun FROM users WHERE id=? LIMIT 1");
$stmt->bind_param('i', $id);
$stmt->execute();
$res = $stmt->get_result();
return $res->fetch_assoc();
}
function has_role($roles) {
if (!is_array($roles)) $roles = [$roles];
return is_logged_in() && in_array($_SESSION['role'], $roles, true);
}
function require_role($roles, $json = false) {
if (!has_role($roles)) {
if ($json) json_response('err', 'Akses ditolak. Role tidak sesuai.', [], 403);
http_response_code(403);
echo '<h2>403 - Akses ditolak</h2>';
exit;
}
}
function post($key, $default = '') {
return isset($_POST[$key]) ? trim((string) $_POST[$key]) : $default;
}
function getv($key, $default = '') {
return isset($_GET[$key]) ? trim((string) $_GET[$key]) : $default;
}
function clean_text($str, $max = 255) {
$str = trim((string) $str);
if (mb_strlen($str) > $max) $str = mb_substr($str, 0, $max);
return $str;
}
function valid_enum($value, $allowed) {
return in_array($value, $allowed, true);
}
function valid_latlng($lat, $lng) {
return is_numeric($lat) && is_numeric($lng) && $lat >= -90 && $lat <= 90 && $lng >= -180 && $lng <= 180;
}
function haversine_meter($lat1, $lon1, $lat2, $lon2) {
$R = 6371000; // meter
$dLat = deg2rad($lat2 - $lat1);
$dLon = deg2rad($lon2 - $lon1);
$a = sin($dLat / 2) * sin($dLat / 2) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLon / 2) * sin($dLon / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return $R * $c;
}
function stmt_bind($stmt, $types, $params) {
if ($types === '' || empty($params)) return true;
$refs = [];
$refs[] = $types;
foreach ($params as $k => $v) {
$refs[] = &$params[$k];
}
return call_user_func_array([$stmt, 'bind_param'], $refs);
}
function fetch_all_stmt($stmt) {
$stmt->execute();
$result = $stmt->get_result();
$rows = [];
while ($row = $result->fetch_assoc()) $rows[] = $row;
return $rows;
}
function build_in_condition($column, $values, &$types, &$params) {
$ids = [];
foreach ((array)$values as $v) {
$iv = (int)$v;
if ($iv > 0) $ids[] = $iv;
}
$ids = array_values(array_unique($ids));
if (empty($ids)) return '1=0';
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$types .= str_repeat('i', count($ids));
foreach ($ids as $id) $params[] = $id;
return "$column IN ($placeholders)";
}
function get_admin_rumah_ibadah_list($user_id = null, $only_diterima = true) {
global $koneksi;
if ($user_id === null) $user_id = (int) $_SESSION['user_id'];
$sql = "SELECT * FROM rumah_ibadah WHERE user_id=? AND COALESCE(status_hapus,'normal')<>'disetujui'";
if ($only_diterima) $sql .= " AND status_pengajuan='diterima'";
$sql .= " ORDER BY id ASC";
$stmt = $koneksi->prepare($sql);
$stmt->bind_param('i', $user_id);
return fetch_all_stmt($stmt);
}
function get_admin_rumah_ibadah_ids($user_id = null, $only_diterima = true) {
$rows = get_admin_rumah_ibadah_list($user_id, $only_diterima);
return array_map('intval', array_column($rows, 'id'));
}
function get_admin_rumah_ibadah($user_id = null) {
// Dipertahankan untuk kompatibilitas lama. Untuk logic baru, pakai get_admin_rumah_ibadah_list()/ids().
$rows = get_admin_rumah_ibadah_list($user_id, true);
return $rows[0] ?? null;
}
function is_admin_rumah_ibadah_id($rumah_ibadah_id, $user_id = null, $only_diterima = true) {
if ($user_id === null) $user_id = (int) $_SESSION['user_id'];
$ids = get_admin_rumah_ibadah_ids($user_id, $only_diterima);
return in_array((int)$rumah_ibadah_id, $ids, true);
}
function get_nearest_rumah_ibadah($lat, $lng, $max_meter = 300, $admin_user_id = null) {
global $koneksi;
$rows = [];
if ($admin_user_id !== null) {
$stmt = $koneksi->prepare("SELECT * FROM rumah_ibadah WHERE user_id=? AND status_pengajuan='diterima' AND COALESCE(status_hapus,'normal')<>'disetujui'");
$admin_user_id = (int)$admin_user_id;
$stmt->bind_param('i', $admin_user_id);
$rumahRows = fetch_all_stmt($stmt);
} else {
$rumahRows = [];
$res = $koneksi->query("SELECT * FROM rumah_ibadah WHERE status_pengajuan='diterima' AND COALESCE(status_hapus,'normal')<>'disetujui'");
while ($row = $res->fetch_assoc()) $rumahRows[] = $row;
}
foreach ($rumahRows as $row) {
$jarak = haversine_meter((float) $lat, (float) $lng, (float) $row['latitude'], (float) $row['longitude']);
if ($jarak <= $max_meter) {
$row['jarak_meter'] = round($jarak, 2);
$rows[] = $row;
}
}
usort($rows, fn($a, $b) => $a['jarak_meter'] <=> $b['jarak_meter']);
return $rows[0] ?? null;
}
function get_penerima_by_id($id) {
global $koneksi;
$stmt = $koneksi->prepare("SELECT pb.*, ri.nama_rumah_ibadah, ri.user_id AS admin_rumah_user_id, ri.latitude AS ri_latitude, ri.longitude AS ri_longitude FROM penerima_bantuan pb LEFT JOIN rumah_ibadah ri ON ri.id=pb.rumah_ibadah_id WHERE pb.id=? LIMIT 1");
$stmt->bind_param('i', $id);
$stmt->execute();
return $stmt->get_result()->fetch_assoc();
}
function get_rumah_ibadah_by_id($id) {
global $koneksi;
$stmt = $koneksi->prepare("SELECT ri.*, u.nama AS nama_admin, u.username AS username_admin FROM rumah_ibadah ri LEFT JOIN users u ON u.id=ri.user_id WHERE ri.id=? LIMIT 1");
$stmt->bind_param('i', $id);
$stmt->execute();
return $stmt->get_result()->fetch_assoc();
}
function can_manage_rumah_ibadah($rumah) {
if (!$rumah || !is_logged_in()) return false;
if ($_SESSION['role'] === 'super_admin') return true;
if ($_SESSION['role'] === 'admin_rumah_ibadah') {
return (int)$rumah['user_id'] === (int)$_SESSION['user_id'];
}
return false;
}
function can_manage_penerima($penerima) {
if (!$penerima || !is_logged_in()) return false;
if ($_SESSION['role'] === 'super_admin') return true;
if ($_SESSION['role'] === 'penerima' && (int) $penerima['user_id'] === (int) $_SESSION['user_id']) return true;
if ($_SESSION['role'] === 'admin_rumah_ibadah') {
return !empty($penerima['rumah_ibadah_id']) && is_admin_rumah_ibadah_id((int)$penerima['rumah_ibadah_id'], null, true);
}
return false;
}
function insert_riwayat_status($penerima_id, $status_lama, $status_baru, $catatan) {
global $koneksi;
$uid = (int) $_SESSION['user_id'];
$stmt = $koneksi->prepare("INSERT INTO riwayat_status (penerima_id, user_id, status_lama, status_baru, catatan) VALUES (?,?,?,?,?)");
$stmt->bind_param('iisss', $penerima_id, $uid, $status_lama, $status_baru, $catatan);
$stmt->execute();
}
function get_user_by_username_role($username, $roles) {
global $koneksi;
if (!is_array($roles)) $roles = [$roles];
$username = clean_text($username, 50);
if ($username === '') return null;
$placeholders = implode(',', array_fill(0, count($roles), '?'));
$types = 's' . str_repeat('s', count($roles));
$params = array_merge([$username], $roles);
$stmt = $koneksi->prepare("SELECT id, nama, username, role, status_akun FROM users WHERE username=? AND role IN ($placeholders) AND status_akun='aktif' LIMIT 1");
stmt_bind($stmt, $types, $params);
$stmt->execute();
return $stmt->get_result()->fetch_assoc();
}
function has_active_penerima_pin($user_id, $exclude_penerima_id = 0) {
global $koneksi;
$user_id = (int)$user_id;
$exclude_penerima_id = (int)$exclude_penerima_id;
if ($user_id <= 0) return false;
$sql = "SELECT id FROM penerima_bantuan
WHERE user_id=?
AND status_pengajuan IN ('menunggu','diterima')
AND COALESCE(status_hapus,'normal')<>'disetujui'";
$types = 'i';
$params = [$user_id];
if ($exclude_penerima_id > 0) {
$sql .= " AND id<>?";
$types .= 'i';
$params[] = $exclude_penerima_id;
}
$sql .= " LIMIT 1";
$stmt = $koneksi->prepare($sql);
stmt_bind($stmt, $types, $params);
$stmt->execute();
return (bool)$stmt->get_result()->fetch_assoc();
}
function sync_single_penerima_area($penerima_id) {
global $koneksi;
$penerima_id = (int)$penerima_id;
$stmt = $koneksi->prepare("SELECT id, latitude, longitude FROM penerima_bantuan WHERE id=? AND status_pengajuan IN ('menunggu','diterima') AND COALESCE(status_hapus,'normal')<>'disetujui' LIMIT 1");
$stmt->bind_param('i', $penerima_id);
$stmt->execute();
$pb = $stmt->get_result()->fetch_assoc();
if (!$pb) return false;
$nearest = get_nearest_rumah_ibadah((float)$pb['latitude'], (float)$pb['longitude'], 300);
if ($nearest) {
$riId = (int)$nearest['id'];
$jarak = (float)$nearest['jarak_meter'];
$stmtUp = $koneksi->prepare("UPDATE penerima_bantuan SET rumah_ibadah_id=?, jarak_meter=? WHERE id=?");
$stmtUp->bind_param('idi', $riId, $jarak, $penerima_id);
} else {
$stmtUp = $koneksi->prepare("UPDATE penerima_bantuan SET rumah_ibadah_id=NULL, jarak_meter=NULL WHERE id=?");
$stmtUp->bind_param('i', $penerima_id);
}
return $stmtUp->execute();
}
function sync_all_penerima_area() {
global $koneksi;
$ids = [];
$res = $koneksi->query("SELECT id FROM penerima_bantuan WHERE status_pengajuan IN ('menunggu','diterima') AND COALESCE(status_hapus,'normal')<>'disetujui'");
while ($row = $res->fetch_assoc()) $ids[] = (int)$row['id'];
$updated = 0;
foreach ($ids as $id) {
if (sync_single_penerima_area($id)) $updated++;
}
return $updated;
}
function sync_penerima_area_by_rumah_ibadah($rumah_ibadah_id) {
// Tetap menerima parameter lama, tetapi sinkronisasi dihitung ulang secara global agar radius beririsan memilih RI terdekat.
return sync_all_penerima_area();
}
function detach_penerima_from_rumah_ibadah($rumah_ibadah_id) {
global $koneksi;
$rumah_ibadah_id = (int) $rumah_ibadah_id;
$stmt = $koneksi->prepare("UPDATE penerima_bantuan SET rumah_ibadah_id=NULL, jarak_meter=NULL WHERE rumah_ibadah_id=? AND status_pengajuan IN ('menunggu','diterima')");
$stmt->bind_param('i', $rumah_ibadah_id);
$stmt->execute();
$affected = $stmt->affected_rows;
// Setelah dilepas, cari ulang rumah ibadah aktif terdekat lain jika ada.
sync_all_penerima_area();
return $affected;
}
?>
+436
View File
@@ -0,0 +1,436 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(false);
$user = current_user();
$role = $user['role'];
$bulanIni = (int) date('n');
$tahunIni = (int) date('Y');
function scalar_count($sql, $types = '', $params = []) {
global $koneksi;
$stmt = $koneksi->prepare($sql);
if ($types) stmt_bind($stmt, $types, $params);
$stmt->execute();
$row = $stmt->get_result()->fetch_assoc();
return (int) array_values($row)[0];
}
$riAdminList = ($role === 'admin_rumah_ibadah') ? get_admin_rumah_ibadah_list((int)$user['id'], true) : [];
$riAdminIds = array_map('intval', array_column($riAdminList, 'id'));
$stats = [];
if ($role === 'super_admin') {
$stats['Total Rumah Ibadah'] = scalar_count("SELECT COUNT(*) FROM rumah_ibadah WHERE COALESCE(status_hapus,'normal')<>'disetujui'");
$stats['Rumah Ibadah Menunggu'] = scalar_count("SELECT COUNT(*) FROM rumah_ibadah WHERE status_pengajuan='menunggu' AND COALESCE(status_hapus,'normal')<>'disetujui'");
$stats['Total Penerima'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE COALESCE(status_hapus,'normal')<>'disetujui'");
$stats['Penerima Menunggu'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE status_pengajuan='menunggu' AND COALESCE(status_hapus,'normal')<>'disetujui'");
$stats['Penerima Diterima'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE status_pengajuan='diterima' AND COALESCE(status_hapus,'normal')<>'disetujui'");
$stats['Penerima Ditolak'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE status_pengajuan='ditolak' AND COALESCE(status_hapus,'normal')<>'disetujui'");
$stats['Penerima Dibatalkan'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE status_pengajuan='dibatalkan' AND COALESCE(status_hapus,'normal')<>'disetujui'");
$stats['Program Aktif'] = scalar_count("SELECT COUNT(*) FROM program_bantuan WHERE status_program='aktif'");
$stats['Bantuan Tersalurkan'] = scalar_count("SELECT COUNT(*) FROM penyaluran_bantuan");
} elseif ($role === 'admin_rumah_ibadah') {
if (!empty($riAdminIds)) {
$stats['Jumlah Rumah Ibadah'] = count($riAdminIds);
$stats['Rumah Ibadah'] = implode(', ', array_column($riAdminList, 'nama_rumah_ibadah'));
$types = ''; $params = []; $in = build_in_condition('rumah_ibadah_id', $riAdminIds, $types, $params);
$stats['Pengajuan Dalam Radius'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE $in AND status_pengajuan='menunggu' AND COALESCE(status_hapus,'normal')<>'disetujui'", $types, $params);
$types = ''; $params = []; $in = build_in_condition('rumah_ibadah_id', $riAdminIds, $types, $params);
$stats['Penerima Diterima'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE $in AND status_pengajuan='diterima' AND COALESCE(status_hapus,'normal')<>'disetujui'", $types, $params);
$types = ''; $params = []; $in = build_in_condition('rumah_ibadah_id', $riAdminIds, $types, $params);
$stats['Penerima Ditolak'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE $in AND status_pengajuan='ditolak' AND COALESCE(status_hapus,'normal')<>'disetujui'", $types, $params);
$types = ''; $params = []; $in = build_in_condition('rumah_ibadah_id', $riAdminIds, $types, $params);
$stats['Program Aktif'] = scalar_count("SELECT COUNT(*) FROM program_bantuan WHERE $in AND status_program='aktif'", $types, $params);
$types = ''; $params = []; $in = build_in_condition('rumah_ibadah_id', $riAdminIds, $types, $params);
$stats['Bantuan Tersalurkan'] = scalar_count("SELECT COUNT(*) FROM penyaluran_bantuan WHERE $in", $types, $params);
} else {
$stats['Rumah Ibadah'] = 'Belum ada rumah ibadah diterima';
}
} elseif ($role === 'penerima') {
$uid = (int)$user['id'];
$stats['Pengajuan Saya'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE user_id=? AND COALESCE(status_hapus,'normal')<>'disetujui'", 'i', [$uid]);
$stats['Status Diterima'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE user_id=? AND status_pengajuan='diterima' AND COALESCE(status_hapus,'normal')<>'disetujui'", 'i', [$uid]);
$stats['Bantuan Bulan Ini'] = scalar_count("SELECT COUNT(*) FROM penyaluran_bantuan pn JOIN penerima_bantuan pb ON pb.id=pn.penerima_id WHERE pb.user_id=? AND MONTH(pn.tanggal_penyaluran)=? AND YEAR(pn.tanggal_penyaluran)=?", 'iii', [$uid, $bulanIni, $tahunIni]);
}
$hapusRumahRows = [];
$hapusPenerimaRows = [];
if ($role === 'super_admin') {
$stmt = $koneksi->prepare("SELECT ri.*, u.nama AS nama_admin, u.username AS username_admin FROM rumah_ibadah ri LEFT JOIN users u ON u.id=ri.user_id WHERE ri.status_hapus='menunggu' ORDER BY ri.updated_at DESC");
$hapusRumahRows = fetch_all_stmt($stmt);
$stmt = $koneksi->prepare("SELECT pb.*, u.username AS username_penerima, ri.nama_rumah_ibadah FROM penerima_bantuan pb LEFT JOIN users u ON u.id=pb.user_id LEFT JOIN rumah_ibadah ri ON ri.id=pb.rumah_ibadah_id WHERE pb.status_hapus='menunggu' ORDER BY pb.updated_at DESC");
$hapusPenerimaRows = fetch_all_stmt($stmt);
}
?>
<!DOCTYPE html><html lang="id"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Dashboard WebGIS Poverty Mapping</title>
<style>
*{box-sizing:border-box}body{margin:0;font-family:Arial,sans-serif;background:#f8fafc;color:#0f172a}.top{background:#0f766e;color:#fff;padding:18px 24px;display:flex;justify-content:space-between;gap:12px;align-items:center}.top h1{font-size:22px;margin:0}.top a{color:#fff;text-decoration:none;background:rgba(255,255,255,.16);padding:8px 12px;border-radius:8px}.wrap{padding:22px;max-width:1280px;margin:auto}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:14px}.card,.section{background:#fff;border:1px solid #e2e8f0;border-radius:14px;box-shadow:0 8px 24px rgba(15,23,42,.06)}.card{padding:16px}.card .k{color:#64748b;font-size:12px}.card .v{font-size:22px;font-weight:800;margin-top:7px}.section{padding:18px;margin-top:18px}.section h2{font-size:18px;margin:0 0 14px}.filters,.row2{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.fg label{display:block;font-size:12px;font-weight:700;color:#475569;margin-bottom:5px}.fg input,.fg select,.fg textarea{width:100%;border:1px solid #cbd5e1;border-radius:9px;padding:9px}.fg textarea{min-height:70px}.btn{border:0;border-radius:9px;padding:9px 12px;font-weight:700;cursor:pointer}.btn[disabled]{cursor:not-allowed;opacity:.55}.btn.primary{background:#0f766e;color:#fff}.btn.gray{background:#e2e8f0;color:#334155}.btn.small{padding:7px 9px;font-size:12px}table{width:100%;border-collapse:collapse;font-size:13px}th,td{padding:10px;border-bottom:1px solid #e2e8f0;text-align:left;vertical-align:top}th{background:#f1f5f9;color:#334155}.muted{color:#64748b;font-size:12px}.badge{display:inline-block;border-radius:999px;padding:4px 8px;font-size:11px;font-weight:700;background:#e2e8f0}.belum_diberi{background:#fee2e2;color:#991b1b}.sudah_diberi,.sesuai_target{background:#dcfce7;color:#166534}.belum_memenuhi_target{background:#fef3c7;color:#92400e}.melebihi_target{background:#e0e7ff;color:#3730a3}.readonly{background:#f8fafc}.notice{background:#ecfeff;border:1px solid #a5f3fc;color:#155e75;padding:10px;border-radius:10px;margin-bottom:12px}.toastbox{position:fixed;right:18px;top:18px;z-index:9999;display:grid;gap:10px}.toast{background:#0f172a;color:#fff;padding:12px 14px;border-radius:10px;box-shadow:0 12px 30px rgba(15,23,42,.25);max-width:360px;font-size:13px}.toast.err{background:#991b1b}.toast.ok{background:#166534}
</style></head><body>
<div class="top"><div><h1>Dashboard WebGIS Poverty Mapping</h1><div class="muted" style="color:#d1fae5">Role: <?= htmlspecialchars($role) ?> | <?= htmlspecialchars($user['nama']) ?></div></div><div><a href="index.php">Buka Peta</a> <a href="logout.php">Logout</a></div></div>
<div id="toastBox" class="toastbox"></div>
<div class="wrap">
<div class="grid"><?php foreach($stats as $k=>$v): ?><div class="card"><div class="k"><?= htmlspecialchars($k) ?></div><div class="v"><?= htmlspecialchars((string)$v) ?></div></div><?php endforeach; ?></div>
<?php if($role === 'super_admin'): ?>
<div class="section">
<h2>Persetujuan Hapus Marker/Pin</h2>
<p class="muted">Admin rumah ibadah dan penerima hanya mengajukan hapus. Marker benar-benar hilang dari peta setelah disetujui super admin.</p>
<h3 style="font-size:15px;margin:12px 0 8px">Pengajuan Hapus Rumah Ibadah</h3>
<div style="overflow:auto"><table><thead><tr><th>Rumah Ibadah</th><th>Admin</th><th>Alasan</th><th>Aksi</th></tr></thead><tbody>
<?php if(empty($hapusRumahRows)): ?><tr><td colspan="4">Tidak ada pengajuan hapus rumah ibadah.</td></tr><?php endif; ?>
<?php foreach($hapusRumahRows as $r): ?><tr>
<td><b><?= htmlspecialchars($r['nama_rumah_ibadah']) ?></b><br><span class="muted"><?= htmlspecialchars($r['agama']) ?> | <?= htmlspecialchars($r['alamat']) ?></span></td>
<td><?= htmlspecialchars($r['nama_admin'] ?? '-') ?><br><span class="muted"><?= htmlspecialchars($r['username_admin'] ?? '-') ?></span></td>
<td><?= htmlspecialchars($r['alasan_hapus'] ?? '-') ?></td>
<td><button class="btn primary small" onclick="updateStatusRumahIbadah(<?= (int)$r['id'] ?>,'hapus_disetujui')">Setujui</button> <button class="btn gray small" onclick="updateStatusRumahIbadah(<?= (int)$r['id'] ?>,'hapus_ditolak')">Tolak</button></td>
</tr><?php endforeach; ?>
</tbody></table></div>
<h3 style="font-size:15px;margin:16px 0 8px">Pengajuan Hapus Penerima</h3>
<div style="overflow:auto"><table><thead><tr><th>Penerima</th><th>Rumah Ibadah</th><th>Alasan</th><th>Aksi</th></tr></thead><tbody>
<?php if(empty($hapusPenerimaRows)): ?><tr><td colspan="4">Tidak ada pengajuan hapus penerima.</td></tr><?php endif; ?>
<?php foreach($hapusPenerimaRows as $p): ?><tr>
<td><b><?= htmlspecialchars($p['nama_penerima']) ?></b><br><span class="muted">User: <?= htmlspecialchars($p['username_penerima'] ?? '-') ?> | <?= htmlspecialchars($p['alamat']) ?></span></td>
<td><?= htmlspecialchars($p['nama_rumah_ibadah'] ?? '-') ?></td>
<td><?= htmlspecialchars($p['alasan_hapus'] ?? '-') ?></td>
<td><button class="btn primary small" onclick="updateStatusPenerima(<?= (int)$p['id'] ?>,'hapus_disetujui')">Setujui</button> <button class="btn gray small" onclick="updateStatusPenerima(<?= (int)$p['id'] ?>,'hapus_ditolak')">Tolak</button></td>
</tr><?php endforeach; ?>
</tbody></table></div>
</div>
<?php endif; ?>
<?php if($role === 'super_admin' || $role === 'admin_rumah_ibadah'): ?>
<div class="section">
<h2>Kelola Akun Pengguna</h2>
<p class="muted">
<?php if($role === 'super_admin'): ?>
Super admin dapat mengelola akun penerima dan admin rumah ibadah. Akun super admin lain tidak ditampilkan di daftar ini.
<?php else: ?>
Admin rumah ibadah hanya dapat mengelola akun penerima. Akun admin rumah ibadah lain tidak bisa dilihat atau diubah.
<?php endif; ?>
</p>
<input type="hidden" id="u_id" value="">
<div class="filters">
<div class="fg"><label>Nama</label><input id="u_nama" placeholder="Contoh: Admin Masjid / Nama Penerima"></div>
<div class="fg"><label>Username</label><input id="u_username" placeholder="Contoh: adminmasjid / penerima01"></div>
<div class="fg"><label>Password</label><input id="u_password" type="password" placeholder="Minimal 6 karakter. Kosongkan saat edit jika tidak diganti."></div>
<div class="fg"><label>Role</label><select id="u_role">
<?php if($role === 'super_admin'): ?><option value="admin_rumah_ibadah">Admin Rumah Ibadah</option><?php endif; ?>
<option value="penerima">Penerima</option>
</select></div>
<div class="fg"><label>Status</label><select id="u_status"><option value="aktif">Aktif</option><option value="nonaktif">Nonaktif</option></select></div>
<div class="fg"><label>&nbsp;</label><button class="btn primary" id="u_submit_btn" onclick="submitUser()">Buat Akun</button></div>
<div class="fg"><label>&nbsp;</label><button class="btn gray" onclick="resetUserForm()">Reset Form</button></div>
</div>
<h3 style="font-size:15px;margin:16px 0 8px">Daftar Akun</h3>
<div class="filters">
<div class="fg"><label>Filter Role</label><select id="fu_role" onchange="loadUsers()">
<?php if($role === 'super_admin'): ?>
<option value="semua">Semua</option>
<option value="admin_rumah_ibadah">Admin Rumah Ibadah</option>
<?php endif; ?>
<option value="penerima">Penerima</option>
</select></div>
<div class="fg"><label>Filter Status</label><select id="fu_status" onchange="loadUsers()"><option value="semua">Semua</option><option value="aktif">Aktif</option><option value="nonaktif">Nonaktif</option></select></div>
<div class="fg"><label>Cari Nama/Username</label><input id="fu_keyword" placeholder="Ketik nama atau username" oninput="debouncedLoadUsers()"></div>
<div class="fg"><label>&nbsp;</label><button class="btn primary" onclick="loadUsers()">Terapkan Filter</button></div>
</div>
<div style="overflow:auto"><table><thead><tr><th>Akun</th><th>Role</th><th>Status</th><th>Relasi Data</th><th>Dibuat/Update</th><th>Aksi</th></tr></thead><tbody id="akunBody"><tr><td colspan="6">Memuat akun...</td></tr></tbody></table></div>
</div>
<?php endif; ?>
<div class="section">
<h2>Monitoring Penyaluran Per Periode</h2>
<div class="filters">
<div class="fg"><label>Bulan</label><select id="f_bulan"><?php for($i=1;$i<=12;$i++): ?><option value="<?= $i ?>" <?= $i===$bulanIni?'selected':'' ?>><?= $i ?></option><?php endfor; ?></select></div>
<div class="fg"><label>Tahun</label><input id="f_tahun" type="number" value="<?= $tahunIni ?>"></div>
<div class="fg"><label>Rumah Ibadah</label><select id="f_rumah_ibadah"><option value="">Semua</option></select></div>
<div class="fg"><label>Agama</label><select id="f_agama"><option value="">Semua</option><option value="islam">Islam</option><option value="kristen">Kristen</option><option value="katolik">Katolik</option><option value="hindu">Hindu</option><option value="buddha">Buddha</option><option value="konghucu">Konghucu</option></select></div>
<div class="fg"><label>Program Bantuan</label><select id="f_program"><option value="">Semua</option></select></div>
<div class="fg"><label>Jenis Bantuan</label><select id="f_jenis"><option value="">Semua</option><option value="zakat">Zakat</option><option value="sembako">Sembako</option><option value="uang_tunai">Uang Tunai</option><option value="lainnya">Lainnya</option></select></div>
<div class="fg"><label>Status</label><select id="f_status"><option value="semua">Semua</option><option value="belum_diberi">Belum diberi</option><option value="sudah_diberi">Sudah diberi</option><option value="belum_memenuhi_target">Belum memenuhi target</option><option value="sesuai_target">Sesuai target</option><option value="melebihi_target">Melebihi target</option></select></div>
<div class="fg"><label>&nbsp;</label><button class="btn primary" onclick="loadLaporanPenyaluran()">Terapkan Filter</button></div>
</div>
</div>
<div class="section">
<h2>Rekap Penyaluran</h2>
<div class="grid" id="rekapBox"></div>
</div>
<?php if($role === 'super_admin' || $role === 'admin_rumah_ibadah'): ?>
<div class="row2">
<div class="section">
<h2>Buat Program Bantuan</h2>
<div class="notice">Jenis bantuan, nominal, dan jumlah item yang diatur di sini akan otomatis dipakai saat penyaluran. Petugas tidak perlu mengetik ulang agar data sinkron.</div>
<?php if($role === 'super_admin' || $role === 'admin_rumah_ibadah'): ?><div class="fg"><label>Rumah Ibadah</label><select id="pr_rumah_ibadah_id"><option value="">Pilih rumah ibadah</option></select></div><?php endif; ?>
<div class="fg"><label>Nama Program</label><input id="pr_nama" placeholder="Contoh: Sembako Januari 2026"></div>
<div class="filters">
<div class="fg"><label>Jenis</label><select id="pr_jenis"><option value="zakat">Zakat</option><option value="sembako">Sembako</option><option value="uang_tunai">Uang Tunai</option><option value="lainnya">Lainnya</option></select></div>
<div class="fg"><label>Bulan</label><input type="number" id="pr_bulan" value="<?= $bulanIni ?>"></div>
<div class="fg"><label>Tahun</label><input type="number" id="pr_tahun" value="<?= $tahunIni ?>"></div>
<div class="fg"><label>Target Frekuensi</label><input type="number" id="pr_target" value="1"></div>
</div>
<div class="filters">
<div class="fg"><label>Nominal Program</label><input type="number" id="pr_nominal" placeholder="Opsional, contoh: 50000"></div>
<div class="fg"><label>Jumlah Item/Paket</label><input id="pr_jumlah_item" placeholder="Contoh: 1 paket sembako / 5 kg beras"></div>
</div>
<div class="fg"><label>Deskripsi</label><textarea id="pr_deskripsi"></textarea></div>
<button class="btn primary" onclick="submitProgramBantuan()">Simpan Program</button>
<h3 style="font-size:15px;margin:16px 0 8px">Daftar Program</h3>
<div style="overflow:auto"><table><thead><tr><th>Program</th><th>Periode</th><th>Jenis</th><th>Nominal/Item</th><th>Status</th><th>Aksi</th></tr></thead><tbody id="programBody"><tr><td colspan="6">Memuat program...</td></tr></tbody></table></div>
</div>
<div class="section">
<h2>Catat Penyaluran Bantuan</h2>
<div class="fg"><label>Program</label><select id="pn_program" onchange="syncProgramDetail()"></select></div>
<div class="fg"><label>Penerima Diterima</label><select id="pn_penerima"></select></div>
<div class="filters">
<div class="fg"><label>Jenis dari Program</label><input id="pn_jenis_view" class="readonly" readonly></div>
<div class="fg"><label>Nominal dari Program</label><input id="pn_nominal_view" class="readonly" readonly></div>
<div class="fg"><label>Jumlah Item dari Program</label><input id="pn_jumlah_item_view" class="readonly" readonly></div>
<div class="fg"><label>Tanggal</label><input type="date" id="pn_tanggal" value="<?= date('Y-m-d') ?>"></div>
</div>
<div class="fg"><label>Keterangan</label><textarea id="pn_keterangan" placeholder="Isi alasan jika bantuan melebihi target program"></textarea></div>
<button class="btn primary" onclick="submitPenyaluran()">Catat Bantuan</button>
</div>
</div>
<?php endif; ?>
<div class="section">
<h2>Data Laporan Penerima Sudah/Belum Diberi Bantuan</h2>
<div style="overflow:auto"><table><thead><tr><th>Penerima</th><th>Rumah Ibadah</th><th>Program</th><th>Target</th><th>Jumlah</th><th>Status</th><th>Terakhir</th><th>Riwayat Bulan Ini</th><th>Aksi</th></tr></thead><tbody id="laporanBody"><tr><td colspan="9">Memuat data...</td></tr></tbody></table></div>
</div>
<div class="section">
<h2>Riwayat Pengajuan dan Bantuan</h2>
<div id="riwayatBox" class="muted">Memuat riwayat...</div>
</div>
</div>
<script>
const USER_ROLE = <?= json_encode($role) ?>;
function esc(v){return String(v??'').replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[m]))}
function toast(msg,type='ok'){ const box=document.getElementById('toastBox'); if(!box){console.log(msg);return;} const el=document.createElement('div'); el.className='toast '+type; el.textContent=msg; box.appendChild(el); setTimeout(()=>{el.style.opacity='0';el.style.transform='translateY(-6px)';},3000); setTimeout(()=>el.remove(),3500); }
function getFilterQuery(){ const q=new URLSearchParams(); q.set('bulan',document.getElementById('f_bulan').value); q.set('tahun',document.getElementById('f_tahun').value); q.set('rumah_ibadah_id',document.getElementById('f_rumah_ibadah')?.value||''); q.set('agama',document.getElementById('f_agama')?.value||''); q.set('program_id',document.getElementById('f_program').value); q.set('jenis_bantuan',document.getElementById('f_jenis').value); q.set('status_penyaluran',document.getElementById('f_status').value); return q.toString(); }
let userSearchTimer=null;
function debouncedLoadUsers(){ clearTimeout(userSearchTimer); userSearchTimer=setTimeout(loadUsers, 350); }
function resetUserForm(){
if(!document.getElementById('u_id')) return;
document.getElementById('u_id').value='';
['u_nama','u_username','u_password'].forEach(id=>document.getElementById(id).value='');
document.getElementById('u_status').value='aktif';
const roleSelect=document.getElementById('u_role'); if(roleSelect) roleSelect.selectedIndex=0;
document.getElementById('u_submit_btn').textContent='Buat Akun';
}
function submitUser(){
const namaEl=document.getElementById('u_nama'); if(!namaEl) return;
const id=document.getElementById('u_id').value;
const usernameEl=document.getElementById('u_username');
const passwordEl=document.getElementById('u_password');
// FIX: validasi input sebelum fetch.
if(!namaEl.value.trim()){ toast('Nama akun wajib diisi.','err'); return; }
if(!usernameEl.value.trim()){ toast('Username wajib diisi.','err'); return; }
if(!id && !passwordEl.value){ toast('Password wajib diisi saat membuat akun baru.','err'); return; }
if(passwordEl.value && passwordEl.value.length < 6){ toast('Password minimal 6 karakter.','err'); return; }
const fd=new FormData();
fd.append('id', id);
fd.append('nama', namaEl.value);
fd.append('username', usernameEl.value);
fd.append('password', passwordEl.value);
fd.append('role', document.getElementById('u_role').value);
fd.append('status_akun', document.getElementById('u_status').value);
const url = id ? 'edit_user.php' : 'tambah_user.php';
fetch(url,{method:'POST',body:fd}).then(r=>r.json()).then(res=>{
toast(res.message, res.status==='ok'?'ok':'err');
if(res.status==='ok'){ resetUserForm(); loadUsers(); }
}).catch(e=>toast('Gagal menyimpan akun: '+e.message,'err'));
}
function loadUsers(){
const body=document.getElementById('akunBody'); if(!body) return;
const q=new URLSearchParams();
q.set('role', document.getElementById('fu_role').value);
q.set('status_akun', document.getElementById('fu_status').value);
q.set('keyword', document.getElementById('fu_keyword').value);
body.innerHTML='<tr><td colspan="6">Memuat akun...</td></tr>';
fetch('ambil_user.php?'+q.toString()).then(r=>r.json()).then(res=>{
if(res.status!=='ok') throw new Error(res.message);
renderUserTable(res.data||[]);
}).catch(e=>{ body.innerHTML=`<tr><td colspan="6">${esc(e.message)}</td></tr>`; });
}
function renderUserTable(rows){
const body=document.getElementById('akunBody'); if(!body) return;
if(!rows.length){body.innerHTML='<tr><td colspan="6">Tidak ada akun sesuai filter.</td></tr>'; return;}
body.innerHTML=rows.map(u=>{
const relasi = u.role==='admin_rumah_ibadah'
? `Rumah ibadah: ${esc(u.rumah_ibadah_dikelola||'-')}`
: `Titik penerima: ${esc(u.jumlah_titik_penerima||0)}<br><span class="muted">Rumah ibadah: ${esc(u.rumah_ibadah_penerima||'-')}</span>`;
return `<tr>
<td><b>${esc(u.nama)}</b><br><span class="muted">${esc(u.username)}</span></td>
<td><span class="badge">${esc(u.role)}</span></td>
<td><span class="badge">${esc(u.status_akun)}</span></td>
<td>${relasi}</td>
<td><span class="muted">${esc(u.created_at||'-')}<br>${esc(u.updated_at||'-')}</span></td>
<td><!--<button class="btn primary small" onclick='editUserForm(${JSON.stringify(u).replace(/'/g,"&#039;")})'>Edit</button>--> <button class="btn gray small" onclick="hapusUser(${u.id})">Hapus/Nonaktifkan</button></td>
</tr>`;
}).join('');
}
function editUserForm(u){
document.getElementById('u_id').value=u.id;
document.getElementById('u_nama').value=u.nama||'';
document.getElementById('u_username').value=u.username||'';
document.getElementById('u_password').value='';
const roleSelect=document.getElementById('u_role');
if([...roleSelect.options].some(o=>o.value===u.role)) roleSelect.value=u.role;
document.getElementById('u_status').value=u.status_akun||'aktif';
document.getElementById('u_submit_btn').textContent='Update Akun';
window.scrollTo({top: document.getElementById('u_nama').getBoundingClientRect().top + window.scrollY - 120, behavior:'smooth'});
}
function hapusUser(id){
if(!confirm('Hapus akun ini? Jika sudah punya relasi data, akun akan dinonaktifkan agar data tidak rusak.')) return;
const fd=new FormData(); fd.append('id', id);
fetch('hapus_user.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{toast(res.message); if(res.status==='ok'){resetUserForm();loadUsers();}}).catch(e=>toast('Gagal hapus akun: '+e.message));
}
function programOption(p){
const label=`${esc(p.nama_program)} (${p.bulan}/${p.tahun}, ${esc(p.jenis_bantuan)}, target ${p.target_frekuensi})`;
// FIX: tambah data-riid agar syncProgramDetail bisa filter penerima per RI.
return `<option value="${p.id}" data-jenis="${esc(p.jenis_bantuan)}" data-nominal="${esc(p.nominal??'')}" data-jumlah="${esc(p.jumlah_item??'')}" data-riid="${esc(p.rumah_ibadah_id??'')}">${label}</option>`;
}
function renderProgramTable(rows){
const body=document.getElementById('programBody'); if(!body) return;
if(!rows.length){body.innerHTML='<tr><td colspan="6">Belum ada program.</td></tr>'; return;}
body.innerHTML=rows.map(p=>`<tr><td><b>${esc(p.nama_program)}</b><br><span class="muted">${esc(p.deskripsi||'')}</span></td><td>${p.bulan}/${p.tahun}<br><span class="muted">Target ${p.target_frekuensi} kali</span></td><td>${esc(p.jenis_bantuan)}</td><td>Nominal: ${esc(p.nominal??'-')}<br>Item: ${esc(p.jumlah_item||'-')}</td><td><span class="badge">${esc(p.status_program)}</span></td><td><button class="btn gray small" onclick="hapusProgramBantuan(${p.id})">Hapus</button></td></tr>`).join('');
}
function hapusProgramBantuan(id){
if(!confirm('Hapus program bantuan ini? Jika sudah ada penyaluran, status program akan dibatalkan.')) return;
const fd=new FormData(); fd.append('id',id);
fetch('hapus_program_bantuan.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{toast(res.message); if(res.status==='ok'){loadProgramBantuan().then(loadLaporanPenyaluran);}}).catch(e=>toast('Gagal hapus program: '+e.message));
}
function syncProgramDetail(){
const sel=document.getElementById('pn_program'); if(!sel) return;
const opt=sel.options[sel.selectedIndex];
document.getElementById('pn_jenis_view').value=opt?.dataset?.jenis||'';
document.getElementById('pn_nominal_view').value=opt?.dataset?.nominal||'-';
document.getElementById('pn_jumlah_item_view').value=opt?.dataset?.jumlah||'-';
// FIX: reload penerima sesuai RI dari program yang dipilih.
const riId=opt?.dataset?.riid||null;
loadPenerimaDiterima(riId);
}
function loadProgramBantuan(){
return fetch('ambil_program_bantuan.php').then(r=>r.json()).then(res=>{
if(res.status!=='ok') throw new Error(res.message);
const f=document.getElementById('f_program'); const pn=document.getElementById('pn_program');
f.innerHTML='<option value="">Semua</option>'; if(pn) pn.innerHTML='';
(res.data||[]).forEach(p=>{ const opt=programOption(p); f.insertAdjacentHTML('beforeend',opt); if(pn && p.status_program!=='dibatalkan') pn.insertAdjacentHTML('beforeend',opt); });
renderProgramTable(res.data||[]);
syncProgramDetail();
});
}
function loadRumahIbadahUntukProgram(){
const pr=document.getElementById('pr_rumah_ibadah_id');
const fr=document.getElementById('f_rumah_ibadah');
fetch('ambil_data_peta.php').then(r=>r.json()).then(res=>{
const rows=(res.data.rumah_ibadah||[]).filter(ri=>ri.status_pengajuan==='diterima');
rows.forEach(ri=>{
const opt=`<option value="${ri.id}">${esc(ri.nama_rumah_ibadah)} - ${esc(ri.agama)}</option>`;
if(pr) pr.insertAdjacentHTML('beforeend',opt);
if(fr) fr.insertAdjacentHTML('beforeend',opt);
});
});
}
function loadPenerimaDiterima(filterRiId=null){
const el=document.getElementById('pn_penerima'); if(!el) return;
fetch('ambil_data_peta.php').then(r=>r.json()).then(res=>{
el.innerHTML='';
let list = (res.data.penerima||[]).filter(p=>p.status_pengajuan==='diterima');
// FIX: filter penerima sesuai RI dari program yang dipilih agar tidak bisa pilih penerima RI lain.
if(filterRiId) list = list.filter(p=>String(p.rumah_ibadah_id)===String(filterRiId));
if(!list.length) el.insertAdjacentHTML('beforeend','<option value="">-- Tidak ada penerima di RI ini --</option>');
list.forEach(p=>el.insertAdjacentHTML('beforeend',`<option value="${p.id}">${esc(p.nama_penerima)} - ${esc(p.nama_rumah_ibadah||'')}</option>`));
});
}
function submitProgramBantuan(){
const namaEl = document.getElementById('pr_nama');
const bulanEl = document.getElementById('pr_bulan');
const tahunEl = document.getElementById('pr_tahun');
// FIX: validasi input wajib sebelum fetch.
if(!namaEl?.value.trim()){ toast('Nama program wajib diisi.','err'); return; }
if(!bulanEl?.value || bulanEl.value < 1 || bulanEl.value > 12){ toast('Bulan tidak valid (1-12).','err'); return; }
if(!tahunEl?.value || tahunEl.value < 2000){ toast('Tahun tidak valid.','err'); return; }
const fd=new FormData();
const ri=document.getElementById('pr_rumah_ibadah_id');
if(ri) fd.append('rumah_ibadah_id',ri.value);
fd.append('nama_program', namaEl.value);
fd.append('jenis_bantuan', document.getElementById('pr_jenis').value);
fd.append('bulan', bulanEl.value);
fd.append('tahun', tahunEl.value);
fd.append('target_frekuensi', document.getElementById('pr_target').value);
fd.append('nominal', document.getElementById('pr_nominal').value);
fd.append('jumlah_item', document.getElementById('pr_jumlah_item').value);
fd.append('deskripsi', document.getElementById('pr_deskripsi').value);
fetch('tambah_program_bantuan.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{
toast(res.message, res.status==='ok'?'ok':'err');
if(res.status==='ok'){ loadProgramBantuan().then(loadLaporanPenyaluran); }
}).catch(e=>toast('Gagal membuat program: '+e.message,'err'));
}
function submitPenyaluran(customProgramId=null, customPenerimaId=null){
const programId = customProgramId || document.getElementById('pn_program').value;
const penerimaId = customPenerimaId || document.getElementById('pn_penerima').value;
// FIX: validasi input wajib sebelum fetch agar tidak kirim request dengan data kosong.
if(!programId){ toast('Pilih program bantuan terlebih dahulu.','err'); return; }
if(!penerimaId){ toast('Pilih penerima terlebih dahulu.','err'); return; }
const fd=new FormData();
fd.append('program_id', programId);
fd.append('penerima_id', penerimaId);
fd.append('keterangan', customProgramId ? 'Dicatat cepat dari tabel laporan.' : document.getElementById('pn_keterangan').value);
fd.append('tanggal_penyaluran', document.getElementById('pn_tanggal')?.value || new Date().toISOString().slice(0,10));
fetch('tambah_penyaluran.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{
toast(res.message, res.status==='ok'?'ok':'err');
if(res.status==='ok'){ loadLaporanPenyaluran(); loadPenerimaDiterima(); }
}).catch(e=>toast('Gagal mencatat penyaluran: '+e.message,'err'));
}
function quickCatatPenyaluran(programId,penerimaId){
if(!confirm('Catat penyaluran bantuan untuk penerima ini sesuai jenis, nominal, dan jumlah item pada program?')) return;
submitPenyaluran(programId,penerimaId);
}
function loadLaporanPenyaluran(){
fetch('ambil_laporan_penyaluran.php?'+getFilterQuery()).then(r=>r.json()).then(res=>{ if(res.status!=='ok') throw new Error(res.message); renderRekapPenyaluran(res.data.rekap); renderTabelPenyaluran(res.data.rows); }).catch(e=>{document.getElementById('laporanBody').innerHTML=`<tr><td colspan="9">${esc(e.message)}</td></tr>`});
}
function renderRekapPenyaluran(r){
const labels={total:'Total Baris',belum_diberi:'Belum diberi',sudah_diberi:'Sudah diberi',belum_memenuhi_target:'Belum memenuhi target',sesuai_target:'Sesuai target',melebihi_target:'Melebihi target'};
document.getElementById('rekapBox').innerHTML=Object.keys(labels).map(k=>`<div class="card"><div class="k">${labels[k]}</div><div class="v">${r?.[k]??0}</div></div>`).join('');
}
function renderTabelPenyaluran(rows){
const body=document.getElementById('laporanBody');
if(!rows.length){body.innerHTML='<tr><td colspan="9">Tidak ada data sesuai filter.</td></tr>'; return;}
body.innerHTML=rows.map(r=>{
const target=Number(r.target_frekuensi||1), jumlah=Number(r.jumlah_penyaluran||0);
const bolehCatat = USER_ROLE!=='penerima' && jumlah < target && r.status_program !== 'selesai';
let aksiLabel = 'Sudah sesuai target';
if(r.status_program==='selesai') aksiLabel = 'Program selesai';
const aksi = bolehCatat ? `<button class="btn primary small" onclick="quickCatatPenyaluran(${r.program_id},${r.penerima_id})">Catat</button>` : `<button class="btn gray small" disabled>${aksiLabel}</button>`;
return `<tr><td><b>${esc(r.nama_penerima)}</b><br><span class="muted">${esc(r.no_hp||'')}</span></td><td>${esc(r.nama_rumah_ibadah)}<br><span class="muted">${esc(r.agama)}</span></td><td>${esc(r.nama_program)}<br><span class="muted">${r.bulan}/${r.tahun} - ${esc(r.jenis_bantuan)}</span><br><span class="muted">Nominal: ${esc(r.nominal??'-')} | Item: ${esc(r.jumlah_item||'-')}</span></td><td>${r.target_frekuensi}</td><td>${r.jumlah_penyaluran}</td><td><span class="badge ${r.status_penyaluran}">${esc(r.status_penyaluran)}</span></td><td>${esc(r.tanggal_terakhir||'-')}</td><td>${(r.riwayat||[]).map(x=>`<div>${esc(x)}</div>`).join('')||'-'}</td><td>${aksi}</td></tr>`;
}).join('');
}
function filterLaporanPenyaluran(){ loadLaporanPenyaluran(); }
function updateStatusPenerima(id,status){
const alasan=prompt('Masukkan catatan/alasan:')||'';
const fd=new FormData(); fd.append('id',id); fd.append('status_pengajuan',status); fd.append('alasan_status',alasan);
fetch('update_status_penerima.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{toast(res.message); if(res.status==='ok') location.reload();}).catch(e=>toast('Gagal update: '+e.message));
}
function updateStatusRumahIbadah(id,status){
const alasan=prompt('Masukkan catatan/alasan:')||'';
const fd=new FormData(); fd.append('id',id); fd.append('status_pengajuan',status); fd.append('keterangan',alasan);
fetch('update_status_rumah_ibadah.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{toast(res.message); if(res.status==='ok') location.reload();}).catch(e=>toast('Gagal update: '+e.message));
}
function loadRiwayat(){ fetch('ambil_riwayat.php').then(r=>r.json()).then(res=>{ const d=res.data; document.getElementById('riwayatBox').innerHTML=`<b>Pengajuan:</b> ${(d.pengajuan||[]).length} data<br><b>Perubahan status:</b> ${(d.status||[]).length} data<br><b>Penyaluran:</b> ${(d.penyaluran||[]).length} data`; }); }
['f_bulan','f_tahun','f_rumah_ibadah','f_agama','f_program','f_jenis','f_status'].forEach(id=>document.getElementById(id)?.addEventListener('change',filterLaporanPenyaluran));
loadUsers();
loadRumahIbadahUntukProgram();
loadProgramBantuan().then(()=>{loadLaporanPenyaluran();loadPenerimaDiterima();loadRiwayat();});
</script></body></html>
+70
View File
@@ -0,0 +1,70 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
$id = (int) post('id', 0);
$penerima = get_penerima_by_id($id);
if (!$penerima || !can_manage_penerima($penerima)) json_response('err', 'Data tidak ditemukan atau akses ditolak.', [], 403);
$allowedKondisi = ['baik','sedang','rusak'];
$allowedJenis = ['zakat','sembako','uang_tunai','lainnya'];
$nama = clean_text(post('nama_penerima'), 100);
$nik = clean_text(post('nik'), 30);
$no_hp = clean_text(post('no_hp'), 20);
$alamat = clean_text(post('alamat'), 1000);
$pekerjaan = clean_text(post('pekerjaan'), 100);
$penghasilan = (int) post('penghasilan', 0);
$jumlah_tanggungan = (int) post('jumlah_tanggungan', 0);
$kondisi_rumah = post('kondisi_rumah');
$jenis_bantuan = post('jenis_bantuan');
$lat = post('latitude', $penerima['latitude']);
$lng = post('longitude', $penerima['longitude']);
if ($nama==='' || $no_hp==='' || $alamat==='' || $pekerjaan==='' || !valid_latlng($lat, $lng)) json_response('err', 'Data wajib belum lengkap atau koordinat tidak valid.');
if (!valid_enum($kondisi_rumah, $allowedKondisi) || !valid_enum($jenis_bantuan, $allowedJenis)) json_response('err', 'Pilihan tidak valid.');
if ($penghasilan < 0 || $jumlah_tanggungan < 0) json_response('err', 'Penghasilan dan tanggungan tidak valid.');
$rumah_ibadah_id = null;
$jarak_meter = null;
// FIX: cek apakah koordinat berubah untuk keperluan riwayat
$latBefore = (float)$penerima['latitude'];
$lngBefore = (float)$penerima['longitude'];
$lokasiBerubar = (abs((float)$lat - $latBefore) > 0.000001 || abs((float)$lng - $lngBefore) > 0.000001);
if ($_SESSION['role'] === 'admin_rumah_ibadah') {
$globalNearest = get_nearest_rumah_ibadah((float)$lat, (float)$lng, 300);
$ownNearest = get_nearest_rumah_ibadah((float)$lat, (float)$lng, 300, (int)$_SESSION['user_id']);
if (!$ownNearest) json_response('err', 'Lokasi baru di luar radius 300 meter seluruh rumah ibadah Anda.');
if ($globalNearest && (int)$globalNearest['id'] !== (int)$ownNearest['id']) {
json_response('err', 'Lokasi baru lebih dekat ke rumah ibadah lain. Sistem hanya boleh mengaitkan penerima ke rumah ibadah terdekat.', [], 409);
}
$rumah_ibadah_id = (int)$ownNearest['id'];
$jarak_meter = (float)$ownNearest['jarak_meter'];
} else {
$nearest = get_nearest_rumah_ibadah((float)$lat, (float)$lng, 300);
if ($nearest) {
$rumah_ibadah_id = (int)$nearest['id'];
$jarak_meter = (float)$nearest['jarak_meter'];
}
}
// FIX: pisahkan cabang UPDATE agar rumah_ibadah_id/jarak_meter NULL tersimpan benar, bukan 0.
if ($rumah_ibadah_id !== null) {
$stmt = $koneksi->prepare("UPDATE penerima_bantuan SET rumah_ibadah_id=?, nama_penerima=?, nik=?, no_hp=?, alamat=?, pekerjaan=?, penghasilan=?, jumlah_tanggungan=?, kondisi_rumah=?, jenis_bantuan=?, latitude=?, longitude=?, jarak_meter=? WHERE id=?");
$stmt->bind_param('isssssiissdddi', $rumah_ibadah_id, $nama, $nik, $no_hp, $alamat, $pekerjaan, $penghasilan, $jumlah_tanggungan, $kondisi_rumah, $jenis_bantuan, $lat, $lng, $jarak_meter, $id);
} else {
$stmt = $koneksi->prepare("UPDATE penerima_bantuan SET rumah_ibadah_id=NULL, nama_penerima=?, nik=?, no_hp=?, alamat=?, pekerjaan=?, penghasilan=?, jumlah_tanggungan=?, kondisi_rumah=?, jenis_bantuan=?, latitude=?, longitude=?, jarak_meter=NULL WHERE id=?");
$stmt->bind_param('sssssiissddi', $nama, $nik, $no_hp, $alamat, $pekerjaan, $penghasilan, $jumlah_tanggungan, $kondisi_rumah, $jenis_bantuan, $lat, $lng, $id);
}
if (!$stmt->execute()) json_response('err', 'Gagal mengubah data: ' . $koneksi->error);
// FIX: catat riwayat saat lokasi berubah agar ada jejak audit.
if ($lokasiBerubar) {
$riNama = $rumah_ibadah_id ? ('RI ID ' . $rumah_ibadah_id) : 'belum terhubung RI';
insert_riwayat_status($id, $penerima['status_pengajuan'], $penerima['status_pengajuan'], "Lokasi penerima diperbarui. RI baru: $riNama, jarak: " . ($jarak_meter ?? 'N/A') . " m.");
}
json_response('ok', 'Data penerima berhasil diubah.', get_penerima_by_id($id));
?>
+36
View File
@@ -0,0 +1,36 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
require_role(['super_admin','admin_rumah_ibadah'], true);
$id = (int) post('id', 0);
$stmt = $koneksi->prepare("SELECT * FROM program_bantuan WHERE id=? LIMIT 1");
$stmt->bind_param('i', $id);
$program = fetch_all_stmt($stmt)[0] ?? null;
if (!$program) json_response('err', 'Program tidak ditemukan.');
if ($_SESSION['role'] === 'admin_rumah_ibadah') {
if (!is_admin_rumah_ibadah_id((int)$program['rumah_ibadah_id'], (int)$_SESSION['user_id'], true)) {
json_response('err', 'Akses ditolak. Program ini bukan milik rumah ibadah yang dikelola akun Anda.', [], 403);
}
}
$jenis = post('jenis_bantuan');
$status = post('status_program', 'aktif');
if (!valid_enum($jenis, ['zakat','sembako','uang_tunai','lainnya']) || !valid_enum($status, ['aktif','selesai','dibatalkan'])) json_response('err', 'Pilihan tidak valid.');
$nama = clean_text(post('nama_program'), 150);
$deskripsi = clean_text(post('deskripsi'), 1500);
$bulan = (int) post('bulan');
$tahun = (int) post('tahun');
$target = (int) post('target_frekuensi', 1);
$nominalInput = post('nominal', '');
$nominal = $nominalInput === '' ? null : (int) $nominalInput;
$jumlah_item = clean_text(post('jumlah_item', ''), 100);
if ($nama==='' || $bulan<1 || $bulan>12 || $tahun<2000 || $target<1) json_response('err', 'Data program tidak valid.');
if ($nominal !== null && $nominal < 0) json_response('err', 'Nominal tidak boleh minus.');
$stmt = $koneksi->prepare("UPDATE program_bantuan SET nama_program=?, jenis_bantuan=?, deskripsi=?, bulan=?, tahun=?, target_frekuensi=?, nominal=?, jumlah_item=?, status_program=? WHERE id=?");
$stmt->bind_param('sssiiiissi', $nama, $jenis, $deskripsi, $bulan, $tahun, $target, $nominal, $jumlah_item, $status, $id);
if (!$stmt->execute()) json_response('err', 'Gagal mengubah program: ' . $koneksi->error);
json_response('ok', 'Program bantuan berhasil diubah.', ['id' => $id]);
?>
+86
View File
@@ -0,0 +1,86 @@
<?php
/**
* edit_user.php
* Update akun pengguna.
* Password boleh dikosongkan jika tidak ingin diganti.
*/
require_once __DIR__ . '/auth.php';
require_login(true);
require_role(['super_admin', 'admin_rumah_ibadah'], true);
require_post();
$actor = current_user();
$actorRole = $actor['role'];
$id = (int) post('id');
$nama = clean_text(post('nama'), 100);
$username = clean_text(post('username'), 50);
$password = post('password');
$role = post('role');
$status_akun = post('status_akun', 'aktif');
$statusList = ['aktif', 'nonaktif'];
if ($id <= 0) json_response('err', 'ID akun tidak valid.', [], 422);
if ($nama === '' || $username === '') json_response('err', 'Nama dan username wajib diisi.', [], 422);
if (!preg_match('/^[a-zA-Z0-9_\.\-]{4,50}$/', $username)) {
json_response('err', 'Username minimal 4 karakter dan hanya boleh huruf, angka, titik, strip, atau underscore.', [], 422);
}
if ($password !== '' && strlen($password) < 6) json_response('err', 'Password minimal 6 karakter.', [], 422);
if (!valid_enum($status_akun, $statusList)) json_response('err', 'Status akun tidak valid.', [], 422);
$stmt = $koneksi->prepare("SELECT id, role FROM users WHERE id=? LIMIT 1");
$stmt->bind_param('i', $id);
$stmt->execute();
$target = $stmt->get_result()->fetch_assoc();
if (!$target) json_response('err', 'Akun tidak ditemukan.', [], 404);
if ($actorRole === 'super_admin') {
if (!in_array($role, ['admin_rumah_ibadah', 'penerima'], true)) {
json_response('err', 'Super admin hanya mengelola akun admin rumah ibadah dan penerima dari halaman ini.', [], 403);
}
if (!in_array($target['role'], ['admin_rumah_ibadah', 'penerima'], true)) {
json_response('err', 'Akun ini tidak boleh diubah dari halaman ini.', [], 403);
}
} else {
// Admin rumah ibadah hanya bisa mengubah akun penerima, dan role tetap penerima.
if ($target['role'] !== 'penerima') json_response('err', 'Admin rumah ibadah hanya boleh mengelola akun penerima.', [], 403);
$role = 'penerima';
$ids = get_admin_rumah_ibadah_ids((int)$actor['id'], true);
$types = 'i';
$params = [$id];
if (!empty($ids)) {
$notIn = build_in_condition('rumah_ibadah_id', $ids, $types, $params);
$stmt = $koneksi->prepare("SELECT COUNT(*) AS jml FROM penerima_bantuan WHERE user_id=? AND COALESCE(status_hapus,'normal')<>'disetujui' AND rumah_ibadah_id IS NOT NULL AND NOT ($notIn)");
} else {
$stmt = $koneksi->prepare("SELECT COUNT(*) AS jml FROM penerima_bantuan WHERE user_id=? AND COALESCE(status_hapus,'normal')<>'disetujui' AND rumah_ibadah_id IS NOT NULL");
}
stmt_bind($stmt, $types, $params);
$stmt->execute();
$row = $stmt->get_result()->fetch_assoc();
if ((int)$row['jml'] > 0) json_response('err', 'Akun penerima ini terhubung dengan rumah ibadah lain.', [], 403);
}
$stmt = $koneksi->prepare("SELECT id FROM users WHERE username=? AND id<>? LIMIT 1");
$stmt->bind_param('si', $username, $id);
$stmt->execute();
if ($stmt->get_result()->fetch_assoc()) json_response('err', 'Username sudah digunakan akun lain.', [], 409);
if ($password !== '') {
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $koneksi->prepare("UPDATE users SET nama=?, username=?, password=?, role=?, status_akun=? WHERE id=?");
$stmt->bind_param('sssssi', $nama, $username, $hash, $role, $status_akun, $id);
} else {
$stmt = $koneksi->prepare("UPDATE users SET nama=?, username=?, role=?, status_akun=? WHERE id=?");
$stmt->bind_param('ssssi', $nama, $username, $role, $status_akun, $id);
}
if (!$stmt->execute()) json_response('err', 'Gagal mengubah akun: ' . $stmt->error, [], 500);
json_response('ok', 'Akun berhasil diubah.', [
'id' => $id,
'nama' => $nama,
'username' => $username,
'role' => $role,
'status_akun' => $status_akun
]);
?>
+35
View File
@@ -0,0 +1,35 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
$id = (int) post('id', 0);
$alasan = clean_text(post('alasan_hapus', post('alasan', '')), 1000);
$penerima = get_penerima_by_id($id);
if (!$penerima) json_response('err', 'Data penerima tidak ditemukan.', [], 404);
// Super admin boleh langsung menghapus marker/pin penerima dari peta.
// Data tidak dihapus permanen agar riwayat penyaluran tetap aman untuk audit.
if ($_SESSION['role'] === 'super_admin') {
$statusBaru = 'dibatalkan';
$hapus = 'disetujui';
$statusLama = $penerima['status_pengajuan'];
$catatan = $alasan ?: 'Marker penerima dihapus oleh super admin.';
$stmt = $koneksi->prepare("UPDATE penerima_bantuan SET status_pengajuan=?, status_hapus=?, alasan_hapus=?, alasan_status=? WHERE id=?");
$stmt->bind_param('ssssi', $statusBaru, $hapus, $catatan, $catatan, $id);
if (!$stmt->execute()) json_response('err', 'Gagal menghapus marker penerima: ' . $koneksi->error);
insert_riwayat_status($id, $statusLama, 'hapus_disetujui', $catatan);
json_response('ok', 'Marker penerima berhasil dihapus oleh super admin.', get_penerima_by_id($id));
}
// Admin rumah ibadah dan penerima hanya mengajukan penghapusan, menunggu persetujuan super admin.
if (!can_manage_penerima($penerima)) json_response('err', 'Akses ditolak.', [], 403);
if ($alasan === '') json_response('err', 'Alasan penghapusan wajib diisi.');
$statusHapus = 'menunggu';
$stmt = $koneksi->prepare("UPDATE penerima_bantuan SET status_hapus=?, alasan_hapus=? WHERE id=?");
$stmt->bind_param('ssi', $statusHapus, $alasan, $id);
if (!$stmt->execute()) json_response('err', 'Gagal mengajukan penghapusan marker penerima: ' . $koneksi->error);
insert_riwayat_status($id, $penerima['status_pengajuan'], 'hapus_menunggu', $alasan);
json_response('ok', 'Pengajuan hapus marker penerima dikirim. Menunggu persetujuan super admin.', get_penerima_by_id($id));
?>
+32
View File
@@ -0,0 +1,32 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
require_role(['super_admin','admin_rumah_ibadah'], true);
$id = (int) post('id', 0);
$stmt = $koneksi->prepare("SELECT * FROM program_bantuan WHERE id=? LIMIT 1");
$stmt->bind_param('i', $id);
$program = fetch_all_stmt($stmt)[0] ?? null;
if (!$program) json_response('err', 'Program tidak ditemukan.');
if ($_SESSION['role'] === 'admin_rumah_ibadah') {
if (!is_admin_rumah_ibadah_id((int)$program['rumah_ibadah_id'], (int)$_SESSION['user_id'], true)) {
json_response('err', 'Akses ditolak. Program ini bukan milik rumah ibadah yang dikelola akun Anda.', [], 403);
}
}
$stmt = $koneksi->prepare("SELECT COUNT(*) AS jml FROM penyaluran_bantuan WHERE program_id=?");
$stmt->bind_param('i', $id);
$row = fetch_all_stmt($stmt)[0];
if ((int)$row['jml'] > 0) {
$status = 'dibatalkan';
$stmt = $koneksi->prepare("UPDATE program_bantuan SET status_program=? WHERE id=?");
$stmt->bind_param('si', $status, $id);
$stmt->execute();
json_response('ok', 'Program sudah memiliki penyaluran, maka status diubah menjadi dibatalkan.', ['id'=>$id]);
}
$stmt = $koneksi->prepare("DELETE FROM program_bantuan WHERE id=?");
$stmt->bind_param('i', $id);
if (!$stmt->execute()) json_response('err', 'Gagal menghapus program: ' . $koneksi->error);
json_response('ok', 'Program bantuan berhasil dihapus.', ['id'=>$id]);
?>
+36
View File
@@ -0,0 +1,36 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
require_role(['super_admin','admin_rumah_ibadah'], true);
$id = (int) post('id', 0);
$alasan = clean_text(post('alasan_hapus', post('alasan', '')), 1000);
$rumah = get_rumah_ibadah_by_id($id);
if (!$rumah) json_response('err', 'Rumah ibadah tidak ditemukan.', [], 404);
if ($_SESSION['role'] === 'super_admin') {
// Super admin langsung menghapus marker rumah ibadah dari peta.
// Data dibuat tidak aktif, bukan dihapus permanen, supaya riwayat tetap aman.
$status = 'ditolak';
$hapus = 'disetujui';
$catatan = $alasan ?: 'Marker rumah ibadah dihapus oleh super admin.';
$stmt = $koneksi->prepare("UPDATE rumah_ibadah SET status_pengajuan=?, status_hapus=?, alasan_hapus=?, keterangan=? WHERE id=?");
$stmt->bind_param('ssssi', $status, $hapus, $catatan, $catatan, $id);
if (!$stmt->execute()) json_response('err', 'Gagal menghapus marker rumah ibadah: ' . $koneksi->error);
$jumlahLepas = detach_penerima_from_rumah_ibadah($id);
$row = get_rumah_ibadah_by_id($id);
$row['jumlah_penerima_dilepas'] = $jumlahLepas;
json_response('ok', 'Marker rumah ibadah berhasil dihapus oleh super admin.', $row);
}
// Admin rumah ibadah hanya boleh mengajukan hapus untuk rumah ibadah miliknya.
if ((int)$rumah['user_id'] !== (int)$_SESSION['user_id']) json_response('err', 'Akses ditolak. Admin hanya boleh mengajukan hapus rumah ibadah miliknya.', [], 403);
if ($alasan === '') json_response('err', 'Alasan penghapusan wajib diisi.');
$statusHapus = 'menunggu';
$stmt = $koneksi->prepare("UPDATE rumah_ibadah SET status_hapus=?, alasan_hapus=? WHERE id=?");
$stmt->bind_param('ssi', $statusHapus, $alasan, $id);
if (!$stmt->execute()) json_response('err', 'Gagal mengajukan penghapusan rumah ibadah: ' . $koneksi->error);
json_response('ok', 'Pengajuan hapus marker rumah ibadah dikirim. Menunggu persetujuan super admin.', get_rumah_ibadah_by_id($id));
?>
+71
View File
@@ -0,0 +1,71 @@
<?php
/**
* hapus_user.php
* Menghapus akun jika belum punya relasi data. Jika sudah punya relasi, akun dinonaktifkan agar data audit tidak rusak.
*/
require_once __DIR__ . '/auth.php';
require_login(true);
require_role(['super_admin', 'admin_rumah_ibadah'], true);
require_post();
$actor = current_user();
$actorRole = $actor['role'];
$id = (int) post('id');
if ($id <= 0) json_response('err', 'ID akun tidak valid.', [], 422);
if ($id === (int)$actor['id']) json_response('err', 'Akun yang sedang dipakai tidak boleh dihapus dari sini.', [], 403);
$stmt = $koneksi->prepare("SELECT id, role FROM users WHERE id=? LIMIT 1");
$stmt->bind_param('i', $id);
$stmt->execute();
$target = $stmt->get_result()->fetch_assoc();
if (!$target) json_response('err', 'Akun tidak ditemukan.', [], 404);
if ($actorRole === 'super_admin') {
if (!in_array($target['role'], ['admin_rumah_ibadah', 'penerima'], true)) {
json_response('err', 'Super admin hanya menghapus/menonaktifkan akun admin rumah ibadah dan penerima dari halaman ini.', [], 403);
}
} else {
if ($target['role'] !== 'penerima') json_response('err', 'Admin rumah ibadah hanya boleh menghapus/menonaktifkan akun penerima.', [], 403);
$ids = get_admin_rumah_ibadah_ids((int)$actor['id'], true);
$types = 'i';
$params = [$id];
if (!empty($ids)) {
$notIn = build_in_condition('rumah_ibadah_id', $ids, $types, $params);
$stmt = $koneksi->prepare("SELECT COUNT(*) AS jml FROM penerima_bantuan WHERE user_id=? AND COALESCE(status_hapus,'normal')<>'disetujui' AND rumah_ibadah_id IS NOT NULL AND NOT ($notIn)");
} else {
$stmt = $koneksi->prepare("SELECT COUNT(*) AS jml FROM penerima_bantuan WHERE user_id=? AND COALESCE(status_hapus,'normal')<>'disetujui' AND rumah_ibadah_id IS NOT NULL");
}
stmt_bind($stmt, $types, $params);
$stmt->execute();
$row = $stmt->get_result()->fetch_assoc();
if ((int)$row['jml'] > 0) json_response('err', 'Akun penerima ini terhubung dengan rumah ibadah lain.', [], 403);
}
// Cek relasi agar tidak merusak foreign key dan riwayat audit.
$related = 0;
$checks = [
["SELECT COUNT(*) AS jml FROM rumah_ibadah WHERE user_id=?", 'i'],
["SELECT COUNT(*) AS jml FROM penerima_bantuan WHERE user_id=?", 'i'],
["SELECT COUNT(*) AS jml FROM penyaluran_bantuan WHERE admin_id=?", 'i'],
["SELECT COUNT(*) AS jml FROM riwayat_status WHERE user_id=?", 'i']
];
foreach ($checks as $c) {
$stmt = $koneksi->prepare($c[0]);
$stmt->bind_param('i', $id);
$stmt->execute();
$row = $stmt->get_result()->fetch_assoc();
$related += (int)$row['jml'];
}
if ($related > 0) {
$stmt = $koneksi->prepare("UPDATE users SET status_akun='nonaktif' WHERE id=?");
$stmt->bind_param('i', $id);
if (!$stmt->execute()) json_response('err', 'Gagal menonaktifkan akun: ' . $stmt->error, [], 500);
json_response('ok', 'Akun memiliki relasi data, jadi tidak dihapus permanen. Akun sudah dinonaktifkan.', ['mode' => 'nonaktif']);
}
$stmt = $koneksi->prepare("DELETE FROM users WHERE id=?");
$stmt->bind_param('i', $id);
if (!$stmt->execute()) json_response('err', 'Gagal menghapus akun: ' . $stmt->error, [], 500);
json_response('ok', 'Akun berhasil dihapus permanen.', ['mode' => 'delete']);
?>
+286
View File
@@ -0,0 +1,286 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(false);
$user = current_user();
$role = $user['role'];
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS Poverty Mapping</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
*{margin:0;padding:0;box-sizing:border-box} html,body{height:100%;font-family:Arial,sans-serif;background:#f8fafc;color:#0f172a}
#header{height:50px;background:#0f766e;color:#fff;display:flex;align-items:center;justify-content:space-between;padding:0 18px;z-index:1000;position:relative;box-shadow:0 2px 12px rgba(15,23,42,.18)}
#header h1{font-size:16px}.nav{display:flex;gap:9px;align-items:center;font-size:13px}.nav a{color:#fff;text-decoration:none;background:rgba(255,255,255,.16);padding:7px 10px;border-radius:8px}.role{font-weight:700;text-transform:uppercase;font-size:11px;letter-spacing:.05em;background:#064e3b;padding:6px 8px;border-radius:999px}
#map{width:100%;height:calc(100vh - 50px)}
#toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%) translateY(60px);background:#1f2937;color:#fff;padding:10px 18px;border-radius:999px;font-size:13px;z-index:9999;opacity:0;transition:.25s;box-shadow:0 10px 24px rgba(0,0,0,.22)}#toast.show{opacity:1;transform:translateX(-50%) translateY(0)}#toast.ok{background:#16a34a}#toast.err{background:#dc2626}
#loading{display:none;position:fixed;inset:0;background:rgba(255,255,255,.55);z-index:9000;align-items:center;justify-content:center}.show-loading{display:flex!important}.spin{width:42px;height:42px;border-radius:50%;border:5px solid #cbd5e1;border-top-color:#0f766e;animation:sp .7s linear infinite}@keyframes sp{to{transform:rotate(360deg)}}
.leaflet-popup-content-wrapper{border-radius:12px!important;overflow:hidden;padding:0!important}.leaflet-popup-content{margin:0!important;min-width:280px}.ph{background:#0f766e;color:white;padding:10px 14px;font-weight:700}.pb{background:#fff;padding:12px 14px}.muted{color:#64748b;font-size:12px}.dt{width:100%;border-collapse:collapse;font-size:12px}.dt td{padding:4px;border-bottom:1px solid #eef2f7;vertical-align:top}.dt td:first-child{color:#64748b;width:115px}.fg{margin:8px 0}.fg label{font-size:11px;text-transform:uppercase;font-weight:700;color:#64748b;display:block;margin-bottom:4px}.fg input,.fg select,.fg textarea{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:8px;font-size:13px}.fg textarea{min-height:58px;resize:vertical}.br{display:flex;gap:7px;margin-top:9px;flex-wrap:wrap}.btn{border:0;border-radius:8px;padding:8px 10px;font-size:12px;font-weight:700;cursor:pointer}.bp{background:#0f766e;color:#fff}.bd{background:#dc2626;color:#fff}.bo{background:#f97316;color:#fff}.bs{background:#e2e8f0;color:#0f172a}.small{font-size:11px}.coord{background:#ecfeff;color:#155e75;border:1px solid #a5f3fc;border-radius:8px;padding:7px;font-size:12px;margin-bottom:8px}.legend{background:white;border-radius:10px;padding:10px 12px;box-shadow:0 6px 18px rgba(15,23,42,.18);font-size:12px}.legend b{display:block;margin-bottom:6px}.li{display:flex;align-items:center;gap:6px;margin:4px 0}.dot{width:12px;height:12px;border-radius:50%;border:1px solid rgba(0,0,0,.25)}.svg-marker{line-height:0}.actions{margin-top:8px}
</style>
</head>
<body>
<div id="header">
<h1>🗺️ WebGIS Poverty Mapping</h1>
<div class="nav"><span class="role"><?= htmlspecialchars($role) ?></span><span><?= htmlspecialchars($user['nama']) ?></span><a href="dashboard.php">Dashboard</a><a href="logout.php">Logout</a></div>
</div>
<div id="map"></div><div id="toast"></div><div id="loading"><div class="spin"></div></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const USER_ROLE = <?= json_encode($role) ?>;
const USER_ID = <?= json_encode((int)$user['id']) ?>;
const USER_NAME = <?= json_encode($user['nama']) ?>;
const markersMap = {};
let map, tempPopup = null;
// Cek Leaflet sebelum inisialisasi peta.
if (typeof L === 'undefined') {
document.getElementById('map').innerHTML = '<div style="padding:20px;color:red">Leaflet gagal dimuat. Periksa internet/CDN.</div>';
throw new Error('Leaflet tidak tersedia');
}
// Inisialisasi peta.
map = L.map('map').setView([-0.05566422376778593, 109.34854622732368], 17);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {maxZoom: 20, attribution:'&copy; OpenStreetMap'}).addTo(map);
// Pembuatan Layer Groups.
const layerIslam = L.layerGroup();
const layerKristen = L.layerGroup();
const layerKatolik = L.layerGroup();
const layerHindu = L.layerGroup();
const layerBuddha = L.layerGroup();
const layerKonghucu = L.layerGroup();
const layerPenerimaDiterima = L.layerGroup();
const layerPenerimaMenunggu = L.layerGroup();
const layerPenerimaDibatalkan = L.layerGroup();
const layerRadiusBantuan = L.layerGroup();
// Layers Control.
const overlayMaps = {
"Rumah Ibadah Islam": layerIslam,
"Rumah Ibadah Kristen": layerKristen,
"Rumah Ibadah Katolik": layerKatolik,
"Rumah Ibadah Hindu": layerHindu,
"Rumah Ibadah Buddha": layerBuddha,
"Rumah Ibadah Konghucu": layerKonghucu,
"Penerima Diterima": layerPenerimaDiterima,
"Pengajuan Menunggu": layerPenerimaMenunggu,
"Penerima Dibatalkan": layerPenerimaDibatalkan,
"Radius Bantuan 300 Meter": layerRadiusBantuan
};
layerIslam.addTo(map); layerKristen.addTo(map); layerKatolik.addTo(map); layerHindu.addTo(map); layerBuddha.addTo(map); layerKonghucu.addTo(map);
layerPenerimaDiterima.addTo(map); layerRadiusBantuan.addTo(map);
if (USER_ROLE !== 'penerima') layerPenerimaMenunggu.addTo(map);
if (USER_ROLE === 'super_admin') layerPenerimaDibatalkan.addTo(map);
L.control.layers(null, overlayMaps).addTo(map);
// Penentuan warna marker.
function getRumahIbadahColor(data){
const c={islam:'#22c55e',kristen:'#3b82f6',katolik:'#8b5cf6',hindu:'#f97316',buddha:'#eab308',konghucu:'#ef4444'};
return c[data.agama] || '#0f766e';
}
function getPenerimaColor(data){
const c={menunggu:'#6b7280',diterima:'#111827',ditolak:'#dc2626',dibatalkan:'#92400e'};
return c[data.status_pengajuan] || '#111827';
}
// Pembuatan SVG marker via L.divIcon.
function svgIcon(color, label){
return L.divIcon({className:'svg-marker', iconSize:[30,42], iconAnchor:[15,42], popupAnchor:[0,-38], html:`
<svg width="30" height="42" viewBox="0 0 30 42" xmlns="http://www.w3.org/2000/svg">
<path d="M15 0C6.7 0 0 6.7 0 15c0 11.2 15 27 15 27s15-15.8 15-27C30 6.7 23.3 0 15 0z" fill="${color}" stroke="white" stroke-width="2"/>
<circle cx="15" cy="15" r="8" fill="white" opacity=".94"/><text x="15" y="19" text-anchor="middle" font-size="10" font-weight="700" fill="${color}">${label}</text>
</svg>`});
}
function createRumahIbadahIcon(data){ return svgIcon(getRumahIbadahColor(data), 'RI'); }
function createPenerimaIcon(data){ return svgIcon(getPenerimaColor(data), 'P'); }
function getRumahIbadahLayer(data){
return ({islam:layerIslam,kristen:layerKristen,katolik:layerKatolik,hindu:layerHindu,buddha:layerBuddha,konghucu:layerKonghucu})[data.agama] || layerIslam;
}
function getPenerimaLayer(data){
if(data.status_pengajuan==='menunggu') return layerPenerimaMenunggu;
if(data.status_pengajuan==='dibatalkan') return layerPenerimaDibatalkan;
return layerPenerimaDiterima;
}
function showToast(message,type='ok'){
const t=document.getElementById('toast'); t.textContent=message; t.className='show '+type; setTimeout(()=>t.className='',3200);
}
function showLoading(){document.getElementById('loading').classList.add('show-loading')}
function hideLoading(){document.getElementById('loading').classList.remove('show-loading')}
function esc(v){return String(v??'').replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[m]))}
function rupiah(n){ if(!n) return '-'; return 'Rp '+Number(n).toLocaleString('id-ID') }
// Perhitungan jarak frontend untuk tampilan. Validasi utama tetap di backend.
function hitungJarakMeter(lat1, lon1, lat2, lon2){
const R=6371000,dLat=(lat2-lat1)*Math.PI/180,dLon=(lon2-lon1)*Math.PI/180;
const a=Math.sin(dLat/2)**2+Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)*Math.sin(dLon/2)**2;
return R*2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a));
}
function alamatDefault(lat,lng){ return `Koordinat: ${Number(lat).toFixed(6)}, ${Number(lng).toFixed(6)}`; }
function isiAlamatOtomatis(lat,lng,fieldId){
const el=document.getElementById(fieldId); if(!el) return;
const awal=alamatDefault(lat,lng); el.value=awal;
// Reverse geocoding dari OpenStreetMap. Jika gagal, alamat koordinat tetap dipakai dan bisa diedit manual.
fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${encodeURIComponent(lat)}&lon=${encodeURIComponent(lng)}&accept-language=id`)
.then(r=>r.ok?r.json():null).then(d=>{
const terbaru=document.getElementById(fieldId);
if(terbaru && d && d.display_name && (terbaru.value==='' || terbaru.value===awal)) terbaru.value=d.display_name;
}).catch(()=>{});
}
function addRumahIbadahMarker(data){
if(data.status_pengajuan==='ditolak' || data.status_hapus==='disetujui') return;
const layer=getRumahIbadahLayer(data);
const marker=L.marker([+data.latitude,+data.longitude],{icon:createRumahIbadahIcon(data)});
let actions = '';
const statusHapus = data.status_hapus || 'normal';
if(USER_ROLE==='super_admin' && data.status_pengajuan==='menunggu'){
actions += `<div class="br actions"><button class="btn bp" onclick="updateStatusRumahIbadah(${data.id},'diterima')">Setujui</button><button class="btn bd" onclick="updateStatusRumahIbadah(${data.id},'ditolak')">Tolak</button></div>`;
}
if(USER_ROLE==='super_admin' && statusHapus==='menunggu'){
actions += `<div class="br actions"><button class="btn bp" onclick="updateStatusRumahIbadah(${data.id},'hapus_disetujui')">Setujui Hapus</button><button class="btn bd" onclick="updateStatusRumahIbadah(${data.id},'hapus_ditolak')">Tolak Hapus</button></div>`;
}
if(USER_ROLE==='super_admin' && statusHapus!=='menunggu'){
actions += `<div class="br actions"><button class="btn bd" onclick="hapusRumahIbadah(${data.id})">Hapus Marker</button></div>`;
}
if(USER_ROLE==='admin_rumah_ibadah' && Number(data.user_id)===Number(USER_ID) && statusHapus!=='menunggu'){
actions += `<div class="br actions"><button class="btn bo" onclick="hapusRumahIbadah(${data.id})">Ajukan Hapus Marker</button></div>`;
}
const hapusInfo = statusHapus==='menunggu' ? `<div class="muted" style="margin-top:8px;color:#92400e"><b>Pengajuan hapus sedang menunggu persetujuan super admin.</b><br>${esc(data.alasan_hapus||'')}</div>` : '';
marker.bindPopup(`<div class="ph">${esc(data.nama_rumah_ibadah)}</div><div class="pb"><table class="dt"><tr><td>Agama</td><td>${esc(data.agama)}</td></tr><tr><td>Status</td><td>${esc(data.status_pengajuan)}</td></tr><tr><td>Status hapus</td><td>${esc(statusHapus)}</td></tr><tr><td>Alamat</td><td>${esc(data.alamat)}</td></tr><tr><td>No HP</td><td>${esc(data.nomor_hp)}</td></tr><tr><td>Koordinat</td><td>${(+data.latitude).toFixed(6)}, ${(+data.longitude).toFixed(6)}</td></tr></table><div class="muted">Radius bantuan: 300 meter.</div>${hapusInfo}${actions}</div>`);
marker.addTo(layer);
let circle=null;
// Pembuatan radius 300 meter untuk rumah ibadah yang diterima.
if(data.status_pengajuan==='diterima'){
circle=L.circle([+data.latitude,+data.longitude],{radius:300,color:getRumahIbadahColor(data),weight:1,fillOpacity:.08}).addTo(layerRadiusBantuan);
}
markersMap['rumah_'+data.id]={marker,layer,circle};
}
function addPenerimaMarker(data){
if(data.status_pengajuan==='ditolak') return;
if(data.status_hapus==='disetujui') return;
if(data.status_pengajuan==='menunggu' && USER_ROLE==='penerima') return;
if(data.status_pengajuan==='dibatalkan' && USER_ROLE!=='super_admin') return;
const layer=getPenerimaLayer(data);
const marker=L.marker([+data.latitude,+data.longitude],{icon:createPenerimaIcon(data)});
let actions='';
const statusHapus = data.status_hapus || 'normal';
if((USER_ROLE==='super_admin'||USER_ROLE==='admin_rumah_ibadah') && data.status_pengajuan==='menunggu'){
actions+=`<div class="br actions"><button class="btn bp" onclick="updateStatusPenerima(${data.id},'diterima')">Setujui</button><button class="btn bd" onclick="updateStatusPenerima(${data.id},'ditolak')">Tolak</button></div>`;
}
if(USER_ROLE==='super_admin' && statusHapus==='menunggu'){
actions+=`<div class="br actions"><button class="btn bp" onclick="updateStatusPenerima(${data.id},'hapus_disetujui')">Setujui Hapus</button><button class="btn bd" onclick="updateStatusPenerima(${data.id},'hapus_ditolak')">Tolak Hapus</button></div>`;
}
if(USER_ROLE==='super_admin' && statusHapus!=='menunggu'){
actions+=`<div class="br actions"><button class="btn bd" onclick="hapusPenerima(${data.id})">Hapus Marker</button></div>`;
}
if((USER_ROLE==='admin_rumah_ibadah'||USER_ROLE==='penerima') && statusHapus!=='menunggu'){
actions+=`<div class="br actions"><button class="btn bo" onclick="hapusPenerima(${data.id})">Ajukan Hapus Pin</button></div>`;
}
const hapusInfo = statusHapus==='menunggu' ? `<div class="muted" style="margin-top:8px;color:#92400e"><b>Pengajuan hapus sedang menunggu persetujuan super admin.</b><br>${esc(data.alasan_hapus||'')}</div>` : '';
marker.bindPopup(`<div class="ph">${esc(data.nama_penerima)}</div><div class="pb"><table class="dt"><tr><td>Status</td><td>${esc(data.status_pengajuan)}</td></tr><tr><td>Status hapus</td><td>${esc(statusHapus)}</td></tr><tr><td>Bantuan</td><td>${esc(data.jenis_bantuan)}</td></tr><tr><td>Pekerjaan</td><td>${esc(data.pekerjaan)}</td></tr><tr><td>Penghasilan</td><td>${rupiah(data.penghasilan)}</td></tr><tr><td>Tanggungan</td><td>${esc(data.jumlah_tanggungan)}</td></tr><tr><td>Rumah ibadah</td><td>${esc(data.nama_rumah_ibadah || '-')}</td></tr><tr><td>Jarak</td><td>${data.jarak_meter?Number(data.jarak_meter).toFixed(1)+' meter':'-'}</td></tr></table>${hapusInfo}${actions}</div>`);
marker.addTo(layer);
markersMap['penerima_'+data.id]={marker,layer,circle:null};
}
function removeMarkerFromMap(id,type){
const key=type+'_'+id, obj=markersMap[key]; if(!obj) return;
if(obj.marker && obj.layer) obj.layer.removeLayer(obj.marker);
if(obj.circle) layerRadiusBantuan.removeLayer(obj.circle);
delete markersMap[key];
}
function refreshMarkerAfterUpdate(data,type){ removeMarkerFromMap(data.id,type); if(type==='penerima') addPenerimaMarker(data); else addRumahIbadahMarker(data); }
function clearAll(){Object.keys(markersMap).forEach(k=>{const [type,id]=k.split('_'); removeMarkerFromMap(id,type)}); layerRadiusBantuan.clearLayers();}
function loadDataPeta(){
showLoading();
fetch('ambil_data_peta.php')
.then(r=>r.json()).then(res=>{
if(res.status!=='ok') throw new Error(res.message);
clearAll();
(res.data.rumah_ibadah||[]).forEach(addRumahIbadahMarker);
(res.data.penerima||[]).forEach(addPenerimaMarker);
}).catch(e=>showToast(e.message,'err')).finally(hideLoading);
}
function formPenerima(lat,lng){ return `<div class="ph">Tambah/Pengajuan Penerima</div><div class="pb"><div class="coord">Lat ${lat.toFixed(6)}<br>Lng ${lng.toFixed(6)}</div>
<input type="hidden" id="p_lat" value="${lat}"><input type="hidden" id="p_lng" value="${lng}">
${USER_ROLE!=='penerima'?'<div class="fg"><label>Username Akun Penerima</label><input id="p_username" placeholder="Username penerima yang terkait"></div>':''}
<div class="fg"><label>Nama Penerima</label><input id="p_nama" value="${USER_ROLE==='penerima'?esc(USER_NAME):''}"></div>
<div class="fg"><label>NIK</label><input id="p_nik"></div><div class="fg"><label>No HP</label><input id="p_hp"></div>
<div class="fg"><label>Alamat</label><textarea id="p_alamat">${esc(alamatDefault(lat,lng))}</textarea></div><div class="fg"><label>Pekerjaan</label><input id="p_pekerjaan"></div>
<div class="fg"><label>Penghasilan</label><input type="number" id="p_penghasilan" value="0"></div><div class="fg"><label>Jumlah Tanggungan</label><input type="number" id="p_tanggungan" value="0"></div>
<div class="fg"><label>Kondisi Rumah</label><select id="p_kondisi"><option value="baik">Baik</option><option value="sedang">Sedang</option><option value="rusak">Rusak</option></select></div>
<div class="fg"><label>Jenis Bantuan</label><select id="p_jenis"><option value="zakat">Zakat</option><option value="sembako">Sembako</option><option value="uang_tunai">Uang Tunai</option><option value="lainnya">Lainnya</option></select></div>
<div class="br"><button class="btn bp" onclick="submitPenerima()">Simpan Pengajuan</button></div></div>`; }
function formRumahIbadah(lat,lng){ return `<div class="ph">Ajukan Rumah Ibadah</div><div class="pb"><div class="coord">Lat ${lat.toFixed(6)}<br>Lng ${lng.toFixed(6)}</div>
<input type="hidden" id="ri_lat" value="${lat}"><input type="hidden" id="ri_lng" value="${lng}">
${USER_ROLE==='super_admin'?'<div class="fg"><label>Username Admin Rumah Ibadah</label><input id="ri_username_admin" placeholder="Username admin pengelola"></div>':''}
<div class="fg"><label>Nama Rumah Ibadah</label><input id="ri_nama"></div><div class="fg"><label>Agama</label><select id="ri_agama"><option value="islam">Islam</option><option value="kristen">Kristen</option><option value="katolik">Katolik</option><option value="hindu">Hindu</option><option value="buddha">Buddha</option><option value="konghucu">Konghucu</option></select></div>
<div class="fg"><label>Alamat</label><textarea id="ri_alamat">${esc(alamatDefault(lat,lng))}</textarea></div><div class="fg"><label>Nomor HP</label><input id="ri_hp"></div>
<div class="br"><button class="btn bp" onclick="submitRumahIbadah()">Ajukan</button></div></div>`; }
function showPopupTambahPenerima(latlng){ tempPopup=L.popup().setLatLng(latlng).setContent(formPenerima(latlng.lat,latlng.lng)).openOn(map); setTimeout(()=>isiAlamatOtomatis(latlng.lat,latlng.lng,'p_alamat'),80); }
function showPopupAjukanRumahIbadah(latlng){ tempPopup=L.popup().setLatLng(latlng).setContent(formRumahIbadah(latlng.lat,latlng.lng)).openOn(map); setTimeout(()=>isiAlamatOtomatis(latlng.lat,latlng.lng,'ri_alamat'),80); }
map.on('click', e=>{
if(USER_ROLE==='penerima') return showPopupTambahPenerima(e.latlng);
if(USER_ROLE==='admin_rumah_ibadah') return L.popup().setLatLng(e.latlng).setContent(`<div class="ph">Pilih Aksi</div><div class="pb"><div class="br"><button class="btn bp" onclick="showPopupTambahPenerima({lat:${e.latlng.lat},lng:${e.latlng.lng}})">Tambah Penerima</button><button class="btn bs" onclick="showPopupAjukanRumahIbadah({lat:${e.latlng.lat},lng:${e.latlng.lng}})">Ajukan Rumah Ibadah</button></div></div>`).openOn(map);
if(USER_ROLE==='super_admin') return L.popup().setLatLng(e.latlng).setContent(`<div class="ph">Pilih Aksi</div><div class="pb"><div class="br"><button class="btn bp" onclick="showPopupTambahPenerima({lat:${e.latlng.lat},lng:${e.latlng.lng}})">Tambah Penerima</button><button class="btn bs" onclick="showPopupAjukanRumahIbadah({lat:${e.latlng.lat},lng:${e.latlng.lng}})">Tambah Rumah Ibadah</button></div></div>`).openOn(map);
});
function submitPenerima(){
const fd=new FormData();
const userEl=document.getElementById('p_username'); if(userEl) fd.append('username_penerima', userEl.value);
fd.append('nama_penerima',document.getElementById('p_nama').value); fd.append('nik',document.getElementById('p_nik').value); fd.append('no_hp',document.getElementById('p_hp').value); fd.append('alamat',document.getElementById('p_alamat').value); fd.append('pekerjaan',document.getElementById('p_pekerjaan').value); fd.append('penghasilan',document.getElementById('p_penghasilan').value); fd.append('jumlah_tanggungan',document.getElementById('p_tanggungan').value); fd.append('kondisi_rumah',document.getElementById('p_kondisi').value); fd.append('jenis_bantuan',document.getElementById('p_jenis').value); fd.append('latitude',document.getElementById('p_lat').value); fd.append('longitude',document.getElementById('p_lng').value);
showLoading(); fetch('tambah_penerima.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{showToast(res.message,res.status==='ok'?'ok':'err'); if(res.status==='ok'){map.closePopup(); loadDataPeta();}}).catch(e=>showToast(e.message,'err')).finally(hideLoading);
}
function submitRumahIbadah(){
const fd=new FormData(); const adminEl=document.getElementById('ri_username_admin'); if(adminEl) fd.append('username_admin', adminEl.value); fd.append('nama_rumah_ibadah',document.getElementById('ri_nama').value); fd.append('agama',document.getElementById('ri_agama').value); fd.append('alamat',document.getElementById('ri_alamat').value); fd.append('nomor_hp',document.getElementById('ri_hp').value); fd.append('latitude',document.getElementById('ri_lat').value); fd.append('longitude',document.getElementById('ri_lng').value);
showLoading(); fetch('ajukan_rumah_ibadah.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{showToast(res.message,res.status==='ok'?'ok':'err'); if(res.status==='ok'){map.closePopup(); loadDataPeta();}}).catch(e=>showToast(e.message,'err')).finally(hideLoading);
}
function hapusPenerima(id){
const alasan=prompt(USER_ROLE==='super_admin'?'Alasan hapus marker penerima:':'Alasan mengajukan hapus pin penerima:')||'';
if(alasan.trim()==='') return showToast('Alasan penghapusan wajib diisi.','err');
const fd=new FormData(); fd.append('id',id); fd.append('alasan_hapus',alasan);
showLoading(); fetch('hapus_penerima.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{showToast(res.message,res.status==='ok'?'ok':'err'); if(res.status==='ok'){map.closePopup(); loadDataPeta();}}).catch(e=>showToast(e.message,'err')).finally(hideLoading);
}
function hapusRumahIbadah(id){
const alasan=prompt(USER_ROLE==='super_admin'?'Alasan hapus marker rumah ibadah:':'Alasan mengajukan hapus marker rumah ibadah:')||'';
if(alasan.trim()==='') return showToast('Alasan penghapusan wajib diisi.','err');
const fd=new FormData(); fd.append('id',id); fd.append('alasan_hapus',alasan);
showLoading(); fetch('hapus_rumah_ibadah.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{showToast(res.message,res.status==='ok'?'ok':'err'); if(res.status==='ok'){map.closePopup(); loadDataPeta();}}).catch(e=>showToast(e.message,'err')).finally(hideLoading);
}
function updateStatusPenerima(id,status,alasan=''){
if(!alasan && (status==='ditolak'||status==='dibatalkan'||status==='hapus_disetujui'||status==='hapus_ditolak')) alasan=prompt('Masukkan alasan/catatan:')||'';
const fd=new FormData(); fd.append('id',id); fd.append('status_pengajuan',status); fd.append('alasan_status',alasan);
showLoading(); fetch('update_status_penerima.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{showToast(res.message,res.status==='ok'?'ok':'err'); if(res.status==='ok'){map.closePopup(); refreshMarkerAfterUpdate(res.data,'penerima');}}).catch(e=>showToast(e.message,'err')).finally(hideLoading);
}
function updateStatusRumahIbadah(id,status,alasan=''){
if(!alasan && (status==='ditolak'||status==='hapus_disetujui'||status==='hapus_ditolak')) alasan=prompt('Masukkan alasan/catatan:')||'';
const fd=new FormData(); fd.append('id',id); fd.append('status_pengajuan',status); fd.append('keterangan',alasan);
showLoading(); fetch('update_status_rumah_ibadah.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{showToast(res.message,res.status==='ok'?'ok':'err'); if(res.status==='ok'){map.closePopup(); loadDataPeta();}}).catch(e=>showToast(e.message,'err')).finally(hideLoading);
}
// Fungsi berikut tersedia agar pola index konsisten dengan dashboard dan endpoint.
function loadProgramBantuan(){ return fetch('ambil_program_bantuan.php').then(r=>r.json()); }
function submitProgramBantuan(){ showToast('Kelola program bantuan tersedia di dashboard.','ok'); }
function submitPenyaluran(){ showToast('Catat penyaluran bantuan tersedia di dashboard.','ok'); }
function loadLaporanPenyaluran(){ return fetch('ambil_laporan_penyaluran.php').then(r=>r.json()); }
function renderTabelPenyaluran(data){ return data; }
function renderRekapPenyaluran(data){ return data; }
function filterLaporanPenyaluran(){ return loadLaporanPenyaluran(); }
function showDetailPenyaluran(id){ showToast('Detail penyaluran dapat dilihat di dashboard/riwayat.','ok'); }
const legend=L.control({position:'bottomright'}); legend.onAdd=function(){const d=L.DomUtil.create('div','legend');d.innerHTML='<b>Legenda</b><div class="li"><span class="dot" style="background:#22c55e"></span>Islam</div><div class="li"><span class="dot" style="background:#3b82f6"></span>Kristen</div><div class="li"><span class="dot" style="background:#8b5cf6"></span>Katolik</div><div class="li"><span class="dot" style="background:#111827"></span>Penerima diterima</div><div class="li"><span class="dot" style="background:#6b7280"></span>Menunggu</div><div class="li"><span class="dot" style="background:#92400e"></span>Dibatalkan</div>';return d};legend.addTo(map);
loadDataPeta();
</script>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
<?php
/**
* Koneksi database Project 05.
*/
$host = getenv('DB_HOST') ?: 'db';
$user = getenv('DB_USER') ?: 'root';
$pass = getenv('DB_PASSWORD') ?: 'webgisidolapass';
$db = '05_povertydb';
$koneksi = new mysqli($host, $user, $pass, $db);
if ($koneksi->connect_error) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'status' => 'err',
'message' => 'Koneksi database gagal: ' . $koneksi->connect_error
]);
exit;
}
$koneksi->set_charset('utf8mb4');
?>
+53
View File
@@ -0,0 +1,53 @@
<?php
require_once __DIR__ . '/auth.php';
if (is_logged_in()) {
header('Location: dashboard.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = clean_text(post('username'), 50);
$password = post('password');
$stmt = $koneksi->prepare("SELECT * FROM users WHERE username=? AND status_akun='aktif' LIMIT 1");
$stmt->bind_param('s', $username);
$stmt->execute();
$user = $stmt->get_result()->fetch_assoc();
// password_verify() digunakan agar password tidak disimpan plain text.
if ($user && password_verify($password, $user['password'])) {
$_SESSION['user_id'] = (int) $user['id'];
$_SESSION['nama'] = $user['nama'];
$_SESSION['role'] = $user['role'];
header('Location: dashboard.php');
exit;
}
$error = 'Username atau password salah.';
}
?>
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login - WebGIS Poverty Mapping</title>
<style>
*{box-sizing:border-box} body{margin:0;min-height:100vh;font-family:Arial,sans-serif;background:linear-gradient(135deg,#0f766e,#0f172a);display:flex;align-items:center;justify-content:center;padding:20px}.card{width:100%;max-width:410px;background:#fff;border-radius:18px;box-shadow:0 18px 45px rgba(0,0,0,.25);padding:28px}.brand{display:flex;gap:12px;align-items:center;margin-bottom:18px}.logo{width:46px;height:46px;border-radius:13px;background:#0f766e;color:white;display:flex;align-items:center;justify-content:center;font-size:24px}h1{font-size:22px;margin:0;color:#0f172a}.muted{color:#64748b;font-size:13px;margin-top:4px}.fg{margin:14px 0}.fg label{font-weight:700;font-size:13px;color:#334155;display:block;margin-bottom:6px}.fg input{width:100%;padding:12px;border:1px solid #cbd5e1;border-radius:10px;font-size:14px}.btn{width:100%;padding:12px;border:0;border-radius:10px;background:#0f766e;color:white;font-weight:700;cursor:pointer}.err{background:#fee2e2;color:#991b1b;padding:10px;border-radius:10px;font-size:13px;margin:10px 0}.links{text-align:center;margin-top:15px;font-size:13px}.links a{color:#0f766e;text-decoration:none;font-weight:700}.demo{background:#f8fafc;border:1px solid #e2e8f0;padding:10px;border-radius:10px;font-size:12px;color:#475569;margin-top:14px}
</style>
</head>
<body>
<div class="card">
<div class="brand"><div class="logo">🗺️</div><div><h1>WebGIS Poverty Mapping</h1><div class="muted">Manajemen bantuan/zakat berbasis lokasi</div></div></div>
<?php if($error): ?><div class="err"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<form method="post">
<div class="fg"><label>Username</label><input name="username" required autofocus></div>
<div class="fg"><label>Password</label><input name="password" type="password" required></div>
<button class="btn">Login</button>
</form>
<div class="links">Belum punya akun penerima? <a href="register.php">Registrasi</a></div>
<div class="demo">Akun awal:<br>username <b>superadmin</b><br>password <b>password</b><br><br><b>Catatan:</b> registrasi hanya untuk penerima. Akun admin rumah ibadah dibuat oleh super admin dari dashboard.</div>
</div>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
<?php
if (session_status() === PHP_SESSION_NONE) session_start();
$_SESSION = [];
session_destroy();
header('Location: login.php');
exit;
?>
+58
View File
@@ -0,0 +1,58 @@
<?php
require_once __DIR__ . '/auth.php';
$error = '';
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nama = clean_text(post('nama'), 100);
$username = clean_text(post('username'), 50);
$password = post('password');
$password2 = post('password2');
if ($nama === '' || $username === '' || strlen($password) < 6) {
$error = 'Nama, username, dan password minimal 6 karakter wajib diisi.';
} elseif ($password !== $password2) {
$error = 'Konfirmasi password tidak sama.';
} else {
$cek = $koneksi->prepare("SELECT id FROM users WHERE username=? LIMIT 1");
$cek->bind_param('s', $username);
$cek->execute();
if ($cek->get_result()->fetch_assoc()) {
$error = 'Username sudah digunakan.';
} else {
// password_hash() untuk keamanan password.
$hash = password_hash($password, PASSWORD_DEFAULT);
$role = 'penerima';
$stmt = $koneksi->prepare("INSERT INTO users (nama, username, password, role) VALUES (?,?,?,?)");
$stmt->bind_param('ssss', $nama, $username, $hash, $role);
if ($stmt->execute()) {
$success = 'Registrasi berhasil. Silakan login.';
} else {
$error = 'Registrasi gagal: ' . $koneksi->error;
}
}
}
}
?>
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Registrasi Penerima</title>
<style>
*{box-sizing:border-box} body{margin:0;min-height:100vh;font-family:Arial,sans-serif;background:#f1f5f9;display:flex;align-items:center;justify-content:center;padding:20px}.card{width:100%;max-width:460px;background:#fff;border-radius:18px;box-shadow:0 12px 32px rgba(15,23,42,.12);padding:28px}h1{margin:0 0 5px;color:#0f172a;font-size:23px}.muted{color:#64748b;font-size:13px;margin-bottom:16px}.fg{margin:13px 0}.fg label{display:block;margin-bottom:6px;font-size:13px;font-weight:700;color:#334155}.fg input{width:100%;border:1px solid #cbd5e1;border-radius:10px;padding:12px}.btn{width:100%;border:0;border-radius:10px;padding:12px;background:#0f766e;color:#fff;font-weight:700;cursor:pointer}.msg{padding:10px;border-radius:10px;font-size:13px;margin:10px 0}.err{background:#fee2e2;color:#991b1b}.ok{background:#dcfce7;color:#166534}.links{text-align:center;margin-top:15px}.links a{color:#0f766e;text-decoration:none;font-weight:700}
</style>
</head>
<body><div class="card">
<h1>Registrasi Penerima</h1><div class="muted">Akun ini digunakan untuk mengajukan penerima bantuan melalui peta.</div>
<?php if($error): ?><div class="msg err"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<?php if($success): ?><div class="msg ok"><?= htmlspecialchars($success) ?></div><?php endif; ?>
<form method="post">
<div class="fg"><label>Nama Lengkap</label><input name="nama" required></div>
<div class="fg"><label>Username</label><input name="username" required></div>
<div class="fg"><label>Password</label><input type="password" name="password" required></div>
<div class="fg"><label>Konfirmasi Password</label><input type="password" name="password2" required></div>
<button class="btn">Daftar</button>
</form>
<div class="links"><a href="login.php">Kembali ke login</a></div>
</div></body></html>
+83
View File
@@ -0,0 +1,83 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
$allowedKondisi = ['baik','sedang','rusak'];
$allowedJenis = ['zakat','sembako','uang_tunai','lainnya'];
$nama = clean_text(post('nama_penerima'), 100);
$nik = clean_text(post('nik'), 30);
$no_hp = clean_text(post('no_hp'), 20);
$alamat = clean_text(post('alamat'), 1000);
$pekerjaan = clean_text(post('pekerjaan'), 100);
$penghasilan = (int) post('penghasilan', 0);
$jumlah_tanggungan = (int) post('jumlah_tanggungan', 0);
$kondisi_rumah = post('kondisi_rumah');
$jenis_bantuan = post('jenis_bantuan');
$username_penerima = clean_text(post('username_penerima'), 50);
$lat = post('latitude');
$lng = post('longitude');
if ($nama==='' || $no_hp==='' || $alamat==='' || $pekerjaan==='' || !valid_latlng($lat, $lng)) json_response('err', 'Data wajib belum lengkap atau koordinat tidak valid.');
if (!valid_enum($kondisi_rumah, $allowedKondisi) || !valid_enum($jenis_bantuan, $allowedJenis)) json_response('err', 'Pilihan kondisi rumah atau jenis bantuan tidak valid.');
if ($penghasilan < 0 || $jumlah_tanggungan < 0) json_response('err', 'Penghasilan dan tanggungan tidak valid.');
$role = $_SESSION['role'];
$user_id = null;
$rumah_ibadah_id = null;
$jarak_meter = null;
$sumber = 'mandiri';
if ($role === 'penerima') {
$user_id = (int) $_SESSION['user_id'];
$sumber = 'mandiri';
} elseif ($role === 'admin_rumah_ibadah' || $role === 'super_admin') {
if ($username_penerima === '') json_response('err', 'Username penerima wajib diisi agar titik rumah terkait ke akun yang benar.', [], 422);
$akunPenerima = get_user_by_username_role($username_penerima, 'penerima');
if (!$akunPenerima) json_response('err', 'Username penerima tidak ditemukan, bukan role penerima, atau akun tidak aktif.', [], 404);
$user_id = (int) $akunPenerima['id'];
$sumber = 'admin_rumah_ibadah';
} else {
json_response('err', 'Role tidak valid.', [], 403);
}
// Satu akun penerima hanya boleh punya satu pin aktif.
if (has_active_penerima_pin($user_id)) {
json_response('err', 'Akun penerima ini sudah memiliki pin aktif. Satu akun penerima hanya boleh punya satu pin aktif.', [], 409);
}
$nearest = get_nearest_rumah_ibadah((float) $lat, (float) $lng, 300);
if ($nearest) {
$rumah_ibadah_id = (int) $nearest['id'];
$jarak_meter = (float) $nearest['jarak_meter'];
}
if ($role === 'admin_rumah_ibadah') {
$ownNearest = get_nearest_rumah_ibadah((float)$lat, (float)$lng, 300, (int)$_SESSION['user_id']);
if (!$ownNearest) json_response('err', 'Lokasi penerima berada di luar radius 300 meter seluruh rumah ibadah Anda.');
if ($nearest && (int)$nearest['id'] !== (int)$ownNearest['id']) {
json_response('err', 'Lokasi penerima lebih dekat ke rumah ibadah lain. Sistem hanya boleh mengaitkan penerima ke rumah ibadah terdekat.', [], 409);
}
$rumah_ibadah_id = (int) $ownNearest['id'];
$jarak_meter = (float) $ownNearest['jarak_meter'];
}
$status = 'menunggu';
// FIX: pisahkan cabang INSERT agar rumah_ibadah_id dan jarak_meter NULL tersimpan benar, bukan 0.
if ($rumah_ibadah_id !== null) {
$stmt = $koneksi->prepare("INSERT INTO penerima_bantuan (user_id, rumah_ibadah_id, nama_penerima, nik, no_hp, alamat, pekerjaan, penghasilan, jumlah_tanggungan, kondisi_rumah, jenis_bantuan, latitude, longitude, jarak_meter, status_pengajuan, sumber_pengajuan) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
$stmt->bind_param('iisssssiissdddss', $user_id, $rumah_ibadah_id, $nama, $nik, $no_hp, $alamat, $pekerjaan, $penghasilan, $jumlah_tanggungan, $kondisi_rumah, $jenis_bantuan, $lat, $lng, $jarak_meter, $status, $sumber);
} else {
$stmt = $koneksi->prepare("INSERT INTO penerima_bantuan (user_id, rumah_ibadah_id, nama_penerima, nik, no_hp, alamat, pekerjaan, penghasilan, jumlah_tanggungan, kondisi_rumah, jenis_bantuan, latitude, longitude, jarak_meter, status_pengajuan, sumber_pengajuan) VALUES (?,NULL,?,?,?,?,?,?,?,?,?,?,?,NULL,?,?)");
$stmt->bind_param('isssssiissddss', $user_id, $nama, $nik, $no_hp, $alamat, $pekerjaan, $penghasilan, $jumlah_tanggungan, $kondisi_rumah, $jenis_bantuan, $lat, $lng, $status, $sumber);
}
if (!$stmt->execute()) json_response('err', 'Gagal menyimpan penerima: ' . $koneksi->error);
$id = $stmt->insert_id;
insert_riwayat_status($id, null, $status, 'Pengajuan penerima dibuat.');
$row = get_penerima_by_id($id);
$msg = $rumah_ibadah_id ? 'Pengajuan penerima berhasil disimpan dan masuk radius rumah ibadah terdekat.' : 'Pengajuan tersimpan, tetapi belum ada pemberi bantuan dalam radius 300 meter. Nanti akan otomatis terhubung jika ada rumah ibadah diterima di radius tersebut.';
json_response('ok', $msg, $row);
?>
+76
View File
@@ -0,0 +1,76 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
require_role(['super_admin','admin_rumah_ibadah'], true);
$program_id = (int) post('program_id', 0);
$penerima_id = (int) post('penerima_id', 0);
$ket = clean_text(post('keterangan'), 1500);
$tanggal = post('tanggal_penyaluran', date('Y-m-d'));
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $tanggal)) json_response('err', 'Tanggal penyaluran tidak valid.');
$stmt = $koneksi->prepare("SELECT p.*, ri.user_id AS admin_rumah_user_id, ri.latitude AS ri_latitude, ri.longitude AS ri_longitude, ri.nama_rumah_ibadah FROM program_bantuan p LEFT JOIN rumah_ibadah ri ON ri.id=p.rumah_ibadah_id WHERE p.id=? AND COALESCE(ri.status_hapus,'normal')<>'disetujui' LIMIT 1");
$stmt->bind_param('i', $program_id);
$program = fetch_all_stmt($stmt)[0] ?? null;
if (!$program || $program['status_program'] === 'dibatalkan') json_response('err', 'Program bantuan tidak ditemukan atau dibatalkan.');
// FIX: cek status program selesai — tidak bisa menyalurkan ke program yang sudah selesai
if ($program['status_program'] === 'selesai') json_response('err', 'Program bantuan sudah berstatus selesai, tidak dapat menyalurkan bantuan baru.', [], 409);
$jenis = $program['jenis_bantuan'];
$nominal = $program['nominal'] === null ? null : (int)$program['nominal'];
$jumlah_item = $program['jumlah_item'] === null ? '' : (string)$program['jumlah_item'];
$penerima = get_penerima_by_id($penerima_id);
if (!$penerima || $penerima['status_pengajuan'] !== 'diterima' || ($penerima['status_hapus'] ?? 'normal') === 'disetujui') json_response('err', 'Penerima tidak ditemukan, belum diterima, atau marker sudah dihapus.');
$rumah_ibadah_id = (int) $program['rumah_ibadah_id'];
if (!$rumah_ibadah_id) json_response('err', 'Program belum terhubung ke rumah ibadah.');
if ((int)$penerima['rumah_ibadah_id'] !== $rumah_ibadah_id) {
json_response('err', 'Program dan penerima tidak berada pada rumah ibadah yang sama. Sinkronkan data penerima terlebih dahulu.', [], 409);
}
if ($_SESSION['role'] === 'admin_rumah_ibadah') {
if (!is_admin_rumah_ibadah_id($rumah_ibadah_id, (int)$_SESSION['user_id'], true)) {
json_response('err', 'Anda hanya bisa menyalurkan bantuan untuk seluruh rumah ibadah milik akun Anda sendiri.', [], 403);
}
}
$jarak = haversine_meter((float)$penerima['latitude'], (float)$penerima['longitude'], (float)$program['ri_latitude'], (float)$program['ri_longitude']);
if ($jarak > 300) json_response('err', 'Penerima berada di luar radius 300 meter rumah ibadah program.');
$bulan = (int) date('m', strtotime($tanggal));
$tahun = (int) date('Y', strtotime($tanggal));
$stmt = $koneksi->prepare("SELECT COUNT(*) AS jml FROM penyaluran_bantuan WHERE penerima_id=? AND program_id=? AND MONTH(tanggal_penyaluran)=? AND YEAR(tanggal_penyaluran)=?");
$stmt->bind_param('iiii', $penerima_id, $program_id, $bulan, $tahun);
$count = fetch_all_stmt($stmt)[0];
$bantuan_ke = ((int)$count['jml']) + 1;
$target = (int)$program['target_frekuensi'];
$warning = '';
if ($bantuan_ke > $target) {
$warning = "Peringatan: penerima ini sudah/akan menerima bantuan $bantuan_ke kali pada program ini. Target program adalah $target kali. Data tetap dicatat untuk audit.";
}
$admin_id = (int) $_SESSION['user_id'];
// FIX: pisahkan cabang INSERT agar nominal NULL tersimpan sebagai NULL, bukan 0.
if ($nominal !== null) {
$stmt = $koneksi->prepare("INSERT INTO penyaluran_bantuan (program_id, penerima_id, rumah_ibadah_id, admin_id, jenis_bantuan, nominal, jumlah_item, bantuan_ke, keterangan, tanggal_penyaluran) VALUES (?,?,?,?,?,?,?,?,?,?)");
$stmt->bind_param('iiiisisiss', $program_id, $penerima_id, $rumah_ibadah_id, $admin_id, $jenis, $nominal, $jumlah_item, $bantuan_ke, $ket, $tanggal);
} else {
$stmt = $koneksi->prepare("INSERT INTO penyaluran_bantuan (program_id, penerima_id, rumah_ibadah_id, admin_id, jenis_bantuan, nominal, jumlah_item, bantuan_ke, keterangan, tanggal_penyaluran) VALUES (?,?,?,?,?,NULL,?,?,?,?)");
$stmt->bind_param('iiiississ', $program_id, $penerima_id, $rumah_ibadah_id, $admin_id, $jenis, $jumlah_item, $bantuan_ke, $ket, $tanggal);
}
if (!$stmt->execute()) json_response('err', 'Gagal mencatat penyaluran bantuan: ' . $koneksi->error);
json_response('ok', 'Penyaluran bantuan berhasil dicatat sesuai master program.' . ($warning ? ' ' . $warning : ''), [
'id' => $stmt->insert_id,
'bantuan_ke' => $bantuan_ke,
'jenis_bantuan' => $jenis,
'nominal' => $nominal,
'jumlah_item' => $jumlah_item,
'warning' => $warning
]);
?>
+48
View File
@@ -0,0 +1,48 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
require_role(['super_admin','admin_rumah_ibadah'], true);
$allowedJenis = ['zakat','sembako','uang_tunai','lainnya'];
$nama = clean_text(post('nama_program'), 150);
$jenis = post('jenis_bantuan');
$deskripsi = clean_text(post('deskripsi'), 1500);
$bulan = (int) post('bulan');
$tahun = (int) post('tahun');
$target = (int) post('target_frekuensi', 1);
$nominalInput = post('nominal', '');
$nominal = ($nominalInput === '') ? null : (int) $nominalInput;
$jumlah_item = clean_text(post('jumlah_item', ''), 100);
$rumah_ibadah_id = post('rumah_ibadah_id', '');
if ($nama==='' || !valid_enum($jenis, $allowedJenis)) json_response('err', 'Nama program atau jenis bantuan tidak valid.');
if ($bulan < 1 || $bulan > 12 || $tahun < 2000 || $target < 1) json_response('err', 'Bulan, tahun, atau target frekuensi tidak valid.');
if ($nominal !== null && $nominal < 0) json_response('err', 'Nominal tidak boleh minus.');
if ($_SESSION['role'] === 'admin_rumah_ibadah') {
$ids = get_admin_rumah_ibadah_ids((int)$_SESSION['user_id'], true);
if (empty($ids)) json_response('err', 'Admin belum memiliki rumah ibadah diterima.');
if ($rumah_ibadah_id === '') {
if (count($ids) > 1) json_response('err', 'Pilih rumah ibadah terlebih dahulu karena akun ini mengelola lebih dari satu rumah ibadah.', [], 422);
$rumah_ibadah_id = $ids[0];
} else {
$rumah_ibadah_id = (int)$rumah_ibadah_id;
if (!in_array($rumah_ibadah_id, $ids, true)) json_response('err', 'Rumah ibadah tidak termasuk milik admin ini.', [], 403);
}
} else {
$rumah_ibadah_id = ($rumah_ibadah_id === '') ? null : (int) $rumah_ibadah_id;
if (!$rumah_ibadah_id) json_response('err', 'Rumah ibadah wajib dipilih.');
}
// FIX: pisahkan cabang INSERT agar nominal NULL tersimpan sebagai NULL, bukan 0.
if ($nominal !== null) {
$stmt = $koneksi->prepare("INSERT INTO program_bantuan (rumah_ibadah_id, nama_program, jenis_bantuan, deskripsi, bulan, tahun, target_frekuensi, nominal, jumlah_item) VALUES (?,?,?,?,?,?,?,?,?)");
$stmt->bind_param('isssiiiis', $rumah_ibadah_id, $nama, $jenis, $deskripsi, $bulan, $tahun, $target, $nominal, $jumlah_item);
} else {
$stmt = $koneksi->prepare("INSERT INTO program_bantuan (rumah_ibadah_id, nama_program, jenis_bantuan, deskripsi, bulan, tahun, target_frekuensi, nominal, jumlah_item) VALUES (?,?,?,?,?,?,?,NULL,?)");
$stmt->bind_param('isssiiis', $rumah_ibadah_id, $nama, $jenis, $deskripsi, $bulan, $tahun, $target, $jumlah_item);
}
if (!$stmt->execute()) json_response('err', 'Gagal membuat program bantuan: ' . $koneksi->error);
json_response('ok', 'Program bantuan berhasil dibuat. Jenis, nominal, dan jumlah item akan menjadi acuan mutlak saat penyaluran.', ['id' => $stmt->insert_id]);
?>
+63
View File
@@ -0,0 +1,63 @@
<?php
/**
* tambah_user.php
* Membuat akun pengguna dari dashboard.
* - Super admin dapat membuat akun admin_rumah_ibadah dan penerima.
* - Admin rumah ibadah hanya dapat membuat akun penerima.
* Password disimpan memakai password_hash().
*/
require_once __DIR__ . '/auth.php';
require_login(true);
require_role(['super_admin', 'admin_rumah_ibadah'], true);
require_post();
$actor = current_user();
$actorRole = $actor['role'];
$nama = clean_text(post('nama'), 100);
$username = clean_text(post('username'), 50);
$password = post('password');
$role = post('role');
$status_akun = post('status_akun', 'aktif');
$statusList = ['aktif', 'nonaktif'];
$allowedRoles = ($actorRole === 'super_admin') ? ['admin_rumah_ibadah', 'penerima'] : ['penerima'];
if ($nama === '' || $username === '' || $password === '') {
json_response('err', 'Nama, username, dan password wajib diisi.', [], 422);
}
if (!preg_match('/^[a-zA-Z0-9_\.\-]{4,50}$/', $username)) {
json_response('err', 'Username minimal 4 karakter dan hanya boleh huruf, angka, titik, strip, atau underscore.', [], 422);
}
if (strlen($password) < 6) {
json_response('err', 'Password minimal 6 karakter.', [], 422);
}
if (!valid_enum($role, $allowedRoles)) {
json_response('err', 'Role tidak boleh dibuat oleh akun ini.', [], 403);
}
if (!valid_enum($status_akun, $statusList)) {
json_response('err', 'Status akun tidak valid.', [], 422);
}
$stmt = $koneksi->prepare("SELECT id FROM users WHERE username=? LIMIT 1");
$stmt->bind_param('s', $username);
$stmt->execute();
if ($stmt->get_result()->fetch_assoc()) {
json_response('err', 'Username sudah digunakan.', [], 409);
}
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $koneksi->prepare("INSERT INTO users (nama, username, password, role, status_akun) VALUES (?,?,?,?,?)");
$stmt->bind_param('sssss', $nama, $username, $hash, $role, $status_akun);
if (!$stmt->execute()) {
json_response('err', 'Gagal membuat akun: ' . $stmt->error, [], 500);
}
json_response('ok', 'Akun berhasil dibuat.', [
'id' => $stmt->insert_id,
'nama' => $nama,
'username' => $username,
'role' => $role,
'status_akun' => $status_akun
]);
?>
+56
View File
@@ -0,0 +1,56 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
require_role(['super_admin','admin_rumah_ibadah'], true);
$id = (int) post('id', 0);
$status = post('status_pengajuan', post('status', ''));
$alasan = clean_text(post('alasan_status', post('alasan', '')), 1000);
$allowed = ['menunggu','diterima','ditolak','dibatalkan','hapus_disetujui','hapus_ditolak'];
if (!valid_enum($status, $allowed)) json_response('err', 'Status tidak valid.');
$penerima = get_penerima_by_id($id);
if (!$penerima) json_response('err', 'Data penerima tidak ditemukan.', [], 404);
// Persetujuan/penolakan hapus marker hanya boleh dilakukan super admin.
if ($status === 'hapus_disetujui' || $status === 'hapus_ditolak') {
require_role('super_admin', true);
$catatan = $alasan ?: ($status === 'hapus_disetujui' ? 'Pengajuan hapus marker penerima disetujui.' : 'Pengajuan hapus marker penerima ditolak.');
if ($status === 'hapus_disetujui') {
$statusPengajuan = 'dibatalkan';
$statusHapus = 'disetujui';
$stmt = $koneksi->prepare("UPDATE penerima_bantuan SET status_pengajuan=?, status_hapus=?, alasan_hapus=?, alasan_status=? WHERE id=?");
$stmt->bind_param('ssssi', $statusPengajuan, $statusHapus, $catatan, $catatan, $id);
} else {
$statusHapus = 'ditolak';
$stmt = $koneksi->prepare("UPDATE penerima_bantuan SET status_hapus=?, alasan_hapus=? WHERE id=?");
$stmt->bind_param('ssi', $statusHapus, $catatan, $id);
}
if (!$stmt->execute()) json_response('err', 'Gagal memproses persetujuan hapus penerima: ' . $koneksi->error);
insert_riwayat_status($id, $penerima['status_pengajuan'], $status, $catatan);
json_response('ok', 'Persetujuan hapus marker penerima berhasil diproses.', get_penerima_by_id($id));
}
if (!can_manage_penerima($penerima)) json_response('err', 'Data tidak ditemukan atau akses ditolak.', [], 403);
// FIX: admin_rumah_ibadah tidak boleh memundurkan status ke 'menunggu' jika sudah 'diterima' atau 'ditolak'.
// Hanya super_admin yang bisa mereset status.
if ($status === 'menunggu' && $_SESSION['role'] === 'admin_rumah_ibadah') {
json_response('err', 'Admin rumah ibadah tidak dapat mereset status penerima ke menunggu. Hubungi super admin.', [], 403);
}
// FIX: admin_rumah_ibadah tidak bisa membatalkan penerima yang sudah 'diterima' — hanya super_admin.
if ($status === 'dibatalkan' && $penerima['status_pengajuan'] === 'diterima' && $_SESSION['role'] === 'admin_rumah_ibadah') {
json_response('err', 'Admin rumah ibadah tidak dapat membatalkan penerima yang sudah berstatus diterima. Hubungi super admin.', [], 403);
}
$status_lama = $penerima['status_pengajuan'];
$stmt = $koneksi->prepare("UPDATE penerima_bantuan SET status_pengajuan=?, alasan_status=? WHERE id=?");
$stmt->bind_param('ssi', $status, $alasan, $id);
if (!$stmt->execute()) json_response('err', 'Gagal update status: ' . $koneksi->error);
insert_riwayat_status($id, $status_lama, $status, $alasan ?: 'Status penerima diubah.');
$row = get_penerima_by_id($id);
json_response('ok', 'Status penerima berhasil diubah.', $row);
?>
+63
View File
@@ -0,0 +1,63 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
require_role('super_admin', true);
$id = (int) post('id', 0);
$status = post('status_pengajuan', post('status', ''));
$ket = clean_text(post('keterangan', post('alasan', '')), 1000);
if (!valid_enum($status, ['menunggu','diterima','ditolak','hapus_disetujui','hapus_ditolak'])) json_response('err', 'Status rumah ibadah tidak valid.');
$rumah = get_rumah_ibadah_by_id($id);
if (!$rumah) json_response('err', 'Rumah ibadah tidak ditemukan.', [], 404);
$jumlahSinkron = 0;
$jumlahLepas = 0;
if ($status === 'hapus_disetujui' || $status === 'hapus_ditolak') {
$catatan = $ket ?: ($status === 'hapus_disetujui' ? 'Pengajuan hapus marker rumah ibadah disetujui.' : 'Pengajuan hapus marker rumah ibadah ditolak.');
if ($status === 'hapus_disetujui') {
$statusPengajuan = 'ditolak';
$statusHapus = 'disetujui';
$stmt = $koneksi->prepare("UPDATE rumah_ibadah SET status_pengajuan=?, status_hapus=?, alasan_hapus=?, keterangan=? WHERE id=?");
$stmt->bind_param('ssssi', $statusPengajuan, $statusHapus, $catatan, $catatan, $id);
if (!$stmt->execute()) json_response('err', 'Gagal menyetujui hapus rumah ibadah: ' . $koneksi->error);
$jumlahLepas = detach_penerima_from_rumah_ibadah($id);
} else {
$statusHapus = 'ditolak';
$stmt = $koneksi->prepare("UPDATE rumah_ibadah SET status_hapus=?, alasan_hapus=? WHERE id=?");
$stmt->bind_param('ssi', $statusHapus, $catatan, $id);
if (!$stmt->execute()) json_response('err', 'Gagal menolak hapus rumah ibadah: ' . $koneksi->error);
}
$row = get_rumah_ibadah_by_id($id);
$row['jumlah_penerima_tersinkron'] = 0;
$row['jumlah_penerima_dilepas'] = $jumlahLepas;
json_response('ok', 'Persetujuan hapus marker rumah ibadah berhasil diproses.', $row);
}
// Update status pengajuan rumah ibadah biasa.
$statusHapus = 'normal';
$stmt = $koneksi->prepare("UPDATE rumah_ibadah SET status_pengajuan=?, status_hapus=?, keterangan=? WHERE id=?");
$stmt->bind_param('sssi', $status, $statusHapus, $ket, $id);
if (!$stmt->execute()) json_response('err', 'Gagal update status rumah ibadah: ' . $koneksi->error);
if ($status === 'diterima') {
// Saat rumah ibadah disetujui, penerima yang dibuat lebih dulu otomatis masuk area jika radius <= 300 meter.
$jumlahSinkron = sync_penerima_area_by_rumah_ibadah($id);
} elseif ($status === 'ditolak') {
// Jika rumah ibadah ditolak, penerima yang sebelumnya terkait dilepas agar tidak mengarah ke pemberi bantuan tidak aktif.
$jumlahLepas = detach_penerima_from_rumah_ibadah($id);
}
$row = get_rumah_ibadah_by_id($id);
if ($row) {
$row['jumlah_penerima_tersinkron'] = $jumlahSinkron;
$row['jumlah_penerima_dilepas'] = $jumlahLepas;
}
$msg = 'Status rumah ibadah berhasil diubah.';
if ($jumlahSinkron) $msg .= " $jumlahSinkron penerima otomatis masuk area tanggung jawab.";
if ($jumlahLepas) $msg .= " $jumlahLepas penerima dilepas dari rumah ibadah ini.";
json_response('ok', $msg, $row);
?>