diff --git a/api/dashboard/stats.php b/api/dashboard/stats.php new file mode 100644 index 0000000..f058620 --- /dev/null +++ b/api/dashboard/stats.php @@ -0,0 +1,196 @@ +query("SELECT * FROM rumah_ibadah WHERE id=$ibadahId"); +} else { + $riRes = $conn->query("SELECT * FROM rumah_ibadah"); +} +$ibadahList = []; +while ($row = $riRes->fetch_assoc()) $ibadahList[] = $row; +$totalIbadah = count($ibadahList); + +// Distribusi per jenis +$jenisDist = []; +foreach ($ibadahList as $ib) { + $j = $ib['jenis'] ?? 'Lainnya'; + $jenisDist[$j] = ($jenisDist[$j] ?? 0) + 1; +} + +// ── 2. Penduduk Miskin ──────────────────────────────────────────────────── +$pmRes = $conn->query("SELECT * FROM penduduk_miskin"); +$allMiskin = []; +while ($row = $pmRes->fetch_assoc()) $allMiskin[] = $row; + +// Filter: pengelola hanya lihat yang dalam radius ibadahnya +if ($isPengelola && $ibadahId && !empty($ibadahList)) { + $ib = $ibadahList[0]; + $filteredMiskin = array_filter($allMiskin, function($m) use ($ib) { + return haversine((float)$ib['lat'], (float)$ib['lng'], (float)$m['lat'], (float)$m['lng']) <= (float)$ib['radius']; + }); + $filteredMiskin = array_values($filteredMiskin); +} else { + $filteredMiskin = $allMiskin; +} + +$totalMiskin = count($filteredMiskin); +$totalJiwa = array_sum(array_column($filteredMiskin, 'jumlah_jiwa')); + +// Kategori bantuan +$kategoriDist = []; +foreach ($filteredMiskin as $m) { + $k = $m['kategori_bantuan'] ?? 'Lainnya'; + $kategoriDist[$k] = ($kategoriDist[$k] ?? 0) + 1; +} + +// Terurus vs Tidak (dalam radius salah satu ibadah) +$terurusCount = 0; +$tidakTerurusCount = 0; +foreach ($filteredMiskin as $m) { + $inAnyRadius = false; + foreach ($ibadahList as $ib) { + if (haversine((float)$ib['lat'], (float)$ib['lng'], (float)$m['lat'], (float)$m['lng']) <= (float)$ib['radius']) { + $inAnyRadius = true; + break; + } + } + if ($inAnyRadius) $terurusCount++; + else $tidakTerurusCount++; +} + +// Jika admin: hitung seluruh miskin dalam radius seluruh ibadah +if (!$isPengelola) { + $allIbadahRes = $conn->query("SELECT * FROM rumah_ibadah"); + $allIbadahList = []; + while ($row = $allIbadahRes->fetch_assoc()) $allIbadahList[] = $row; + + $terurusCount = 0; + $tidakTerurusCount = 0; + foreach ($allMiskin as $m) { + $inAnyRadius = false; + foreach ($allIbadahList as $ib) { + if (haversine((float)$ib['lat'], (float)$ib['lng'], (float)$m['lat'], (float)$m['lng']) <= (float)$ib['radius']) { + $inAnyRadius = true; + break; + } + } + if ($inAnyRadius) $terurusCount++; + else $tidakTerurusCount++; + } +} + +// ── 3. Log Bantuan ──────────────────────────────────────────────────────── +if ($isPengelola && $ibadahId) { + $logRes = $conn->query(" + SELECT lb.*, ri.nama as nama_ibadah, ri.jenis as jenis_ibadah, pm.nama as nama_kk + FROM log_bantuan lb + JOIN rumah_ibadah ri ON ri.id = lb.ibadah_id + JOIN penduduk_miskin pm ON pm.id = lb.miskin_id + WHERE lb.ibadah_id = $ibadahId + ORDER BY lb.tanggal DESC, lb.id DESC + LIMIT 10 + "); + $totalLogRes = $conn->query("SELECT COUNT(*) as cnt FROM log_bantuan WHERE ibadah_id=$ibadahId"); +} else { + $logRes = $conn->query(" + SELECT lb.*, ri.nama as nama_ibadah, ri.jenis as jenis_ibadah, pm.nama as nama_kk + FROM log_bantuan lb + JOIN rumah_ibadah ri ON ri.id = lb.ibadah_id + JOIN penduduk_miskin pm ON pm.id = lb.miskin_id + ORDER BY lb.tanggal DESC, lb.id DESC + LIMIT 10 + "); + $totalLogRes = $conn->query("SELECT COUNT(*) as cnt FROM log_bantuan"); +} + +$recentLogs = []; +while ($row = $logRes->fetch_assoc()) $recentLogs[] = $row; + +$totalLog = 0; +if ($totalLogRes && $trow = $totalLogRes->fetch_assoc()) $totalLog = (int)$trow['cnt']; + +// Distribusi tipe bantuan dari log +$tipeDist = []; +if ($isPengelola && $ibadahId) { + $tipeRes = $conn->query("SELECT tipe_bantuan, COUNT(*) as cnt FROM log_bantuan WHERE ibadah_id=$ibadahId GROUP BY tipe_bantuan"); +} else { + $tipeRes = $conn->query("SELECT tipe_bantuan, COUNT(*) as cnt FROM log_bantuan GROUP BY tipe_bantuan"); +} +while ($row = $tipeRes->fetch_assoc()) $tipeDist[$row['tipe_bantuan']] = (int)$row['cnt']; + +// Bantuan per bulan (6 bulan terakhir) +if ($isPengelola && $ibadahId) { + $bulanRes = $conn->query(" + SELECT DATE_FORMAT(tanggal, '%Y-%m') as bulan, COUNT(*) as cnt + FROM log_bantuan + WHERE ibadah_id=$ibadahId AND tanggal >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH) + GROUP BY bulan ORDER BY bulan ASC + "); +} else { + $bulanRes = $conn->query(" + SELECT DATE_FORMAT(tanggal, '%Y-%m') as bulan, COUNT(*) as cnt + FROM log_bantuan + WHERE tanggal >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH) + GROUP BY bulan ORDER BY bulan ASC + "); +} +$bantuanPerBulan = []; +while ($row = $bulanRes->fetch_assoc()) $bantuanPerBulan[] = $row; + +// ── 4. Ibadah Tipe (admin only: performa per ibadah) ───────────────────── +$ibadahPerforma = []; +if (!$isPengelola) { + $perfRes = $conn->query(" + SELECT ri.id, ri.nama, ri.jenis, ri.radius, + COUNT(DISTINCT lb.id) as total_log, + COUNT(DISTINCT lb.miskin_id) as total_penerima + FROM rumah_ibadah ri + LEFT JOIN log_bantuan lb ON lb.ibadah_id = ri.id + GROUP BY ri.id + ORDER BY total_log DESC + "); + while ($row = $perfRes->fetch_assoc()) $ibadahPerforma[] = $row; +} + +// ── Output ──────────────────────────────────────────────────────────────── +echo json_encode([ + 'status' => 'success', + 'role' => $user['role'], + 'ibadah_id' => $ibadahId, + 'stats' => [ + 'total_ibadah' => $totalIbadah, + 'total_miskin' => $totalMiskin, + 'total_jiwa' => $totalJiwa, + 'total_log' => $totalLog, + 'terurus' => $terurusCount, + 'tidak_terurus' => $tidakTerurusCount, + 'pct_terurus' => $totalMiskin > 0 ? round($terurusCount / $totalMiskin * 100, 1) : 0, + ], + 'distribusi' => [ + 'jenis_ibadah' => $jenisDist, + 'kategori' => $kategoriDist, + 'tipe_bantuan' => $tipeDist, + 'per_bulan' => $bantuanPerBulan, + ], + 'ibadah_list' => $ibadahList, + 'ibadah_performa' => $ibadahPerforma, + 'recent_logs' => $recentLogs, +]); diff --git a/api/penduduk_miskin/bulk_create.php b/api/penduduk_miskin/bulk_create.php index 2e2a65d..974d79a 100644 --- a/api/penduduk_miskin/bulk_create.php +++ b/api/penduduk_miskin/bulk_create.php @@ -4,11 +4,29 @@ require_once '../auth_helper.php'; requireAdminOrPengelola(); include_once '../db_connect.php'; +// Ambil info ibadah pengelola jika role pengelola +$currentUser = getCurrentUser(); +$ibadahFilter = null; // null = admin (tidak dibatasi) +if ($currentUser && $currentUser['role'] === 'pengelola') { + $ibadah_id = $currentUser['ibadah_id']; + if (!$ibadah_id) { + echo json_encode(['status' => 'error', 'message' => 'Akun pengelola tidak terhubung ke rumah ibadah manapun.']); + exit; + } + $res = $conn->query("SELECT lat, lng, radius FROM rumah_ibadah WHERE id=$ibadah_id"); + if (!$res || $res->num_rows === 0) { + echo json_encode(['status' => 'error', 'message' => 'Rumah ibadah yang dikelola tidak ditemukan.']); + exit; + } + $ibadahFilter = $res->fetch_assoc(); +} + $data = json_decode(file_get_contents("php://input")); if (is_array($data)) { $conn->begin_transaction(); $inserted = 0; + $skipped = 0; $errors = []; foreach ($data as $item) { if (isset($item->lat) && isset($item->lng) && $item->lat !== '' && $item->lng !== '') { @@ -18,6 +36,21 @@ if (is_array($data)) { $lat = (float)$item->lat; $lng = (float)$item->lng; + // Jika pengelola, cek apakah item berada dalam radius ibadahnya + if ($ibadahFilter !== null) { + $R = 6371000; + $lat1 = deg2rad((float)$ibadahFilter['lat']); + $lat2 = deg2rad($lat); + $dLat = $lat2 - $lat1; + $dLng = deg2rad($lng - (float)$ibadahFilter['lng']); + $a = sin($dLat/2)*sin($dLat/2) + cos($lat1)*cos($lat2)*sin($dLng/2)*sin($dLng/2); + $dist = $R * 2 * atan2(sqrt($a), sqrt(1-$a)); + if ($dist > (float)$ibadahFilter['radius']) { + $skipped++; + continue; // Lewati item di luar radius + } + } + $query = "INSERT INTO penduduk_miskin (nama, kategori_bantuan, jumlah_jiwa, lat, lng) VALUES ('$nama', '$kategori', $jumlah_jiwa, $lat, $lng)"; @@ -31,10 +64,15 @@ if (is_array($data)) { if (empty($errors) && $inserted > 0) { $conn->commit(); - echo json_encode(["status" => "success", "message" => "Berhasil mengimpor $inserted data penduduk miskin."]); + $msg = "Berhasil mengimpor $inserted data penduduk miskin."; + if ($skipped > 0) $msg .= " ($skipped data di luar radius diabaikan.)"; + echo json_encode(["status" => "success", "message" => $msg]); } else if ($inserted === 0) { $conn->rollback(); - echo json_encode(["status" => "error", "message" => "Tidak ada data valid yang diimpor."]); + $msg = $skipped > 0 + ? "Semua $skipped data berada di luar radius rumah ibadah Anda, tidak ada yang diimpor." + : "Tidak ada data valid yang diimpor."; + echo json_encode(["status" => "error", "message" => $msg]); } else { $conn->rollback(); echo json_encode(["status" => "error", "message" => "Gagal mengimpor beberapa data. Transaksi dibatalkan.", "errors" => $errors]); diff --git a/api/penduduk_miskin/create.php b/api/penduduk_miskin/create.php index 51f9ec9..85037bd 100644 --- a/api/penduduk_miskin/create.php +++ b/api/penduduk_miskin/create.php @@ -27,6 +27,38 @@ if (!empty($_POST)) { } } +// Validasi pengelola: hanya boleh tambah jika lokasi berada dalam radius ibadah yang dikelolanya +$currentUser = getCurrentUser(); +if ($currentUser && $currentUser['role'] === 'pengelola') { + $ibadah_id = $currentUser['ibadah_id']; + if (!$ibadah_id) { + http_response_code(403); + echo json_encode(['status' => 'error', 'message' => 'Akun pengelola tidak terhubung ke rumah ibadah manapun.']); + exit; + } + // Ambil data ibadah (lat, lng, radius) + $res = $conn->query("SELECT lat, lng, radius FROM rumah_ibadah WHERE id=$ibadah_id"); + if (!$res || $res->num_rows === 0) { + http_response_code(403); + echo json_encode(['status' => 'error', 'message' => 'Rumah ibadah yang dikelola tidak ditemukan.']); + exit; + } + $ib = $res->fetch_assoc(); + // Hitung jarak menggunakan Haversine formula + $R = 6371000; // meter + $lat1 = deg2rad((float)$ib['lat']); + $lat2 = deg2rad($lat); + $dLat = $lat2 - $lat1; + $dLng = deg2rad($lng - (float)$ib['lng']); + $a = sin($dLat/2)*sin($dLat/2) + cos($lat1)*cos($lat2)*sin($dLng/2)*sin($dLng/2); + $dist = $R * 2 * atan2(sqrt($a), sqrt(1-$a)); + if ($dist > (float)$ib['radius']) { + http_response_code(403); + echo json_encode(['status' => 'error', 'message' => 'Lokasi penduduk di luar radius rumah ibadah Anda. Hanya boleh menambah penduduk dalam radius yang dikelola.']); + exit; + } +} + if ($lat != 0 && $lng != 0) { $upload_dir = "../../uploads/"; if (!file_exists($upload_dir)) { diff --git a/api/penduduk_miskin/delete.php b/api/penduduk_miskin/delete.php index 839ef2f..b994db8 100644 --- a/api/penduduk_miskin/delete.php +++ b/api/penduduk_miskin/delete.php @@ -9,6 +9,49 @@ $data = json_decode(file_get_contents("php://input")); if(!empty($data->id)) { $id = (int)$data->id; + // Validasi pengelola: hanya boleh hapus penduduk dalam radius ibadah yang dikelolanya + $currentUser = getCurrentUser(); + if ($currentUser && $currentUser['role'] === 'pengelola') { + $ibadah_id = $currentUser['ibadah_id']; + if (!$ibadah_id) { + http_response_code(403); + echo json_encode(['status' => 'error', 'message' => 'Akun pengelola tidak terhubung ke rumah ibadah manapun.']); + exit; + } + // Ambil koordinat penduduk + $mRes = $conn->query("SELECT lat, lng FROM penduduk_miskin WHERE id=$id"); + if (!$mRes || $mRes->num_rows === 0) { + echo json_encode(['status' => 'error', 'message' => 'Data penduduk tidak ditemukan.']); + exit; + } + $mRow = $mRes->fetch_assoc(); + $pLat = (float)$mRow['lat']; + $pLng = (float)$mRow['lng']; + + // Ambil data ibadah + $iRes = $conn->query("SELECT lat, lng, radius FROM rumah_ibadah WHERE id=$ibadah_id"); + if (!$iRes || $iRes->num_rows === 0) { + http_response_code(403); + echo json_encode(['status' => 'error', 'message' => 'Rumah ibadah yang dikelola tidak ditemukan.']); + exit; + } + $ib = $iRes->fetch_assoc(); + + // Hitung jarak Haversine + $R = 6371000; + $lat1 = deg2rad((float)$ib['lat']); + $lat2 = deg2rad($pLat); + $dLat = $lat2 - $lat1; + $dLng = deg2rad($pLng - (float)$ib['lng']); + $a = sin($dLat/2)*sin($dLat/2) + cos($lat1)*cos($lat2)*sin($dLng/2)*sin($dLng/2); + $dist = $R * 2 * atan2(sqrt($a), sqrt(1-$a)); + if ($dist > (float)$ib['radius']) { + http_response_code(403); + echo json_encode(['status' => 'error', 'message' => 'Penduduk ini berada di luar radius rumah ibadah Anda. Tidak diizinkan menghapus.']); + exit; + } + } + $query = "DELETE FROM penduduk_miskin WHERE id=$id"; if($conn->query($query)) { diff --git a/api/penduduk_miskin/update.php b/api/penduduk_miskin/update.php index 532718e..2f8539c 100644 --- a/api/penduduk_miskin/update.php +++ b/api/penduduk_miskin/update.php @@ -30,6 +30,47 @@ if (!empty($_POST)) { } } +// Validasi pengelola: hanya boleh edit penduduk yang lokasinya dalam radius ibadah yang dikelolanya +$currentUser = getCurrentUser(); +if ($currentUser && $currentUser['role'] === 'pengelola') { + $ibadah_id = $currentUser['ibadah_id']; + if (!$ibadah_id) { + http_response_code(403); + echo json_encode(['status' => 'error', 'message' => 'Akun pengelola tidak terhubung ke rumah ibadah manapun.']); + exit; + } + $res = $conn->query("SELECT lat, lng, radius FROM rumah_ibadah WHERE id=$ibadah_id"); + if (!$res || $res->num_rows === 0) { + http_response_code(403); + echo json_encode(['status' => 'error', 'message' => 'Rumah ibadah yang dikelola tidak ditemukan.']); + exit; + } + $ib = $res->fetch_assoc(); + // Gunakan koordinat penduduk yang sedang diedit (dari DB jika lat/lng 0, atau dari input) + $checkLat = $lat; + $checkLng = $lng; + if ($checkLat == 0 && $checkLng == 0 && $id > 0) { + $mRes = $conn->query("SELECT lat, lng FROM penduduk_miskin WHERE id=$id"); + if ($mRes && $mRow = $mRes->fetch_assoc()) { + $checkLat = (float)$mRow['lat']; + $checkLng = (float)$mRow['lng']; + } + } + $R = 6371000; + $lat1 = deg2rad((float)$ib['lat']); + $lat2 = deg2rad($checkLat); + $dLat = $lat2 - $lat1; + $dLng = deg2rad($checkLng - (float)$ib['lng']); + $a = sin($dLat/2)*sin($dLat/2) + cos($lat1)*cos($lat2)*sin($dLng/2)*sin($dLng/2); + $dist = $R * 2 * atan2(sqrt($a), sqrt(1-$a)); + if ($dist > (float)$ib['radius']) { + http_response_code(403); + echo json_encode(['status' => 'error', 'message' => 'Penduduk ini berada di luar radius rumah ibadah Anda. Tidak diizinkan mengedit.']); + exit; + } +} + + if ($id > 0) { // Get current files to delete them if updated $old_foto_rumah = null; diff --git a/assets/css/style.css b/assets/css/style.css index 7dd697c..385032c 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -22,8 +22,8 @@ body, html { .ui-container { position: absolute; top: 20px; - left: 50%; - transform: translateX(-50%); + left: 30%; + transform: translateX(-70%); z-index: 1000; width: 100%; max-width: 400px; @@ -1020,3 +1020,268 @@ body.right-panel-open .auth-widget { border-bottom: 1px solid #e2e8f0; } + +/* ============================================================ + DASHBOARD STYLES + ============================================================ */ + +#dashboardModal .modal-content { + width: min(920px, 96vw); + max-height: 90vh; + overflow: hidden; + display: flex; + flex-direction: column; +} + +#dashboardModal .modal-header { + background: linear-gradient(135deg, #1e1b4b 0%, #312e81 50%, #4338ca 100%); + border-radius: 12px 12px 0 0; + padding: 18px 20px; + flex-shrink: 0; +} + +#dashboardModal .modal-header h3 { + color: white; + font-size: 17px; + font-weight: 700; + letter-spacing: 0.3px; +} + +#dashboardModal .modal-close { + color: rgba(255,255,255,0.7); + font-size: 22px; + transition: color 0.2s; +} +#dashboardModal .modal-close:hover { color: white; } + +#dashboardBody { + flex: 1; + overflow-y: auto; + padding: 20px; + display: flex; + flex-direction: column; + gap: 20px; + background: #f1f5f9; +} + +.db-loading { + display: flex; + align-items: center; + justify-content: center; + gap: 14px; + padding: 60px 0; + color: #6b7280; + font-size: 14px; +} + +.db-spinner { + width: 28px; + height: 28px; + border: 3px solid #e2e8f0; + border-top-color: #6366f1; + border-radius: 50%; + animation: dbSpin 0.7s linear infinite; +} +@keyframes dbSpin { to { transform: rotate(360deg); } } + +.db-error { + text-align: center; + color: #ef4444; + padding: 40px; + font-size: 14px; +} + +.db-ibadah-header { + background: linear-gradient(135deg, #312e81, #4338ca); + border-radius: 14px; + padding: 16px 20px; + display: flex; + align-items: center; + gap: 16px; + color: white; + box-shadow: 0 4px 20px rgba(67,56,202,0.3); +} + +.db-ibadah-emoji { font-size: 36px; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2)); } +.db-ibadah-name { font-size: 18px; font-weight: 700; } +.db-ibadah-meta { font-size: 12px; color: rgba(255,255,255,0.75); margin-top: 3px; } + +.db-kpi-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 14px; +} + +.db-kpi-card { + background: white; + border-radius: 16px; + padding: 18px 16px; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + box-shadow: 0 2px 8px rgba(0,0,0,0.06); + transition: transform 0.2s, box-shadow 0.2s; + position: relative; + overflow: hidden; +} + +.db-kpi-card::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 4px; + border-radius: 16px 16px 0 0; +} + +.kpi-ibadah::before { background: linear-gradient(90deg, #f97316, #ea580c); } +.kpi-miskin::before { background: linear-gradient(90deg, #ef4444, #dc2626); } +.kpi-jiwa::before { background: linear-gradient(90deg, #6366f1, #4f46e5); } +.kpi-log::before { background: linear-gradient(90deg, #22c55e, #16a34a); } + +.db-kpi-card:hover { transform: translateY(-3px); box-shadow: 0 8px 24px rgba(0,0,0,0.1); } + +.db-kpi-icon { font-size: 28px; line-height: 1; margin-bottom: 2px; } +.db-kpi-val { font-size: 32px; font-weight: 800; color: #1e1e2e; line-height: 1; } +.db-kpi-label { font-size: 11px; font-weight: 600; color: #6b7280; text-align: center; text-transform: uppercase; letter-spacing: 0.4px; } + +.db-section { + background: white; + border-radius: 14px; + padding: 16px 18px; + box-shadow: 0 2px 8px rgba(0,0,0,0.06); +} + +.db-section-title { + font-size: 13px; + font-weight: 700; + color: #374151; + margin-bottom: 12px; + letter-spacing: 0.2px; +} + +.db-coverage-labels { + display: flex; + justify-content: space-between; + font-size: 13px; + color: #374151; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 8px; +} + +.db-progress-bar { + height: 12px; + background: #fee2e2; + border-radius: 99px; + overflow: hidden; +} + +.db-progress-fill { + height: 100%; + border-radius: 99px; + transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1); +} + +.db-charts-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 14px; +} + +.db-chart-card { + background: white; + border-radius: 14px; + padding: 14px 16px; + box-shadow: 0 2px 8px rgba(0,0,0,0.06); +} + +.db-chart-wide { grid-column: 1 / -1; } + +.db-chart-title { + font-size: 11px; + font-weight: 700; + color: #374151; + margin-bottom: 12px; + text-transform: uppercase; + letter-spacing: 0.4px; +} + +.db-chart-card canvas { width: 100% !important; display: block; } + +.db-log-list { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 260px; + overflow-y: auto; +} + +.db-log-item { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 10px 12px; + background: #f8fafc; + border-radius: 10px; + transition: background 0.15s; +} + +.db-log-item:hover { background: #f1f5f9; } + +.db-log-dot { + width: 8px; height: 8px; + border-radius: 50%; + background: #6366f1; + flex-shrink: 0; + margin-top: 5px; +} + +.db-log-info { flex: 1; min-width: 0; } + +.db-log-main { + font-size: 12px; + font-weight: 600; + color: #1e1e2e; + display: flex; + gap: 6px; + flex-wrap: wrap; + align-items: center; +} + +.db-log-tipe { + background: #ede9fe; + color: #4f46e5; + padding: 1px 7px; + border-radius: 99px; + font-size: 11px; + font-weight: 700; +} + +.db-log-kk { color: #374151; } + +.db-log-meta { font-size: 10px; color: #9ca3af; margin-top: 3px; } + +.db-table-wrap { + overflow-x: auto; + border-radius: 8px; + border: 1px solid #e5e7eb; +} + +.db-table { width: 100%; border-collapse: collapse; font-size: 13px; } +.db-table thead tr { background: #f8fafc; } +.db-table th { padding: 10px 14px; text-align: left; font-size: 11px; font-weight: 700; color: #6b7280; text-transform: uppercase; letter-spacing: 0.4px; border-bottom: 1px solid #e5e7eb; } +.db-table td { padding: 10px 14px; color: #374151; border-bottom: 1px solid #f3f4f6; } +.db-table tbody tr:last-child td { border-bottom: none; } +.db-table tbody tr:hover td { background: #f8fafc; } + +.db-badge { + background: linear-gradient(135deg, #6366f1, #4f46e5); + color: white; + padding: 2px 9px; + border-radius: 99px; + font-size: 11px; + font-weight: 700; +} + +.db-empty { text-align: center; color: #aaa; font-size: 13px; padding: 20px; } diff --git a/assets/js/features/auth.js b/assets/js/features/auth.js index 77fe7a4..6d76deb 100644 --- a/assets/js/features/auth.js +++ b/assets/js/features/auth.js @@ -52,6 +52,8 @@ function updateAuthUI() { if (!authWidget) return; + const dashboardBtn = document.getElementById('menuDashboard'); + if (window.currentUser) { // Logged In state const roleLabel = window.currentUser.role === 'admin' ? 'Admin' : 'Pengelola'; @@ -71,6 +73,9 @@ document.getElementById('authLogoutBtn').addEventListener('click', logout); + // Show Dashboard button for all logged-in users + if (dashboardBtn) dashboardBtn.style.display = 'flex'; + // Show user management button in sidebar for Admin const adminDivider = document.getElementById('sidebarAdminDivider'); if (window.currentUser.role === 'admin') { @@ -87,6 +92,7 @@ `; document.getElementById('authLoginBtn').addEventListener('click', showLoginModal); if (menuUsersBtn) menuUsersBtn.style.display = 'none'; + if (dashboardBtn) dashboardBtn.style.display = 'none'; const adminDivider = document.getElementById('sidebarAdminDivider'); if (adminDivider) adminDivider.style.display = 'none'; } @@ -177,6 +183,14 @@ }); } + // Dashboard Button + const menuDashboardBtn = document.getElementById('menuDashboard'); + if (menuDashboardBtn) { + menuDashboardBtn.addEventListener('click', function() { + if (typeof window.openDashboard === 'function') window.openDashboard(); + }); + } + if (closeUserManagementModal) { closeUserManagementModal.addEventListener('click', function() { userManagementModal.classList.remove('show'); diff --git a/assets/js/features/dashboard.js b/assets/js/features/dashboard.js new file mode 100644 index 0000000..92aae21 --- /dev/null +++ b/assets/js/features/dashboard.js @@ -0,0 +1,350 @@ +// ===== Dashboard Feature ===== +// Dashboard informatif untuk Admin dan Pengelola Rumah Ibadah + +(function () { + 'use strict'; + + // ── State ────────────────────────────────────────────────────────────── + let dashboardData = null; + let chartInstances = {}; + + // ── Modal Element ────────────────────────────────────────────────────── + const modal = document.getElementById('dashboardModal'); + if (!modal) return; + + // ── Open / Close ─────────────────────────────────────────────────────── + window.openDashboard = function () { + modal.classList.add('show'); + loadDashboard(); + }; + + window.closeDashboard = function () { + modal.classList.remove('show'); + }; + + document.getElementById('closeDashboardModal')?.addEventListener('click', window.closeDashboard); + modal.addEventListener('click', function (e) { + if (e.target === modal) window.closeDashboard(); + }); + + // ── Load Data ────────────────────────────────────────────────────────── + function loadDashboard() { + const body = document.getElementById('dashboardBody'); + body.innerHTML = ` +
| # | +Nama | +Jenis | +Total Log | +Penerima Unik | +
|---|---|---|---|---|
| ${i + 1} | +${esc(ib.nama)} | +${esc(ib.jenis)} | +${ib.total_log} | +${ib.total_penerima} | +
Anda hanya dapat menambah penduduk miskin di dalam radius rumah ibadah yang Anda kelola.
+| Nama | +Kepala Keluarga | Kategori | Jiwa | Koordinat | @@ -845,7 +873,7 @@ function handleImportFileChange(e) { parsedImportData = validRows; if (validRows.length === 0) { - errorMsg.textContent = "Tidak ditemukan data valid (pastikan kolom nama, lat, dan lng terisi)."; + errorMsg.textContent = "Tidak ditemukan data valid (pastikan kolom nama_kk/nama, lat, dan lng terisi)."; errorMsg.style.display = 'block'; return; } diff --git a/assets/js/panel.js b/assets/js/panel.js index b9dc6bf..7eeab4b 100644 --- a/assets/js/panel.js +++ b/assets/js/panel.js @@ -165,9 +165,13 @@ rightPanelTitle.textContent = '🏠 Penduduk Miskin'; const canManage = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola')); if (canManage) { + const isPengelola = window.currentUser.role === 'pengelola'; + const addLabel = isPengelola + ? ' Tambah (dalam radius)' + : ' Tambah'; rightPanelActions.innerHTML = `
|---|