diff --git a/webgis-poverty-pontianak.zip b/webgis-poverty-pontianak.zip index 83ce69f..805b993 100644 Binary files a/webgis-poverty-pontianak.zip and b/webgis-poverty-pontianak.zip differ diff --git a/webgis-poverty-pontianak/README.md b/webgis-poverty-pontianak/README.md deleted file mode 100644 index 5ba2575..0000000 --- a/webgis-poverty-pontianak/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# WebGIS Pemetaan Kemiskinan Kota Pontianak - -Aplikasi WebGIS sederhana berbasis Leaflet.js untuk memetakan data kemiskinan, sebaran penduduk miskin, log bantuan, dan rumah ibadah di Kota Pontianak. - -> [!WARNING] -> **DISCLAIMER — DATA DUMMY / BUKAN DATA NYATA** -> -> Seluruh data yang ditampilkan dalam aplikasi ini, termasuk namun tidak terbatas pada: -> - Data nama, lokasi, dan kategori **penduduk miskin** -> - **Foto rumah** dan **Foto Kartu Keluarga (KK)** yang diunggah -> - Data **rumah ibadah** beserta radius jangkauannya -> - **Log bantuan** yang tercatat di sistem -> -> adalah **data fiktif / dummy** yang dibuat semata-mata untuk keperluan **demonstrasi, pengembangan, dan pengujian sistem** ini. -> Data tersebut **tidak mewakili kondisi, identitas, atau situasi nyata** dari individu maupun lokasi mana pun di Kota Pontianak. - -## Fitur Utama - -- **Pemetaan Interaktif**: Visualisasi peta dengan Leaflet.js yang berpusat di Kota Pontianak. -- **Manajemen Data Spasial**: - - **Rumah Ibadah**: Lokasi titik rumah ibadah (Masjid, Gereja, Vihara, dll) beserta jangkauan radius jemaah/bantuan. - - **Penduduk Miskin**: Pemetaan warga kurang mampu dan integrasi log/riwayat bantuan sosial yang disalurkan melalui rumah ibadah terdekat. -- **Pencarian Cepat**: Cari data rumah ibadah langsung dari bilah pencarian. -- **Impor GeoJSON Eksternal**: Pengguna dapat menambahkan file GeoJSON dari perangkat lokal ke dalam peta. - -## Persyaratan Sistem - -- **Server Web**: Apache (misal menggunakan XAMPP atau Laragon). -- **Bahasa Pemrograman**: PHP 7.4 ke atas. -- **Database**: MySQL versi 5.7+ atau MariaDB versi 10.2+ (Wajib mendukung fitur fungsi spasial seperti `ST_GeomFromGeoJSON` dan `ST_AsGeoJSON`). - -## Cara Instalasi & Penggunaan - -1. **Unduh/Clone Repositori**: - Tempatkan folder `webgis-poverty-pontianak` di dalam direktori root server web Anda (misalnya `C:/xampp/htdocs/webgis/webgis-poverty-pontianak`). - -2. **Setup Database**: - - Buka phpMyAdmin atau MySQL client pilihan Anda. - - Buat database baru bernama `webgis`: - ```sql - CREATE DATABASE webgis; - ``` - - Import file `database.sql` ke dalam database `webgis` tersebut. - -3. **Konfigurasi Koneksi**: - - Buka file [api/db_connect.php](api/db_connect.php). - - Sesuaikan konfigurasi host, user, password, dan database sesuai dengan setup database MySQL lokal Anda: - ```php - $host = "localhost"; - $user = "root"; - $pass = ""; - $db = "webgis"; - ``` - -4. **Jalankan Aplikasi**: - - Aktifkan modul **Apache** dan **MySQL** di Control Panel XAMPP Anda. - - Buka browser dan navigasikan ke alamat: - [http://localhost/webgis/webgis-poverty-pontianak/index.php](http://localhost/webgis/webgis-poverty-pontianak/index.php) - -## Akun Akses Default (Login) - -Anda dapat masuk ke dalam sistem menggunakan akun simulasi berikut: - -- **Akun Administrator (Admin)**: - - **Username**: `admin` - - **Password**: `password` - - **Hak Akses**: Mengelola data spasial rumah ibadah, penduduk miskin, mengelola akun pengelola rumah ibadah, dan manajemen pengguna secara penuh. - -- **Akun Pengelola Rumah Ibadah**: - - **Username**: `pengelola_mujahidin` - - **Password**: `password` - - **Hak Akses**: Mengelola (tambah, edit, hapus, impor, geser lokasi) data penduduk miskin, serta menginput log bantuan sosial yang disalurkan melalui masjid kelolaannya (Masjid Raya Mujahidin). - -## Struktur Folder - -- `api/` — Kumpulan REST API endpoint untuk operasi CRUD masing-masing fitur. - - `rumah_ibadah/`, `penduduk_miskin/`, `log_bantuan/` -- `assets/` — File aset frontend. - - `css/` — Gaya UI (custom style.css). - - `js/` — Logika frontend peta dan pengelolaan interaksi panel data. - - `features/` — Modul fitur Leaflet (manajemen layer, geolokasi, dll). -- `index.php` — Halaman utama interface WebGIS. -- `database.sql` — Skema database beserta **data dummy** Pontianak untuk pengetesan awal. Seluruh data di dalamnya bersifat fiktif dan tidak merepresentasikan data nyata. -- `uploads/` — Folder penyimpanan foto yang diunggah. File yang sudah ada merupakan contoh dummy semata. diff --git a/webgis-poverty-pontianak/api/dashboard/stats.php b/webgis-poverty-pontianak/api/dashboard/stats.php deleted file mode 100644 index f058620..0000000 --- a/webgis-poverty-pontianak/api/dashboard/stats.php +++ /dev/null @@ -1,196 +0,0 @@ -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/webgis-poverty-pontianak/api/dashboard/stats_extended.php b/webgis-poverty-pontianak/api/dashboard/stats_extended.php deleted file mode 100644 index 5ce391c..0000000 --- a/webgis-poverty-pontianak/api/dashboard/stats_extended.php +++ /dev/null @@ -1,171 +0,0 @@ -connect_error) return null; - $c->set_charset("utf8mb4"); - return $c; -} - -// ── Koneksi ke masing-masing database ──────────────────────────────────── -$connPoverty = makeConn("webgis_poverty"); // atau "webgis" sesuai db.sql -$connJalan = makeConn("jalan"); -$connSpbu = makeConn("spbu"); - -// ── 1. Data dari webgis_poverty (kemiskinan) ───────────────────────────── -$poverty = [ - 'total_miskin' => 0, - 'total_jiwa' => 0, - 'total_ibadah' => 0, - 'total_log' => 0, - 'terurus' => 0, - 'tidak_terurus' => 0, - 'pct_terurus' => 0, - 'distribusi' => ['kategori' => [], 'tipe_bantuan' => [], 'per_bulan' => []], -]; - -if ($connPoverty) { - // Total KK miskin & jiwa - $r = $connPoverty->query("SELECT COUNT(*) as c, COALESCE(SUM(jumlah_jiwa),0) as j FROM penduduk_miskin"); - if ($r && $row = $r->fetch_assoc()) { - $poverty['total_miskin'] = (int)$row['c']; - $poverty['total_jiwa'] = (int)$row['j']; - } - - // Total rumah ibadah - $r = $connPoverty->query("SELECT COUNT(*) as c FROM rumah_ibadah"); - if ($r && $row = $r->fetch_assoc()) $poverty['total_ibadah'] = (int)$row['c']; - - // Total log bantuan - $r = $connPoverty->query("SELECT COUNT(*) as c FROM log_bantuan"); - if ($r && $row = $r->fetch_assoc()) $poverty['total_log'] = (int)$row['c']; - - // Kategori bantuan - $r = $connPoverty->query("SELECT kategori_bantuan, COUNT(*) as c FROM penduduk_miskin GROUP BY kategori_bantuan"); - if ($r) while ($row = $r->fetch_assoc()) $poverty['distribusi']['kategori'][$row['kategori_bantuan']] = (int)$row['c']; - - // Tipe bantuan dari log - $r = $connPoverty->query("SELECT tipe_bantuan, COUNT(*) as c FROM log_bantuan GROUP BY tipe_bantuan"); - if ($r) while ($row = $r->fetch_assoc()) $poverty['distribusi']['tipe_bantuan'][$row['tipe_bantuan']] = (int)$row['c']; - - // Bantuan per bulan (6 bulan terakhir) - $r = $connPoverty->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 - "); - if ($r) while ($row = $r->fetch_assoc()) $poverty['distribusi']['per_bulan'][] = $row; - - // Terurus vs tidak (radius sederhana: ada di radius salah satu ibadah) - $ibadahRes = $connPoverty->query("SELECT lat, lng, radius FROM rumah_ibadah"); - $ibadahArr = []; - if ($ibadahRes) while ($row = $ibadahRes->fetch_assoc()) $ibadahArr[] = $row; - - $miskinRes = $connPoverty->query("SELECT lat, lng FROM penduduk_miskin"); - if ($miskinRes) { - while ($m = $miskinRes->fetch_assoc()) { - $inRadius = false; - foreach ($ibadahArr as $ib) { - $dlat = deg2rad($ib['lat'] - $m['lat']); - $dlng = deg2rad($ib['lng'] - $m['lng']); - $a = sin($dlat/2)**2 + cos(deg2rad($m['lat']))*cos(deg2rad($ib['lat']))*sin($dlng/2)**2; - $dist = 6371000 * 2 * atan2(sqrt($a), sqrt(1-$a)); - if ($dist <= (float)$ib['radius']) { $inRadius = true; break; } - } - if ($inRadius) $poverty['terurus']++; else $poverty['tidak_terurus']++; - } - $tot = $poverty['total_miskin']; - $poverty['pct_terurus'] = $tot > 0 ? round($poverty['terurus'] / $tot * 100, 1) : 0; - } - $connPoverty->close(); -} - -// ── 2. Data Jalan ───────────────────────────────────────────────────────── -$jalan = [ - 'total_jalan' => 0, - 'total_panjang_m' => 0, - 'per_status' => [], - 'total_parsil' => 0, - 'total_luas_m2' => 0, - 'per_kepemilikan' => [], -]; - -if ($connJalan) { - $r = $connJalan->query("SELECT COUNT(*) as c, COALESCE(SUM(panjang_meter),0) as p FROM jalan"); - if ($r && $row = $r->fetch_assoc()) { - $jalan['total_jalan'] = (int)$row['c']; - $jalan['total_panjang_m'] = (float)$row['p']; - } - - $r = $connJalan->query("SELECT status_jalan, COUNT(*) as c FROM jalan GROUP BY status_jalan"); - if ($r) while ($row = $r->fetch_assoc()) $jalan['per_status'][$row['status_jalan']] = (int)$row['c']; - - $r = $connJalan->query("SELECT COUNT(*) as c, COALESCE(SUM(luas_m2),0) as l FROM parsil_tanah"); - if ($r && $row = $r->fetch_assoc()) { - $jalan['total_parsil'] = (int)$row['c']; - $jalan['total_luas_m2'] = (float)$row['l']; - } - - $r = $connJalan->query("SELECT status_kepemilikan, COUNT(*) as c FROM parsil_tanah GROUP BY status_kepemilikan"); - if ($r) while ($row = $r->fetch_assoc()) $jalan['per_kepemilikan'][$row['status_kepemilikan']] = (int)$row['c']; - - $connJalan->close(); -} - -// ── 3. Data SPBU ────────────────────────────────────────────────────────── -$spbu = [ - 'total_spbu' => 0, - 'buka_24_jam' => 0, - 'tidak_24_jam' => 0, - 'list' => [], -]; - -if ($connSpbu) { - $r = $connSpbu->query("SELECT COUNT(*) as c FROM spbu"); - if ($r && $row = $r->fetch_assoc()) $spbu['total_spbu'] = (int)$row['c']; - - $r = $connSpbu->query("SELECT SUM(buka_24_jam=1) as buka, SUM(buka_24_jam=0) as tidak FROM spbu"); - if ($r && $row = $r->fetch_assoc()) { - $spbu['buka_24_jam'] = (int)$row['buka']; - $spbu['tidak_24_jam'] = (int)$row['tidak']; - } - - $r = $connSpbu->query("SELECT nama_spbu, nomor_wa, buka_24_jam, latitude, longitude FROM spbu ORDER BY nama_spbu"); - if ($r) while ($row = $r->fetch_assoc()) $spbu['list'][] = $row; - - $connSpbu->close(); -} - -// ── 4. Data Penduduk (choropleth — static dari pontianak.json) ──────────── -// Data kepadatan per kecamatan sudah ada di file JS, ringkasan di sini -$penduduk = [ - 'kecamatan' => [ - ['nama' => 'Pontianak Barat', 'density' => 9.331], - ['nama' => 'Pontianak Kota', 'density' => 8.102], - ['nama' => 'Pontianak Timur', 'density' => 9.196], - ['nama' => 'Pontianak Selatan', 'density' => 5.673], - ['nama' => 'Pontianak Utara', 'density' => 3.642], - ['nama' => 'Pontianak Tenggara', 'density' => 3.103], - ] -]; -$penduduk['total_kecamatan'] = count($penduduk['kecamatan']); -$penduduk['rata_density'] = round(array_sum(array_column($penduduk['kecamatan'],'density')) / $penduduk['total_kecamatan'], 3); -$penduduk['terpadat'] = $penduduk['kecamatan'][array_search(max(array_column($penduduk['kecamatan'],'density')), array_column($penduduk['kecamatan'],'density'))]['nama']; - -// ── Output ──────────────────────────────────────────────────────────────── -echo json_encode([ - 'status' => 'success', - 'poverty' => $poverty, - 'jalan' => $jalan, - 'spbu' => $spbu, - 'penduduk' => $penduduk, -], JSON_UNESCAPED_UNICODE); diff --git a/webgis-poverty-pontianak/api/penduduk_miskin/create.php b/webgis-poverty-pontianak/api/penduduk_miskin/create.php index 85037bd..c2a4978 100644 --- a/webgis-poverty-pontianak/api/penduduk_miskin/create.php +++ b/webgis-poverty-pontianak/api/penduduk_miskin/create.php @@ -59,42 +59,34 @@ if ($currentUser && $currentUser['role'] === 'pengelola') { } } -if ($lat != 0 && $lng != 0) { - $upload_dir = "../../uploads/"; - if (!file_exists($upload_dir)) { - mkdir($upload_dir, 0777, true); - } +// Handle file upload +$upload_dir = dirname(dirname(__DIR__)) . '/uploads/'; +if (!file_exists($upload_dir)) { + mkdir($upload_dir, 0777, true); +} - $foto_rumah = null; - $foto_kk = null; - - function uploadFile($fileKey, $upload_dir) { - if (isset($_FILES[$fileKey]) && $_FILES[$fileKey]['error'] === UPLOAD_ERR_OK) { - $fileTmpPath = $_FILES[$fileKey]['tmp_name']; - $fileName = $_FILES[$fileKey]['name']; - - $fileNameCmps = explode(".", $fileName); - $fileExtension = strtolower(end($fileNameCmps)); - - $allowedfileExtensions = array('jpg', 'jpeg', 'png', 'gif'); - if (in_array($fileExtension, $allowedfileExtensions)) { - $newFileName = uniqid() . '_' . preg_replace('/[^a-zA-Z0-9\._-]/', '_', $fileName); - $dest_path = $upload_dir . $newFileName; - - if (move_uploaded_file($fileTmpPath, $dest_path)) { - return $newFileName; - } +function uploadFileCreate($fileKey, $upload_dir) { + if (isset($_FILES[$fileKey]) && $_FILES[$fileKey]['error'] === UPLOAD_ERR_OK) { + $fileTmpPath = $_FILES[$fileKey]['tmp_name']; + $fileName = $_FILES[$fileKey]['name']; + $fileExtension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); + $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif']; + if (in_array($fileExtension, $allowedExtensions)) { + $newFileName = uniqid() . '_' . preg_replace('/[^a-zA-Z0-9\\._-]/', '_', $fileName); + if (move_uploaded_file($fileTmpPath, $upload_dir . $newFileName)) { + return $newFileName; } } - return null; } + return null; +} - $foto_rumah = uploadFile('foto_rumah', $upload_dir); - $foto_kk = uploadFile('foto_kk', $upload_dir); - - $val_foto_rumah = $foto_rumah ? "'$foto_rumah'" : "NULL"; - $val_foto_kk = $foto_kk ? "'$foto_kk'" : "NULL"; +$foto_rumah = uploadFileCreate('foto_rumah', $upload_dir); +$foto_kk = uploadFileCreate('foto_kk', $upload_dir); +$val_foto_rumah = $foto_rumah ? "'" . $conn->real_escape_string($foto_rumah) . "'" : "NULL"; +$val_foto_kk = $foto_kk ? "'" . $conn->real_escape_string($foto_kk) . "'" : "NULL"; +if ($lat != 0 && $lng != 0) { $query = "INSERT INTO penduduk_miskin (nama, kategori_bantuan, jumlah_jiwa, lat, lng, foto_rumah, foto_kk) VALUES ('$nama', '$kategori', $jumlah_jiwa, $lat, $lng, $val_foto_rumah, $val_foto_kk)"; diff --git a/webgis-poverty-pontianak/api/penduduk_miskin/update.php b/webgis-poverty-pontianak/api/penduduk_miskin/update.php index 2f8539c..85561be 100644 --- a/webgis-poverty-pontianak/api/penduduk_miskin/update.php +++ b/webgis-poverty-pontianak/api/penduduk_miskin/update.php @@ -81,7 +81,7 @@ if ($id > 0) { $old_foto_kk = $row['foto_kk']; } - $upload_dir = "../../uploads/"; + $upload_dir = dirname(dirname(__DIR__)) . '/uploads/'; if (!file_exists($upload_dir)) { mkdir($upload_dir, 0777, true); } diff --git a/webgis-poverty-pontianak/assets/css/style.css b/webgis-poverty-pontianak/assets/css/style.css index d6762c2..c478109 100644 --- a/webgis-poverty-pontianak/assets/css/style.css +++ b/webgis-poverty-pontianak/assets/css/style.css @@ -1,718 +1,827 @@ +/* ============================================================ + WebGIS Kemiskinan Pontianak — Redesign + Theme: Dark Sidebar + Clean Map UI + ============================================================ */ + /* Reset & Base */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; +* { margin: 0; padding: 0; box-sizing: border-box; } + +:root { + --primary: #2563eb; + --primary-dark: #1d4ed8; + --primary-light: #dbeafe; + --accent: #0ea5e9; + --danger: #ef4444; + --success: #10b981; + --warning: #f59e0b; + --sidebar-bg: #0f172a; + --sidebar-item: #1e293b; + --sidebar-active: #2563eb; + --sidebar-text: #94a3b8; + --sidebar-text-active: #ffffff; + --surface: #ffffff; + --surface-2: #f8fafc; + --border: #e2e8f0; + --text-primary: #0f172a; + --text-secondary: #64748b; + --shadow-sm: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06); + --shadow-md: 0 4px 12px rgba(0,0,0,0.12); + --shadow-lg: 0 8px 30px rgba(0,0,0,0.16); + --radius: 10px; + --sidebar-w: 220px; } body, html { height: 100%; - font-family: 'Google Sans Flex', 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + font-family: 'Inter', 'Segoe UI', system-ui, sans-serif; overflow: hidden; + background: #0f172a; } -/* Map Container */ +/* ============================================================ + MAP + ============================================================ */ #map { - width: 100vw; - height: 100vh; + position: fixed; + top: 0; + left: var(--sidebar-w); + right: 0; + bottom: 0; z-index: 1; + transition: left 0.3s ease; } -/* Custom UI Container over Map */ -.ui-container { - position: absolute; - top: 20px; - left: 30%; - transform: translateX(-70%); - z-index: 1000; - width: 100%; - max-width: 400px; +body.sidebar-collapsed #map { left: 64px; } + +/* ============================================================ + LEFT SIDEBAR (Vertical Nav) + ============================================================ */ +.left-sidebar { + position: fixed; + top: 0; + left: 0; + width: var(--sidebar-w); + height: 100%; + background: var(--sidebar-bg); + z-index: 1100; display: flex; flex-direction: column; - gap: 10px; - padding: 0 15px; + transition: width 0.3s ease; + overflow: hidden; } -/* Search Bar */ -.search-bar { +body.sidebar-collapsed .left-sidebar { width: 64px; } + +/* Brand */ +.sidebar-brand { display: flex; align-items: center; - background-color: white; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - overflow: hidden; - height: 48px; - transition: box-shadow 0.3s; + gap: 12px; + padding: 18px 16px; + border-bottom: 1px solid rgba(255,255,255,0.07); + flex-shrink: 0; } -.search-bar:focus-within { - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25); -} - -.search-icon { - padding: 0 15px; - color: #666; +.sidebar-brand-icon { + width: 36px; + height: 36px; + background: var(--primary); + border-radius: 10px; display: flex; align-items: center; justify-content: center; + font-size: 18px; + flex-shrink: 0; +} + +.sidebar-brand-text { flex: 1; overflow: hidden; } +.sidebar-brand-title { + font-size: 13px; + font-weight: 700; + color: #f1f5f9; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.sidebar-brand-sub { + font-size: 10px; + color: var(--sidebar-text); + white-space: nowrap; +} + +/* Nav Items */ +.sidebar-nav { + flex: 1; + padding: 12px 8px; + display: flex; + flex-direction: column; + gap: 2px; + overflow-y: auto; +} + +.sidebar-nav-label { + font-size: 9px; + font-weight: 700; + color: #334155; + text-transform: uppercase; + letter-spacing: 0.8px; + padding: 8px 10px 4px; + white-space: nowrap; + overflow: hidden; +} + +.sidebar-btn { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 10px; + border-radius: 8px; + border: none; + background: transparent; + color: var(--sidebar-text); + cursor: pointer; + text-align: left; + width: 100%; + transition: all 0.15s; + white-space: nowrap; + font-family: inherit; +} + +.sidebar-btn:hover { + background: var(--sidebar-item); + color: #e2e8f0; +} + +.sidebar-btn.active { + background: var(--sidebar-active); + color: white; +} + +.sidebar-btn .btn-icon { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + font-size: 16px; + flex-shrink: 0; + background: rgba(255,255,255,0.05); +} + +.sidebar-btn.active .btn-icon { + background: rgba(255,255,255,0.2); +} + +.btn-label { + font-size: 13px; + font-weight: 500; + flex: 1; +} + +/* Sidebar Footer */ +.sidebar-footer { + padding: 12px 8px; + border-top: 1px solid rgba(255,255,255,0.07); +} + +/* Collapse Toggle */ +.sidebar-collapse-btn { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 10px; + border-radius: 8px; + border: none; + background: transparent; + color: var(--sidebar-text); + cursor: pointer; + width: 100%; + font-family: inherit; + transition: all 0.15s; +} + +.sidebar-collapse-btn:hover { + background: var(--sidebar-item); + color: #e2e8f0; +} + +.collapse-icon { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 14px; +} + +.collapse-label { + font-size: 13px; + font-weight: 500; +} + +/* ============================================================ + TOP BAR (search + controls) + ============================================================ */ +.top-bar { + position: fixed; + top: 14px; + left: calc(var(--sidebar-w) + 14px); + right: 14px; + z-index: 1000; + display: flex; + align-items: center; + gap: 10px; + transition: left 0.3s ease; + pointer-events: none; +} + +body.sidebar-collapsed .top-bar { left: 78px; } + +.top-bar > * { pointer-events: auto; } + +/* Search */ +.search-bar { + flex: 1; + max-width: 380px; + display: flex; + align-items: center; + background: white; + border-radius: var(--radius); + box-shadow: var(--shadow-md); + height: 44px; + overflow: hidden; + border: 1.5px solid transparent; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.search-bar:focus-within { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(37,99,235,0.12), var(--shadow-md); +} + +.search-icon { + padding: 0 12px; + color: #94a3b8; + font-size: 14px; + display: flex; + align-items: center; } .search-input { flex: 1; border: none; outline: none; - font-size: 16px; - padding: 10px 0; - color: #333; + font-size: 14px; + color: var(--text-primary); + font-family: inherit; + background: transparent; } +.search-input::placeholder { color: #94a3b8; } + .search-clear { - padding: 0 15px; - color: #999; + padding: 0 12px; + color: #cbd5e1; cursor: pointer; background: none; border: none; - display: flex; - align-items: center; - justify-content: center; visibility: hidden; + font-size: 14px; } -.search-clear:hover { - color: #333; +.search-clear:hover { color: #64748b; } + +#searchResults { + position: absolute; + top: calc(100% + 6px); + left: 0; + right: 0; + max-width: 380px; + background: white; + border-radius: var(--radius); + box-shadow: var(--shadow-lg); + max-height: 220px; + overflow-y: auto; + border: 1px solid var(--border); + z-index: 1001; } -/* Custom Layer Control Button */ -.custom-layer-btn { - background-color: white; - border-radius: 8px; - width: 48px; - height: 48px; +.search-result-item { + padding: 10px 14px; + cursor: pointer; + font-size: 13px; + color: var(--text-primary); + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + gap: 8px; + transition: background 0.1s; +} + +.search-result-item:last-child { border-bottom: none; } +.search-result-item:hover { background: var(--primary-light); } + +/* Top Bar Buttons */ +.top-btn { + width: 44px; + height: 44px; + background: white; + border: none; + border-radius: var(--radius); + box-shadow: var(--shadow-md); display: flex; align-items: center; justify-content: center; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); cursor: pointer; - border: none; - color: #333; -} - -.custom-layer-btn:hover { - background-color: #f8f9fa; -} - -#layerBtn { - position: absolute; - top: 20px; - right: 20px; - z-index: 1000; - transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -body.right-panel-open #layerBtn { - right: 340px; -} - -.custom-layer-panel { - position: absolute; - top: 80px; - right: 20px; - z-index: 1000; - background-color: white; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - padding: 15px; - width: 250px; - display: none; - transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -body.right-panel-open .custom-layer-panel { - right: 340px; -} - - -.custom-layer-panel h3 { - margin-bottom: 10px; + color: var(--text-secondary); font-size: 16px; - border-bottom: 1px solid #eee; - padding-bottom: 5px; + transition: all 0.15s; + flex-shrink: 0; +} + +.top-btn:hover { + background: var(--primary); + color: white; + box-shadow: 0 4px 14px rgba(37,99,235,0.35); +} + +.top-btn.active { + background: var(--primary); + color: white; +} + +/* Auth button in top bar */ +#authWidget { + margin-left: auto; +} + +.btn-login { + background: var(--primary); + color: white; + border: none; + border-radius: var(--radius); + height: 44px; + padding: 0 18px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + box-shadow: var(--shadow-md); + display: flex; + align-items: center; + gap: 8px; + transition: all 0.15s; + font-family: inherit; +} + +.btn-login:hover { + background: var(--primary-dark); + box-shadow: 0 4px 14px rgba(37,99,235,0.4); +} + +.user-profile-pill { + background: white; + border-radius: var(--radius); + height: 44px; + padding: 0 8px 0 14px; + box-shadow: var(--shadow-md); + display: flex; + align-items: center; + gap: 10px; + border: 1px solid var(--border); +} + +.user-profile-info { display: flex; flex-direction: column; } +.user-profile-name { font-size: 12px; font-weight: 700; color: var(--text-primary); } +.user-profile-role { font-size: 9px; font-weight: 600; color: var(--primary); text-transform: uppercase; } + +.btn-profile-logout { + width: 30px; + height: 30px; + border-radius: 8px; + border: none; + background: var(--surface-2); + color: var(--text-secondary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s; +} + +.btn-profile-logout:hover { background: #fee2e2; color: var(--danger); } + +/* ============================================================ + ZOOM CONTROLS + ============================================================ */ +.zoom-controls { + position: fixed; + bottom: 80px; + right: 14px; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 4px; +} + +.zoom-btn { + width: 38px; + height: 38px; + background: white; + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: var(--shadow-sm); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + color: var(--text-secondary); + transition: all 0.15s; +} + +.zoom-btn:hover { background: var(--primary); color: white; border-color: var(--primary); } + +/* ============================================================ + LAYER PANEL + ============================================================ */ +.custom-layer-panel { + position: fixed; + top: 72px; + right: 14px; + z-index: 1000; + background: white; + border-radius: var(--radius); + box-shadow: var(--shadow-lg); + padding: 16px; + width: 240px; + display: none; + border: 1px solid var(--border); +} + +.custom-layer-panel.show { display: block; } + +.layer-panel-title { + font-size: 13px; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 14px; + display: flex; + align-items: center; + gap: 8px; } .layer-section-title { - font-size: 11px; + font-size: 10px; font-weight: 700; - color: #6366f1; + color: var(--primary); text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: 8px; - margin-top: 12px; - padding-bottom: 2px; - border-bottom: 1px dashed #e2e8f0; + letter-spacing: 0.6px; + margin: 12px 0 8px; + padding-bottom: 4px; + border-bottom: 1px solid var(--border); } .layer-option { display: flex; align-items: center; - gap: 10px; - margin-bottom: 8px; - font-size: 14px; -} - -/* ===== Sidebar Toggle Button ===== */ -.sidebar-toggle-btn { - position: absolute; - left: 20px; - top: 190px; - z-index: 1001; - border-radius: 8px; - width: 48px; - height: 48px; - background: white; - border: none; - box-shadow: 0 3px 10px rgba(0,0,0,0.15); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - color: #6366f1; - font-size: 13px; - transition: all 0.2s; -} - -.sidebar-toggle-btn:hover { - background: #6366f1; - color: white; - box-shadow: 0 4px 14px rgba(99,102,241,0.4); -} - -.sidebar-toggle-btn.open { - background: #6366f1; - color: white; -} - -/* ===== Left Sidebar ===== */ -.left-sidebar { - position: absolute; - left: 20px; - top: 250px; /* di bawah toggle button */ - z-index: 1000; - display: flex; - flex-direction: column; gap: 8px; -} - -.sidebar-section-label { - font-size: 9px; - font-weight: 700; - color: #94a3b8; - text-transform: uppercase; - text-align: center; - letter-spacing: 0.8px; - margin-top: 6px; - margin-bottom: 2px; - pointer-events: none; - user-select: none; -} - -.sidebar-divider { - height: 1px; - background: #e2e8f0; - margin: 4px 6px; -} - -.sidebar-btn { - width: 60px; - height: 60px; - background: white; - border: none; - border-radius: 12px; - box-shadow: 0 4px 12px rgba(0,0,0,0.12); + margin-bottom: 8px; + font-size: 13px; + color: var(--text-secondary); cursor: pointer; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 2px; - transition: all 0.2s ease; - padding: 6px 4px; } -.sidebar-btn:hover { - transform: translateX(3px); - box-shadow: 0 6px 18px rgba(0,0,0,0.18); - background: #f8f9fa; +.layer-option input[type="checkbox"] { + accent-color: var(--primary); + width: 15px; + height: 15px; } -.sidebar-btn.active { - background: linear-gradient(135deg, #6366f1, #4f46e5); - color: white; - box-shadow: 0 6px 18px rgba(99,102,241,0.4); - transform: translateX(4px); -} - -.sidebar-emoji { - font-size: 22px; - line-height: 1; -} - -.sidebar-label { - font-size: 9px; - font-weight: 600; - letter-spacing: 0.3px; - color: #555; - text-transform: uppercase; -} - -.sidebar-btn.active .sidebar-label { - color: rgba(255,255,255,0.85); -} - -/* ===== Right Panel ===== */ -.right-panel { - position: absolute; - top: 0; - right: 0; - bottom: 0; - width: 320px; - background: #f8f9fb; - z-index: 999; - box-shadow: -4px 0 20px rgba(0,0,0,0.12); - display: flex; - flex-direction: column; - transform: translateX(100%); - transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); - /* shift layer control when panel open */ -} - -.right-panel.open { - transform: translateX(0); -} - -.right-panel-header { - padding: 16px 16px 12px; - background: white; - border-bottom: 1px solid #eee; +.layer-group-header { display: flex; align-items: center; justify-content: space-between; +} + +.layer-toggle-icon { + cursor: pointer; + padding: 2px 6px; + color: #94a3b8; + font-size: 10px; + border-radius: 4px; + transition: background 0.15s; +} + +.layer-toggle-icon:hover { background: var(--surface-2); } +.layer-toggle-icon i { transition: transform 0.2s; } +.layer-toggle-icon.collapsed i { transform: rotate(-90deg); } + +.sub-layer-list { + padding-left: 16px; + margin-top: 4px; + border-left: 2px solid var(--primary-light); + overflow: hidden; +} + +.sub-option { font-size: 12px !important; margin-bottom: 4px !important; } + +/* GeoJSON section */ +.geojson-section { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid var(--border); +} + +.geojson-section-header { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); + cursor: pointer; + margin-bottom: 6px; +} + +/* ============================================================ + RIGHT PANEL (Data) + ============================================================ */ +.right-panel { + position: fixed; + top: 0; + right: -360px; + width: 340px; + height: 100%; + background: white; + z-index: 1050; + box-shadow: -4px 0 24px rgba(0,0,0,0.12); + display: flex; + flex-direction: column; + transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1); + border-left: 1px solid var(--border); +} + +.right-panel.open { right: 0; } + +.right-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 18px; + border-bottom: 1px solid var(--border); + background: white; flex-shrink: 0; - padding-top: 26px; } .right-panel-header h3 { font-size: 15px; font-weight: 700; - color: #1e1e2e; + color: var(--text-primary); + display: flex; + align-items: center; + gap: 8px; } .right-panel-close { - width: 30px; - height: 30px; + width: 32px; + height: 32px; border: none; - background: #f3f4f6; + background: var(--surface-2); border-radius: 8px; cursor: pointer; - color: #555; display: flex; align-items: center; justify-content: center; - font-size: 13px; - transition: background 0.2s; + color: var(--text-secondary); + font-size: 14px; + transition: all 0.15s; } -.right-panel-close:hover { - background: #e5e7eb; - color: #333; -} +.right-panel-close:hover { background: #fee2e2; color: var(--danger); } .right-panel-actions { - padding: 12px 14px; - background: white; - border-bottom: 1px solid #eee; + padding: 12px 18px; + border-bottom: 1px solid var(--border); display: flex; gap: 8px; + flex-wrap: wrap; flex-shrink: 0; } -.right-panel-actions button { - flex: 1; - padding: 9px 10px; - border: none; - border-radius: 8px; - font-size: 12px; - font-weight: 600; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - transition: all 0.2s; -} - -.btn-panel-add { - background: linear-gradient(135deg, #6366f1, #4f46e5); - color: white; - box-shadow: 0 3px 10px rgba(99,102,241,0.3); -} - -.btn-panel-add:hover { - box-shadow: 0 5px 14px rgba(99,102,241,0.45); - transform: translateY(-1px); -} - -.btn-panel-import { - background: #f0fdf4; - color: #16a34a; - border: 1px solid #bbf7d0 !important; -} - -.btn-panel-import:hover { - background: #dcfce7; -} - .right-panel-body { flex: 1; overflow-y: auto; - padding: 12px; - display: flex; - flex-direction: column; - gap: 8px; + padding: 14px 18px; } -/* Panel Section Heading */ -.panel-section-title { - font-size: 11px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.5px; - color: #9ca3af; - padding: 4px 2px 6px; - border-bottom: 1px solid #e5e7eb; - margin-top: 4px; -} - -/* Panel Empty State */ +/* Panel Items */ .panel-empty { text-align: center; - color: #aaa; + color: #94a3b8; font-size: 13px; - padding: 40px 10px; + padding: 40px 20px; } -/* ===== Data Card ===== */ -.data-card { - background: white; - border-radius: 12px; - padding: 12px 12px 10px; - box-shadow: 0 1px 4px rgba(0,0,0,0.07); - transition: box-shadow 0.2s; - cursor: pointer; -} - -.data-card:hover { - box-shadow: 0 3px 12px rgba(0,0,0,0.12); -} - -.data-card-main { +.panel-item { display: flex; - align-items: center; - gap: 10px; + align-items: flex-start; + gap: 12px; + padding: 12px; + border-radius: var(--radius); + cursor: pointer; + border: 1.5px solid transparent; + transition: all 0.15s; + margin-bottom: 8px; } -.data-card-icon { - width: 40px; - height: 40px; +.panel-item:hover { + background: var(--surface-2); + border-color: var(--border); +} + +.panel-item.active { + background: var(--primary-light); + border-color: var(--primary); +} + +.panel-item-icon { + width: 38px; + height: 38px; border-radius: 10px; + background: var(--surface-2); display: flex; align-items: center; justify-content: center; - font-size: 20px; + font-size: 18px; flex-shrink: 0; } -.card-icon-ibadah { background: #fff7ed; } -.card-icon-miskin-out { background: #f0fdf4; } -.card-icon-miskin-in { background: #fef2f2; } - -.data-card-info { - flex: 1; - min-width: 0; -} - -.data-card-name { +.panel-item-info { flex: 1; min-width: 0; } +.panel-item-name { font-size: 13px; font-weight: 600; - color: #1e1e2e; + color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.panel-item-meta { font-size: 11px; color: var(--text-secondary); margin-top: 2px; } -.data-card-sub { - font-size: 11px; - color: #6b7280; - margin-top: 2px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.data-card-badge { - font-size: 10px; - font-weight: 600; - padding: 2px 7px; - border-radius: 99px; - margin-top: 4px; - display: inline-block; -} - -.badge-green { background: #dcfce7; color: #16a34a; } -.badge-red { background: #fee2e2; color: #dc2626; } -.badge-blue { background: #dbeafe; color: #2563eb; } -.badge-orange { background: #ffedd5; color: #ea580c; } -.badge-yellow { background: #fef9c3; color: #ca8a04; } - -.data-card-actions { +.panel-item-actions { display: flex; - gap: 6px; + gap: 4px; flex-shrink: 0; - align-items: center; + opacity: 0; + transition: opacity 0.15s; } +.panel-item:hover .panel-item-actions { opacity: 1; } -.btn-card-action { - width: 30px; - height: 30px; +.panel-action-btn { + width: 28px; + height: 28px; border: none; border-radius: 7px; + background: white; + border: 1px solid var(--border); cursor: pointer; display: flex; align-items: center; justify-content: center; - font-size: 12px; + font-size: 11px; + color: var(--text-secondary); transition: all 0.15s; } -.btn-card-edit { - background: #eff6ff; - color: #3b82f6; -} +.panel-action-btn:hover { background: var(--primary); color: white; border-color: var(--primary); } +.panel-action-btn.danger:hover { background: var(--danger); color: white; border-color: var(--danger); } -.btn-card-edit:hover { - background: #3b82f6; - color: white; -} - -.btn-card-delete { - background: #fef2f2; - color: #ef4444; -} - -.btn-card-delete:hover { - background: #ef4444; - color: white; -} - -/* Expandable card (miskin) */ -.data-card-expand { - overflow: hidden; - max-height: 0; - transition: max-height 0.3s ease, padding 0.2s; - border-top: 0px solid #f3f4f6; -} - -.data-card.expanded .data-card-expand { - max-height: 400px; - border-top: 1px solid #f3f4f6; - margin-top: 10px; - padding-top: 10px; -} - -.expand-row { - display: flex; - justify-content: space-between; - font-size: 12px; - color: #6b7280; - padding: 3px 0; -} - -.expand-row span:first-child { - font-weight: 500; - color: #374151; -} - -.btn-log-bantuan { - margin-top: 10px; - width: 100%; - padding: 8px; - background: linear-gradient(135deg, #6366f1, #4f46e5); - color: white; - border: none; +/* Panel action buttons in header */ +.btn-panel-action { + height: 34px; + padding: 0 14px; border-radius: 8px; + border: 1px solid var(--border); + background: white; + color: var(--text-secondary); font-size: 12px; font-weight: 600; cursor: pointer; display: flex; align-items: center; - justify-content: center; gap: 6px; - transition: all 0.2s; + transition: all 0.15s; + font-family: inherit; } -.btn-log-bantuan:hover { - box-shadow: 0 4px 12px rgba(99,102,241,0.4); -} - -/* Log Bantuan Item */ -.log-item { - background: #f8f9fb; - border-radius: 8px; - padding: 10px 12px; - display: flex; - gap: 10px; - align-items: flex-start; -} - -.log-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: #6366f1; - flex-shrink: 0; - margin-top: 5px; -} - -.log-info { - flex: 1; -} - -.log-type { - font-size: 12px; - font-weight: 600; - color: #1e1e2e; -} - -.log-meta { - font-size: 11px; - color: #9ca3af; - margin-top: 2px; -} - -.log-keterangan { - font-size: 11px; - color: #6b7280; - margin-top: 3px; - font-style: italic; -} - -/* Popup Form */ -.popup-form { - display: flex; - flex-direction: column; - gap: 10px; - width: 200px; -} - -.popup-form label { - font-size: 12px; - font-weight: bold; - color: #555; -} - -.popup-form input[type="text"], -.popup-form input[type="number"] { - width: 100%; - padding: 6px; - border: 1px solid #ccc; - border-radius: 4px; -} - -.popup-form .radio-group { - display: flex; - gap: 10px; -} - -.popup-form button { - padding: 8px; - background-color: #007bff; +.btn-panel-action:hover { + background: var(--primary); color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-weight: bold; + border-color: var(--primary); } -.popup-form button.btn-danger { - background-color: #dc3545; +.btn-panel-action.primary { + background: var(--primary); + color: white; + border-color: var(--primary); } -.popup-form button:hover { - opacity: 0.9; +.btn-panel-action.primary:hover { + background: var(--primary-dark); } -/* Unified Modal */ +/* ============================================================ + MODALS + ============================================================ */ .unified-modal { display: none; position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0,0,0,0.5); + inset: 0; + background: rgba(15,23,42,0.55); z-index: 2000; align-items: center; justify-content: center; + backdrop-filter: blur(2px); } -.unified-modal.show { - display: flex; -} +.unified-modal.show { display: flex; } .modal-content { - background-color: white; - border-radius: 12px; - width: 400px; - max-width: 90%; - box-shadow: 0 8px 30px rgba(0,0,0,0.2); + background: white; + border-radius: 14px; + width: 420px; + max-width: 95vw; + box-shadow: var(--shadow-lg); display: flex; flex-direction: column; + max-height: 90vh; + overflow: hidden; } .modal-header { - padding: 15px; - border-bottom: 1px solid #eee; + padding: 16px 20px; + border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; + flex-shrink: 0; } .modal-header h3 { - font-size: 16px; - margin: 0; + font-size: 15px; + font-weight: 700; + color: var(--text-primary); } .modal-close { - font-size: 20px; - font-weight: bold; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; cursor: pointer; - color: #888; + color: var(--text-secondary); + font-size: 18px; + font-weight: 400; + border-radius: 7px; + background: var(--surface-2); + transition: all 0.15s; } -.modal-close:hover { - color: #333; -} +.modal-close:hover { background: #fee2e2; color: var(--danger); } .modal-body { - padding: 15px; + padding: 16px 20px; display: flex; flex-direction: column; - gap: 12px; + gap: 14px; + overflow-y: auto; + flex: 1; } -.modal-body label { - font-size: 13px; +.modal-footer { + padding: 14px 20px; + border-top: 1px solid var(--border); + display: flex; + justify-content: flex-end; + gap: 8px; + flex-shrink: 0; +} + +/* Form */ +.form-group { + display: flex; + flex-direction: column; + gap: 5px; +} + +.form-group label, .modal-body label { + font-size: 12px; font-weight: 600; - color: #444; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.3px; } .modal-body input[type="text"], @@ -722,140 +831,120 @@ body.right-panel-open .custom-layer-panel { .modal-body select, .modal-body textarea { width: 100%; - padding: 8px; - border: 1px solid #ccc; - border-radius: 6px; - font-size: 14px; + padding: 9px 12px; + border: 1.5px solid var(--border); + border-radius: 8px; + font-size: 13px; font-family: inherit; + color: var(--text-primary); + background: white; + transition: border-color 0.2s, box-shadow 0.2s; + outline: none; } -.modal-footer { - padding: 15px; - border-top: 1px solid #eee; - display: flex; - justify-content: flex-end; - gap: 10px; +.modal-body input:focus, +.modal-body select:focus, +.modal-body textarea:focus { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(37,99,235,0.1); } .btn-save { - background: linear-gradient(135deg, #6366f1, #4f46e5); + background: var(--primary); color: white; border: none; - padding: 8px 18px; - border-radius: 7px; + padding: 9px 20px; + border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 13px; + font-family: inherit; + transition: all 0.15s; } +.btn-save:hover { background: var(--primary-dark); } + .btn-cancel { - background-color: #f8f9fa; - border: 1px solid #ddd; - padding: 8px 18px; - border-radius: 7px; + background: var(--surface-2); + border: 1px solid var(--border); + padding: 9px 20px; + border-radius: 8px; cursor: pointer; font-size: 13px; + font-family: inherit; + color: var(--text-secondary); + transition: all 0.15s; } -.form-group { +.btn-cancel:hover { background: var(--border); } + +/* ============================================================ + TOAST + ============================================================ */ +#toastContainer { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + z-index: 9999; display: flex; flex-direction: column; - gap: 4px; -} - -/* Custom Cursor Tooltip */ -.custom-cursor-tooltip { - position: absolute; - display: none; - background-color: rgba(0, 0, 0, 0.75); - color: white; - padding: 6px 12px; - border-radius: 4px; - font-size: 13px; - font-weight: 500; + align-items: center; + gap: 8px; pointer-events: none; - z-index: 9999; - white-space: nowrap; - box-shadow: 0 2px 6px rgba(0,0,0,0.2); } -/* Sembunyikan default tooltip Leaflet Draw */ -.leaflet-draw-tooltip { - display: none !important; -} - -/* Sub-layer collapsible */ -.layer-group-item { - margin-bottom: 6px; -} - -.layer-group-item > .layer-group-header { +.toast { display: flex; align-items: center; - justify-content: space-between; + gap: 10px; + padding: 11px 18px; + border-radius: 10px; + font-size: 13px; + font-weight: 500; + color: white; + box-shadow: 0 6px 20px rgba(0,0,0,0.15); + pointer-events: auto; + animation: toastIn 0.28s cubic-bezier(0.175, 0.885, 0.32, 1.275) both; + max-width: 320px; } -.layer-toggle-icon { - cursor: pointer; - padding: 2px 6px; - color: #666; - font-size: 11px; - border-radius: 4px; - transition: background 0.2s; - flex-shrink: 0; -} +.toast.hiding { animation: toastOut 0.25s ease forwards; } +.toast-success { background: #065f46; } +.toast-error { background: #7f1d1d; } +.toast-info { background: #1e3a5f; } +.toast-warning { background: #78350f; } +.toast-icon { font-size: 15px; flex-shrink: 0; } -.layer-toggle-icon:hover { - background: #f0f0f0; - color: #333; -} +@keyframes toastIn { from { opacity:0; transform:translateY(12px) scale(0.92); } to { opacity:1; transform:translateY(0) scale(1); } } +@keyframes toastOut { from { opacity:1; } to { opacity:0; transform:translateY(6px) scale(0.95); } } -.layer-toggle-icon i { - transition: transform 0.2s; -} - -.layer-toggle-icon.collapsed i { - transform: rotate(-90deg); -} - -.sub-layer-list { - padding-left: 18px; - margin-top: 4px; - border-left: 2px solid #eee; - overflow: hidden; - transition: max-height 0.2s ease; -} - -.sub-option { - font-size: 12px !important; - color: #555; - margin-bottom: 3px !important; -} - -/* ===== Emoji Marker (Pin Bubble) ===== */ +/* ============================================================ + MAP MARKERS + ============================================================ */ .emoji-marker { display: flex; flex-direction: column; align-items: center; - pointer-events: auto; /* changed from none to allow click/drag */ + pointer-events: auto; } .emoji-marker .bubble { - width: 38px; - height: 38px; + width: 36px; + height: 36px; border-radius: 50% 50% 50% 0; transform: rotate(-45deg); display: flex; align-items: center; justify-content: center; - box-shadow: 0 3px 10px rgba(0,0,0,0.25); - border: 2px solid rgba(255,255,255,0.8); - animation: markerPop 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275) both; + box-shadow: 0 3px 10px rgba(0,0,0,0.2); + border: 2px solid rgba(255,255,255,0.9); + animation: markerPop 0.22s cubic-bezier(0.175, 0.885, 0.32, 1.275) both; } .emoji-marker .bubble span { transform: rotate(45deg); - font-size: 18px; + font-size: 16px; line-height: 1; } @@ -864,195 +953,26 @@ body.right-panel-open .custom-layer-panel { 100% { transform: rotate(-45deg) scale(1); } } -/* Warna bubble per tipe */ -.emoji-marker .bubble.ibadah { background: linear-gradient(135deg, #f97316, #ea580c); } -.emoji-marker .bubble.miskin-in { background: linear-gradient(135deg, #ef4444, #dc2626); } -.emoji-marker .bubble.miskin-out { background: linear-gradient(135deg, #22c55e, #16a34a); } - -/* ===== Toast Notification ===== */ -#toastContainer { - position: fixed; - bottom: 28px; - left: 50%; - transform: translateX(-50%); - z-index: 9999; - display: flex; - flex-direction: column; - align-items: center; - gap: 10px; - pointer-events: none; -} - -.toast { - display: flex; - align-items: center; - gap: 10px; - padding: 12px 20px; - border-radius: 12px; - font-size: 13px; - font-weight: 500; - color: white; - box-shadow: 0 6px 24px rgba(0,0,0,0.18); - backdrop-filter: blur(6px); - pointer-events: auto; - animation: toastSlideIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) both; - max-width: 340px; - text-align: center; -} - -.toast.hiding { - animation: toastSlideOut 0.3s ease forwards; -} - -.toast-success { background: linear-gradient(135deg, #22c55e, #16a34a); } -.toast-error { background: linear-gradient(135deg, #ef4444, #dc2626); } -.toast-info { background: linear-gradient(135deg, #6366f1, #4f46e5); } -.toast-warning { background: linear-gradient(135deg, #f59e0b, #d97706); } - -.toast-icon { font-size: 16px; flex-shrink: 0; } - -@keyframes toastSlideIn { - 0% { opacity: 0; transform: translateY(20px) scale(0.9); } - 100% { opacity: 1; transform: translateY(0) scale(1); } -} - -@keyframes toastSlideOut { - 0% { opacity: 1; transform: translateY(0) scale(1); } - 100% { opacity: 0; transform: translateY(10px) scale(0.95); } -} - -/* Auth Widget Styling */ -.auth-widget { - position: absolute; - top: 20px; - right: 80px; - z-index: 1000; - transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -body.right-panel-open .auth-widget { - right: 400px; -} - -.btn-login { - background: linear-gradient(135deg, #6366f1, #4f46e5); - color: white; - border: none; - border-radius: 24px; - height: 48px; - padding: 0 24px; - font-size: 14px; - font-weight: 600; - cursor: pointer; - box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); - display: flex; - align-items: center; - gap: 8px; - transition: all 0.2s ease; -} - -.btn-login:hover { - box-shadow: 0 6px 16px rgba(99, 102, 241, 0.45); - transform: translateY(-1px); -} - -.user-profile-pill { - background-color: white; - border-radius: 24px; - height: 48px; - padding: 0 8px 0 16px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - display: flex; - align-items: center; - gap: 12px; - border: 1px solid rgba(226, 232, 240, 0.8); - backdrop-filter: blur(10px); - background: rgba(255, 255, 255, 0.9); -} - -.user-profile-info { - display: flex; - flex-direction: column; -} - -.user-profile-name { - font-size: 12px; - font-weight: 700; - color: #1e1e2e; -} - -.user-profile-role { - font-size: 9px; - font-weight: 600; - color: #6366f1; - text-transform: uppercase; -} - -.btn-profile-logout { - width: 32px; - height: 32px; - border-radius: 50%; - border: none; - background: #f1f5f9; - color: #64748b; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s ease; -} - -.btn-profile-logout:hover { - background: #fee2e2; - color: #ef4444; -} - -/* User management table styling */ -#userManagementModal table th { - padding: 10px 12px; - font-weight: 600; - background-color: #f1f5f9; - border-bottom: 1px solid #cbd5e1; -} - -#userManagementModal table td { - padding: 10px 12px; - border-bottom: 1px solid #e2e8f0; -} - +.emoji-marker .bubble.ibadah { background: linear-gradient(135deg, #f97316, #ea580c); } +.emoji-marker .bubble.miskin-in { background: linear-gradient(135deg, #ef4444, #dc2626); } +.emoji-marker .bubble.miskin-out { background: linear-gradient(135deg, #10b981, #059669); } /* ============================================================ - DASHBOARD STYLES + DASHBOARD MODAL ============================================================ */ - #dashboardModal .modal-content { - width: min(920px, 96vw); + width: min(940px, 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; + background: linear-gradient(135deg, #0f172a 0%, #1e3a5f 100%); + border-radius: 14px 14px 0 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; } +#dashboardModal .modal-header h3 { color: white; } +#dashboardModal .modal-close { color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.1); } +#dashboardModal .modal-close:hover { background: rgba(239,68,68,0.3); color: white; } #dashboardBody { flex: 1; @@ -1060,242 +980,16 @@ body.right-panel-open .auth-widget { 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); + background: var(--surface-2); } -.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; } - -/* ══════════════════════════════════════════════════════ - Dashboard Tab Bar (Integrasi Multi-Database) - ══════════════════════════════════════════════════════ */ - .db-tab-bar { display: flex; gap: 4px; padding: 12px 20px 0; - background: #f8fafc; - border-bottom: 2px solid #e5e7eb; + background: white; + border-bottom: 2px solid var(--border); flex-wrap: wrap; } @@ -1306,25 +1000,16 @@ body.right-panel-open .auth-widget { border-radius: 8px 8px 0 0; font-size: 13px; font-weight: 500; - color: #6b7280; + color: var(--text-secondary); cursor: pointer; - transition: all 0.2s; + transition: all 0.15s; margin-bottom: -2px; border-bottom: 2px solid transparent; font-family: inherit; } -.db-tab-btn:hover { - background: #ede9fe; - color: #4f46e5; -} - -.db-tab-btn.active { - background: white; - color: #4f46e5; - border-bottom: 2px solid #4f46e5; - font-weight: 700; -} +.db-tab-btn:hover { background: var(--primary-light); color: var(--primary); } +.db-tab-btn.active { background: white; color: var(--primary); border-bottom-color: var(--primary); font-weight: 700; } .db-tab-content { padding: 20px; @@ -1332,10 +1017,728 @@ body.right-panel-open .auth-widget { max-height: calc(80vh - 100px); } -/* KPI card color override via CSS variable */ +.db-loading { display: flex; align-items: center; justify-content: center; gap: 12px; padding: 60px 0; color: var(--text-secondary); font-size: 14px; } +.db-spinner { width: 24px; height: 24px; border: 3px solid var(--border); border-top-color: var(--primary); border-radius: 50%; animation: dbSpin 0.7s linear infinite; } +@keyframes dbSpin { to { transform: rotate(360deg); } } + +.db-error { text-align: center; color: var(--danger); padding: 40px; font-size: 14px; } + +.db-ibadah-header { + background: linear-gradient(135deg, #1e3a5f, #2563eb); + border-radius: 12px; + padding: 16px 20px; + display: flex; + align-items: center; + gap: 16px; + color: white; + box-shadow: 0 4px 16px rgba(37,99,235,0.25); +} + +.db-ibadah-emoji { font-size: 34px; } +.db-ibadah-name { font-size: 17px; font-weight: 700; } +.db-ibadah-meta { font-size: 12px; color: rgba(255,255,255,0.7); margin-top: 2px; } + +.db-kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 12px; } + .db-kpi-card { - --kpi-color: #6366f1; + background: white; + border-radius: 12px; + padding: 16px; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + box-shadow: var(--shadow-sm); + position: relative; + overflow: hidden; + transition: transform 0.15s, box-shadow 0.15s; } -.db-kpi-card .db-kpi-val { - color: var(--kpi-color, #6366f1); + +.db-kpi-card::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 3px; + border-radius: 12px 12px 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, #2563eb, #1d4ed8); } +.kpi-log::before { background: linear-gradient(90deg, #10b981, #059669); } + +.db-kpi-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); } +.db-kpi-icon { font-size: 26px; } +.db-kpi-val { font-size: 28px; font-weight: 800; color: var(--text-primary); } +.db-kpi-label { font-size: 10px; font-weight: 700; color: var(--text-secondary); text-align: center; text-transform: uppercase; letter-spacing: 0.4px; } + +.db-section { background: white; border-radius: 12px; padding: 16px; box-shadow: var(--shadow-sm); } +.db-section-title { font-size: 12px; font-weight: 700; color: var(--text-primary); margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.4px; } + +.db-coverage-labels { display: flex; justify-content: space-between; font-size: 13px; color: var(--text-primary); flex-wrap: wrap; gap: 6px; margin-bottom: 8px; } +.db-progress-bar { height: 10px; 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: 12px; } +.db-chart-card { background: white; border-radius: 12px; padding: 14px 16px; box-shadow: var(--shadow-sm); } +.db-chart-wide { grid-column: 1 / -1; } +.db-chart-title { font-size: 11px; font-weight: 700; color: var(--text-secondary); 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: var(--surface-2); border-radius: 10px; transition: background 0.1s; } +.db-log-item:hover { background: var(--primary-light); } +.db-log-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--primary); flex-shrink: 0; margin-top: 5px; } +.db-log-info { flex: 1; min-width: 0; } +.db-log-main { font-size: 12px; font-weight: 600; color: var(--text-primary); display: flex; gap: 6px; flex-wrap: wrap; align-items: center; } +.db-log-tipe { background: var(--primary-light); color: var(--primary); padding: 1px 7px; border-radius: 99px; font-size: 11px; font-weight: 700; } +.db-log-kk { color: var(--text-primary); } +.db-log-meta { font-size: 10px; color: #9ca3af; margin-top: 3px; } + +.db-table-wrap { overflow-x: auto; border-radius: 8px; border: 1px solid var(--border); } +.db-table { width: 100%; border-collapse: collapse; font-size: 13px; } +.db-table thead tr { background: var(--surface-2); } +.db-table th { padding: 9px 14px; text-align: left; font-size: 11px; font-weight: 700; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.4px; border-bottom: 1px solid var(--border); } +.db-table td { padding: 9px 14px; color: var(--text-primary); border-bottom: 1px solid #f3f4f6; } +.db-table tbody tr:last-child td { border-bottom: none; } +.db-table tbody tr:hover td { background: var(--surface-2); } + +.db-badge { background: var(--primary); color: white; padding: 2px 8px; border-radius: 99px; font-size: 11px; font-weight: 700; } +.db-empty { text-align: center; color: #9ca3af; font-size: 13px; padding: 20px; } + +/* ============================================================ + USER MANAGEMENT MODAL + ============================================================ */ +#userManagementModal .modal-content { + width: min(720px, 96vw); +} + +/* ============================================================ + CUSTOM CURSOR TOOLTIP + ============================================================ */ +.custom-cursor-tooltip { + position: absolute; + display: none; + background: rgba(15,23,42,0.85); + color: white; + padding: 5px 10px; + border-radius: 6px; + font-size: 12px; + font-weight: 500; + pointer-events: none; + z-index: 9999; + white-space: nowrap; +} + +/* ============================================================ + POPUP FORM (Leaflet) + ============================================================ */ +.popup-form { display: flex; flex-direction: column; gap: 8px; width: 200px; } +.popup-form label { font-size: 11px; font-weight: 600; color: var(--text-secondary); } +.popup-form input[type="text"], .popup-form input[type="number"] { width: 100%; padding: 6px 8px; border: 1px solid var(--border); border-radius: 6px; font-size: 12px; } +.popup-form button { padding: 7px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 12px; } +.popup-form button.btn-danger { background: var(--danger); } + +/* ============================================================ + MISC + ============================================================ */ +.leaflet-draw-tooltip { display: none !important; } + +.sidebar-divider { + height: 1px; + background: rgba(255,255,255,0.07); + margin: 6px 10px; +} + +/* Leaflet popup customization */ +.leaflet-popup-content-wrapper { + border-radius: 10px; + box-shadow: var(--shadow-lg); + border: 1px solid var(--border); +} + +.leaflet-popup-tip { display: none; } + +/* Attribution */ +.leaflet-control-attribution { + font-size: 10px; +} + +/* ============================================================ + LOG BANTUAN MODAL + ============================================================ */ +.modal-log-bantuan { + width: min(560px, 96vw); + max-height: 90vh; + display: flex; + flex-direction: column; +} + +.modal-log-bantuan .modal-body { + flex: 1; + overflow: hidden; + padding: 0; + display: flex; + flex-direction: column; +} + +.log-form-section { + padding: 16px 20px; + background: var(--surface-2); + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} + +.log-form-title { + font-size: 12px; + font-weight: 700; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.4px; + margin-bottom: 12px; +} + +.log-form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-bottom: 12px; +} + +.log-list-section { + flex: 1; + overflow-y: auto; + padding: 16px 20px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.log-list-title { + font-size: 12px; + font-weight: 700; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.4px; + margin-bottom: 4px; + flex-shrink: 0; +} + +.log-item { + background: var(--surface-2); + border: 1px solid var(--border); + border-radius: 10px; + padding: 12px 14px; + display: flex; + flex-direction: column; + gap: 5px; + transition: background 0.15s; +} + +.log-item:hover { background: var(--primary-light); } + +.log-item-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.log-tipe-badge { + background: var(--primary); + color: white; + font-size: 11px; + font-weight: 700; + padding: 2px 10px; + border-radius: 99px; +} + +.log-tgl { + font-size: 11px; + color: var(--text-secondary); + white-space: nowrap; +} + +.log-ibadah { + font-size: 12px; + color: var(--text-primary); + font-weight: 500; + display: flex; + align-items: center; + gap: 5px; +} + +.log-jenis { + font-size: 10px; + background: #f1f5f9; + color: var(--text-secondary); + padding: 1px 6px; + border-radius: 4px; +} + +.log-ket { + font-size: 11px; + color: var(--text-secondary); + font-style: italic; +} + +/* ============================================================ + RESPONSIVE — MOBILE (max-width: 768px) + ============================================================ */ +@media (max-width: 768px) { + :root { --sidebar-w: 64px; } + + .left-sidebar { + width: 64px !important; + } + + .sidebar-brand-text, + .btn-label, + .sidebar-nav-label, + .collapse-label { + display: none !important; + } + + .sidebar-collapse-btn { display: none !important; } + + #map { + left: 64px !important; + } + + .top-bar { + left: 64px !important; + padding: 0 8px; + gap: 6px; + } + + .top-bar .search-bar { max-width: 100%; } + + .right-panel { + width: 100vw !important; + max-width: 100vw; + right: 0; + border-radius: 16px 16px 0 0; + top: auto; + bottom: 0; + height: 70vh; + transform: translateY(100%); + transition: transform 0.3s ease; + } + + .right-panel.open { + transform: translateY(0); + } + + body.right-panel-open #map { + bottom: 70vh; + } + + .modal-content { + width: 96vw !important; + margin: auto 2vw; + max-height: 90vh; + } + + .log-form-grid { + grid-template-columns: 1fr; + } + + #userManagementModal .modal-content { + width: 96vw !important; + } + + #userManagementModal [style*="grid-template-columns:1fr 1fr"] { + grid-template-columns: 1fr !important; + } + + .zoom-controls { + bottom: calc(70vh + 12px) !important; + } + + .custom-layer-panel { + left: 72px; + max-width: calc(100vw - 80px); + } + + .db-charts-grid { + grid-template-columns: 1fr !important; + } + + .db-kpi-grid { grid-template-columns: 1fr 1fr !important; } +} + +/* ============================================================ + RESPONSIVE — TABLET (769px – 1024px) + ============================================================ */ +@media (min-width: 769px) and (max-width: 1024px) { + :root { --sidebar-w: 180px; } + + .right-panel { width: 300px; } + + body.right-panel-open #map { right: 300px; } +} + +/* ============================================================ + DATA CARD — Panel Kiri (Ibadah & Miskin) + ============================================================ */ +.data-card { + background: white; + border: 1.5px solid var(--border); + border-radius: 12px; + margin-bottom: 8px; + overflow: hidden; + transition: box-shadow 0.15s, border-color 0.15s; +} +.data-card:hover { border-color: #bfdbfe; box-shadow: 0 2px 10px rgba(37,99,235,0.08); } +.data-card.expanded { border-color: var(--primary); } + +.data-card-main { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 14px; + cursor: pointer; + user-select: none; +} + +.data-card-icon { + width: 38px; + height: 38px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + flex-shrink: 0; +} +.card-icon-ibadah { background: #fef3c7; } +.card-icon-miskin-in { background: #dcfce7; } +.card-icon-miskin-out { background: #fee2e2; } + +.data-card-info { flex: 1; min-width: 0; } +.data-card-name { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.data-card-sub { + font-size: 11px; + color: var(--text-secondary); + margin-top: 2px; + display: flex; + align-items: center; + gap: 5px; + flex-wrap: wrap; +} + +.data-card-badge { + font-size: 10px; + font-weight: 700; + padding: 1px 7px; + border-radius: 99px; + white-space: nowrap; +} +.badge-orange { background: #ffedd5; color: #c2410c; } +.badge-blue { background: #dbeafe; color: #1d4ed8; } +.badge-green { background: #dcfce7; color: #15803d; } + +.data-card-actions { + display: flex; + gap: 4px; + flex-shrink: 0; + opacity: 0; + transition: opacity 0.15s; +} +.data-card:hover .data-card-actions { opacity: 1; } + +.btn-card-action { + width: 28px; + height: 28px; + border: 1px solid var(--border); + border-radius: 7px; + background: white; + color: var(--text-secondary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + transition: all 0.15s; +} +.btn-card-edit:hover { background: var(--primary); color: white; border-color: var(--primary); } +.btn-card-delete:hover { background: var(--danger); color: white; border-color: var(--danger); } + +/* Expanded section */ +.data-card-expand { + display: none; + padding: 0 14px 14px; + flex-direction: column; + gap: 6px; + border-top: 1px dashed var(--border); +} +.data-card.expanded .data-card-expand { display: flex; } + +.expand-row { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 12px; + gap: 10px; + padding: 4px 0; +} +.expand-row > span:first-child { color: var(--text-secondary); flex-shrink: 0; } +.expand-row > span:last-child { color: var(--text-primary); font-weight: 500; text-align: right; } + +.btn-log-bantuan { + margin-top: 6px; + width: 100%; + padding: 8px 12px; + border: 1.5px solid #c7d2fe; + background: #eef2ff; + color: #4338ca; + border-radius: 8px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s; + font-family: inherit; +} +.btn-log-bantuan:hover { + background: #4338ca; + color: white; + border-color: #4338ca; +} + +/* Panel action buttons */ +.btn-panel-add, .btn-panel-import { + display: flex; + align-items: center; + gap: 6px; + height: 34px; + padding: 0 14px; + border-radius: 8px; + border: none; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s; + font-family: inherit; + white-space: nowrap; +} +.btn-panel-add { + background: var(--primary); + color: white; +} +.btn-panel-add:hover { background: var(--primary-dark); } +.btn-panel-import { + background: white; + color: var(--text-secondary); + border: 1px solid var(--border); +} +.btn-panel-import:hover { background: var(--surface-2); color: var(--text-primary); } + +/* Section title inside panel */ +.panel-section-title { + font-size: 10px; + font-weight: 700; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 10px 2px 4px; +} + +/* ============================================================ + LOG BANTUAN PANEL — Kartu Penduduk (lebih bagus) + ============================================================ */ +.log-person-card { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 14px; + background: white; + border: 1.5px solid var(--border); + border-radius: 12px; + margin-bottom: 8px; + cursor: pointer; + transition: all 0.18s; + text-decoration: none; +} +.log-person-card:hover { + border-color: var(--primary); + background: #f0f5ff; + transform: translateX(2px); + box-shadow: 0 2px 10px rgba(37,99,235,0.1); +} +.log-person-avatar { + width: 42px; + height: 42px; + border-radius: 12px; + background: linear-gradient(135deg, #6366f1, #8b5cf6); + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + flex-shrink: 0; +} +.log-person-avatar.avatar-makan { background: linear-gradient(135deg, #f97316, #ef4444); } +.log-person-avatar.avatar-pmbr { background: linear-gradient(135deg, #3b82f6, #6366f1); } +.log-person-info { flex: 1; min-width: 0; } +.log-person-name { + font-size: 13px; + font-weight: 700; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.log-person-meta { + font-size: 11px; + color: var(--text-secondary); + margin-top: 3px; + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} +.log-person-arrow { + color: #c7d2fe; + font-size: 14px; + flex-shrink: 0; + transition: transform 0.15s, color 0.15s; +} +.log-person-card:hover .log-person-arrow { + color: var(--primary); + transform: translateX(3px); +} + +/* Panel hint strip */ +.panel-hint { + background: #eff6ff; + border: 1px solid #bfdbfe; + border-radius: 10px; + padding: 8px 12px; + font-size: 11px; + color: #1e40af; + margin-bottom: 10px; + display: flex; + align-items: center; + gap: 6px; +} + +/* Improved log-item in modal */ +.log-item { + background: white; + border: 1.5px solid var(--border); + border-radius: 12px; + padding: 12px 14px; + display: flex; + flex-direction: column; + gap: 6px; + transition: all 0.15s; + margin-bottom: 8px; +} +.log-item:hover { border-color: #bfdbfe; background: #f8faff; box-shadow: 0 2px 8px rgba(37,99,235,0.07); } + +.log-item-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} +.log-tipe-badge { + background: var(--primary); + color: white; + font-size: 11px; + font-weight: 700; + padding: 3px 10px; + border-radius: 99px; + letter-spacing: 0.2px; +} +.log-tgl { + font-size: 11px; + color: var(--text-secondary); + white-space: nowrap; + display: flex; + align-items: center; + gap: 4px; +} +.log-ibadah { + font-size: 12px; + color: var(--text-primary); + font-weight: 500; + display: flex; + align-items: center; + gap: 5px; +} +.log-jenis { + font-size: 10px; + background: #f1f5f9; + color: var(--text-secondary); + padding: 1px 6px; + border-radius: 5px; +} +.log-ket { + font-size: 11px; + color: var(--text-secondary); + font-style: italic; + padding-top: 2px; + border-top: 1px dashed var(--border); + margin-top: 2px; +} + +/* Log bantuan modal improvements */ +.modal-log-bantuan { + width: min(560px, 96vw); + max-height: 90vh; + display: flex; + flex-direction: column; +} +.modal-log-bantuan .modal-body { + flex: 1; + overflow: hidden; + padding: 0; + display: flex; + flex-direction: column; +} +.log-form-section { + padding: 16px 20px; + background: var(--surface-2); + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} +.log-form-title { + font-size: 11px; + font-weight: 700; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 6px; +} +.log-form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-bottom: 12px; +} +.log-list-section { + flex: 1; + overflow-y: auto; + padding: 16px 20px; +} +.log-list-title { + font-size: 11px; + font-weight: 700; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 6px; } diff --git a/webgis-poverty-pontianak/assets/js/features/auth.js b/webgis-poverty-pontianak/assets/js/features/auth.js index 6d76deb..a381708 100644 --- a/webgis-poverty-pontianak/assets/js/features/auth.js +++ b/webgis-poverty-pontianak/assets/js/features/auth.js @@ -77,7 +77,7 @@ if (dashboardBtn) dashboardBtn.style.display = 'flex'; // Show user management button in sidebar for Admin - const adminDivider = document.getElementById('sidebarAdminDivider'); + const adminDivider = document.getElementById('adminNavLabel'); if (window.currentUser.role === 'admin') { if (menuUsersBtn) menuUsersBtn.style.display = 'flex'; if (adminDivider) adminDivider.style.display = 'block'; @@ -93,7 +93,7 @@ document.getElementById('authLoginBtn').addEventListener('click', showLoginModal); if (menuUsersBtn) menuUsersBtn.style.display = 'none'; if (dashboardBtn) dashboardBtn.style.display = 'none'; - const adminDivider = document.getElementById('sidebarAdminDivider'); + const adminDivider = document.getElementById('adminNavLabel'); if (adminDivider) adminDivider.style.display = 'none'; } } diff --git a/webgis-poverty-pontianak/assets/js/features/geolocation.js b/webgis-poverty-pontianak/assets/js/features/geolocation.js index aa6179b..579575a 100644 --- a/webgis-poverty-pontianak/assets/js/features/geolocation.js +++ b/webgis-poverty-pontianak/assets/js/features/geolocation.js @@ -9,7 +9,8 @@ geoBtn.style.left = '0'; geoBtn.style.right = 'auto'; geoBtn.innerHTML = ''; geoBtn.title = 'Lokasi Saya'; -document.querySelector('.custom-zoom-control').appendChild(geoBtn); +const zoomContainer = document.querySelector('.zoom-controls'); +if (zoomContainer) zoomContainer.appendChild(geoBtn); let userMarker = null; diff --git a/webgis-poverty-pontianak/assets/js/features/kemiskinan.js b/webgis-poverty-pontianak/assets/js/features/kemiskinan.js index f977216..60b7587 100644 --- a/webgis-poverty-pontianak/assets/js/features/kemiskinan.js +++ b/webgis-poverty-pontianak/assets/js/features/kemiskinan.js @@ -444,8 +444,8 @@ function addMiskinMarker(item) { photoLinksHtml += `