Upload perdana untuk deploy
@@ -0,0 +1,21 @@
|
||||
-- ============================================================
|
||||
-- add_users.sql — Tabel Users untuk Login & Role
|
||||
-- ============================================================
|
||||
|
||||
USE webgis;
|
||||
|
||||
-- Tabel Users
|
||||
DROP TABLE IF EXISTS users;
|
||||
CREATE TABLE users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
role ENUM('admin','petugas','guest') NOT NULL DEFAULT 'guest',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- Insert default accounts (password: 'password')
|
||||
INSERT INTO users (username, password, role) VALUES
|
||||
('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin'),
|
||||
('petugas', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'petugas'),
|
||||
('guest', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'guest');
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
// 1. Fetch all fasilitas_publik
|
||||
$stmtFp = $pdo->query("SELECT * FROM fasilitas_publik ORDER BY id DESC");
|
||||
$fasilitasPublik = $stmtFp->fetchAll();
|
||||
|
||||
// 2. Fetch all penduduk_miskin (dengan nama FP jika ada join)
|
||||
$stmtPm = $pdo->query("
|
||||
SELECT p.*, f.nama as fp_nama
|
||||
FROM penduduk_miskin p
|
||||
LEFT JOIN fasilitas_publik f ON p.fasilitas_publik_id = f.id
|
||||
ORDER BY p.id DESC
|
||||
");
|
||||
$pendudukMiskin = $stmtPm->fetchAll();
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'fasilitasPublik' => $fasilitasPublik,
|
||||
'pendudukMiskin' => $pendudukMiskin
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Database error: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
include '../koneksi.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$id = $_GET['id'] ?? null;
|
||||
if (!$id) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'ID tidak diberikan']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT * FROM points WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($data) {
|
||||
echo json_encode(['status' => 'success', 'data' => $data]);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak ditemukan']);
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || !in_array($_SESSION['role'], ['admin', 'petugas'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$status_bantuan = trim($_POST['status_bantuan'] ?? '');
|
||||
|
||||
if (!$id || !$status_bantuan) {
|
||||
echo json_encode(['status'=>'error','message'=>'Data tidak valid.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("UPDATE penduduk_miskin SET status_bantuan=? WHERE id=?");
|
||||
$stmt->execute([$status_bantuan, $id]);
|
||||
echo json_encode(['status'=>'success']);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
session_start();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include 'koneksi.php';
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
try {
|
||||
if ($action === 'approve_petugas') {
|
||||
$id = (int)$_POST['id'];
|
||||
$stmt = $pdo->prepare("UPDATE users SET status = 'active' WHERE id = ? AND role = 'petugas'");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode(['status' => 'success']);
|
||||
}
|
||||
elseif ($action === 'reject_petugas') {
|
||||
$id = (int)$_POST['id'];
|
||||
$stmt = $pdo->prepare("DELETE FROM users WHERE id = ? AND role = 'petugas' AND status = 'pending'");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode(['status' => 'success']);
|
||||
}
|
||||
elseif ($action === 'toggle_setting') {
|
||||
$key = $_POST['key'];
|
||||
$val = $_POST['value'];
|
||||
$stmt = $pdo->prepare("UPDATE settings SET setting_value = ? WHERE setting_key = ?");
|
||||
$stmt->execute([$val, $key]);
|
||||
echo json_encode(['status' => 'success']);
|
||||
}
|
||||
elseif ($action === 'update_user') {
|
||||
$id = (int)$_POST['id'];
|
||||
$role = $_POST['role'];
|
||||
$status = $_POST['status'];
|
||||
|
||||
// Cek jika ini adalah admin utama
|
||||
$stmt = $pdo->prepare("SELECT username FROM users WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$userTarget = $stmt->fetch();
|
||||
if ($userTarget && $userTarget['username'] === 'admin') {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Akun Admin Utama tidak dapat diubah hak akses atau statusnya.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Cek admin terakhir
|
||||
if ($role !== 'admin' || $status !== 'active') {
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM users WHERE role = 'admin' AND status = 'active' AND id != ?");
|
||||
$stmt->execute([$id]);
|
||||
if ($stmt->fetchColumn() == 0) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Harus ada minimal satu admin aktif']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE users SET role = ?, status = ? WHERE id = ?");
|
||||
$stmt->execute([$role, $status, $id]);
|
||||
echo json_encode(['status' => 'success']);
|
||||
}
|
||||
elseif ($action === 'delete_user') {
|
||||
$id = (int)$_POST['id'];
|
||||
|
||||
$stmt = $pdo->prepare("SELECT username, role, status FROM users WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'User tidak ditemukan']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($user['username'] === 'admin') {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Akun Admin Utama tidak dapat dihapus.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Cek admin terakhir
|
||||
if ($user['role'] === 'admin' && $user['status'] === 'active') {
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM users WHERE role = 'admin' AND status = 'active' AND id != ?");
|
||||
$stmt->execute([$id]);
|
||||
if ($stmt->fetchColumn() == 0) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Tidak dapat menghapus admin aktif terakhir']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM users WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode(['status' => 'success']);
|
||||
}
|
||||
else {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid action']);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,609 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$role = $_SESSION['role'] ?? 'guest';
|
||||
$username = $_SESSION['username'] ?? 'User';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard – WebGIS Pengentasan Kemiskinan</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['"Plus Jakarta Sans"', 'sans-serif'],
|
||||
heading: ['"Plus Jakarta Sans"', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
background: '#f8fafc',
|
||||
foreground: '#0f172a',
|
||||
primary: { DEFAULT: '#e11d48', foreground: '#ffffff' },
|
||||
muted: { DEFAULT: '#f1f5f9', foreground: '#64748b' },
|
||||
border: '#e2e8f0',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
/* Custom Pink Scrollbar */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #e11d48; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #be123c; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background font-sans text-foreground h-screen flex flex-col relative overflow-hidden selection:bg-primary/20 selection:text-primary">
|
||||
|
||||
<!-- Ambient Background Effects -->
|
||||
<div aria-hidden="true" class="pointer-events-none fixed inset-0 z-0">
|
||||
<div class="absolute -top-40 -right-40 h-96 w-96 rounded-full bg-primary/10 blur-[100px]"></div>
|
||||
<div class="absolute top-1/3 -left-40 h-96 w-96 rounded-full bg-rose-500/10 blur-[100px]"></div>
|
||||
<div class="absolute -bottom-40 left-1/2 h-96 w-96 -translate-x-1/2 rounded-full bg-primary/5 blur-[100px]"></div>
|
||||
<!-- Grid pattern -->
|
||||
<div class="absolute inset-0 opacity-[0.03]" style="background-image: linear-gradient(to right, #0f172a 1px, transparent 1px), linear-gradient(to bottom, #0f172a 1px, transparent 1px); background-size: 40px 40px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Header Navigation -->
|
||||
<header class="relative z-10 w-full border-b border-border/60 bg-white/60 backdrop-blur-xl">
|
||||
<div class="mx-auto flex h-16 max-w-6xl items-center justify-between px-6 lg:px-8">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-primary to-rose-700 text-white shadow-md shadow-primary/20">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-5 w-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M12 21s-7-5.6-7-11a7 7 0 1 1 14 0c0 5.4-7 11-7 11Z" />
|
||||
<circle cx="12" cy="10" r="2.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="font-heading text-sm font-bold tracking-wide text-foreground">WebGIS Geospasial</h1>
|
||||
<p class="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">Kementerian Analisis Spasial</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="hidden sm:flex flex-col items-end mr-2">
|
||||
<span class="text-sm font-bold text-foreground capitalize"><?php echo htmlspecialchars($username); ?></span>
|
||||
<span class="text-xs font-semibold text-primary capitalize bg-primary/10 px-2 py-0.5 rounded-full"><?php echo htmlspecialchars($role); ?></span>
|
||||
</div>
|
||||
<div class="h-9 w-9 rounded-full bg-muted border border-border flex items-center justify-center text-muted-foreground font-bold overflow-hidden">
|
||||
<!-- User Avatar Placeholder -->
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
|
||||
</div>
|
||||
<div class="w-px h-6 bg-border mx-1"></div>
|
||||
<a href="logout.php" class="text-sm font-medium text-muted-foreground hover:text-primary transition-colors flex items-center gap-1.5" title="Keluar">
|
||||
<span class="hidden sm:inline">Keluar</span>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="relative z-10 mx-auto w-full max-w-6xl px-6 py-12 lg:px-8 flex-1 overflow-y-auto">
|
||||
|
||||
<!-- Welcome Section -->
|
||||
<div class="mb-12 max-w-3xl">
|
||||
<div class="mb-3 inline-flex items-center gap-2 rounded-full bg-white px-3 py-1 text-xs font-semibold text-primary ring-1 ring-border shadow-sm">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-primary animate-pulse"></span>
|
||||
Sistem Aktif
|
||||
</div>
|
||||
<h2 class="font-heading text-3xl sm:text-4xl font-extrabold tracking-tight text-foreground mb-3">
|
||||
Halo, <span class="text-primary capitalize"><?php echo htmlspecialchars($username); ?></span> 👋
|
||||
</h2>
|
||||
<p class="text-base text-muted-foreground leading-relaxed">
|
||||
Pilih modul analisis spasial yang ingin Anda kerjakan hari ini. Sistem menyediakan berbagai perangkat untuk pemetaan, digitasi, dan analisis data kemiskinan terpadu.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Modules Grid -->
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
|
||||
<!-- Module 1: SPBU -->
|
||||
<a href="index.php?modul=spbu" class="group relative flex flex-col justify-between rounded-3xl border border-white/60 bg-white/70 p-6 shadow-sm backdrop-blur-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-[0_20px_40px_-15px_rgba(225,29,72,0.15)] hover:border-primary/30">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-orange-100 text-orange-600 transition-transform group-hover:scale-110 group-hover:rotate-3 shadow-inner">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 22v-8c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2v8"/><path d="M11 22H3"/><path d="M14 9V4a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v5"/><path d="M14 13h2a2 2 0 0 1 2 2v2a2 2 0 0 0 2 2h0a2 2 0 0 0 2-2V9.83a2 2 0 0 0-.59-1.42L18 5"/></svg>
|
||||
</div>
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-muted/50 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 group-hover:bg-primary/10 group-hover:text-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-heading text-lg font-bold text-foreground mb-2 group-hover:text-primary transition-colors">Pemetaan SPBU</h3>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||
Kelola titik lokasi SPBU, pantau status operasional 24 jam, dan update informasi layanan bahan bakar.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Module 2: Bantuan -->
|
||||
<a href="index.php?modul=bantuan" class="group relative flex flex-col justify-between rounded-3xl border border-white/60 bg-white/70 p-6 shadow-sm backdrop-blur-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-[0_20px_40px_-15px_rgba(225,29,72,0.15)] hover:border-primary/30">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-100 text-emerald-600 transition-transform group-hover:scale-110 group-hover:rotate-3 shadow-inner">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
|
||||
</div>
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-muted/50 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 group-hover:bg-primary/10 group-hover:text-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-heading text-lg font-bold text-foreground mb-2 group-hover:text-primary transition-colors">Analisis Bantuan</h3>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||
Data spasial penerima bantuan kemiskinan, upload dokumentasi foto rumah dan KK secara terpadu.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Module 3: Tematik -->
|
||||
<a href="index.php?modul=tematik" class="group relative flex flex-col justify-between rounded-3xl border border-white/60 bg-white/70 p-6 shadow-sm backdrop-blur-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-[0_20px_40px_-15px_rgba(225,29,72,0.15)] hover:border-primary/30">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-blue-100 text-blue-600 transition-transform group-hover:scale-110 group-hover:rotate-3 shadow-inner">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="3 6 9 3 15 6 21 3 21 18 15 21 9 18 3 21"></polygon><line x1="9" y1="3" x2="9" y2="18"></line><line x1="15" y1="6" x2="15" y2="21"></line></svg>
|
||||
</div>
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-muted/50 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 group-hover:bg-primary/10 group-hover:text-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-heading text-lg font-bold text-foreground mb-2 group-hover:text-primary transition-colors">Peta Tematik</h3>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||
Visualisasi sebaran wilayah rawan bencana (Banjir, Longsor, Kebakaran) melalui layer GeoJSON yang interaktif.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Module 4: Digitasi -->
|
||||
<a href="index.php?modul=digitasi" class="group relative flex flex-col justify-between rounded-3xl border border-white/60 bg-white/70 p-6 shadow-sm backdrop-blur-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-[0_20px_40px_-15px_rgba(225,29,72,0.15)] hover:border-primary/30">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-purple-100 text-purple-600 transition-transform group-hover:scale-110 group-hover:rotate-3 shadow-inner">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"></path><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path></svg>
|
||||
</div>
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-muted/50 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 group-hover:bg-primary/10 group-hover:text-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-heading text-lg font-bold text-foreground mb-2 group-hover:text-primary transition-colors">Digitasi Lahan</h3>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||
Fitur editing geometri spasial. Buat dan edit batas area (polygon) langsung di atas peta untuk pemetaan aset.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Module 5: Utuh -->
|
||||
<a href="index.php?modul=utuh" class="group relative flex flex-col justify-between rounded-3xl border border-white/60 bg-white/70 p-6 shadow-sm backdrop-blur-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-[0_20px_40px_-15px_rgba(225,29,72,0.15)] hover:border-primary/30 sm:col-span-2 lg:col-span-1">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-primary/10 text-primary transition-transform group-hover:scale-110 group-hover:rotate-3 shadow-inner">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
|
||||
</div>
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-muted/50 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 group-hover:bg-primary/10 group-hover:text-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-heading text-lg font-bold text-foreground mb-2 group-hover:text-primary transition-colors">WebGIS Terpadu</h3>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||
Tampilan peta keseluruhan yang menggabungkan semua layer dan modul secara bersamaan.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<?php if ($role === 'admin'): ?>
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
// Ambil data petugas pending
|
||||
$stmtPending = $pdo->query("SELECT id, username, email, nama_lengkap, created_at FROM users WHERE role = 'petugas' AND status = 'pending'");
|
||||
$pendingUsers = $stmtPending->fetchAll();
|
||||
|
||||
// Ambil semua data pengguna untuk manajemen
|
||||
$stmtAll = $pdo->query("SELECT id, username, email, nama_lengkap, role, status, created_at FROM users ORDER BY created_at DESC");
|
||||
$allUsers = $stmtAll->fetchAll();
|
||||
|
||||
// Ambil pengaturan show_demo_accounts
|
||||
$stmtSet = $pdo->query("SELECT setting_value FROM settings WHERE setting_key = 'show_demo_accounts'");
|
||||
$showDemo = '0';
|
||||
if ($r = $stmtSet->fetch()) $showDemo = $r['setting_value'];
|
||||
?>
|
||||
<!-- Admin Dashboard Section -->
|
||||
<div class="mt-12 space-y-8">
|
||||
<h2 class="font-heading text-2xl font-bold text-foreground">Panel Administrator</h2>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Approval Panel -->
|
||||
<div class="rounded-3xl border border-white/60 bg-white/70 p-6 shadow-sm backdrop-blur-xl">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h3 class="font-heading text-lg font-bold text-foreground">Approval Petugas Lapangan</h3>
|
||||
<span class="inline-flex items-center justify-center rounded-full bg-rose-100 px-2.5 py-0.5 text-xs font-bold text-rose-700">
|
||||
<?= count($pendingUsers) ?> Menunggu
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<?php if (count($pendingUsers) > 0): ?>
|
||||
<div class="overflow-x-auto rounded-xl border border-border">
|
||||
<table class="w-full text-left text-sm text-muted-foreground">
|
||||
<thead class="bg-muted/50 text-xs uppercase text-foreground">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Nama Lengkap</th>
|
||||
<th class="px-4 py-3">Email</th>
|
||||
<th class="px-4 py-3 text-right">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border">
|
||||
<?php foreach ($pendingUsers as $u): ?>
|
||||
<tr class="hover:bg-muted/30 transition-colors" id="row-<?= $u['id'] ?>">
|
||||
<td class="px-4 py-3 font-medium text-foreground"><?= htmlspecialchars($u['nama_lengkap'] ?: $u['username']) ?></td>
|
||||
<td class="px-4 py-3"><?= htmlspecialchars($u['email'] ?? '-') ?></td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<button onclick="handleAdminAction('approve_petugas', <?= $u['id'] ?>)" class="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-100 text-emerald-600 hover:bg-emerald-200 transition-colors" title="Setujui">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
||||
</button>
|
||||
<button onclick="handleAdminAction('reject_petugas', <?= $u['id'] ?>)" class="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-rose-100 text-rose-600 hover:bg-rose-200 transition-colors ml-1" title="Tolak">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="flex flex-col items-center justify-center py-8 text-center bg-muted/20 rounded-xl border border-dashed border-border">
|
||||
<span class="text-3xl mb-2">🎉</span>
|
||||
<p class="text-sm font-medium text-foreground">Tidak ada pendaftaran baru</p>
|
||||
<p class="text-xs text-muted-foreground mt-1">Semua petugas telah diverifikasi.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Settings Panel -->
|
||||
<div class="rounded-3xl border border-white/60 bg-white/70 p-6 shadow-sm backdrop-blur-xl">
|
||||
<h3 class="font-heading text-lg font-bold text-foreground mb-4">Pengaturan Sistem</h3>
|
||||
|
||||
<div class="flex items-center justify-between rounded-xl border border-border bg-white p-4">
|
||||
<div>
|
||||
<p class="font-semibold text-foreground text-sm">Tampilkan Tombol Akun Demo</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Memunculkan helper login untuk demonstrasi aplikasi.</p>
|
||||
</div>
|
||||
<label class="relative inline-flex cursor-pointer items-center">
|
||||
<input type="checkbox" id="toggleDemo" class="peer sr-only" <?= $showDemo === '1' ? 'checked' : '' ?> onchange="toggleSetting('show_demo_accounts', this.checked ? '1' : '0')">
|
||||
<div class="h-6 w-11 rounded-full bg-slate-200 after:absolute after:top-[2px] after:left-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-primary peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary/20"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manajemen Pengguna Section -->
|
||||
<div class="rounded-3xl border border-white/60 bg-white/70 p-6 shadow-sm backdrop-blur-xl mt-8">
|
||||
<div class="mb-6 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="font-heading text-lg font-bold text-foreground flex items-center gap-2">
|
||||
<span class="flex h-8 w-8 items-center justify-center rounded-lg bg-indigo-100 text-indigo-600">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
|
||||
</span>
|
||||
Manajemen Akun Terdaftar
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground mt-1">Atur hak akses (role) dan status pengguna sistem.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-h-[500px] overflow-y-auto overflow-x-auto rounded-xl border border-border bg-white relative">
|
||||
<table class="w-full text-left text-sm text-muted-foreground whitespace-nowrap">
|
||||
<thead class="bg-muted/50 text-xs uppercase text-foreground sticky top-0 z-10 shadow-sm">
|
||||
<tr>
|
||||
<th class="px-4 py-3 font-semibold">Nama Lengkap</th>
|
||||
<th class="px-4 py-3 font-semibold">Email / Username</th>
|
||||
<th class="px-4 py-3 font-semibold">Role</th>
|
||||
<th class="px-4 py-3 font-semibold">Status</th>
|
||||
<th class="px-4 py-3 font-semibold">Terdaftar</th>
|
||||
<th class="px-4 py-3 text-right font-semibold">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border">
|
||||
<?php foreach ($allUsers as $u): ?>
|
||||
<tr class="hover:bg-muted/30 transition-colors" id="user-row-<?= $u['id'] ?>">
|
||||
<td class="px-4 py-3 font-medium text-foreground">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-8 w-8 rounded-full bg-gradient-to-br from-slate-100 to-slate-200 border border-slate-300 flex items-center justify-center text-slate-600 font-bold">
|
||||
<?= strtoupper(substr($u['nama_lengkap'] ?: $u['username'], 0, 1)) ?>
|
||||
</div>
|
||||
<?= htmlspecialchars($u['nama_lengkap'] ?: '-') ?>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-foreground"><?= htmlspecialchars($u['email'] ?: '-') ?></span>
|
||||
<span class="text-xs text-muted-foreground">@<?= htmlspecialchars($u['username']) ?></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 relative">
|
||||
<?php if ($u['username'] === 'admin'): ?>
|
||||
<span class="inline-flex h-8 items-center justify-center rounded-full bg-slate-100 px-3 text-xs font-bold tracking-wide text-slate-500 border border-slate-200 shadow-sm cursor-not-allowed">
|
||||
Super Admin
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<div class="custom-select-wrapper relative inline-block min-w-[110px]" data-id="<?= $u['id'] ?>" data-type="role" data-value="<?= $u['role'] ?>">
|
||||
<button type="button" class="select-btn flex items-center justify-between w-full rounded-full border border-slate-200 bg-slate-50/50 py-1.5 pl-3 pr-2.5 text-xs font-bold tracking-wide text-slate-700 shadow-sm transition-all hover:bg-slate-100 hover:border-slate-300 focus:outline-none focus:ring-2 focus:ring-indigo-500/20">
|
||||
<span class="select-text capitalize"><?= $u['role'] ?></span>
|
||||
<svg class="h-3.5 w-3.5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"></path></svg>
|
||||
</button>
|
||||
<div class="select-menu hidden absolute z-50 left-0 mt-1.5 w-full rounded-2xl border border-white bg-white/95 backdrop-blur-xl p-1.5 shadow-[0_10px_30px_rgba(0,0,0,0.08)]">
|
||||
<div class="select-option cursor-pointer rounded-xl px-3 py-2 text-xs font-bold tracking-wide text-slate-600 hover:bg-indigo-50 hover:text-indigo-700 transition-colors mb-1" data-val="admin">Admin</div>
|
||||
<div class="select-option cursor-pointer rounded-xl px-3 py-2 text-xs font-bold tracking-wide text-slate-600 hover:bg-indigo-50 hover:text-indigo-700 transition-colors mb-1" data-val="petugas">Petugas</div>
|
||||
<div class="select-option cursor-pointer rounded-xl px-3 py-2 text-xs font-bold tracking-wide text-slate-600 hover:bg-indigo-50 hover:text-indigo-700 transition-colors" data-val="guest">Guest</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-4 py-3 relative">
|
||||
<?php if ($u['username'] === 'admin'): ?>
|
||||
<span class="inline-flex h-8 items-center justify-center rounded-full bg-emerald-50 px-3 text-xs font-bold tracking-wide text-emerald-700 border border-emerald-200 shadow-sm cursor-not-allowed">
|
||||
Active
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<div class="custom-select-wrapper relative inline-block min-w-[110px]" data-id="<?= $u['id'] ?>" data-type="status" data-value="<?= $u['status'] ?>">
|
||||
<button type="button" class="select-btn flex items-center justify-between w-full rounded-full border py-1.5 pl-3 pr-2.5 text-xs font-bold tracking-wide shadow-sm transition-all focus:outline-none focus:ring-2 focus:ring-offset-1 <?= $u['status'] === 'active' ? 'border-emerald-200 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 focus:ring-emerald-500/30' : 'border-amber-200 bg-amber-50 text-amber-700 hover:bg-amber-100 focus:ring-amber-500/30' ?>">
|
||||
<span class="select-text capitalize"><?= $u['status'] ?></span>
|
||||
<svg class="h-3.5 w-3.5 <?= $u['status'] === 'active' ? 'text-emerald-500' : 'text-amber-500' ?>" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"></path></svg>
|
||||
</button>
|
||||
<div class="select-menu hidden absolute z-50 left-0 mt-1.5 w-full rounded-2xl border border-white bg-white/95 backdrop-blur-xl p-1.5 shadow-[0_10px_30px_rgba(0,0,0,0.08)]">
|
||||
<div class="select-option cursor-pointer rounded-xl px-3 py-2 text-xs font-bold tracking-wide text-emerald-700 hover:bg-emerald-100 transition-colors mb-1" data-val="active">Active</div>
|
||||
<div class="select-option cursor-pointer rounded-xl px-3 py-2 text-xs font-bold tracking-wide text-amber-700 hover:bg-amber-100 transition-colors" data-val="pending">Pending</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<?= date('d M Y, H:i', strtotime($u['created_at'])) ?>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<?php if ($u['id'] != $_SESSION['user_id']): // Jangan tampilkan tombol hapus untuk diri sendiri ?>
|
||||
<button onclick="deleteUser(<?= $u['id'] ?>)" class="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-rose-50 text-rose-600 hover:bg-rose-100 border border-rose-100 transition-colors" title="Hapus Akun">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"></path><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<span class="inline-flex h-8 items-center justify-center rounded-lg bg-slate-100 px-2 text-xs font-semibold text-slate-400 border border-slate-200 cursor-not-allowed">Anda</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Admin JS Scripts -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
function handleAdminAction(action, id) {
|
||||
Swal.fire({
|
||||
title: action === 'approve_petugas' ? 'Setujui Petugas?' : 'Tolak & Hapus?',
|
||||
text: action === 'approve_petugas' ? 'Akun ini akan langsung aktif.' : 'Akun ini akan dihapus permanen.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: action === 'approve_petugas' ? '#10b981' : '#e11d48',
|
||||
cancelButtonColor: '#64748b',
|
||||
confirmButtonText: 'Ya, Lanjutkan!',
|
||||
cancelButtonText: 'Batal'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
const formData = new FormData();
|
||||
formData.append('action', action);
|
||||
formData.append('id', id);
|
||||
|
||||
fetch('api_admin.php', { method: 'POST', body: formData })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
Swal.fire({
|
||||
icon: 'success', title: 'Berhasil', showConfirmButton: false, timer: 1500
|
||||
}).then(() => location.reload());
|
||||
} else {
|
||||
Swal.fire('Error', data.message, 'error');
|
||||
}
|
||||
}).catch(err => Swal.fire('Error', 'Terjadi kesalahan sistem.', 'error'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSetting(key, val) {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'toggle_setting');
|
||||
formData.append('key', key);
|
||||
formData.append('value', val);
|
||||
|
||||
fetch('api_admin.php', { method: 'POST', body: formData })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
const Toast = Swal.mixin({
|
||||
toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, timerProgressBar: true
|
||||
});
|
||||
Toast.fire({ icon: 'success', title: 'Pengaturan disimpan' });
|
||||
} else {
|
||||
Swal.fire('Error', data.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
if(!e.target.closest('.custom-select-wrapper') && !e.target.closest('.select-menu')) {
|
||||
document.querySelectorAll('.select-menu:not(.hidden)').forEach(m => {
|
||||
m.classList.add('hidden');
|
||||
const orig = document.querySelector(`.custom-select-wrapper[data-id="${m.dataset.ownerId}"][data-type="${m.dataset.ownerType}"]`);
|
||||
if(orig) orig.appendChild(m);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
document.querySelectorAll('.select-menu:not(.hidden)').forEach(m => {
|
||||
m.classList.add('hidden');
|
||||
const orig = document.querySelector(`.custom-select-wrapper[data-id="${m.dataset.ownerId}"][data-type="${m.dataset.ownerType}"]`);
|
||||
if(orig) orig.appendChild(m);
|
||||
});
|
||||
}, true);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
document.querySelectorAll('.select-menu:not(.hidden)').forEach(m => {
|
||||
m.classList.add('hidden');
|
||||
const orig = document.querySelector(`.custom-select-wrapper[data-id="${m.dataset.ownerId}"][data-type="${m.dataset.ownerType}"]`);
|
||||
if(orig) orig.appendChild(m);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.custom-select-wrapper').forEach(wrapper => {
|
||||
const btn = wrapper.querySelector('.select-btn');
|
||||
const menu = wrapper.querySelector('.select-menu');
|
||||
const textSpan = wrapper.querySelector('.select-text');
|
||||
const type = wrapper.dataset.type;
|
||||
const id = wrapper.dataset.id;
|
||||
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// Hide other open menus and return them to their wrappers
|
||||
document.querySelectorAll('.select-menu:not(.hidden)').forEach(m => {
|
||||
if(m !== menu) {
|
||||
m.classList.add('hidden');
|
||||
const orig = document.querySelector(`.custom-select-wrapper[data-id="${m.dataset.ownerId}"][data-type="${m.dataset.ownerType}"]`);
|
||||
if(orig) orig.appendChild(m);
|
||||
}
|
||||
});
|
||||
|
||||
if (menu.classList.contains('hidden')) {
|
||||
menu.classList.remove('hidden');
|
||||
|
||||
// Pindahkan ke body agar tidak terpotong oleh overflow tabel atau terjebak backdrop-blur
|
||||
menu.dataset.ownerId = id;
|
||||
menu.dataset.ownerType = type;
|
||||
document.body.appendChild(menu);
|
||||
|
||||
// Hitung posisi absolut terhadap dokumen
|
||||
const rect = btn.getBoundingClientRect();
|
||||
menu.style.position = 'absolute';
|
||||
menu.style.top = (rect.bottom + window.scrollY + 4) + 'px';
|
||||
menu.style.left = (rect.left + window.scrollX) + 'px';
|
||||
menu.style.width = rect.width + 'px';
|
||||
menu.style.zIndex = '99999';
|
||||
} else {
|
||||
menu.classList.add('hidden');
|
||||
wrapper.appendChild(menu);
|
||||
}
|
||||
});
|
||||
|
||||
menu.querySelectorAll('.select-option').forEach(opt => {
|
||||
opt.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const val = opt.dataset.val;
|
||||
wrapper.dataset.value = val;
|
||||
textSpan.textContent = val;
|
||||
|
||||
// Kembalikan ke wrapper
|
||||
menu.classList.add('hidden');
|
||||
wrapper.appendChild(menu);
|
||||
|
||||
if(type === 'status') {
|
||||
const icon = btn.querySelector('svg');
|
||||
if(val === 'active') {
|
||||
btn.className = "select-btn flex items-center justify-between w-full rounded-full border py-1.5 pl-3 pr-2.5 text-xs font-bold tracking-wide shadow-sm transition-all focus:outline-none focus:ring-2 focus:ring-offset-1 border-emerald-200 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 focus:ring-emerald-500/30";
|
||||
icon.className = "h-3.5 w-3.5 text-emerald-500";
|
||||
} else {
|
||||
btn.className = "select-btn flex items-center justify-between w-full rounded-full border py-1.5 pl-3 pr-2.5 text-xs font-bold tracking-wide shadow-sm transition-all focus:outline-none focus:ring-2 focus:ring-offset-1 border-amber-200 bg-amber-50 text-amber-700 hover:bg-amber-100 focus:ring-amber-500/30";
|
||||
icon.className = "h-3.5 w-3.5 text-amber-500";
|
||||
}
|
||||
}
|
||||
|
||||
updateUserCustom(id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function updateUserCustom(id) {
|
||||
const roleWrapper = document.querySelector(`.custom-select-wrapper[data-id="${id}"][data-type="role"]`);
|
||||
const statusWrapper = document.querySelector(`.custom-select-wrapper[data-id="${id}"][data-type="status"]`);
|
||||
|
||||
const role = roleWrapper ? roleWrapper.dataset.value : '';
|
||||
const status = statusWrapper ? statusWrapper.dataset.value : '';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'update_user');
|
||||
formData.append('id', id);
|
||||
formData.append('role', role);
|
||||
formData.append('status', status);
|
||||
|
||||
fetch('api_admin.php', { method: 'POST', body: formData })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
const Toast = Swal.mixin({
|
||||
toast: true, position: 'top-end', showConfirmButton: false, timer: 2000, timerProgressBar: true
|
||||
});
|
||||
Toast.fire({ icon: 'success', title: 'Akun berhasil diperbarui' });
|
||||
} else {
|
||||
Swal.fire('Error', data.message, 'error').then(() => location.reload());
|
||||
}
|
||||
}).catch(err => Swal.fire('Error', 'Terjadi kesalahan jaringan.', 'error'));
|
||||
}
|
||||
|
||||
function deleteUser(id) {
|
||||
Swal.fire({
|
||||
title: 'Hapus Akun?',
|
||||
text: 'Akun ini akan dihapus permanen beserta aksesnya.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#e11d48',
|
||||
cancelButtonColor: '#64748b',
|
||||
confirmButtonText: 'Ya, Hapus!',
|
||||
cancelButtonText: 'Batal'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'delete_user');
|
||||
formData.append('id', id);
|
||||
|
||||
fetch('api_admin.php', { method: 'POST', body: formData })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
const row = document.getElementById('user-row-' + id);
|
||||
if (row) row.remove();
|
||||
Swal.fire({
|
||||
icon: 'success', title: 'Dihapus', text: 'Akun berhasil dihapus.', showConfirmButton: false, timer: 1500
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Error', data.message, 'error');
|
||||
}
|
||||
}).catch(err => Swal.fire('Error', 'Terjadi kesalahan sistem.', 'error'));
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="relative z-10 w-full mt-auto py-8">
|
||||
<div class="mx-auto max-w-6xl px-6 text-center lg:px-8">
|
||||
<p class="text-sm text-muted-foreground font-medium">
|
||||
© <?php echo date('Y'); ?> WebGIS Geospasial · Kementerian Analisis Spasial
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +1,12 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak. Hanya admin yang dapat menghapus data.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || !in_array($_SESSION['role'], ['admin', 'petugas'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// fasilitas_publik/simpan.php
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || !in_array($_SESSION['role'], ['admin', 'petugas'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
/**
|
||||
* Script to fix broken emoji encoding in app.js
|
||||
* Replaces mojibake characters with HTML entity codes
|
||||
*/
|
||||
|
||||
$file = __DIR__ . '/assets/js/app.js';
|
||||
$content = file_get_contents($file);
|
||||
|
||||
if ($content === false) {
|
||||
die("ERROR: Cannot read file: $file\n");
|
||||
}
|
||||
|
||||
$original_length = strlen($content);
|
||||
|
||||
// Map of broken mojibake -> correct HTML entity replacement
|
||||
$replacements = [
|
||||
// Broken trash can emoji (ðŸ—') -> HTML entity for 🗑
|
||||
"\xC3\xB0\xC5\xB8\xE2\x80\x97\xE2\x80\x99" => "🗑",
|
||||
|
||||
// Broken mosque emoji (🕌) -> HTML entity for 🏢
|
||||
"\xC3\xB0\xC5\xB8\xE2\x80\xA2\xC5\x92" => "🏢",
|
||||
|
||||
// Broken person emoji (ðŸ'¤) -> HTML entity for 👤
|
||||
"\xC3\xB0\xC5\xB8\xE2\x80\x98\xC2\xA4" => "👤",
|
||||
|
||||
// Broken satellite emoji (ðŸ"¡) -> HTML entity for 📡
|
||||
"\xC3\xB0\xC5\xB8\xE2\x80\x9C\xC2\xA1" => "📡",
|
||||
|
||||
// Broken pencil (âœ) -> HTML entity for ✏️
|
||||
"\xC3\xA2\xC5\x93" => "✏️",
|
||||
|
||||
// Broken square emoji (â¬) -> HTML entity for 📐
|
||||
"\xC3\xA2\xC2\xAC" => "📐",
|
||||
|
||||
// Broken m² (m²) -> correct m²
|
||||
"m\xC3\x82\xC2\xB2" => "m²",
|
||||
|
||||
// Broken info icon (ⓘ)
|
||||
"\xE2\x93\x98" => "ℹ",
|
||||
|
||||
// Broken warning icon (âš )
|
||||
"\xC3\xA2\xC5\xA1\xC2\xA0" => "⚠",
|
||||
|
||||
// Broken bullet (•)
|
||||
"\xC3\xA2\xE2\x80\x9A\xC2\xAC" => "•",
|
||||
];
|
||||
|
||||
$count = 0;
|
||||
foreach ($replacements as $broken => $fixed) {
|
||||
$occurrences = substr_count($content, $broken);
|
||||
if ($occurrences > 0) {
|
||||
$content = str_replace($broken, $fixed, $content);
|
||||
echo "Fixed: '$fixed' ($occurrences occurrences)\n";
|
||||
$count += $occurrences;
|
||||
}
|
||||
}
|
||||
|
||||
if ($count === 0) {
|
||||
echo "No broken emoji patterns found. Trying alternative byte patterns...\n";
|
||||
|
||||
// Try reading the file as raw bytes and searching for patterns
|
||||
$hex = bin2hex($content);
|
||||
|
||||
// Let's search for common patterns around "Hapus" text
|
||||
$hapus_pos = strpos($content, 'Hapus');
|
||||
if ($hapus_pos !== false) {
|
||||
// Look at the bytes right before "Hapus" to see what the broken emoji looks like
|
||||
$before = substr($content, max(0, $hapus_pos - 20), 20);
|
||||
echo "Bytes before first 'Hapus': " . bin2hex($before) . "\n";
|
||||
echo "Chars before first 'Hapus': " . $before . "\n";
|
||||
}
|
||||
|
||||
// Search for "Edit" button text
|
||||
$edit_pos = strpos($content, 'Edit</button>');
|
||||
if ($edit_pos !== false) {
|
||||
$before = substr($content, max(0, $edit_pos - 10), 10);
|
||||
echo "Bytes before 'Edit': " . bin2hex($before) . "\n";
|
||||
}
|
||||
|
||||
// Search for mosque pattern near "item.nama"
|
||||
$fp_title = strpos($content, '${item.nama}');
|
||||
if ($fp_title !== false) {
|
||||
$before = substr($content, max(0, $fp_title - 15), 15);
|
||||
echo "Bytes before '\${item.nama}': " . bin2hex($before) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
if ($count > 0) {
|
||||
// Backup
|
||||
copy($file, $file . '.bak');
|
||||
|
||||
// Write fixed content
|
||||
file_put_contents($file, $content);
|
||||
echo "\nTotal: $count emoji fixes applied.\n";
|
||||
echo "Backup saved as: app.js.bak\n";
|
||||
} else {
|
||||
echo "\nNo fixes applied.\n";
|
||||
}
|
||||
@@ -1,9 +1,28 @@
|
||||
<?php
|
||||
<?php
|
||||
// ============================================================
|
||||
// index.php – Halaman Utama WebGIS
|
||||
// ============================================================
|
||||
session_start();
|
||||
|
||||
// Cek apakah sudah login
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$currentUser = $_SESSION['username'];
|
||||
$currentRole = $_SESSION['role'];
|
||||
$modul = $_GET['modul'] ?? 'utuh';
|
||||
|
||||
include 'koneksi.php';
|
||||
|
||||
// Eksekusi ALTER TABLE secara diam-diam jika kolom belum ada
|
||||
try {
|
||||
$pdo->exec("ALTER TABLE penduduk_miskin ADD COLUMN status_bantuan ENUM('Belum Disalurkan', 'Proses', 'Sudah Disalurkan') DEFAULT 'Belum Disalurkan'");
|
||||
} catch(PDOException $e) {
|
||||
// Abaikan error jika kolom sudah ada
|
||||
}
|
||||
|
||||
// Ambil semua data
|
||||
$stmt = $pdo->query("SELECT * FROM points ORDER BY id DESC");
|
||||
$points = $stmt->fetchAll();
|
||||
@@ -26,6 +45,10 @@ $totalPM = count($pendudukMiskin);
|
||||
$ditangani = count(array_filter($pendudukMiskin, fn($p) => $p['status_jangkauan'] === 'Dalam Jangkauan'));
|
||||
$belumDitangani = $totalPM - $ditangani;
|
||||
$persen = $totalPM > 0 ? round(($ditangani / $totalPM) * 100, 1) : 0;
|
||||
|
||||
$totalSPBU = count($points);
|
||||
$spbu24 = count(array_filter($points, fn($p) => $p['buka_24_jam'] === 'Ya'));
|
||||
$spbuTidak = $totalSPBU - $spbu24;
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
@@ -38,200 +61,437 @@ $persen = $totalPM > 0 ? round(($ditangani / $totalPM) * 100, 1) : 0;
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" 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=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<!-- App CSS -->
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=1">
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
|
||||
<!-- SweetAlert2 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<!-- Lottie Web -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.12.2/lottie.min.js"></script>
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=36">
|
||||
<style>
|
||||
/* Sticky Header Sidebar */
|
||||
.sidebar-top-sticky {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-bottom: 1px solid transparent;
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Saat di-scroll */
|
||||
.sidebar-top-sticky.scrolled {
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
|
||||
border-bottom: 1px solid var(--sidebar-border);
|
||||
}
|
||||
|
||||
/* Animasi logo sub */
|
||||
.logo-sub {
|
||||
transition: all 0.3s ease;
|
||||
max-height: 20px;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-top-sticky.scrolled .btn-back-dashboard {
|
||||
padding: 8px 16px !important;
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
|
||||
.sidebar-top-sticky.scrolled .sidebar-header {
|
||||
padding: 10px 16px !important;
|
||||
}
|
||||
|
||||
.sidebar-top-sticky.scrolled .logo-sub {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.sidebar-top-sticky.scrolled .logo-icon svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.sidebar-scrollable {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar Merah Putih untuk sidebar-scrollable */
|
||||
.sidebar-scrollable::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.sidebar-scrollable::-webkit-scrollbar-track {
|
||||
background: rgba(0,0,0,0.02);
|
||||
}
|
||||
.sidebar-scrollable::-webkit-scrollbar-thumb {
|
||||
background: rgba(225, 29, 72, 0.2); /* Merah transparan */
|
||||
border-radius: 6px;
|
||||
}
|
||||
.sidebar-scrollable::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(225, 29, 72, 0.5); /* Merah saat dihover */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
|
||||
<!-- ============================================================
|
||||
SPLASH SCREEN PRELOADER
|
||||
============================================================ -->
|
||||
<div id="splashScreen" style="position:fixed; inset:0; z-index:9999; background:rgba(15, 23, 42, 0.85); backdrop-filter:blur(16px); -webkit-backdrop-filter:blur(16px); display:flex; flex-direction:column; align-items:center; justify-content:center; transition:opacity 0.5s ease, visibility 0.5s ease;">
|
||||
<!-- Lottie Container -->
|
||||
<div id="lottieContainer" style="width:250px; height:250px; margin-bottom:20px;"></div>
|
||||
<!-- Pulse Text -->
|
||||
<div style="color:white; font-family:'Plus Jakarta Sans', sans-serif; font-weight:600; font-size:16px; letter-spacing:0.5px; animation:pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;">
|
||||
Menyiapkan Engine Geospasial...
|
||||
</div>
|
||||
<style>
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: .5; }
|
||||
}
|
||||
.splash-hidden {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================
|
||||
SIDEBAR
|
||||
â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• -->
|
||||
============================================================ -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-top-sticky" id="sidebarSticky">
|
||||
<a href="dashboard.php" class="btn-back-dashboard" style="display:flex;align-items:center;gap:8px;padding:12px 16px;color:var(--sidebar-text);text-decoration:none;border-bottom:1px solid var(--sidebar-border);transition:all 0.3s;font-size:0.85rem;font-weight:600;">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
||||
Kembali ke Dashboard
|
||||
</a>
|
||||
|
||||
<!-- Header Sidebar -->
|
||||
<div class="sidebar-header">
|
||||
<div class="sidebar-logo">
|
||||
<div class="logo-icon">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z" fill="currentColor" opacity=".9"/>
|
||||
<circle cx="12" cy="9" r="2.5" fill="white"/>
|
||||
<!-- Header Sidebar -->
|
||||
<div class="sidebar-header">
|
||||
<div class="sidebar-logo">
|
||||
<div class="logo-icon">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z" fill="currentColor" opacity=".9"/>
|
||||
<circle cx="12" cy="9" r="2.5" fill="white"/>
|
||||
</svg>
|
||||
</div>
|
||||
<?php
|
||||
$modulNames = [
|
||||
'utuh' => 'Semua Modul',
|
||||
'spbu' => 'Modul SPBU',
|
||||
'bantuan' => 'Modul Bantuan',
|
||||
'tematik' => 'Peta Tematik',
|
||||
'digitasi' => 'Modul Digitasi'
|
||||
];
|
||||
$modulTitle = $modulNames[$modul] ?? 'Analisis Spasial';
|
||||
?>
|
||||
<div class="logo-text">
|
||||
<span class="logo-title">WebGIS</span>
|
||||
<span class="logo-sub" style="color: var(--accent); font-weight: 700;"><?= $modulTitle ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-toggle" id="sidebarToggle" title="Sembunyikan Sidebar">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<path d="M15 18l-6-6 6-6"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="logo-text">
|
||||
<span class="logo-title">WebGIS</span>
|
||||
<span class="logo-sub">Analisis Spasial</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<button class="sidebar-toggle" id="sidebarToggle" title="Sembunyikan Sidebar">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<path d="M15 18l-6-6 6-6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Kotak Pencarian Baru -->
|
||||
<div class="sidebar-search">
|
||||
<div class="search-input-wrap">
|
||||
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
<input type="text" id="searchInput" placeholder="Cari nama, NIK, atau lokasi..." autocomplete="off">
|
||||
</div>
|
||||
<div class="search-results hidden" id="searchResults"></div>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Statistik -->
|
||||
<div class="sidebar-section">
|
||||
<div class="section-label">Dashboard Statistik</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card stat-blue">
|
||||
<div class="stat-icon">🏢</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value" id="statTotalRI"><?= $totalFP ?></div>
|
||||
<div class="stat-label">Fasilitas Publik</div>
|
||||
<!-- Scrollable Area -->
|
||||
<div class="sidebar-scrollable" id="sidebarScrollable">
|
||||
<!-- User Info -->
|
||||
<div class="sidebar-user">
|
||||
<div class="user-info">
|
||||
<div class="user-avatar"><?= strtoupper(substr($currentUser, 0, 1)) ?></div>
|
||||
<div class="user-detail">
|
||||
<span class="user-name"><?= htmlspecialchars($currentUser) ?></span>
|
||||
<span class="user-role"><?= ucfirst($currentRole) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-amber">
|
||||
<div class="stat-icon">👥</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value" id="statTotalPM"><?= $totalPM ?></div>
|
||||
<div class="stat-label">Penduduk Miskin</div>
|
||||
<a href="logout.php" class="btn-logout" title="Logout">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
|
||||
<polyline points="16 17 21 12 16 7"/>
|
||||
<line x1="21" y1="12" x2="9" y2="12"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Dashboard Statistik -->
|
||||
<?php if (in_array($modul, ['spbu', 'bantuan', 'utuh'])): ?>
|
||||
<div class="sidebar-section">
|
||||
<div class="section-label">Dashboard Statistik</div>
|
||||
|
||||
<?php if ($modul === 'spbu' || $modul === 'utuh'): ?>
|
||||
<div class="stats-grid mb-4">
|
||||
<div class="stat-card stat-blue">
|
||||
<div class="stat-icon">⛽</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value" id="statTotalSPBU"><?= $totalSPBU ?></div>
|
||||
<div class="stat-label">Total SPBU</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-green">
|
||||
<div class="stat-icon">✅</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value" id="statSPBU24"><?= $spbu24 ?></div>
|
||||
<div class="stat-label">Buka 24 Jam</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-red">
|
||||
<div class="stat-icon">⚠️</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value" id="statSPBUTidak"><?= $spbuTidak ?></div>
|
||||
<div class="stat-label">Tidak 24 Jam</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-green">
|
||||
<div class="stat-icon">✅</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value" id="statDitangani"><?= $ditangani ?></div>
|
||||
<div class="stat-label">Ditangani</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($modul === 'bantuan' || $modul === 'utuh'): ?>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card stat-blue">
|
||||
<div class="stat-icon">🏢</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value" id="statTotalRI"><?= $totalFP ?></div>
|
||||
<div class="stat-label">Fasilitas Publik</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-amber">
|
||||
<div class="stat-icon">👥</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value" id="statTotalPM"><?= $totalPM ?></div>
|
||||
<div class="stat-label">Penduduk Miskin</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-green">
|
||||
<div class="stat-icon">✅</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value" id="statDitangani"><?= $ditangani ?></div>
|
||||
<div class="stat-label">Ditangani</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-red">
|
||||
<div class="stat-icon">⚠️</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value" id="statBelum"><?= $belumDitangani ?></div>
|
||||
<div class="stat-label">Belum Ditangani</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-red">
|
||||
<div class="stat-icon">⚠️</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value" id="statBelum"><?= $belumDitangani ?></div>
|
||||
<div class="stat-label">Belum Ditangani</div>
|
||||
|
||||
<!-- Progress cakupan -->
|
||||
<div class="coverage-bar-wrap">
|
||||
<div class="coverage-bar-header">
|
||||
<span>Cakupan Bantuan</span>
|
||||
<strong id="statPersen"><?= $persen ?>%</strong>
|
||||
</div>
|
||||
<div class="coverage-bar-track">
|
||||
<div class="coverage-bar-fill" id="statProgressBar" style="width: <?= $persen ?>%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Tools -->
|
||||
<?php if (in_array($modul, ['spbu', 'bantuan', 'digitasi', 'utuh'])): ?>
|
||||
<div class="sidebar-section<?= $currentRole === 'guest' ? ' hidden' : '' ?>" id="sectionTools">
|
||||
<div class="section-label">Alat Peta</div>
|
||||
<div class="tool-grid">
|
||||
<?php if ($modul === 'spbu' || $modul === 'utuh'): ?>
|
||||
<?php if ($currentRole === 'admin' || $currentRole === 'petugas'): ?>
|
||||
<button class="tool-btn active" id="btnToolPoint" data-mode="point" title="Tambah SPBU/Titik">
|
||||
<span class="tool-icon">📍</span>
|
||||
<span><?= $modul === 'spbu' ? 'Tambah SPBU' : 'Titik' ?></span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($modul === 'digitasi' || $modul === 'utuh'): ?>
|
||||
<?php if ($currentRole === 'admin'): ?>
|
||||
<button class="tool-btn" id="btnToolJalan" data-mode="jalan" title="Gambar Jalan">
|
||||
<span class="tool-icon">🛣️</span>
|
||||
<span>Jalan</span>
|
||||
</button>
|
||||
<button class="tool-btn" id="btnToolPolygon" data-mode="polygon" title="Gambar Polygon">
|
||||
<span class="tool-icon">⬜</span>
|
||||
<span>Polygon</span>
|
||||
</button>
|
||||
<button class="tool-btn" id="btnToolFasilitasPublik" data-mode="fasilitas_publik" title="Fasilitas Publik">
|
||||
<span class="tool-icon">🏢</span>
|
||||
<span>Fasilitas</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($modul === 'bantuan' || $modul === 'utuh'): ?>
|
||||
<?php if ($currentRole === 'admin' || $currentRole === 'petugas'): ?>
|
||||
<button class="tool-btn<?= ($modul === 'bantuan' || $currentRole === 'petugas') ? ' active' : '' ?>" id="btnToolPenduduk" data-mode="penduduk_miskin" title="Penduduk Miskin">
|
||||
<span class="tool-icon">👥</span>
|
||||
<span>Miskin</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<button class="tool-btn" id="btnToolLokasi" data-mode="lokasi" title="Lokasi Saya">
|
||||
<span class="tool-icon">📡</span>
|
||||
<span>Lokasi</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="help-text" id="helpText">
|
||||
Pilih alat lalu klik pada peta untuk menambahkan data.
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Kontrol Layer -->
|
||||
<div class="sidebar-section">
|
||||
<div class="section-label">Kontrol Layer</div>
|
||||
<div class="layer-list" id="layerList">
|
||||
<?php if (in_array($modul, ['bantuan', 'utuh'])): ?>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="layerFasilitasPublik" checked>
|
||||
<span class="layer-icon" style="font-size:14px; margin-right:6px;">🏢</span>
|
||||
<span>Fasilitas Publik</span>
|
||||
</label>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="layerPendudukDalam" checked>
|
||||
<span class="layer-icon" style="font-size:14px; margin-right:6px;">✅</span>
|
||||
<span>Penduduk Ditangani</span>
|
||||
</label>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="layerPendudukLuar" checked>
|
||||
<span class="layer-icon" style="font-size:14px; margin-right:6px;">❌</span>
|
||||
<span>Penduduk Luar Jangkauan</span>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (in_array($modul, ['spbu', 'utuh'])): ?>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="layerSpbu24" checked>
|
||||
<span class="layer-icon" style="font-size:14px; margin-right:6px;">⛽</span>
|
||||
<span>SPBU Buka 24 Jam</span>
|
||||
</label>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="layerSpbuTidak" checked>
|
||||
<span class="layer-icon" style="font-size:14px; margin-right:6px;">🚧</span>
|
||||
<span>SPBU Tidak 24 Jam</span>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (in_array($modul, ['tematik', 'utuh'])): ?>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="layerKecamatan" <?= $modul === 'tematik' ? 'checked' : '' ?>>
|
||||
<span class="layer-icon" style="font-size:14px; margin-right:6px;">🗺️</span>
|
||||
<span>Batas Kecamatan</span>
|
||||
</label>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="layerSungai" <?= $modul === 'tematik' ? 'checked' : '' ?>>
|
||||
<span class="layer-icon" style="font-size:14px; margin-right:6px;">🌊</span>
|
||||
<span>Sungai Besar</span>
|
||||
</label>
|
||||
<label class="layer-item"><input type="checkbox" id="layerGeoJalan" <?= $modul === 'tematik' ? 'checked' : '' ?>>
|
||||
<span class="layer-icon" style="font-size:14px; margin-right:6px;">🛣️</span>
|
||||
<span>Jaringan Jalan</span>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (in_array($modul, ['digitasi', 'utuh'])): ?>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="layerJalan">
|
||||
<span class="layer-icon" style="font-size:14px; margin-right:6px;">🛣️</span>
|
||||
<span>Jalan (Data)</span>
|
||||
</label>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="layerPolygon">
|
||||
<span class="layer-icon" style="font-size:14px; margin-right:6px;">⬜</span>
|
||||
<span>Polygon (Data)</span>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress cakupan -->
|
||||
<div class="coverage-bar-wrap">
|
||||
<div class="coverage-bar-header">
|
||||
<span>Cakupan Bantuan</span>
|
||||
<strong id="statPersen"><?= $persen ?>%</strong>
|
||||
</div>
|
||||
<div class="coverage-bar-track">
|
||||
<div class="coverage-bar-fill" id="statProgressBar" style="width: <?= $persen ?>%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tools -->
|
||||
<div class="sidebar-section">
|
||||
<div class="section-label">Alat Peta</div>
|
||||
<div class="tool-grid">
|
||||
<button class="tool-btn active" id="btnToolPoint" data-mode="point" title="Tambah Titik">
|
||||
<span class="tool-icon">📍</span>
|
||||
<span>Titik</span>
|
||||
</button>
|
||||
<button class="tool-btn" id="btnToolJalan" data-mode="jalan" title="Gambar Jalan">
|
||||
<span class="tool-icon">🛣️</span>
|
||||
<span>Jalan</span>
|
||||
</button>
|
||||
<button class="tool-btn" id="btnToolPolygon" data-mode="polygon" title="Gambar Polygon">
|
||||
<span class="tool-icon">⬜</span>
|
||||
<span>Polygon</span>
|
||||
</button>
|
||||
<button class="tool-btn" id="btnToolFasilitasPublik" data-mode="fasilitas_publik" title="Fasilitas Publik">
|
||||
<span class="tool-icon">🏢</span>
|
||||
<span>Fasilitas</span>
|
||||
</button>
|
||||
<button class="tool-btn" id="btnToolPenduduk" data-mode="penduduk_miskin" title="Penduduk Miskin">
|
||||
<span class="tool-icon">👥</span>
|
||||
<span>Miskin</span>
|
||||
</button>
|
||||
<button class="tool-btn" id="btnToolLokasi" data-mode="lokasi" title="Lokasi Saya">
|
||||
<span class="tool-icon">📡</span>
|
||||
<span>Lokasi</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="help-text" id="helpText">
|
||||
Pilih alat lalu klik pada peta untuk menambahkan data.
|
||||
</div>
|
||||
</div>
|
||||
<!-- Legenda -->
|
||||
<?php if ($modul !== 'digitasi'): ?>
|
||||
<div class="sidebar-section">
|
||||
<div class="section-label">Legenda</div>
|
||||
<div class="legend-list">
|
||||
<?php if (in_array($modul, ['bantuan', 'utuh'])): ?>
|
||||
<div class="legend-item">
|
||||
<span class="legend-marker-wrapper"><span class="legend-pin fp"></span></span>
|
||||
<span>Fasilitas Publik + Radius</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-marker-wrapper"><span class="legend-pin pm-dalam"></span></span>
|
||||
<span>Penduduk Ditangani (Dalam Jangkauan)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-marker-wrapper"><span class="legend-pin pm-luar"></span></span>
|
||||
<span>Penduduk Luar Jangkauan</span>
|
||||
</div>
|
||||
<div class="legend-item" style="flex-direction:column; align-items:flex-start; margin-top:10px; background: rgba(0,0,0,0.02); padding: 8px; border-radius: 8px;">
|
||||
<span style="font-size:10px; font-weight:700; color:#64748b; margin-bottom:6px; letter-spacing:0.5px;">STATUS BANTUAN (BADGE)</span>
|
||||
<div style="display:flex; gap:6px; flex-wrap:wrap;">
|
||||
<span style="font-size:9.5px; padding:3px 8px; border-radius:6px; font-weight:700; background:#f1f5f9; color:#475569; border: 1px solid rgba(0,0,0,0.05);">Belum Disalurkan</span>
|
||||
<span style="font-size:9.5px; padding:3px 8px; border-radius:6px; font-weight:700; background:#fef3c7; color:#d97706; border: 1px solid rgba(217,119,6,0.1);">Proses</span>
|
||||
<span style="font-size:9.5px; padding:3px 8px; border-radius:6px; font-weight:700; background:#dcfce7; color:#166534; border: 1px solid rgba(22,101,52,0.1);">Sudah Disalurkan</span>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (in_array($modul, ['spbu', 'utuh'])): ?>
|
||||
<div class="legend-item">
|
||||
<span class="legend-marker-wrapper"><span class="legend-pin" style="background:#2e7d32"></span></span>
|
||||
<span>SPBU Buka 24 Jam</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-marker-wrapper"><span class="legend-pin" style="background:#e53935"></span></span>
|
||||
<span>SPBU Tidak 24 Jam</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Layer Control -->
|
||||
<div class="sidebar-section">
|
||||
<div class="section-label">Kontrol Layer</div>
|
||||
<div class="layer-list" id="layerList">
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="layerFasilitasPublik" checked>
|
||||
<span class="layer-dot" style="background:#e53935"></span>
|
||||
<span>Fasilitas Publik</span>
|
||||
</label>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="layerPendudukDalam" checked>
|
||||
<span class="layer-dot" style="background:#e53935;border:2px solid #333"></span>
|
||||
<span>Penduduk Ditangani</span>
|
||||
</label>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="layerPendudukLuar" checked>
|
||||
<span class="layer-dot" style="background:#2e7d32"></span>
|
||||
<span>Penduduk Luar Jangkauan</span>
|
||||
</label>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="layerTitik"><span class="layer-dot" style="background:#78909c"></span><span>Titik Lokasi (Data)</span></label><label class="layer-item"><input type="checkbox" id="layerJalan"><span class="layer-dot" style="background:#f57c00"></span><span>Jalan (Data)</span></label><label class="layer-item"><input type="checkbox" id="layerPolygon"><span class="layer-dot" style="background:#ba68c8"></span><span>Polygon (Data)</span></label><label class="layer-item"><input type="checkbox" id="layerGeoJalan">
|
||||
<span class="layer-dot" style="background:#d32f2f"></span>
|
||||
<span>Jaringan Jalan</span>
|
||||
</label>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="layerKecamatan">
|
||||
<span class="layer-dot" style="background:#ef6c00;border-radius:2px"></span>
|
||||
<span>Batas Kecamatan</span>
|
||||
</label>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="layerSungai">
|
||||
<span class="layer-dot" style="background:#0d47a1"></span>
|
||||
<span>Sungai Besar</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legenda -->
|
||||
<div class="sidebar-section">
|
||||
<div class="section-label">Legenda</div>
|
||||
<div class="legend-list">
|
||||
<div class="legend-item">
|
||||
<span class="legend-marker marker-ri"></span>
|
||||
<span>Fasilitas Publik + Radius</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-circle" style="background:rgba(229,57,53,0.2);border:2px solid #e53935"></span>
|
||||
<span>Penduduk Ditangani</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-circle" style="background:rgba(46,125,50,0.2);border:2px solid #2e7d32"></span>
|
||||
<span>Penduduk Luar Jangkauan</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-line" style="background:#d32f2f"></span>
|
||||
<span>Jalan Nasional</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-line" style="background:#f57c00"></span>
|
||||
<span>Jalan Provinsi</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-line" style="background:#388e3c"></span>
|
||||
<span>Jalan Kabupaten</span>
|
||||
<?php if (in_array($modul, ['tematik', 'utuh'])): ?>
|
||||
<div class="legend-item">
|
||||
<span class="legend-line" style="background:#d32f2f"></span>
|
||||
<span>Jalan Nasional</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-line" style="background:#f57c00"></span>
|
||||
<span>Jalan Provinsi</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-line" style="background:#388e3c"></span>
|
||||
<span>Jalan Kabupaten</span>
|
||||
</div>
|
||||
<?php endif; ?> <!-- Endif for in_array tematik, utuh -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?> <!-- Endif for modul !== digitasi -->
|
||||
</div> <!-- End sidebar-scrollable -->
|
||||
|
||||
<!-- Footer -->
|
||||
<!-- Sidebar Footer (Sticky Bottom) -->
|
||||
<div class="sidebar-footer">
|
||||
<span>© 2025 WebGIS Spasial</span>
|
||||
<button class="btn-statistik" id="btnLihatStatistik">
|
||||
<span class="btn-statistik-icon">📊</span>
|
||||
<span class="btn-statistik-text">
|
||||
<strong>Lihat Statistik Modul</strong>
|
||||
<small>Grafik interaktif & ringkasan data</small>
|
||||
</span>
|
||||
<span class="btn-statistik-arrow">›</span>
|
||||
</button>
|
||||
<p class="sidebar-footer-copy">© 2026 WebGIS Sistem Pengentasan Kemiskinan.<br>Semua Hak Cipta Dilindungi.</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -242,9 +502,81 @@ $persen = $totalPM > 0 ? round(($ditangani / $totalPM) * 100, 1) : 0;
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
|
||||
<!-- ============================================================
|
||||
RIGHT SIDEBAR (PANEL KANAN)
|
||||
============================================================ -->
|
||||
<aside id="rightSidebar">
|
||||
<!-- Header -->
|
||||
<div class="right-sidebar-header">
|
||||
<div class="right-sidebar-title">
|
||||
<div class="right-sidebar-title-icon">🗂️</div>
|
||||
<span>Data <?= $modulTitle ?></span>
|
||||
</div>
|
||||
<button class="right-sidebar-close" id="rightSidebarClose" title="Tutup Panel">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="right-search-wrap">
|
||||
<div class="right-search-inner">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" id="rightSearchInput" placeholder="Cari nama, NIK, atau lokasi..." autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($modul === 'bantuan'): ?>
|
||||
<div style="padding: 10px 16px 0;">
|
||||
<button id="btnManajemenData" class="btn-manajemen" onclick="openManajemenModal()">
|
||||
<span style="font-size:18px">🗃️</span> Buka Manajemen Data
|
||||
</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($modul === 'utuh'): ?>
|
||||
<!-- Tabs untuk Modul Utuh -->
|
||||
<div class="right-tabs" id="rightTabs">
|
||||
<button class="right-tab-btn active" data-tab="spbu">⛽ SPBU</button>
|
||||
<button class="right-tab-btn" data-tab="bantuan">👥 Bantuan</button>
|
||||
<button class="right-tab-btn" data-tab="jalan">🛣️ Jalan</button>
|
||||
<button class="right-tab-btn" data-tab="polygon">⬜ Parsil</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Meta info -->
|
||||
<div class="right-sidebar-meta">
|
||||
<span class="right-sidebar-meta-label">Daftar Data</span>
|
||||
<span class="right-count-badge" id="rightCardCount">— entri</span>
|
||||
</div>
|
||||
|
||||
<!-- Card List -->
|
||||
<div class="right-card-list" id="rightCardList">
|
||||
<!-- Skeleton loading awal -->
|
||||
<?php if ($modul !== 'tematik'): ?>
|
||||
<?php for ($i = 0; $i < 4; $i++): ?>
|
||||
<div class="skeleton-card">
|
||||
<div class="skeleton-icon"></div>
|
||||
<div class="skeleton-lines">
|
||||
<div class="skeleton-line w-80"></div>
|
||||
<div class="skeleton-line w-50"></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Toggle Button untuk Right Sidebar -->
|
||||
<button id="rightSidebarToggleBtn" title="Panel Data">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<path d="M15 18l-6-6 6-6"/>
|
||||
</svg>
|
||||
<span class="rs-toggle-label">Data</span>
|
||||
</button>
|
||||
|
||||
<!-- ============================================================
|
||||
MAP
|
||||
â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• -->
|
||||
============================================================ -->
|
||||
<div id="mapContainer">
|
||||
<div id="map"></div>
|
||||
|
||||
@@ -258,9 +590,9 @@ $persen = $totalPM > 0 ? round(($ditangani / $totalPM) * 100, 1) : 0;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
|
||||
<!-- ============================================================
|
||||
MODALS
|
||||
â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• -->
|
||||
============================================================ -->
|
||||
|
||||
<!-- BACKDROP -->
|
||||
<div class="modal-backdrop hidden" id="modalBackdrop"></div>
|
||||
@@ -281,12 +613,12 @@ $persen = $totalPM > 0 ? round(($ditangani / $totalPM) * 100, 1) : 0;
|
||||
<input type="hidden" id="point_id">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nama <span class="req">*</span></label>
|
||||
<input type="text" class="form-input" id="point_nama" placeholder="Nama titik...">
|
||||
<label class="form-label">Nama <span style="color:#e63946">*</span></label>
|
||||
<input type="text" class="form-input" id="point_nama" placeholder="Nama titik..." autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">No. WhatsApp <span class="req">*</span></label>
|
||||
<input type="text" class="form-input" id="point_no_wa" placeholder="08xx...">
|
||||
<label class="form-label">No. WhatsApp <span style="color:#e63946">*</span></label>
|
||||
<input type="text" class="form-input" id="point_no_wa" placeholder="08xx..." autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -430,8 +762,8 @@ $persen = $totalPM > 0 ? round(($ditangani / $totalPM) * 100, 1) : 0;
|
||||
<input type="hidden" id="fp_id">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nama Fasilitas Publik <span class="req">*</span></label>
|
||||
<input type="text" class="form-input" id="fp_nama" placeholder="Nama...">
|
||||
<label class="form-label">Nama Fasilitas <span style="color:#e63946">*</span></label>
|
||||
<input type="text" class="form-input" id="fp_nama" placeholder="Cth: SDN 01 Pagi" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Jenis</label>
|
||||
@@ -499,22 +831,42 @@ $persen = $totalPM > 0 ? round(($ditangani / $totalPM) * 100, 1) : 0;
|
||||
<input type="hidden" id="pm_id">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nama Lengkap <span class="req">*</span></label>
|
||||
<input type="text" class="form-input" id="pm_nama" placeholder="Nama...">
|
||||
<label class="form-label">Nama Lengkap <span style="color:#e63946">*</span></label>
|
||||
<input type="text" class="form-input" id="pm_nama" placeholder="Sesuai KTP" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">NIK</label>
|
||||
<input type="text" class="form-input mono" id="pm_nik" placeholder="16 digit NIK...">
|
||||
<label class="form-label">NIK <span style="color:#e63946">*</span></label>
|
||||
<input type="text" class="form-input" id="pm_nik" placeholder="16 digit angka" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Keterangan</label>
|
||||
<textarea class="form-input" id="pm_keterangan" rows="2" placeholder="Kondisi ekonomi, pekerjaan, dll..."></textarea>
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="flex:2">
|
||||
<label class="form-label">Keterangan</label>
|
||||
<textarea class="form-input" id="pm_keterangan" rows="2" placeholder="Kondisi ekonomi, pekerjaan, dll..."></textarea>
|
||||
</div>
|
||||
<div class="form-group" style="flex:1">
|
||||
<label class="form-label">Status Penyaluran</label>
|
||||
<select class="form-input" id="pm_status_bantuan">
|
||||
<option value="Belum Disalurkan">Belum Disalurkan</option>
|
||||
<option value="Proses">Proses</option>
|
||||
<option value="Sudah Disalurkan">Sudah Disalurkan</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Foto Rumah</label>
|
||||
<input type="file" class="form-input" id="pm_foto_rumah" accept="image/png, image/jpeg" style="padding:10px;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Foto KK</label>
|
||||
<input type="file" class="form-input" id="pm_foto_kk" accept="image/png, image/jpeg" style="padding:10px;">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Status Jangkauan (auto) -->
|
||||
<div class="status-jangkauan-card" id="statusJangkauanCard">
|
||||
<div class="sj-label">Status Jangkauan</div>
|
||||
<div class="sj-value" id="pm_status">—</div>
|
||||
<div class="sj-value" id="pm_status">—</div>
|
||||
<div class="sj-ditangani" id="pm_ditangani"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
@@ -553,11 +905,93 @@ $persen = $totalPM > 0 ? round(($ditangani / $totalPM) * 100, 1) : 0;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
|
||||
DATA PHP → JS
|
||||
â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• -->
|
||||
<!-- ============================================================
|
||||
CHART MODAL
|
||||
============================================================ -->
|
||||
<div id="chartModal" class="hidden">
|
||||
<div class="chart-modal-box">
|
||||
<div class="chart-modal-header">
|
||||
<div class="chart-modal-icon" id="chartModalIcon">📊</div>
|
||||
<div class="chart-modal-meta">
|
||||
<div class="chart-modal-title" id="chartModalTitle">Statistik Modul</div>
|
||||
<div class="chart-modal-subtitle" id="chartModalSubtitle">Visualisasi data interaktif</div>
|
||||
</div>
|
||||
<button class="chart-modal-close" id="chartModalClose">✕</button>
|
||||
</div>
|
||||
<div class="chart-modal-body" id="chartModalBody">
|
||||
<!-- Konten dinamis diisi oleh JS -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL: Manajemen Data Penduduk -->
|
||||
<div class="modal hidden" id="manajemenModal" style="max-width: 900px; width: 95%;">
|
||||
<div class="modal-header" style="justify-content: space-between;">
|
||||
<div class="modal-title-wrap">
|
||||
<div class="modal-icon" style="background:#fce4ec;color:#e91e63">🗃️</div>
|
||||
<div>
|
||||
<h3 class="modal-title">Manajemen Data Penduduk</h3>
|
||||
<p class="modal-subtitle">Pusat kontrol penyaluran bantuan sosial</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button class="btn btn-primary" onclick="window.print()" style="padding: 6px 12px; font-size: 0.85rem;">
|
||||
📥 Cetak Data
|
||||
</button>
|
||||
<button class="modal-close" id="closeManajemenModal" style="position:static; margin-left:10px;">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body" style="background: #f8fafc; padding: 0;">
|
||||
<div class="manajemen-filter" style="padding: 16px; border-bottom: 1px solid #e2e8f0; display:flex; gap:10px; flex-wrap:wrap; background:white;">
|
||||
<button class="btn btn-ghost mj-tab active" data-filter="Semua">Semua</button>
|
||||
<button class="btn btn-ghost mj-tab" data-filter="Belum Disalurkan">Belum Disalurkan</button>
|
||||
<button class="btn btn-ghost mj-tab" data-filter="Proses">Proses</button>
|
||||
<button class="btn btn-ghost mj-tab" data-filter="Sudah Disalurkan">Sudah Disalurkan</button>
|
||||
</div>
|
||||
<div class="table-responsive" style="max-height: 60vh; overflow-y: auto; padding: 16px;">
|
||||
<table class="mj-table" style="width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.05);">
|
||||
<thead style="background: #f1f5f9; text-align: left;">
|
||||
<tr>
|
||||
<th style="padding: 12px 16px; font-weight: 600; font-size: 0.8rem; color:#475569; text-transform:uppercase;">No</th>
|
||||
<th style="padding: 12px 16px; font-weight: 600; font-size: 0.8rem; color:#475569; text-transform:uppercase;">Identitas</th>
|
||||
<th style="padding: 12px 16px; font-weight: 600; font-size: 0.8rem; color:#475569; text-transform:uppercase;">Jarak (Status)</th>
|
||||
<th style="padding: 12px 16px; font-weight: 600; font-size: 0.8rem; color:#475569; text-transform:uppercase;">Foto Rumah</th>
|
||||
<th style="padding: 12px 16px; font-weight: 600; font-size: 0.8rem; color:#475569; text-transform:uppercase;">Status Bantuan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manajemenTableBody">
|
||||
<!-- Data akan di-render via JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL: Alert / Peringatan -->
|
||||
<div class="modal modal-sm hidden" id="alertModal" style="z-index: 9999;">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title-wrap">
|
||||
<div class="modal-icon" style="background:#fff3e0;color:#e65100">⚠️</div>
|
||||
<div>
|
||||
<h3 class="modal-title" id="alertTitle">Perhatian</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="alertMessage" style="font-size: 13px; color: #334155; line-height: 1.5;"></p>
|
||||
</div>
|
||||
<div class="modal-footer" style="justify-content: center;">
|
||||
<button class="btn btn-primary" id="btnAlertOk" style="width: 100%;">Mengerti</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================
|
||||
DATA PHP → JS
|
||||
============================================================ -->
|
||||
<script>
|
||||
const USER_ROLE = '<?= $currentRole ?>';
|
||||
const APP_DATA = {
|
||||
modul: '<?= htmlspecialchars($modul) ?>',
|
||||
points: <?= json_encode($points, JSON_UNESCAPED_UNICODE) ?>,
|
||||
jalan: <?= json_encode($jalan, JSON_UNESCAPED_UNICODE) ?>,
|
||||
polygons: <?= json_encode($polygons, JSON_UNESCAPED_UNICODE) ?>,
|
||||
@@ -569,8 +1003,23 @@ const APP_DATA = {
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
|
||||
<!-- App JS -->
|
||||
<script src="assets/js/app.js?v=1"></script>
|
||||
<script src="assets/js/app.js?v=36"></script>
|
||||
<script>
|
||||
// Fitur scroll dinamis untuk mengecilkan header sidebar tanpa blink
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const scrollable = document.getElementById('sidebarScrollable');
|
||||
const stickyHeader = document.getElementById('sidebarSticky');
|
||||
|
||||
if (scrollable && stickyHeader) {
|
||||
scrollable.addEventListener('scroll', function() {
|
||||
if (scrollable.scrollTop > 10) {
|
||||
stickyHeader.classList.add('scrolled');
|
||||
} else {
|
||||
stickyHeader.classList.remove('scrolled');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak. Hanya admin yang dapat menghapus data.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || !in_array($_SESSION['role'], ['admin', 'petugas'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
@@ -19,6 +19,46 @@ $options = [
|
||||
|
||||
try {
|
||||
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
|
||||
|
||||
// ============================================================
|
||||
// AUTO-MIGRATION SCRIPT (Sesuai Permintaan)
|
||||
// ============================================================
|
||||
|
||||
// 1. Buat tabel users jika belum ada (fallback)
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
role ENUM('admin','petugas','guest') NOT NULL DEFAULT 'guest',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB");
|
||||
|
||||
// 2. Cek dan tambahkan kolom baru pada tabel users
|
||||
$columns = $pdo->query("SHOW COLUMNS FROM users")->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (!in_array('email', $columns)) {
|
||||
$pdo->exec("ALTER TABLE users ADD COLUMN email VARCHAR(150) UNIQUE NULL AFTER username");
|
||||
}
|
||||
if (!in_array('nama_lengkap', $columns)) {
|
||||
$pdo->exec("ALTER TABLE users ADD COLUMN nama_lengkap VARCHAR(150) NULL AFTER email");
|
||||
}
|
||||
if (!in_array('status', $columns)) {
|
||||
// Update user lama menjadi active terlebih dahulu jika diperlukan, tapi defaultnya active
|
||||
$pdo->exec("ALTER TABLE users ADD COLUMN status ENUM('pending', 'active') NOT NULL DEFAULT 'active' AFTER role");
|
||||
}
|
||||
if (!in_array('auth_provider', $columns)) {
|
||||
$pdo->exec("ALTER TABLE users ADD COLUMN auth_provider ENUM('local', 'google') NOT NULL DEFAULT 'local' AFTER status");
|
||||
}
|
||||
|
||||
// 3. Buat tabel settings
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS settings (
|
||||
setting_key VARCHAR(50) PRIMARY KEY,
|
||||
setting_value VARCHAR(255) NOT NULL
|
||||
) ENGINE=InnoDB");
|
||||
|
||||
// 4. Masukkan data default settings jika belum ada
|
||||
$pdo->exec("INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('show_demo_accounts', '1')");
|
||||
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
die(json_encode([
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
<?php
|
||||
session_start();
|
||||
// Jika sudah login, redirect ke dashboard.php
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
header('Location: dashboard.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
include 'koneksi.php';
|
||||
|
||||
// Ambil pengaturan show_demo_accounts
|
||||
$show_demo = 1; // default
|
||||
try {
|
||||
$stmt = $pdo->query("SELECT setting_value FROM settings WHERE setting_key = 'show_demo_accounts'");
|
||||
if ($row = $stmt->fetch()) {
|
||||
$show_demo = (int)$row['setting_value'];
|
||||
}
|
||||
} catch (Exception $e) {}
|
||||
|
||||
$error = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = trim($_POST['username'] ?? ''); // Bisa email atau username
|
||||
$password = trim($_POST['password'] ?? '');
|
||||
|
||||
if ($username && $password) {
|
||||
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? OR email = ?");
|
||||
$stmt->execute([$username, $username]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user && password_verify($password, $user['password'])) {
|
||||
// Cek status pending
|
||||
if ($user['status'] === 'pending') {
|
||||
$error = 'Akun Anda sedang menunggu persetujuan Admin.';
|
||||
} else {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
$_SESSION['role'] = $user['role'];
|
||||
header('Location: dashboard.php');
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
$error = 'Email/Username atau password salah.';
|
||||
}
|
||||
} else {
|
||||
$error = 'Silakan isi Email/Username dan password.';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login – WebGIS Pengentasan Kemiskinan</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['"Plus Jakarta Sans"', 'sans-serif'],
|
||||
heading: ['"Plus Jakarta Sans"', 'sans-serif'],
|
||||
mono: ['"JetBrains Mono"', 'monospace'],
|
||||
},
|
||||
colors: {
|
||||
background: '#f8fafc',
|
||||
foreground: '#0f172a',
|
||||
primary: { DEFAULT: '#e11d48', foreground: '#ffffff' },
|
||||
muted: { DEFAULT: '#f1f5f9', foreground: '#64748b' },
|
||||
border: '#e2e8f0',
|
||||
accent: '#e11d48',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.group-focus-within\:text-primary:focus-within { color: #e11d48; }
|
||||
.focus-visible\:ring-primary\/20:focus-visible { --tw-ring-color: rgba(225, 29, 72, 0.2); }
|
||||
.hover\:border-primary\/60:hover { border-color: rgba(225, 29, 72, 0.6); }
|
||||
.focus-visible\:border-primary:focus-visible { border-color: #e11d48; }
|
||||
/* Checkbox styling */
|
||||
input[type="checkbox"]:checked {
|
||||
background-color: #e11d48;
|
||||
border-color: #e11d48;
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
|
||||
}
|
||||
/* Custom Pink Scrollbar */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #e11d48; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #be123c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Akses Ditolak',
|
||||
text: '<?= htmlspecialchars($error) ?>',
|
||||
confirmButtonColor: '#e11d48'
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<main class="flex h-screen w-full overflow-hidden bg-background font-sans text-foreground">
|
||||
<!-- Left: hero / map -->
|
||||
<section class="relative hidden w-1/2 overflow-hidden lg:block">
|
||||
<img src="assets/img/map-bg.png" alt="Visualisasi peta analisis spasial" class="absolute inset-0 object-cover w-full h-full" onerror="this.src='https://images.unsplash.com/photo-1524661135-423995f22d0b?q=80&w=2074&auto=format&fit=crop'" />
|
||||
<div aria-hidden="true" class="absolute inset-0 bg-gradient-to-br from-red-900/85 via-red-800/80 to-rose-900/90"></div>
|
||||
<div aria-hidden="true" class="absolute inset-0 opacity-[0.06]" style="background-image: linear-gradient(to right, white 1px, transparent 1px), linear-gradient(to bottom, white 1px, transparent 1px); background-size: 52px 52px;"></div>
|
||||
|
||||
<div class="relative z-10 flex h-full flex-col justify-between p-12 xl:p-16">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="flex h-9 w-9 items-center justify-center rounded-xl bg-white/15 backdrop-blur-sm ring-1 ring-white/25">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-5 w-5 text-white" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M12 21s-7-5.6-7-11a7 7 0 1 1 14 0c0 5.4-7 11-7 11Z" />
|
||||
<circle cx="12" cy="10" r="2.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="font-heading text-sm font-bold tracking-wide text-white shadow-sm">WebGIS Geospasial</h1>
|
||||
<p class="text-[11px] font-medium text-white/70 uppercase tracking-wider">Kementerian Analisis Spasial</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="font-heading text-4xl font-bold leading-tight tracking-tight text-white xl:text-5xl">
|
||||
Sistem Informasi <br> Geografis <span class="text-rose-200">Terpadu</span>
|
||||
</h2>
|
||||
<p class="mt-4 max-w-md leading-relaxed text-rose-100/90 text-sm xl:text-base">
|
||||
Platform pemetaan cerdas untuk analisis spasial persebaran infrastruktur, demografi kemiskinan, dan evaluasi penyaluran bantuan secara real-time.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Right: Login Form -->
|
||||
<section class="flex w-full lg:w-1/2 h-full overflow-y-auto p-6">
|
||||
<div class="w-full max-w-sm sm:max-w-md m-auto">
|
||||
<!-- Mobile branding -->
|
||||
<div class="mb-10 flex flex-col items-center gap-3 lg:hidden">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br from-primary to-rose-700 text-white shadow-md shadow-primary/20">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-6 w-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M12 21s-7-5.6-7-11a7 7 0 1 1 14 0c0 5.4-7 11-7 11Z" />
|
||||
<circle cx="12" cy="10" r="2.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h1 class="font-heading text-lg font-bold text-foreground">WebGIS Geospasial</h1>
|
||||
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wider">Kementerian Analisis Spasial</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h2 class="font-heading text-2xl font-bold tracking-tight text-foreground sm:text-3xl">Selamat Datang Kembali</h2>
|
||||
<p class="mt-2 text-sm text-muted-foreground">Silakan masuk menggunakan kredensial Anda untuk melanjutkan ke dashboard.</p>
|
||||
</div>
|
||||
|
||||
<!-- Card -->
|
||||
<div class="rounded-2xl border border-white/60 bg-white/70 p-6 shadow-[0_8px_30px_rgb(0,0,0,0.04)] backdrop-blur-xl sm:p-8">
|
||||
<form action="login.php" method="POST" class="flex flex-col gap-5">
|
||||
<!-- Input Email / Username -->
|
||||
<div class="group">
|
||||
<label for="username" class="mb-1.5 block text-sm font-semibold text-foreground group-focus-within:text-primary transition-colors">
|
||||
Email atau Username
|
||||
</label>
|
||||
<div class="relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5 text-muted-foreground group-focus-within:text-primary transition-colors">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-4.5 w-4.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<input type="text" id="username" name="username" class="block w-full rounded-xl border border-border bg-white py-2.5 pl-10 pr-3 text-sm text-foreground transition-all focus:outline-none focus-visible:border-primary focus-visible:ring-4 focus-visible:ring-primary/20 hover:border-primary/60" placeholder="admin@domain.com" required autocomplete="username">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Password -->
|
||||
<div class="group">
|
||||
<label for="password" class="mb-1.5 block text-sm font-semibold text-foreground group-focus-within:text-primary transition-colors">
|
||||
Kata Sandi
|
||||
</label>
|
||||
<div class="relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5 text-muted-foreground group-focus-within:text-primary transition-colors">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-4.5 w-4.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<input type="password" id="password" name="password" class="block w-full rounded-xl border border-border bg-white py-2.5 pl-10 pr-10 text-sm text-foreground transition-all focus:outline-none focus-visible:border-primary focus-visible:ring-4 focus-visible:ring-primary/20 hover:border-primary/60" placeholder="••••••••" required autocomplete="current-password">
|
||||
<button type="button" id="togglePassword" class="absolute inset-y-0 right-0 flex items-center pr-3.5 text-muted-foreground hover:text-foreground focus:outline-none" aria-label="Toggle password visibility">
|
||||
<svg id="eyeIcon" viewBox="0 0 24 24" fill="none" class="h-4.5 w-4.5" stroke="currentColor" stroke-width="2" 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"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
<div class="flex items-center justify-between mt-1">
|
||||
<label class="flex items-center gap-2 cursor-pointer group">
|
||||
<input type="checkbox" name="remember" class="h-4 w-4 rounded border-border text-primary shadow-sm transition-all focus:ring-primary focus:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50">
|
||||
<span class="text-sm font-medium text-muted-foreground group-hover:text-foreground transition-colors">Ingat saya</span>
|
||||
</label>
|
||||
<a href="#" class="text-sm font-semibold text-primary transition-colors hover:text-primary/80">
|
||||
Lupa sandi?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<button type="submit" class="group mt-1 flex h-12 items-center justify-center gap-2 rounded-xl bg-primary text-sm font-semibold text-white shadow-lg shadow-primary/25 transition-all duration-300 hover:-translate-y-0.5 hover:bg-rose-700 hover:shadow-xl hover:shadow-primary/35 active:translate-y-0">
|
||||
Masuk
|
||||
<svg class="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
<polyline points="12 5 19 12 12 19"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 text-center text-sm text-muted-foreground">
|
||||
Belum punya akun? <a href="register.php" class="font-semibold text-primary hover:underline">Daftar sekarang</a>
|
||||
</div>
|
||||
|
||||
<?php if ($show_demo === 1): ?>
|
||||
<!-- Demo accounts helper -->
|
||||
<div class="mt-7 rounded-xl border border-border/70 bg-gray-50 p-4">
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-primary"></span>
|
||||
<p class="text-xs font-bold uppercase tracking-wide text-foreground">Info Akun Demo</p>
|
||||
<span class="text-[11px] font-medium text-muted-foreground">· klik untuk mengisi</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<button type="button" onclick="fillDemo('admin', 'password')" class="group flex cursor-pointer items-center justify-between gap-3 rounded-lg border border-transparent bg-white/60 px-3 py-2 text-left transition-all hover:border-primary/30 hover:bg-white hover:shadow-sm w-full">
|
||||
<span class="flex items-center gap-2.5">
|
||||
<span aria-hidden="true" class="text-sm leading-none">👑</span>
|
||||
<span class="text-xs font-semibold text-foreground">Admin</span>
|
||||
</span>
|
||||
<span class="font-mono text-[11px] text-muted-foreground transition-colors group-hover:text-primary">
|
||||
admin · password
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" onclick="fillDemo('petugas', 'password')" class="group flex cursor-pointer items-center justify-between gap-3 rounded-lg border border-transparent bg-white/60 px-3 py-2 text-left transition-all hover:border-primary/30 hover:bg-white hover:shadow-sm w-full">
|
||||
<span class="flex items-center gap-2.5">
|
||||
<span aria-hidden="true" class="text-sm leading-none">📝</span>
|
||||
<span class="text-xs font-semibold text-foreground">Petugas</span>
|
||||
</span>
|
||||
<span class="font-mono text-[11px] text-muted-foreground transition-colors group-hover:text-primary">
|
||||
petugas · password
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" onclick="fillDemo('guest', 'password')" class="group flex cursor-pointer items-center justify-between gap-3 rounded-lg border border-transparent bg-white/60 px-3 py-2 text-left transition-all hover:border-primary/30 hover:bg-white hover:shadow-sm w-full">
|
||||
<span class="flex items-center gap-2.5">
|
||||
<span aria-hidden="true" class="text-sm leading-none">👁️</span>
|
||||
<span class="text-xs font-semibold text-foreground">Guest</span>
|
||||
</span>
|
||||
<span class="font-mono text-[11px] text-muted-foreground transition-colors group-hover:text-primary">
|
||||
guest · password
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<p class="mt-6 text-center text-xs text-muted-foreground">
|
||||
© <script>document.write(new Date().getFullYear())</script> WebGIS Geospasial · Kementerian Analisis Spasial
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Toggle Password Visibility
|
||||
const toggleBtn = document.getElementById('togglePassword');
|
||||
const passInput = document.getElementById('password');
|
||||
const eyeIcon = document.getElementById('eyeIcon');
|
||||
|
||||
if (toggleBtn && passInput) {
|
||||
toggleBtn.addEventListener('click', function() {
|
||||
if (passInput.type === 'password') {
|
||||
passInput.type = 'text';
|
||||
eyeIcon.innerHTML = '<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line>';
|
||||
} else {
|
||||
passInput.type = 'password';
|
||||
eyeIcon.innerHTML = '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fillDemo(user, pass) {
|
||||
document.getElementById('username').value = user;
|
||||
document.getElementById('password').value = pass;
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
session_start();
|
||||
$_SESSION = [];
|
||||
if (ini_get('session.use_cookies')) {
|
||||
$params = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time() - 42000,
|
||||
$params['path'], $params['domain'],
|
||||
$params['secure'], $params['httponly']
|
||||
);
|
||||
}
|
||||
session_destroy();
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
?>
|
||||
@@ -1,4 +1,12 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak. Hanya admin yang dapat menghapus data.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || !in_array($_SESSION['role'], ['admin', 'petugas'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
@@ -8,6 +16,7 @@ $keterangan = trim($_POST['keterangan'] ?? '');
|
||||
$latitude = trim($_POST['latitude'] ?? '');
|
||||
$longitude = trim($_POST['longitude'] ?? '');
|
||||
$status_jangkauan= trim($_POST['status_jangkauan']?? 'Luar Jangkauan');
|
||||
$status_bantuan = trim($_POST['status_bantuan'] ?? 'Belum Disalurkan');
|
||||
$fasilitas_publik_id = trim($_POST['fasilitas_publik_id'] ?? '');
|
||||
|
||||
if (!$nama || !$latitude || !$longitude) {
|
||||
@@ -20,12 +29,40 @@ if (!in_array($status_jangkauan, $valid_status)) $status_jangkauan = 'Luar Jangk
|
||||
|
||||
$fp_id = ($fasilitas_publik_id !== '') ? (int)$fasilitas_publik_id : null;
|
||||
|
||||
// Fungsi upload foto
|
||||
function uploadFoto($file_input_name) {
|
||||
if (isset($_FILES[$file_input_name]) && $_FILES[$file_input_name]['error'] === UPLOAD_ERR_OK) {
|
||||
$file_tmp_path = $_FILES[$file_input_name]['tmp_name'];
|
||||
$file_name = $_FILES[$file_input_name]['name'];
|
||||
$file_size = $_FILES[$file_input_name]['size'];
|
||||
$file_type = mime_content_type($file_tmp_path);
|
||||
|
||||
$allowed_mime_types = ['image/jpeg', 'image/png', 'image/jpg'];
|
||||
$max_size = 2 * 1024 * 1024; // 2MB
|
||||
|
||||
if (in_array($file_type, $allowed_mime_types) && $file_size <= $max_size) {
|
||||
$file_extension = pathinfo($file_name, PATHINFO_EXTENSION);
|
||||
$new_file_name = uniqid($file_input_name . '_', true) . '.' . $file_extension;
|
||||
$upload_file_dir = '../uploads/';
|
||||
$dest_path = $upload_file_dir . $new_file_name;
|
||||
|
||||
if (move_uploaded_file($file_tmp_path, $dest_path)) {
|
||||
return $new_file_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$foto_rumah = uploadFoto('foto_rumah');
|
||||
$foto_kk = uploadFoto('foto_kk');
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO penduduk_miskin (nama, nik, keterangan, latitude, longitude, status_jangkauan, fasilitas_publik_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)"
|
||||
"INSERT INTO penduduk_miskin (nama, nik, keterangan, latitude, longitude, status_jangkauan, fasilitas_publik_id, foto_rumah, foto_kk, status_bantuan)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->execute([$nama, $nik, $keterangan, $latitude, $longitude, $status_jangkauan, $fp_id]);
|
||||
$stmt->execute([$nama, $nik, $keterangan, $latitude, $longitude, $status_jangkauan, $fp_id, $foto_rumah, $foto_kk, $status_bantuan]);
|
||||
$id = $pdo->lastInsertId();
|
||||
|
||||
$stmtSelect = $pdo->prepare("SELECT pm.*, fp.nama AS fp_nama FROM penduduk_miskin pm LEFT JOIN fasilitas_publik fp ON pm.fasilitas_publik_id = fp.id WHERE pm.id = ?");
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || !in_array($_SESSION['role'], ['admin', 'petugas'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
@@ -9,6 +17,7 @@ $keterangan = trim($_POST['keterangan'] ?? '');
|
||||
$latitude = trim($_POST['latitude'] ?? '');
|
||||
$longitude = trim($_POST['longitude'] ?? '');
|
||||
$status_jangkauan= trim($_POST['status_jangkauan']?? 'Luar Jangkauan');
|
||||
$status_bantuan = trim($_POST['status_bantuan'] ?? 'Belum Disalurkan');
|
||||
$fasilitas_publik_id = trim($_POST['fasilitas_publik_id'] ?? '');
|
||||
|
||||
if (!$id || !$nama || !$latitude || !$longitude) {
|
||||
@@ -18,12 +27,52 @@ if (!$id || !$nama || !$latitude || !$longitude) {
|
||||
|
||||
$fp_id = ($fasilitas_publik_id !== '') ? (int)$fasilitas_publik_id : null;
|
||||
|
||||
// Fungsi upload foto
|
||||
function uploadFoto($file_input_name) {
|
||||
if (isset($_FILES[$file_input_name]) && $_FILES[$file_input_name]['error'] === UPLOAD_ERR_OK) {
|
||||
$file_tmp_path = $_FILES[$file_input_name]['tmp_name'];
|
||||
$file_name = $_FILES[$file_input_name]['name'];
|
||||
$file_size = $_FILES[$file_input_name]['size'];
|
||||
$file_type = mime_content_type($file_tmp_path);
|
||||
|
||||
$allowed_mime_types = ['image/jpeg', 'image/png', 'image/jpg'];
|
||||
$max_size = 2 * 1024 * 1024; // 2MB
|
||||
|
||||
if (in_array($file_type, $allowed_mime_types) && $file_size <= $max_size) {
|
||||
$file_extension = pathinfo($file_name, PATHINFO_EXTENSION);
|
||||
$new_file_name = uniqid($file_input_name . '_', true) . '.' . $file_extension;
|
||||
$upload_file_dir = '../uploads/';
|
||||
$dest_path = $upload_file_dir . $new_file_name;
|
||||
|
||||
if (move_uploaded_file($file_tmp_path, $dest_path)) {
|
||||
return $new_file_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$foto_rumah = uploadFoto('foto_rumah');
|
||||
$foto_kk = uploadFoto('foto_kk');
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE penduduk_miskin SET nama=?, nik=?, keterangan=?, latitude=?, longitude=?,
|
||||
status_jangkauan=?, fasilitas_publik_id=? WHERE id=?"
|
||||
);
|
||||
$stmt->execute([$nama, $nik, $keterangan, $latitude, $longitude, $status_jangkauan, $fp_id, $id]);
|
||||
$query = "UPDATE penduduk_miskin SET nama=?, nik=?, keterangan=?, latitude=?, longitude=?, status_jangkauan=?, fasilitas_publik_id=?, status_bantuan=?";
|
||||
$params = [$nama, $nik, $keterangan, $latitude, $longitude, $status_jangkauan, $fp_id, $status_bantuan];
|
||||
|
||||
if ($foto_rumah) {
|
||||
$query .= ", foto_rumah=?";
|
||||
$params[] = $foto_rumah;
|
||||
}
|
||||
if ($foto_kk) {
|
||||
$query .= ", foto_kk=?";
|
||||
$params[] = $foto_kk;
|
||||
}
|
||||
|
||||
$query .= " WHERE id=?";
|
||||
$params[] = $id;
|
||||
|
||||
$stmt = $pdo->prepare($query);
|
||||
$stmt->execute($params);
|
||||
|
||||
$stmtSelect = $pdo->prepare("SELECT pm.*, fp.nama AS fp_nama FROM penduduk_miskin pm LEFT JOIN fasilitas_publik fp ON pm.fasilitas_publik_id = fp.id WHERE pm.id = ?");
|
||||
$stmtSelect->execute([$id]);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
session_start();
|
||||
include '../koneksi.php';
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = $_POST['id'] ?? '';
|
||||
$latitude = $_POST['latitude'] ?? '';
|
||||
$longitude = $_POST['longitude'] ?? '';
|
||||
$status_jangkauan = $_POST['status_jangkauan'] ?? '';
|
||||
$fasilitas_publik_id = $_POST['fasilitas_publik_id'] ?? null;
|
||||
|
||||
if (!$id || !$latitude || !$longitude) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($fasilitas_publik_id === '') {
|
||||
$fasilitas_publik_id = null;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($status_jangkauan !== '') {
|
||||
$stmt = $pdo->prepare("UPDATE penduduk_miskin SET latitude = ?, longitude = ?, status_jangkauan = ?, fasilitas_publik_id = ? WHERE id = ?");
|
||||
$stmt->execute([$latitude, $longitude, $status_jangkauan, $fasilitas_publik_id, $id]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("UPDATE penduduk_miskin SET latitude = ?, longitude = ? WHERE id = ?");
|
||||
$stmt->execute([$latitude, $longitude, $id]);
|
||||
}
|
||||
|
||||
echo json_encode(['status' => 'success']);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Database error: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
<?php
|
||||
session_start();
|
||||
// Role check: hanya admin yang boleh menghapus
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak. Hanya admin yang dapat menghapus data.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
<?php
|
||||
session_start();
|
||||
// Role check: hanya admin dan petugas yang boleh menyimpan
|
||||
if (!isset($_SESSION['role']) || !in_array($_SESSION['role'], ['admin', 'petugas'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak. Hanya admin/petugas yang dapat menambah data.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// point/simpan.php
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || !in_array($_SESSION['role'], ['admin', 'petugas'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
session_start();
|
||||
include '../koneksi.php';
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = $_POST['id'] ?? '';
|
||||
$latitude = $_POST['latitude'] ?? '';
|
||||
$longitude = $_POST['longitude'] ?? '';
|
||||
|
||||
if (!$id || !$latitude || !$longitude) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("UPDATE points SET latitude = ?, longitude = ? WHERE id = ?");
|
||||
$stmt->execute([$latitude, $longitude, $id]);
|
||||
|
||||
echo json_encode(['status' => 'success']);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Database error']);
|
||||
}
|
||||
@@ -1,4 +1,12 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak. Hanya admin yang dapat menghapus data.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || !in_array($_SESSION['role'], ['admin', 'petugas'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
session_start();
|
||||
// Jika sudah login, redirect ke dashboard.php
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
header('Location: dashboard.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
include 'koneksi.php';
|
||||
|
||||
// Konfigurasi Google OAuth (DUMMY/HARUS DIISI OLEH USER)
|
||||
define('GOOGLE_CLIENT_ID', '575444350307-likfkam3fkppn8rjl8044uppdm4ovf35.apps.googleusercontent.com');
|
||||
define('GOOGLE_CLIENT_SECRET', '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');
|
||||
define('GOOGLE_REDIRECT_URI', 'http://localhost/webgis/register.php');
|
||||
|
||||
$sweetAlertScript = '';
|
||||
|
||||
// Proses Google OAuth Callback
|
||||
if (isset($_GET['code'])) {
|
||||
$code = $_GET['code'];
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, "https://oauth2.googleapis.com/token");
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
|
||||
'client_id' => GOOGLE_CLIENT_ID,
|
||||
'client_secret' => GOOGLE_CLIENT_SECRET,
|
||||
'redirect_uri' => GOOGLE_REDIRECT_URI,
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $code
|
||||
]));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$token_data = json_decode($response, true);
|
||||
if (isset($token_data['access_token'])) {
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, "https://www.googleapis.com/oauth2/v3/userinfo");
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer " . $token_data['access_token']]);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$user_data = json_decode(curl_exec($ch), true);
|
||||
curl_close($ch);
|
||||
|
||||
if (isset($user_data['email'])) {
|
||||
$email = $user_data['email'];
|
||||
$name = $user_data['name'];
|
||||
$state_role = $_GET['state'] ?? 'guest';
|
||||
$role = in_array($state_role, ['guest', 'petugas']) ? $state_role : 'guest';
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ? OR username = ?");
|
||||
$stmt->execute([$email, $email]);
|
||||
$existing_user = $stmt->fetch();
|
||||
|
||||
if ($existing_user) {
|
||||
if ($existing_user['status'] === 'pending') {
|
||||
$sweetAlertScript = "Swal.fire({icon: 'warning', title: 'Menunggu Persetujuan', text: 'Akun Anda sedang menunggu persetujuan Admin.', confirmButtonColor: '#e11d48'});";
|
||||
} else {
|
||||
$_SESSION['user_id'] = $existing_user['id'];
|
||||
$_SESSION['username'] = $existing_user['username'];
|
||||
$_SESSION['role'] = $existing_user['role'];
|
||||
header('Location: dashboard.php');
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
$status = ($role === 'petugas') ? 'pending' : 'active';
|
||||
$username = explode('@', $email)[0] . rand(100,999);
|
||||
$stmt = $pdo->prepare("INSERT INTO users (username, email, nama_lengkap, password, role, status, auth_provider) VALUES (?, ?, ?, ?, ?, ?, 'google')");
|
||||
$stmt->execute([$username, $email, $name, password_hash(bin2hex(random_bytes(8)), PASSWORD_DEFAULT), $role, $status]);
|
||||
|
||||
if ($role === 'petugas') {
|
||||
$sweetAlertScript = "Swal.fire({icon: 'success', title: 'Pendaftaran Berhasil', text: 'Pendaftaran via Google berhasil. Akun Anda menunggu persetujuan Admin.', confirmButtonColor: '#e11d48'}).then(() => { window.location.href = 'login.php'; });";
|
||||
} else {
|
||||
$_SESSION['user_id'] = $pdo->lastInsertId();
|
||||
$_SESSION['username'] = $username;
|
||||
$_SESSION['role'] = 'guest';
|
||||
header('Location: dashboard.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$sweetAlertScript = "Swal.fire({icon: 'error', title: 'Google Login Gagal', text: 'Terjadi kesalahan kredensial OAuth.', confirmButtonColor: '#e11d48'});";
|
||||
}
|
||||
}
|
||||
|
||||
// Proses Registrasi Lokal
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['register'])) {
|
||||
$nama_lengkap = trim($_POST['nama_lengkap'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$password = trim($_POST['password'] ?? '');
|
||||
$role = trim($_POST['role'] ?? 'guest');
|
||||
|
||||
if ($nama_lengkap && $email && $password && in_array($role, ['guest', 'petugas'])) {
|
||||
$stmt = $pdo->prepare("SELECT id FROM users WHERE email = ? OR username = ?");
|
||||
$stmt->execute([$email, $email]);
|
||||
if ($stmt->fetch()) {
|
||||
$sweetAlertScript = "Swal.fire({icon: 'error', title: 'Registrasi Gagal', text: 'Email tersebut sudah terdaftar.', confirmButtonColor: '#e11d48'});";
|
||||
} else {
|
||||
$status = ($role === 'petugas') ? 'pending' : 'active';
|
||||
$username = explode('@', $email)[0] . rand(100,999);
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO users (username, email, nama_lengkap, password, role, status, auth_provider) VALUES (?, ?, ?, ?, ?, ?, 'local')");
|
||||
if ($stmt->execute([$username, $email, $nama_lengkap, password_hash($password, PASSWORD_DEFAULT), $role, $status])) {
|
||||
if ($role === 'petugas') {
|
||||
$sweetAlertScript = "Swal.fire({icon: 'success', title: 'Pendaftaran Berhasil', text: 'Pendaftaran berhasil. Menunggu persetujuan Admin.', confirmButtonColor: '#e11d48'}).then(() => { window.location.href = 'login.php'; });";
|
||||
} else {
|
||||
$sweetAlertScript = "Swal.fire({icon: 'success', title: 'Pendaftaran Berhasil', text: 'Akun Anda aktif, silakan masuk ke sistem.', confirmButtonColor: '#e11d48'}).then(() => { window.location.href = 'login.php'; });";
|
||||
}
|
||||
} else {
|
||||
$sweetAlertScript = "Swal.fire({icon: 'error', title: 'Gagal', text: 'Terjadi kesalahan sistem internal.', confirmButtonColor: '#e11d48'});";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$sweetAlertScript = "Swal.fire({icon: 'warning', title: 'Data Belum Lengkap', text: 'Harap lengkapi semua bidang yang tersedia.', confirmButtonColor: '#e11d48'});";
|
||||
}
|
||||
}
|
||||
|
||||
// URL OAuth Google
|
||||
$google_auth_url = "https://accounts.google.com/o/oauth2/v2/auth?client_id=" . GOOGLE_CLIENT_ID . "&redirect_uri=" . urlencode(GOOGLE_REDIRECT_URI) . "&response_type=code&scope=email%20profile";
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Register – WebGIS Pengentasan Kemiskinan</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['"Plus Jakarta Sans"', 'sans-serif'],
|
||||
heading: ['"Plus Jakarta Sans"', 'sans-serif'],
|
||||
mono: ['"JetBrains Mono"', 'monospace'],
|
||||
},
|
||||
colors: {
|
||||
background: '#f8fafc',
|
||||
foreground: '#0f172a',
|
||||
primary: { DEFAULT: '#e11d48', foreground: '#ffffff' },
|
||||
muted: { DEFAULT: '#f1f5f9', foreground: '#64748b' },
|
||||
border: '#e2e8f0',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.group-focus-within\:text-primary:focus-within { color: #e11d48; }
|
||||
.focus-visible\:ring-primary\/20:focus-visible { --tw-ring-color: rgba(225, 29, 72, 0.2); }
|
||||
.hover\:border-primary\/60:hover { border-color: rgba(225, 29, 72, 0.6); }
|
||||
.focus-visible\:border-primary:focus-visible { border-color: #e11d48; }
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #e11d48; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #be123c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<?php if ($sweetAlertScript): ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
<?= $sweetAlertScript ?>
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<main class="flex h-screen w-full overflow-hidden bg-background font-sans text-foreground">
|
||||
<!-- Left: hero / map -->
|
||||
<section class="relative hidden w-1/2 overflow-hidden lg:block">
|
||||
<img src="assets/img/map-bg.png" alt="Visualisasi peta" class="absolute inset-0 object-cover w-full h-full" onerror="this.src='https://images.unsplash.com/photo-1524661135-423995f22d0b?q=80&w=2074&auto=format&fit=crop'" />
|
||||
<div aria-hidden="true" class="absolute inset-0 bg-gradient-to-br from-red-900/85 via-red-800/80 to-rose-900/90"></div>
|
||||
<div aria-hidden="true" class="absolute inset-0 opacity-[0.06]" style="background-image: linear-gradient(to right, white 1px, transparent 1px), linear-gradient(to bottom, white 1px, transparent 1px); background-size: 52px 52px;"></div>
|
||||
|
||||
<div class="relative z-10 flex h-full flex-col justify-between p-12 xl:p-16">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="flex h-9 w-9 items-center justify-center rounded-xl bg-white/15 backdrop-blur-sm ring-1 ring-white/25">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-5 w-5 text-white" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M12 21s-7-5.6-7-11a7 7 0 1 1 14 0c0 5.4-7 11-7 11Z" />
|
||||
<circle cx="12" cy="10" r="2.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="font-heading text-sm font-bold tracking-wide text-white shadow-sm">WebGIS Geospasial</h1>
|
||||
<p class="text-[11px] font-medium text-white/70 uppercase tracking-wider">Kementerian Analisis Spasial</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="font-heading text-4xl font-bold leading-tight tracking-tight text-white xl:text-5xl">
|
||||
Bergabung Bersama <br> Komunitas <span class="text-rose-200">Spasial</span>
|
||||
</h2>
|
||||
<p class="mt-4 max-w-md leading-relaxed text-rose-100/90 text-sm xl:text-base">
|
||||
Jadilah bagian dari inisiatif digital pemetaan wilayah, dan ikut serta dalam evaluasi kemiskinan berbasis data geospasial.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Right: Register Form -->
|
||||
<section class="flex w-full lg:w-1/2 h-full overflow-y-auto p-6">
|
||||
<div class="w-full max-w-sm sm:max-w-md m-auto">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-8 mt-10 lg:mt-0">
|
||||
<h2 class="font-heading text-2xl font-bold tracking-tight text-foreground sm:text-3xl">Buat Akun Baru</h2>
|
||||
<p class="mt-2 text-sm text-muted-foreground">Isi data di bawah ini untuk membuat akun WebGIS Anda.</p>
|
||||
</div>
|
||||
|
||||
<!-- Card -->
|
||||
<div class="rounded-2xl border border-white/60 bg-white/70 p-6 shadow-[0_8px_30px_rgb(0,0,0,0.04)] backdrop-blur-xl sm:p-8">
|
||||
|
||||
<!-- Google Auth Button -->
|
||||
<button type="button" onclick="startGoogleAuth()" class="flex h-11 w-full items-center justify-center gap-3 rounded-xl border border-border bg-white text-sm font-semibold text-foreground shadow-sm hover:bg-gray-50 transition-colors mb-6">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
|
||||
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
|
||||
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
|
||||
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
|
||||
</svg>
|
||||
Daftar dengan Google
|
||||
</button>
|
||||
|
||||
<div class="relative flex items-center mb-6">
|
||||
<div class="flex-grow border-t border-border"></div>
|
||||
<span class="flex-shrink-0 mx-4 text-xs text-muted-foreground uppercase tracking-wider font-semibold">atau dengan email</span>
|
||||
<div class="flex-grow border-t border-border"></div>
|
||||
</div>
|
||||
|
||||
<form action="register.php" method="POST" class="flex flex-col gap-4">
|
||||
<input type="hidden" name="register" value="1">
|
||||
|
||||
<!-- Input Nama -->
|
||||
<div class="group">
|
||||
<label for="nama_lengkap" class="mb-1.5 block text-sm font-semibold text-foreground group-focus-within:text-primary transition-colors">
|
||||
Nama Lengkap
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input type="text" id="nama_lengkap" name="nama_lengkap" class="block w-full rounded-xl border border-border bg-white py-2.5 px-3 text-sm text-foreground transition-all focus:outline-none focus-visible:border-primary focus-visible:ring-4 focus-visible:ring-primary/20 hover:border-primary/60" placeholder="John Doe" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Email -->
|
||||
<div class="group">
|
||||
<label for="email" class="mb-1.5 block text-sm font-semibold text-foreground group-focus-within:text-primary transition-colors">
|
||||
Alamat Email
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input type="email" id="email" name="email" class="block w-full rounded-xl border border-border bg-white py-2.5 px-3 text-sm text-foreground transition-all focus:outline-none focus-visible:border-primary focus-visible:ring-4 focus-visible:ring-primary/20 hover:border-primary/60" placeholder="john@example.com" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Password -->
|
||||
<div class="group">
|
||||
<label for="password" class="mb-1.5 block text-sm font-semibold text-foreground group-focus-within:text-primary transition-colors">
|
||||
Kata Sandi
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input type="password" id="password" name="password" class="block w-full rounded-xl border border-border bg-white py-2.5 px-3 pr-10 text-sm text-foreground transition-all focus:outline-none focus-visible:border-primary focus-visible:ring-4 focus-visible:ring-primary/20 hover:border-primary/60" placeholder="••••••••" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Role -->
|
||||
<div class="group">
|
||||
<label for="role" class="mb-1.5 block text-sm font-semibold text-foreground group-focus-within:text-primary transition-colors">
|
||||
Daftar Sebagai
|
||||
</label>
|
||||
<div class="relative">
|
||||
<select id="role" name="role" class="block w-full rounded-xl border border-border bg-white py-2.5 px-3 text-sm text-foreground transition-all focus:outline-none focus-visible:border-primary focus-visible:ring-4 focus-visible:ring-primary/20 hover:border-primary/60 appearance-none" required>
|
||||
<option value="guest">Guest (Pengunjung Umum)</option>
|
||||
<option value="petugas">Petugas (Verifikator Lapangan)</option>
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-muted-foreground">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<button type="submit" class="group mt-3 flex h-12 items-center justify-center gap-2 rounded-xl bg-primary text-sm font-semibold text-white shadow-lg shadow-primary/25 transition-all duration-300 hover:-translate-y-0.5 hover:bg-rose-700 hover:shadow-xl hover:shadow-primary/35 active:translate-y-0">
|
||||
Daftar Sekarang
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 text-center text-sm text-muted-foreground">
|
||||
Sudah punya akun? <a href="login.php" class="font-semibold text-primary hover:underline">Masuk di sini</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-6 text-center text-xs text-muted-foreground">
|
||||
© <script>document.write(new Date().getFullYear())</script> WebGIS Geospasial · Kementerian Analisis Spasial
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function startGoogleAuth() {
|
||||
const role = document.getElementById('role').value;
|
||||
window.location.href = "<?= $google_auth_url ?>" + "&state=" + encodeURIComponent(role);
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,4 @@
|
||||
USE webgis;
|
||||
ALTER TABLE penduduk_miskin
|
||||
ADD COLUMN foto_rumah VARCHAR(255) NULL,
|
||||
ADD COLUMN foto_kk VARCHAR(255) NULL;
|
||||
@@ -0,0 +1 @@
|
||||
# Gitkeep
|
||||
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 374 KiB |
@@ -0,0 +1,15 @@
|
||||
# v0 sandbox internal files
|
||||
__v0_runtime_loader.js
|
||||
__v0_devtools.tsx
|
||||
__v0_jsx-dev-runtime.ts
|
||||
.snowflake/
|
||||
.v0-trash/
|
||||
.vercel/
|
||||
|
||||
# Environment variables
|
||||
.env*.local
|
||||
|
||||
# Common ignores
|
||||
node_modules
|
||||
.next/
|
||||
.DS_Store
|
||||
@@ -0,0 +1,130 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
@import 'shadcn/tailwind.css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--font-heading: var(--font-jakarta), 'Plus Jakarta Sans Fallback';
|
||||
--font-sans: var(--font-jakarta), 'Plus Jakarta Sans Fallback';
|
||||
--font-mono: var(--font-geist-mono), 'Geist Mono Fallback';
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: oklch(0.985 0.003 17);
|
||||
--foreground: oklch(0.22 0.02 25);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.22 0.02 25);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.22 0.02 25);
|
||||
--primary: oklch(0.52 0.21 27);
|
||||
--primary-foreground: oklch(0.99 0.005 17);
|
||||
--secondary: oklch(0.96 0.008 20);
|
||||
--secondary-foreground: oklch(0.3 0.02 25);
|
||||
--muted: oklch(0.96 0.006 20);
|
||||
--muted-foreground: oklch(0.52 0.02 25);
|
||||
--accent: oklch(0.95 0.03 22);
|
||||
--accent-foreground: oklch(0.45 0.2 27);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.91 0.008 25);
|
||||
--input: oklch(0.91 0.008 25);
|
||||
--ring: oklch(0.52 0.21 27);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
html {
|
||||
@apply font-sans;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Analytics } from '@vercel/analytics/next'
|
||||
import type { Metadata } from 'next'
|
||||
import { Plus_Jakarta_Sans, Geist_Mono } from 'next/font/google'
|
||||
import './globals.css'
|
||||
|
||||
const jakarta = Plus_Jakarta_Sans({
|
||||
variable: '--font-jakarta',
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
})
|
||||
const geistMono = Geist_Mono({
|
||||
variable: '--font-geist-mono',
|
||||
subsets: ['latin'],
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'WebGIS Geospasial — Masuk',
|
||||
description:
|
||||
'Platform analisis spasial untuk pengentasan kemiskinan berbasis data geospasial.',
|
||||
generator: 'v0.app',
|
||||
icons: {
|
||||
icon: [
|
||||
{
|
||||
url: '/icon-light-32x32.png',
|
||||
media: '(prefers-color-scheme: light)',
|
||||
},
|
||||
{
|
||||
url: '/icon-dark-32x32.png',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: '/icon.svg',
|
||||
type: 'image/svg+xml',
|
||||
},
|
||||
],
|
||||
apple: '/apple-icon.png',
|
||||
},
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
return (
|
||||
<html lang="id" className={`${jakarta.variable} ${geistMono.variable} bg-background`}>
|
||||
<body className="font-sans antialiased">{children}{process.env.NODE_ENV === 'production' && <Analytics />}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import Image from "next/image"
|
||||
import { LoginForm } from "@/components/login-form"
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<main className="flex min-h-svh w-full">
|
||||
{/* Left: hero / map */}
|
||||
<section className="relative hidden w-1/2 overflow-hidden lg:block">
|
||||
<Image
|
||||
src="/images/map-bg.png"
|
||||
alt="Visualisasi peta analisis spasial"
|
||||
fill
|
||||
priority
|
||||
className="object-cover"
|
||||
/>
|
||||
{/* Layered red overlay for elegant depth */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 bg-gradient-to-br from-[oklch(0.42_0.2_27/0.85)] via-[oklch(0.32_0.15_25/0.8)] to-[oklch(0.18_0.06_25/0.92)]"
|
||||
/>
|
||||
{/* Fine grid texture */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 opacity-[0.06]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"linear-gradient(to right, white 1px, transparent 1px), linear-gradient(to bottom, white 1px, transparent 1px)",
|
||||
backgroundSize: "52px 52px",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 flex h-full flex-col justify-between p-12 xl:p-16">
|
||||
{/* Top badge */}
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-white/15 backdrop-blur-sm ring-1 ring-white/25">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
className="h-5 w-5 text-white"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden
|
||||
>
|
||||
<path d="M12 21s-7-5.6-7-11a7 7 0 1 1 14 0c0 5.4-7 11-7 11Z" />
|
||||
<circle cx="12" cy="10" r="2.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="font-heading text-sm font-bold tracking-wide text-white">
|
||||
WebGIS Geospasial
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Headline */}
|
||||
<div className="relative max-w-xl">
|
||||
{/* glowing radial gradient behind headline */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -left-10 top-8 -z-10 h-72 w-[34rem] -translate-y-4"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(closest-side, rgba(255,255,255,0.32), rgba(255,120,120,0.12), transparent)",
|
||||
filter: "blur(8px)",
|
||||
}}
|
||||
/>
|
||||
<div className="mb-5 inline-flex items-center gap-2 rounded-full bg-white/12 px-3.5 py-1.5 text-xs font-semibold text-white/90 ring-1 ring-white/20 backdrop-blur-sm">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-white" />
|
||||
Platform Analisis Data Spasial
|
||||
</div>
|
||||
<h1 className="font-heading text-4xl font-extrabold leading-[1.1] tracking-tight text-white text-balance xl:text-5xl">
|
||||
Analisis Spasial Pengentasan Kemiskinan
|
||||
</h1>
|
||||
<p className="mt-5 max-w-md text-base leading-relaxed text-white/80 text-pretty">
|
||||
Petakan, pantau, dan analisis indikator kemiskinan secara geospasial untuk
|
||||
pengambilan keputusan kebijakan yang tepat sasaran dan berbasis data.
|
||||
</p>
|
||||
|
||||
{/* Mini stats */}
|
||||
<div className="mt-10 grid grid-cols-3 gap-4 border-t border-white/15 pt-7">
|
||||
{[
|
||||
{ v: "514", l: "Kabupaten/Kota" },
|
||||
{ v: "34", l: "Provinsi" },
|
||||
{ v: "99.9%", l: "Akurasi Data" },
|
||||
].map((s) => (
|
||||
<div key={s.l}>
|
||||
<p className="font-heading text-2xl font-bold text-white">{s.v}</p>
|
||||
<p className="mt-1 text-xs font-medium text-white/65">{s.l}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs font-medium text-white/55">
|
||||
Data terintegrasi · Pembaruan berkala · Aman & terenkripsi
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Right: form */}
|
||||
<section className="relative flex w-full items-center justify-center bg-background px-6 py-12 sm:px-10 lg:w-1/2">
|
||||
{/* soft ambient accents */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute right-0 top-0 h-72 w-72 rounded-full bg-primary/8 blur-3xl"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute bottom-0 left-0 h-72 w-72 rounded-full bg-accent/40 blur-3xl"
|
||||
/>
|
||||
<LoginForm />
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "base-nova",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useState } from "react"
|
||||
import { Eye, EyeOff, Lock, User, Loader2, ArrowRight } from "lucide-react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export function LoginForm() {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [username, setUsername] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setTimeout(() => setLoading(false), 1600)
|
||||
}
|
||||
|
||||
const demoAccounts = [
|
||||
{ icon: "👑", role: "Admin", user: "admin", pass: "password" },
|
||||
{ icon: "📝", role: "Petugas", user: "petugas", pass: "password" },
|
||||
{ icon: "👁️", role: "Guest", user: "guest", pass: "password" },
|
||||
]
|
||||
|
||||
function fillDemo(user: string, pass: string) {
|
||||
setUsername(user)
|
||||
setPassword(pass)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-md">
|
||||
{/* Glassmorphism card */}
|
||||
<div className="relative rounded-3xl border border-white/60 bg-white/70 p-8 shadow-[0_40px_120px_-30px_rgba(198,40,40,0.28),0_18px_50px_-20px_rgba(198,40,40,0.14)] backdrop-blur-xl sm:p-10">
|
||||
{/* subtle red glow accent */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -right-6 -top-6 h-24 w-24 rounded-full bg-primary/15 blur-2xl"
|
||||
/>
|
||||
|
||||
{/* Brand */}
|
||||
<div className="mb-8 flex items-center gap-3">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-primary text-primary-foreground shadow-lg shadow-primary/25">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
className="h-6 w-6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden
|
||||
>
|
||||
<path d="M12 21s-7-5.6-7-11a7 7 0 1 1 14 0c0 5.4-7 11-7 11Z" />
|
||||
<circle cx="12" cy="10" r="2.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-heading text-base font-bold leading-none tracking-tight text-foreground">
|
||||
WebGIS Geospasial
|
||||
</p>
|
||||
<p className="mt-1 text-xs font-medium text-muted-foreground">Sistem Analisis Spasial</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-7">
|
||||
<h2 className="font-heading text-2xl font-bold tracking-tight text-foreground text-balance">
|
||||
Selamat Datang Kembali
|
||||
</h2>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground text-pretty">
|
||||
Masuk ke akun Anda untuk mengakses peta dan data analisis spasial.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
|
||||
{/* Username */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="username" className="text-sm font-semibold text-foreground">
|
||||
Nama Pengguna
|
||||
</Label>
|
||||
<div className="group relative">
|
||||
<User
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute left-3.5 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-muted-foreground transition-colors group-focus-within:text-primary"
|
||||
/>
|
||||
<Input
|
||||
id="username"
|
||||
type="text"
|
||||
required
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="Masukkan nama pengguna"
|
||||
className="h-12 rounded-xl border-border/80 bg-white/80 pl-11 text-sm shadow-sm transition-all hover:border-primary/60 focus-visible:border-primary focus-visible:ring-[3px] focus-visible:ring-primary/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="password" className="text-sm font-semibold text-foreground">
|
||||
Kata Sandi
|
||||
</Label>
|
||||
<div className="group relative">
|
||||
<Lock
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute left-3.5 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-muted-foreground transition-colors group-focus-within:text-primary"
|
||||
/>
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Masukkan kata sandi"
|
||||
className="h-12 rounded-xl border-border/80 bg-white/80 pl-11 pr-11 text-sm shadow-sm transition-all hover:border-primary/60 focus-visible:border-primary focus-visible:ring-[3px] focus-visible:ring-primary/20"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword((s) => !s)}
|
||||
aria-label={showPassword ? "Sembunyikan kata sandi" : "Tampilkan kata sandi"}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 rounded-md p-1 text-muted-foreground transition-colors hover:text-primary"
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-[18px] w-[18px]" /> : <Eye className="h-[18px] w-[18px]" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Remember + forgot */}
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="remember" className="flex cursor-pointer items-center gap-2 select-none">
|
||||
<Checkbox
|
||||
id="remember"
|
||||
className="data-[state=checked]:border-primary data-[state=checked]:bg-primary"
|
||||
/>
|
||||
<span className="text-sm font-medium text-muted-foreground">Ingat saya</span>
|
||||
</label>
|
||||
<a
|
||||
href="#"
|
||||
className="text-sm font-semibold text-primary transition-colors hover:text-primary/80"
|
||||
>
|
||||
Lupa sandi?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className={cn(
|
||||
"group mt-1 flex h-12 items-center justify-center gap-2 rounded-xl bg-primary text-sm font-semibold text-primary-foreground",
|
||||
"shadow-lg shadow-primary/25 transition-all duration-300",
|
||||
"hover:-translate-y-0.5 hover:bg-[oklch(0.46_0.21_27)] hover:shadow-xl hover:shadow-primary/35",
|
||||
"active:translate-y-0 disabled:cursor-not-allowed disabled:opacity-80",
|
||||
)}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Memproses...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Masuk
|
||||
<ArrowRight className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Demo accounts helper */}
|
||||
<div className="mt-7 rounded-xl border border-border/70 bg-gray-50 p-4">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-primary" />
|
||||
<p className="text-xs font-bold uppercase tracking-wide text-foreground">Info Akun Demo</p>
|
||||
<span className="text-[11px] font-medium text-muted-foreground">· klik untuk mengisi</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{demoAccounts.map((acc) => (
|
||||
<button
|
||||
key={acc.role}
|
||||
type="button"
|
||||
onClick={() => fillDemo(acc.user, acc.pass)}
|
||||
className="group flex cursor-pointer items-center justify-between gap-3 rounded-lg border border-transparent bg-white/60 px-3 py-2 text-left transition-all hover:border-primary/30 hover:bg-white hover:shadow-sm"
|
||||
>
|
||||
<span className="flex items-center gap-2.5">
|
||||
<span aria-hidden className="text-sm leading-none">{acc.icon}</span>
|
||||
<span className="text-xs font-semibold text-foreground">{acc.role}</span>
|
||||
</span>
|
||||
<span className="font-mono text-[11px] text-muted-foreground transition-colors group-hover:text-primary">
|
||||
{acc.user} · {acc.pass}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-6 text-center text-xs text-muted-foreground">
|
||||
© {new Date().getFullYear()} WebGIS Geospasial · Kementerian Analisis Spasial
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Button as ButtonPrimitive } from '@base-ui/react/button'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
|
||||
outline:
|
||||
'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
|
||||
ghost:
|
||||
'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
|
||||
destructive:
|
||||
'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
||||
icon: 'size-8',
|
||||
'icon-xs':
|
||||
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||
'icon-sm':
|
||||
'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
|
||||
'icon-lg': 'size-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
...props
|
||||
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
||||
return (
|
||||
<ButtonPrimitive
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
|
||||
>
|
||||
<CheckIcon
|
||||
/>
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as React from "react"
|
||||
import { Input as InputPrimitive } from "@base-ui/react/input"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<InputPrimitive
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({ className, ...props }: React.ComponentProps<"label">) {
|
||||
return (
|
||||
<label
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "my-project",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.5.0",
|
||||
"@vercel/analytics": "1.6.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.16.0",
|
||||
"next": "16.2.6",
|
||||
"react": "^19",
|
||||
"react-dom": "^19",
|
||||
"shadcn": "^4.8.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.0",
|
||||
"@types/node": "^24",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"postcss": "^8.5",
|
||||
"tailwindcss": "^4.2.0",
|
||||
"typescript": "5.7.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 585 B |
|
After Width: | Height: | Size: 566 B |
@@ -0,0 +1,26 @@
|
||||
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
@media (prefers-color-scheme: light) {
|
||||
.background { fill: black; }
|
||||
.foreground { fill: white; }
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.background { fill: white; }
|
||||
.foreground { fill: black; }
|
||||
}
|
||||
</style>
|
||||
<g clip-path="url(#clip0_7960_43945)">
|
||||
<rect class="background" width="180" height="180" rx="37" />
|
||||
<g style="transform: scale(95%); transform-origin: center">
|
||||
<path class="foreground"
|
||||
d="M101.141 53H136.632C151.023 53 162.689 64.6662 162.689 79.0573V112.904H148.112V79.0573C148.112 78.7105 148.098 78.3662 148.072 78.0251L112.581 112.898C112.701 112.902 112.821 112.904 112.941 112.904H148.112V126.672H112.941C98.5504 126.672 86.5638 114.891 86.5638 100.5V66.7434H101.141V100.5C101.141 101.15 101.191 101.792 101.289 102.422L137.56 66.7816C137.255 66.7563 136.945 66.7434 136.632 66.7434H101.141V53Z" />
|
||||
<path class="foreground"
|
||||
d="M65.2926 124.136L14 66.7372H34.6355L64.7495 100.436V66.7372H80.1365V118.47C80.1365 126.278 70.4953 129.958 65.2926 124.136Z" />
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7960_43945">
|
||||
<rect width="180" height="180" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 568 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="215" height="48" fill="none"><path fill="#000" d="M57.588 9.6h6L73.828 38h-5.2l-2.36-6.88h-11.36L52.548 38h-5.2l10.24-28.4Zm7.16 17.16-4.16-12.16-4.16 12.16h8.32Zm23.694-2.24c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.486-7.72.12 3.4c.534-1.227 1.307-2.173 2.32-2.84 1.04-.693 2.267-1.04 3.68-1.04 1.494 0 2.76.387 3.8 1.16 1.067.747 1.827 1.813 2.28 3.2.507-1.44 1.294-2.52 2.36-3.24 1.094-.747 2.414-1.12 3.96-1.12 1.414 0 2.64.307 3.68.92s1.84 1.52 2.4 2.72c.56 1.2.84 2.667.84 4.4V38h-4.96V25.92c0-1.813-.293-3.187-.88-4.12-.56-.96-1.413-1.44-2.56-1.44-.906 0-1.68.213-2.32.64-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.84-.48 3.04V38h-4.56V25.92c0-1.2-.133-2.213-.4-3.04-.24-.827-.626-1.453-1.16-1.88-.506-.427-1.133-.64-1.88-.64-.906 0-1.68.227-2.32.68-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.827-.48 3V38h-4.96V16.8h4.48Zm26.723 10.6c0-2.24.427-4.187 1.28-5.84.854-1.68 2.067-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.84 0 3.494.413 4.96 1.24 1.467.827 2.64 2.08 3.52 3.76.88 1.653 1.347 3.693 1.4 6.12v1.32h-15.08c.107 1.813.614 3.227 1.52 4.24.907.987 2.134 1.48 3.68 1.48.987 0 1.88-.253 2.68-.76a4.803 4.803 0 0 0 1.84-2.2l5.08.36c-.64 2.027-1.84 3.64-3.6 4.84-1.733 1.173-3.733 1.76-6 1.76-2.08 0-3.906-.453-5.48-1.36-1.573-.907-2.786-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84Zm15.16-2.04c-.213-1.733-.76-3.013-1.64-3.84-.853-.827-1.893-1.24-3.12-1.24-1.44 0-2.6.453-3.48 1.36-.88.88-1.44 2.12-1.68 3.72h9.92ZM163.139 9.6V38h-5.04V9.6h5.04Zm8.322 7.2.24 5.88-.64-.36c.32-2.053 1.094-3.56 2.32-4.52 1.254-.987 2.787-1.48 4.6-1.48 2.32 0 4.107.733 5.36 2.2 1.254 1.44 1.88 3.387 1.88 5.84V38h-4.96V25.92c0-1.253-.12-2.28-.36-3.08-.24-.8-.64-1.413-1.2-1.84-.533-.427-1.253-.64-2.16-.64-1.44 0-2.573.48-3.4 1.44-.8.933-1.2 2.307-1.2 4.12V38h-4.96V16.8h4.48Zm30.003 7.72c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.443 8.16V38h-5.6v-5.32h5.6Z"/><path fill="#171717" fill-rule="evenodd" d="m7.839 40.783 16.03-28.054L20 6 0 40.783h7.839Zm8.214 0H40L27.99 19.894l-4.02 7.032 3.976 6.914H20.02l-3.967 6.943Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"target": "ES6",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||