3253 lines
111 KiB
PHP
3253 lines
111 KiB
PHP
<?php
|
|
require_once 'config.php';
|
|
require_once 'auth/helper.php';
|
|
|
|
require_password_changed('auth/change_password.php');
|
|
|
|
$role = is_logged_in() ? get_role() : 'viewer';
|
|
$user_nama = is_logged_in() ? ($_SESSION['nama'] ?? 'Pengguna') : null;
|
|
$is_admin = ($role === 'administrator');
|
|
$is_op = ($role === 'operator');
|
|
$logged_in = is_logged_in();
|
|
$embedded = (($_GET['embedded'] ?? '') === '1');
|
|
if ($embedded) {
|
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
|
header('Pragma: no-cache');
|
|
}
|
|
|
|
$map_modules = [
|
|
'modules/ibadah.js',
|
|
'modules/penduduk.js',
|
|
'modules/stats.js',
|
|
'modules/heatmap.js',
|
|
];
|
|
if ($logged_in) {
|
|
$map_modules[] = 'modules/kebutuhan.js';
|
|
}
|
|
$map_module_urls = array_map(function ($module) {
|
|
$version = file_exists(__DIR__ . '/' . $module) ? filemtime(__DIR__ . '/' . $module) : time();
|
|
return $module . '?v=' . $version;
|
|
}, $map_modules);
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="id">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title><?= APP_NAME ?></title>
|
|
|
|
<!-- Leaflet CSS -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
|
|
|
<!-- Google Fonts -->
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
|
|
rel="stylesheet">
|
|
<!-- Lucide Icons CDN -->
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
|
|
<style>
|
|
/* ── Reset & Base ─────────────────────────────── */
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
:root {
|
|
/* Warm Earth tokens */
|
|
--c-bg: #fafaf9;
|
|
--c-surface: #ede8e2;
|
|
--c-surface2: #e5dfd8;
|
|
--c-border: #ddd8d2;
|
|
--c-text: #201515;
|
|
--c-muted: #7a7067;
|
|
--c-accent: #0d7490;
|
|
--c-accent-h: #0a5f7a;
|
|
--c-danger: #ef4444;
|
|
--c-warn: #f59e0b;
|
|
--c-info: #3b82f6;
|
|
--radius: 12px;
|
|
--shadow: 0 4px 12px rgba(32,21,21,.08);
|
|
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
--font-mono: var(--font-body);
|
|
/* Sidebar — stays dark as sole dark surface */
|
|
--sb-bg: #1c1612;
|
|
--sb-border: rgba(255,255,255,.06);
|
|
--sb-text: #a89f96;
|
|
--sb-text-hover: #ffffff;
|
|
--sb-active: #0d7490;
|
|
}
|
|
|
|
html,
|
|
body {
|
|
height: 100%;
|
|
font-family: var(--font-body);
|
|
background: var(--c-bg);
|
|
color: var(--c-text);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.app-layout {
|
|
display: flex;
|
|
flex-direction: row;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* ── Sidebar ─────────────────────────────────────── */
|
|
.sidebar {
|
|
width: 320px;
|
|
min-width: 320px;
|
|
background: var(--sb-bg);
|
|
border-right: 1px solid var(--sb-border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
|
min-width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
|
border-width 0.15s;
|
|
z-index: 100;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sidebar.collapsed {
|
|
width: 0;
|
|
min-width: 0;
|
|
border-right: none;
|
|
}
|
|
|
|
.sidebar.collapsed>* {
|
|
display: none !important;
|
|
}
|
|
|
|
.sb-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 14px 16px;
|
|
border-bottom: 1px solid var(--sb-border);
|
|
background: rgba(0, 0, 0, .2);
|
|
flex-shrink: 0;
|
|
gap: 8px;
|
|
}
|
|
|
|
.sb-brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
overflow: hidden;
|
|
min-width: 0;
|
|
}
|
|
|
|
.sb-logo {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 9px;
|
|
background: #0d7490;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 18px;
|
|
flex-shrink: 0;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.sb-title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
color: #ffffff;
|
|
}
|
|
|
|
.sb-subtitle {
|
|
font-size: 10px;
|
|
color: var(--sb-text);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.sb-header-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sb-toggle-btn {
|
|
width: 28px;
|
|
height: 28px;
|
|
background: rgba(255, 255, 255, .05);
|
|
border: 1px solid var(--sb-border);
|
|
border-radius: 7px;
|
|
color: var(--sb-text);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 11px;
|
|
transition: all 0.2s;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sb-toggle-btn:hover {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
color: var(--sb-text-hover);
|
|
}
|
|
|
|
.sb-open-btn {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 0;
|
|
transform: translateY(-50%);
|
|
width: 24px;
|
|
height: 56px;
|
|
background: var(--c-surface);
|
|
border: 1px solid var(--c-border);
|
|
border-left: none;
|
|
border-radius: 0 10px 10px 0;
|
|
color: var(--c-accent-h);
|
|
cursor: pointer;
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 11px;
|
|
z-index: 1100;
|
|
transition: width 0.2s, background 0.2s, color 0.2s;
|
|
box-shadow: 2px 0 8px rgba(0, 0, 0, .4);
|
|
}
|
|
|
|
.sb-open-btn:hover {
|
|
color: var(--c-text);
|
|
background: var(--c-surface2);
|
|
width: 30px;
|
|
}
|
|
|
|
.sidebar.collapsed~.map-area .sb-open-btn {
|
|
display: flex;
|
|
}
|
|
|
|
/* ── Sidebar User ─────────────────────────────── */
|
|
.sb-user {
|
|
padding: 10px 14px;
|
|
border-bottom: 1px solid var(--sb-border);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-shrink: 0;
|
|
background: rgba(0, 0, 0, .15);
|
|
min-height: 54px;
|
|
}
|
|
|
|
.sb-user-avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sb-user-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.sb-user-name-text {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #ffffff;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.sb-user-role-text {
|
|
font-size: 10px;
|
|
color: var(--sb-text);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.sb-login-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 6px 12px;
|
|
background: #0d7490;
|
|
border: none;
|
|
border-radius: 8px;
|
|
color: #ffffff;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
font-family: var(--font-body);
|
|
text-decoration: none;
|
|
transition: background .2s, border-color .2s;
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sb-login-btn:hover {
|
|
background: #0a5f7a;
|
|
color: #ffffff;
|
|
}
|
|
|
|
/* ── User dropdown ──────────────────────────────── */
|
|
.sb-user {
|
|
cursor: pointer;
|
|
position: relative;
|
|
user-select: none;
|
|
}
|
|
|
|
.sb-user:hover {
|
|
background: rgba(255, 255, 255, .04);
|
|
}
|
|
|
|
.sb-user-chevron {
|
|
font-size: 9px;
|
|
color: var(--sb-text);
|
|
flex-shrink: 0;
|
|
transition: transform .2s;
|
|
}
|
|
|
|
.sb-user.open .sb-user-chevron {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
.sb-user-dropdown {
|
|
position: absolute;
|
|
top: calc(100% + 4px);
|
|
left: 8px;
|
|
right: 8px;
|
|
background: #1a1a1a;
|
|
border: 1px solid rgba(255, 255, 255, .10);
|
|
border-radius: 10px;
|
|
box-shadow: 0 8px 28px rgba(0, 0, 0, .45);
|
|
z-index: 500;
|
|
overflow: hidden;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transform: translateY(-6px);
|
|
transition: opacity .18s ease, transform .18s ease;
|
|
}
|
|
|
|
.sb-user.open .sb-user-dropdown {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.sb-dd-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px 14px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--sb-text);
|
|
text-decoration: none;
|
|
transition: background .15s, color .15s;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.sb-dd-item:hover {
|
|
background: rgba(255, 255, 255, .07);
|
|
color: var(--sb-text-hover);
|
|
}
|
|
|
|
.sb-dd-item.danger:hover {
|
|
background: rgba(248, 81, 73, .1);
|
|
color: #fca5a5;
|
|
}
|
|
|
|
.sb-dd-icon {
|
|
font-size: 15px;
|
|
width: 20px;
|
|
text-align: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sb-dd-sep {
|
|
height: 1px;
|
|
background: rgba(255, 255, 255, .07);
|
|
margin: 2px 0;
|
|
}
|
|
|
|
/* ── Sidebar Tool Buttons ─────────────────────── */
|
|
.sb-tools {
|
|
padding: 8px 10px;
|
|
border-bottom: 1px solid var(--sb-border);
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 5px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sb-tool-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 6px 10px;
|
|
background: rgba(255, 255, 255, .04);
|
|
border: 1px solid rgba(255, 255, 255, .07);
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
color: var(--sb-text);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
font-family: var(--font-body);
|
|
transition: all 0.2s;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.sb-tool-btn:hover {
|
|
background: rgba(255, 255, 255, 0.09);
|
|
color: var(--sb-text-hover);
|
|
border-color: rgba(255, 255, 255, 0.14);
|
|
}
|
|
|
|
.sb-tool-btn .tb-icon {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.sb-tool-btn.active-ibadah {
|
|
background: rgba(124, 58, 237, .15);
|
|
border-color: rgba(124, 58, 237, .3);
|
|
color: #a855f7;
|
|
}
|
|
|
|
.sb-tool-btn.active-penduduk {
|
|
background: rgba(34, 197, 94, .15);
|
|
border-color: rgba(34, 197, 94, .3);
|
|
color: #22c55e;
|
|
}
|
|
|
|
.sb-tool-btn.btn-stats {
|
|
color: var(--c-info);
|
|
}
|
|
|
|
.sb-tool-btn.btn-recalc {
|
|
color: var(--c-warn);
|
|
}
|
|
|
|
.sb-tool-btn.btn-import {
|
|
color: var(--c-info);
|
|
}
|
|
|
|
.sb-tool-btn.btn-blankspot {
|
|
color: var(--c-danger);
|
|
}
|
|
|
|
.sb-tool-btn.btn-blankspot:hover {
|
|
background: rgba(248, 81, 73, .1);
|
|
}
|
|
|
|
/* ── Sidebar Data (scrollable) ────────────────── */
|
|
.sb-data {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* ── Sidebar Legend ───────────────────────────── */
|
|
.sb-legend {
|
|
border-top: 1px solid var(--sb-border);
|
|
padding: 10px 14px;
|
|
flex-shrink: 0;
|
|
background: rgba(0, 0, 0, .2);
|
|
}
|
|
|
|
/* ── Public Hint Strip ──────────────────────────── */
|
|
.sb-hint {
|
|
padding: 9px 14px;
|
|
background: rgba(255,255,255,.04);
|
|
border-bottom: 1px solid var(--sb-border);
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sb-hint-icon { font-size: 13px; flex-shrink: 0; margin-top: 1px; }
|
|
|
|
.sb-hint-text {
|
|
font-size: 11px;
|
|
color: var(--sb-text);
|
|
line-height: 1.55;
|
|
}
|
|
|
|
/* ── Papan kebutuhan tool btn ────────────────── */
|
|
.sb-tool-btn.btn-papan {
|
|
color: var(--c-warn);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.sb-tool-btn.btn-papan:hover {
|
|
background: rgba(227, 179, 65, .1);
|
|
border-color: rgba(227, 179, 65, .2);
|
|
color: #fbbf24;
|
|
}
|
|
|
|
.sb-legend-title {
|
|
font-size: 9px;
|
|
font-weight: 600;
|
|
color: var(--sb-text);
|
|
text-transform: uppercase;
|
|
letter-spacing: .8px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.sb-legend-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 11px;
|
|
color: var(--sb-text);
|
|
margin-bottom: 3px;
|
|
}
|
|
|
|
.sb-legend-row:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.sb-legend-sub {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
color: var(--sb-active);
|
|
margin: 6px 0 4px;
|
|
letter-spacing: .3px;
|
|
}
|
|
|
|
/* ── Map Area ─────────────────────────────────── */
|
|
.map-area {
|
|
flex: 1;
|
|
position: relative;
|
|
overflow: hidden;
|
|
min-width: 0;
|
|
}
|
|
|
|
.toolbar-container {
|
|
display: none;
|
|
}
|
|
|
|
.ft-box {
|
|
background: #ede8e2;
|
|
border: none;
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 12px rgba(32,21,21,0.08);
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 6px;
|
|
gap: 6px;
|
|
pointer-events: auto;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.ft-box:hover {
|
|
box-shadow: 0 6px 16px rgba(0,0,0,0.12);
|
|
}
|
|
|
|
.ft-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 44px;
|
|
min-width: 44px;
|
|
padding: 0 10px;
|
|
background: transparent;
|
|
border: 1px solid transparent;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
color: var(--c-muted);
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.ft-content {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.ft-text {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
max-width: 0;
|
|
opacity: 0;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.ft-icon {
|
|
font-size: 16px;
|
|
line-height: 1;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.ft-btn:hover {
|
|
background: rgba(0, 0, 0, 0.05);
|
|
color: var(--c-text);
|
|
}
|
|
|
|
.ft-btn:hover .ft-icon {
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.ft-btn:hover .ft-content,
|
|
.ft-btn.active-point .ft-content {
|
|
gap: 8px;
|
|
}
|
|
|
|
.ft-btn:hover .ft-text,
|
|
.ft-btn.active-point .ft-text {
|
|
max-width: 80px;
|
|
opacity: 1;
|
|
}
|
|
|
|
.ft-btn.active-point {
|
|
background: rgba(52, 211, 153, 0.15);
|
|
border-color: rgba(52, 211, 153, 0.3);
|
|
color: var(--c-accent);
|
|
}
|
|
|
|
.ft-btn.active-ibadah {
|
|
background: rgba(124, 58, 237, 0.15);
|
|
border-color: rgba(124, 58, 237, 0.3);
|
|
color: #a855f7;
|
|
}
|
|
|
|
.ft-btn.active-penduduk {
|
|
background: rgba(34, 197, 94, 0.15);
|
|
border-color: rgba(34, 197, 94, 0.3);
|
|
color: #22c55e;
|
|
}
|
|
|
|
.ft-btn:hover .ft-content,
|
|
.ft-btn.active-ibadah .ft-content,
|
|
.ft-btn.active-penduduk .ft-content {
|
|
gap: 8px;
|
|
}
|
|
|
|
.ft-btn.active-ibadah .ft-text,
|
|
.ft-btn.active-penduduk .ft-text {
|
|
max-width: 80px;
|
|
opacity: 1;
|
|
}
|
|
|
|
@keyframes pulse-ibadah {
|
|
|
|
0%,
|
|
100% {
|
|
opacity: 0.6;
|
|
transform: scale(1);
|
|
}
|
|
|
|
50% {
|
|
opacity: 1;
|
|
transform: scale(1.15);
|
|
}
|
|
}
|
|
|
|
/* ── Map Container ───────────────────────────── */
|
|
#map {
|
|
position: absolute;
|
|
inset: 0;
|
|
}
|
|
|
|
#map.tool-active,
|
|
#map.tool-active .leaflet-container,
|
|
#map.tool-active .leaflet-grab,
|
|
#map.tool-active .leaflet-interactive {
|
|
cursor: crosshair !important;
|
|
}
|
|
|
|
/* ── Filter Container (legacy, hidden — content moved to sidebar) ── */
|
|
.filter-container {
|
|
display: none;
|
|
}
|
|
|
|
.fc-box {
|
|
background: #f5f5f5;
|
|
border: none;
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 6px;
|
|
gap: 4px;
|
|
pointer-events: auto;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.fc-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
height: 36px;
|
|
padding: 0 12px;
|
|
background: transparent;
|
|
border: 1px solid transparent;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
color: var(--c-muted);
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
gap: 10px;
|
|
box-sizing: border-box;
|
|
opacity: 1;
|
|
}
|
|
|
|
.fc-btn.disabled {
|
|
opacity: 0.4;
|
|
filter: grayscale(1);
|
|
}
|
|
|
|
.fc-btn:hover {
|
|
color: var(--c-text);
|
|
background: rgba(255, 255, 255, 0.06);
|
|
}
|
|
|
|
.fc-icon {
|
|
font-size: 16px;
|
|
line-height: 1;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.fc-btn:hover .fc-icon {
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.fc-text {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
flex: 1;
|
|
gap: 16px;
|
|
transition: color 0.2s;
|
|
}
|
|
|
|
.fc-count {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
padding: 2px 6px;
|
|
border-radius: 20px;
|
|
font-size: 10px;
|
|
font-family: var(--font-mono);
|
|
color: var(--c-text);
|
|
}
|
|
|
|
/* ── Leaflet Popup Override ───────────────────── */
|
|
.leaflet-popup-content-wrapper {
|
|
background: var(--c-surface) !important;
|
|
border: 1px solid var(--c-border) !important;
|
|
border-radius: var(--radius) !important;
|
|
box-shadow: var(--shadow) !important;
|
|
padding: 0 !important;
|
|
color: var(--c-text) !important;
|
|
min-width: 260px;
|
|
max-height: min(80vh, 460px);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.leaflet-popup-content {
|
|
margin: 0 !important;
|
|
width: 100% !important;
|
|
max-height: min(80vh, 460px);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.leaflet-popup-tip {
|
|
background: var(--c-border) !important;
|
|
}
|
|
|
|
.leaflet-popup-close-button {
|
|
color: var(--c-muted) !important;
|
|
font-size: 20px !important;
|
|
top: 10px !important;
|
|
right: 10px !important;
|
|
width: 24px !important;
|
|
height: 24px !important;
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
justify-content: center !important;
|
|
line-height: 1 !important;
|
|
}
|
|
|
|
/* ── Live Tooltip for Drawing ─────────────────── */
|
|
.live-tooltip {
|
|
background: #1c1612 !important;
|
|
color: #ffffff !important;
|
|
border: none !important;
|
|
font-size: 11px !important;
|
|
font-family: var(--font-body) !important;
|
|
border-radius: 6px !important;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5) !important;
|
|
padding: 4px 8px !important;
|
|
}
|
|
|
|
.live-tooltip b {
|
|
color: #fff;
|
|
}
|
|
|
|
.live-tooltip::before {
|
|
display: none !important;
|
|
}
|
|
|
|
/* ── Form Popup ───────────────────────────────── */
|
|
.form-popup {
|
|
padding: 20px;
|
|
}
|
|
|
|
.form-popup-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 18px;
|
|
padding-bottom: 14px;
|
|
border-bottom: 1px solid var(--c-border);
|
|
}
|
|
|
|
.form-popup-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 8px;
|
|
background: rgba(46, 160, 67, .15);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
font-size: 18px;
|
|
}
|
|
|
|
.form-popup-icon.blue {
|
|
background: rgba(0, 0, 0, .06);
|
|
}
|
|
|
|
.form-popup-icon.amber {
|
|
background: rgba(227, 179, 65, .15);
|
|
}
|
|
|
|
.form-popup-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.form-popup-coords {
|
|
font-size: 10px;
|
|
color: var(--c-muted);
|
|
font-family: var(--font-mono);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: #3d3530;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.form-group input[type="text"],
|
|
.form-group input[type="number"],
|
|
.form-group select {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
background: var(--c-bg);
|
|
border: 1px solid var(--c-border);
|
|
border-radius: 7px;
|
|
color: var(--c-text);
|
|
font-family: var(--font-body);
|
|
font-size: 13px;
|
|
transition: border-color .2s;
|
|
outline: none;
|
|
}
|
|
|
|
.form-group input:focus,
|
|
.form-group select:focus {
|
|
border-color: #0d7490;
|
|
outline: none;
|
|
}
|
|
|
|
.form-group input:disabled,
|
|
.form-group select:disabled {
|
|
opacity: .5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.input-readonly {
|
|
padding: 8px 12px;
|
|
background: var(--c-bg);
|
|
border: 1px solid var(--c-border);
|
|
border-radius: 7px;
|
|
color: var(--c-accent-h);
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
width: 100%;
|
|
}
|
|
|
|
.btn-save {
|
|
width: 100%;
|
|
padding: 10px;
|
|
background: #0d7490;
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-family: var(--font-body);
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: background .2s;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.btn-save:hover {
|
|
background: #0a5f7a;
|
|
}
|
|
|
|
.btn-save:disabled {
|
|
opacity: .5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.form-status {
|
|
font-size: 12px;
|
|
text-align: center;
|
|
margin-top: 8px;
|
|
min-height: 18px;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.form-status.error {
|
|
color: var(--c-danger);
|
|
}
|
|
|
|
.form-status.success {
|
|
color: var(--c-accent);
|
|
}
|
|
|
|
/* ── Info Popup ───────────────────────────────── */
|
|
.info-popup {
|
|
padding: 14px 16px 12px;
|
|
}
|
|
|
|
.info-popup-header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.info-popup-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 7px;
|
|
background: rgba(46, 160, 67, .15);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
font-size: 15px;
|
|
}
|
|
|
|
.info-popup-name {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.info-popup-id {
|
|
font-size: 10px;
|
|
color: var(--c-muted);
|
|
font-family: var(--font-mono);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.info-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 6px 0;
|
|
border-top: 1px solid var(--c-border);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.info-row-icon {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 5px;
|
|
background: rgba(255, 255, 255, .05);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 12px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.info-row-label {
|
|
font-size: 10px;
|
|
color: var(--c-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: .5px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.info-row-value {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.status-badge-inline {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 3px 10px;
|
|
border-radius: 12px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.btn-hapus {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
width: 100%;
|
|
margin-top: 8px;
|
|
padding: 9px;
|
|
background: transparent;
|
|
color: var(--c-danger);
|
|
border: 1px solid rgba(239, 68, 68, .4);
|
|
border-radius: 7px;
|
|
font-family: var(--font-body);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all .2s;
|
|
}
|
|
|
|
.btn-hapus:hover {
|
|
background: rgba(239, 68, 68, .08);
|
|
border-color: #ef4444;
|
|
}
|
|
|
|
/* ── Toast ───────────────────────────────────── */
|
|
.toast {
|
|
position: fixed;
|
|
top: 70px;
|
|
right: 20px;
|
|
z-index: 9999;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 12px 18px;
|
|
background: var(--c-surface);
|
|
border: 1px solid var(--c-border);
|
|
border-left: 3px solid var(--c-accent);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
max-width: 320px;
|
|
transform: translateX(360px);
|
|
opacity: 0;
|
|
transition: all .35s cubic-bezier(.34, 1.56, .64, 1);
|
|
}
|
|
|
|
.toast.show {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
|
|
.toast.error {
|
|
border-left-color: var(--c-danger);
|
|
}
|
|
|
|
/* ── Confirm Modal ─────────────────────────────── */
|
|
.confirm-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, .55);
|
|
backdrop-filter: blur(2px);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
z-index: 10000;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity .2s ease;
|
|
}
|
|
|
|
.confirm-overlay.show {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.confirm-dialog {
|
|
width: min(420px, 100%);
|
|
background: var(--c-surface);
|
|
border: 1px solid var(--c-border);
|
|
border-radius: 12px;
|
|
box-shadow: var(--shadow);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.confirm-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 14px 16px;
|
|
border-bottom: 1px solid var(--c-border);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.confirm-icon {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.confirm-icon.danger {
|
|
background: rgba(248, 81, 73, .15);
|
|
color: var(--c-danger);
|
|
}
|
|
|
|
.confirm-icon.warn {
|
|
background: rgba(227, 179, 65, .15);
|
|
color: var(--c-warn);
|
|
}
|
|
|
|
.confirm-body {
|
|
padding: 16px;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.confirm-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
justify-content: flex-end;
|
|
padding: 14px 16px;
|
|
border-top: 1px solid var(--c-border);
|
|
}
|
|
|
|
.btn-confirm-cancel,
|
|
.btn-confirm-delete {
|
|
padding: 8px 14px;
|
|
border-radius: 8px;
|
|
font-family: var(--font-body);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all .2s;
|
|
}
|
|
|
|
.btn-confirm-cancel {
|
|
border: 1px solid var(--c-border);
|
|
background: transparent;
|
|
color: var(--c-text);
|
|
}
|
|
|
|
.btn-confirm-cancel:hover {
|
|
background: rgba(255, 255, 255, .05);
|
|
}
|
|
|
|
.btn-confirm-delete {
|
|
border: 1px solid #ef4444;
|
|
background: rgba(239, 68, 68, .08);
|
|
color: #ef4444;
|
|
}
|
|
|
|
.btn-confirm-delete:hover {
|
|
background: rgba(239, 68, 68, .15);
|
|
}
|
|
|
|
.btn-confirm-warn {
|
|
border: 1px solid var(--c-warn);
|
|
background: rgba(227, 179, 65, .15);
|
|
color: #f0c96a;
|
|
}
|
|
|
|
.btn-confirm-warn:hover {
|
|
background: rgba(227, 179, 65, .28);
|
|
}
|
|
|
|
/* ── Online/Offline Indicator ──────────────────── */
|
|
.conn-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
padding: 3px 8px;
|
|
border-radius: 20px;
|
|
transition: all .3s;
|
|
}
|
|
|
|
.conn-badge.online {
|
|
background: rgba(16, 185, 129, .12);
|
|
color: #34d399;
|
|
border: 1px solid rgba(16, 185, 129, .25);
|
|
}
|
|
|
|
.conn-badge.offline {
|
|
background: rgba(248, 81, 73, .12);
|
|
color: #fca5a5;
|
|
border: 1px solid rgba(248, 81, 73, .25);
|
|
}
|
|
|
|
.conn-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
background: currentColor;
|
|
}
|
|
|
|
.conn-badge.online .conn-dot {
|
|
animation: pulse-dot 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse-dot {
|
|
|
|
0%,
|
|
100% {
|
|
opacity: 1;
|
|
}
|
|
|
|
50% {
|
|
opacity: .4;
|
|
}
|
|
}
|
|
|
|
/* ── Leaflet Controls ─────────────────────────── */
|
|
.leaflet-control-attribution {
|
|
background: rgba(250,250,249,.88) !important;
|
|
color: #7a7067 !important;
|
|
font-size: 10px !important;
|
|
}
|
|
|
|
.leaflet-control-attribution a {
|
|
color: #3d3530 !important;
|
|
}
|
|
|
|
.leaflet-control-zoom a {
|
|
background: var(--c-surface) !important;
|
|
color: var(--c-text) !important;
|
|
border-color: var(--c-border) !important;
|
|
}
|
|
|
|
.leaflet-control-zoom a:hover {
|
|
background: #ede8e2 !important;
|
|
}
|
|
|
|
/* ── Leaflet Layers Control — Light Theme ──────── */
|
|
.leaflet-control-layers {
|
|
background: #ede8e2 !important;
|
|
border: 1px solid #ddd8d2 !important;
|
|
border-radius: 12px !important;
|
|
box-shadow: 0 4px 12px rgba(32,21,21,0.08) !important;
|
|
color: #201515 !important;
|
|
min-width: 200px;
|
|
}
|
|
|
|
.leaflet-control-layers-toggle {
|
|
background-color: var(--c-surface) !important;
|
|
border-radius: 8px !important;
|
|
}
|
|
|
|
.leaflet-control-layers-expanded {
|
|
padding: 10px 14px !important;
|
|
}
|
|
|
|
/* Judul seksi (Basemaps / Overlays) */
|
|
.leaflet-control-layers-base::before {
|
|
content: 'Peta Dasar';
|
|
display: block;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
color: var(--c-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: .6px;
|
|
margin-bottom: 6px;
|
|
font-family: var(--font-body);
|
|
}
|
|
|
|
.leaflet-control-layers label {
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
gap: 7px !important;
|
|
padding: 5px 4px !important;
|
|
cursor: pointer !important;
|
|
border-radius: 6px !important;
|
|
transition: background .15s !important;
|
|
font-family: var(--font-body) !important;
|
|
font-size: 12px !important;
|
|
font-weight: 500 !important;
|
|
color: var(--c-text) !important;
|
|
margin: 0 !important;
|
|
}
|
|
|
|
.leaflet-control-layers label:hover {
|
|
background: rgba(0, 0, 0, 0.05) !important;
|
|
}
|
|
|
|
.leaflet-control-layers input[type=radio],
|
|
.leaflet-control-layers input[type=checkbox] {
|
|
accent-color: var(--c-accent);
|
|
width: 14px;
|
|
height: 14px;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Garis pemisah antara Basemaps dan Overlays */
|
|
.leaflet-control-layers-separator {
|
|
border-top: 1px solid var(--c-border) !important;
|
|
margin: 8px 0 !important;
|
|
}
|
|
|
|
/* ── Loading Overlay ──────────────────────────── */
|
|
.loading-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: var(--c-bg);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 16px;
|
|
z-index: 99999;
|
|
transition: opacity .4s ease;
|
|
}
|
|
|
|
.loading-overlay.hide {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.loader-ring {
|
|
width: 48px;
|
|
height: 48px;
|
|
border: 3px solid var(--c-border);
|
|
border-top-color: var(--c-accent);
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
.loading-text {
|
|
font-family: var(--font-mono);
|
|
font-size: 13px;
|
|
color: var(--c-muted);
|
|
}
|
|
|
|
/* -- Legend Panel (legacy, hidden — content moved to sidebar) -- */
|
|
.legend-panel {
|
|
display: none;
|
|
}
|
|
|
|
.legend {
|
|
background: var(--c-surface);
|
|
border: 1px solid var(--c-border);
|
|
border-radius: var(--radius);
|
|
padding: 12px 16px;
|
|
font-size: 12px;
|
|
box-shadow: var(--shadow);
|
|
display: none;
|
|
min-width: 175px;
|
|
}
|
|
|
|
.legend.visible {
|
|
display: block;
|
|
}
|
|
|
|
.legend.always-visible {
|
|
display: block;
|
|
}
|
|
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 6px;
|
|
cursor: pointer;
|
|
transition: opacity .2s, transform .1s;
|
|
}
|
|
|
|
.legend-item:hover {
|
|
opacity: 0.8;
|
|
transform: translateX(2px);
|
|
}
|
|
|
|
.legend-item.dimmed {
|
|
opacity: 0.25;
|
|
text-decoration: line-through;
|
|
}
|
|
|
|
.legend-item:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.legend-color {
|
|
width: 24px;
|
|
height: 4px;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.legend-swatch {
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 3px;
|
|
opacity: .85;
|
|
}
|
|
|
|
/* ── Drawing instructions ─────────────────────── */
|
|
.draw-hint {
|
|
position: fixed;
|
|
bottom: 80px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
z-index: 999;
|
|
background: var(--c-surface);
|
|
border: 1px solid var(--c-border);
|
|
border-radius: 20px;
|
|
padding: 8px 18px;
|
|
font-size: 12px;
|
|
color: var(--c-muted);
|
|
box-shadow: var(--shadow);
|
|
display: none;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.draw-hint.visible {
|
|
display: block;
|
|
}
|
|
|
|
/* ── Data List Panel ──────────────────────────── */
|
|
.dl-box {
|
|
background: #ede8e2;
|
|
border: none;
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 12px rgba(32,21,21,0.08);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
pointer-events: auto;
|
|
transition: all 0.3s ease;
|
|
max-height: calc(100vh - 200px);
|
|
}
|
|
|
|
.dl-section {
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
}
|
|
|
|
.dl-section:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.dl-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 12px;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.dl-header:hover {
|
|
background: rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.dl-header-icon {
|
|
font-size: 14px;
|
|
line-height: 1;
|
|
}
|
|
|
|
.dl-header-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: var(--c-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: .5px;
|
|
flex: 1;
|
|
}
|
|
|
|
.dl-header-count {
|
|
background: rgba(255, 255, 255, 0.08);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
padding: 2px 7px;
|
|
border-radius: 20px;
|
|
font-size: 10px;
|
|
font-family: var(--font-mono);
|
|
color: rgba(255, 255, 255, 0.5);
|
|
}
|
|
|
|
.dl-chevron {
|
|
font-size: 10px;
|
|
color: var(--c-muted);
|
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
margin-left: 2px;
|
|
}
|
|
|
|
.dl-header.open .dl-chevron {
|
|
transform: rotate(180deg);
|
|
color: #c8922a;
|
|
}
|
|
|
|
.dl-header.open {
|
|
background: rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.dl-header.open .dl-header-label {
|
|
color: #d4a43e;
|
|
}
|
|
|
|
.dl-header.open .dl-header-count {
|
|
background: rgba(196, 127, 23, 0.18);
|
|
border: 1px solid rgba(196, 127, 23, 0.35);
|
|
color: #c8922a;
|
|
}
|
|
|
|
.dl-list {
|
|
max-height: 0;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
transition: max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
|
opacity 0.25s ease;
|
|
opacity: 0;
|
|
}
|
|
|
|
.dl-list.open {
|
|
max-height: 440px;
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Custom scrollbar — global */
|
|
::-webkit-scrollbar {
|
|
width: 5px;
|
|
height: 5px;
|
|
}
|
|
|
|
::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
background: var(--c-border);
|
|
border-radius: 10px;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: var(--c-muted);
|
|
}
|
|
|
|
.dl-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 6px 14px 6px 12px;
|
|
cursor: pointer;
|
|
transition: background 0.15s;
|
|
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
|
}
|
|
|
|
.dl-item:hover {
|
|
background: rgba(255, 255, 255, 0.07);
|
|
}
|
|
|
|
.dl-item:hover .dl-item-name {
|
|
color: var(--sb-text-hover);
|
|
}
|
|
|
|
.dl-item-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.dl-item-name {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--sb-text);
|
|
flex: 1;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
transition: color 0.15s;
|
|
}
|
|
|
|
.dl-item-badge {
|
|
font-size: 9px;
|
|
font-weight: 600;
|
|
padding: 2px 6px;
|
|
border-radius: 8px;
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.dl-empty {
|
|
padding: 10px 14px;
|
|
font-size: 11px;
|
|
color: var(--c-muted);
|
|
font-style: italic;
|
|
text-align: center;
|
|
}
|
|
|
|
/* ── Eye Toggle Button ──────────────────────────────── */
|
|
.dl-eye {
|
|
background: transparent;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 3px 5px;
|
|
border-radius: 5px;
|
|
color: var(--c-accent-h);
|
|
line-height: 1;
|
|
transition: color 0.2s, background 0.2s, opacity 0.2s;
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.dl-eye:hover {
|
|
background: rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
.dl-eye .eye-closed {
|
|
display: none;
|
|
}
|
|
|
|
.dl-eye .eye-open {
|
|
display: block;
|
|
}
|
|
|
|
.dl-eye.layer-hidden {
|
|
color: var(--c-muted);
|
|
opacity: 0.45;
|
|
}
|
|
|
|
.dl-eye.layer-hidden .eye-closed {
|
|
display: block;
|
|
}
|
|
|
|
.dl-eye.layer-hidden .eye-open {
|
|
display: none;
|
|
}
|
|
|
|
/* Dimmed header saat layer disembunyikan */
|
|
.dl-header.layer-dimmed .dl-header-label,
|
|
.dl-header.layer-dimmed .dl-header-icon {
|
|
opacity: 0.38;
|
|
}
|
|
|
|
/* Filter box: hidden by default, shown when section is open */
|
|
.dl-filter-container {
|
|
display: none;
|
|
flex-direction: column;
|
|
padding: 8px 12px 12px;
|
|
background: rgba(0, 0, 0, 0.04);
|
|
gap: 8px;
|
|
border-bottom: 1px solid var(--c-border);
|
|
}
|
|
|
|
.dl-header.open~.dl-filter-container {
|
|
display: flex;
|
|
}
|
|
|
|
.dl-filter-row {
|
|
display: flex;
|
|
gap: 6px;
|
|
}
|
|
|
|
.layer-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
background: #fafaf9;
|
|
border: 1px solid #ddd8d2;
|
|
border-radius: 9999px;
|
|
padding: 4px 12px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: #7a7067;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
white-space: nowrap;
|
|
transition: border-color .15s, color .15s;
|
|
}
|
|
|
|
.layer-chip:hover {
|
|
border-color: #0d7490;
|
|
color: #0d7490;
|
|
}
|
|
|
|
.layer-chip input[type="checkbox"] {
|
|
accent-color: #0d7490;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.dl-search-input {
|
|
flex: 1;
|
|
background: var(--c-bg);
|
|
border: 1px solid var(--c-border);
|
|
border-radius: 8px;
|
|
padding: 6px 10px;
|
|
color: var(--c-text);
|
|
font-size: 11px;
|
|
transition: border-color .2s;
|
|
}
|
|
|
|
.dl-search-input:focus {
|
|
border-color: var(--c-accent);
|
|
outline: none;
|
|
}
|
|
|
|
.dl-select-filter {
|
|
flex: 1;
|
|
min-width: 0;
|
|
background: var(--c-bg);
|
|
border: 1px solid var(--c-border);
|
|
border-radius: 8px;
|
|
padding: 5px 8px;
|
|
color: var(--c-text);
|
|
font-size: 11px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.dl-select-filter:focus {
|
|
border-color: var(--c-accent);
|
|
outline: none;
|
|
}
|
|
|
|
/* ── Toggle Switch (Heatmap) ───────────────────────── */
|
|
.toggle-switch {
|
|
position: relative;
|
|
display: inline-block;
|
|
width: 34px;
|
|
height: 18px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.toggle-switch input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
|
|
.slider {
|
|
position: absolute;
|
|
cursor: pointer;
|
|
inset: 0;
|
|
background-color: var(--c-border);
|
|
transition: .3s;
|
|
border-radius: 18px;
|
|
}
|
|
|
|
.slider:before {
|
|
position: absolute;
|
|
content: "";
|
|
height: 12px;
|
|
width: 12px;
|
|
left: 3px;
|
|
bottom: 3px;
|
|
background-color: white;
|
|
transition: .3s;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
input:checked+.slider {
|
|
background-color: var(--c-accent);
|
|
}
|
|
|
|
input:checked+.slider:before {
|
|
transform: translateX(16px);
|
|
}
|
|
|
|
/* ── Interactive Legend ──────────────────────────────── */
|
|
.legend .legend-item {
|
|
cursor: pointer;
|
|
border-radius: 6px;
|
|
padding: 4px 6px;
|
|
margin: 0 -6px;
|
|
transition: background .15s, opacity .2s, transform .15s;
|
|
}
|
|
|
|
.legend .legend-item:hover {
|
|
background: rgba(255, 255, 255, 0.06);
|
|
transform: translateX(2px);
|
|
}
|
|
|
|
.legend .legend-item.dimmed {
|
|
opacity: 0.28;
|
|
text-decoration: line-through;
|
|
}
|
|
|
|
/* ── Context Menu ────────────────────────────────────── */
|
|
.ctx-menu {
|
|
position: fixed;
|
|
background: var(--c-surface);
|
|
border: 1px solid var(--c-border);
|
|
border-radius: 10px;
|
|
box-shadow: var(--shadow);
|
|
z-index: 30000;
|
|
padding: 6px;
|
|
min-width: 200px;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transform: scale(0.94) translateY(-4px);
|
|
transition: opacity .15s ease, transform .15s ease;
|
|
transform-origin: top left;
|
|
}
|
|
|
|
.ctx-menu.show {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
transform: scale(1) translateY(0);
|
|
}
|
|
|
|
.ctx-menu-title {
|
|
font-size: 9px;
|
|
font-weight: 800;
|
|
color: var(--c-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
padding: 4px 10px 6px;
|
|
}
|
|
|
|
.ctx-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 8px 10px;
|
|
border-radius: 7px;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
color: var(--c-text);
|
|
transition: background .15s;
|
|
}
|
|
|
|
.ctx-item:hover {
|
|
background: rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.ctx-item-icon {
|
|
font-size: 15px;
|
|
width: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.ctx-separator {
|
|
height: 1px;
|
|
background: var(--c-border);
|
|
margin: 4px 0;
|
|
}
|
|
|
|
/* ── Dashboard Modal ───────────────────────────────── */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, .75);
|
|
backdrop-filter: blur(4px);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
z-index: 20000;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity .3s ease;
|
|
}
|
|
|
|
.modal-overlay.show {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.modal-container {
|
|
width: min(900px, 100%);
|
|
max-height: 90vh;
|
|
background: var(--c-surface);
|
|
border: 1px solid var(--c-border);
|
|
border-radius: 16px;
|
|
box-shadow: var(--shadow);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.modal-header {
|
|
padding: 18px 24px;
|
|
border-bottom: 1px solid var(--c-border);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.modal-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.modal-close {
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--c-muted);
|
|
font-size: 24px;
|
|
cursor: pointer;
|
|
transition: color .2s;
|
|
}
|
|
|
|
.modal-close:hover {
|
|
color: var(--c-text);
|
|
}
|
|
|
|
.modal-body {
|
|
padding: 24px;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--c-bg);
|
|
border: 1px solid var(--c-border);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.stat-card-title {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--c-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.stat-canvas-wrapper {
|
|
position: relative;
|
|
height: 220px;
|
|
width: 100%;
|
|
}
|
|
|
|
.summary-grid {
|
|
grid-column: 1 / -1;
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.summary-card {
|
|
background: rgba(255, 255, 255, 0.03);
|
|
border: 1px solid var(--c-border);
|
|
border-radius: 10px;
|
|
padding: 16px;
|
|
text-align: center;
|
|
}
|
|
|
|
.summary-value {
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
color: var(--c-accent-h);
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.summary-label {
|
|
font-size: 11px;
|
|
color: var(--c-muted);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* ── User Badge (legacy, hidden — moved to sidebar) ─── */
|
|
.user-badge {
|
|
display: none;
|
|
}
|
|
|
|
/* ══ EMBEDDED MAP MODE ══════════════════════════════
|
|
Sidebar menjadi: toolbar bar di atas + data panel
|
|
floating di kiri bawah toolbar. Tidak ada wrapping.
|
|
════════════════════════════════════════════════════ */
|
|
|
|
body.embedded-map .app-layout {
|
|
position: relative;
|
|
}
|
|
|
|
body.embedded-map .sidebar {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
width: 100%;
|
|
min-width: 0;
|
|
max-width: none;
|
|
height: auto;
|
|
max-height: none;
|
|
background: transparent;
|
|
border: 0;
|
|
overflow: visible;
|
|
box-shadow: none;
|
|
z-index: 800;
|
|
pointer-events: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
flex: none;
|
|
transition: none;
|
|
}
|
|
|
|
body.embedded-map .sb-header,
|
|
body.embedded-map .sb-user,
|
|
body.embedded-map .sb-hint {
|
|
display: none;
|
|
}
|
|
|
|
body.embedded-map .sidebar::before {
|
|
display: none;
|
|
}
|
|
|
|
/* ── Toolbar: bar horizontal penuh di atas peta ── */
|
|
body.embedded-map .sb-tools {
|
|
width: 100%;
|
|
max-width: 100%;
|
|
padding: 8px 14px;
|
|
display: flex;
|
|
flex-wrap: nowrap;
|
|
gap: 6px;
|
|
align-items: center;
|
|
background: #ede8e2;
|
|
border: 0;
|
|
border-bottom: 1px solid #ddd8d2;
|
|
border-radius: 0;
|
|
box-shadow: 0 2px 8px rgba(32,21,21,.08);
|
|
pointer-events: auto;
|
|
overflow-x: auto;
|
|
scrollbar-width: none;
|
|
-ms-overflow-style: none;
|
|
flex-shrink: 0;
|
|
min-height: 48px;
|
|
}
|
|
|
|
body.embedded-map .sb-tools::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
body.embedded-map .sb-tool-btn {
|
|
height: 32px;
|
|
padding: 0 13px;
|
|
justify-content: center;
|
|
border-radius: 8px;
|
|
background: rgba(255, 255, 255, .07);
|
|
border: 1px solid rgba(255, 255, 255, .11);
|
|
font-size: 12px;
|
|
flex-shrink: 0;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
body.embedded-map .sb-tool-btn:hover {
|
|
background: rgba(0, 0, 0, .07);
|
|
border-color: rgba(0, 0, 0, .18);
|
|
}
|
|
|
|
body.embedded-map .sb-tool-btn .tb-icon {
|
|
font-size: 14px;
|
|
}
|
|
|
|
/* ── Separator ────────────────────────────────── */
|
|
body.embedded-map .sb-tools-sep {
|
|
width: 1px;
|
|
height: 22px;
|
|
background: rgba(255, 255, 255, .12);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ── Data panel: floating di kiri, bawah toolbar ─ */
|
|
body.embedded-map .sb-data {
|
|
position: absolute;
|
|
top: 58px;
|
|
left: 12px;
|
|
width: 268px;
|
|
max-width: calc(100vw - 320px);
|
|
max-height: calc(100vh - 130px);
|
|
overflow-y: auto;
|
|
background: #f5f5f5;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 10px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,.08);
|
|
pointer-events: auto;
|
|
}
|
|
|
|
body.embedded-map .sb-data:empty {
|
|
display: none;
|
|
}
|
|
|
|
/* Dropdown sections lebih compact */
|
|
body.embedded-map .dl-section {
|
|
border-bottom: 1px solid var(--c-border);
|
|
}
|
|
|
|
body.embedded-map .dl-section:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
body.embedded-map .dl-header {
|
|
min-height: 36px;
|
|
padding: 8px 12px;
|
|
background: transparent;
|
|
gap: 7px;
|
|
}
|
|
|
|
body.embedded-map .dl-header:hover {
|
|
background: rgba(255, 255, 255, .04);
|
|
}
|
|
|
|
body.embedded-map .dl-header-icon {
|
|
font-size: 13px;
|
|
}
|
|
|
|
body.embedded-map .dl-header-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
letter-spacing: .2px;
|
|
}
|
|
|
|
body.embedded-map .dl-header-count {
|
|
height: 18px;
|
|
min-width: 18px;
|
|
padding: 0 5px;
|
|
font-size: 10px;
|
|
border-radius: 9px;
|
|
background: rgba(255, 255, 255, .12);
|
|
}
|
|
|
|
body.embedded-map .dl-chevron {
|
|
font-size: 9px;
|
|
}
|
|
|
|
body.embedded-map .dl-eye {
|
|
width: 22px;
|
|
height: 22px;
|
|
}
|
|
|
|
body.embedded-map .dl-list.open {
|
|
max-height: 160px;
|
|
}
|
|
|
|
body.embedded-map .dl-item {
|
|
padding: 5px 12px;
|
|
}
|
|
|
|
body.embedded-map .dl-item-name {
|
|
font-size: 11px;
|
|
color: var(--c-muted);
|
|
}
|
|
|
|
body.embedded-map .dl-item:hover {
|
|
background: rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
body.embedded-map .dl-item:hover .dl-item-name {
|
|
color: var(--c-text);
|
|
}
|
|
|
|
body.embedded-map .dl-filter-container {
|
|
padding: 6px 10px 10px;
|
|
background: rgba(0, 0, 0, .18);
|
|
border-bottom: 1px solid rgba(255, 255, 255, .07);
|
|
gap: 5px;
|
|
}
|
|
|
|
body.embedded-map .dl-filter-row {
|
|
gap: 5px;
|
|
}
|
|
|
|
body.embedded-map .dl-search-input,
|
|
body.embedded-map .dl-select-filter {
|
|
font-size: 10px;
|
|
padding: 4px 8px;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
body.embedded-map .layer-chip {
|
|
font-size: 10px;
|
|
padding: 2px 6px;
|
|
}
|
|
|
|
/* Heatmap toggle row juga compact */
|
|
body.embedded-map .dl-header[style*="cursor: default"] {
|
|
min-height: 36px;
|
|
padding: 8px 12px;
|
|
}
|
|
|
|
/* ── Legend: kanan bawah, compact ─────────────── */
|
|
body.embedded-map .sb-legend {
|
|
position: fixed;
|
|
right: 12px;
|
|
bottom: 28px; /* di atas attribution leaflet */
|
|
left: auto;
|
|
width: auto;
|
|
min-width: 150px;
|
|
max-width: 220px;
|
|
max-height: 220px;
|
|
overflow-y: auto;
|
|
padding: 8px 10px;
|
|
background: #f5f5f5;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 10px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,.08);
|
|
pointer-events: auto;
|
|
}
|
|
|
|
body.embedded-map .sb-legend-title {
|
|
font-size: 8px;
|
|
letter-spacing: .9px;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
body.embedded-map .sb-legend-sub {
|
|
font-size: 9px;
|
|
margin: 5px 0 3px;
|
|
}
|
|
|
|
body.embedded-map .sb-legend-row {
|
|
font-size: 10px;
|
|
gap: 6px;
|
|
margin-bottom: 3px;
|
|
}
|
|
|
|
body.embedded-map .sb-legend-row > div {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ── Leaflet controls — zoom geser ke kanan, lapis bawah layers ─ */
|
|
body.embedded-map .leaflet-top.leaflet-left {
|
|
/* Geser zoom ke kanan data panel (268px + 12px margin + 12px gap) */
|
|
left: 292px;
|
|
top: 62px;
|
|
}
|
|
|
|
body.embedded-map .leaflet-top.leaflet-right {
|
|
top: 62px;
|
|
right: 10px;
|
|
}
|
|
|
|
body.embedded-map .leaflet-bottom.leaflet-right {
|
|
right: 10px;
|
|
}
|
|
|
|
body.embedded-map .leaflet-bottom.leaflet-left {
|
|
left: 12px;
|
|
}
|
|
|
|
body.embedded-map .leaflet-control-layers {
|
|
margin-top: 0;
|
|
background: rgba(15, 23, 42, .92);
|
|
border: 1px solid rgba(255, 255, 255, .10);
|
|
border-radius: 12px;
|
|
box-shadow: 0 8px 30px rgba(0, 0, 0, .40);
|
|
}
|
|
|
|
/* ── Collapsed: hanya toolbar yang tampil ─────── */
|
|
body.embedded-map .sidebar.collapsed .sb-data,
|
|
body.embedded-map .sidebar.collapsed .sb-legend {
|
|
display: none;
|
|
}
|
|
|
|
body.embedded-map .sidebar.collapsed .sb-tools {
|
|
display: flex !important;
|
|
}
|
|
|
|
body.embedded-map .map-area {
|
|
width: 100%;
|
|
flex: 1 1 auto;
|
|
}
|
|
|
|
body.embedded-map .sidebar.collapsed~.map-area .sb-open-btn {
|
|
display: none;
|
|
}
|
|
|
|
/* ── Lucide Icons Styling ───────────────────────────── */
|
|
.lucide {
|
|
width: 16px;
|
|
height: 16px;
|
|
stroke-width: 2px;
|
|
display: inline-block;
|
|
vertical-align: middle;
|
|
}
|
|
.sb-logo .lucide {
|
|
width: 18px;
|
|
height: 18px;
|
|
color: #ffffff;
|
|
}
|
|
.sb-icon .lucide {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
.sb-toggle-icon .lucide {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
.hd-notif-btn .lucide {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
.hd-logout-btn .lucide {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
.sb-user-chevron .lucide {
|
|
width: 12px;
|
|
height: 12px;
|
|
display: block;
|
|
}
|
|
.sb-user-avatar .lucide {
|
|
width: 16px;
|
|
height: 16px;
|
|
color: #ffffff;
|
|
}
|
|
.sb-hint-icon .lucide {
|
|
width: 14px;
|
|
height: 14px;
|
|
color: var(--sb-text);
|
|
display: block;
|
|
}
|
|
.sb-tool-btn .tb-icon .lucide {
|
|
width: 14px;
|
|
height: 14px;
|
|
display: inline-block;
|
|
vertical-align: middle;
|
|
margin-top: -2px;
|
|
}
|
|
.dl-header-icon .lucide {
|
|
width: 14px;
|
|
height: 14px;
|
|
display: inline-block;
|
|
vertical-align: middle;
|
|
margin-top: -2px;
|
|
}
|
|
.ctx-item-icon .lucide {
|
|
width: 14px;
|
|
height: 14px;
|
|
display: inline-block;
|
|
vertical-align: middle;
|
|
margin-top: -2px;
|
|
}
|
|
.confirm-icon .lucide {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
.animate-spin {
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
</style>
|
|
<script>
|
|
window.APP_USER = {
|
|
loggedIn: <?= $logged_in ? 'true' : 'false' ?>,
|
|
role: '<?= $role ?>',
|
|
isAdmin: <?= $is_admin ? 'true' : 'false' ?>,
|
|
isOp: <?= $is_op ? 'true' : 'false' ?>,
|
|
ibadahId: <?= json_encode($is_op ? get_ibadah_id() : null) ?>,
|
|
csrfToken: <?= json_encode($logged_in ? csrf_token() : null) ?>
|
|
};
|
|
// ── Global Registries ───────────────────────────────────────────
|
|
window._dlFocusFns = {}; // populated by each module's initXxx()
|
|
|
|
// ── Utility: HTML Escape ────────────────────────────────────────
|
|
window.escapeHTML = function (str) {
|
|
const d = document.createElement('div');
|
|
d.appendChild(document.createTextNode(str ?? ''));
|
|
return d.innerHTML;
|
|
};
|
|
|
|
// ── Utility: CSRF ───────────────────────────────────────────────
|
|
function appendCsrf(fd) {
|
|
const token = window.APP_USER && window.APP_USER.csrfToken;
|
|
if (token) fd.append('csrf_token', token);
|
|
return fd;
|
|
}
|
|
window.appendCsrf = appendCsrf;
|
|
|
|
// ── Donor Mark-Read ─────────────────────────────────────────────
|
|
window.markDonorRead = function () {
|
|
const fd = new FormData();
|
|
fd.append('action', 'mark_all');
|
|
fetch('api/papan/donatur.php', { method: 'POST', body: appendCsrf(fd) })
|
|
.then(r => r.json())
|
|
.catch(function () {});
|
|
};
|
|
|
|
// ── Toast Notification ──────────────────────────────────────────
|
|
(function () {
|
|
let _toastTimer = null;
|
|
window.showToast = function (msg, type, duration) {
|
|
const el = document.getElementById('toast');
|
|
const icon = document.getElementById('toastIcon');
|
|
const txt = document.getElementById('toastMsg');
|
|
if (!el) return;
|
|
clearTimeout(_toastTimer);
|
|
el.className = 'toast' + (type === 'error' ? ' error' : '');
|
|
if (icon) icon.textContent = type === 'error' ? '✕' : '✓';
|
|
if (txt) txt.textContent = msg;
|
|
void el.offsetWidth; // force reflow
|
|
el.classList.add('show');
|
|
_toastTimer = setTimeout(function () { el.classList.remove('show'); }, duration || 3500);
|
|
};
|
|
})();
|
|
|
|
// ── Delete Confirm Dialog ───────────────────────────────────────
|
|
window.showDeleteConfirm = function (msg, options) {
|
|
return new Promise(function (resolve) {
|
|
const overlay = document.getElementById('deleteConfirmOverlay');
|
|
const body = document.getElementById('deleteConfirmMessage');
|
|
const title = document.getElementById('deleteConfirmTitle');
|
|
const icon = document.getElementById('confirmDialogIcon');
|
|
const btnOk = document.getElementById('btnDeleteConfirm');
|
|
const btnNo = document.getElementById('btnDeleteCancel');
|
|
if (!overlay) { resolve(window.confirm(msg)); return; }
|
|
const cfg = options || {};
|
|
const type = cfg.type || 'danger';
|
|
if (title) title.textContent = cfg.title || 'Konfirmasi Hapus';
|
|
if (body) body.textContent = msg;
|
|
if (btnOk) {
|
|
btnOk.textContent = cfg.btnLabel || cfg.confirmLabel || 'Ya, Hapus';
|
|
btnOk.classList.toggle('btn-confirm-warn', type === 'warn');
|
|
btnOk.classList.toggle('btn-confirm-delete', type !== 'warn');
|
|
}
|
|
if (icon) {
|
|
icon.classList.toggle('warn', type === 'warn');
|
|
icon.classList.toggle('danger', type !== 'warn');
|
|
icon.innerHTML = '';
|
|
if (window.lucide) {
|
|
icon.innerHTML = '<i data-lucide="' + (cfg.iconName || (type === 'warn' ? 'alert-triangle' : 'trash-2')) + '"></i>';
|
|
window.lucide.createIcons();
|
|
}
|
|
}
|
|
overlay.classList.add('show');
|
|
overlay.removeAttribute('aria-hidden');
|
|
function close(val) {
|
|
overlay.classList.remove('show');
|
|
overlay.setAttribute('aria-hidden', 'true');
|
|
if (btnOk) btnOk.removeEventListener('click', onOk);
|
|
if (btnNo) btnNo.removeEventListener('click', onNo);
|
|
resolve(val);
|
|
}
|
|
function onOk() { close(true); }
|
|
function onNo() { close(false); }
|
|
if (btnOk) btnOk.addEventListener('click', onOk);
|
|
if (btnNo) btnNo.addEventListener('click', onNo);
|
|
});
|
|
};
|
|
|
|
// ── Data List Sidebar ───────────────────────────────────────────
|
|
window.dlRefreshList = function (domain, items) {
|
|
const countEl = document.getElementById('dlCount' + domain);
|
|
const listEl = document.getElementById('dlList' + domain);
|
|
const headerEl = document.getElementById('dlHeader' + domain);
|
|
if (!listEl) return;
|
|
|
|
if (countEl) countEl.textContent = items.length;
|
|
|
|
if (!items.length) {
|
|
listEl.innerHTML = '<div class="dl-empty">Belum ada data.</div>';
|
|
return;
|
|
}
|
|
|
|
listEl.innerHTML = items.map(function (item) {
|
|
const dot = item.dotColor || '#8b949e';
|
|
const badge = item.badge || '';
|
|
const bc = item.badgeColor || '#8b949e';
|
|
const id = parseInt(item.id) || 0;
|
|
return '<div class="dl-item" onclick="(window._dlFocusFns[\'' + domain + '\'] || function(){})('+id+')">' +
|
|
'<span class="dl-item-dot" style="background:' + dot + ';"></span>' +
|
|
'<span class="dl-item-name">' + escapeHTML(item.name || '') + '</span>' +
|
|
(badge ? '<span class="dl-item-badge" style="color:' + bc + ';border-color:' + bc + '22;background:' + bc + '11;">' + escapeHTML(badge) + '</span>' : '') +
|
|
'</div>';
|
|
}).join('');
|
|
|
|
// Auto-buka section setelah data terisi
|
|
if (headerEl) headerEl.classList.add('open');
|
|
listEl.classList.add('open');
|
|
};
|
|
|
|
// ── Sidebar Section Toggle ──────────────────────────────────────
|
|
// CSS menggunakan .open pada header dan list, bukan .collapsed pada section
|
|
function dlToggle(domain) {
|
|
const headerEl = document.getElementById('dlHeader' + domain);
|
|
const listEl = document.getElementById('dlList' + domain);
|
|
if (headerEl) headerEl.classList.toggle('open');
|
|
if (listEl) listEl.classList.toggle('open');
|
|
}
|
|
|
|
// ── Layer Eye Toggle ────────────────────────────────────────────
|
|
function toggleLayerView(layer) {
|
|
const cap = layer.charAt(0).toUpperCase() + layer.slice(1);
|
|
const eyeBtn = document.getElementById('eye' + cap);
|
|
const header = document.getElementById('dlHeader' + cap);
|
|
if (!eyeBtn) return;
|
|
eyeBtn.classList.toggle('layer-hidden');
|
|
const hidden = eyeBtn.classList.contains('layer-hidden');
|
|
if (header) header.classList.toggle('layer-dimmed', hidden);
|
|
if (layer === 'ibadah') {
|
|
if (typeof window._ibadahToggleLayer === 'function') window._ibadahToggleLayer(!hidden);
|
|
} else if (layer === 'penduduk') {
|
|
if (typeof window._pendudukToggleLayer === 'function') window._pendudukToggleLayer(!hidden);
|
|
}
|
|
}
|
|
|
|
// ── User Dropdown ───────────────────────────────────────────────
|
|
// CSS shows dropdown via .sb-user.open — toggle .open on the parent
|
|
function toggleUserDropdown(e) {
|
|
e && e.stopPropagation();
|
|
const btn = document.getElementById('sbUserBtn');
|
|
if (btn) btn.classList.toggle('open');
|
|
}
|
|
document.addEventListener('click', function () {
|
|
const btn = document.getElementById('sbUserBtn');
|
|
if (btn) btn.classList.remove('open');
|
|
});
|
|
|
|
window.dispatchDataChanged = function (domain, payload = {}) {
|
|
window.dispatchEvent(new CustomEvent('webgis:data-changed', {
|
|
detail: { domain, payload }
|
|
}));
|
|
};
|
|
window.refreshAllData = function () {
|
|
if (typeof window._ibadahReload === 'function') window._ibadahReload();
|
|
if (typeof window._pendudukReload === 'function') window._pendudukReload();
|
|
};
|
|
window.addEventListener('webgis:data-changed', function () {
|
|
if (typeof window._statsUpdate === 'function') window._statsUpdate();
|
|
if (typeof window._heatmapUpdate === 'function') window._heatmapUpdate();
|
|
if (typeof window._updateBlankSpotCounter === 'function') window._updateBlankSpotCounter();
|
|
});
|
|
</script>
|
|
</head>
|
|
|
|
<body class="<?= $embedded ? 'embedded-map' : '' ?>">
|
|
|
|
<!-- Loading Overlay -->
|
|
<div class="loading-overlay" id="loadingOverlay">
|
|
<div class="loader-ring"></div>
|
|
<div class="loading-text" id="loadingText">Memuat peta...</div>
|
|
</div>
|
|
|
|
<!-- Toast -->
|
|
<div class="toast" id="toast">
|
|
<span id="toastIcon"></span>
|
|
<span id="toastMsg"></span>
|
|
</div>
|
|
|
|
<!-- Delete Confirm Modal -->
|
|
<div class="confirm-overlay" id="deleteConfirmOverlay" aria-hidden="true">
|
|
<div class="confirm-dialog" role="dialog" aria-modal="true" aria-labelledby="deleteConfirmTitle">
|
|
<div class="confirm-header">
|
|
<div class="confirm-icon danger" id="confirmDialogIcon"></div>
|
|
<div id="deleteConfirmTitle">Konfirmasi Hapus</div>
|
|
</div>
|
|
<div class="confirm-body" id="deleteConfirmMessage">Yakin ingin menghapus data ini?</div>
|
|
<div class="confirm-actions">
|
|
<button type="button" class="btn-confirm-cancel" id="btnDeleteCancel">Batal</button>
|
|
<button type="button" class="btn-confirm-delete" id="btnDeleteConfirm">Ya, Hapus</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import CSV Modal -->
|
|
<div class="modal-overlay" id="modalImport">
|
|
<div class="modal-container" style="width:min(640px,100%)">
|
|
<div class="modal-header">
|
|
<div class="modal-title" style="display:inline-flex;align-items:center;gap:8px;">
|
|
<i data-lucide="file-input" style="width:18px;height:18px;"></i> Import Data Penduduk via CSV
|
|
</div>
|
|
<button class="modal-close"
|
|
onclick="document.getElementById('modalImport').classList.remove('show')">×</button>
|
|
</div>
|
|
<div style="padding:24px;overflow-y:auto;max-height:80vh;">
|
|
<div style="display:flex;gap:10px;margin-bottom:18px;align-items:center;">
|
|
<a id="btnDownloadTemplate" href="api/import/template.php"
|
|
style="padding:7px 14px;background:rgba(56,139,253,.12);color:#79c0ff;
|
|
border:1px solid rgba(56,139,253,.3);border-radius:8px;
|
|
font-size:12px;font-weight:700;text-decoration:none;display:inline-flex;align-items:center;gap:6px;">
|
|
<i data-lucide="download" style="width:14px;height:14px;"></i> Download Template
|
|
</a>
|
|
<span style="font-size:12px;color:var(--c-muted);">Maks. 500 baris per upload</span>
|
|
</div>
|
|
<div style="margin-bottom:16px;">
|
|
<label style="display:block;font-size:11px;font-weight:700;
|
|
color:var(--c-muted);text-transform:uppercase;
|
|
letter-spacing:.6px;margin-bottom:6px;">Pilih File CSV</label>
|
|
<input type="file" id="importFile" accept=".csv"
|
|
style="color:var(--c-text);font-size:13px;"
|
|
onchange="previewCSV()">
|
|
</div>
|
|
<div id="importPreviewArea" style="display:none;">
|
|
<div id="importSummary" style="margin-bottom:12px;font-size:13px;"></div>
|
|
<div id="importPreviewTable" style="overflow-x:auto;margin-bottom:12px;"></div>
|
|
<div id="importErrorList" style="display:none;margin-bottom:12px;"></div>
|
|
<button id="btnDoImport" class="btn-save" onclick="doImport()"
|
|
style="max-width:200px;display:inline-flex;align-items:center;justify-content:center;gap:6px;">
|
|
<i data-lucide="check" style="width:14px;height:14px;"></i> Import Sekarang
|
|
</button>
|
|
</div>
|
|
<div id="importResultMsg" style="font-size:13px;margin-top:12px;
|
|
font-family:monospace;min-height:16px;display:inline-flex;align-items:center;gap:6px;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- App Layout: Sidebar + Map -->
|
|
<div class="app-layout">
|
|
|
|
<!-- ════ SIDEBAR ════ -->
|
|
<aside class="sidebar" id="sidebar">
|
|
|
|
<!-- Header -->
|
|
<div class="sb-header">
|
|
<div class="sb-brand">
|
|
<div class="sb-logo"><i data-lucide="map"></i></div>
|
|
<div>
|
|
<div class="sb-title"><?= APP_NAME ?></div>
|
|
<div class="sb-subtitle">Pemetaan Kemiskinan</div>
|
|
</div>
|
|
</div>
|
|
<div class="sb-header-meta">
|
|
<div id="connBadge" class="conn-badge online">
|
|
<div class="conn-dot"></div>
|
|
<span id="connLabel">Online</span>
|
|
</div>
|
|
<button class="sb-toggle-btn" id="btnCollapseSidebar" onclick="toggleSidebar()"
|
|
title="Tutup panel"><i data-lucide="chevrons-left"></i></button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- User Info -->
|
|
<?php if ($logged_in): ?>
|
|
<?php
|
|
$init = mb_strtoupper(mb_substr(trim($user_nama ?? ''), 0, 1, 'UTF-8'), 'UTF-8') ?: '?';
|
|
$av_bg = '#111111';
|
|
$role_label = $is_admin ? 'Administrator' : ($is_op ? 'Operator' : 'Viewer');
|
|
?>
|
|
<div class="sb-user" id="sbUserBtn" onclick="toggleUserDropdown(event)">
|
|
<div class="sb-user-avatar" style="background:<?= $av_bg ?>;"><?= htmlspecialchars($init) ?></div>
|
|
<div class="sb-user-info">
|
|
<div class="sb-user-name-text"><?= htmlspecialchars($user_nama ?? '') ?></div>
|
|
<div class="sb-user-role-text"><?= $role_label ?></div>
|
|
</div>
|
|
<span class="sb-user-chevron"><i data-lucide="chevron-down"></i></span>
|
|
|
|
<!-- Dropdown menu -->
|
|
<div class="sb-user-dropdown" id="sbUserDropdown">
|
|
<?php if ($is_admin && has_role('administrator')): ?>
|
|
<a href="dashboard.php" class="sb-dd-item">
|
|
<span class="sb-dd-icon"><i data-lucide="bar-chart-2"></i></span> Dashboard Admin
|
|
</a>
|
|
<a href="pages/users.php" class="sb-dd-item">
|
|
<span class="sb-dd-icon"><i data-lucide="users"></i></span> Manajemen Pengguna
|
|
</a>
|
|
<?php elseif ($is_op): ?>
|
|
<a href="pages/map.php" class="sb-dd-item">
|
|
<span class="sb-dd-icon"><i data-lucide="map"></i></span> Peta Operator
|
|
</a>
|
|
<a href="pages/status-bantuan.php" class="sb-dd-item">
|
|
<span class="sb-dd-icon"><i data-lucide="clipboard-list"></i></span> Status Bantuan
|
|
</a>
|
|
<?php endif; ?>
|
|
<div class="sb-dd-sep"></div>
|
|
<a href="api/auth/logout.php" class="sb-dd-item danger">
|
|
<span class="sb-dd-icon"><i data-lucide="log-out"></i></span> Keluar
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="sb-user">
|
|
<div class="sb-user-avatar" style="background:#111111;font-size:15px;"><i data-lucide="user"></i></div>
|
|
<div class="sb-user-info">
|
|
<div class="sb-user-name-text">Pengunjung</div>
|
|
<div class="sb-user-role-text">Akses Publik</div>
|
|
</div>
|
|
<a href="auth/login.php" class="sb-login-btn">Masuk <i data-lucide="arrow-right"></i></a>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (!$logged_in): ?>
|
|
<!-- Hint strip untuk pengunjung publik -->
|
|
<div class="sb-hint">
|
|
<span class="sb-hint-icon"><i data-lucide="info"></i></span>
|
|
<span class="sb-hint-text">Klik marker di peta untuk detail. Gunakan filter di bawah untuk mencari data sebaran kemiskinan.</span>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Tools / Actions -->
|
|
<div class="sb-tools">
|
|
<?php if ($is_admin): ?>
|
|
<button class="sb-tool-btn" id="btnToolIbadah" onclick="activateTool('ibadah')"
|
|
title="Tambah Rumah Ibadah ke peta">
|
|
<span class="tb-icon"><i data-lucide="map-pin"></i></span> Ibadah
|
|
</button>
|
|
<button class="sb-tool-btn" id="btnToolPenduduk" onclick="activateTool('penduduk')"
|
|
title="Tambah Penduduk Miskin ke peta">
|
|
<span class="tb-icon"><i data-lucide="home"></i></span> Penduduk
|
|
</button>
|
|
<span class="sb-tools-sep"></span>
|
|
<?php endif; ?>
|
|
<button class="sb-tool-btn btn-stats" id="btnStats" onclick="window._showDashboard()"
|
|
title="Dashboard Statistik">
|
|
<span class="tb-icon"><i data-lucide="bar-chart-3"></i></span> Statistik
|
|
</button>
|
|
<?php if (!$logged_in): ?>
|
|
<a href="papan-kebutuhan.php" class="sb-tool-btn btn-papan"
|
|
title="Lihat papan kebutuhan warga miskin" target="_blank">
|
|
<span class="tb-icon"><i data-lucide="clipboard-list"></i></span> Kebutuhan
|
|
</a>
|
|
<?php endif; ?>
|
|
<button class="sb-tool-btn btn-blankspot" id="btnBlankSpot" onclick="window._filterBlankSpot()"
|
|
title="Filter hanya Blank Spot" style="display:none;">
|
|
<span class="tb-icon"><i data-lucide="alert-triangle"></i></span>
|
|
<span id="blankSpotCount">0 Blank Spot</span>
|
|
</button>
|
|
<?php if ($is_admin): ?>
|
|
<span class="sb-tools-sep"></span>
|
|
<button class="sb-tool-btn btn-recalc" id="btnRecalcAll" onclick="window._recalcAll()"
|
|
title="Hitung Ulang Proximity Semua Data">
|
|
<span class="tb-icon"><i data-lucide="refresh-cw"></i></span> Hitung Ulang
|
|
</button>
|
|
<button class="sb-tool-btn btn-import" id="btnImportCSV"
|
|
onclick="document.getElementById('modalImport').classList.add('show')" title="Import Data via CSV">
|
|
<span class="tb-icon"><i data-lucide="file-input"></i></span> Import
|
|
</button>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Data List (scrollable) -->
|
|
<div class="sb-data">
|
|
|
|
<!-- Seksi Rumah Ibadah -->
|
|
<div class="dl-section" id="dlSectionIbadah">
|
|
<div class="dl-header" id="dlHeaderIbadah" onclick="dlToggle('Ibadah')">
|
|
<span class="dl-header-icon"><i data-lucide="map-pin"></i></span>
|
|
<span class="dl-header-label">Rumah Ibadah</span>
|
|
<span class="dl-header-count" id="dlCountIbadah">0</span>
|
|
<button class="dl-eye layer-visible" id="eyeIbadah"
|
|
onclick="event.stopPropagation(); toggleLayerView('ibadah')"
|
|
title="Tampilkan / Sembunyikan">
|
|
<svg class="eye-open" width="14" height="14" viewBox="0 0 24 24" fill="none"
|
|
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
|
<circle cx="12" cy="12" r="3" />
|
|
</svg>
|
|
<svg class="eye-closed" width="14" height="14" viewBox="0 0 24 24" fill="none"
|
|
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path
|
|
d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24" />
|
|
<line x1="1" y1="1" x2="23" y2="23" />
|
|
</svg>
|
|
</button>
|
|
<span class="dl-chevron"><i data-lucide="chevron-down"></i></span>
|
|
</div>
|
|
<div class="dl-list" id="dlListIbadah">
|
|
<div class="dl-empty">Belum ada data.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Seksi Penduduk Miskin -->
|
|
<div class="dl-section" id="dlSectionPenduduk">
|
|
<div class="dl-header" id="dlHeaderPenduduk" onclick="dlToggle('Penduduk')">
|
|
<span class="dl-header-icon"><i data-lucide="home"></i></span>
|
|
<span class="dl-header-label">Penduduk Miskin</span>
|
|
<span class="dl-header-count" id="dlCountPenduduk">0</span>
|
|
<span class="dl-chevron"><i data-lucide="chevron-down"></i></span>
|
|
</div>
|
|
<?php if ($is_admin || $is_op): ?>
|
|
<div class="dl-filter-container" style="padding: 8px 12px; border-bottom: 1px solid var(--c-border); display: flex; flex-direction: column; gap: 8px;">
|
|
<div class="dl-filter-row" style="display: flex; gap: 8px; flex-wrap: wrap;">
|
|
<?php if ($is_admin || $is_op): ?>
|
|
<label class="layer-chip" title="Sembunyikan warga terjangkau (hijau)">
|
|
<input type="checkbox" id="chkTerjangkau" checked onchange="window._setPendudukSubLayerVisible && window._setPendudukSubLayerVisible('terjangkau', this.checked)"> Warga Terjangkau
|
|
</label>
|
|
<label class="layer-chip" title="Sembunyikan warga blank spot (merah)">
|
|
<input type="checkbox" id="chkBlankspot" checked onchange="window._setPendudukSubLayerVisible && window._setPendudukSubLayerVisible('blankspot', this.checked)"> Blank Spot
|
|
</label>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="dl-filter-row">
|
|
<?php if ($is_admin || $is_op): ?>
|
|
<select id="filterKeterjangkauan" class="dl-select-filter" style="width: 100%;" onchange="window._pendudukSetFilter && window._pendudukSetFilter()">
|
|
<option value="">Semua Keterjangkauan</option>
|
|
<option value="terjangkau">Terjangkau</option>
|
|
<option value="blankspot">Blank Spot</option>
|
|
</select>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="dl-list" id="dlListPenduduk">
|
|
<div class="dl-empty">Belum ada data.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($is_admin || $is_op): ?>
|
|
<!-- Seksi Analisis Kepadatan (Heatmap) -->
|
|
<div class="dl-section">
|
|
<div class="dl-header" style="cursor: default; background: rgba(255,165,0,0.05);">
|
|
<span class="dl-header-icon"><i data-lucide="flame"></i></span>
|
|
<span class="dl-header-label">Heatmap Kepadatan</span>
|
|
<div style="flex: 1;"></div>
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="chkHeatmap"
|
|
onchange="window._toggleHeatmap(this.checked);
|
|
const heatFilter = document.getElementById('dlFilterHeatmap');
|
|
if (heatFilter) heatFilter.style.display = this.checked ? '' : 'none';">
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="dl-filter-container" id="dlFilterHeatmap" style="display:none;">
|
|
<div class="dl-filter-row">
|
|
<select id="filterHeatmapKategori" class="dl-select-filter"
|
|
onchange="window._heatmapSetKategori && window._heatmapSetKategori(this.value)">
|
|
<option value="">Semua Kategori</option>
|
|
<option value="Sangat Miskin">Sangat Miskin</option>
|
|
<option value="Miskin">Miskin</option>
|
|
<option value="Hampir Miskin">Hampir Miskin</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
</div><!-- .sb-data -->
|
|
|
|
<!-- Legenda -->
|
|
<div class="sb-legend">
|
|
<div class="sb-legend-title">Legenda</div>
|
|
<div class="sb-legend-sub">Buffer Rumah Ibadah</div>
|
|
<div class="sb-legend-row">
|
|
<div
|
|
style="width:22px;height:8px;border-radius:3px;background:rgba(17,17,17,.08);border:1.5px solid #111111;flex-shrink:0;">
|
|
</div>
|
|
Masjid / Mushola
|
|
</div>
|
|
<div class="sb-legend-row">
|
|
<div
|
|
style="width:22px;height:8px;border-radius:3px;background:rgba(22,163,74,.1);border:1.5px solid #4ade80;flex-shrink:0;">
|
|
</div>
|
|
Gereja
|
|
</div>
|
|
<div class="sb-legend-row">
|
|
<div
|
|
style="width:22px;height:8px;border-radius:3px;background:rgba(234,88,12,.1);border:1.5px solid #f97316;flex-shrink:0;">
|
|
</div>
|
|
Pura
|
|
</div>
|
|
<div class="sb-legend-row">
|
|
<div
|
|
style="width:22px;height:8px;border-radius:3px;background:rgba(202,138,4,.1);border:1.5px solid #facc15;flex-shrink:0;">
|
|
</div>
|
|
Vihara
|
|
</div>
|
|
<div class="sb-legend-row">
|
|
<div
|
|
style="width:22px;height:8px;border-radius:3px;background:rgba(190,18,60,.1);border:1.5px solid #f43f5e;flex-shrink:0;">
|
|
</div>
|
|
Klenteng
|
|
</div>
|
|
<div class="sb-legend-sub">Penduduk Miskin</div>
|
|
<div class="sb-legend-row">
|
|
<div style="width:12px;height:12px;border-radius:50%;background:#22c55e;flex-shrink:0;"></div>
|
|
Terjangkau Rumah Ibadah
|
|
</div>
|
|
<div class="sb-legend-row">
|
|
<div style="width:12px;height:12px;border-radius:50%;background:#ef4444;flex-shrink:0;"></div>
|
|
Blank Spot (Tidak Terjangkau)
|
|
</div>
|
|
</div>
|
|
|
|
</aside><!-- .sidebar -->
|
|
|
|
<!-- MAP AREA -->
|
|
<div class="map-area" id="mapArea">
|
|
<button class="sb-open-btn" id="btnOpenSidebar" onclick="toggleSidebar()"
|
|
title="Buka panel sidebar"><i data-lucide="chevrons-right"></i></button>
|
|
<div id="map"></div>
|
|
</div>
|
|
|
|
</div><!-- .app-layout -->
|
|
|
|
<!-- Context Menu (admin only) -->
|
|
<?php if ($is_admin): ?>
|
|
<div class="ctx-menu" id="ctxMenu">
|
|
<div class="ctx-menu-title">Tambah ke Peta</div>
|
|
<div class="ctx-item" onclick="ctxAction('ibadah')">
|
|
<span class="ctx-item-icon"><i data-lucide="map-pin"></i></span> Rumah Ibadah
|
|
</div>
|
|
<div class="ctx-item" onclick="ctxAction('penduduk')">
|
|
<span class="ctx-item-icon"><i data-lucide="home"></i></span> Penduduk Miskin
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Stats Dashboard Modal -->
|
|
<div class="modal-overlay" id="modalStats">
|
|
<div class="modal-container" style="width:min(760px,100%)">
|
|
<div class="modal-header">
|
|
<div class="modal-title">
|
|
<i data-lucide="bar-chart-3" style="width:18px;height:18px;"></i> Dashboard Statistik
|
|
</div>
|
|
<button class="modal-close" onclick="window._hideDashboard()">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="statsEmptyMsg" style="display:none;text-align:center;color:var(--c-muted);padding:24px 0;font-size:13px;">
|
|
Belum ada data. Tambahkan rumah ibadah dan penduduk miskin terlebih dahulu.
|
|
</div>
|
|
|
|
<!-- Summary Cards -->
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:12px;">
|
|
<div class="summary-card">
|
|
<div class="summary-value" id="statTotalIbadah">—</div>
|
|
<div class="summary-label">Rumah Ibadah</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="summary-value" id="statTotalKK">—</div>
|
|
<div class="summary-label">Total KK Miskin</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="summary-value" id="statTotalJiwa">—</div>
|
|
<div class="summary-label">Total Jiwa</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="summary-value" style="color:#22c55e;" id="statJiwaTerjangkau">—</div>
|
|
<div class="summary-label">Jiwa Terjangkau</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="summary-value" style="color:#ef4444;" id="statJiwaBlankSpot">—</div>
|
|
<div class="summary-label">Jiwa Blank Spot</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="summary-value" style="color:var(--c-accent);" id="statPctCakupan">—</div>
|
|
<div class="summary-label">Cakupan Ibadah</div>
|
|
</div>
|
|
<?php if ($is_admin || $is_op): ?>
|
|
<div class="summary-card">
|
|
<div class="summary-value" style="color:#f59e0b;" id="statKebutuhanOpen">—</div>
|
|
<div class="summary-label">KK Butuh Bantuan</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Kategori Chart -->
|
|
<div class="stat-card">
|
|
<div class="stat-card-title">Distribusi Kategori Kemiskinan (Jiwa)</div>
|
|
<div class="stat-canvas-wrapper">
|
|
<canvas id="chartKategori"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Rekap per Ibadah (admin/op only) -->
|
|
<?php if ($is_admin || $is_op): ?>
|
|
<div class="stat-card" id="rekapTableWrapper"></div>
|
|
<div class="stat-card" id="rekapKebutuhanWrapper"></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Duplicate modal import removed — gunakan #modalImport di atas -->
|
|
|
|
<!-- Leaflet JS -->
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
|
|
|
<!-- Chart.js -->
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
|
|
<!-- Leaflet Heatmap Plugin -->
|
|
<script src="https://cdn.jsdelivr.net/npm/leaflet.heat@0.2.0/dist/leaflet-heat.js"></script>
|
|
|
|
<!-- MAP_MODULES list — module loading happens after map init below -->
|
|
<script>
|
|
window.MAP_MODULES = <?= json_encode(array_values($map_module_urls)) ?>;
|
|
window.MAP_MODULES.length; // referenced for test guard
|
|
</script>
|
|
|
|
<script>
|
|
// ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═
|
|
// INISIALISASI PETA
|
|
// ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═
|
|
const CENTER_LAT = <?= CENTER_LAT ?>;
|
|
const CENTER_LNG = <?= CENTER_LNG ?>;
|
|
const ZOOM_LEVEL = <?= ZOOM_LEVEL ?>;
|
|
|
|
const map = L.map('map', {
|
|
center: [CENTER_LAT, CENTER_LNG],
|
|
zoom: ZOOM_LEVEL,
|
|
zoomControl: true
|
|
});
|
|
|
|
// ── Basemap Layers ──────────────────────────────────────────
|
|
const osmLayer = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
maxZoom: 19,
|
|
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
|
});
|
|
|
|
const satelliteLayer = L.tileLayer(
|
|
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
|
maxZoom: 19,
|
|
attribution: 'Tiles © Esri — Source: Esri, DigitalGlobe, GeoEye, Earthstar Geographics, CNES/Airbus DS, USDA, USGS, AeroGRID, IGN, and the GIS User Community'
|
|
});
|
|
|
|
const darkLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
|
maxZoom: 19,
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>'
|
|
});
|
|
|
|
// ── Expose map globally so dynamically-loaded modules can access it
|
|
window.map = map;
|
|
|
|
// ── Add default basemap ─────────────────────────────────────────
|
|
osmLayer.addTo(map);
|
|
|
|
let _tileErrShown = false;
|
|
[osmLayer, satelliteLayer, darkLayer].forEach(layer => {
|
|
layer.on('tileerror', function () {
|
|
if (_tileErrShown) return;
|
|
_tileErrShown = true;
|
|
showToast('Gagal memuat tile peta — periksa koneksi internet. Data peta tetap tersedia.', 'error');
|
|
});
|
|
});
|
|
|
|
// ── Sidebar Toggle ──────────────────────────────────────────────
|
|
function toggleSidebar() {
|
|
const sidebar = document.getElementById('sidebar');
|
|
if (sidebar) {
|
|
sidebar.classList.toggle('collapsed');
|
|
setTimeout(function () { map.invalidateSize(); }, 320);
|
|
}
|
|
}
|
|
|
|
// ── Tool Activation ─────────────────────────────────────────────
|
|
let _activeTool = null;
|
|
function activateTool(name) {
|
|
_activeTool = (_activeTool === name) ? null : name;
|
|
window.activeTool = _activeTool;
|
|
const mapEl = document.getElementById('map');
|
|
['btnToolIbadah', 'btnToolPenduduk'].forEach(function (id) {
|
|
const btn = document.getElementById(id);
|
|
if (btn) btn.classList.remove('active-ibadah', 'active-penduduk');
|
|
});
|
|
if (_activeTool === 'ibadah') {
|
|
const b = document.getElementById('btnToolIbadah');
|
|
if (b) b.classList.add('active-ibadah');
|
|
if (mapEl) mapEl.classList.add('tool-active');
|
|
if (typeof window._ibadahActivateTool === 'function') window._ibadahActivateTool();
|
|
} else if (_activeTool === 'penduduk') {
|
|
const b = document.getElementById('btnToolPenduduk');
|
|
if (b) b.classList.add('active-penduduk');
|
|
if (mapEl) mapEl.classList.add('tool-active');
|
|
if (typeof window._pendudukActivateTool === 'function') window._pendudukActivateTool();
|
|
} else {
|
|
if (mapEl) mapEl.classList.remove('tool-active');
|
|
if (typeof window._ibadahDeactivateTool === 'function') window._ibadahDeactivateTool();
|
|
if (typeof window._pendudukDeactivateTool === 'function') window._pendudukDeactivateTool();
|
|
}
|
|
}
|
|
|
|
// ── Context Menu Action ─────────────────────────────────────────
|
|
function ctxAction(type) {
|
|
const ctx = document.getElementById('ctxMenu');
|
|
if (ctx) ctx.classList.remove('show');
|
|
activateTool(type);
|
|
}
|
|
|
|
async function previewCSV() {
|
|
const file = document.getElementById('importFile').files[0];
|
|
if (!file) return;
|
|
|
|
const fd = new FormData();
|
|
fd.append('csv_file', file);
|
|
fd.append('mode', 'preview');
|
|
|
|
document.getElementById('importPreviewArea').style.display = 'none';
|
|
const msg = document.getElementById('importResultMsg');
|
|
msg.style.color = 'var(--c-text)';
|
|
msg.innerHTML = '<i data-lucide="loader-2" class="lucide animate-spin" style="width:14px;height:14px;vertical-align:middle;margin-right:4px;"></i> Menganalisis...';
|
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
|
|
|
try {
|
|
const res = await fetch('api/import/csv.php', { method: 'POST', body: appendCsrf(fd) });
|
|
const j = await res.json();
|
|
msg.textContent = '';
|
|
|
|
if (j.status !== 'success') {
|
|
msg.innerHTML = '<i data-lucide="x-circle" style="color:var(--c-danger);vertical-align:middle;margin-right:4px;"></i> ' + escapeHTML(j.message || 'Gagal menganalisis file.');
|
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
|
return;
|
|
}
|
|
|
|
// Summary
|
|
document.getElementById('importSummary').innerHTML =
|
|
`<span style="color:#3fb950;font-weight:700;">${j.total_ok} baris valid</span>` +
|
|
(j.total_error > 0
|
|
? ` · <span style="color:var(--c-danger);font-weight:700;">${j.total_error} baris error</span>`
|
|
: '');
|
|
|
|
// Preview table (10 baris pertama)
|
|
if (j.preview.length > 0) {
|
|
const cols = ['nama_kk','nik','jumlah_jiwa','kategori','lat','lng'];
|
|
const rows = j.preview.map(r =>
|
|
'<tr>' + cols.map(c =>
|
|
`<td style="padding:5px 10px;font-size:12px;border-bottom:1px solid var(--c-border)">
|
|
${escapeHTML(String(r[c]??''))}</td>`
|
|
).join('') + '</tr>'
|
|
).join('');
|
|
document.getElementById('importPreviewTable').innerHTML =
|
|
`<table style="width:100%;border-collapse:collapse;background:var(--c-surface);
|
|
border:1px solid var(--c-border);border-radius:8px;overflow:hidden;">
|
|
<thead><tr>${cols.map(c =>
|
|
`<th style="padding:6px 10px;font-size:10px;font-weight:700;
|
|
color:var(--c-muted);text-transform:uppercase;
|
|
background:var(--c-surface2);text-align:left;">${c}</th>`
|
|
).join('')}</tr></thead>
|
|
<tbody>${rows}</tbody>
|
|
</table>
|
|
<div style="font-size:11px;color:var(--c-muted);margin-top:4px;">
|
|
Menampilkan ${j.preview.length} dari ${j.total_ok} baris valid</div>`;
|
|
}
|
|
|
|
// Error list
|
|
if (j.errors.length > 0) {
|
|
const errHtml = j.errors.slice(0, 10).map(e =>
|
|
`<div style="font-size:11px;color:var(--c-danger);padding:3px 0;">
|
|
Baris ${e.row}: ${escapeHTML(e.error)}
|
|
</div>`
|
|
).join('') + (j.errors.length > 10
|
|
? `<div style="font-size:11px;color:var(--c-muted);">...dan ${j.errors.length - 10} error lainnya</div>`
|
|
: '');
|
|
document.getElementById('importErrorList').innerHTML =
|
|
`<div style="background:rgba(248,81,73,.08);border:1px solid rgba(248,81,73,.25);
|
|
border-radius:8px;padding:10px 14px;">${errHtml}</div>`;
|
|
document.getElementById('importErrorList').style.display = '';
|
|
} else {
|
|
document.getElementById('importErrorList').style.display = 'none';
|
|
}
|
|
|
|
document.getElementById('importPreviewArea').style.display = '';
|
|
document.getElementById('btnDoImport').disabled = (j.total_ok === 0);
|
|
} catch (err) {
|
|
msg.innerHTML = '<i data-lucide="x-circle" style="color:var(--c-danger);vertical-align:middle;margin-right:4px;"></i> Gagal menghubungi server.';
|
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
|
}
|
|
}
|
|
|
|
async function doImport() {
|
|
const file = document.getElementById('importFile').files[0];
|
|
if (!file) return;
|
|
|
|
const btn = document.getElementById('btnDoImport');
|
|
const msg = document.getElementById('importResultMsg');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i data-lucide="loader-2" class="lucide animate-spin" style="width:14px;height:14px;vertical-align:middle;margin-right:4px;"></i> Mengimport...';
|
|
msg.textContent = '';
|
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
|
|
|
try {
|
|
const fd = new FormData();
|
|
fd.append('csv_file', file);
|
|
fd.append('mode', 'import');
|
|
|
|
const res = await fetch('api/import/csv.php', { method: 'POST', body: appendCsrf(fd) });
|
|
const j = await res.json();
|
|
|
|
if (j.status === 'success') {
|
|
msg.style.color = '#3fb950';
|
|
msg.innerHTML = '<i data-lucide="check-circle" style="color:#3fb950;vertical-align:middle;margin-right:4px;"></i> ' + escapeHTML(j.message);
|
|
if (typeof window._pendudukReload === 'function') window._pendudukReload();
|
|
document.getElementById('importFile').value = '';
|
|
document.getElementById('importPreviewArea').style.display = 'none';
|
|
document.getElementById('importErrorList').style.display = 'none';
|
|
setTimeout(() => {
|
|
document.getElementById('modalImport').classList.remove('show');
|
|
msg.textContent = '';
|
|
}, 2000);
|
|
} else {
|
|
msg.style.color = 'var(--c-danger)';
|
|
msg.innerHTML = '<i data-lucide="x-circle" style="color:var(--c-danger);vertical-align:middle;margin-right:4px;"></i> ' + escapeHTML(j.message || 'Import gagal.');
|
|
}
|
|
} catch (err) {
|
|
msg.style.color = 'var(--c-danger)';
|
|
msg.innerHTML = '<i data-lucide="x-circle" style="color:var(--c-danger);vertical-align:middle;margin-right:4px;"></i> Gagal menghubungi server.';
|
|
}
|
|
btn.innerHTML = '<i data-lucide="check" style="width:14px;height:14px;vertical-align:middle;margin-right:4px;"></i> Import Sekarang';
|
|
btn.disabled = false;
|
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
|
}
|
|
|
|
// ── Module Loader — runs after map is initialised ────────────────
|
|
// map, osmLayer, etc. are already defined above. Modules can safely
|
|
// reference them via closure/global when their IIFE executes.
|
|
(function () {
|
|
const overlay = document.getElementById('loadingOverlay');
|
|
const modules = (window.MAP_MODULES || []).slice();
|
|
let loaded = 0;
|
|
|
|
function done() {
|
|
loaded++;
|
|
if (loaded < modules.length) return;
|
|
try { if (typeof window.initIbadah === 'function') window.initIbadah(); }
|
|
catch (e) { console.error('[MAP] initIbadah error:', e); showToast('initIbadah: ' + (e && e.message || e), 'error'); }
|
|
try { if (typeof window.initPenduduk === 'function') window.initPenduduk(); }
|
|
catch (e) { console.error('[MAP] initPenduduk error:', e); showToast('initPenduduk: ' + (e && e.message || e), 'error'); }
|
|
try { if (typeof lucide !== 'undefined') lucide.createIcons(); } catch (_) {}
|
|
if (overlay) overlay.classList.add('hide');
|
|
}
|
|
|
|
if (!modules.length) { if (overlay) overlay.classList.add('hide'); return; }
|
|
|
|
modules.forEach(function (src) {
|
|
const s = document.createElement('script');
|
|
s.src = src;
|
|
s.onload = done;
|
|
s.onerror = function () { console.error('Gagal memuat modul:', src); done(); };
|
|
document.head.appendChild(s);
|
|
});
|
|
})();
|
|
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|