feat: update components with Mantine UI and improve dark mode support

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-02-12 15:47:31 +08:00
parent cffb9f4aa4
commit 4ed1c664d1
22 changed files with 3074 additions and 588 deletions

285
Dashboard-MD/BUMDES.md Normal file
View File

@@ -0,0 +1,285 @@
# BUMDES & UMKM Desa — UI & Functional Specification
Dokumen ini menjelaskan spesifikasi **UI, tata letak, gaya visual, responsivitas, serta ekspektasi data & interaksi** untuk halaman **BUMDes & UMKM Desa (Jenna Analytic Module)**. Dokumen ini menjadi kontrak desain antara **UI/UX, Frontend, dan Backend**.
---
## 1. Tujuan Halaman
Halaman **BUMDes & UMKM Desa** berfungsi sebagai dashboard monitoring:
* Aktivitas UMKM desa
* Performa penjualan produk unggulan
* Omzet BUMDes
* Stok & tren produk
Target pengguna:
* Kepala Desa
* Admin Desa
* Operator BUMDes
---
## 2. Tata Letak Keseluruhan (Layout)
### 2.1 Struktur Utama
Layout menggunakan pola **Dashboard Sidebar Layout**:
* **Sidebar kiri (fixed)**
* **Topbar/header (fixed)**
* **Main content (scrollable)**
Grid utama:
* Desktop: `12-column grid`
* Tablet: `8-column grid`
* Mobile: `4-column grid`
---
### 2.2 Sidebar
Komponen:
* Logo DESA + tagline
* Search bar ("cari apa saja")
* Menu navigasi vertikal
Menu aktif:
* **Bumdes & UMKM Desa** (highlight biru muda)
Perilaku:
* Collapsible pada layar kecil
* Ikon + label (label hidden di mobile)
---
### 2.3 Topbar
Komponen:
* Nama desa
* Nama & jabatan user
* Avatar user
* Ikon notifikasi (badge)
* Ikon pengaturan
Perilaku:
* Sticky
* Dropdown user menu
---
## 3. Komponen UI Utama
### 3.1 KPI Cards (Ringkasan Atas)
Menampilkan ringkasan metrik utama:
| KPI | Deskripsi |
| -------------- | ---------------------- |
| UMKM Aktif | Jumlah UMKM beroperasi |
| UMKM Terdaftar | Total UMKM terdaftar |
| Omzet | Omzet BUMDes per bulan |
| Kategori UMKM | Jumlah kategori aktif |
Karakteristik:
* Card rounded
* Icon di kanan
* Angka besar (headline)
---
### 3.2 Update Penjualan Produk
Komponen:
* Section header berwarna biru gelap
* Button filter waktu:
* "Minggu ini"
* "Bulan ini"
Perilaku:
* Toggle data chart & tabel
* Default: Bulan ini
---
### 3.3 Produk Unggulan (Left Column)
Sub-komponen:
1. **Total Penjualan**
* Nominal
* Persentase perubahan
2. **Produk Aktif**
* Jumlah produk
* Jumlah kategori
3. **Total Transaksi**
* Jumlah transaksi bulan berjalan
4. **Top 3 Produk Terlaris**
* Ranking
* Nama produk
* Pelaku UMKM
* Growth indicator
---
### 3.4 Detail Penjualan Produk (Right Column)
Bentuk:
* Tabel data
Kolom:
* Produk
* Penjualan bulan ini
* Bulan lalu
* Trend
* Volume
* Stok
* Aksi
Elemen khusus:
* Trend: arrow hijau/merah
* Stok: badge warna (aman / kritis)
* Aksi: tombol "Detail"
Filter:
* Dropdown filter kategori / stok
---
## 4. Gaya & Tema Visual
### 4.1 Warna
| Elemen | Warna |
| ---------- | ------------------------- |
| Primary | Biru tua (#183A63 approx) |
| Secondary | Biru muda (#E6F0FF) |
| Success | Hijau (#22C55E) |
| Danger | Merah (#EF4444) |
| Background | Abu muda / putih |
---
### 4.2 Tipografi
* Font family: **Inter / Poppins / Sans-serif modern**
* Heading: Semi-bold
* Body text: Regular
* Angka KPI: Bold
Ukuran:
* H1: 2428px
* H2: 1820px
* Body: 1416px
---
### 4.3 Spasi & Card
* Padding card: 1620px
* Gap antar card: 16px
* Border radius: 1216px
* Shadow ringan
---
## 5. Responsivitas
### Desktop
* Sidebar terbuka penuh
* Tabel full
### Tablet
* Sidebar collapse
* KPI card wrap
### Mobile
* Sidebar drawer
* KPI card stack
* Tabel menjadi card list
---
## 6. Interaksi & UX Behavior
* Hover effect pada card & row tabel
* Tooltip pada ikon & trend
* Loading skeleton saat fetch data
* Empty state ("Belum ada data")
---
## 7. Ekspektasi Data (Backend Contract)
### KPI API
```json
{
"umkmAktif": 45,
"umkmTerdaftar": 68,
"omzet": 48000000,
"kategori": 34
}
```
### Produk API
```json
{
"produk": "Beras Premium Organik",
"penjualanBulanIni": 8500000,
"bulanLalu": 8500000,
"trend": 10,
"volume": "650 Kg",
"stok": "850 Kg"
}
```
---
## 8. Catatan Implementasi Teknis
* Frontend: React + Vite
* State management: query-based (TanStack Query)
* Chart (jika ditambah): Recharts / Chart.js
* Format uang: Intl.NumberFormat("id-ID")
---
## 9. Acceptance Checklist
* [ ] Semua KPI muncul & valid
* [ ] Filter waktu berfungsi
* [ ] Trend naik/turun akurat
* [ ] Responsif mobile
* [ ] Empty & loading state tersedia
---
Dokumen ini bersifat **final reference** untuk implementasi halaman **BUMDes & UMKM Desa**.

224
Dashboard-MD/KEAMANAN.md Normal file
View File

@@ -0,0 +1,224 @@
# KEAMANAN
Dokumen ini mendeskripsikan spesifikasi UI/UX dan ekspektasi teknis untuk modul **Keamanan Lingkungan Desa** pada sistem dashboard DESA. Dokumen ini menjadi acuan bersama bagi tim desain, frontend, dan backend.
---
## 1. Tujuan Halaman
Memberikan visibilitas real-time dan historis terhadap kondisi keamanan desa, meliputi:
* Status CCTV aktif
* Laporan kejadian keamanan
* Peta lokasi CCTV
* Monitoring tren dan respons kejadian
Target pengguna utama:
* Kepala Desa
* Perangkat Desa / Petugas Keamanan
---
## 2. Struktur Tata Letak (Layout)
### 2.1 Pola Umum
* **Layout dashboard dua kolom**
* Kolom kiri: monitoring visual (peta)
* Kolom kanan: daftar laporan keamanan
* Header KPI ringkas di bagian atas
* Semua konten dibungkus dalam **card container** dengan radius konsisten
Grid:
* Desktop: 12-column grid
* Tablet: 8-column grid
* Mobile: 1-column (stack vertikal)
---
## 3. Komponen UI
### 3.1 KPI Cards (Header)
1. **CCTV Aktif**
* Value: `20`
* Subtext: `Kamera Online`
* Ikon: Kamera CCTV
2. **Laporan Keamanan**
* Value: `15`
* Subtext: `Minggu ini`
* Ikon: Chat / laporan
Perilaku:
* Tooltip saat hover (penjelasan metrik)
* Klik opsional → navigasi ke halaman detail
---
### 3.2 Peta Keamanan CCTV
**Judul Card:** Peta Keamanan CCTV
**Subjudul:** Titik Lokasi CCTV
Isi:
* Embedded map (Google Maps / Mapbox)
* Marker lokasi CCTV
* Hijau: aktif
* Abu-abu: offline (future)
Kontrol:
* Toggle tampilan (grid / fullscreen)
* Zoom in / out
Interaksi:
* Klik marker → popup info CCTV
* ID CCTV
* Lokasi
* Status
* Waktu terakhir aktif
---
### 3.3 Daftar Laporan Keamanan
**Judul:** Laporan Keamanan Lingkungan
Item laporan (card list):
* Judul kejadian (contoh: *Pencurian Motor*)
* Timestamp relatif ("2 jam yang lalu")
* Tanggal & jam
* Lokasi kejadian
* Badge status: `Baru`
Perilaku:
* Scrollable container
* Klik item → detail laporan (modal / halaman baru)
---
## 4. Gaya & Tema Visual
### 4.1 Warna
* Primary: Navy Blue (`#1F3A5F` perkiraan)
* Background: Light Blue / Off-white (`#F5F9FC`)
* Card: Putih
* Success: Hijau (status aktif)
* Alert: Merah muda / merah untuk laporan baru
### 4.2 Tipografi
* Font utama: Sans-serif modern (Inter / Poppins)
* Heading: Semi-bold
* Body text: Regular
* Angka KPI: Bold
Ukuran relatif:
* Heading: 1618px
* Body: 1314px
* KPI Value: 2228px
### 4.3 Spasi & Elevasi
* Padding card: 1620px
* Gap antar card: 16px
* Border radius: 1216px
* Shadow ringan untuk card
---
## 5. Responsivitas
### Desktop
* Peta dan laporan tampil berdampingan
### Tablet
* KPI tetap horizontal
* Peta dan laporan bisa stack 2 baris
### Mobile
* Semua komponen stack vertikal
* Peta dalam mode scroll / fullscreen
* List laporan menjadi full-width
---
## 6. Ekspektasi Interaksi & UX
* Hover state pada card & list item
* Tooltip pada ikon KPI
* Loading skeleton saat data fetch
* Empty state bila tidak ada laporan
* Badge status real-time (baru / diproses / selesai future)
---
## 7. Ekspektasi Data (Backend Contract)
### 7.1 KPI Summary
```json
{
"cctvActive": 20,
"securityReportsWeekly": 15
}
```
### 7.2 CCTV Locations
```json
[
{
"id": "CCTV-01",
"lat": -8.5,
"lng": 115.2,
"status": "active",
"lastSeen": "2025-10-06T14:30:00Z"
}
]
```
### 7.3 Security Reports
```json
[
{
"id": "REP-001",
"title": "Pencurian Motor",
"reportedAt": "2025-10-06T15:30:00Z",
"location": "Jl. Kecubung 20",
"status": "new"
}
]
```
---
## 8. Catatan Pengembangan Lanjutan
* Filter laporan berdasarkan jenis & waktu
* Integrasi snapshot CCTV
* Heatmap area rawan
* Role-based access (admin / petugas)
---
Dokumen ini wajib diperbarui bila terdapat perubahan UI atau kebutuhan data pada modul **Keamanan**.

View File

@@ -0,0 +1,239 @@
# KINERJA-DIVISI-2
## 1. Tujuan Halaman
Halaman **Kinerja Divisi** berfungsi sebagai dashboard operasional untuk memantau progres pekerjaan lintas divisi perangkat desa secara real-time, mencakup status tugas, arsip digital, kegiatan, diskusi internal, dan statistik pendukung.
Halaman ini dipakai oleh:
* Kepala Desa
* Sekretaris Desa
* Kepala Divisi
* Admin Operasional
---
## 2. Struktur Tata Letak (Overall Layout)
### Pola Layout
* **Dashboard berbasis Card + Grid**
* Scroll vertikal satu halaman
* Tidak ada sidebar khusus (diasumsikan sudah ada global sidebar)
### Urutan Section (Top → Bottom)
1. Grafik Progres Tugas per Divisi
2. Ringkasan Tugas per Divisi (Sekretariat, Keuangan, Sosial, Humas)
3. Arsip Digital Perangkat Desa
4. Kartu Progres Kegiatan (Event / Program)
5. Statistik Dokumen & Progres Kegiatan
6. Diskusi Internal
7. Agenda / Acara Hari Ini
---
## 3. Komponen UI
### 3.1 Grafik Progres Tugas per Divisi
**Tipe:** Grouped Bar Chart
**Data:**
* Divisi: Sekretariat, Keuangan, Sosial, Humas
* Status:
* Selesai
* Berjalan
* Tertunda
**Interaksi:**
* Hover tooltip menampilkan jumlah tugas
* Legend clickable (toggle series)
---
### 3.2 Card Ringkasan Divisi
Setiap divisi memiliki satu card berisi daftar tugas.
**Struktur Card:**
* Judul divisi
* List tugas:
* Nama tugas
* Badge status: `selesai | proses | tertunda`
**Status Badge:**
* Hijau: Selesai
* Kuning: Proses
* Merah: Tertunda
---
### 3.3 Arsip Digital Perangkat Desa
**Tipe:** Action Card / Button Card
**Item:**
* Surat Keputusan
* Laporan Keuangan
* Dokumentasi
* Notulensi Rapat
**Interaksi:**
* Klik membuka modul arsip
* Role-based access (view / upload)
---
### 3.4 Progres Kegiatan / Program
**Tipe:** Progress Card
**Isi:**
* Nama kegiatan
* Progress bar horizontal
* Tanggal
* Badge status (selesai)
---
### 3.5 Statistik Dokumen
#### a. Jumlah Dokumen
**Tipe:** Bar Chart
* Kategori: Gambar, Dokumen
#### b. Progres Kegiatan
**Tipe:** Pie Chart
**Status:**
* Selesai
* Dikerjakan
* Segera Dikerjakan
* Dibatalkan
---
### 3.6 Diskusi Internal
**Tipe:** List Card
**Isi:**
* Judul diskusi
* Pengirim
* Timestamp
**Interaksi:**
* Klik membuka thread diskusi
---
### 3.7 Agenda / Acara Hari Ini
**Tipe:** Informational Card
**State:**
* Empty state: "Tidak ada acara hari ini"
---
## 4. Gaya & Tema Visual
### 4.1 Warna
* Background utama: `#0F172A` / `#111827`
* Card: `#1E293B`
* Primary accent: Biru tua
* Success: Hijau
* Warning: Kuning
* Danger: Merah
### 4.2 Tipografi
* Font utama: **Inter / Poppins**
* Heading: Semi-bold
* Body: Regular
* Angka & statistik: Medium
### 4.3 Spasi & Radius
* Padding card: 1624px
* Gap grid: 1620px
* Border radius: 1216px
---
## 5. Responsivitas
### Desktop
* Multi-column grid (24 kolom)
### Tablet
* Grid 2 kolom
* Grafik full-width
### Mobile
* Semua card 1 kolom
* Chart di-scroll horizontal bila perlu
---
## 6. Ekspektasi Interaksi & Data
### 6.1 State Management
* Filter status tugas
* Sinkronisasi real-time (polling / websocket)
### 6.2 Contoh Struktur Data
```json
{
"division": "Keuangan",
"tasks": [
{ "title": "Laporan APBDes", "status": "selesai" },
{ "title": "Verifikasi Dana", "status": "tertunda" }
]
}
```
---
## 7. Acceptance Criteria
* Semua status warna konsisten
* Chart sesuai data backend
* Empty state ditampilkan bila data kosong
* Mobile friendly tanpa overflow
---
## 8. Catatan Implementasi
* Chart library: Chart.js / Recharts / ECharts
* Dark mode default
* Akses berbasis role (viewer, editor, admin)
---
**Status Dokumen:** Final Draft
**Digunakan untuk:** Implementasi Frontend & Kontrak API

225
Dashboard-MD/PENGAUDAN.md Normal file
View File

@@ -0,0 +1,225 @@
# PENGADUAN-2.md
## 1. Tujuan Halaman
Dashboard **Pengaduan Masyarakat** berfungsi untuk memantau, menganalisis, dan menindaklanjuti pengaduan warga secara real-time. Halaman ini mendukung **Light Mode** dan **Dark Mode** dengan struktur UI dan interaksi yang konsisten.
---
## 2. Struktur Komponen UI
### 2.1 Summary Cards (Statistik Utama)
Menampilkan ringkasan status pengaduan bulan berjalan.
**Komponen:**
* Total Pengaduan
* Baru (Belum diproses)
* Diproses (Sedang ditangani)
* Selesai (Terselesaikan)
**Elemen UI:**
* Angka utama (bold, besar)
* Label deskriptif
* Ikon status (chat, alert, clock, check)
**Interaksi:**
* Hover: efek elevasi + highlight border
* Click (opsional): filter dashboard berdasarkan status
---
### 2.2 Grafik Tren Pengaduan (Line Chart)
Menampilkan jumlah pengaduan per bulan.
**Komponen:**
* Sumbu X: Bulan
* Sumbu Y: Jumlah pengaduan
* Garis tren + titik data
**Interaksi:**
* Tooltip saat hover (bulan & jumlah)
* Toggle range waktu (bulanan/tahunan opsional)
---
### 2.3 Surat Terbanyak (Horizontal Bar Chart)
Menunjukkan jenis surat/pengaduan yang paling sering diajukan.
**Contoh Data:**
* KTP
* KK
* Domisili
* Usaha
* Lainnya
**Interaksi:**
* Hover tooltip
* Click bar → filter daftar pengajuan
---
### 2.4 Pengajuan Terbaru (List)
Daftar pengaduan terbaru dari warga.
**Isi Item:**
* Nama pengaju
* Jenis pengaduan
* Waktu pengajuan
* Badge status: Baru / Diproses / Selesai
**Interaksi:**
* Click item → halaman detail
* Scrollable list
---
### 2.5 Ajuan Ide Inovatif (List)
Menampilkan ide/inisiatif warga untuk desa.
**Isi Item:**
* Nama pengaju
* Judul ide
* Kategori
* Tombol "Detail"
---
## 3. Tata Letak Keseluruhan (Layout)
**Desktop:**
* Grid 12 kolom
* Baris 1: 4 Summary Cards
* Baris 2: Line Chart (full width)
* Baris 3: 3 kolom (Bar Chart, Pengajuan Terbaru, Ide Inovatif)
**Tablet:**
* Summary card 2x2
* Chart full width
* List stack vertikal
**Mobile:**
* Semua komponen stacked
* Chart scroll horizontal
---
## 4. Gaya & Tema Visual
### 4.1 Light Mode
**Warna:**
* Background utama: #F6F9FC
* Card: #FFFFFF
* Primary: #2563EB (Blue)
* Text utama: #0F172A
* Text sekunder: #64748B
* Border: #E2E8F0
**Chart:**
* Line: Biru
* Bar: Biru tua
* Grid: Abu muda
---
### 4.2 Dark Mode
**Warna:**
* Background utama: #0B1220
* Card: #111827 / #0F172A
* Primary: #3B82F6
* Text utama: #E5E7EB
* Text sekunder: #9CA3AF
* Border: #1F2937
**Chart:**
* Line: Biru terang
* Bar: Biru medium
* Grid: Abu gelap
---
## 5. Tipografi
* Font utama: Inter / Poppins
* Heading: 600700
* Body: 400500
* Angka statistik: 700
---
## 6. Spasi & Radius
* Padding card: 1624px
* Gap grid: 1624px
* Border radius card: 1216px
* Icon container: circle / rounded-full
---
## 7. Responsivitas
* Mobile-first
* Breakpoint umum: 640px, 768px, 1024px
* Chart auto-resize
* List menjadi full-width
---
## 8. Ekspektasi Data & Interaksi Backend
### 8.1 Contoh Data Summary
```json
{
"total": 42,
"baru": 14,
"diproses": 14,
"selesai": 14
}
```
### 8.2 Contoh Data Tren
```json
[
{ "bulan": "Apr", "jumlah": 30 },
{ "bulan": "Mei", "jumlah": 50 },
{ "bulan": "Jun", "jumlah": 42 }
]
```
---
## 9. Catatan Teknis
* Chart: Recharts / Chart.js / ECharts
* State: loading, empty, error
* Theme switch: CSS variable / Tailwind dark mode
* Konsistensi warna status di seluruh modul
---
Dokumen ini menjadi **UI Spec + UX Contract** untuk modul Pengaduan dan siap diturunkan ke desain Figma maupun implementasi Frontend.

37
Dashboard-MD/SIDEBAR.md Normal file
View File

@@ -0,0 +1,37 @@
# SIDEBAR.md
## Spesifikasi Sidebar Navigasi DESA+
### Tujuan
Membuat sidebar navigasi dengan **indikator menu aktif (isActive)** seperti pada desain:
- Menu aktif memiliki **background biru muda**
- Ada **indikator garis vertikal di sisi kiri**
- Status aktif mengikuti **route/path yang sedang dibuka**
---
## Perilaku isActive
### Definisi isActive
Sebuah menu dianggap **aktif** jika:
- `currentPath === menu.path`
- atau (opsional) `currentPath.startsWith(menu.path)` untuk nested route
---
## Tampilan Menu Aktif
Jika menu **AKTIF**, maka:
- Background: `#E6F0FF` (biru muda)
- Text: bold / semibold
- Border kiri:
- Lebar: `4px`
- Warna: `#1E40AF` (biru tua)
- Border radius: `8px`
- Transition halus (`150200ms`)
Jika menu **TIDAK AKTIF**:
- Background transparan
- Text normal
- Hover state:
- Background `#F1F5F9`
---

230
Dashboard-MD/SOSIAL.md Normal file
View File

@@ -0,0 +1,230 @@
# SOSIAL.md
Dokumen ini mendeskripsikan spesifikasi UI/UX, tata letak, gaya visual, serta ekspektasi data & interaksi untuk **Halaman Sosial Desa**. Digunakan sebagai acuan bersama antara UI/UX Designer, Frontend Developer, dan Backend/API.
---
## 1. Tujuan Halaman
Halaman **Sosial** berfungsi sebagai dashboard monitoring:
* Kesehatan masyarakat (ibu hamil, balita, stunting)
* Aktivitas Posyandu
* Pendidikan desa
* Beasiswa
* Event sosial & budaya
Fokus utama: **ringkas, informatif, real-time friendly**, dan mudah dipahami perangkat desa.
---
## 2. Tata Letak Keseluruhan
### Struktur Global
* **Sidebar kiri (persisten)**
* Navigasi utama aplikasi desa
* Menu aktif: `Sosial`
* **Topbar**
* Nama desa
* Profil user (Kepala Desa)
* Notifikasi
* Toggle tema (opsional)
* **Main Content Area (scrollable)**
* Grid berbasis card
* Layout desktop: 23 kolom
* Mobile: 1 kolom (stack)
---
## 3. Komponen UI
### 3.1 Statistik Kesehatan (Summary Cards)
**Tipe:** KPI Cards
| Komponen | Deskripsi |
| ---------------- | -------------------------------- |
| Ibu Hamil Aktif | Total ibu hamil aktif saat ini |
| Balita Terdaftar | Total balita tercatat |
| Alert Stunting | Jumlah kasus/peringatan stunting |
| Posyandu Aktif | Jumlah posyandu yang aktif |
**Elemen UI:**
* Angka besar (headline)
* Label kecil
* Ikon kontekstual
---
### 3.2 Statistik Kesehatan (Progress Bar)
Menampilkan capaian dalam bentuk **horizontal progress bar**:
* Imunisasi Lengkap
* Pemeriksaan Rutin
* Gizi Baik
* Target Stunting
**Behavior:**
* Persentase dinamis
* Warna bar menyesuaikan status (normal / warning)
---
### 3.3 Jadwal Posyandu
**Tipe:** List Card
Setiap item menampilkan:
* Nama Posyandu
* Tanggal
* Jam kegiatan
**Interaksi (opsional):**
* Klik item → detail jadwal
* Filter berdasarkan wilayah / tanggal
---
### 3.4 Pendidikan
**Tipe:** Informational List
#### Data Siswa
* TK / PAUD
* SD
* SMP
* SMA
#### Info Sekolah
* Jumlah lembaga pendidikan
* Jumlah tenaga pengajar
Layout sederhana, fokus ke angka.
---
### 3.5 Beasiswa Desa
**Tipe:** Highlight Card
* Jumlah penerima beasiswa
* Total dana tersalurkan
* Tahun ajaran
Digunakan sebagai **headline informasi bantuan pendidikan**.
---
### 3.6 Kalender Event Budaya
**Tipe:** Event List
Setiap event:
* Nama kegiatan
* Tanggal
* Lokasi
**Interaksi:**
* Klik → detail event
* Potensi integrasi kalender
---
## 4. Gaya & Tema Visual
### Warna
* **Primary:** Biru navy (#1F3A5F estimasi)
* **Secondary:** Biru muda / abu terang
* **Success:** Hijau
* **Warning:** Oranye / Merah (stunting alert)
### Font
* Sans-serif modern (Inter / Poppins)
* Hierarki jelas:
* Judul card
* Angka utama
* Label kecil
### Spasi & Bentuk
* Padding card: 1624px
* Border radius: 1216px
* Shadow ringan (soft elevation)
---
## 5. Responsivitas
### Desktop
* Grid 23 kolom
* Semua card terlihat tanpa overflow horizontal
### Tablet
* Grid 2 kolom
### Mobile
* 1 kolom (stack)
* KPI cards jadi swipe / vertical list
---
## 6. Ekspektasi Interaksi & Data
### Interaksi Umum
* Hover state pada card
* Click-through ke halaman detail
* Data auto-refresh (optional)
### Ekspektasi API (contoh)
```json
{
"ibu_hamil": 87,
"balita": 342,
"alert_stunting": 12,
"posyandu_aktif": 8,
"kesehatan": {
"imunisasi": 92,
"pemeriksaan": 88,
"gizi": 86,
"stunting": 14
}
}
```
---
## 7. Acceptance Checklist
* [ ] Semua data tampil konsisten
* [ ] Tidak ada overflow di mobile
* [ ] Progress bar sesuai persentase
* [ ] Event & jadwal dapat diklik
* [ ] Alert stunting terlihat jelas
---
**Catatan:**
Dokumen ini bersifat living document dan dapat diperluas ke modul detail (kesehatan, pendidikan, bantuan sosial).

View File

@@ -0,0 +1,303 @@
import { useState } from "react";
import {
Card,
Grid,
GridCol,
Group,
Text,
Title,
Button,
Badge,
Table,
Stack,
Select,
useMantineColorScheme
} from "@mantine/core";
import { IconBuildingStore, IconCategory, IconCurrency, IconUsers } from "@tabler/icons-react";
const BumdesPage = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === 'dark';
const [timeFilter, setTimeFilter] = useState<string>("bulan");
// Sample data for KPI cards
const kpiData = [
{
title: "UMKM Aktif",
value: 45,
icon: <IconUsers size={24} />,
color: "darmasaba-blue",
},
{
title: "UMKM Terdaftar",
value: 68,
icon: <IconBuildingStore size={24} />,
color: "darmasaba-success",
},
{
title: "Omzet",
value: "Rp 48.000.000",
icon: <IconCurrency size={24} />,
color: "darmasaba-warning",
},
{
title: "Kategori UMKM",
value: 34,
icon: <IconCategory size={24} />,
color: "darmasaba-danger",
},
];
// Sample data for top products
const topProducts = [
{
rank: 1,
name: "Beras Premium Organik",
umkmOwner: "Warung Pak Joko",
growth: "+12%",
},
{
rank: 2,
name: "Keripik Singkong",
umkmOwner: "Ibu Sari Snack",
growth: "+8%",
},
{
rank: 3,
name: "Madu Alami",
umkmOwner: "Peternakan Lebah",
growth: "+5%",
},
];
// Sample data for product sales
const productSales = [
{
produk: "Beras Premium Organik",
penjualanBulanIni: "Rp 8.500.000",
bulanLalu: "Rp 8.500.000",
trend: 10,
volume: "650 Kg",
stok: "850 Kg",
},
{
produk: "Keripik Singkong",
penjualanBulanIni: "Rp 4.200.000",
bulanLalu: "Rp 3.800.000",
trend: 10,
volume: "320 Kg",
stok: "120 Kg",
},
{
produk: "Madu Alami",
penjualanBulanIni: "Rp 3.750.000",
bulanLalu: "Rp 4.100.000",
trend: -8,
volume: "150 Liter",
stok: "45 Liter",
},
{
produk: "Kecap Tradisional",
penjualanBulanIni: "Rp 2.800.000",
bulanLalu: "Rp 2.500.000",
trend: 12,
volume: "280 Botol",
stok: "95 Botol",
},
];
return (
<Stack gap="lg">
{/* Page Header */}
<Group justify="space-between" align="center">
<Title order={2} c={dark ? "dark.0" : "black"}>
BUMDes & UMKM Desa
</Title>
</Group>
{/* KPI Cards */}
<Grid gutter="md">
{kpiData.map((kpi, index) => (
<GridCol key={index} span={{ base: 12, sm: 6, md: 3 }}>
<Card withBorder radius="md" padding="lg">
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{kpi.title}
</Text>
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
{typeof kpi.value === 'number' ? kpi.value.toLocaleString() : kpi.value}
</Text>
</Stack>
<Badge
variant="light"
color={kpi.color}
p={8}
radius="md"
>
{kpi.icon}
</Badge>
</Group>
</Card>
</GridCol>
))}
</Grid>
{/* Update Penjualan Produk Header */}
<Card withBorder radius="md" bg={dark ? "dark.8" : "darmasaba-blue.0"}>
<Group justify="space-between" align="center" px="md" py="xs">
<Title order={3} c={dark ? "dark.0" : "black"}>
Update Penjualan Produk
</Title>
<Group>
<Button
variant={timeFilter === "minggu" ? "filled" : "light"}
onClick={() => setTimeFilter("minggu")}
color="darmasaba-blue"
>
Minggu ini
</Button>
<Button
variant={timeFilter === "bulan" ? "filled" : "light"}
onClick={() => setTimeFilter("bulan")}
color="darmasaba-blue"
>
Bulan ini
</Button>
</Group>
</Group>
</Card>
<Grid gutter="md">
{/* Produk Unggulan (Left Column) */}
<GridCol span={{ base: 12, lg: 4 }}>
<Stack gap="md">
{/* Total Penjualan, Produk Aktif, Total Transaksi */}
<Card withBorder radius="md" p="lg">
<Stack gap="md">
<Group justify="space-between">
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>Total Penjualan</Text>
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>Rp 28.500.000</Text>
</Group>
<Group justify="space-between">
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>Produk Aktif</Text>
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>124 Produk</Text>
</Group>
<Group justify="space-between">
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>Total Transaksi</Text>
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>1.240 Transaksi</Text>
</Group>
</Stack>
</Card>
{/* Top 3 Produk Terlaris */}
<Card withBorder radius="md" p="lg">
<Title order={4} mb="md" c={dark ? "dark.0" : "black"}>Top 3 Produk Terlaris</Title>
<Stack gap="sm">
{topProducts.map((product) => (
<Group key={product.rank} justify="space-between" align="center">
<Group gap="sm">
<Badge
variant="filled"
color={product.rank === 1 ? "gold" : product.rank === 2 ? "gray" : "bronze"}
radius="xl"
size="lg"
>
{product.rank}
</Badge>
<Stack gap={0}>
<Text fw={500} c={dark ? "dark.0" : "black"}>{product.name}</Text>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{product.umkmOwner}</Text>
</Stack>
</Group>
<Badge
variant="light"
color={product.growth.startsWith('+') ? "green" : "red"}
>
{product.growth}
</Badge>
</Group>
))}
</Stack>
</Card>
</Stack>
</GridCol>
{/* Detail Penjualan Produk (Right Column) */}
<GridCol span={{ base: 12, lg: 8 }}>
<Card withBorder radius="md" p="lg">
<Group justify="space-between" mb="md">
<Title order={4} c={dark ? "dark.0" : "black"}>Detail Penjualan Produk</Title>
<Select
placeholder="Filter kategori"
data={[
{ value: 'semua', label: 'Semua Kategori' },
{ value: 'makanan', label: 'Makanan' },
{ value: 'minuman', label: 'Minuman' },
{ value: 'kerajinan', label: 'Kerajinan' },
]}
defaultValue="semua"
w={200}
/>
</Group>
<Table striped highlightOnHover withColumnBorders>
<Table.Thead>
<Table.Tr>
<Table.Th><Text c={dark ? "dark.3" : "dimmed"}>Produk</Text></Table.Th>
<Table.Th><Text c={dark ? "dark.3" : "dimmed"}>Penjualan Bulan Ini</Text></Table.Th>
<Table.Th><Text c={dark ? "dark.3" : "dimmed"}>Bulan Lalu</Text></Table.Th>
<Table.Th><Text c={dark ? "dark.3" : "dimmed"}>Trend</Text></Table.Th>
<Table.Th><Text c={dark ? "dark.3" : "dimmed"}>Volume</Text></Table.Th>
<Table.Th><Text c={dark ? "dark.3" : "dimmed"}>Stok</Text></Table.Th>
<Table.Th><Text c={dark ? "dark.3" : "dimmed"}>Aksi</Text></Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{productSales.map((product, index) => (
<Table.Tr key={index}>
<Table.Td>
<Text fw={500} c={dark ? "dark.0" : "black"}>{product.produk}</Text>
</Table.Td>
<Table.Td>
<Text fz={"sm"} c={dark ? "dark.0" : "black"}>{product.penjualanBulanIni}</Text>
</Table.Td>
<Table.Td>
<Text fz={"sm"} c={dark ? "dark.3" : "dimmed"}>{product.bulanLalu}</Text>
</Table.Td>
<Table.Td>
<Group gap="xs">
<Text c={product.trend >= 0 ? "green" : "red"}>
{product.trend >= 0 ? '↑' : '↓'} {Math.abs(product.trend)}%
</Text>
</Group>
</Table.Td>
<Table.Td>
<Text fz={"sm"} c={dark ? "dark.0" : "black"}>{product.volume}</Text>
</Table.Td>
<Table.Td>
<Badge
variant="light"
color={parseInt(product.stok) > 200 ? "green" : "yellow"}
>
{product.stok}
</Badge>
</Table.Td>
<Table.Td>
<Button variant="subtle" size="compact-sm" color="darmasaba-blue">
Detail
</Button>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Card>
</GridCol>
</Grid>
</Stack>
);
};
export default BumdesPage;

View File

@@ -74,7 +74,7 @@ export function DashboardContent() {
{/* Stats Cards */}
<Grid gutter="md">
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
<Card p="md" radius="md" h="100%" withBorder>
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" h="100%" withBorder bg={dark ? "#141D34" : "white"}>
<Group justify="space-between" align="flex-start" w="100%">
<Box style={{ flex: 1 }}>
<Text size="sm" c="dimmed" mb="xs">
@@ -99,7 +99,7 @@ export function DashboardContent() {
</Card>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
<Card p="md" radius="md" h="100%" withBorder>
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" h="100%" withBorder bg={dark ? "#141D34" : "white"}>
<Group justify="space-between" align="flex-start" w="100%">
<Box style={{ flex: 1 }}>
<Text size="sm" c="dimmed" mb="xs">
@@ -121,7 +121,7 @@ export function DashboardContent() {
</Card>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
<Card p="md" radius="md" h="100%" withBorder>
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" h="100%" withBorder bg={dark ? "#141D34" : "white"}>
<Group justify="space-between" align="flex-start" w="100%">
<Box style={{ flex: 1 }}>
<Text size="sm" c="dimmed" mb="xs">
@@ -146,7 +146,7 @@ export function DashboardContent() {
</Card>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
<Card p="md" radius="md" h="100%" withBorder>
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" h="100%" withBorder bg={dark ? "#141D34" : "white"}>
<Group justify="space-between" align="flex-start" w="100%">
<Box style={{ flex: 1 }}>
<Text size="sm" c="dimmed" mb="xs">
@@ -171,7 +171,7 @@ export function DashboardContent() {
<Grid gutter="lg">
{/* Bar Chart */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card p="md" radius="md" withBorder>
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" withBorder bg={dark ? "#141D34" : "white"}>
<Group justify="space-between" mb="md">
<Box>
<Title order={4} mb={5}>
@@ -232,7 +232,7 @@ export function DashboardContent() {
{/* Pie Chart */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card p="md" radius="md" withBorder>
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" withBorder bg={dark ? "#141D34" : "white"}>
<Title order={4} mb={5}>
Tingkat Kepuasan
</Title>
@@ -283,7 +283,7 @@ export function DashboardContent() {
<Grid gutter="lg">
{/* Divisi Teraktif */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card p="md" radius="md" withBorder>
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" withBorder bg={dark ? "#141D34" : "white"}>
<Group gap="xs" mb="lg">
<Box>
{/* Original SVG icon */}
@@ -355,7 +355,7 @@ export function DashboardContent() {
{/* Kalender */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card p="md" radius="md" withBorder>
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" withBorder bg={dark ? "#141D34" : "white"}>
<Group gap="xs" mb="lg">
<Calendar style={{ width: 20, height: 20 }} />
<Title order={4}>Kalender & Kegiatan Mendatang</Title>
@@ -378,7 +378,7 @@ export function DashboardContent() {
</Grid>
{/* APBDes Chart */}
<Card p="md" radius="md" withBorder>
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" withBorder bg={dark ? "#141D34" : "white"}>
<Title order={4} mb="lg">
Grafik APBDes
</Title>

View File

@@ -140,7 +140,7 @@ const DemografiPekerjaan = () => {
<Box className="space-y-6">
<Stack gap="xl">
<Group justify="space-between" align="center">
<Title order={1} fw={700}>
<Title order={2} fw={700}>
Demografi & Kependudukan
</Title>
<Button variant="filled">Export Data</Button>

View File

@@ -22,7 +22,7 @@ export function Header() {
const getPageTitle = () => {
switch (location.pathname) {
case "/":
return "Dashboard";
return "Desa Darmasaba";
case "/kinerja-divisi":
return "Kinerja Divisi";
case "/pengaduan":
@@ -43,14 +43,14 @@ export function Header() {
case "/pengaturan":
return "Pengaturan";
default:
return "Dashboard";
return "Desa Darmasaba";
}
};
return (
<Group justify="space-between" w="100%">
{/* Title */}
<Title order={2}>{getPageTitle()}</Title>
<Title order={3} c={"white"}>{getPageTitle()}</Title>
{/* Right Section */}
<Group gap="md">
@@ -59,15 +59,15 @@ export function Header() {
{/* User Info */}
<Group gap="sm">
<Box ta="right">
<Text size="sm" fw={500}>
<Text c={"white"} size="sm" fw={500}>
I. B. Surya Prabhawa M...
</Text>
<Text size="xs" c="dimmed">
<Text c={"white"} size="xs">
Kepala Desa
</Text>
</Box>
<Avatar color="blue" radius="xl">
<UserIcon style={{ width: "70%", height: "70%" }} />
<UserIcon color="white" style={{ width: "70%", height: "70%" }} />
</Avatar>
</Group>
@@ -84,13 +84,13 @@ export function Header() {
aria-label="Toggle color scheme"
>
{dark ? (
<Sun style={{ width: "70%", height: "70%" }} />
<Sun color="white" style={{ width: "70%", height: "70%" }} />
) : (
<Moon style={{ width: "70%", height: "70%" }} />
<Moon color="white" style={{ width: "70%", height: "70%" }} />
)}
</ActionIcon>
<ActionIcon variant="subtle" size="lg" radius="xl" pos="relative">
<Bell style={{ width: "70%", height: "70%" }} />
<Bell color="white" style={{ width: "70%", height: "70%" }} />
<Badge
size="xs"
color="red"

View File

@@ -10,6 +10,7 @@ import {
Stack,
Grid,
Box,
useMantineColorScheme,
} from "@mantine/core";
import { BarChart } from "@mantine/charts";
@@ -133,21 +134,17 @@ const busyHours = [
];
const JennaAnalytic = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return (
<Box className="space-y-6">
<Stack gap="xl">
<Group justify="space-between" align="center">
<Title order={1} fw={700}>
Jenna Analytic
</Title>
<Button variant="filled">Export Data</Button>
</Group>
{/* KPI Cards */}
<Grid gutter="lg">
{kpiData.map((kpi) => (
<Grid.Col key={kpi.id} span={{ base: 12, md: 6, lg: 3 }}>
<Card shadow="sm" padding="lg" radius="md" withBorder>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
<Group justify="space-between" align="flex-start" mb="xs">
<Text size="sm" fw={500} c="dimmed">
{kpi.title}
@@ -185,21 +182,42 @@ const JennaAnalytic = () => {
))}
</Grid>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
<Title order={3} fw={500} mb="md">
Interaksi Chatbot
</Title>
<BarChart
h={300}
data={chartData}
dataKey="day"
series={[{ name: 'total', color: 'blue' }]}
withLegend
/>
</Card>
{/* Charts and Lists Section */}
<Grid gutter="lg">
{/* Grafik Interaksi Chatbot (now Bar Chart) */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card shadow="sm" padding="lg" radius="md" withBorder>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
<Title order={3} fw={500} mb="md">
Interaksi Chatbot
Jam Tersibuk
</Title>
<BarChart
h={300}
data={chartData}
dataKey="day"
series={[{ name: 'total', color: 'blue' }]}
withLegend
/>
<Stack gap="sm">
{busyHours.map((item, index) => (
<Box key={index}>
<Text size="sm">
{item.period}
</Text>
<Group align="center">
<Progress value={item.percentage} flex={1} />
<Text size="sm" fw={500}>
{item.percentage}%
</Text>
</Group>
</Box>
))}
</Stack>
</Card>
</Grid.Col>
@@ -207,7 +225,7 @@ const JennaAnalytic = () => {
<Grid.Col span={{ base: 12, lg: 6 }}>
<Stack gap="lg">
{/* Topik Pertanyaan Terbanyak */}
<Card shadow="sm" padding="lg" radius="md" withBorder>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
<Title order={3} fw={500} mb="md">
Topik Pertanyaan Terbanyak
</Title>
@@ -231,29 +249,12 @@ const JennaAnalytic = () => {
</Card>
{/* Jam Tersibuk */}
<Card shadow="sm" padding="lg" radius="md" withBorder>
<Title order={3} fw={500} mb="md">
Jam Tersibuk
</Title>
<Stack gap="sm">
{busyHours.map((item, index) => (
<Group key={index} align="center">
<Text w={80} size="sm">
{item.period}
</Text>
<Progress value={item.percentage} flex={1} />
<Text size="sm" fw={500}>
{item.percentage}%
</Text>
</Group>
))}
</Stack>
</Card>
</Stack>
</Grid.Col>
</Grid>
</Stack>
</Box>
</Grid >
</Stack >
</Box >
);
}

View File

@@ -0,0 +1,225 @@
import { useState } from "react";
import {
Card,
Grid,
GridCol,
Group,
Text,
Title,
Stack,
useMantineColorScheme,
Badge,
List,
ThemeIcon,
Box
} from "@mantine/core";
import {
IconCamera,
IconAlertTriangle,
IconMapPin,
IconClock,
IconEye,
IconShieldLock
} from "@tabler/icons-react";
const KeamananPage = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === 'dark';
// Sample data for KPI cards
const kpiData = [
{
title: "CCTV Aktif",
value: 20,
subtitle: "Kamera Online",
icon: <IconCamera size={24} />,
color: "darmasaba-success",
},
{
title: "Laporan Keamanan",
value: 15,
subtitle: "Minggu ini",
icon: <IconAlertTriangle size={24} />,
color: "darmasaba-danger",
},
];
// Sample data for CCTV locations
const cctvLocations = [
{ id: "CCTV-01", lat: -8.5, lng: 115.2, status: "active", lastSeen: "2 jam yang lalu", location: "Balai Desa" },
{ id: "CCTV-02", lat: -8.6, lng: 115.3, status: "active", lastSeen: "1 jam yang lalu", location: "Pintu Masuk Desa" },
{ id: "CCTV-03", lat: -8.4, lng: 115.1, status: "offline", lastSeen: "1 hari yang lalu", location: "Taman Desa" },
{ id: "CCTV-04", lat: -8.7, lng: 115.4, status: "active", lastSeen: "30 menit yang lalu", location: "Pasar Desa" },
];
// Sample data for security reports
const securityReports = [
{
id: "REP-001",
title: "Pencurian Motor",
reportedAt: "2 jam yang lalu",
date: "12 Feb 2026, 14:30",
location: "Jl. Kecubung 20",
status: "baru",
},
{
id: "REP-002",
title: "Kerusuhan Antar Warga",
reportedAt: "4 jam yang lalu",
date: "12 Feb 2026, 12:15",
location: "RT 05 RW 02",
status: "baru",
},
{
id: "REP-003",
title: "Kebakaran Rumah",
reportedAt: "1 hari yang lalu",
date: "11 Feb 2026, 08:45",
location: "Jl. Flamboyan 15",
status: "diproses",
},
{
id: "REP-004",
title: "Kehilangan Barang",
reportedAt: "2 hari yang lalu",
date: "10 Feb 2026, 16:20",
location: "Taman Desa",
status: "selesai",
},
];
return (
<Stack gap="lg">
{/* Page Header */}
<Group justify="space-between" align="center">
<Title order={2} c={dark ? "dark.0" : "black"}>
Keamanan Lingkungan Desa
</Title>
</Group>
{/* KPI Cards */}
<Grid gutter="md">
{kpiData.map((kpi, index) => (
<GridCol key={index} span={{ base: 12, sm: 6, md: 6 }}>
<Card withBorder radius="md" padding="lg">
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{kpi.subtitle}
</Text>
<Group gap="xs" align="center">
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
{kpi.value}
</Text>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{kpi.title}
</Text>
</Group>
</Stack>
<ThemeIcon variant="light" color={kpi.color} size="xl" radius="xl">
{kpi.icon}
</ThemeIcon>
</Group>
</Card>
</GridCol>
))}
</Grid>
<Grid gutter="md">
{/* Peta Keamanan CCTV */}
<GridCol span={{ base: 12, lg: 6 }}>
<Card withBorder radius="md" p="lg">
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>Peta Keamanan CCTV</Title>
<Text size="sm" c={dark ? "dark.3" : "dimmed"} mb="md">Titik Lokasi CCTV</Text>
{/* Placeholder for map */}
<Box
style={{
backgroundColor: dark ? '#2d3748' : '#e2e8f0',
borderRadius: '8px',
height: '400px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Stack align="center">
<IconMapPin size={48} stroke={1.5} color={dark ? '#94a3b8' : '#64748b'} />
<Text c={dark ? "dark.3" : "dimmed"}>Peta Lokasi CCTV</Text>
<Text size="sm" c={dark ? "dark.3" : "dimmed"} ta="center">Integrasi dengan Google Maps atau Mapbox akan ditampilkan di sini</Text>
</Stack>
</Box>
{/* CCTV Locations List */}
<Stack mt="md" gap="sm">
<Title order={4} c={dark ? "dark.0" : "black"}>Daftar CCTV</Title>
{cctvLocations.map((cctv, index) => (
<Card key={index} withBorder radius="md" p="md">
<Group justify="space-between">
<Stack gap={0}>
<Group gap="xs">
<Text fw={500} c={dark ? "dark.0" : "black"}>{cctv.id}</Text>
<Badge
variant="dot"
color={cctv.status === "active" ? "green" : "gray"}
>
{cctv.status === "active" ? "Online" : "Offline"}
</Badge>
</Group>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{cctv.location}</Text>
</Stack>
<Group gap="xs">
<IconClock size={16} stroke={1.5} />
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{cctv.lastSeen}</Text>
</Group>
</Group>
</Card>
))}
</Stack>
</Card>
</GridCol>
{/* Daftar Laporan Keamanan */}
<GridCol span={{ base: 12, lg: 6 }}>
<Card withBorder radius="md" p="lg">
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>Laporan Keamanan Lingkungan</Title>
<Stack gap="sm">
{securityReports.map((report, index) => (
<Card key={index} withBorder radius="md" p="md">
<Group justify="space-between" mb="sm">
<Text fw={500} c={dark ? "dark.0" : "black"}>{report.title}</Text>
<Badge
variant="light"
color={
report.status === "baru" ? "red" :
report.status === "diproses" ? "yellow" : "green"
}
>
{report.status}
</Badge>
</Group>
<Group justify="space-between">
<Group gap="xs">
<IconMapPin size={16} stroke={1.5} />
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{report.location}</Text>
</Group>
<Group gap="xs">
<IconClock size={16} stroke={1.5} />
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{report.reportedAt}</Text>
</Group>
</Group>
<Text size="sm" c={dark ? "dark.3" : "dimmed"} mt="sm">{report.date}</Text>
</Card>
))}
</Stack>
</Card>
</GridCol>
</Grid>
</Stack>
);
};
export default KeamananPage;

View File

@@ -116,10 +116,10 @@ const apbdReport = {
const KeuanganAnggaran = () => {
return (
<Box p="md">
<Box>
<Stack gap="xl">
<Group justify="space-between" align="center">
<Title order={1} fw={700}>
<Title order={2} fw={700}>
Keuangan & Anggaran
</Title>
<Button variant="filled">Export Laporan</Button>

View File

@@ -1,264 +1,342 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; // Correct import for Button
import {
Stack,
Grid,
GridCol,
Group,
Text,
Title,
ActionIcon,
Progress as MantineProgress,
Box,
Badge as MantineBadge,
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
useMantineColorScheme,
ThemeIcon,
List,
Divider,
Skeleton
} from "@mantine/core";
import { Button } from "@/components/ui/button";
import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from "recharts";
const KinerjaDivisi = () => {
// Sample data for division performance
const divisions = [
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === 'dark';
// Data for division progress chart
const divisionProgressData = [
{ name: "Sekretariat", selesai: 12, berjalan: 5, tertunda: 2 },
{ name: "Keuangan", selesai: 8, berjalan: 7, tertunda: 1 },
{ name: "Sosial", selesai: 10, berjalan: 3, tertunda: 4 },
{ name: "Humas", selesai: 6, berjalan: 9, tertunda: 3 },
];
// Division task summaries
const divisionTasks = [
{
id: 1,
name: "Divisi Teknologi",
target: 95,
achievement: 87,
status: "On Track",
projects: 12,
budget: "Rp 2.5M",
lastUpdate: "2 days ago",
name: "Sekretariat",
tasks: [
{ title: "Laporan Bulanan", status: "selesai" },
{ title: "Arsip Dokumen", status: "berjalan" },
{ title: "Undangan Rapat", status: "tertunda" },
]
},
{
id: 2,
name: "Divisi Keuangan",
target: 90,
achievement: 92,
status: "Above Target",
projects: 8,
budget: "Rp 1.8M",
lastUpdate: "1 day ago",
name: "Keuangan",
tasks: [
{ title: "Laporan APBDes", status: "selesai" },
{ title: "Verifikasi Dana", status: "tertunda" },
{ title: "Pengeluaran Harian", status: "berjalan" },
]
},
{
id: 3,
name: "Divisi SDM",
target: 85,
achievement: 78,
status: "Needs Attention",
projects: 6,
budget: "Rp 1.2M",
lastUpdate: "3 days ago",
name: "Sosial",
tasks: [
{ title: "Program Bantuan", status: "selesai" },
{ title: "Kegiatan Posyandu", status: "berjalan" },
{ title: "Monitoring Stunting", status: "tertunda" },
]
},
{
id: 4,
name: "Divisi Operasional",
target: 92,
achievement: 89,
status: "On Track",
projects: 15,
budget: "Rp 3.2M",
lastUpdate: "5 hours ago",
},
{
id: 5,
name: "Divisi Pemasaran",
target: 88,
achievement: 91,
status: "Above Target",
projects: 10,
budget: "Rp 2.1M",
lastUpdate: "1 day ago",
name: "Humas",
tasks: [
{ title: "Publikasi Kegiatan", status: "selesai" },
{ title: "Koordinasi Media", status: "berjalan" },
{ title: "Laporan Kegiatan", status: "tertunda" },
]
},
];
// Archive items
const archiveItems = [
{ name: "Surat Keputusan", count: 12 },
{ name: "Laporan Keuangan", count: 8 },
{ name: "Dokumentasi", count: 24 },
{ name: "Notulensi Rapat", count: 15 },
];
// Activity progress
const activityProgress = [
{ name: "Pembangunan Jalan", progress: 75, date: "15 Feb 2026", status: "berjalan" },
{ name: "Posyandu Bulanan", progress: 100, date: "10 Feb 2026", status: "selesai" },
{ name: "Vaksinasi Massal", progress: 45, date: "20 Feb 2026", status: "berjalan" },
{ name: "Festival Budaya", progress: 20, date: "5 Mar 2026", status: "berjalan" },
];
// Document statistics
const documentStats = [
{ name: "Gambar", value: 42 },
{ name: "Dokumen", value: 87 },
];
// Activity progress statistics
const activityProgressStats = [
{ name: "Selesai", value: 12 },
{ name: "Dikerjakan", value: 8 },
{ name: "Segera Dikerjakan", value: 5 },
{ name: "Dibatalkan", value: 2 },
];
const COLORS = ['#10B981', '#F59E0B', '#EF4444', '#6B7280'];
const STATUS_COLORS: Record<string, string> = {
selesai: 'green',
berjalan: 'blue',
tertunda: 'red',
proses: 'yellow'
};
// Discussion data
const discussions = [
{ title: "Pembahasan APBDes 2026", sender: "Kepala Desa", timestamp: "2 jam yang lalu" },
{ title: "Kegiatan Posyandu", sender: "Divisi Sosial", timestamp: "5 jam yang lalu" },
{ title: "Festival Budaya", sender: "Divisi Humas", timestamp: "1 hari yang lalu" },
];
// Today's agenda
const todayAgenda = [
{ time: "09:00", event: "Rapat Evaluasi Bulanan" },
{ time: "14:00", event: "Koordinasi Program Bantuan" },
];
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Kinerja Divisi
</h1>
<div className="flex space-x-4">
<Button variant="default">Export Data</Button>
<Button variant="outline">Filter</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium dark:text-gray-100">Total Divisi</CardTitle>
<div className="h-6 w-6 text-muted-foreground">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="h-6 w-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
/>
</svg>
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">5</div>
<p className="text-xs text-muted-foreground">Jumlah divisi aktif</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium dark:text-gray-100">
Rata-rata Pencapaian
</CardTitle>
<div className="h-6 w-6 text-muted-foreground">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="h-6 w-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg>
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">87.4%</div>
<p className="text-xs text-muted-foreground">Target tercapai</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium dark:text-gray-100">
Divisi Melebihi Target
</CardTitle>
<div className="h-6 w-6 text-muted-foreground">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="h-6 w-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
/>
</svg>
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">2</div>
<p className="text-xs text-muted-foreground">Dari total 5 divisi</p>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle className="dark:text-gray-100">Detail Kinerja Divisi</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead className="dark:text-white">Nama Divisi</TableHead>
<TableHead className="dark:text-white">Target (%)</TableHead>
<TableHead className="dark:text-white">Pencapaian (%)</TableHead>
<TableHead className="dark:text-white">Status</TableHead>
<TableHead className="dark:text-white">Proyek Aktif</TableHead>
<TableHead className="dark:text-white">Anggaran</TableHead>
<TableHead className="dark:text-white">Terakhir Diperbarui</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{divisions.map((division) => (
<TableRow key={division.id}>
<TableCell className="font-medium dark:text-white">
{division.name}
</TableCell>
<TableCell className="dark:text-white">
{division.target}%
</TableCell>
<TableCell className="dark:text-white">
{division.achievement}%
</TableCell>
<TableCell>
<div className="flex items-center">
<Progress
value={division.achievement}
max={100}
className="w-24 mr-2"
/>
<Badge
variant={
division.status === "Above Target"
? "success"
: division.status === "On Track"
? "secondary"
: "destructive"
}
>
{division.status}
</Badge>
</div>
</TableCell>
<TableCell className="dark:text-white">
{division.projects}
</TableCell>
<TableCell className="dark:text-white">
{division.budget}
</TableCell>
<TableCell className="dark:text-white">
{division.lastUpdate}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
<Stack gap="lg">
{/* Grafik Progres Tugas per Divisi */}
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} >
<Title order={4} mb="md" c={dark ? 'white' : 'darmasaba-navy'}>
Grafik Progres Tugas per Divisi
</Title>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={divisionProgressData}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={dark ? "#141D34" : "white"} />
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "var(--mantine-color-text)" : "var(--mantine-color-text)" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "var(--mantine-color-text)" : "var(--mantine-color-text)" }}
/>
<Tooltip
contentStyle={dark
? { backgroundColor: 'var(--mantine-color-dark-7)', borderColor: 'var(--mantine-color-dark-6)' }
: {}}
/>
<Bar dataKey="selesai" stackId="a" fill="#10B981" name="Selesai" radius={[4, 4, 0, 0]} />
<Bar dataKey="berjalan" stackId="a" fill="#3B82F6" name="Berjalan" radius={[4, 4, 0, 0]} />
<Bar dataKey="tertunda" stackId="a" fill="#EF4444" name="Tertunda" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="dark:text-gray-100">Grafik Pencapaian Divisi</CardTitle>
</CardHeader> <CardContent>
<div className="h-80 flex items-center justify-center bg-gray-50 dark:bg-gray-700 rounded-lg">
<p className="text-gray-500 dark:text-gray-300">
Grafik pencapaian akan ditampilkan di sini
</p>
</div>
</CardContent>
</Card>
{/* Ringkasan Tugas per Divisi */}
<Grid gutter="md">
{divisionTasks.map((division, index) => (
<GridCol key={index} span={{ base: 12, md: 6, lg: 3 }}>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
<Title order={4} mb="sm" c={dark ? 'white' : 'darmasaba-navy'}>
{division.name}
</Title>
<Stack gap="sm">
{division.tasks.map((task, taskIndex) => (
<Box key={taskIndex}>
<Group justify="space-between">
<Text size="sm" c={dark ? 'white' : 'darmasaba-navy'}>{task.title}</Text>
<MantineBadge
color={STATUS_COLORS[task.status] || 'gray'}
variant="light"
>
{task.status}
</MantineBadge>
</Group>
</Box>
))}
</Stack>
</Card>
</GridCol>
))}
</Grid>
<Card>
<CardHeader>
<CardTitle className="dark:text-gray-100">Distribusi Anggaran Divisi</CardTitle>
</CardHeader> <CardContent>
<div className="h-80 flex items-center justify-center bg-gray-50 dark:bg-gray-700 rounded-lg">
<p className="text-gray-500 dark:text-gray-300">
Diagram distribusi anggaran akan ditampilkan di sini
</p>
</div>
</CardContent>
</Card>
</div>
</div>
{/* Arsip Digital Perangkat Desa */}
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
<Title order={4} mb="md" c={dark ? 'white' : 'darmasaba-navy'}>
Arsip Digital Perangkat Desa
</Title>
<Grid gutter="md">
{archiveItems.map((item, index) => (
<GridCol key={index} span={{ base: 12, md: 6, lg: 3 }}>
<Card p="md" radius="md" withBorder bg={dark ? "#263852ff" : "#F1F5F9"} style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}>
<Group justify="space-between">
<Text c={dark ? 'white' : 'darmasaba-navy'} fw={500}>{item.name}</Text>
<Text c={dark ? 'white' : 'darmasaba-navy'} fw={700}>{item.count}</Text>
</Group>
</Card>
</GridCol>
))}
</Grid>
</Card>
{/* Kartu Progres Kegiatan */}
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
<Title order={4} mb="md" c={dark ? 'white' : 'darmasaba-navy'}>
Progres Kegiatan / Program
</Title>
<Stack gap="md">
{activityProgress.map((activity, index) => (
<Card key={index} p="md" radius="md" withBorder bg={dark ? "#263852ff" : "#F1F5F9"} style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}>
<Group justify="space-between" mb="sm">
<Text c={dark ? 'white' : 'darmasaba-navy'} fw={500}>{activity.name}</Text>
<MantineBadge
color={STATUS_COLORS[activity.status] || 'gray'}
variant="light"
>
{activity.status}
</MantineBadge>
</Group>
<Group justify="space-between">
<MantineProgress
value={activity.progress}
size="sm"
radius="xl"
color={activity.progress === 100 ? "green" : "blue"}
w="calc(100% - 80px)"
/>
<Text size="sm" c={dark ? 'white' : 'darmasaba-navy'}>{activity.progress}%</Text>
</Group>
<Text size="sm" c="dimmed" mt="sm">{activity.date}</Text>
</Card>
))}
</Stack>
</Card>
{/* Statistik Dokumen & Progres Kegiatan */}
<Grid gutter="md">
<GridCol span={{ base: 12, lg: 6 }}>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
<Title order={4} mb="md" c={dark ? 'white' : 'darmasaba-navy'}>
Jumlah Dokumen
</Title>
<ResponsiveContainer width="100%" height={200}>
<BarChart data={documentStats}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={dark ? "#141D34" : "white"} />
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "var(--mantine-color-text)" : "var(--mantine-color-text)" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "var(--mantine-color-text)" : "var(--mantine-color-text)" }}
/>
<Tooltip
contentStyle={dark
? { backgroundColor: 'var(--mantine-color-dark-7)', borderColor: 'var(--mantine-color-dark-6)' }
: {}}
/>
<Bar dataKey="value" fill={dark ? "var(--mantine-color-blue-6)" : "var(--mantine-color-blue-filled)"} radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</Card>
</GridCol>
<GridCol span={{ base: 12, lg: 6 }}>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
<Title order={4} mb="md" c={dark ? 'white' : 'darmasaba-navy'}>
Progres Kegiatan
</Title>
<ResponsiveContainer width="100%" height={200}>
<PieChart>
<Pie
data={activityProgressStats}
cx="50%"
cy="50%"
labelLine={false}
outerRadius={80}
fill="#8884d8"
dataKey="value"
label={({ name, percent }) => `${name}: ${percent ? (percent * 100).toFixed(0) : '0'}%`}
>
{activityProgressStats.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip
contentStyle={dark
? { backgroundColor: 'var(--mantine-color-dark-7)', borderColor: 'var(--mantine-color-dark-6)' }
: {}}
/>
</PieChart>
</ResponsiveContainer>
</Card>
</GridCol>
</Grid>
{/* Diskusi Internal */}
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
<Title order={4} mb="md" c={dark ? 'white' : 'darmasaba-navy'}>
Diskusi Internal
</Title>
<Stack gap="sm">
{discussions.map((discussion, index) => (
<Card key={index} p="md" radius="md" withBorder bg={dark ? "#263852ff" : "#F1F5F9"} style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}>
<Group justify="space-between">
<Text c={dark ? 'white' : 'darmasaba-navy'} fw={500}>{discussion.title}</Text>
<Text size="sm" c="dimmed">{discussion.timestamp}</Text>
</Group>
<Text size="sm" c="dimmed">{discussion.sender}</Text>
</Card>
))}
</Stack>
</Card>
{/* Agenda / Acara Hari Ini */}
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
<Title order={4} mb="md" c={dark ? 'white' : 'darmasaba-navy'}>
Agenda / Acara Hari Ini
</Title>
{todayAgenda.length > 0 ? (
<Stack gap="sm">
{todayAgenda.map((agenda, index) => (
<Group key={index} align="flex-start">
<Box w={60}>
<Text c="dimmed">{agenda.time}</Text>
</Box>
<Divider orientation="vertical" mx="sm" />
<Text c={dark ? 'white' : 'darmasaba-navy'}>{agenda.event}</Text>
</Group>
))}
</Stack>
) : (
<Text c="dimmed" ta="center" py="md">
Tidak ada acara hari ini
</Text>
)}
</Card>
</Stack>
);
};

View File

@@ -1,26 +1,76 @@
import type React from "react";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Button,
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Select } from "@/components/ui/select";
import {
Grid,
GridCol,
Group,
Text,
Title,
TextInput,
Textarea,
Select,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Textarea } from "@/components/ui/textarea";
Badge,
Stack,
useMantineColorScheme,
List,
Divider,
ActionIcon,
Box
} from "@mantine/core";
import { IconMessage, IconAlertTriangle, IconClock, IconCheck, IconChevronRight } from "@tabler/icons-react";
import { Line, LineChart, Bar, BarChart, CartesianGrid, XAxis, YAxis, Tooltip, ResponsiveContainer } from "recharts";
const PengaduanLayananPublik = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === 'dark';
// Summary data
const summaryData = {
total: 42,
baru: 14,
diproses: 14,
selesai: 14
};
// Tren pengaduan data
const trenData = [
{ bulan: "Jan", jumlah: 30 },
{ bulan: "Feb", jumlah: 50 },
{ bulan: "Mar", jumlah: 42 },
{ bulan: "Apr", jumlah: 38 },
{ bulan: "Mei", jumlah: 45 },
{ bulan: "Jun", jumlah: 42 }
];
// Surat terbanyak data
const suratData = [
{ jenis: "KTP", jumlah: 24 },
{ jenis: "KK", jumlah: 18 },
{ jenis: "Domisili", jumlah: 15 },
{ jenis: "Usaha", jumlah: 12 },
{ jenis: "Lainnya", jumlah: 8 }
];
// Pengajuan terbaru data
const pengajuanTerbaru = [
{ nama: "Budi Santoso", jenis: "Ketertiban Umum", waktu: "2 jam yang lalu", status: "baru" },
{ nama: "Siti Rahayu", jenis: "Pelayanan Kesehatan", waktu: "5 jam yang lalu", status: "diproses" },
{ nama: "Ahmad Fauzi", jenis: "Infrastruktur", waktu: "1 hari yang lalu", status: "selesai" },
{ nama: "Dewi Lestari", jenis: "Administrasi", waktu: "1 hari yang lalu", status: "baru" },
{ nama: "Joko Widodo", jenis: "Keamanan", waktu: "2 hari yang lalu", status: "diproses" }
];
// Ide inovatif data
const ideInovatif = [
{ nama: "Andi Prasetyo", judul: "Penerapan Smart Village", kategori: "Teknologi" },
{ nama: "Rina Kusuma", judul: "Program Ekowisata Desa", kategori: "Ekonomi" },
{ nama: "Bambang Suryono", judul: "Peningkatan Sanitasi", kategori: "Kesehatan" },
{ nama: "Lina Marlina", judul: "Pusat Kreatif Anak Muda", kategori: "Pendidikan" }
];
const [activeTab, setActiveTab] = useState<"complaints" | "services">(
"complaints",
);
@@ -133,279 +183,462 @@ const PengaduanLayananPublik = () => {
setNewComplaint({ title: "", category: "", description: "" });
};
// Render complaint table rows
const complaintRows = complaints.map((complaint) => (
<Table.Tr key={complaint.id}>
<Table.Td className="font-medium">
<Text c={dark ? "white" : "dark.3"}>{complaint.title}</Text>
</Table.Td>
<Table.Td>
<Text c={dark ? "white" : "dark.3"}>{complaint.category}</Text>
</Table.Td>
<Table.Td>
<Badge
variant="filled"
color={
complaint.status === "Resolved"
? "green"
: complaint.status === "In Progress"
? "yellow"
: "red"
}
>
{complaint.status}
</Badge>
</Table.Td>
<Table.Td>
<Badge
variant="filled"
color={
complaint.priority === "High"
? "red"
: complaint.priority === "Medium"
? "yellow"
: "blue"
}
>
{complaint.priority}
</Badge>
</Table.Td>
<Table.Td>
<Text c={dark ? "white" : "dark.3"}>{complaint.date}</Text>
</Table.Td>
</Table.Tr>
));
// Status badge color mapping
const getStatusColor = (status: string) => {
switch (status) {
case 'baru': return 'red';
case 'diproses': return 'yellow';
case 'selesai': return 'green';
default: return 'gray';
}
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Pengaduan & Layanan Publik
</h1>
<div className="flex space-x-4">
<Button
variant={activeTab === "complaints" ? "default" : "outline"}
onClick={() => setActiveTab("complaints")}
className="dark:bg-slate-700 dark:hover:bg-slate-600 dark:text-white"
>
Pengaduan
</Button>
<Button
variant={activeTab === "services" ? "default" : "outline"}
onClick={() => setActiveTab("services")}
className="dark:bg-slate-700 dark:hover:bg-slate-600 dark:text-white"
>
Layanan Publik
</Button>
</div>
</div>
<Stack gap="lg">
{activeTab === "complaints" ? (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Complaint Submission Form */}
<div className="lg:col-span-1">
<Card className="dark:bg-gray-800 dark:border-gray-700">
<CardHeader>
<CardTitle className="dark:text-white">
Ajukan Pengaduan
</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmitComplaint} className="space-y-4">
<div>
<label
htmlFor="title"
className="block text-sm font-medium mb-1 dark:text-gray-300"
>
Judul Pengaduan
</label>
<Input
id="title"
name="title"
value={newComplaint.title}
onChange={handleInputChange}
placeholder="Masukkan judul pengaduan"
className="dark:bg-gray-700 dark:border-gray-600 dark:text-white"
required
/>
</div>
<div>
<label
htmlFor="category"
className="block text-sm font-medium mb-1 dark:text-gray-300"
>
Kategori
</label>
<Select
id="category"
name="category"
value={newComplaint.category}
onChange={handleSelectChange}
placeholder="Pilih kategori"
data={[
{ value: "infrastruktur", label: "Infrastruktur" },
{ value: "administrasi", label: "Administrasi" },
{ value: "utilitas", label: "Utilitas" },
{ value: "sanitasi", label: "Sanitasi" },
{ value: "kesehatan", label: "Kesehatan" },
{ value: "pendidikan", label: "Pendidikan" },
]}
className="dark:bg-gray-700 dark:border-gray-600 dark:text-white"
clearable
/>
</div>
<div>
<label
htmlFor="description"
className="block text-sm font-medium mb-1 dark:text-gray-300"
>
Deskripsi
</label>
<Textarea
id="description"
name="description"
value={newComplaint.description}
onChange={handleInputChange}
placeholder="Jelaskan pengaduan Anda secara detail..."
rows={4}
className="dark:bg-gray-700 dark:border-gray-600 dark:text-white"
required
/>
</div>
<Button
type="submit"
className="w-full dark:bg-blue-600 dark:hover:bg-blue-700"
<>
{/* Summary Cards */}
<Grid gutter="md">
<GridCol span={{ base: 12, md: 6, lg: 3 }}>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
Total Pengaduan
</Text>
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
{summaryData.total}
</Text>
</Stack>
<Badge
variant="light"
color="darmasaba-blue"
p={8}
radius="md"
>
Kirim Pengaduan
</Button>
</form>
</CardContent>
</Card>
</div>
<IconMessage size={20} />
</Badge>
</Group>
</Card>
</GridCol>
{/* Complaints List */}
<div className="lg:col-span-2">
<Card className="dark:bg-gray-800 dark:border-gray-700">
<CardHeader>
<CardTitle className="dark:text-white">
Daftar Pengaduan
</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead className="dark:text-gray-300">
Judul
</TableHead>
<TableHead className="dark:text-gray-300">
Kategori
</TableHead>
<TableHead className="dark:text-gray-300">
Status
</TableHead>
<TableHead className="dark:text-gray-300">
Prioritas
</TableHead>
<TableHead className="dark:text-gray-300">
Tanggal
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{complaints.map((complaint) => (
<TableRow key={complaint.id}>
<TableCell className="font-medium dark:text-white">
{complaint.title}
</TableCell>
<TableCell className="dark:text-gray-300">
{complaint.category}
</TableCell>
<TableCell>
<Badge
variant={
complaint.status === "Resolved"
? "success"
: complaint.status === "In Progress"
? "secondary"
: "destructive"
}
>
{complaint.status}
</Badge>
</TableCell>
<TableCell>
<Badge
variant={
complaint.priority === "High"
? "destructive"
: complaint.priority === "Medium"
? "secondary"
: "default"
}
>
{complaint.priority}
</Badge>
</TableCell>
<TableCell className="dark:text-gray-300">
{complaint.date}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
</div>
<GridCol span={{ base: 12, md: 6, lg: 3 }}>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
Baru
</Text>
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
{summaryData.baru}
</Text>
</Stack>
<Badge
variant="light"
color="red"
p={8}
radius="md"
>
<IconAlertTriangle size={20} />
</Badge>
</Group>
</Card>
</GridCol>
<GridCol span={{ base: 12, md: 6, lg: 3 }}>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
Diproses
</Text>
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
{summaryData.diproses}
</Text>
</Stack>
<Badge
variant="light"
color="yellow"
p={8}
radius="md"
>
<IconClock size={20} />
</Badge>
</Group>
</Card>
</GridCol>
<GridCol span={{ base: 12, md: 6, lg: 3 }}>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
Selesai
</Text>
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
{summaryData.selesai}
</Text>
</Stack>
<Badge
variant="light"
color="green"
p={8}
radius="md"
>
<IconCheck size={20} />
</Badge>
</Group>
</Card>
</GridCol>
</Grid>
{/* Grafik Tren Pengaduan */}
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} >
<Title order={4} mb="md" c={dark ? "dark.0" : "black"}>
Grafik Tren Pengaduan
</Title>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={trenData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "var(--mantine-color-gray-7)" : "var(--mantine-color-gray-3)"}
/>
<XAxis
dataKey="bulan"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "var(--mantine-color-text)" : "var(--mantine-color-text)" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "var(--mantine-color-text)" : "var(--mantine-color-text)" }}
/>
<Tooltip
contentStyle={dark
? { backgroundColor: 'var(--mantine-color-dark-7)', borderColor: 'var(--mantine-color-dark-6)' }
: {}}
/>
<Line
type="monotone"
dataKey="jumlah"
stroke={dark ? "var(--mantine-color-blue-6)" : "var(--mantine-color-blue-filled)"}
strokeWidth={2}
dot={{ stroke: dark ? "var(--mantine-color-blue-6)" : "var(--mantine-color-blue-filled)", strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: '#fff', strokeWidth: 2 }}
/>
</LineChart>
</ResponsiveContainer>
</Card>
{/* Surat Terbanyak & Pengajuan Terbaru & Ide Inovatif */}
<Grid gutter="md">
{/* Surat Terbanyak */}
<GridCol span={{ base: 12, lg: 4 }}>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
<Title order={4} mb="md" c={dark ? "dark.0" : "black"}>
Surat Terbanyak
</Title>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={suratData} layout="horizontal">
<CartesianGrid
strokeDasharray="3 3"
horizontal={false}
stroke={dark ? "var(--mantine-color-gray-7)" : "var(--mantine-color-gray-3)"}
/>
<XAxis
dataKey="jumlah"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "var(--mantine-color-text)" : "var(--mantine-color-text)" }}
/>
<YAxis
dataKey="jenis"
type="category"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "var(--mantine-color-text)" : "var(--mantine-color-text)" }}
width={80}
/>
<Tooltip
contentStyle={dark
? { backgroundColor: 'var(--mantine-color-dark-7)', borderColor: 'var(--mantine-color-dark-6)' }
: {}}
/>
<Bar
dataKey="jumlah"
fill={dark ? "var(--mantine-color-blue-6)" : "var(--mantine-color-blue-filled)"}
radius={[0, 4, 4, 0]}
/>
</BarChart>
</ResponsiveContainer>
</Card>
</GridCol>
{/* Pengajuan Terbaru */}
<GridCol span={{ base: 12, lg: 4 }}>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
<Title order={4} mb="md" c={dark ? "dark.0" : "black"}>
Pengajuan Terbaru
</Title>
{pengajuanTerbaru.map((item, index) => (
<Box key={index}>
<Group justify="space-between">
<Stack gap={0}>
<Text fw={500} c={dark ? "dark.0" : "black"}>{item.nama}</Text>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{item.jenis}</Text>
</Stack>
<Stack gap={0} align="flex-end">
<Badge color={getStatusColor(item.status)} variant="light">
{item.status}
</Badge>
<Text size="xs" c={dark ? "dark.4" : "dimmed"}>{item.waktu}</Text>
</Stack>
</Group>
<Divider my="sm" />
</Box>
))}
</Card>
</GridCol>
{/* Ajuan Ide Inovatif */}
<GridCol span={{ base: 12, lg: 4 }}>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
<Title order={4} mb="md" c={dark ? "dark.0" : "black"}>
Ajuan Ide Inovatif
</Title>
{ideInovatif.map((item, index) => (
<Box key={index}>
<Group justify="space-between">
<Stack gap={0}>
<Text fw={500} c={dark ? "dark.0" : "black"}>{item.judul}</Text>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{item.nama}</Text>
</Stack>
<Group>
<Badge color="blue" variant="light">
{item.kategori}
</Badge>
<ActionIcon variant="subtle" color="darmasaba-blue">
<IconChevronRight size={16} />
</ActionIcon>
</Group>
</Group>
<Divider my="sm" />
</Box>
))}
</Card>
</GridCol>
</Grid>
{/* Complaint Submission Form and List */}
<Grid gutter="md">
{/* Complaint Submission Form */}
<GridCol span={{ base: 12, lg: 4 }}>
<Card p="md" withBorder radius="md" h="100%" bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
<Card.Section withBorder inheritPadding py="xs">
<Title order={3} py="xs">Ajukan Pengaduan</Title>
</Card.Section>
<Card.Section>
<form onSubmit={handleSubmitComplaint}>
<Stack gap="md" p={"sm"}>
<TextInput
label="Judul Pengaduan"
id="title"
name="title"
value={newComplaint.title}
onChange={handleInputChange}
placeholder="Masukkan judul pengaduan"
required
withAsterisk
/>
<Select
label="Kategori"
id="category"
name="category"
value={newComplaint.category}
onChange={handleSelectChange}
placeholder="Pilih kategori"
data={[
{ value: "infrastruktur", label: "Infrastruktur" },
{ value: "administrasi", label: "Administrasi" },
{ value: "utilitas", label: "Utilitas" },
{ value: "sanitasi", label: "Sanitasi" },
{ value: "kesehatan", label: "Kesehatan" },
{ value: "pendidikan", label: "Pendidikan" },
]}
clearable
/>
<Textarea
label="Deskripsi"
id="description"
name="description"
value={newComplaint.description}
onChange={handleInputChange}
placeholder="Jelaskan pengaduan Anda secara detail..."
minRows={4}
required
withAsterisk
/>
<Button type="submit" mt="md" color="darmasaba-blue">
Kirim Pengaduan
</Button>
</Stack>
</form>
</Card.Section>
</Card>
</GridCol>
{/* Complaints List */}
<GridCol span={{ base: 12, lg: 8 }}>
<Card withBorder radius="md" bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
<Card.Section withBorder inheritPadding py="xs">
<Title order={3} py="xs">Daftar Pengaduan</Title>
</Card.Section>
<Card.Section py="md" px="xs">
<Table withColumnBorders>
<Table.Thead>
<Table.Tr>
<Table.Th><Text c={dark ? "white" : "dark.3" }>Judul</Text></Table.Th>
<Table.Th><Text c={dark ? "white" : "dark.3" }>Kategori</Text></Table.Th>
<Table.Th><Text c={dark ? "white" : "dark.3" }>Status</Text></Table.Th>
<Table.Th><Text c={dark ? "white" : "dark.3" }>Prioritas</Text></Table.Th>
<Table.Th><Text c={dark ? "white" : "dark.3" }>Tanggal</Text></Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{complaintRows}
</Table.Tbody>
</Table>
</Card.Section>
</Card>
</GridCol>
</Grid>
</>
) : (
<div>
<Card className="dark:bg-gray-800 dark:border-gray-700">
<CardHeader>
<CardTitle className="dark:text-white">
Layanan Publik Tersedia
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Stack gap="lg">
<Card withBorder radius="md">
<Card.Section withBorder inheritPadding py="xs">
<Title order={3} py="xs">Layanan Publik Tersedia</Title>
</Card.Section>
<Card.Section pt="md">
<Grid gutter="md">
{services.map((service) => (
<Card
key={service.id}
className="dark:bg-gray-700 dark:border-gray-600"
>
<CardHeader>
<CardTitle className="text-lg dark:text-white">
{service.name}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
<GridCol key={service.id} span={{ base: 12, md: 6, lg: 4 }}>
<Card withBorder radius="md" h="100%">
<Title order={4} mb="sm">{service.name}</Title>
<Text size="sm" c={dark ? "white" : "dark.3" } mb="md">
{service.description}
</p>
<div className="flex justify-between items-center">
</Text>
<Group justify="space-between">
<Badge
variant={
variant="filled"
color={
service.status === "Available"
? "success"
? "green"
: service.status === "Limited"
? "secondary"
: "destructive"
? "yellow"
: "red"
}
>
{service.status}
</Badge>
<span className="text-xs text-gray-500 dark:text-gray-400">
<Text size="sm" c={dark ? "white" : "dark.3" }>
{service.category}
</span>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
</Text>
</Group>
<Text size="xs" c={dark ? "white" : "dark.3" } mt="sm">
Terakhir diperbarui: {service.lastUpdated}
</p>
</CardContent>
</Card>
</Text>
</Card>
</GridCol>
))}
</div>
</CardContent>
</Grid>
</Card.Section>
</Card>
<Card className="mt-6 dark:bg-gray-800 dark:border-gray-700">
<CardHeader>
<CardTitle className="dark:text-white">
Statistik Layanan
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg">
<h3 className="text-lg font-semibold dark:text-white">
Jumlah Layanan Tersedia
</h3>
<p className="text-3xl font-bold text-blue-600 dark:text-blue-400">
12
</p>
</div>
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg">
<h3 className="text-lg font-semibold dark:text-white">
Layanan Terpopuler
</h3>
<p className="text-3xl font-bold text-green-600 dark:text-green-400">
4
</p>
</div>
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg">
<h3 className="text-lg font-semibold dark:text-white">
Permintaan Baru
</h3>
<p className="text-3xl font-bold text-purple-600 dark:text-purple-400">
23
</p>
</div>
</div>
</CardContent>
<Card withBorder radius="md">
<Card.Section withBorder inheritPadding py="xs">
<Title order={3} py="xs">Statistik Layanan</Title>
</Card.Section>
<Card.Section pt="md">
<Grid gutter="md">
<GridCol span={{ base: 12, md: 4 }}>
<Card p="md" bg={dark ? "dark.7" : "gray.0"} radius="md">
<Title order={4} mb="xs">Jumlah Layanan Tersedia</Title>
<Text size="xl" fw={700} c="darmasaba-blue">
12
</Text>
</Card>
</GridCol>
<GridCol span={{ base: 12, md: 4 }}>
<Card p="md" bg={dark ? "dark.7" : "gray.0"} radius="md">
<Title order={4} mb="xs">Layanan Terpopuler</Title>
<Text size="xl" fw={700} c="darmasaba-success">
4
</Text>
</Card>
</GridCol>
<GridCol span={{ base: 12, md: 4 }}>
<Card p="md" bg={dark ? "dark.7" : "gray.0"} radius="md">
<Title order={4} mb="xs">Permintaan Baru</Title>
<Text size="xl" fw={700} c="darmasaba-warning">
23
</Text>
</Card>
</GridCol>
</Grid>
</Card.Section>
</Card>
</div>
</Stack>
)}
</div>
</Stack>
);
};

View File

@@ -8,6 +8,7 @@ import {
Input,
NavLink as MantineNavLink,
Box,
useMantineColorScheme,
} from "@mantine/core";
interface SidebarProps {
@@ -17,6 +18,9 @@ interface SidebarProps {
export function Sidebar({ className }: SidebarProps) {
const location = useLocation();
const navigate = useNavigate();
const { colorScheme } = useMantineColorScheme();
const isActiveBg = colorScheme === 'dark' ? "#182949" : "#E6F0FF";
const isActiveBorder = colorScheme === 'dark' ? "#00398D" : "#1F41AE";
// Define menu items with their paths
const menuItems = [
@@ -75,16 +79,34 @@ export function Sidebar({ className }: SidebarProps) {
{/* Menu Items */}
<Stack gap={0} px="xs" flex={1} style={{ overflowY: "auto" }}>
{menuItems.map((item, index) => (
<MantineNavLink
key={index}
onClick={() => navigate({ to: item.path })}
label={item.name}
active={location.pathname === item.path}
variant="subtle"
color="blue"
/>
))}
{menuItems.map((item, index) => {
const isActive = location.pathname === item.path;
return (
<MantineNavLink
key={index}
onClick={() => navigate({ to: item.path })}
label={item.name}
active={isActive}
variant="subtle"
color="blue"
style={{
background: isActive ? isActiveBg : "transparent",
fontWeight: isActive ? "bold" : "normal",
borderLeft: isActive ? `4px solid ${isActiveBorder}` : "4px solid transparent",
borderRadius: "8px",
transition: "all 200ms ease",
margin: "2px 0",
}}
styles={{
body: {
"&:hover": {
background: "#F1F5F9",
}
}
}}
/>
);
})}
</Stack>
</Box>
);

View File

@@ -0,0 +1,296 @@
import { useState } from "react";
import {
Card,
Grid,
GridCol,
Group,
Text,
Title,
Progress,
Stack,
useMantineColorScheme,
Badge,
List,
ThemeIcon
} from "@mantine/core";
import {
IconHeartbeat,
IconBabyCarriage,
IconStethoscope,
IconMedicalCross,
IconSchool,
IconBook,
IconCalendarEvent,
IconAward
} from "@tabler/icons-react";
const SosialPage = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === 'dark';
// Sample data for health statistics
const healthStats = {
ibuHamil: 87,
balita: 342,
alertStunting: 12,
posyanduAktif: 8,
};
// Sample data for health progress
const healthProgress = [
{ label: "Imunisasi Lengkap", value: 92, color: "green" },
{ label: "Pemeriksaan Rutin", value: 88, color: "blue" },
{ label: "Gizi Baik", value: 86, color: "teal" },
{ label: "Target Stunting", value: 14, color: "red" },
];
// Sample data for posyandu schedule
const posyanduSchedule = [
{ nama: "Posyandu Mawar", tanggal: "Senin, 15 Feb 2026", jam: "08:00 - 11:00" },
{ nama: "Posyandu Melati", tanggal: "Selasa, 16 Feb 2026", jam: "08:00 - 11:00" },
{ nama: "Posyandu Dahlia", tanggal: "Rabu, 17 Feb 2026", jam: "08:00 - 11:00" },
{ nama: "Posyandu Anggrek", tanggal: "Kamis, 18 Feb 2026", jam: "08:00 - 11:00" },
];
// Sample data for education stats
const educationStats = {
siswa: {
tk: 125,
sd: 480,
smp: 210,
sma: 150,
},
sekolah: {
jumlah: 8,
guru: 42,
}
};
// Sample data for scholarships
const scholarshipData = {
penerima: 45,
dana: "Rp 1.200.000.000",
tahunAjaran: "2025/2026",
};
// Sample data for cultural events
const culturalEvents = [
{ nama: "Hari Kesaktian Pancasila", tanggal: "1 Oktober 2025", lokasi: "Balai Desa" },
{ nama: "Festival Budaya Desa", tanggal: "20 Mei 2026", lokasi: "Lapangan Desa" },
{ nama: "Perayaan HUT Desa", tanggal: "17 Agustus 2026", lokasi: "Balai Desa" },
];
return (
<Stack gap="lg">
{/* Page Header */}
<Group justify="space-between" align="center">
<Title order={2} c={dark ? "dark.0" : "black"}>
Sosial Desa
</Title>
</Group>
{/* Health Statistics Cards */}
<Grid gutter="md">
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
<Card withBorder radius="md" padding="lg">
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
Ibu Hamil Aktif
</Text>
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
{healthStats.ibuHamil}
</Text>
</Stack>
<ThemeIcon variant="light" color="darmasaba-blue" size="xl" radius="xl">
<IconHeartbeat size={24} />
</ThemeIcon>
</Group>
</Card>
</GridCol>
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
<Card withBorder radius="md" padding="lg">
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
Balita Terdaftar
</Text>
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
{healthStats.balita}
</Text>
</Stack>
<ThemeIcon variant="light" color="darmasaba-success" size="xl" radius="xl">
<IconBabyCarriage size={24} />
</ThemeIcon>
</Group>
</Card>
</GridCol>
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
<Card withBorder radius="md" padding="lg">
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
Alert Stunting
</Text>
<Text size="xl" fw={700} c="red">
{healthStats.alertStunting}
</Text>
</Stack>
<ThemeIcon variant="light" color="red" size="xl" radius="xl">
<IconStethoscope size={24} />
</ThemeIcon>
</Group>
</Card>
</GridCol>
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
<Card withBorder radius="md" padding="lg">
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
Posyandu Aktif
</Text>
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
{healthStats.posyanduAktif}
</Text>
</Stack>
<ThemeIcon variant="light" color="darmasaba-warning" size="xl" radius="xl">
<IconMedicalCross size={24} />
</ThemeIcon>
</Group>
</Card>
</GridCol>
</Grid>
{/* Health Progress Bars */}
<Card withBorder radius="md" p="lg">
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>Statistik Kesehatan</Title>
<Stack gap="md">
{healthProgress.map((item, index) => (
<div key={index}>
<Group justify="space-between" mb={5}>
<Text size="sm" fw={500} c={dark ? "dark.0" : "black"}>
{item.label}
</Text>
<Text size="sm" fw={600} c={dark ? "dark.0" : "black"}>
{item.value}%
</Text>
</Group>
<Progress
value={item.value}
size="lg"
radius="xl"
color={item.color}
/>
</div>
))}
</Stack>
</Card>
<Grid gutter="md">
{/* Jadwal Posyandu */}
<GridCol span={{ base: 12, lg: 6 }}>
<Card withBorder radius="md" p="lg">
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>Jadwal Posyandu</Title>
<Stack gap="sm">
{posyanduSchedule.map((item, index) => (
<Card key={index} withBorder radius="md" p="md">
<Group justify="space-between">
<Stack gap={0}>
<Text fw={500} c={dark ? "dark.0" : "black"}>{item.nama}</Text>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{item.tanggal}</Text>
</Stack>
<Badge variant="light" color="darmasaba-blue">
{item.jam}
</Badge>
</Group>
</Card>
))}
</Stack>
</Card>
</GridCol>
{/* Pendidikan */}
<GridCol span={{ base: 12, lg: 6 }}>
<Card withBorder radius="md" p="lg">
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>Pendidikan</Title>
<Stack gap="md">
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "black"}>TK / PAUD</Text>
<Text fw={700} c={dark ? "dark.0" : "black"}>{educationStats.siswa.tk}</Text>
</Group>
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "black"}>SD</Text>
<Text fw={700} c={dark ? "dark.0" : "black"}>{educationStats.siswa.sd}</Text>
</Group>
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "black"}>SMP</Text>
<Text fw={700} c={dark ? "dark.0" : "black"}>{educationStats.siswa.smp}</Text>
</Group>
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "black"}>SMA</Text>
<Text fw={700} c={dark ? "dark.0" : "black"}>{educationStats.siswa.sma}</Text>
</Group>
<Card withBorder radius="md" p="md" mt="md">
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "black"}>Jumlah Lembaga Pendidikan</Text>
<Text fw={700} c={dark ? "dark.0" : "black"}>{educationStats.sekolah.jumlah}</Text>
</Group>
<Group justify="space-between" mt="sm">
<Text fw={500} c={dark ? "dark.0" : "black"}>Jumlah Tenaga Pengajar</Text>
<Text fw={700} c={dark ? "dark.0" : "black"}>{educationStats.sekolah.guru}</Text>
</Group>
</Card>
</Stack>
</Card>
</GridCol>
</Grid>
<Grid gutter="md">
{/* Beasiswa Desa */}
<GridCol span={{ base: 12, lg: 6 }}>
<Card withBorder radius="md" p="lg" bg={dark ? "dark.8" : "darmasaba-blue.0"}>
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>Beasiswa Desa</Text>
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>Penerima: {scholarshipData.penerima}</Text>
</Stack>
<ThemeIcon variant="light" color="darmasaba-success" size="xl" radius="xl">
<IconAward size={24} />
</ThemeIcon>
</Group>
<Text mt="md" c={dark ? "dark.0" : "black"}>Dana Tersalurkan: <Text span fw={700}>{scholarshipData.dana}</Text></Text>
<Text mt="sm" c={dark ? "dark.3" : "dimmed"}>Tahun Ajaran: {scholarshipData.tahunAjaran}</Text>
</Card>
</GridCol>
{/* Kalender Event Budaya */}
<GridCol span={{ base: 12, lg: 6 }}>
<Card withBorder radius="md" p="lg">
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>Kalender Event Budaya</Title>
<List spacing="sm">
{culturalEvents.map((event, index) => (
<List.Item key={index} icon={
<ThemeIcon color="darmasaba-blue" size={24} radius="xl">
<IconCalendarEvent size={12} />
</ThemeIcon>
}>
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "black"}>{event.nama}</Text>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{event.lokasi}</Text>
</Group>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{event.tanggal}</Text>
</List.Item>
))}
</List>
</Card>
</GridCol>
</Grid>
</Stack>
);
};
export default SosialPage;

View File

@@ -20,11 +20,14 @@ import { Route as DashboardIndexRouteImport } from './routes/dashboard/index'
import { Route as AdminIndexRouteImport } from './routes/admin/index'
import { Route as UsersIdRouteImport } from './routes/users/$id'
import { Route as ProfileEditRouteImport } from './routes/profile/edit'
import { Route as DashboardSosialRouteImport } from './routes/dashboard/sosial'
import { Route as DashboardPengaduanLayananPublikRouteImport } from './routes/dashboard/pengaduan-layanan-publik'
import { Route as DashboardKinerjaDivisiRouteImport } from './routes/dashboard/kinerja-divisi'
import { Route as DashboardKeuanganAnggaranRouteImport } from './routes/dashboard/keuangan-anggaran'
import { Route as DashboardKeamananRouteImport } from './routes/dashboard/keamanan'
import { Route as DashboardJennaAnalyticRouteImport } from './routes/dashboard/jenna-analytic'
import { Route as DashboardDemografiPekerjaanRouteImport } from './routes/dashboard/demografi-pekerjaan'
import { Route as DashboardBumdesRouteImport } from './routes/dashboard/bumdes'
import { Route as AdminUsersRouteImport } from './routes/admin/users'
import { Route as AdminSettingsRouteImport } from './routes/admin/settings'
import { Route as AdminApikeyRouteImport } from './routes/admin/apikey'
@@ -84,6 +87,11 @@ const ProfileEditRoute = ProfileEditRouteImport.update({
path: '/profile/edit',
getParentRoute: () => rootRouteImport,
} as any)
const DashboardSosialRoute = DashboardSosialRouteImport.update({
id: '/sosial',
path: '/sosial',
getParentRoute: () => DashboardRouteRoute,
} as any)
const DashboardPengaduanLayananPublikRoute =
DashboardPengaduanLayananPublikRouteImport.update({
id: '/pengaduan-layanan-publik',
@@ -101,6 +109,11 @@ const DashboardKeuanganAnggaranRoute =
path: '/keuangan-anggaran',
getParentRoute: () => DashboardRouteRoute,
} as any)
const DashboardKeamananRoute = DashboardKeamananRouteImport.update({
id: '/keamanan',
path: '/keamanan',
getParentRoute: () => DashboardRouteRoute,
} as any)
const DashboardJennaAnalyticRoute = DashboardJennaAnalyticRouteImport.update({
id: '/jenna-analytic',
path: '/jenna-analytic',
@@ -112,6 +125,11 @@ const DashboardDemografiPekerjaanRoute =
path: '/demografi-pekerjaan',
getParentRoute: () => DashboardRouteRoute,
} as any)
const DashboardBumdesRoute = DashboardBumdesRouteImport.update({
id: '/bumdes',
path: '/bumdes',
getParentRoute: () => DashboardRouteRoute,
} as any)
const AdminUsersRoute = AdminUsersRouteImport.update({
id: '/users',
path: '/users',
@@ -137,11 +155,14 @@ export interface FileRoutesByFullPath {
'/admin/apikey': typeof AdminApikeyRoute
'/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/dashboard/bumdes': typeof DashboardBumdesRoute
'/dashboard/demografi-pekerjaan': typeof DashboardDemografiPekerjaanRoute
'/dashboard/jenna-analytic': typeof DashboardJennaAnalyticRoute
'/dashboard/keamanan': typeof DashboardKeamananRoute
'/dashboard/keuangan-anggaran': typeof DashboardKeuanganAnggaranRoute
'/dashboard/kinerja-divisi': typeof DashboardKinerjaDivisiRoute
'/dashboard/pengaduan-layanan-publik': typeof DashboardPengaduanLayananPublikRoute
'/dashboard/sosial': typeof DashboardSosialRoute
'/profile/edit': typeof ProfileEditRoute
'/users/$id': typeof UsersIdRoute
'/admin/': typeof AdminIndexRoute
@@ -156,11 +177,14 @@ export interface FileRoutesByTo {
'/admin/apikey': typeof AdminApikeyRoute
'/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/dashboard/bumdes': typeof DashboardBumdesRoute
'/dashboard/demografi-pekerjaan': typeof DashboardDemografiPekerjaanRoute
'/dashboard/jenna-analytic': typeof DashboardJennaAnalyticRoute
'/dashboard/keamanan': typeof DashboardKeamananRoute
'/dashboard/keuangan-anggaran': typeof DashboardKeuanganAnggaranRoute
'/dashboard/kinerja-divisi': typeof DashboardKinerjaDivisiRoute
'/dashboard/pengaduan-layanan-publik': typeof DashboardPengaduanLayananPublikRoute
'/dashboard/sosial': typeof DashboardSosialRoute
'/profile/edit': typeof ProfileEditRoute
'/users/$id': typeof UsersIdRoute
'/admin': typeof AdminIndexRoute
@@ -178,11 +202,14 @@ export interface FileRoutesById {
'/admin/apikey': typeof AdminApikeyRoute
'/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/dashboard/bumdes': typeof DashboardBumdesRoute
'/dashboard/demografi-pekerjaan': typeof DashboardDemografiPekerjaanRoute
'/dashboard/jenna-analytic': typeof DashboardJennaAnalyticRoute
'/dashboard/keamanan': typeof DashboardKeamananRoute
'/dashboard/keuangan-anggaran': typeof DashboardKeuanganAnggaranRoute
'/dashboard/kinerja-divisi': typeof DashboardKinerjaDivisiRoute
'/dashboard/pengaduan-layanan-publik': typeof DashboardPengaduanLayananPublikRoute
'/dashboard/sosial': typeof DashboardSosialRoute
'/profile/edit': typeof ProfileEditRoute
'/users/$id': typeof UsersIdRoute
'/admin/': typeof AdminIndexRoute
@@ -201,11 +228,14 @@ export interface FileRouteTypes {
| '/admin/apikey'
| '/admin/settings'
| '/admin/users'
| '/dashboard/bumdes'
| '/dashboard/demografi-pekerjaan'
| '/dashboard/jenna-analytic'
| '/dashboard/keamanan'
| '/dashboard/keuangan-anggaran'
| '/dashboard/kinerja-divisi'
| '/dashboard/pengaduan-layanan-publik'
| '/dashboard/sosial'
| '/profile/edit'
| '/users/$id'
| '/admin/'
@@ -220,11 +250,14 @@ export interface FileRouteTypes {
| '/admin/apikey'
| '/admin/settings'
| '/admin/users'
| '/dashboard/bumdes'
| '/dashboard/demografi-pekerjaan'
| '/dashboard/jenna-analytic'
| '/dashboard/keamanan'
| '/dashboard/keuangan-anggaran'
| '/dashboard/kinerja-divisi'
| '/dashboard/pengaduan-layanan-publik'
| '/dashboard/sosial'
| '/profile/edit'
| '/users/$id'
| '/admin'
@@ -241,11 +274,14 @@ export interface FileRouteTypes {
| '/admin/apikey'
| '/admin/settings'
| '/admin/users'
| '/dashboard/bumdes'
| '/dashboard/demografi-pekerjaan'
| '/dashboard/jenna-analytic'
| '/dashboard/keamanan'
| '/dashboard/keuangan-anggaran'
| '/dashboard/kinerja-divisi'
| '/dashboard/pengaduan-layanan-publik'
| '/dashboard/sosial'
| '/profile/edit'
| '/users/$id'
| '/admin/'
@@ -345,6 +381,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ProfileEditRouteImport
parentRoute: typeof rootRouteImport
}
'/dashboard/sosial': {
id: '/dashboard/sosial'
path: '/sosial'
fullPath: '/dashboard/sosial'
preLoaderRoute: typeof DashboardSosialRouteImport
parentRoute: typeof DashboardRouteRoute
}
'/dashboard/pengaduan-layanan-publik': {
id: '/dashboard/pengaduan-layanan-publik'
path: '/pengaduan-layanan-publik'
@@ -366,6 +409,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DashboardKeuanganAnggaranRouteImport
parentRoute: typeof DashboardRouteRoute
}
'/dashboard/keamanan': {
id: '/dashboard/keamanan'
path: '/keamanan'
fullPath: '/dashboard/keamanan'
preLoaderRoute: typeof DashboardKeamananRouteImport
parentRoute: typeof DashboardRouteRoute
}
'/dashboard/jenna-analytic': {
id: '/dashboard/jenna-analytic'
path: '/jenna-analytic'
@@ -380,6 +430,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DashboardDemografiPekerjaanRouteImport
parentRoute: typeof DashboardRouteRoute
}
'/dashboard/bumdes': {
id: '/dashboard/bumdes'
path: '/bumdes'
fullPath: '/dashboard/bumdes'
preLoaderRoute: typeof DashboardBumdesRouteImport
parentRoute: typeof DashboardRouteRoute
}
'/admin/users': {
id: '/admin/users'
path: '/users'
@@ -423,20 +480,26 @@ const AdminRouteRouteWithChildren = AdminRouteRoute._addFileChildren(
)
interface DashboardRouteRouteChildren {
DashboardBumdesRoute: typeof DashboardBumdesRoute
DashboardDemografiPekerjaanRoute: typeof DashboardDemografiPekerjaanRoute
DashboardJennaAnalyticRoute: typeof DashboardJennaAnalyticRoute
DashboardKeamananRoute: typeof DashboardKeamananRoute
DashboardKeuanganAnggaranRoute: typeof DashboardKeuanganAnggaranRoute
DashboardKinerjaDivisiRoute: typeof DashboardKinerjaDivisiRoute
DashboardPengaduanLayananPublikRoute: typeof DashboardPengaduanLayananPublikRoute
DashboardSosialRoute: typeof DashboardSosialRoute
DashboardIndexRoute: typeof DashboardIndexRoute
}
const DashboardRouteRouteChildren: DashboardRouteRouteChildren = {
DashboardBumdesRoute: DashboardBumdesRoute,
DashboardDemografiPekerjaanRoute: DashboardDemografiPekerjaanRoute,
DashboardJennaAnalyticRoute: DashboardJennaAnalyticRoute,
DashboardKeamananRoute: DashboardKeamananRoute,
DashboardKeuanganAnggaranRoute: DashboardKeuanganAnggaranRoute,
DashboardKinerjaDivisiRoute: DashboardKinerjaDivisiRoute,
DashboardPengaduanLayananPublikRoute: DashboardPengaduanLayananPublikRoute,
DashboardSosialRoute: DashboardSosialRoute,
DashboardIndexRoute: DashboardIndexRoute,
}

View File

@@ -0,0 +1,7 @@
import BumdesPage from '@/components/bumdes-page'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard/bumdes')({
component: BumdesPage,
})

View File

@@ -0,0 +1,7 @@
import { createFileRoute } from '@tanstack/react-router'
import KeamananPage from '@/components/keamanan-page'
export const Route = createFileRoute('/dashboard/keamanan')({
component: KeamananPage,
})

View File

@@ -1,7 +1,7 @@
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { Header } from "@/components/header";
import { Sidebar } from "@/components/sidebar";
import { AppShell, Burger, Group } from "@mantine/core";
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
export const Route = createFileRoute("/dashboard")({
@@ -10,7 +10,10 @@ export const Route = createFileRoute("/dashboard")({
function RouteComponent() {
const [opened, { toggle }] = useDisclosure();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === 'dark' ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === 'dark' ? "#11192D" : "white";
const mainBgColor = colorScheme === 'dark' ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
@@ -21,18 +24,18 @@ function RouteComponent() {
}}
padding="md"
>
<AppShell.Header>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
<Header />
</Group>
</AppShell.Header>
<AppShell.Navbar p="md">
<AppShell.Navbar p="md" bg={navbarBgColor}>
<Sidebar />
</AppShell.Navbar>
<AppShell.Main>
<AppShell.Main bg={mainBgColor}>
<Outlet />
</AppShell.Main>
</AppShell>

View File

@@ -0,0 +1,8 @@
import { createFileRoute } from '@tanstack/react-router'
import SocialPage from '@/components/sosial-page'
export const Route = createFileRoute('/dashboard/sosial')({
component: SocialPage,
})