Compare commits
12 Commits
nico/17-ma
...
nico/25-ma
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c8012d277 | |||
| 687ce11a81 | |||
| 1ba4643e23 | |||
| 113dd7ba6f | |||
| 71a305cd4b | |||
| 84b96ca3be | |||
| 8159216a2c | |||
| d714c09efc | |||
| 0a97e31416 | |||
| 158a2db435 | |||
| 2d68d4dc06 | |||
| 97e6caa332 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -37,6 +37,9 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|||||||
# Dashboard-MD
|
# Dashboard-MD
|
||||||
Dashboard-MD
|
Dashboard-MD
|
||||||
|
|
||||||
|
# md
|
||||||
|
*.md
|
||||||
|
|
||||||
# Playwright artifacts
|
# Playwright artifacts
|
||||||
test-results/
|
test-results/
|
||||||
playwright-report/
|
playwright-report/
|
||||||
|
|||||||
168
Pengaduan-New.md
Normal file
168
Pengaduan-New.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
Create a modern analytics dashboard UI for a village complaint system (Pengaduan Dashboard).
|
||||||
|
|
||||||
|
Tech stack:
|
||||||
|
- React 19 + Vite (Bun runtime)
|
||||||
|
- Mantine UI (core components)
|
||||||
|
- TailwindCSS (layout & spacing only)
|
||||||
|
- Recharts (charts)
|
||||||
|
- TanStack Router
|
||||||
|
- Icons: lucide-react
|
||||||
|
- State: Valtio
|
||||||
|
- Date: dayjs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 DESIGN STYLE
|
||||||
|
|
||||||
|
- Clean, minimal, and soft dashboard
|
||||||
|
- Background: light gray (#f3f4f6)
|
||||||
|
- Card: white with subtle shadow
|
||||||
|
- Border radius: 16px–24px (rounded-2xl)
|
||||||
|
- Typography: medium contrast (not too bold)
|
||||||
|
- Primary color: navy blue (#1E3A5F)
|
||||||
|
- Accent: soft blue + neutral gray
|
||||||
|
- Icons inside circular solid background
|
||||||
|
|
||||||
|
Spacing:
|
||||||
|
- Use gap-6 consistently
|
||||||
|
- Internal padding: p-5 or p-6
|
||||||
|
- Layout must feel breathable (no clutter)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧱 LAYOUT STRUCTURE
|
||||||
|
|
||||||
|
### 🔹 TOP SECTION (4 STAT CARDS - GRID)
|
||||||
|
Grid: 4 columns (responsive → 2 / 1)
|
||||||
|
|
||||||
|
Each card contains:
|
||||||
|
- Title (small, muted)
|
||||||
|
- Big number (bold, large)
|
||||||
|
- Subtitle (small gray text)
|
||||||
|
- Right side: circular icon container
|
||||||
|
|
||||||
|
Example:
|
||||||
|
- Total Pengaduan → 42 → "Bulan ini"
|
||||||
|
- Baru → 14 → "Belum diproses"
|
||||||
|
- Diproses → 14 → "Sedang ditangani"
|
||||||
|
- Selesai → 14 → "Terselesaikan"
|
||||||
|
|
||||||
|
Use:
|
||||||
|
- Mantine Card
|
||||||
|
- Group justify="space-between"
|
||||||
|
- Icon inside circle (bg navy, icon white)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 MAIN CHART (FULL WIDTH)
|
||||||
|
Title: "Tren Pengaduan"
|
||||||
|
|
||||||
|
- Use Recharts LineChart
|
||||||
|
- Smooth line (monotone)
|
||||||
|
- Show dots on each point
|
||||||
|
- Data: Apr → Okt
|
||||||
|
- Value range: 30–60
|
||||||
|
|
||||||
|
Style:
|
||||||
|
- Minimal grid (light dashed)
|
||||||
|
- No heavy colors (use gray/blue line)
|
||||||
|
- Rounded container card
|
||||||
|
- Add small top-right icon (expand)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 BOTTOM SECTION (3 COLUMN GRID)
|
||||||
|
|
||||||
|
### 🔹 LEFT: "Surat Terbanyak"
|
||||||
|
- Horizontal bar chart (Recharts)
|
||||||
|
- Categories:
|
||||||
|
- KTP
|
||||||
|
- KK
|
||||||
|
- Domisili
|
||||||
|
- Usaha
|
||||||
|
- Lainnya
|
||||||
|
|
||||||
|
Style:
|
||||||
|
- Dark blue bars
|
||||||
|
- Rounded edges
|
||||||
|
- Clean axis
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔹 CENTER: "Pengajuan Terbaru"
|
||||||
|
List of activity cards:
|
||||||
|
|
||||||
|
Each item:
|
||||||
|
- Name (bold)
|
||||||
|
- Subtitle (jenis surat)
|
||||||
|
- Time (small text)
|
||||||
|
- Status badge (kanan)
|
||||||
|
|
||||||
|
Status:
|
||||||
|
- baru → red
|
||||||
|
- proses → blue
|
||||||
|
- selesai → green
|
||||||
|
|
||||||
|
Style:
|
||||||
|
- Card per item
|
||||||
|
- Soft border
|
||||||
|
- Rounded
|
||||||
|
- Compact spacing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔹 RIGHT: "Ajuan Ide Inovatif"
|
||||||
|
List mirip dengan pengajuan terbaru:
|
||||||
|
|
||||||
|
Each item:
|
||||||
|
- Nama
|
||||||
|
- Judul ide
|
||||||
|
- Waktu
|
||||||
|
- Button kecil "Detail"
|
||||||
|
|
||||||
|
Style:
|
||||||
|
- Right-aligned action button
|
||||||
|
- Light border
|
||||||
|
- Clean spacing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ COMPONENT STRUCTURE
|
||||||
|
|
||||||
|
components/
|
||||||
|
- StatCard.tsx
|
||||||
|
- LineChartCard.tsx
|
||||||
|
- BarChartCard.tsx
|
||||||
|
- ActivityList.tsx
|
||||||
|
- IdeaList.tsx
|
||||||
|
|
||||||
|
routes/
|
||||||
|
- dashboard.tsx
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ INTERACTIONS (IMPORTANT)
|
||||||
|
|
||||||
|
- Hover card → scale(1.02)
|
||||||
|
- Transition: 150ms ease
|
||||||
|
- Icon circle slightly pop on hover
|
||||||
|
- List item hover → subtle bg change
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 UX DETAILS
|
||||||
|
|
||||||
|
- Numbers must be visually dominant
|
||||||
|
- Icons must balance layout (not too big)
|
||||||
|
- Avoid heavy borders
|
||||||
|
- Keep everything aligned perfectly
|
||||||
|
- No clutter
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 OUTPUT
|
||||||
|
|
||||||
|
- Modular React components (NOT one file)
|
||||||
|
- Clean code (production-ready)
|
||||||
|
- Use Mantine properly (no hacky inline styles unless needed)
|
||||||
|
- Use Tailwind only for layout/grid/spacing
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
Buat halaman dashboard admin modern untuk sistem pemerintahan desa bernama **Darmasaba Dashboard NOC**.
|
|
||||||
|
|
||||||
Gunakan stack berikut:
|
|
||||||
|
|
||||||
Frontend:
|
|
||||||
|
|
||||||
* React 19
|
|
||||||
* Bun runtime
|
|
||||||
* Vite
|
|
||||||
* TailwindCSS
|
|
||||||
* Mantine UI
|
|
||||||
* Mantine Charts atau Recharts
|
|
||||||
* Tabler Icons
|
|
||||||
* TanStack Router
|
|
||||||
* Dayjs
|
|
||||||
|
|
||||||
UI harus modular dengan reusable components.
|
|
||||||
|
|
||||||
Gunakan **TailwindCSS sebagai styling utama** dengan warna dari konfigurasi berikut:
|
|
||||||
|
|
||||||
Primary:
|
|
||||||
darmasaba-navy (#1E3A5F)
|
|
||||||
|
|
||||||
Secondary:
|
|
||||||
darmasaba-blue (#3B82F6)
|
|
||||||
|
|
||||||
Success:
|
|
||||||
#22C55E
|
|
||||||
|
|
||||||
Warning:
|
|
||||||
#FACC15
|
|
||||||
|
|
||||||
Danger:
|
|
||||||
#EF4444
|
|
||||||
|
|
||||||
Background:
|
|
||||||
#F5F8FB
|
|
||||||
|
|
||||||
Dashboard harus memiliki **Light Mode dan Dark Mode**.
|
|
||||||
|
|
||||||
Dark Mode Color Rules:
|
|
||||||
background: #0F172A
|
|
||||||
card: #1E293B
|
|
||||||
border: #334155
|
|
||||||
text: #E2E8F0
|
|
||||||
|
|
||||||
Card style:
|
|
||||||
|
|
||||||
* rounded-xl
|
|
||||||
* soft shadow
|
|
||||||
* padding besar
|
|
||||||
* border subtle
|
|
||||||
* smooth hover animation
|
|
||||||
|
|
||||||
Gunakan grid layout responsive.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
SECTION 1 — PROGRAM KEGIATAN
|
|
||||||
|
|
||||||
Buat 4 card horizontal di bagian atas yang menampilkan kegiatan desa.
|
|
||||||
|
|
||||||
Setiap card memiliki:
|
|
||||||
|
|
||||||
* header biru
|
|
||||||
* progress bar kegiatan
|
|
||||||
* tanggal kegiatan
|
|
||||||
* badge status
|
|
||||||
|
|
||||||
Data card:
|
|
||||||
|
|
||||||
1.
|
|
||||||
|
|
||||||
Judul: Rakor 2025
|
|
||||||
Tanggal: 3 Juli 2025
|
|
||||||
Progress: 90%
|
|
||||||
Status: selesai
|
|
||||||
|
|
||||||
2.
|
|
||||||
|
|
||||||
Judul: Pemutakhiran Indeks Desa
|
|
||||||
Tanggal: 3 Juli 2025
|
|
||||||
Progress: 85%
|
|
||||||
Status: selesai
|
|
||||||
|
|
||||||
3.
|
|
||||||
|
|
||||||
Judul: Mengurus Akta Cerai Warga
|
|
||||||
Tanggal: 3 Juli 2025
|
|
||||||
Progress: 80%
|
|
||||||
Status: selesai
|
|
||||||
|
|
||||||
4.
|
|
||||||
|
|
||||||
Judul: Pasek 7 Desa Adat
|
|
||||||
Tanggal: 3 Juli 2025
|
|
||||||
Progress: 92%
|
|
||||||
Status: selesai
|
|
||||||
|
|
||||||
Progress bar:
|
|
||||||
|
|
||||||
* rounded
|
|
||||||
* warna warning
|
|
||||||
* animasi smooth
|
|
||||||
|
|
||||||
Status badge:
|
|
||||||
|
|
||||||
* success color
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
SECTION 2 — GRID DASHBOARD
|
|
||||||
|
|
||||||
Layout:
|
|
||||||
|
|
||||||
3 column grid.
|
|
||||||
|
|
||||||
Left column (sidebar style):
|
|
||||||
Divisi Teraktif
|
|
||||||
|
|
||||||
List item card dengan arrow icon.
|
|
||||||
|
|
||||||
Data:
|
|
||||||
|
|
||||||
Kesejahteraan — 37 kegiatan
|
|
||||||
Pemerintahan — 26 kegiatan
|
|
||||||
Keuangan — 17 kegiatan
|
|
||||||
Sekretaris Desa — 15 kegiatan
|
|
||||||
Tata Usaha TK — 14 kegiatan
|
|
||||||
Perangkat Kewilayahan — 12 kegiatan
|
|
||||||
Pelayanan — 10 kegiatan
|
|
||||||
Perencanaan — 9 kegiatan
|
|
||||||
Tata Usaha & Umum — 7 kegiatan
|
|
||||||
|
|
||||||
Setiap item:
|
|
||||||
|
|
||||||
* rounded
|
|
||||||
* hover effect
|
|
||||||
* arrow icon kanan
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Middle column:
|
|
||||||
|
|
||||||
Jumlah Dokumen
|
|
||||||
|
|
||||||
Gunakan **Bar Chart**.
|
|
||||||
|
|
||||||
Kategori:
|
|
||||||
|
|
||||||
* Gambar
|
|
||||||
* Dokumen
|
|
||||||
|
|
||||||
Nilai:
|
|
||||||
|
|
||||||
* Gambar: 300
|
|
||||||
* Dokumen: 310
|
|
||||||
|
|
||||||
Gunakan:
|
|
||||||
Recharts atau Mantine Charts.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Right column:
|
|
||||||
|
|
||||||
Progres Kegiatan
|
|
||||||
|
|
||||||
Gunakan **Pie Chart**.
|
|
||||||
|
|
||||||
Data:
|
|
||||||
|
|
||||||
Selesai — 83.33%
|
|
||||||
Dikerjakan — 16.67%
|
|
||||||
Segera Dikerjakan — 0%
|
|
||||||
Dibatalkan — 0%
|
|
||||||
|
|
||||||
Legend harus berwarna.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
SECTION 3 — DISCUSSION PANEL
|
|
||||||
|
|
||||||
Judul: Diskusi
|
|
||||||
|
|
||||||
Tampilkan list diskusi internal staf.
|
|
||||||
|
|
||||||
Item card memiliki:
|
|
||||||
|
|
||||||
* icon chat
|
|
||||||
* judul pesan
|
|
||||||
* nama pengirim
|
|
||||||
* tanggal
|
|
||||||
|
|
||||||
Contoh data:
|
|
||||||
|
|
||||||
"Kepada Pelayanan, mohon di cek..."
|
|
||||||
Pengirim: I.B Surya Prabhawa Manu
|
|
||||||
Tanggal: 12 Apr 2025
|
|
||||||
|
|
||||||
"Kepada staf perencanaan @suar..."
|
|
||||||
Pengirim: Ni Nyoman Yuliani
|
|
||||||
Tanggal: 14 Jun 2025
|
|
||||||
|
|
||||||
"ijin atau mohon kepada KBD sar..."
|
|
||||||
Pengirim: Ni Wayan Martini
|
|
||||||
Tanggal: 12 Apr 2025
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
SECTION 4 — ACARA HARI INI
|
|
||||||
|
|
||||||
Card sederhana.
|
|
||||||
|
|
||||||
Jika tidak ada acara tampilkan:
|
|
||||||
|
|
||||||
"Tidak ada acara hari ini"
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
SECTION 5 — ARSIP DIGITAL PERANGKAT DESA
|
|
||||||
|
|
||||||
Grid 2 column.
|
|
||||||
|
|
||||||
Menu arsip:
|
|
||||||
|
|
||||||
Surat Keputusan
|
|
||||||
Dokumentasi
|
|
||||||
Laporan Keuangan
|
|
||||||
Notulensi Rapat
|
|
||||||
|
|
||||||
Setiap item berupa card clickable dengan:
|
|
||||||
|
|
||||||
* icon dokumen
|
|
||||||
* border
|
|
||||||
* hover effect
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
DESIGN STYLE
|
|
||||||
|
|
||||||
Gunakan gaya:
|
|
||||||
|
|
||||||
Modern Government Dashboard
|
|
||||||
Clean UI
|
|
||||||
Soft shadow
|
|
||||||
Rounded-xl
|
|
||||||
Spacing besar
|
|
||||||
Minimalistic
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
RESPONSIVE RULES
|
|
||||||
|
|
||||||
Desktop:
|
|
||||||
12 column grid
|
|
||||||
|
|
||||||
Tablet:
|
|
||||||
6 column grid
|
|
||||||
|
|
||||||
Mobile:
|
|
||||||
single column stack
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
COMPONENT STRUCTURE
|
|
||||||
|
|
||||||
src/components/dashboard
|
|
||||||
|
|
||||||
activity-card.tsx
|
|
||||||
division-list.tsx
|
|
||||||
document-chart.tsx
|
|
||||||
progress-chart.tsx
|
|
||||||
discussion-panel.tsx
|
|
||||||
event-card.tsx
|
|
||||||
archive-card.tsx
|
|
||||||
|
|
||||||
src/pages
|
|
||||||
|
|
||||||
dashboard.tsx
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
CODE QUALITY
|
|
||||||
|
|
||||||
Gunakan:
|
|
||||||
|
|
||||||
* React hooks
|
|
||||||
* reusable components
|
|
||||||
* Mantine components jika perlu
|
|
||||||
* Tailwind utility classes
|
|
||||||
* dark mode support
|
|
||||||
* responsive layout
|
|
||||||
* clean TypeScript
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Output:
|
|
||||||
|
|
||||||
* Halaman dashboard lengkap
|
|
||||||
* Semua komponen reusable
|
|
||||||
* Chart sudah bekerja
|
|
||||||
* Layout identik dengan desain dashboard modern pemerintahan
|
|
||||||
BIN
public/light-mode.png
Normal file
BIN
public/light-mode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
BIN
public/white.png
Normal file
BIN
public/white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
@@ -1,385 +1,38 @@
|
|||||||
import {
|
import { Grid, GridCol, Stack } from "@mantine/core";
|
||||||
Badge,
|
import { HeaderToggle } from "./umkm/header-toggle";
|
||||||
Button,
|
import { ProdukUnggulan } from "./umkm/produk-unggulan";
|
||||||
Card,
|
import type { SalesData } from "./umkm/sales-table";
|
||||||
Grid,
|
import { SalesTable } from "./umkm/sales-table";
|
||||||
GridCol,
|
import { SummaryCards } from "./umkm/summary-cards";
|
||||||
Group,
|
import { TopProducts } from "./umkm/top-products";
|
||||||
Select,
|
|
||||||
Stack,
|
|
||||||
Table,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
useMantineColorScheme,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import {
|
|
||||||
IconBuildingStore,
|
|
||||||
IconCategory,
|
|
||||||
IconCurrency,
|
|
||||||
IconUsers,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
const BumdesPage = () => {
|
const BumdesPage = () => {
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const handleDetailClick = (product: SalesData) => {
|
||||||
const dark = colorScheme === "dark";
|
console.log("Detail clicked for:", product);
|
||||||
|
// TODO: Open modal or navigate to detail page
|
||||||
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 (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
{/* KPI Cards */}
|
{/* KPI Summary Cards */}
|
||||||
<Grid gutter="md">
|
<SummaryCards />
|
||||||
{kpiData.map((kpi, index) => (
|
|
||||||
<GridCol key={index} span={{ base: 12, sm: 6, md: 3 }}>
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<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 */}
|
{/* Header with Time Range Toggle */}
|
||||||
<Card
|
<HeaderToggle />
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
|
{/* Main Content - 2 Column Layout */}
|
||||||
<Grid gutter="md">
|
<Grid gutter="md">
|
||||||
{/* Produk Unggulan (Left Column) */}
|
{/* Left Panel - Produk Unggulan */}
|
||||||
<GridCol span={{ base: 12, lg: 4 }}>
|
<GridCol span={{ base: 12, lg: 4 }}>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{/* Total Penjualan, Produk Aktif, Total Transaksi */}
|
<ProdukUnggulan />
|
||||||
<Card
|
<TopProducts />
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<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
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<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>
|
</Stack>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
|
|
||||||
{/* Detail Penjualan Produk (Right Column) */}
|
{/* Right Panel - Detail Penjualan Produk */}
|
||||||
<GridCol span={{ base: 12, lg: 8 }}>
|
<GridCol span={{ base: 12, lg: 8 }}>
|
||||||
<Card
|
<SalesTable onDetailClick={handleDetailClick} />
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<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 ? "white" : "dimmed"}>Produk</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>
|
|
||||||
Penjualan Bulan Ini
|
|
||||||
</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>Bulan Lalu</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>Trend</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>Volume</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>Stok</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "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 ? "white" : "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>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export function ActivityList() {
|
|||||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
}}
|
}}
|
||||||
|
h="100%"
|
||||||
>
|
>
|
||||||
<Group gap="xs" mb="lg">
|
<Group gap="xs" mb="lg">
|
||||||
<Calendar
|
<Calendar
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export function ChartAPBDes() {
|
|||||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
}}
|
}}
|
||||||
|
h="100%"
|
||||||
>
|
>
|
||||||
<Title order={4} c={dark ? "white" : "gray.9"} mb="lg">
|
<Title order={4} c={dark ? "white" : "gray.9"} mb="lg">
|
||||||
Grafik APBDes
|
Grafik APBDes
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export function ChartSurat() {
|
|||||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
}}
|
}}
|
||||||
|
h="100%"
|
||||||
>
|
>
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb="md">
|
||||||
<Box>
|
<Box>
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export function DivisionProgress() {
|
|||||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
}}
|
}}
|
||||||
|
h="100%"
|
||||||
>
|
>
|
||||||
<Title order={4} c={dark ? "white" : "gray.9"} mb="lg">
|
<Title order={4} c={dark ? "white" : "gray.9"} mb="lg">
|
||||||
Divisi Teraktif
|
Divisi Teraktif
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export function SatisfactionChart() {
|
|||||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
}}
|
}}
|
||||||
|
h="100%"
|
||||||
>
|
>
|
||||||
<Title order={4} c={dark ? "white" : "gray.9"} mb={5}>
|
<Title order={4} c={dark ? "white" : "gray.9"} mb={5}>
|
||||||
Tingkat Kepuasan
|
Tingkat Kepuasan
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function StatCard({
|
|||||||
trend,
|
trend,
|
||||||
trendValue,
|
trendValue,
|
||||||
icon,
|
icon,
|
||||||
iconColor = "darmasaba-blue",
|
iconColor = "#1E3A5F",
|
||||||
}: StatCardProps) {
|
}: StatCardProps) {
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const dark = colorScheme === "dark";
|
const dark = colorScheme === "dark";
|
||||||
@@ -77,6 +77,7 @@ export function StatCard({
|
|||||||
size="xl"
|
size="xl"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
color={dark ? "gray" : iconColor}
|
color={dark ? "gray" : iconColor}
|
||||||
|
bg={dark ? "gray" : iconColor}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
|
|||||||
@@ -1,116 +1,73 @@
|
|||||||
import { BarChart, PieChart } from "@mantine/charts";
|
|
||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Card,
|
Card,
|
||||||
Grid,
|
Grid,
|
||||||
|
GridCol,
|
||||||
Group,
|
Group,
|
||||||
|
Progress,
|
||||||
Stack,
|
Stack,
|
||||||
Table,
|
|
||||||
Text,
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
IconArrowDown,
|
Baby,
|
||||||
IconArrowUp,
|
BarChart3,
|
||||||
IconBabyCarriage,
|
Building2,
|
||||||
IconSkull,
|
Home,
|
||||||
} from "@tabler/icons-react";
|
PieChart as PieChartIcon,
|
||||||
import React from "react";
|
TrendingDown,
|
||||||
|
Users,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
Cell,
|
||||||
|
Pie,
|
||||||
|
PieChart,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
|
|
||||||
// Sample Data
|
// KPI Data
|
||||||
const kpiData = [
|
const kpiData = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: "Total Penduduk",
|
title: "Total Penduduk",
|
||||||
value: "5.634",
|
value: "5.634",
|
||||||
sub: "Aktif terdaftar",
|
subtitle: "Aktif terdaftar",
|
||||||
icon: (
|
icon: Users,
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-6 w-6 text-muted-foreground"
|
|
||||||
role="img"
|
|
||||||
aria-label="Icon penduduk"
|
|
||||||
>
|
|
||||||
<title>Total Penduduk</title>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: "Kepala Keluarga",
|
title: "Kepala Keluarga",
|
||||||
value: "1.354",
|
value: "1.354",
|
||||||
sub: "Total KK",
|
subtitle: "Total KK",
|
||||||
icon: (
|
icon: Home,
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-6 w-6 text-muted-foreground"
|
|
||||||
role="img"
|
|
||||||
aria-label="Icon kepala keluarga"
|
|
||||||
>
|
|
||||||
<title>Kepala Keluarga</title>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: "Kelahiran",
|
title: "Kelahiran",
|
||||||
value: "23",
|
value: "23",
|
||||||
sub: "Tahun ini",
|
subtitle: "Tahun ini",
|
||||||
icon: (
|
icon: Baby,
|
||||||
<IconBabyCarriage
|
|
||||||
className="h-6 w-6 text-muted-foreground"
|
|
||||||
role="img"
|
|
||||||
aria-label="Icon kelahiran"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
title: "Kemiskinan",
|
title: "Kemiskinan",
|
||||||
value: "324",
|
value: "324",
|
||||||
delta: "-10% dari tahun lalu",
|
subtitle: "-10% dari tahun lalu",
|
||||||
deltaType: "positive",
|
trend: "positive",
|
||||||
icon: (
|
icon: TrendingDown,
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-6 w-6 text-muted-foreground"
|
|
||||||
role="img"
|
|
||||||
aria-label="Icon kemiskinan"
|
|
||||||
>
|
|
||||||
<title>Kemiskinan</title>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Age Distribution Data
|
||||||
const ageDistributionData = [
|
const ageDistributionData = [
|
||||||
{ ageRange: "17-25", total: 850 },
|
{ ageRange: "17-25", total: 850 },
|
||||||
{ ageRange: "26-35", total: 1200 },
|
{ ageRange: "26-35", total: 1200 },
|
||||||
@@ -120,6 +77,7 @@ const ageDistributionData = [
|
|||||||
{ ageRange: "65+", total: 484 },
|
{ ageRange: "65+", total: 484 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Job Distribution Data
|
||||||
const jobDistributionData = [
|
const jobDistributionData = [
|
||||||
{ job: "Sipil", total: 1200 },
|
{ job: "Sipil", total: 1200 },
|
||||||
{ job: "Guru", total: 850 },
|
{ job: "Guru", total: 850 },
|
||||||
@@ -128,283 +86,597 @@ const jobDistributionData = [
|
|||||||
{ job: "Wiraswasta", total: 984 },
|
{ job: "Wiraswasta", total: 984 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Religion Data
|
||||||
const religionData = [
|
const religionData = [
|
||||||
{ religion: "Hindu", total: 4234, color: "red" },
|
{ name: "Hindu", value: 4234, color: "#EF4444" },
|
||||||
{ religion: "Islam", total: 856, color: "blue" },
|
{ name: "Islam", value: 856, color: "#3B82F6" },
|
||||||
{ religion: "Kristen", total: 412, color: "green" },
|
{ name: "Kristen", value: 412, color: "#22C55E" },
|
||||||
{ religion: "Buddha", total: 202, color: "yellow" },
|
{ name: "Buddha", value: 202, color: "#FACC15" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Banjar Data
|
||||||
const banjarData = [
|
const banjarData = [
|
||||||
{ banjar: "Banjar Darmasaba", population: 1200, kk: 300, poor: 45 },
|
{ banjar: "Darmasaba", population: 1200, kk: 300, poor: 45 },
|
||||||
{ banjar: "Banjar Manesa", population: 950, kk: 240, poor: 32 },
|
{ banjar: "Manesa", population: 950, kk: 240, poor: 32 },
|
||||||
{ banjar: "Banjar Cabe", population: 800, kk: 200, poor: 28 },
|
{ banjar: "Cabe", population: 800, kk: 200, poor: 28 },
|
||||||
{ banjar: "Banjar Penenjoan", population: 1100, kk: 280, poor: 38 },
|
{ banjar: "Penenjoan", population: 1100, kk: 280, poor: 38 },
|
||||||
{ banjar: "Banjar Baler Pasar", population: 984, kk: 250, poor: 42 },
|
{ banjar: "Baler Pasar", population: 984, kk: 250, poor: 42 },
|
||||||
{ banjar: "Banjar Bucu", population: 600, kk: 184, poor: 25 },
|
{ banjar: "Bucu", population: 600, kk: 184, poor: 25 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Dynamic Stats Data
|
||||||
const dynamicStats = [
|
const dynamicStats = [
|
||||||
{
|
{
|
||||||
title: "Kelahiran",
|
title: "Kelahiran",
|
||||||
value: "23",
|
value: "23",
|
||||||
icon: <IconBabyCarriage size={16} />,
|
icon: Baby,
|
||||||
color: "green",
|
color: "#22C55E",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Kematian",
|
title: "Kematian",
|
||||||
value: "12",
|
value: "12",
|
||||||
icon: <IconSkull size={16} />,
|
icon: TrendingDown,
|
||||||
color: "red",
|
color: "#EF4444",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Pindah Masuk",
|
title: "Pindah Masuk",
|
||||||
value: "45",
|
value: "45",
|
||||||
icon: <IconArrowDown size={16} />,
|
icon: Users,
|
||||||
color: "blue",
|
color: "#3B82F6",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Pindah Keluar",
|
title: "Pindah Keluar",
|
||||||
value: "32",
|
value: "32",
|
||||||
icon: <IconArrowUp size={16} />,
|
icon: Users,
|
||||||
color: "orange",
|
color: "#3B82F6",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Sektor Unggulan Data
|
||||||
|
const sektorUnggulanData = [
|
||||||
|
{ sektor: "Pertanian", value: 65 },
|
||||||
|
{ sektor: "Perdagangan", value: 45 },
|
||||||
|
{ sektor: "Industri", value: 38 },
|
||||||
|
{ sektor: "Jasa", value: 52 },
|
||||||
|
];
|
||||||
|
|
||||||
const DemografiPekerjaan = () => {
|
const DemografiPekerjaan = () => {
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const dark = colorScheme === "dark";
|
const dark = colorScheme === "dark";
|
||||||
return (
|
|
||||||
<Box className="space-y-6">
|
|
||||||
<Stack gap="xl">
|
|
||||||
{/* KPI Cards */}
|
|
||||||
<Grid gutter="lg">
|
|
||||||
{kpiData.map((kpi) => (
|
|
||||||
<Grid.Col key={kpi.id} span={{ base: 12, md: 6, lg: 3 }}>
|
|
||||||
<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={dark ? "dark.3" : "dimmed"}>
|
|
||||||
{kpi.title}
|
|
||||||
</Text>
|
|
||||||
{React.cloneElement(kpi.icon, {
|
|
||||||
className: "h-6 w-6",
|
|
||||||
color: dark
|
|
||||||
? "var(--mantine-color-dark-3)"
|
|
||||||
: "var(--mantine-color-dimmed)",
|
|
||||||
})}
|
|
||||||
</Group>
|
|
||||||
<Title order={3} fw={700} c={dark ? "dark.0" : "black"} mt="xs">
|
|
||||||
{kpi.value}
|
|
||||||
</Title>
|
|
||||||
{kpi.delta && (
|
|
||||||
<Text
|
|
||||||
size="xs"
|
|
||||||
c={
|
|
||||||
kpi.deltaType === "positive"
|
|
||||||
? "green"
|
|
||||||
: kpi.deltaType === "negative"
|
|
||||||
? "red"
|
|
||||||
: dark
|
|
||||||
? "dark.3"
|
|
||||||
: "dimmed"
|
|
||||||
}
|
|
||||||
mt={4}
|
|
||||||
>
|
|
||||||
{kpi.delta}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{kpi.sub && (
|
|
||||||
<Text size="xs" c={dark ? "dark.3" : "dimmed"} mt={2}>
|
|
||||||
{kpi.sub}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Charts Section */}
|
return (
|
||||||
<Grid gutter="lg">
|
<Stack gap="lg">
|
||||||
{/* Grafik Pengelompokan Umur */}
|
{/* TOP SECTION - 4 STAT CARDS */}
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
<Grid gutter="md">
|
||||||
|
{kpiData.map((item) => (
|
||||||
|
<Grid.Col key={item.id} span={{ base: 12, sm: 6, lg: 3 }}>
|
||||||
<Card
|
<Card
|
||||||
p="md"
|
p="md"
|
||||||
radius="md"
|
radius="xl"
|
||||||
withBorder
|
withBorder
|
||||||
bg={dark ? "#141D34" : "white"}
|
bg={dark ? "#1E293B" : "white"}
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
transition: "transform 0.15s ease, box-shadow 0.15s ease",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
>
|
>
|
||||||
<Title order={3} fw={500} mb="md">
|
<Group justify="space-between" align="flex-start" w="100%">
|
||||||
Grafik Pengelompokan Umur
|
<Stack gap={2}>
|
||||||
</Title>
|
<Text size="sm" c="dimmed">
|
||||||
<BarChart
|
{item.title}
|
||||||
h={300}
|
</Text>
|
||||||
data={ageDistributionData}
|
<Text size="xl" fw={700} c={dark ? "white" : "gray.9"}>
|
||||||
dataKey="ageRange"
|
{item.value}
|
||||||
series={[{ name: "total", color: "darmasaba-navy" }]}
|
</Text>
|
||||||
withLegend
|
<Group gap={4} align="flex-start">
|
||||||
/>
|
{item.trend === "positive" && (
|
||||||
|
<TrendingDown size={14} color="#22C55E" />
|
||||||
|
)}
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
c={
|
||||||
|
item.trend === "positive"
|
||||||
|
? "green"
|
||||||
|
: dark
|
||||||
|
? "gray.4"
|
||||||
|
: "gray.5"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.subtitle}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
<ThemeIcon
|
||||||
|
color="#1E3A5F"
|
||||||
|
variant="filled"
|
||||||
|
size="lg"
|
||||||
|
radius="xl"
|
||||||
|
>
|
||||||
|
<item.icon style={{ width: "60%", height: "60%" }} />
|
||||||
|
</ThemeIcon>
|
||||||
|
</Group>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
{/* Demografi Pekerjaan */}
|
{/* ROW 2 - 3 COLUMNS */}
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
<Grid gutter="lg">
|
||||||
<Card
|
{/* LEFT: PENGELOMPOKAN UMUR */}
|
||||||
p="md"
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||||
radius="md"
|
<Card
|
||||||
withBorder
|
p="md"
|
||||||
bg={dark ? "#141D34" : "white"}
|
radius="xl"
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
withBorder
|
||||||
>
|
bg={dark ? "#1E293B" : "white"}
|
||||||
<Title order={3} fw={500} mb="md">
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<BarChart3 size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
|
Pengelompokan Umur
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<ResponsiveContainer width="100%" height={250}>
|
||||||
|
<BarChart data={ageDistributionData}>
|
||||||
|
<CartesianGrid
|
||||||
|
strokeDasharray="3 3"
|
||||||
|
vertical={false}
|
||||||
|
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="ageRange"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="total"
|
||||||
|
fill="#1E3A5F"
|
||||||
|
radius={[8, 8, 0, 0]}
|
||||||
|
maxBarSize={40}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
{/* CENTER: DEMOGRAFI PEKERJAAN */}
|
||||||
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<Building2 size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
Demografi Pekerjaan
|
Demografi Pekerjaan
|
||||||
</Title>
|
</Title>
|
||||||
<BarChart
|
</Group>
|
||||||
h={300}
|
<ResponsiveContainer width="100%" height={250}>
|
||||||
data={jobDistributionData}
|
<BarChart data={jobDistributionData} layout="vertical">
|
||||||
dataKey="job"
|
<CartesianGrid
|
||||||
series={[{ name: "total", color: "darmasaba-navy" }]}
|
strokeDasharray="3 3"
|
||||||
withLegend
|
horizontal={false}
|
||||||
/>
|
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||||
</Card>
|
/>
|
||||||
</Grid.Col>
|
<XAxis
|
||||||
</Grid>
|
type="number"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
type="category"
|
||||||
|
dataKey="job"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
width={90}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="total"
|
||||||
|
fill="#1E3A5F"
|
||||||
|
radius={[0, 8, 8, 0]}
|
||||||
|
maxBarSize={30}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
{/* Agama & Data per Banjar */}
|
{/* RIGHT: STATISTIK DINAMIKA PENDUDUK */}
|
||||||
<Grid gutter="lg">
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||||
{/* Distribusi Agama */}
|
<Card
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
p="md"
|
||||||
<Card
|
radius="xl"
|
||||||
p="md"
|
withBorder
|
||||||
radius="md"
|
bg={dark ? "#1E293B" : "white"}
|
||||||
withBorder
|
style={{
|
||||||
bg={dark ? "#141D34" : "white"}
|
borderColor: dark ? "#334155" : "white",
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
>
|
}}
|
||||||
<Title order={3} fw={500} mb="md">
|
h="100%"
|
||||||
Distribusi Agama
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<BarChart3 size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
|
Dinamika Penduduk
|
||||||
</Title>
|
</Title>
|
||||||
<PieChart
|
</Group>
|
||||||
h={300}
|
<Grid gutter="sm">
|
||||||
data={religionData.map((item) => ({
|
{dynamicStats.map((stat, index) => (
|
||||||
name: item.religion,
|
<Grid.Col key={index} span={6}>
|
||||||
value: item.total,
|
<Card
|
||||||
color: item.color,
|
p="sm"
|
||||||
}))}
|
radius="lg"
|
||||||
withLabels
|
bg={dark ? "#334155" : "#F1F5F9"}
|
||||||
withLabelsLine
|
style={{
|
||||||
labelsPosition="outside"
|
transition: "transform 0.15s ease",
|
||||||
labelsType="percent"
|
cursor: "pointer",
|
||||||
/>
|
}}
|
||||||
</Card>
|
>
|
||||||
</Grid.Col>
|
<Stack gap={2} align="center">
|
||||||
|
<ThemeIcon
|
||||||
{/* Data per Banjar */}
|
color={stat.color}
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
variant="filled"
|
||||||
<Card
|
size="md"
|
||||||
p="md"
|
radius="lg"
|
||||||
radius="md"
|
>
|
||||||
withBorder
|
<stat.icon size={14} />
|
||||||
bg={dark ? "#141D34" : "white"}
|
</ThemeIcon>
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
<Text size="xs" c="dimmed" ta="center">
|
||||||
>
|
|
||||||
<Title order={3} fw={500} c={dark ? "dark.0" : "black"} mb="md">
|
|
||||||
Data per Banjar
|
|
||||||
</Title>
|
|
||||||
<Table striped highlightOnHover>
|
|
||||||
<Table.Thead>
|
|
||||||
<Table.Tr>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "dark.0" : "black"}>Banjar</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "dark.0" : "black"}>Penduduk</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "dark.0" : "black"}>KK</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "dark.0" : "black"}>Miskin</Text>
|
|
||||||
</Table.Th>
|
|
||||||
</Table.Tr>
|
|
||||||
</Table.Thead>
|
|
||||||
<Table.Tbody>
|
|
||||||
{banjarData.map((item, index) => (
|
|
||||||
<Table.Tr key={`${item.banjar}-${index}`}>
|
|
||||||
<Table.Td>
|
|
||||||
<Text c={dark ? "dark.0" : "black"}>{item.banjar}</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Text c={dark ? "dark.0" : "black"}>
|
|
||||||
{item.population.toLocaleString()}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Text c={dark ? "dark.0" : "black"}>
|
|
||||||
{item.kk.toLocaleString()}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Text c={dark ? "red.4" : "red"}>
|
|
||||||
{item.poor.toLocaleString()}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
))}
|
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Statistik Dinamika Penduduk */}
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Title order={3} fw={500} c={dark ? "dark.0" : "black"} mb="md">
|
|
||||||
Statistik Dinamika Penduduk
|
|
||||||
</Title>
|
|
||||||
<Grid gutter="md">
|
|
||||||
{dynamicStats.map((stat, index) => (
|
|
||||||
<Grid.Col
|
|
||||||
key={`${stat.title}-${index}`}
|
|
||||||
span={{ base: 12, md: 3 }}
|
|
||||||
>
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Box>
|
|
||||||
<Text size="sm" fw={500} c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
{stat.title}
|
{stat.title}
|
||||||
</Text>
|
</Text>
|
||||||
<Title order={4} fw={700} c={stat.color}>
|
<Text
|
||||||
|
size="lg"
|
||||||
|
fw={700}
|
||||||
|
c={stat.color}
|
||||||
|
style={{ lineHeight: 1 }}
|
||||||
|
>
|
||||||
{stat.value}
|
{stat.value}
|
||||||
</Title>
|
</Text>
|
||||||
</Box>
|
</Stack>
|
||||||
<Box c={stat.color}>{stat.icon}</Box>
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* ROW 3 - 3 COLUMNS */}
|
||||||
|
<Grid gutter="lg">
|
||||||
|
{/* LEFT: DISTRIBUSI AGAMA */}
|
||||||
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<PieChartIcon size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
|
Distribusi Agama
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<ResponsiveContainer width="100%" height={250}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={religionData}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={60}
|
||||||
|
outerRadius={90}
|
||||||
|
paddingAngle={2}
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{religionData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
<Stack gap="xs" mt="md">
|
||||||
|
{religionData.map((item, index) => (
|
||||||
|
<Group key={index} justify="space-between">
|
||||||
|
<Group gap="xs">
|
||||||
|
<Box
|
||||||
|
w={10}
|
||||||
|
h={10}
|
||||||
|
style={{
|
||||||
|
backgroundColor: item.color,
|
||||||
|
borderRadius: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text size="sm" c={dark ? "white" : "gray.7"}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
<Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
|
||||||
</Grid.Col>
|
{item.value.toLocaleString()}
|
||||||
))}
|
</Text>
|
||||||
</Grid>
|
</Group>
|
||||||
</Card>
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
{/* CENTER: DATA PER BANJAR */}
|
||||||
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<Users size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
|
Data per Banjar
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<Box style={{ overflowX: "auto" }}>
|
||||||
|
<table style={{ width: "100%", borderCollapse: "collapse" }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
textAlign: "left",
|
||||||
|
padding: "8px",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: dark ? "#94A3B8" : "#64748B",
|
||||||
|
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Banjar
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
textAlign: "right",
|
||||||
|
padding: "8px",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: dark ? "#94A3B8" : "#64748B",
|
||||||
|
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Penduduk
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
textAlign: "right",
|
||||||
|
padding: "8px",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: dark ? "#94A3B8" : "#64748B",
|
||||||
|
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
KK
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
textAlign: "right",
|
||||||
|
padding: "8px",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: dark ? "#94A3B8" : "#64748B",
|
||||||
|
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Miskin
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{banjarData.map((item, index) => (
|
||||||
|
<tr
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
index % 2 === 0
|
||||||
|
? dark
|
||||||
|
? "#334155"
|
||||||
|
: "#F8FAFC"
|
||||||
|
: "transparent",
|
||||||
|
transition: "background-color 0.15s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
padding: "10px 8px",
|
||||||
|
fontSize: "13px",
|
||||||
|
fontWeight: 500,
|
||||||
|
color: dark ? "#E2E8F0" : "#1E293B",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.banjar}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
padding: "10px 8px",
|
||||||
|
textAlign: "right",
|
||||||
|
fontSize: "13px",
|
||||||
|
color: dark ? "#E2E8F0" : "#1E293B",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.population.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
padding: "10px 8px",
|
||||||
|
textAlign: "right",
|
||||||
|
fontSize: "13px",
|
||||||
|
color: dark ? "#E2E8F0" : "#1E293B",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.kk.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
padding: "10px 8px",
|
||||||
|
textAlign: "right",
|
||||||
|
fontSize: "13px",
|
||||||
|
color: "#EF4444",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.poor.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
{/* RIGHT: STATISTIK SEKTOR UNGGULAN */}
|
||||||
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<BarChart3 size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
|
Sektor Unggulan
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<ResponsiveContainer width="100%" height={250}>
|
||||||
|
<BarChart data={sektorUnggulanData} layout="vertical">
|
||||||
|
<CartesianGrid
|
||||||
|
strokeDasharray="3 3"
|
||||||
|
horizontal={false}
|
||||||
|
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
type="number"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
type="category"
|
||||||
|
dataKey="sektor"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
width={90}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="value"
|
||||||
|
fill="#1E3A5F"
|
||||||
|
radius={[0, 8, 8, 0]}
|
||||||
|
maxBarSize={40}
|
||||||
|
>
|
||||||
|
{sektorUnggulanData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill="#1E3A5F" />
|
||||||
|
))}
|
||||||
|
</Bar>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,18 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconUserShield } from "@tabler/icons-react";
|
import {
|
||||||
|
IconLayoutSidebarLeftCollapse,
|
||||||
|
IconUserShield,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
import { useLocation, useNavigate } from "@tanstack/react-router";
|
import { useLocation, useNavigate } from "@tanstack/react-router";
|
||||||
import { Bell, Moon, Sun, User as UserIcon } from "lucide-react"; // Renamed User to UserIcon to avoid conflict with Mantine's User component if it exists
|
import { Bell, Moon, Sun, User as UserIcon } from "lucide-react";
|
||||||
|
|
||||||
export function Header() {
|
interface HeaderProps {
|
||||||
|
onSidebarToggle?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Header({ onSidebarToggle }: HeaderProps) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||||
const dark = colorScheme === "dark";
|
const dark = colorScheme === "dark";
|
||||||
@@ -56,9 +63,24 @@ export function Header() {
|
|||||||
return (
|
return (
|
||||||
<Group justify="space-between" w="100%">
|
<Group justify="space-between" w="100%">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<Title order={3} c={"white"}>
|
<Group gap="md">
|
||||||
{getPageTitle()}
|
<ActionIcon
|
||||||
</Title>
|
onClick={onSidebarToggle}
|
||||||
|
variant="subtle"
|
||||||
|
size="lg"
|
||||||
|
radius="xl"
|
||||||
|
visibleFrom="sm"
|
||||||
|
aria-label="Toggle sidebar"
|
||||||
|
>
|
||||||
|
<IconLayoutSidebarLeftCollapse
|
||||||
|
color="white"
|
||||||
|
style={{ width: "70%", height: "70%" }}
|
||||||
|
/>
|
||||||
|
</ActionIcon>
|
||||||
|
{/* <Title order={3} c={"white"}>
|
||||||
|
{getPageTitle()}
|
||||||
|
</Title> */}
|
||||||
|
</Group>
|
||||||
|
|
||||||
{/* Right Section */}
|
{/* Right Section */}
|
||||||
<Group gap="md">
|
<Group gap="md">
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ const HelpPage = () => {
|
|||||||
<HelpCard
|
<HelpCard
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||||
bg={dark ? "#141D34" : "white"}
|
bg={dark ? "#141D34" : "white"}
|
||||||
icon={<IconBook size={24} />}
|
icon={<IconBook size={24} color="white" />}
|
||||||
title="Panduan Memulai"
|
title="Panduan Memulai"
|
||||||
h="100%"
|
h="100%"
|
||||||
>
|
>
|
||||||
@@ -211,7 +211,7 @@ const HelpPage = () => {
|
|||||||
<HelpCard
|
<HelpCard
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||||
bg={dark ? "#141D34" : "white"}
|
bg={dark ? "#141D34" : "white"}
|
||||||
icon={<IconVideo size={24} />}
|
icon={<IconVideo size={24} color="white" />}
|
||||||
title="Video Tutorial"
|
title="Video Tutorial"
|
||||||
h="100%"
|
h="100%"
|
||||||
>
|
>
|
||||||
@@ -241,7 +241,7 @@ const HelpPage = () => {
|
|||||||
<HelpCard
|
<HelpCard
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||||
bg={dark ? "#141D34" : "white"}
|
bg={dark ? "#141D34" : "white"}
|
||||||
icon={<IconHelpCircle size={24} />}
|
icon={<IconHelpCircle size={24} color="white" />}
|
||||||
title="FAQ"
|
title="FAQ"
|
||||||
h="100%"
|
h="100%"
|
||||||
>
|
>
|
||||||
@@ -273,7 +273,7 @@ const HelpPage = () => {
|
|||||||
<HelpCard
|
<HelpCard
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||||
bg={dark ? "#141D34" : "white"}
|
bg={dark ? "#141D34" : "white"}
|
||||||
icon={<IconHeadphones size={24} />}
|
icon={<IconHeadphones size={24} color="white" />}
|
||||||
title="Hubungi Support"
|
title="Hubungi Support"
|
||||||
h="100%"
|
h="100%"
|
||||||
>
|
>
|
||||||
@@ -308,7 +308,7 @@ const HelpPage = () => {
|
|||||||
<HelpCard
|
<HelpCard
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||||
bg={dark ? "#141D34" : "white"}
|
bg={dark ? "#141D34" : "white"}
|
||||||
icon={<IconFileText size={24} />}
|
icon={<IconFileText size={24} color="white" />}
|
||||||
title="Dokumentasi"
|
title="Dokumentasi"
|
||||||
h="100%"
|
h="100%"
|
||||||
>
|
>
|
||||||
@@ -340,7 +340,7 @@ const HelpPage = () => {
|
|||||||
<HelpCard
|
<HelpCard
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||||
bg={dark ? "#141D34" : "white"}
|
bg={dark ? "#141D34" : "white"}
|
||||||
icon={<IconMessage size={24} />}
|
icon={<IconMessage size={24} color="white" />}
|
||||||
title="Jenna - Virtual Assistant"
|
title="Jenna - Virtual Assistant"
|
||||||
h="100%"
|
h="100%"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,123 +1,79 @@
|
|||||||
import { BarChart } from "@mantine/charts";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
|
||||||
Card,
|
Card,
|
||||||
Grid,
|
Grid,
|
||||||
|
GridCol,
|
||||||
Group,
|
Group,
|
||||||
Progress,
|
Progress,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import React from "react";
|
import {
|
||||||
|
AlertTriangle,
|
||||||
|
CheckCircle,
|
||||||
|
Clock,
|
||||||
|
MessageCircle,
|
||||||
|
TrendingUp,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
|
|
||||||
// Sample Data
|
// KPI Data
|
||||||
const kpiData = [
|
const kpiData = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: "Interaksi Hari Ini",
|
title: "Interaksi Hari Ini",
|
||||||
value: "61",
|
value: "61",
|
||||||
delta: "+15% dari kemarin",
|
subtitle: "+15% dari kemarin",
|
||||||
deltaType: "positive",
|
trend: "positive",
|
||||||
icon: (
|
icon: MessageCircle,
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-6 w-6 text-muted-foreground"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M8.625 12a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H16.5m-13.5 3h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: "Jawaban Otomatis",
|
title: "Jawaban Otomatis",
|
||||||
value: "87%",
|
value: "87%",
|
||||||
sub: "53 dari 61 interaksi",
|
subtitle: "53 dari 61 interaksi",
|
||||||
icon: (
|
icon: CheckCircle,
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-6 w-6 text-muted-foreground"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.473-1.688 3.342-.48.485-.926.97-1.378 1.44c-1.472 1.58-2.306 2.787-2.91 3.514-.15.18-.207.33-.207.33A.75.75 0 0 1 15 21h-3c-1.104 0-2.08-.542-2.657-1.455-.139-.201-.264-.406-.38-.614l-.014-.025C8.85 18.067 8.156 17.2 7.5 16.325.728 12.56.728 7.44 7.5 3.675c3.04-.482 5.584.47 7.042 1.956.674.672 1.228 1.462 1.696 2.307.426.786.793 1.582 1.113 2.392h.001Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: "Belum Ditindak",
|
title: "Belum Ditindak",
|
||||||
value: "8",
|
value: "8",
|
||||||
sub: "Perlu respon manual",
|
subtitle: "Perlu respon manual",
|
||||||
deltaType: "negative",
|
icon: AlertTriangle,
|
||||||
icon: (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-6 w-6 text-muted-foreground"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
title: "Waktu Respon",
|
title: "Waktu Respon",
|
||||||
value: "2.3 sec",
|
value: "2.3 sec",
|
||||||
sub: "Rata-rata",
|
subtitle: "Rata-rata",
|
||||||
icon: (
|
icon: Clock,
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-6 w-6 text-muted-foreground"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Chart Data
|
||||||
const chartData = [
|
const chartData = [
|
||||||
{ day: "Sen", total: 100 },
|
{ day: "Sen", total: 45 },
|
||||||
{ day: "Sel", total: 120 },
|
{ day: "Sel", total: 62 },
|
||||||
{ day: "Rab", total: 90 },
|
{ day: "Rab", total: 38 },
|
||||||
{ day: "Kam", total: 150 },
|
{ day: "Kam", total: 75 },
|
||||||
{ day: "Jum", total: 110 },
|
{ day: "Jum", total: 58 },
|
||||||
{ day: "Sab", total: 80 },
|
{ day: "Sab", total: 32 },
|
||||||
{ day: "Min", total: 130 },
|
{ day: "Min", total: 51 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Top Topics Data
|
||||||
const topTopics = [
|
const topTopics = [
|
||||||
{ topic: "Cara mengurus KTP", count: 89 },
|
{ topic: "Cara mengurus KTP", count: 89 },
|
||||||
{ topic: "Syarat Kartu Keluarga", count: 76 },
|
{ topic: "Syarat Kartu Keluarga", count: 76 },
|
||||||
@@ -126,6 +82,7 @@ const topTopics = [
|
|||||||
{ topic: "Info program bansos", count: 48 },
|
{ topic: "Info program bansos", count: 48 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Busy Hours Data
|
||||||
const busyHours = [
|
const busyHours = [
|
||||||
{ period: "Pagi (08–12)", percentage: 30 },
|
{ period: "Pagi (08–12)", percentage: 30 },
|
||||||
{ period: "Siang (12–16)", percentage: 40 },
|
{ period: "Siang (12–16)", percentage: 40 },
|
||||||
@@ -138,146 +95,206 @@ const JennaAnalytic = () => {
|
|||||||
const dark = colorScheme === "dark";
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="space-y-6">
|
<Stack gap="lg">
|
||||||
<Stack gap="xl">
|
{/* TOP SECTION - 4 STAT CARDS */}
|
||||||
{/* KPI Cards */}
|
<Grid gutter="md">
|
||||||
<Grid gutter="lg">
|
{kpiData.map((item) => (
|
||||||
{kpiData.map((kpi) => (
|
<Grid.Col key={item.id} span={{ base: 12, sm: 6, lg: 3 }}>
|
||||||
<Grid.Col key={kpi.id} span={{ base: 12, md: 6, lg: 3 }}>
|
|
||||||
<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}
|
|
||||||
</Text>
|
|
||||||
{React.cloneElement(kpi.icon, {
|
|
||||||
className: "h-6 w-6", // Keeping classes for now, can be replaced by Mantine Icon component if available or styled with sx prop
|
|
||||||
color: "var(--mantine-color-dimmed)", // Set color via prop
|
|
||||||
})}
|
|
||||||
</Group>
|
|
||||||
<Title order={3} fw={700} mt="xs">
|
|
||||||
{kpi.value}
|
|
||||||
</Title>
|
|
||||||
{kpi.delta && (
|
|
||||||
<Text
|
|
||||||
size="xs"
|
|
||||||
c={
|
|
||||||
kpi.deltaType === "positive"
|
|
||||||
? "green"
|
|
||||||
: kpi.deltaType === "negative"
|
|
||||||
? "red"
|
|
||||||
: "dimmed"
|
|
||||||
}
|
|
||||||
mt={4}
|
|
||||||
>
|
|
||||||
{kpi.delta}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{kpi.sub && (
|
|
||||||
<Text size="xs" c="dimmed" mt={2}>
|
|
||||||
{kpi.sub}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
|
||||||
))}
|
|
||||||
</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
|
<Card
|
||||||
p="md"
|
p="md"
|
||||||
radius="md"
|
radius="xl"
|
||||||
withBorder
|
withBorder
|
||||||
bg={dark ? "#141D34" : "white"}
|
bg={dark ? "#1E293B" : "white"}
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
transition: "transform 0.15s ease, box-shadow 0.15s ease",
|
||||||
|
}}
|
||||||
h="100%"
|
h="100%"
|
||||||
>
|
>
|
||||||
<Title order={3} fw={500} mb="md">
|
<Group justify="space-between" align="flex-start" w="100%">
|
||||||
Jam Tersibuk
|
<Stack gap={2}>
|
||||||
</Title>
|
<Text size="sm" c="dimmed">
|
||||||
<Stack gap="sm">
|
{item.title}
|
||||||
{busyHours.map((item, index) => (
|
</Text>
|
||||||
<Box key={index}>
|
<Text size="xl" fw={700} c={dark ? "white" : "gray.9"}>
|
||||||
<Text size="sm">{item.period}</Text>
|
{item.value}
|
||||||
<Group align="center">
|
</Text>
|
||||||
<Progress value={item.percentage} flex={1} />
|
<Group gap={4} align="flex-start">
|
||||||
<Text size="sm" fw={500}>
|
{item.trend === "positive" && (
|
||||||
{item.percentage}%
|
<TrendingUp size={14} color="#22C55E" />
|
||||||
</Text>
|
)}
|
||||||
</Group>
|
<Text
|
||||||
</Box>
|
size="xs"
|
||||||
))}
|
c={
|
||||||
</Stack>
|
item.trend === "positive"
|
||||||
|
? "green"
|
||||||
|
: dark
|
||||||
|
? "gray.4"
|
||||||
|
: "gray.5"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.subtitle}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
<ThemeIcon
|
||||||
|
color="#1E3A5F"
|
||||||
|
variant="filled"
|
||||||
|
size="lg"
|
||||||
|
radius="xl"
|
||||||
|
>
|
||||||
|
<item.icon style={{ width: "60%", height: "60%" }} />
|
||||||
|
</ThemeIcon>
|
||||||
|
</Group>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
{/* Topik Pertanyaan Terbanyak & Jam Tersibuk */}
|
{/* MAIN CHART - INTERAKSI CHATBOT */}
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
<Card
|
||||||
<Stack gap="lg">
|
p="md"
|
||||||
{/* Topik Pertanyaan Terbanyak */}
|
radius="xl"
|
||||||
<Card
|
withBorder
|
||||||
p="md"
|
bg={dark ? "#1E293B" : "white"}
|
||||||
radius="md"
|
style={{
|
||||||
withBorder
|
borderColor: dark ? "#334155" : "white",
|
||||||
bg={dark ? "#141D34" : "white"}
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
}}
|
||||||
h="100%"
|
>
|
||||||
>
|
<Group justify="space-between" mb="md">
|
||||||
<Title order={3} fw={500} mb="md">
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
Topik Pertanyaan Terbanyak
|
Interaksi Chatbot
|
||||||
</Title>
|
</Title>
|
||||||
<Stack gap="xs">
|
</Group>
|
||||||
{topTopics.map((item, index) => (
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<Group
|
<BarChart data={chartData}>
|
||||||
key={index}
|
<CartesianGrid
|
||||||
justify="space-between"
|
strokeDasharray="3 3"
|
||||||
align="center"
|
vertical={false}
|
||||||
p="xs"
|
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="day"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
|
||||||
|
cursor={{ fill: dark ? "#334155" : "#f3f4f6" }}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="total"
|
||||||
|
fill="#1E3A5F"
|
||||||
|
radius={[8, 8, 0, 0]}
|
||||||
|
maxBarSize={60}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* BOTTOM SECTION - 2 COLUMNS */}
|
||||||
|
<Grid gutter="lg">
|
||||||
|
{/* LEFT: TOPIK PERTANYAAN TERBANYAK */}
|
||||||
|
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"} mb="md">
|
||||||
|
Topik Pertanyaan Terbanyak
|
||||||
|
</Title>
|
||||||
|
<Stack gap="xs">
|
||||||
|
{topTopics.map((item, index) => (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
p="sm"
|
||||||
|
bg={dark ? "#334155" : "#F1F5F9"}
|
||||||
|
style={{
|
||||||
|
transition: "background-color 0.15s ease",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text size="sm" fw={500} c={dark ? "white" : "gray.9"}>
|
||||||
|
{item.topic}
|
||||||
|
</Text>
|
||||||
|
<Badge
|
||||||
|
variant="light"
|
||||||
|
color="darmasaba-blue"
|
||||||
|
radius="sm"
|
||||||
|
fw={600}
|
||||||
>
|
>
|
||||||
<Text size="sm" fw={500}>
|
{item.count}x
|
||||||
{item.topic}
|
</Badge>
|
||||||
</Text>
|
</Group>
|
||||||
<Badge variant="light" color="gray">
|
</Box>
|
||||||
{item.count}x
|
))}
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Jam Tersibuk */}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid.Col>
|
</Card>
|
||||||
</Grid>
|
</Grid.Col>
|
||||||
</Stack>
|
|
||||||
</Box>
|
{/* RIGHT: JAM TERSIBUK */}
|
||||||
|
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"} mb="md">
|
||||||
|
Jam Tersibuk
|
||||||
|
</Title>
|
||||||
|
<Stack gap="md">
|
||||||
|
{busyHours.map((item, index) => (
|
||||||
|
<Box key={index}>
|
||||||
|
<Group justify="space-between" mb={5}>
|
||||||
|
<Text size="sm" fw={500} c={dark ? "white" : "gray.9"}>
|
||||||
|
{item.period}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
|
||||||
|
{item.percentage}%
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Progress
|
||||||
|
value={item.percentage}
|
||||||
|
size="lg"
|
||||||
|
radius="xl"
|
||||||
|
color="#1E3A5F"
|
||||||
|
animated
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default JennaAnalytic;
|
export default JennaAnalytic;
|
||||||
|
|||||||
@@ -5,22 +5,18 @@ import {
|
|||||||
Grid,
|
Grid,
|
||||||
GridCol,
|
GridCol,
|
||||||
Group,
|
Group,
|
||||||
List,
|
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
useMantineColorScheme,
|
useMantineColorScheme
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
IconAlertTriangle,
|
IconAlertTriangle,
|
||||||
IconCamera,
|
IconCamera,
|
||||||
IconClock,
|
IconClock,
|
||||||
IconEye,
|
IconMapPin
|
||||||
IconMapPin,
|
|
||||||
IconShieldLock,
|
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
const KeamananPage = () => {
|
const KeamananPage = () => {
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
@@ -125,10 +121,53 @@ const KeamananPage = () => {
|
|||||||
</Title>
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* KPI Cards */}
|
|
||||||
<Grid gutter="md">
|
<Grid gutter="md">
|
||||||
{kpiData.map((kpi, index) => (
|
{/* Peta Keamanan CCTV */}
|
||||||
<GridCol key={index} span={{ base: 12, sm: 6, md: 6 }}>
|
<GridCol span={{ base: 12, lg: 6 }}>
|
||||||
|
<Stack gap={"xs"}>
|
||||||
|
{/* KPI Cards */}
|
||||||
|
<Grid gutter="md">
|
||||||
|
{kpiData.map((kpi, index) => (
|
||||||
|
<GridCol key={index} span={{ base: 12, sm: 6, md: 6 }}>
|
||||||
|
<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"}>
|
||||||
|
{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>
|
||||||
<Card
|
<Card
|
||||||
p="md"
|
p="md"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -137,119 +176,81 @@ const KeamananPage = () => {
|
|||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||||
h="100%"
|
h="100%"
|
||||||
>
|
>
|
||||||
<Group justify="space-between" align="center">
|
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
|
||||||
<Stack gap={0}>
|
Peta Keamanan CCTV
|
||||||
<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
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
h="100%"
|
|
||||||
>
|
|
||||||
<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>
|
</Title>
|
||||||
{cctvLocations.map((cctv, index) => (
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"} mb="md">
|
||||||
<Card
|
Titik Lokasi CCTV
|
||||||
key={index}
|
</Text>
|
||||||
p="md"
|
|
||||||
radius="md"
|
{/* Placeholder for map */}
|
||||||
withBorder
|
<Box
|
||||||
bg={dark ? "#263852ff" : "#F1F5F9"}
|
style={{
|
||||||
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
|
backgroundColor: dark ? "#2d3748" : "#e2e8f0",
|
||||||
>
|
borderRadius: "8px",
|
||||||
<Group justify="space-between">
|
height: "400px",
|
||||||
<Stack gap={0}>
|
display: "flex",
|
||||||
<Group gap="xs">
|
alignItems: "center",
|
||||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
justifyContent: "center",
|
||||||
{cctv.id}
|
}}
|
||||||
|
>
|
||||||
|
<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}
|
||||||
|
p="md"
|
||||||
|
radius="md"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#263852ff" : "#F1F5F9"}
|
||||||
|
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
|
||||||
|
>
|
||||||
|
<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>
|
</Text>
|
||||||
<Badge
|
|
||||||
variant="dot"
|
|
||||||
color={cctv.status === "active" ? "green" : "gray"}
|
|
||||||
>
|
|
||||||
{cctv.status === "active" ? "Online" : "Offline"}
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
</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>
|
||||||
</Group>
|
</Card>
|
||||||
</Card>
|
))}
|
||||||
))}
|
</Stack>
|
||||||
</Stack>
|
</Card>
|
||||||
</Card>
|
</Stack>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
|
|
||||||
{/* Daftar Laporan Keamanan */}
|
{/* Daftar Laporan Keamanan */}
|
||||||
@@ -262,10 +263,6 @@ const KeamananPage = () => {
|
|||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||||
h="100%"
|
h="100%"
|
||||||
>
|
>
|
||||||
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
|
|
||||||
Laporan Keamanan Lingkungan
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
{securityReports.map((report, index) => (
|
{securityReports.map((report, index) => (
|
||||||
<Card
|
<Card
|
||||||
|
|||||||
@@ -1,73 +1,70 @@
|
|||||||
import { BarChart } from "@mantine/charts";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
|
||||||
Card,
|
Card,
|
||||||
Grid,
|
Grid,
|
||||||
|
GridCol,
|
||||||
Group,
|
Group,
|
||||||
Progress,
|
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
IconCurrency,
|
CheckCircle,
|
||||||
IconTrendingDown,
|
Coins,
|
||||||
IconTrendingUp,
|
PieChart as PieChartIcon,
|
||||||
} from "@tabler/icons-react";
|
Receipt,
|
||||||
import React from "react";
|
TrendingDown,
|
||||||
|
TrendingUp,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
Line,
|
||||||
|
LineChart,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
|
|
||||||
// Sample Data
|
// KPI Data
|
||||||
const kpiData = [
|
const kpiData = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: "Total APBDes",
|
title: "Total APBDes",
|
||||||
value: "Rp 5.2M",
|
value: "Rp 5.2M",
|
||||||
sub: "Tahun 2025",
|
subtitle: "Tahun 2025",
|
||||||
icon: <IconCurrency className="h-6 w-6 text-muted-foreground" />,
|
icon: Coins,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: "Realisasi",
|
title: "Realisasi",
|
||||||
value: "68%",
|
value: "68%",
|
||||||
sub: "Rp 3.5M dari 5.2M",
|
subtitle: "Rp 3.5M dari 5.2M",
|
||||||
icon: (
|
icon: CheckCircle,
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-6 w-6 text-muted-foreground"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.473-1.688 3.342-.48.485-.926.97-1.378 1.44c-1.472 1.58-2.306 2.787-2.91 3.514-.15.18-.207.33-.207.33A.75.75 0 0 1 15 21h-3c-1.104 0-2.08-.542-2.657-1.455-.139-.201-.264-.406-.38-.614l-.014-.025C8.85 18.067 8.156 17.2 7.5 16.325.728 12.56.728 7.44 7.5 3.675c3.04-.482 5.584.47 7.042 1.956.674.672 1.228 1.462 1.696 2.307.426.786.793 1.582 1.113 2.392h.001Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: "Pemasukan",
|
title: "Pemasukan",
|
||||||
value: "Rp 580jt",
|
value: "Rp 580jt",
|
||||||
sub: "Bulan ini",
|
subtitle: "Bulan ini",
|
||||||
delta: "+8%",
|
trend: "+8%",
|
||||||
deltaType: "positive",
|
icon: TrendingUp,
|
||||||
icon: <IconTrendingUp className="h-6 w-6 text-muted-foreground" />,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
title: "Pengeluaran",
|
title: "Pengeluaran",
|
||||||
value: "Rp 520jt",
|
value: "Rp 520jt",
|
||||||
sub: "Bulan ini",
|
subtitle: "Bulan ini",
|
||||||
icon: <IconTrendingDown className="h-6 w-6 text-muted-foreground" />,
|
icon: TrendingDown,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Income & Expense Data
|
||||||
const incomeExpenseData = [
|
const incomeExpenseData = [
|
||||||
{ month: "Apr", income: 450, expense: 380 },
|
{ month: "Apr", income: 450, expense: 380 },
|
||||||
{ month: "Mei", income: 520, expense: 420 },
|
{ month: "Mei", income: 520, expense: 420 },
|
||||||
@@ -78,6 +75,7 @@ const incomeExpenseData = [
|
|||||||
{ month: "Okt", income: 580, expense: 520 },
|
{ month: "Okt", income: 580, expense: 520 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Sector Allocation Data
|
||||||
const allocationData = [
|
const allocationData = [
|
||||||
{ sector: "Pembangunan", amount: 1200 },
|
{ sector: "Pembangunan", amount: 1200 },
|
||||||
{ sector: "Kesehatan", amount: 800 },
|
{ sector: "Kesehatan", amount: 800 },
|
||||||
@@ -87,13 +85,7 @@ const allocationData = [
|
|||||||
{ sector: "Teknologi", amount: 300 },
|
{ sector: "Teknologi", amount: 300 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const assistanceFundData = [
|
// APBDes Report Data
|
||||||
{ source: "Dana Desa (DD)", amount: 1800, status: "cair" },
|
|
||||||
{ source: "Alokasi Dana Desa (ADD)", amount: 950, status: "cair" },
|
|
||||||
{ source: "Bagi Hasil Pajak", amount: 450, status: "cair" },
|
|
||||||
{ source: "Hibah Provinsi", amount: 300, status: "proses" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const apbdReport = {
|
const apbdReport = {
|
||||||
income: [
|
income: [
|
||||||
{ category: "Dana Desa", amount: 1800 },
|
{ category: "Dana Desa", amount: 1800 },
|
||||||
@@ -113,244 +105,410 @@ const apbdReport = {
|
|||||||
totalExpenses: 2155,
|
totalExpenses: 2155,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Aid & Grants Data
|
||||||
|
const assistanceFundData = [
|
||||||
|
{ source: "Dana Desa (DD)", amount: 1800, status: "cair" },
|
||||||
|
{ source: "Alokasi Dana Desa (ADD)", amount: 950, status: "cair" },
|
||||||
|
{ source: "Bagi Hasil Pajak", amount: 450, status: "cair" },
|
||||||
|
{ source: "Hibah Provinsi", amount: 300, status: "proses" },
|
||||||
|
];
|
||||||
|
|
||||||
const KeuanganAnggaran = () => {
|
const KeuanganAnggaran = () => {
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const dark = colorScheme === "dark";
|
const dark = colorScheme === "dark";
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Stack gap="xl">
|
|
||||||
{/* KPI Cards */}
|
|
||||||
<Grid gutter="lg">
|
|
||||||
{kpiData.map((kpi) => (
|
|
||||||
<Grid.Col key={kpi.id} 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="flex-start" mb="xs">
|
|
||||||
<Text size="sm" fw={500} c="dimmed">
|
|
||||||
{kpi.title}
|
|
||||||
</Text>
|
|
||||||
{React.cloneElement(kpi.icon, {
|
|
||||||
className: "h-6 w-6",
|
|
||||||
color: "var(--mantine-color-dimmed)",
|
|
||||||
})}
|
|
||||||
</Group>
|
|
||||||
<Title order={3} fw={700} mt="xs">
|
|
||||||
{kpi.value}
|
|
||||||
</Title>
|
|
||||||
{kpi.delta && (
|
|
||||||
<Text
|
|
||||||
size="xs"
|
|
||||||
c={
|
|
||||||
kpi.deltaType === "positive"
|
|
||||||
? "green"
|
|
||||||
: kpi.deltaType === "negative"
|
|
||||||
? "red"
|
|
||||||
: "dimmed"
|
|
||||||
}
|
|
||||||
mt={4}
|
|
||||||
>
|
|
||||||
{kpi.delta}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{kpi.sub && (
|
|
||||||
<Text size="xs" c="dimmed" mt="auto">
|
|
||||||
{kpi.sub}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Charts Section */}
|
return (
|
||||||
<Grid gutter="lg">
|
<Stack gap="lg">
|
||||||
{/* Grafik Pemasukan vs Pengeluaran */}
|
{/* TOP SECTION - 4 STAT CARDS */}
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
<Grid gutter="md">
|
||||||
|
{kpiData.map((item) => (
|
||||||
|
<Grid.Col key={item.id} span={{ base: 12, sm: 6, lg: 3 }}>
|
||||||
<Card
|
<Card
|
||||||
p="md"
|
p="md"
|
||||||
radius="md"
|
radius="xl"
|
||||||
withBorder
|
withBorder
|
||||||
bg={dark ? "#141D34" : "white"}
|
bg={dark ? "#1E293B" : "white"}
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
transition: "transform 0.15s ease, box-shadow 0.15s ease",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
>
|
>
|
||||||
<Title order={3} fw={500} mb="md">
|
<Group justify="space-between" align="flex-start" w="100%">
|
||||||
Pemasukan vs Pengeluaran
|
<Stack gap={2}>
|
||||||
</Title>
|
<Text size="sm" c="dimmed">
|
||||||
<BarChart
|
{item.title}
|
||||||
h={300}
|
</Text>
|
||||||
data={incomeExpenseData}
|
<Text size="xl" fw={700} c={dark ? "white" : "gray.9"}>
|
||||||
dataKey="month"
|
{item.value}
|
||||||
series={[
|
</Text>
|
||||||
{ name: "income", color: "green", label: "Pemasukan" },
|
<Group gap={4} align="flex-start">
|
||||||
{ name: "expense", color: "red", label: "Pengeluaran" },
|
{item.trend && <TrendingUp size={14} color="#22C55E" />}
|
||||||
]}
|
<Text
|
||||||
withLegend
|
size="xs"
|
||||||
/>
|
c={item.trend ? "green" : dark ? "gray.4" : "gray.5"}
|
||||||
|
>
|
||||||
|
{item.subtitle}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
<ThemeIcon
|
||||||
|
color="#1E3A5F"
|
||||||
|
variant="filled"
|
||||||
|
size="lg"
|
||||||
|
radius="xl"
|
||||||
|
>
|
||||||
|
<item.icon style={{ width: "60%", height: "60%" }} />
|
||||||
|
</ThemeIcon>
|
||||||
|
</Group>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
{/* Alokasi Anggaran Per Sektor */}
|
{/* MAIN CHART SECTION */}
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
<Grid gutter="lg">
|
||||||
<Card
|
{/* LEFT: PEMASUKAN DAN PENGELUARAN (70%) */}
|
||||||
p="md"
|
<Grid.Col span={{ base: 12, lg: 8 }}>
|
||||||
radius="md"
|
<Card
|
||||||
withBorder
|
p="md"
|
||||||
bg={dark ? "#141D34" : "white"}
|
radius="xl"
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
withBorder
|
||||||
>
|
bg={dark ? "#1E293B" : "white"}
|
||||||
<Title order={3} fw={500} mb="md">
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<PieChartIcon size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
|
Pemasukan dan Pengeluaran
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<LineChart data={incomeExpenseData}>
|
||||||
|
<CartesianGrid
|
||||||
|
strokeDasharray="3 3"
|
||||||
|
vertical={false}
|
||||||
|
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="month"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
tickFormatter={(value) => `Rp ${value}jt`}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
|
||||||
|
formatter={(value: number | undefined) => [
|
||||||
|
`Rp ${value}jt`,
|
||||||
|
"",
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="income"
|
||||||
|
stroke="#22C55E"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ fill: "#22C55E", strokeWidth: 2, r: 4 }}
|
||||||
|
activeDot={{ r: 6 }}
|
||||||
|
name="Pemasukan"
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="expense"
|
||||||
|
stroke="#EF4444"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ fill: "#EF4444", strokeWidth: 2, r: 4 }}
|
||||||
|
activeDot={{ r: 6 }}
|
||||||
|
name="Pengeluaran"
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
{/* RIGHT: ALOKASI ANGGARAN PER SEKTOR (30%) */}
|
||||||
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<PieChartIcon size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
Alokasi Anggaran Per Sektor
|
Alokasi Anggaran Per Sektor
|
||||||
</Title>
|
</Title>
|
||||||
<BarChart
|
</Group>
|
||||||
h={300}
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
data={allocationData}
|
<BarChart data={allocationData} layout="vertical">
|
||||||
dataKey="sector"
|
<CartesianGrid
|
||||||
series={[
|
strokeDasharray="3 3"
|
||||||
{ name: "amount", color: "darmasaba-navy", label: "Jumlah" },
|
horizontal={false}
|
||||||
]}
|
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||||
withLegend
|
/>
|
||||||
orientation="horizontal"
|
<XAxis
|
||||||
/>
|
type="number"
|
||||||
</Card>
|
axisLine={false}
|
||||||
</Grid.Col>
|
tickLine={false}
|
||||||
</Grid>
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
tickFormatter={(value) => `${value}`}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
type="category"
|
||||||
|
dataKey="sector"
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{
|
||||||
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
|
fontSize: 11,
|
||||||
|
}}
|
||||||
|
width={100}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
formatter={(value: number | undefined) => [
|
||||||
|
`Rp ${value}jt`,
|
||||||
|
"Jumlah",
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="amount"
|
||||||
|
fill="#1E3A5F"
|
||||||
|
radius={[0, 8, 8, 0]}
|
||||||
|
maxBarSize={30}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Grid gutter="lg">
|
{/* BOTTOM SECTION */}
|
||||||
{/* Dana Bantuan & Hibah */}
|
<Grid gutter="lg">
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
{/* LEFT: LAPORAN APBDES */}
|
||||||
<Card
|
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||||
p="md"
|
<Card
|
||||||
radius="md"
|
p="md"
|
||||||
withBorder
|
radius="xl"
|
||||||
bg={dark ? "#141D34" : "white"}
|
withBorder
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
bg={dark ? "#1E293B" : "white"}
|
||||||
>
|
style={{
|
||||||
<Title order={3} fw={500} mb="md">
|
borderColor: dark ? "#334155" : "white",
|
||||||
Dana Bantuan & Hibah
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<Receipt size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
|
Laporan APBDes
|
||||||
</Title>
|
</Title>
|
||||||
<Stack gap="sm">
|
</Group>
|
||||||
{assistanceFundData.map((fund, index) => (
|
|
||||||
<Group
|
<Grid gutter="md">
|
||||||
key={index}
|
{/* Pendapatan */}
|
||||||
justify="space-between"
|
<Grid.Col span={6}>
|
||||||
align="center"
|
<Card p="sm" radius="lg" bg={dark ? "#064E3B" : "#DCFCE7"}>
|
||||||
p="sm"
|
<Title order={5} c="#22C55E" mb="sm">
|
||||||
style={{
|
Pendapatan
|
||||||
border: "1px solid var(--mantine-color-gray-3)",
|
</Title>
|
||||||
borderRadius: "var(--mantine-radius-sm)",
|
<Stack gap="xs">
|
||||||
}}
|
{apbdReport.income.map((item, index) => (
|
||||||
>
|
<Group key={index} justify="space-between">
|
||||||
|
<Text size="sm" c={dark ? "gray.3" : "gray.7"}>
|
||||||
|
{item.category}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={600} c="#22C55E">
|
||||||
|
Rp {item.amount.toLocaleString()}jt
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
mt="sm"
|
||||||
|
pt="sm"
|
||||||
|
style={{
|
||||||
|
borderTop: `1px solid ${dark ? "#065F46" : "#86EFAC"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text fw={700} c="#22C55E">
|
||||||
|
Total:
|
||||||
|
</Text>
|
||||||
|
<Text fw={700} c="#22C55E">
|
||||||
|
Rp {apbdReport.totalIncome.toLocaleString()}jt
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
{/* Belanja */}
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Card p="sm" radius="lg" bg={dark ? "#7F1D1D" : "#FEE2E2"}>
|
||||||
|
<Title order={5} c="#EF4444" mb="sm">
|
||||||
|
Belanja
|
||||||
|
</Title>
|
||||||
|
<Stack gap="xs">
|
||||||
|
{apbdReport.expenses.map((item, index) => (
|
||||||
|
<Group key={index} justify="space-between">
|
||||||
|
<Text size="sm" c={dark ? "gray.3" : "gray.7"}>
|
||||||
|
{item.category}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={600} c="#EF4444">
|
||||||
|
Rp {item.amount.toLocaleString()}jt
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
mt="sm"
|
||||||
|
pt="sm"
|
||||||
|
style={{
|
||||||
|
borderTop: `1px solid ${dark ? "#991B1B" : "#FCA5A5"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text fw={700} c="#EF4444">
|
||||||
|
Total:
|
||||||
|
</Text>
|
||||||
|
<Text fw={700} c="#EF4444">
|
||||||
|
Rp {apbdReport.totalExpenses.toLocaleString()}jt
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Saldo */}
|
||||||
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
mt="md"
|
||||||
|
pt="md"
|
||||||
|
style={{
|
||||||
|
borderTop: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text fw={700} c={dark ? "white" : "gray.9"}>
|
||||||
|
Saldo:
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fw={700}
|
||||||
|
size="lg"
|
||||||
|
c={
|
||||||
|
apbdReport.totalIncome > apbdReport.totalExpenses
|
||||||
|
? "#22C55E"
|
||||||
|
: "#EF4444"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Rp{" "}
|
||||||
|
{(
|
||||||
|
apbdReport.totalIncome - apbdReport.totalExpenses
|
||||||
|
).toLocaleString()}
|
||||||
|
jt
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
{/* RIGHT: DANA BANTUAN DAN HIBAH */}
|
||||||
|
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#1E293B" : "white"}
|
||||||
|
style={{
|
||||||
|
borderColor: dark ? "#334155" : "white",
|
||||||
|
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
|
}}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<Group gap="xs" mb="md">
|
||||||
|
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
|
||||||
|
<Coins size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||||
|
Dana Bantuan dan Hibah
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{assistanceFundData.map((fund, index) => (
|
||||||
|
<Card
|
||||||
|
key={index}
|
||||||
|
p="sm"
|
||||||
|
radius="lg"
|
||||||
|
bg={dark ? "#334155" : "#F1F5F9"}
|
||||||
|
style={{
|
||||||
|
borderColor: "transparent",
|
||||||
|
transition: "background-color 0.15s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
<Box>
|
<Box>
|
||||||
<Text size="sm" fw={500}>
|
<Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
|
||||||
{fund.source}
|
{fund.source}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
Rp {fund.amount.toLocaleString()}jt
|
Rp {fund.amount.toLocaleString()}jt
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Badge
|
<Badge
|
||||||
variant="light"
|
variant="light"
|
||||||
color={fund.status === "cair" ? "green" : "yellow"}
|
color={fund.status === "cair" ? "green" : "yellow"}
|
||||||
|
radius="sm"
|
||||||
|
fw={600}
|
||||||
>
|
>
|
||||||
{fund.status}
|
{fund.status === "cair" ? "Cair" : "Proses"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
</Card>
|
||||||
</Stack>
|
))}
|
||||||
</Card>
|
</Stack>
|
||||||
</Grid.Col>
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
{/* Laporan APBDes */}
|
</Grid>
|
||||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
</Stack>
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Title order={3} fw={500} mb="md">
|
|
||||||
Laporan APBDes
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
<Box mb="md">
|
|
||||||
<Title order={4} mb="sm">
|
|
||||||
Pendapatan
|
|
||||||
</Title>
|
|
||||||
<Stack gap="xs">
|
|
||||||
{apbdReport.income.map((item, index) => (
|
|
||||||
<Group key={index} justify="space-between">
|
|
||||||
<Text size="sm">{item.category}</Text>
|
|
||||||
<Text size="sm" c="green">
|
|
||||||
Rp {item.amount.toLocaleString()}jt
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
))}
|
|
||||||
<Group justify="space-between" mt="sm">
|
|
||||||
<Text fw={700}>Total Pendapatan:</Text>
|
|
||||||
<Text fw={700} c="green">
|
|
||||||
Rp {apbdReport.totalIncome.toLocaleString()}jt
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Title order={4} mb="sm">
|
|
||||||
Belanja
|
|
||||||
</Title>
|
|
||||||
<Stack gap="xs">
|
|
||||||
{apbdReport.expenses.map((item, index) => (
|
|
||||||
<Group key={index} justify="space-between">
|
|
||||||
<Text size="sm">{item.category}</Text>
|
|
||||||
<Text size="sm" c="red">
|
|
||||||
Rp {item.amount.toLocaleString()}jt
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
))}
|
|
||||||
<Group justify="space-between" mt="sm">
|
|
||||||
<Text fw={700}>Total Belanja:</Text>
|
|
||||||
<Text fw={700} c="red">
|
|
||||||
Rp {apbdReport.totalExpenses.toLocaleString()}jt
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
mt="md"
|
|
||||||
pt="md"
|
|
||||||
style={{ borderTop: "1px solid var(--mantine-color-gray-3)" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text fw={700}>Saldo:</Text>
|
|
||||||
<Text
|
|
||||||
fw={700}
|
|
||||||
c={
|
|
||||||
apbdReport.totalIncome > apbdReport.totalExpenses
|
|
||||||
? "green"
|
|
||||||
: "red"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Rp{" "}
|
|
||||||
{(
|
|
||||||
apbdReport.totalIncome - apbdReport.totalExpenses
|
|
||||||
).toLocaleString()}
|
|
||||||
jt
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Box>
|
|
||||||
</Card>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { Grid, Stack } from "@mantine/core";
|
import { Grid, Stack } from "@mantine/core";
|
||||||
import { ActivityCard, } from "./kinerja-divisi/activity-card";
|
import { ActivityCard } from "./kinerja-divisi/activity-card";
|
||||||
|
import { ArchiveCard } from "./kinerja-divisi/archive-card";
|
||||||
|
import { DiscussionPanel } from "./kinerja-divisi/discussion-panel";
|
||||||
import { DivisionList } from "./kinerja-divisi/division-list";
|
import { DivisionList } from "./kinerja-divisi/division-list";
|
||||||
import { DocumentChart } from "./kinerja-divisi/document-chart";
|
import { DocumentChart } from "./kinerja-divisi/document-chart";
|
||||||
import { ProgressChart } from "./kinerja-divisi/progress-chart";
|
|
||||||
import { DiscussionPanel } from "./kinerja-divisi/discussion-panel";
|
|
||||||
import { EventCard } from "./kinerja-divisi/event-card";
|
import { EventCard } from "./kinerja-divisi/event-card";
|
||||||
import { ArchiveCard } from "./kinerja-divisi/archive-card";
|
import { ProgressChart } from "./kinerja-divisi/progress-chart";
|
||||||
|
|
||||||
|
|
||||||
// Data for program kegiatan (Section 1)
|
// Data for program kegiatan (Section 1)
|
||||||
const programKegiatanData = [
|
const programKegiatanData = [
|
||||||
@@ -14,25 +13,25 @@ const programKegiatanData = [
|
|||||||
title: "Rakor 2025",
|
title: "Rakor 2025",
|
||||||
date: "3 Juli 2025",
|
date: "3 Juli 2025",
|
||||||
progress: 90,
|
progress: 90,
|
||||||
status: "selesai" as const,
|
status: "Selesai" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Pemutakhiran Indeks Desa",
|
title: "Pemutakhiran Indeks Desa",
|
||||||
date: "3 Juli 2025",
|
date: "3 Juli 2025",
|
||||||
progress: 85,
|
progress: 85,
|
||||||
status: "selesai" as const,
|
status: "Selesai" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Mengurus Akta Cerai Warga",
|
title: "Mengurus Akta Cerai Warga",
|
||||||
date: "3 Juli 2025",
|
date: "3 Juli 2025",
|
||||||
progress: 80,
|
progress: 80,
|
||||||
status: "selesai" as const,
|
status: "Selesai" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Pasek 7 Desa Adat",
|
title: "Pasek 7 Desa Adat",
|
||||||
date: "3 Juli 2025",
|
date: "3 Juli 2025",
|
||||||
progress: 92,
|
progress: 92,
|
||||||
status: "selesai" as const,
|
status: "Selesai" as const,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import {
|
import { Box, Card, Group, Progress, Text } from "@mantine/core";
|
||||||
Box,
|
|
||||||
Card,
|
|
||||||
Group,
|
|
||||||
Progress,
|
|
||||||
Text,
|
|
||||||
useMantineColorScheme,
|
|
||||||
} from "@mantine/core";
|
|
||||||
|
|
||||||
interface ActivityCardProps {
|
interface ActivityCardProps {
|
||||||
title: string;
|
title: string;
|
||||||
date: string;
|
date: string;
|
||||||
progress: number;
|
progress: number;
|
||||||
status: "selesai" | "berjalan" | "tertunda";
|
status: "Selesai" | "Berjalan" | "Tertunda";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActivityCard({
|
export function ActivityCard({
|
||||||
@@ -20,16 +13,13 @@ export function ActivityCard({
|
|||||||
progress,
|
progress,
|
||||||
status,
|
status,
|
||||||
}: ActivityCardProps) {
|
}: ActivityCardProps) {
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const getStatusColor = () => {
|
||||||
const dark = colorScheme === "dark";
|
switch (status) {
|
||||||
|
case "Selesai":
|
||||||
const getStatusColor = (s: string) => {
|
|
||||||
switch (s) {
|
|
||||||
case "selesai":
|
|
||||||
return "#22C55E";
|
return "#22C55E";
|
||||||
case "berjalan":
|
case "Berjalan":
|
||||||
return "#3B82F6";
|
return "#3B82F6";
|
||||||
case "tertunda":
|
case "Tertunda":
|
||||||
return "#EF4444";
|
return "#EF4444";
|
||||||
default:
|
default:
|
||||||
return "#9CA3AF";
|
return "#9CA3AF";
|
||||||
@@ -38,58 +28,62 @@ export function ActivityCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
p="md"
|
|
||||||
radius="xl"
|
radius="xl"
|
||||||
withBorder
|
p={0}
|
||||||
bg={dark ? "#1E293B" : "white"}
|
withBorder={false}
|
||||||
style={{
|
style={{
|
||||||
borderColor: dark ? "#334155" : "white",
|
backgroundColor: "#F3F4F6",
|
||||||
boxShadow: dark
|
overflow: "hidden",
|
||||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
|
||||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* 🔵 HEADER */}
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
borderLeft: `4px solid #3B82F6`,
|
backgroundColor: "#1E3A5F",
|
||||||
paddingLeft: 12,
|
padding: "16px",
|
||||||
marginBottom: 12,
|
textAlign: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}>
|
<Text c="white" fw={700} size="md">
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group justify="space-between" mb="xs">
|
{/* CONTENT */}
|
||||||
<Text size="xs" c="dimmed">
|
<Box p="md">
|
||||||
{date}
|
{/* PROGRESS */}
|
||||||
</Text>
|
<Progress
|
||||||
<Box
|
value={progress}
|
||||||
style={{
|
radius="xl"
|
||||||
backgroundColor: getStatusColor(status),
|
size="lg"
|
||||||
color: "white",
|
color="orange"
|
||||||
padding: "2px 8px",
|
styles={{
|
||||||
borderRadius: 4,
|
root: {
|
||||||
fontSize: 11,
|
height: 16,
|
||||||
fontWeight: 600,
|
},
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{status.toUpperCase()}
|
|
||||||
</Box>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Progress
|
{/* FOOTER */}
|
||||||
value={progress}
|
<Group justify="space-between" mt="md">
|
||||||
size="sm"
|
<Text size="sm" fw={500}>
|
||||||
radius="xl"
|
{date}
|
||||||
color={progress === 100 ? "green" : "yellow"}
|
</Text>
|
||||||
animated={progress < 100}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text size="xs" c="dimmed" mt="xs" ta="right">
|
<Box
|
||||||
{progress}%
|
style={{
|
||||||
</Text>
|
backgroundColor: getStatusColor(),
|
||||||
|
color: "white",
|
||||||
|
padding: "4px 12px",
|
||||||
|
borderRadius: 999,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export function ArchiveCard({ item, onClick }: ArchiveCardProps) {
|
|||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
transition: "transform 0.2s, box-shadow 0.2s",
|
transition: "transform 0.2s, box-shadow 0.2s",
|
||||||
}}
|
}}
|
||||||
|
h="100%"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<Group gap="md">
|
<Group gap="md">
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export function DiscussionPanel() {
|
|||||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
}}
|
}}
|
||||||
|
h="100%"
|
||||||
>
|
>
|
||||||
<Group gap="xs" mb="md">
|
<Group gap="xs" mb="md">
|
||||||
<MessageCircle size={20} color={dark ? "#E2E8F0" : "#1E3A5F"} />
|
<MessageCircle size={20} color={dark ? "#E2E8F0" : "#1E3A5F"} />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Bar,
|
Bar,
|
||||||
BarChart,
|
BarChart,
|
||||||
CartesianGrid,
|
CartesianGrid,
|
||||||
|
Cell,
|
||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
XAxis,
|
XAxis,
|
||||||
@@ -10,8 +11,8 @@ import {
|
|||||||
} from "recharts";
|
} from "recharts";
|
||||||
|
|
||||||
const documentData = [
|
const documentData = [
|
||||||
{ name: "Gambar", value: 300 },
|
{ name: "Gambar", jumlah: 300, color: "#FACC15" },
|
||||||
{ name: "Dokumen", value: 310 },
|
{ name: "Dokumen", jumlah: 310, color: "#22C55E" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function DocumentChart() {
|
export function DocumentChart() {
|
||||||
@@ -61,7 +62,11 @@ export function DocumentChart() {
|
|||||||
}}
|
}}
|
||||||
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
|
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
|
||||||
/>
|
/>
|
||||||
<Bar dataKey="value" fill="#3B82F6" radius={[4, 4, 0, 0]} />
|
<Bar dataKey="jumlah" radius={[4, 4, 0, 0]}>
|
||||||
|
{documentData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
|
))}
|
||||||
|
</Bar>
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export function EventCard({ agendas = [] }: EventCardProps) {
|
|||||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||||
}}
|
}}
|
||||||
|
h="100%"
|
||||||
>
|
>
|
||||||
<Group gap="xs" mb="md">
|
<Group gap="xs" mb="md">
|
||||||
<Calendar size={20} color={dark ? "#E2E8F0" : "#1E3A5F"} />
|
<Calendar size={20} color={dark ? "#E2E8F0" : "#1E3A5F"} />
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
|
|||||||
|
|
||||||
const progressData = [
|
const progressData = [
|
||||||
{ name: "Selesai", value: 83.33, color: "#22C55E" },
|
{ name: "Selesai", value: 83.33, color: "#22C55E" },
|
||||||
{ name: "Dikerjakan", value: 16.67, color: "#FACC15" },
|
{ name: "Dikerjakan", value: 16.67, color: "#F59E0B" },
|
||||||
{ name: "Segera Dikerjakan", value: 0, color: "#3B82F6" },
|
{ name: "Segera Dikerjakan", value: 0, color: "#3B82F6" },
|
||||||
{ name: "Dibatalkan", value: 0, color: "#EF4444" },
|
{ name: "Dibatalkan", value: 0, color: "#EF4444" },
|
||||||
];
|
];
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,189 +1,78 @@
|
|||||||
import {
|
import { Box, Button, Group, Stack, Switch, Text, Title } from "@mantine/core";
|
||||||
ActionIcon,
|
|
||||||
Alert,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Group,
|
|
||||||
Modal,
|
|
||||||
Select,
|
|
||||||
Space,
|
|
||||||
Table,
|
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
Title,
|
|
||||||
useMantineColorScheme,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import {
|
|
||||||
IconEdit,
|
|
||||||
IconInfoCircle,
|
|
||||||
IconTrash,
|
|
||||||
IconUser,
|
|
||||||
IconUserPlus,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
const AksesDanTimSettings = () => {
|
const AksesDanTimSettings = () => {
|
||||||
const [opened, setOpened] = useState(false);
|
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const dark = colorScheme === "dark";
|
|
||||||
|
|
||||||
// Sample team members data
|
|
||||||
const teamMembers = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: "Admin Utama",
|
|
||||||
email: "admin@desa.go.id",
|
|
||||||
role: "Administrator",
|
|
||||||
status: "Aktif",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "Operator Desa",
|
|
||||||
email: "operator@desa.go.id",
|
|
||||||
role: "Operator",
|
|
||||||
status: "Aktif",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: "Staff Keuangan",
|
|
||||||
email: "keuangan@desa.go.id",
|
|
||||||
role: "Keuangan",
|
|
||||||
status: "Aktif",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: "Staff Umum",
|
|
||||||
email: "umum@desa.go.id",
|
|
||||||
role: "Umum",
|
|
||||||
status: "Nonaktif",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const roles = [
|
|
||||||
{ value: "administrator", label: "Administrator" },
|
|
||||||
{ value: "operator", label: "Operator" },
|
|
||||||
{ value: "keuangan", label: "Keuangan" },
|
|
||||||
{ value: "umum", label: "Umum" },
|
|
||||||
{ value: "keamanan", label: "Keamanan" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Stack pr={"50%"} gap={"xl"}>
|
||||||
withBorder
|
<Box>
|
||||||
radius="md"
|
<Stack gap={"xs"}>
|
||||||
p="xl"
|
<Title order={2}>Manajemen Tim</Title>
|
||||||
bg={dark ? "#141D34" : "white"}
|
<Button bg={"#1E3A5F"} radius={"md"} c={"white"} fullWidth>
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
Undangan Anggota Baru
|
||||||
>
|
|
||||||
<Modal
|
|
||||||
opened={opened}
|
|
||||||
onClose={() => setOpened(false)}
|
|
||||||
title="Tambah Anggota Tim"
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
<TextInput
|
|
||||||
label="Nama Lengkap"
|
|
||||||
placeholder="Masukkan nama lengkap anggota tim"
|
|
||||||
mb="md"
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label="Alamat Email"
|
|
||||||
placeholder="Masukkan alamat email"
|
|
||||||
mb="md"
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
label="Peran"
|
|
||||||
placeholder="Pilih peran anggota tim"
|
|
||||||
data={roles}
|
|
||||||
mb="md"
|
|
||||||
/>
|
|
||||||
<Group justify="flex-end" mt="xl">
|
|
||||||
<Button variant="outline" onClick={() => setOpened(false)}>
|
|
||||||
Batal
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button>Undang Anggota</Button>
|
<Button bg={"#1E3A5F"} radius={"md"} c={"white"} fullWidth>
|
||||||
</Group>
|
Kelola Role & Permission
|
||||||
</Modal>
|
</Button>
|
||||||
|
<Group justify="space-between">
|
||||||
<Title order={2} mb="lg">
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
Akses & Tim
|
Daftar Anggota Teraktif
|
||||||
</Title>
|
</Text>
|
||||||
<Text color="dimmed" mb="xl">
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
Kelola akses dan anggota tim Anda
|
12 Anggota
|
||||||
</Text>
|
</Text>
|
||||||
|
</Group>
|
||||||
<Space h="lg" />
|
</Stack>
|
||||||
|
</Box>
|
||||||
<Group justify="space-between" mb="md">
|
<Box>
|
||||||
<Title order={4}>Anggota Tim</Title>
|
<Stack gap={"xs"}>
|
||||||
<Button
|
<Title order={2}>Hak Akses</Title>
|
||||||
leftSection={<IconUserPlus size={16} />}
|
<Group justify="space-between">
|
||||||
onClick={() => setOpened(true)}
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
>
|
Administrator
|
||||||
Tambah Anggota
|
</Text>
|
||||||
</Button>
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
</Group>
|
2 Orang
|
||||||
|
</Text>
|
||||||
<Table highlightOnHover>
|
</Group>
|
||||||
<Table.Thead>
|
<Group justify="space-between">
|
||||||
<Table.Tr>
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
<Table.Th>Nama</Table.Th>
|
Editor
|
||||||
<Table.Th>Email</Table.Th>
|
</Text>
|
||||||
<Table.Th>Peran</Table.Th>
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
<Table.Th>Status</Table.Th>
|
5 Orang
|
||||||
<Table.Th>Aksi</Table.Th>
|
</Text>
|
||||||
</Table.Tr>
|
</Group>
|
||||||
</Table.Thead>
|
<Group justify="space-between">
|
||||||
<Table.Tbody>
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
{teamMembers.map((member) => (
|
Viewer
|
||||||
<Table.Tr key={member.id}>
|
</Text>
|
||||||
<Table.Td>
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
<Group gap="sm">
|
5 Orang
|
||||||
<IconUser size={20} />
|
</Text>
|
||||||
<Text>{member.name}</Text>
|
</Group>
|
||||||
</Group>
|
</Stack>
|
||||||
</Table.Td>
|
</Box>
|
||||||
<Table.Td>{member.email}</Table.Td>
|
<Box>
|
||||||
<Table.Td>
|
<Stack gap={"xs"}>
|
||||||
<Text fw={500}>{member.role}</Text>
|
<Title order={2}>Kolaborasi</Title>
|
||||||
</Table.Td>
|
<Group mb="md" justify="space-between">
|
||||||
<Table.Td>
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
<Text c={member.status === "Aktif" ? "green" : "red"} fw={500}>
|
Izin Export Data
|
||||||
{member.status}
|
</Text>
|
||||||
</Text>
|
<Switch defaultChecked />
|
||||||
</Table.Td>
|
</Group>
|
||||||
<Table.Td>
|
<Group mb="md" justify="space-between">
|
||||||
<Group>
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
<ActionIcon variant="subtle" color="blue">
|
Require Approval Untuk Perubahan
|
||||||
<IconEdit size={16} />
|
</Text>
|
||||||
</ActionIcon>
|
<Switch defaultChecked />
|
||||||
<ActionIcon variant="subtle" color="red">
|
</Group>
|
||||||
<IconTrash size={16} />
|
</Stack>
|
||||||
</ActionIcon>
|
</Box>
|
||||||
</Group>
|
<Group justify="flex-start" mt="xl">
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
))}
|
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<Space h="xl" />
|
|
||||||
|
|
||||||
<Alert
|
|
||||||
icon={<IconInfoCircle size={16} />}
|
|
||||||
title="Informasi"
|
|
||||||
color="blue"
|
|
||||||
mb="md"
|
|
||||||
>
|
|
||||||
Administrator memiliki akses penuh ke semua fitur. Peran lainnya
|
|
||||||
memiliki akses terbatas sesuai kebutuhan.
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Group justify="flex-end" mt="xl">
|
|
||||||
<Button variant="outline">Batal</Button>
|
<Button variant="outline">Batal</Button>
|
||||||
<Button>Simpan Perubahan</Button>
|
<Button>Simpan Perubahan</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,89 +1,64 @@
|
|||||||
import {
|
import { Box, Button, Group, Stack, Switch, Text, Title } from "@mantine/core";
|
||||||
Alert,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Group,
|
|
||||||
PasswordInput,
|
|
||||||
Space,
|
|
||||||
Switch,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
useMantineColorScheme,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { IconInfoCircle, IconLock } from "@tabler/icons-react";
|
|
||||||
|
|
||||||
const KeamananSettings = () => {
|
const KeamananSettings = () => {
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const dark = colorScheme === "dark";
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Stack pr={"50%"} gap={"xl"}>
|
||||||
withBorder
|
<Box>
|
||||||
radius="md"
|
<Stack gap={"xs"}>
|
||||||
p="xl"
|
<Title order={2}>Autentikasi</Title>
|
||||||
bg={dark ? "#141D34" : "white"}
|
<Group mb="md" justify="space-between">
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
>
|
Two-Factor Authentication
|
||||||
<Title order={2} mb="lg">
|
</Text>
|
||||||
Pengaturan Keamanan
|
<Switch defaultChecked />
|
||||||
</Title>
|
</Group>
|
||||||
<Text color="dimmed" mb="xl">
|
<Group mb="md" justify="space-between">
|
||||||
Kelola keamanan akun Anda
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
</Text>
|
Biometrik Login
|
||||||
|
</Text>
|
||||||
<Space h="lg" />
|
<Switch defaultChecked />
|
||||||
|
</Group>
|
||||||
<PasswordInput
|
<Group mb="md" justify="space-between">
|
||||||
label="Kata Sandi Saat Ini"
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
placeholder="Masukkan kata sandi saat ini"
|
IP Whitelist
|
||||||
mb="md"
|
</Text>
|
||||||
/>
|
<Switch defaultChecked />
|
||||||
|
</Group>
|
||||||
<PasswordInput
|
</Stack>
|
||||||
label="Kata Sandi Baru"
|
</Box>
|
||||||
placeholder="Masukkan kata sandi baru"
|
<Box>
|
||||||
mb="md"
|
<Stack gap={"xs"}>
|
||||||
/>
|
<Title order={2}>Password</Title>
|
||||||
|
<Button bg={"#1E3A5F"} radius={"md"} c={"white"} fullWidth>
|
||||||
<PasswordInput
|
Ubah Password
|
||||||
label="Konfirmasi Kata Sandi Baru"
|
</Button>
|
||||||
placeholder="Konfirmasi kata sandi baru"
|
<Button bg={"#1E3A5F"} radius={"md"} c={"white"} fullWidth>
|
||||||
mb="md"
|
Riwayat Login
|
||||||
/>
|
</Button>
|
||||||
|
<Button bg={"#1E3A5F"} radius={"md"} c={"white"} fullWidth>
|
||||||
<Space h="md" />
|
Perangkat Terdaftar
|
||||||
|
</Button>
|
||||||
<Group mb="md">
|
</Stack>
|
||||||
<Switch label="Verifikasi Dua Langkah" />
|
</Box>
|
||||||
<Switch label="Login Otentikasi Aplikasi" />
|
<Box>
|
||||||
</Group>
|
<Stack gap={"xs"}>
|
||||||
|
<Title order={2}>Audit & Log</Title>
|
||||||
<Space h="md" />
|
<Group mb="md" justify="space-between">
|
||||||
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
<Alert
|
Log Aktivitas
|
||||||
icon={<IconLock size={16} />}
|
</Text>
|
||||||
title="Keamanan"
|
<Switch defaultChecked />
|
||||||
color="orange"
|
</Group>
|
||||||
mb="md"
|
<Button bg={"#1E3A5F"} radius={"md"} c={"white"} fullWidth>
|
||||||
>
|
Download Log
|
||||||
Gunakan kata sandi yang kuat dan unik. Hindari menggunakan kata sandi
|
</Button>
|
||||||
yang sama di banyak layanan.
|
</Stack>
|
||||||
</Alert>
|
</Box>
|
||||||
|
<Group justify="flex-start" mt="xl">
|
||||||
<Alert
|
|
||||||
icon={<IconInfoCircle size={16} />}
|
|
||||||
title="Informasi"
|
|
||||||
color="blue"
|
|
||||||
mb="md"
|
|
||||||
>
|
|
||||||
Setelah mengganti kata sandi, Anda akan diminta logout dari semua
|
|
||||||
perangkat.
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Group justify="flex-end" mt="xl">
|
|
||||||
<Button variant="outline">Batal</Button>
|
<Button variant="outline">Batal</Button>
|
||||||
<Button>Perbarui Kata Sandi</Button>
|
<Button>Simpan Perubahan</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
Grid,
|
||||||
|
GridCol,
|
||||||
Group,
|
Group,
|
||||||
Space,
|
Space,
|
||||||
|
Stack,
|
||||||
Switch,
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
@@ -16,70 +20,101 @@ const NotifikasiSettings = () => {
|
|||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const dark = colorScheme === "dark";
|
const dark = colorScheme === "dark";
|
||||||
return (
|
return (
|
||||||
<Card
|
<Stack pr={"20%"} gap={"xs"}>
|
||||||
withBorder
|
<Grid gutter={{ base: 5, xs: "md", md: "xl", xl: 50 }}>
|
||||||
radius="md"
|
<GridCol span={6}>
|
||||||
p="xl"
|
<Stack gap={"xs"}>
|
||||||
bg={dark ? "#141D34" : "white"}
|
<Title order={3} mb="sm">
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
Metode Notifikasi
|
||||||
>
|
</Title>
|
||||||
<Title order={2} mb="lg">
|
<Group mb="md" justify="space-between">
|
||||||
Pengaturan Notifikasi
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
</Title>
|
Laporan Harian
|
||||||
<Text color="dimmed" mb="xl">
|
</Text>
|
||||||
Kelola preferensi notifikasi Anda
|
<Switch defaultChecked />
|
||||||
</Text>
|
</Group>
|
||||||
|
<Group mb="md" justify="space-between">
|
||||||
<Space h="lg" />
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
|
Alert Sistem
|
||||||
<Checkbox.Group defaultValue={["email", "push"]} mb="md">
|
</Text>
|
||||||
<Title order={4} mb="sm">
|
<Switch defaultChecked />
|
||||||
Metode Notifikasi
|
</Group>
|
||||||
</Title>
|
<Group mb="md" justify="space-between">
|
||||||
<Group>
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
<Checkbox value="email" label="Email" />
|
Update Keamanan
|
||||||
<Checkbox value="push" label="Notifikasi Push" />
|
</Text>
|
||||||
<Checkbox value="sms" label="SMS" />
|
<Switch defaultChecked />
|
||||||
</Group>
|
</Group>
|
||||||
</Checkbox.Group>
|
<Group mb="md" justify="space-between">
|
||||||
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
<Space h="md" />
|
Newsletter Bulanan
|
||||||
|
</Text>
|
||||||
<Group mb="md">
|
<Switch defaultChecked />
|
||||||
<Switch label="Notifikasi Email" defaultChecked />
|
</Group>
|
||||||
<Switch label="Notifikasi Push" defaultChecked />
|
</Stack>
|
||||||
</Group>
|
</GridCol>
|
||||||
|
<GridCol span={6}>
|
||||||
<Space h="md" />
|
<Stack gap={"xs"}>
|
||||||
|
<Title order={3} mb="sm">
|
||||||
<Title order={4} mb="sm">
|
Preferensi Alert
|
||||||
Jenis Notifikasi
|
</Title>
|
||||||
</Title>
|
<Group mb="md" justify="space-between">
|
||||||
<Group align="start">
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
<Switch label="Pengaduan Baru" defaultChecked />
|
Treshold Memori
|
||||||
<Switch label="Update Status Pengaduan" defaultChecked />
|
</Text>
|
||||||
<Switch label="Laporan Mingguan" />
|
<Switch defaultChecked />
|
||||||
<Switch label="Pemberitahuan Keamanan" defaultChecked />
|
</Group>
|
||||||
<Switch label="Aktivitas Akun" defaultChecked />
|
<Group mb="md" justify="space-between">
|
||||||
</Group>
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
|
Treshold CPU
|
||||||
<Space h="md" />
|
</Text>
|
||||||
|
<Switch defaultChecked />
|
||||||
<Alert
|
</Group>
|
||||||
icon={<IconInfoCircle size={16} />}
|
<Group mb="md" justify="space-between">
|
||||||
title="Tip"
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
color="blue"
|
Treshold Disk
|
||||||
mb="md"
|
</Text>
|
||||||
>
|
<Switch defaultChecked />
|
||||||
Anda dapat menyesuaikan frekuensi notifikasi mingguan sesuai kebutuhan
|
</Group>
|
||||||
Anda.
|
</Stack>
|
||||||
</Alert>
|
</GridCol>
|
||||||
|
<GridCol span={6}>
|
||||||
<Group justify="flex-end" mt="xl">
|
<Stack gap={"xs"}>
|
||||||
|
<Title order={3} mb="sm">
|
||||||
|
Notifikasi Push
|
||||||
|
</Title>
|
||||||
|
<Group mb="md" justify="space-between">
|
||||||
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
|
Alert Kritis
|
||||||
|
</Text>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</Group>
|
||||||
|
<Group mb="md" justify="space-between">
|
||||||
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
|
Aktivitas Tim
|
||||||
|
</Text>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</Group>
|
||||||
|
<Group mb="md" justify="space-between">
|
||||||
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
|
Komentar & Mention
|
||||||
|
</Text>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</Group>
|
||||||
|
<Group mb="md" justify="space-between">
|
||||||
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
|
Bunyi Notifikasi
|
||||||
|
</Text>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</GridCol>
|
||||||
|
</Grid>
|
||||||
|
<Group justify="flex-start" mt="xl">
|
||||||
<Button variant="outline">Batal</Button>
|
<Button variant="outline">Batal</Button>
|
||||||
<Button>Simpan Preferensi</Button>
|
<Button>Simpan Preferensi</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,12 @@
|
|||||||
import {
|
import { Box, Button, Group, Select, Switch, Text, Title } from "@mantine/core";
|
||||||
Alert,
|
import { DateInput } from "@mantine/dates";
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Group,
|
|
||||||
Select,
|
|
||||||
Space,
|
|
||||||
Switch,
|
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
Title,
|
|
||||||
useMantineColorScheme,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { IconInfoCircle } from "@tabler/icons-react";
|
|
||||||
|
|
||||||
const UmumSettings = () => {
|
const UmumSettings = () => {
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const dark = colorScheme === "dark";
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Box pr={"50%"}>
|
||||||
withBorder
|
|
||||||
radius="md"
|
|
||||||
p="xl"
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Title order={2} mb="lg">
|
<Title order={2} mb="lg">
|
||||||
Pengaturan Umum
|
Preferensi Tampilan
|
||||||
</Title>
|
</Title>
|
||||||
<Text color="dimmed" mb="xl">
|
|
||||||
Kelola pengaturan umum aplikasi Anda
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Space h="lg" />
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
label="Nama Aplikasi"
|
|
||||||
placeholder="Masukkan nama aplikasi"
|
|
||||||
defaultValue="Dashboard Desa Plus"
|
|
||||||
mb="md"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
label="Bahasa Aplikasi"
|
label="Bahasa Aplikasi"
|
||||||
@@ -61,25 +29,53 @@ const UmumSettings = () => {
|
|||||||
mb="md"
|
mb="md"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Group mb="md">
|
<DateInput label="Format Tanggal" mb={"xl"} />
|
||||||
<Switch label="Notifikasi Email" defaultChecked />
|
|
||||||
|
<Title order={2} mb="lg">
|
||||||
|
Dashboard
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<Group mb="md" justify="space-between">
|
||||||
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
|
Refresh Otomatis
|
||||||
|
</Text>
|
||||||
|
<Switch defaultChecked />
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Alert
|
<Group mb="md" justify="space-between">
|
||||||
icon={<IconInfoCircle size={16} />}
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
title="Informasi"
|
Interval Refresh
|
||||||
color="blue"
|
</Text>
|
||||||
mb="md"
|
<Select
|
||||||
>
|
data={[
|
||||||
Beberapa pengaturan mungkin memerlukan restart aplikasi untuk diterapkan
|
{ value: "1", label: "30d" },
|
||||||
sepenuhnya.
|
{ value: "2", label: "60d" },
|
||||||
</Alert>
|
{ value: "3", label: "90d" },
|
||||||
|
]}
|
||||||
|
defaultValue="1"
|
||||||
|
w={90}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group mb="md" justify="space-between">
|
||||||
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
|
Tampilkan Grid
|
||||||
|
</Text>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group mb="md" justify="space-between">
|
||||||
|
<Text fw={"bold"} fz={"sm"}>
|
||||||
|
Animasi Transisi
|
||||||
|
</Text>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Group justify="flex-end" mt="xl">
|
<Group justify="flex-end" mt="xl">
|
||||||
<Button variant="outline">Batal</Button>
|
<Button variant="outline">Batal</Button>
|
||||||
<Button>Simpan Perubahan</Button>
|
<Button>Simpan Perubahan</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function Sidebar({ className }: SidebarProps) {
|
|||||||
return (
|
return (
|
||||||
<Box className={className}>
|
<Box className={className}>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Image src="/logo-desa-plus.png" alt="Logo" />
|
<Image src={dark ? "/white.png" : "/light-mode.png"} alt="Logo" />
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<Box p="md">
|
<Box p="md">
|
||||||
|
|||||||
@@ -1,463 +1,45 @@
|
|||||||
import {
|
import { Grid, GridCol, Stack } from "@mantine/core";
|
||||||
Badge,
|
import { Beasiswa } from "./sosial/beasiswa";
|
||||||
Card,
|
import { EventCalendar } from "./sosial/event-calendar";
|
||||||
Grid,
|
import { HealthStats } from "./sosial/health-stats";
|
||||||
GridCol,
|
import { Pendidikan } from "./sosial/pendidikan";
|
||||||
Group,
|
import { PosyanduSchedule } from "./sosial/posyandu-schedule";
|
||||||
List,
|
import { SummaryCards } from "./sosial/summary-cards";
|
||||||
Progress,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
ThemeIcon,
|
|
||||||
Title,
|
|
||||||
useMantineColorScheme,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import {
|
|
||||||
IconAward,
|
|
||||||
IconBabyCarriage,
|
|
||||||
IconBook,
|
|
||||||
IconCalendarEvent,
|
|
||||||
IconHeartbeat,
|
|
||||||
IconMedicalCross,
|
|
||||||
IconSchool,
|
|
||||||
IconStethoscope,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
const SosialPage = () => {
|
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 (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
{/* Health Statistics Cards */}
|
{/* Top Summary Cards - 4 Grid */}
|
||||||
|
<SummaryCards />
|
||||||
|
|
||||||
|
{/* Second Row - 2 Column Grid */}
|
||||||
<Grid gutter="md">
|
<Grid gutter="md">
|
||||||
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
|
{/* Left - Statistik Kesehatan */}
|
||||||
<Card
|
<GridCol span={{ base: 12, lg: 6 }}>
|
||||||
p="md"
|
<HealthStats />
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<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>
|
||||||
|
|
||||||
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
|
{/* Right - Jadwal Posyandu */}
|
||||||
<Card
|
<GridCol span={{ base: 12, lg: 6 }}>
|
||||||
p="md"
|
<PosyanduSchedule />
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<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
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<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
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<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>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Health Progress Bars */}
|
{/* Third Row - 2 Column Grid */}
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<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">
|
<Grid gutter="md">
|
||||||
{/* Jadwal Posyandu */}
|
{/* Left - Pendidikan */}
|
||||||
<GridCol span={{ base: 12, lg: 6 }}>
|
<GridCol span={{ base: 12, lg: 6 }}>
|
||||||
<Card
|
<Pendidikan />
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
|
|
||||||
Jadwal Posyandu
|
|
||||||
</Title>
|
|
||||||
<Stack gap="sm">
|
|
||||||
{posyanduSchedule.map((item, index) => (
|
|
||||||
<Card
|
|
||||||
key={index}
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#263852ff" : "#F1F5F9"}
|
|
||||||
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
|
|
||||||
h="100%"
|
|
||||||
>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Stack gap={0}>
|
|
||||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
|
||||||
{item.nama}
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c={dark ? "dark.0" : "black"}>
|
|
||||||
{item.tanggal}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<Badge variant="light" color="darmasaba-blue">
|
|
||||||
{item.jam}
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
</GridCol>
|
||||||
|
|
||||||
{/* Pendidikan */}
|
{/* Right - Beasiswa Desa */}
|
||||||
<GridCol span={{ base: 12, lg: 6 }}>
|
<GridCol span={{ base: 12, lg: 6 }}>
|
||||||
<Card
|
<Beasiswa />
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
h="100%"
|
|
||||||
>
|
|
||||||
<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"
|
|
||||||
bg={dark ? "#263852ff" : "#F1F5F9"}
|
|
||||||
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
|
|
||||||
>
|
|
||||||
<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>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid gutter="md">
|
{/* Bottom Section - Event Budaya */}
|
||||||
{/* Beasiswa Desa */}
|
<EventCalendar />
|
||||||
<GridCol span={{ base: 12, lg: 6 }}>
|
|
||||||
<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"}>
|
|
||||||
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
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
h="100%"
|
|
||||||
>
|
|
||||||
<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>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
76
src/components/sosial/beasiswa.tsx
Normal file
76
src/components/sosial/beasiswa.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
|
Title,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { IconAward } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
interface ScholarshipData {
|
||||||
|
penerima: number;
|
||||||
|
dana: string;
|
||||||
|
tahunAjaran: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BeasiswaProps {
|
||||||
|
data?: ScholarshipData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Beasiswa = ({ data }: BeasiswaProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const defaultData: ScholarshipData = {
|
||||||
|
penerima: 45,
|
||||||
|
dana: "Rp 1.200.000.000",
|
||||||
|
tahunAjaran: "2025/2026",
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayData = data || defaultData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
h={"100%"}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"} fw={500}>
|
||||||
|
Beasiswa Desa
|
||||||
|
</Text>
|
||||||
|
<Text size="xl" fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
Penerima: {displayData.penerima}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<ThemeIcon
|
||||||
|
variant="light"
|
||||||
|
color="darmasaba-success"
|
||||||
|
size="xl"
|
||||||
|
radius="xl"
|
||||||
|
>
|
||||||
|
<IconAward size={24} />
|
||||||
|
</ThemeIcon>
|
||||||
|
</Group>
|
||||||
|
<Stack gap="xs" mt="md">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text c={dark ? "dark.3" : "dimmed"}>Dana Tersalurkan:</Text>
|
||||||
|
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{displayData.dana}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text c={dark ? "dark.3" : "dimmed"}>Tahun Ajaran:</Text>
|
||||||
|
<Text c={dark ? "dark.0" : "#1e3a5f"}>{displayData.tahunAjaran}</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
101
src/components/sosial/event-calendar.tsx
Normal file
101
src/components/sosial/event-calendar.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
|
Title,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { IconCalendarEvent } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
interface EventItem {
|
||||||
|
id: string;
|
||||||
|
nama: string;
|
||||||
|
tanggal: string;
|
||||||
|
lokasi: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventCalendarProps {
|
||||||
|
data?: EventItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EventCalendar = ({ data }: EventCalendarProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const defaultData: EventItem[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
nama: "Hari Kesaktian Pancasila",
|
||||||
|
tanggal: "1 Oktober 2025",
|
||||||
|
lokasi: "Balai Desa",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
nama: "Festival Budaya Desa",
|
||||||
|
tanggal: "20 Mei 2026",
|
||||||
|
lokasi: "Lapangan Desa",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
nama: "Perayaan HUT Desa",
|
||||||
|
tanggal: "17 Agustus 2026",
|
||||||
|
lokasi: "Balai Desa",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayData = data || defaultData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
>
|
||||||
|
<Title order={3} mb="md" c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
Kalender Event Budaya
|
||||||
|
</Title>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{displayData.map((event) => (
|
||||||
|
<Card
|
||||||
|
key={event.id}
|
||||||
|
p="md"
|
||||||
|
radius="md"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#263852ff" : "#F1F5F9"}
|
||||||
|
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
|
||||||
|
hoverable
|
||||||
|
>
|
||||||
|
<Group justify="space-between" mb="xs">
|
||||||
|
<Group gap="sm" align="center">
|
||||||
|
<ThemeIcon
|
||||||
|
color="darmasaba-blue"
|
||||||
|
size="md"
|
||||||
|
radius="xl"
|
||||||
|
variant="light"
|
||||||
|
>
|
||||||
|
<IconCalendarEvent size={16} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text fw={600} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{event.nama}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"} fw={500}>
|
||||||
|
{event.lokasi}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group pl={36}>
|
||||||
|
<Text size="sm" c={dark ? "dark.4" : "gray.6"}>
|
||||||
|
{event.tanggal}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
73
src/components/sosial/health-stats.tsx
Normal file
73
src/components/sosial/health-stats.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Progress,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
|
||||||
|
interface HealthProgressItem {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HealthStatsProps {
|
||||||
|
data?: HealthProgressItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HealthStats = ({ data }: HealthStatsProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const defaultData: HealthProgressItem[] = [
|
||||||
|
{ 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" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayData = data || defaultData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
h={"100%"}
|
||||||
|
>
|
||||||
|
<Title order={3} mb="md" c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
Statistik Kesehatan
|
||||||
|
</Title>
|
||||||
|
<Stack gap="md">
|
||||||
|
{displayData.map((item, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<Group justify="space-between" mb={5}>
|
||||||
|
<Text size="sm" fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
fw={600}
|
||||||
|
c={item.color === "red" ? "red" : dark ? "dark.0" : "#1e3a5f"}
|
||||||
|
>
|
||||||
|
{item.value}%
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Progress
|
||||||
|
value={item.value}
|
||||||
|
size="lg"
|
||||||
|
radius="xl"
|
||||||
|
color={item.color}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
120
src/components/sosial/pendidikan.tsx
Normal file
120
src/components/sosial/pendidikan.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
|
||||||
|
interface EducationData {
|
||||||
|
siswa: {
|
||||||
|
tk: number;
|
||||||
|
sd: number;
|
||||||
|
smp: number;
|
||||||
|
sma: number;
|
||||||
|
};
|
||||||
|
sekolah: {
|
||||||
|
jumlah: number;
|
||||||
|
guru: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PendidikanProps {
|
||||||
|
data?: EducationData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Pendidikan = ({ data }: PendidikanProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const defaultData: EducationData = {
|
||||||
|
siswa: {
|
||||||
|
tk: 125,
|
||||||
|
sd: 480,
|
||||||
|
smp: 210,
|
||||||
|
sma: 150,
|
||||||
|
},
|
||||||
|
sekolah: {
|
||||||
|
jumlah: 8,
|
||||||
|
guru: 42,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayData = data || defaultData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
>
|
||||||
|
<Title order={3} mb="md" c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
Pendidikan
|
||||||
|
</Title>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
TK / PAUD
|
||||||
|
</Text>
|
||||||
|
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{displayData.siswa.tk}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
SD
|
||||||
|
</Text>
|
||||||
|
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{displayData.siswa.sd}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
SMP
|
||||||
|
</Text>
|
||||||
|
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{displayData.siswa.smp}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
SMA
|
||||||
|
</Text>
|
||||||
|
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{displayData.siswa.sma}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
withBorder
|
||||||
|
radius="md"
|
||||||
|
p="md"
|
||||||
|
mt="md"
|
||||||
|
bg={dark ? "#263852ff" : "#F1F5F9"}
|
||||||
|
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
|
||||||
|
>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
Jumlah Lembaga Pendidikan
|
||||||
|
</Text>
|
||||||
|
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{displayData.sekolah.jumlah}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group justify="space-between" mt="sm">
|
||||||
|
<Text fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
Jumlah Tenaga Pengajar
|
||||||
|
</Text>
|
||||||
|
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{displayData.sekolah.guru}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
96
src/components/sosial/posyandu-schedule.tsx
Normal file
96
src/components/sosial/posyandu-schedule.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
|
||||||
|
interface PosyanduItem {
|
||||||
|
id: string;
|
||||||
|
nama: string;
|
||||||
|
tanggal: string;
|
||||||
|
jam: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PosyanduScheduleProps {
|
||||||
|
data?: PosyanduItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PosyanduSchedule = ({ data }: PosyanduScheduleProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const defaultData: PosyanduItem[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
nama: "Posyandu Mawar",
|
||||||
|
tanggal: "Senin, 15 Feb 2026",
|
||||||
|
jam: "08:00 - 11:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
nama: "Posyandu Melati",
|
||||||
|
tanggal: "Selasa, 16 Feb 2026",
|
||||||
|
jam: "08:00 - 11:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
nama: "Posyandu Dahlia",
|
||||||
|
tanggal: "Rabu, 17 Feb 2026",
|
||||||
|
jam: "08:00 - 11:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
nama: "Posyandu Anggrek",
|
||||||
|
tanggal: "Kamis, 18 Feb 2026",
|
||||||
|
jam: "08:00 - 11:00",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayData = data || defaultData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
>
|
||||||
|
<Title order={3} mb="md" c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
Jadwal Posyandu
|
||||||
|
</Title>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{displayData.map((item) => (
|
||||||
|
<Card
|
||||||
|
key={item.id}
|
||||||
|
p="md"
|
||||||
|
radius="md"
|
||||||
|
withBorder
|
||||||
|
bg={dark ? "#263852ff" : "#F1F5F9"}
|
||||||
|
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
|
||||||
|
hoverable
|
||||||
|
>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Text fw={600} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{item.nama}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||||
|
{item.tanggal}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Badge variant="light" color="darmasaba-blue" size="md">
|
||||||
|
{item.jam}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
140
src/components/sosial/summary-cards.tsx
Normal file
140
src/components/sosial/summary-cards.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Grid,
|
||||||
|
GridCol,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import {
|
||||||
|
IconBabyCarriage,
|
||||||
|
IconHeartbeat,
|
||||||
|
IconMedicalCross,
|
||||||
|
IconStethoscope,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
|
interface SummaryCardProps {
|
||||||
|
title: string;
|
||||||
|
value: number;
|
||||||
|
subtitle?: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
color: string;
|
||||||
|
highlight?: boolean;
|
||||||
|
backgroundColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SummaryCard = ({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
subtitle,
|
||||||
|
icon,
|
||||||
|
color,
|
||||||
|
highlight = false,
|
||||||
|
backgroundColor,
|
||||||
|
}: SummaryCardProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"} fw={500}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
size="xl"
|
||||||
|
fw={700}
|
||||||
|
c={highlight ? "red" : dark ? "dark.0" : "#1e3a5f"}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
{subtitle && (
|
||||||
|
<Text size="xs" c={dark ? "dark.4" : "gray.6"}>
|
||||||
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
<ThemeIcon bg={backgroundColor} color={color} size="xl" radius="xl">
|
||||||
|
{icon}
|
||||||
|
</ThemeIcon>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface HealthSummaryData {
|
||||||
|
ibuHamil: number;
|
||||||
|
balita: number;
|
||||||
|
alertStunting: number;
|
||||||
|
posyanduAktif: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SummaryCardsProps {
|
||||||
|
data?: HealthSummaryData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SummaryCards = ({ data }: SummaryCardsProps) => {
|
||||||
|
const defaultData: HealthSummaryData = {
|
||||||
|
ibuHamil: 87,
|
||||||
|
balita: 342,
|
||||||
|
alertStunting: 12,
|
||||||
|
posyanduAktif: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayData = data || defaultData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid gutter="md">
|
||||||
|
<GridCol span={{ base: 12, sm: 6, lg: 3 }}>
|
||||||
|
<SummaryCard
|
||||||
|
title="Ibu Hamil Aktif"
|
||||||
|
value={displayData.ibuHamil}
|
||||||
|
subtitle="Aktif"
|
||||||
|
icon={<IconHeartbeat size={20} />}
|
||||||
|
color="white"
|
||||||
|
backgroundColor="#1E3A5F"
|
||||||
|
/>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol span={{ base: 12, sm: 6, lg: 3 }}>
|
||||||
|
<SummaryCard
|
||||||
|
title="Balita Terdaftar"
|
||||||
|
value={displayData.balita}
|
||||||
|
subtitle="Terdaftar"
|
||||||
|
icon={<IconBabyCarriage size={20} />}
|
||||||
|
color="white"
|
||||||
|
backgroundColor="#1E3A5F"
|
||||||
|
/>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol span={{ base: 12, sm: 6, lg: 3 }}>
|
||||||
|
<SummaryCard
|
||||||
|
title="Alert Stunting"
|
||||||
|
value={displayData.alertStunting}
|
||||||
|
subtitle="Perhatian"
|
||||||
|
icon={<IconStethoscope size={20} />}
|
||||||
|
color="white"
|
||||||
|
backgroundColor="#1E3A5F"
|
||||||
|
/>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol span={{ base: 12, sm: 6, lg: 3 }}>
|
||||||
|
<SummaryCard
|
||||||
|
title="Posyandu Aktif"
|
||||||
|
value={displayData.posyanduAktif}
|
||||||
|
subtitle="Aktif"
|
||||||
|
icon={<IconMedicalCross size={20} />}
|
||||||
|
color="white"
|
||||||
|
backgroundColor="#1E3A5F"
|
||||||
|
/>
|
||||||
|
</GridCol>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -51,9 +51,7 @@ export const HelpCard = ({
|
|||||||
{icon && (
|
{icon && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: isDark
|
backgroundColor: isDark ? "#263852ff" : "#1E3A5F",
|
||||||
? theme.colors.blue[8]
|
|
||||||
: theme.colors.blue[0],
|
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
padding: "8px",
|
padding: "8px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|||||||
75
src/components/umkm/header-toggle.tsx
Normal file
75
src/components/umkm/header-toggle.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Title,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useSnapshot } from "valtio";
|
||||||
|
import { setRange, umkmStore } from "../../store/umkm";
|
||||||
|
|
||||||
|
type TimeRange = "minggu" | "bulan";
|
||||||
|
|
||||||
|
interface HeaderToggleProps {
|
||||||
|
title?: string;
|
||||||
|
onRangeChange?: (range: TimeRange) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HeaderToggle = ({
|
||||||
|
title = "Update Penjualan Produk",
|
||||||
|
onRangeChange,
|
||||||
|
}: HeaderToggleProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
const { selectedRange } = useSnapshot(umkmStore);
|
||||||
|
|
||||||
|
const handleRangeChange = (range: TimeRange) => {
|
||||||
|
setRange(range);
|
||||||
|
onRangeChange?.(range);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#1e3a5f" : "#1e3a5f"}
|
||||||
|
style={{ borderColor: dark ? "#1e3a5f" : "#1e3a5f" }}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="center" px="md" py="xs">
|
||||||
|
<Title order={3} c="white">
|
||||||
|
{title}
|
||||||
|
</Title>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Button
|
||||||
|
variant={selectedRange === "minggu" ? "white" : "transparent"}
|
||||||
|
onClick={() => handleRangeChange("minggu")}
|
||||||
|
c={selectedRange === "minggu" ? "#1e3a5f" : "white"}
|
||||||
|
fw={600}
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
style={{
|
||||||
|
opacity: selectedRange === "minggu" ? 1 : 0.8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Minggu ini
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={selectedRange === "bulan" ? "white" : "transparent"}
|
||||||
|
onClick={() => handleRangeChange("bulan")}
|
||||||
|
c={selectedRange === "bulan" ? "#1e3a5f" : "white"}
|
||||||
|
fw={600}
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
style={{
|
||||||
|
opacity: selectedRange === "bulan" ? 1 : 0.8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Bulan ini
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
100
src/components/umkm/produk-unggulan.tsx
Normal file
100
src/components/umkm/produk-unggulan.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { Card, Group, Stack, Text, useMantineColorScheme } from "@mantine/core";
|
||||||
|
|
||||||
|
interface MetricCardProps {
|
||||||
|
title: string;
|
||||||
|
value: string | number;
|
||||||
|
trend?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const MetricCard = ({ title, value, trend }: MetricCardProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"} fw={500}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Stack gap={0} align="flex-end">
|
||||||
|
<Text size="lg" fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
{trend && (
|
||||||
|
<Text size="xs" c={trend.value >= 0 ? "green" : "red"} fw={600}>
|
||||||
|
{trend.value >= 0 ? "↑" : "↓"} {Math.abs(trend.value)}%{" "}
|
||||||
|
{trend.label}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ProdukUnggulanProps {
|
||||||
|
data?: {
|
||||||
|
totalPenjualan: number;
|
||||||
|
produkAktif: number;
|
||||||
|
totalTransaksi: number;
|
||||||
|
trend?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProdukUnggulan = ({ data }: ProdukUnggulanProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const defaultData = {
|
||||||
|
totalPenjualan: 30900000,
|
||||||
|
produkAktif: 7,
|
||||||
|
totalTransaksi: 500,
|
||||||
|
trend: {
|
||||||
|
value: 18,
|
||||||
|
label: "vs bulan lalu",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayData = data || defaultData;
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
if (value >= 1000000) {
|
||||||
|
return `Rp ${(value / 1000000).toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
if (value >= 1000) {
|
||||||
|
return `Rp ${(value / 1000).toFixed(0)}K`;
|
||||||
|
}
|
||||||
|
return `Rp ${value.toLocaleString()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
>
|
||||||
|
<Stack gap="lg">
|
||||||
|
<MetricCard
|
||||||
|
title="Total Penjualan"
|
||||||
|
value={formatCurrency(displayData.totalPenjualan)}
|
||||||
|
trend={displayData.trend}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
title="Produk Aktif"
|
||||||
|
value={`${displayData.produkAktif} kategori`}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
title="Total Transaksi"
|
||||||
|
value={`${displayData.totalTransaksi} transaksi`}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
260
src/components/umkm/sales-table.tsx
Normal file
260
src/components/umkm/sales-table.tsx
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Select,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { IconArrowDown, IconArrowUp } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
export interface SalesData {
|
||||||
|
id: string;
|
||||||
|
produk: string;
|
||||||
|
penjualanBulanIni: number;
|
||||||
|
bulanLalu: number;
|
||||||
|
trend: number;
|
||||||
|
volume: string;
|
||||||
|
stok: number;
|
||||||
|
unit: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SalesTableProps {
|
||||||
|
data?: SalesData[];
|
||||||
|
onDetailClick?: (product: SalesData) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SalesTable = ({ data, onDetailClick }: SalesTableProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const defaultData: SalesData[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
produk: "Beras Premium Organik",
|
||||||
|
penjualanBulanIni: 8500000,
|
||||||
|
bulanLalu: 7600000,
|
||||||
|
trend: 12,
|
||||||
|
volume: "650 Kg",
|
||||||
|
stok: 850,
|
||||||
|
unit: "Kg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
produk: "Keripik Singkong",
|
||||||
|
penjualanBulanIni: 4200000,
|
||||||
|
bulanLalu: 3800000,
|
||||||
|
trend: 11,
|
||||||
|
volume: "320 Kg",
|
||||||
|
stok: 120,
|
||||||
|
unit: "Kg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
produk: "Madu Alami",
|
||||||
|
penjualanBulanIni: 3750000,
|
||||||
|
bulanLalu: 4100000,
|
||||||
|
trend: -9,
|
||||||
|
volume: "150 Liter",
|
||||||
|
stok: 45,
|
||||||
|
unit: "Liter",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
produk: "Kecap Tradisional",
|
||||||
|
penjualanBulanIni: 2800000,
|
||||||
|
bulanLalu: 2500000,
|
||||||
|
trend: 12,
|
||||||
|
volume: "280 Botol",
|
||||||
|
stok: 95,
|
||||||
|
unit: "Botol",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
produk: "Sambal Bu Rudy",
|
||||||
|
penjualanBulanIni: 2100000,
|
||||||
|
bulanLalu: 2300000,
|
||||||
|
trend: -9,
|
||||||
|
volume: "180 Botol",
|
||||||
|
stok: 35,
|
||||||
|
unit: "Botol",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayData = data || defaultData;
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
if (value >= 1000000) {
|
||||||
|
return `Rp ${(value / 1000000).toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
if (value >= 1000) {
|
||||||
|
return `Rp ${(value / 1000).toFixed(0)}K`;
|
||||||
|
}
|
||||||
|
return `Rp ${value.toLocaleString()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStockStatus = (stock: number) => {
|
||||||
|
if (stock > 200) return { color: "green", label: "Aman" };
|
||||||
|
if (stock > 50) return { color: "yellow", label: "Sedang" };
|
||||||
|
return { color: "red", label: "Rendah" };
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" mb="md">
|
||||||
|
<Title order={4} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
Detail Penjualan Produk
|
||||||
|
</Title>
|
||||||
|
<Group gap="xs">
|
||||||
|
<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={180}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
placeholder="Filter UMKM"
|
||||||
|
data={[
|
||||||
|
{ value: "semua", label: "Semua UMKM" },
|
||||||
|
{ value: "umkm1", label: "Warung Pak Joko" },
|
||||||
|
{ value: "umkm2", label: "Ibu Sari Snack" },
|
||||||
|
]}
|
||||||
|
defaultValue="semua"
|
||||||
|
w={180}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
stickyHeader
|
||||||
|
stickyHeaderOffset={60}
|
||||||
|
highlightOnHover
|
||||||
|
withRowBorders={false}
|
||||||
|
verticalSpacing="sm"
|
||||||
|
>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||||
|
Produk
|
||||||
|
</Text>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||||
|
Penjualan Bulan Ini
|
||||||
|
</Text>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||||
|
Bulan Lalu
|
||||||
|
</Text>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||||
|
Trend
|
||||||
|
</Text>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||||
|
Volume
|
||||||
|
</Text>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||||
|
Stok
|
||||||
|
</Text>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||||
|
Aksi
|
||||||
|
</Text>
|
||||||
|
</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{displayData.map((product) => {
|
||||||
|
const stockStatus = getStockStatus(product.stok);
|
||||||
|
return (
|
||||||
|
<Table.Tr
|
||||||
|
key={product.id}
|
||||||
|
style={{
|
||||||
|
backgroundColor: dark ? "#141D34" : "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table.Td>
|
||||||
|
<Text fw={600} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{product.produk}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{formatCurrency(product.penjualanBulanIni)}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||||
|
{formatCurrency(product.bulanLalu)}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Group gap="xs">
|
||||||
|
{product.trend >= 0 ? (
|
||||||
|
<IconArrowUp size={16} color="green" />
|
||||||
|
) : (
|
||||||
|
<IconArrowDown size={16} color="red" />
|
||||||
|
)}
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
fw={600}
|
||||||
|
c={product.trend >= 0 ? "green" : "red"}
|
||||||
|
>
|
||||||
|
{Math.abs(product.trend)}%
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm" c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{product.volume}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Badge variant="light" color={stockStatus.color} size="sm">
|
||||||
|
{product.stok} {product.unit} ({stockStatus.label})
|
||||||
|
</Badge>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
size="compact-sm"
|
||||||
|
color="darmasaba-blue"
|
||||||
|
radius="xl"
|
||||||
|
onClick={() => onDetailClick?.(product)}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
155
src/components/umkm/summary-cards.tsx
Normal file
155
src/components/umkm/summary-cards.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Card,
|
||||||
|
Grid,
|
||||||
|
GridCol,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import {
|
||||||
|
IconCategory,
|
||||||
|
IconCurrencyDollar,
|
||||||
|
IconTrendingUp,
|
||||||
|
IconUsers,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
|
interface KpiCardProps {
|
||||||
|
title: string;
|
||||||
|
value: string | number;
|
||||||
|
subtitle?: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
color: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KpiCard = ({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
subtitle,
|
||||||
|
icon,
|
||||||
|
color,
|
||||||
|
backgroundColor,
|
||||||
|
}: KpiCardProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const formatValue = (val: string | number) => {
|
||||||
|
if (typeof val === "number") {
|
||||||
|
if (val >= 1000000) {
|
||||||
|
return `${(val / 1000000).toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
if (val >= 1000) {
|
||||||
|
return `${(val / 1000).toFixed(1)}K`;
|
||||||
|
}
|
||||||
|
return val.toLocaleString();
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"} fw={500}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text size="xl" fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{formatValue(value)}
|
||||||
|
</Text>
|
||||||
|
{subtitle && (
|
||||||
|
<Text size="xs" c={dark ? "dark.4" : "gray.6"}>
|
||||||
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
<Avatar
|
||||||
|
color={color}
|
||||||
|
bg={backgroundColor}
|
||||||
|
size={40}
|
||||||
|
radius="xl"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</Avatar>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SummaryCardsProps {
|
||||||
|
data?: {
|
||||||
|
umkmAktif: number;
|
||||||
|
umkmTerdaftar: number;
|
||||||
|
omzet: number;
|
||||||
|
kategoriTerbanyak: { count: number; name: string };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SummaryCards = ({ data }: SummaryCardsProps) => {
|
||||||
|
const defaultData = {
|
||||||
|
umkmAktif: 45,
|
||||||
|
umkmTerdaftar: 68,
|
||||||
|
omzet: 48000000,
|
||||||
|
kategoriTerbanyak: { count: 34, name: "Kuliner" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayData = data || defaultData;
|
||||||
|
|
||||||
|
const kpiData: KpiCardProps[] = [
|
||||||
|
{
|
||||||
|
title: "UMKM Aktif",
|
||||||
|
value: displayData.umkmAktif,
|
||||||
|
subtitle: "Beroperasi",
|
||||||
|
icon: <IconCurrencyDollar size={25} />,
|
||||||
|
color: "white",
|
||||||
|
backgroundColor: "#1E3A5F",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "UMKM Terdaftar",
|
||||||
|
value: displayData.umkmTerdaftar,
|
||||||
|
subtitle: "Total registrasi",
|
||||||
|
icon: <IconUsers size={25} />,
|
||||||
|
color: "white",
|
||||||
|
backgroundColor: "#1E3A5F",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Omzet",
|
||||||
|
value: displayData.omzet,
|
||||||
|
subtitle: "Omzet BUMDes per bulan",
|
||||||
|
icon: <IconTrendingUp size={25} />,
|
||||||
|
color: "white",
|
||||||
|
backgroundColor: "#1E3A5F",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "UMKM Terbanyak",
|
||||||
|
value: displayData.kategoriTerbanyak.count,
|
||||||
|
subtitle: `Kategori ${displayData.kategoriTerbanyak.name}`,
|
||||||
|
icon: <IconTrendingUp size={25} />,
|
||||||
|
color: "white",
|
||||||
|
backgroundColor: "#1E3A5F",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid gutter="md">
|
||||||
|
{kpiData.map((kpi, index) => (
|
||||||
|
<GridCol key={index} span={{ base: 12, sm: 6, lg: 3 }}>
|
||||||
|
<KpiCard {...kpi} />
|
||||||
|
</GridCol>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
140
src/components/umkm/top-products.tsx
Normal file
140
src/components/umkm/top-products.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
|
||||||
|
interface TopProduct {
|
||||||
|
rank: number;
|
||||||
|
name: string;
|
||||||
|
umkmName: string;
|
||||||
|
revenue: number;
|
||||||
|
quantitySold: number;
|
||||||
|
trend: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TopProductsProps {
|
||||||
|
products?: TopProduct[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
if (value >= 1000000) {
|
||||||
|
return `${(value / 1000000).toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
if (value >= 1000) {
|
||||||
|
return `${(value / 1000).toFixed(0)}K`;
|
||||||
|
}
|
||||||
|
return value.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatNumber = (value: number) => {
|
||||||
|
if (value >= 1000) {
|
||||||
|
return `${(value / 1000).toFixed(1)}K`;
|
||||||
|
}
|
||||||
|
return value.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TopProducts = ({ products }: TopProductsProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const defaultProducts: TopProduct[] = [
|
||||||
|
{
|
||||||
|
rank: 1,
|
||||||
|
name: "Beras Premium Organik",
|
||||||
|
umkmName: "Warung Pak Joko",
|
||||||
|
revenue: 8500000,
|
||||||
|
quantitySold: 650,
|
||||||
|
trend: 12,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rank: 2,
|
||||||
|
name: "Keripik Singkong",
|
||||||
|
umkmName: "Ibu Sari Snack",
|
||||||
|
revenue: 4200000,
|
||||||
|
quantitySold: 320,
|
||||||
|
trend: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rank: 3,
|
||||||
|
name: "Madu Alami",
|
||||||
|
umkmName: "Peternakan Lebah",
|
||||||
|
revenue: 3750000,
|
||||||
|
quantitySold: 150,
|
||||||
|
trend: 5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayProducts = products || defaultProducts;
|
||||||
|
|
||||||
|
const getRankColor = (rank: number) => {
|
||||||
|
if (rank === 1) return "yellow";
|
||||||
|
if (rank === 2) return "gray";
|
||||||
|
if (rank === 3) return "orange";
|
||||||
|
return "blue";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
p="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
shadow="sm"
|
||||||
|
bg={dark ? "#141D34" : "white"}
|
||||||
|
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||||
|
>
|
||||||
|
<Title order={4} mb="md" c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
Top 3 Produk Terlaris
|
||||||
|
</Title>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{displayProducts.map((product) => (
|
||||||
|
<Group key={product.rank} justify="space-between" align="center">
|
||||||
|
<Group gap="sm">
|
||||||
|
<Badge
|
||||||
|
variant="filled"
|
||||||
|
color={getRankColor(product.rank)}
|
||||||
|
radius="xl"
|
||||||
|
size="lg"
|
||||||
|
w={30}
|
||||||
|
h={30}
|
||||||
|
>
|
||||||
|
{product.rank}
|
||||||
|
</Badge>
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Text fw={600} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||||
|
{product.name}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||||
|
{product.umkmName}
|
||||||
|
</Text>
|
||||||
|
<Group gap="xs" mt={2}>
|
||||||
|
<Text size="xs" c={dark ? "dark.4" : "gray.6"}>
|
||||||
|
Rp {formatCurrency(product.revenue)}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c={dark ? "dark.4" : "gray.6"}>
|
||||||
|
•
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c={dark ? "dark.4" : "gray.6"}>
|
||||||
|
{formatNumber(product.quantitySold)} terjual
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
<Badge
|
||||||
|
variant="light"
|
||||||
|
color={product.trend >= 0 ? "green" : "red"}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{product.trend >= 0 ? "+" : ""}
|
||||||
|
{product.trend}%
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
35
src/hooks/use-sidebar-fullscreen.ts
Normal file
35
src/hooks/use-sidebar-fullscreen.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function useSidebarFullscreen() {
|
||||||
|
const [opened, { toggle: toggleMobile }] = useDisclosure();
|
||||||
|
const [sidebarCollapsed, setSidebarCollapsed] = useDisclosure(false);
|
||||||
|
const [clickCount, setClickCount] = useState(0);
|
||||||
|
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
setSidebarCollapsed.toggle();
|
||||||
|
setClickCount(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMainClick = () => {
|
||||||
|
if (!sidebarCollapsed) {
|
||||||
|
const newCount = clickCount + 1;
|
||||||
|
setClickCount(newCount);
|
||||||
|
|
||||||
|
if (newCount === 2) {
|
||||||
|
toggleSidebar();
|
||||||
|
} else {
|
||||||
|
setTimeout(() => setClickCount(0), 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
isCollapsed: sidebarCollapsed,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,16 +1,22 @@
|
|||||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { Header } from "@/components/header";
|
import { Header } from "@/components/header";
|
||||||
import HelpPage from "@/components/help-page";
|
import HelpPage from "@/components/help-page";
|
||||||
import { Sidebar } from "@/components/sidebar";
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
|
||||||
export const Route = createFileRoute("/bantuan")({
|
export const Route = createFileRoute("/bantuan")({
|
||||||
component: BantuanPage,
|
component: BantuanRoute,
|
||||||
});
|
});
|
||||||
|
|
||||||
function BantuanPage() {
|
function BantuanRoute() {
|
||||||
const [opened, { toggle }] = useDisclosure();
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||||
@@ -22,14 +28,19 @@ function BantuanPage() {
|
|||||||
navbar={{
|
navbar={{
|
||||||
width: 300,
|
width: 300,
|
||||||
breakpoint: "sm",
|
breakpoint: "sm",
|
||||||
collapsed: { mobile: !opened },
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
}}
|
}}
|
||||||
padding="md"
|
padding="md"
|
||||||
>
|
>
|
||||||
<AppShell.Header bg={headerBgColor}>
|
<AppShell.Header bg={headerBgColor}>
|
||||||
<Group h="100%" px="md">
|
<Group h="100%" px="md">
|
||||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
<Burger
|
||||||
<Header />
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
</Group>
|
</Group>
|
||||||
</AppShell.Header>
|
</AppShell.Header>
|
||||||
|
|
||||||
@@ -43,7 +54,11 @@ function BantuanPage() {
|
|||||||
</div>
|
</div>
|
||||||
</AppShell.Navbar>
|
</AppShell.Navbar>
|
||||||
|
|
||||||
<AppShell.Main bg={mainBgColor}>
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
<HelpPage />
|
<HelpPage />
|
||||||
</AppShell.Main>
|
</AppShell.Main>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
|||||||
@@ -1,9 +1,66 @@
|
|||||||
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import BumdesPage from "@/components/bumdes-page";
|
||||||
|
import { Header } from "@/components/header";
|
||||||
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
|
||||||
export const Route = createFileRoute("/bumdes")({
|
export const Route = createFileRoute("/bumdes")({
|
||||||
component: RouteComponent,
|
component: BumdesRoute,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function BumdesRoute() {
|
||||||
return <div>Hello "/bumdes"!</div>;
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
|
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 }}
|
||||||
|
navbar={{
|
||||||
|
width: 300,
|
||||||
|
breakpoint: "sm",
|
||||||
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
|
}}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Header bg={headerBgColor}>
|
||||||
|
<Group h="100%" px="md">
|
||||||
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
|
<AppShell.Navbar
|
||||||
|
p="md"
|
||||||
|
bg={navbarBgColor}
|
||||||
|
style={{ display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
|
||||||
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
|
<BumdesPage />
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { Header } from "@/components/header";
|
import { Header } from "@/components/header";
|
||||||
import { Sidebar } from "@/components/sidebar";
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
import DemografiPekerjaan from "../components/demografi-pekerjaan";
|
import DemografiPekerjaan from "../components/demografi-pekerjaan";
|
||||||
|
|
||||||
export const Route = createFileRoute("/demografi-pekerjaan")({
|
export const Route = createFileRoute("/demografi-pekerjaan")({
|
||||||
@@ -10,7 +10,13 @@ export const Route = createFileRoute("/demografi-pekerjaan")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function DemografiPekerjaanPage() {
|
function DemografiPekerjaanPage() {
|
||||||
const [opened, { toggle }] = useDisclosure();
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||||
@@ -22,14 +28,19 @@ function DemografiPekerjaanPage() {
|
|||||||
navbar={{
|
navbar={{
|
||||||
width: 300,
|
width: 300,
|
||||||
breakpoint: "sm",
|
breakpoint: "sm",
|
||||||
collapsed: { mobile: !opened },
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
}}
|
}}
|
||||||
padding="md"
|
padding="md"
|
||||||
>
|
>
|
||||||
<AppShell.Header bg={headerBgColor}>
|
<AppShell.Header bg={headerBgColor}>
|
||||||
<Group h="100%" px="md">
|
<Group h="100%" px="md">
|
||||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
<Burger
|
||||||
<Header />
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
</Group>
|
</Group>
|
||||||
</AppShell.Header>
|
</AppShell.Header>
|
||||||
|
|
||||||
@@ -43,7 +54,11 @@ function DemografiPekerjaanPage() {
|
|||||||
</div>
|
</div>
|
||||||
</AppShell.Navbar>
|
</AppShell.Navbar>
|
||||||
|
|
||||||
<AppShell.Main bg={mainBgColor}>
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
<DemografiPekerjaan />
|
<DemografiPekerjaan />
|
||||||
</AppShell.Main>
|
</AppShell.Main>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { useState } from "react";
|
||||||
import { DashboardContent } from "@/components/dashboard-content";
|
import { DashboardContent } from "@/components/dashboard-content";
|
||||||
import { Header } from "@/components/header";
|
import { Header } from "@/components/header";
|
||||||
import { Sidebar } from "@/components/sidebar";
|
import { Sidebar } from "@/components/sidebar";
|
||||||
@@ -11,25 +12,41 @@ export const Route = createFileRoute("/")({
|
|||||||
|
|
||||||
function DashboardPage() {
|
function DashboardPage() {
|
||||||
const [opened, { toggle }] = useDisclosure();
|
const [opened, { toggle }] = useDisclosure();
|
||||||
|
const [sidebarCollapsed, setSidebarCollapsed] = useDisclosure(false);
|
||||||
|
const [clickCount, setClickCount] = useState(0);
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||||
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||||
|
|
||||||
|
const handleMainClick = () => {
|
||||||
|
if (!sidebarCollapsed) {
|
||||||
|
const newCount = clickCount + 1;
|
||||||
|
setClickCount(newCount);
|
||||||
|
|
||||||
|
if (newCount === 2) {
|
||||||
|
setSidebarCollapsed.toggle();
|
||||||
|
setClickCount(0);
|
||||||
|
} else {
|
||||||
|
setTimeout(() => setClickCount(0), 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
header={{ height: 60 }}
|
header={{ height: 60 }}
|
||||||
navbar={{
|
navbar={{
|
||||||
width: 300,
|
width: 300,
|
||||||
breakpoint: "sm",
|
breakpoint: "sm",
|
||||||
collapsed: { mobile: !opened },
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
}}
|
}}
|
||||||
padding="md"
|
padding="md"
|
||||||
>
|
>
|
||||||
<AppShell.Header bg={headerBgColor}>
|
<AppShell.Header bg={headerBgColor}>
|
||||||
<Group h="100%" px="md">
|
<Group h="100%" px="md">
|
||||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||||
<Header />
|
<Header onSidebarToggle={setSidebarCollapsed.toggle} />
|
||||||
</Group>
|
</Group>
|
||||||
</AppShell.Header>
|
</AppShell.Header>
|
||||||
|
|
||||||
@@ -43,7 +60,11 @@ function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</AppShell.Navbar>
|
</AppShell.Navbar>
|
||||||
|
|
||||||
<AppShell.Main bg={mainBgColor}>
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
<DashboardContent />
|
<DashboardContent />
|
||||||
</AppShell.Main>
|
</AppShell.Main>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
|||||||
@@ -1,9 +1,66 @@
|
|||||||
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { Header } from "@/components/header";
|
||||||
|
import JennaAnalytic from "@/components/jenna-analytic";
|
||||||
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
|
||||||
export const Route = createFileRoute("/jenna-analytic")({
|
export const Route = createFileRoute("/jenna-analytic")({
|
||||||
component: RouteComponent,
|
component: JennaAnalyticPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function JennaAnalyticPage() {
|
||||||
return <div>Hello "/jenna-analytic"!</div>;
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
|
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 }}
|
||||||
|
navbar={{
|
||||||
|
width: 300,
|
||||||
|
breakpoint: "sm",
|
||||||
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
|
}}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Header bg={headerBgColor}>
|
||||||
|
<Group h="100%" px="md">
|
||||||
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
|
<AppShell.Navbar
|
||||||
|
p="md"
|
||||||
|
bg={navbarBgColor}
|
||||||
|
style={{ display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
|
||||||
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
|
<JennaAnalytic />
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,66 @@
|
|||||||
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { Header } from "@/components/header";
|
||||||
|
import KeamananPage from "@/components/keamanan-page";
|
||||||
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
|
||||||
export const Route = createFileRoute("/keamanan")({
|
export const Route = createFileRoute("/keamanan")({
|
||||||
component: RouteComponent,
|
component: KeamananRoute,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function KeamananRoute() {
|
||||||
return <div>Hello "/keamanan"!</div>;
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
|
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 }}
|
||||||
|
navbar={{
|
||||||
|
width: 300,
|
||||||
|
breakpoint: "sm",
|
||||||
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
|
}}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Header bg={headerBgColor}>
|
||||||
|
<Group h="100%" px="md">
|
||||||
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
|
<AppShell.Navbar
|
||||||
|
p="md"
|
||||||
|
bg={navbarBgColor}
|
||||||
|
style={{ display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
|
||||||
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
|
<KeamananPage />
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { Header } from "@/components/header";
|
import { Header } from "@/components/header";
|
||||||
import KeuanganAnggaran from "@/components/keuangan-anggaran";
|
import KeuanganAnggaran from "@/components/keuangan-anggaran";
|
||||||
import { Sidebar } from "@/components/sidebar";
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
|
||||||
export const Route = createFileRoute("/keuangan-anggaran")({
|
export const Route = createFileRoute("/keuangan-anggaran")({
|
||||||
component: KeuanganAnggaranPage,
|
component: KeuanganAnggaranPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
function KeuanganAnggaranPage() {
|
function KeuanganAnggaranPage() {
|
||||||
const [opened, { toggle }] = useDisclosure();
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||||
@@ -22,14 +28,19 @@ function KeuanganAnggaranPage() {
|
|||||||
navbar={{
|
navbar={{
|
||||||
width: 300,
|
width: 300,
|
||||||
breakpoint: "sm",
|
breakpoint: "sm",
|
||||||
collapsed: { mobile: !opened },
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
}}
|
}}
|
||||||
padding="md"
|
padding="md"
|
||||||
>
|
>
|
||||||
<AppShell.Header bg={headerBgColor}>
|
<AppShell.Header bg={headerBgColor}>
|
||||||
<Group h="100%" px="md">
|
<Group h="100%" px="md">
|
||||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
<Burger
|
||||||
<Header />
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
</Group>
|
</Group>
|
||||||
</AppShell.Header>
|
</AppShell.Header>
|
||||||
|
|
||||||
@@ -43,7 +54,11 @@ function KeuanganAnggaranPage() {
|
|||||||
</div>
|
</div>
|
||||||
</AppShell.Navbar>
|
</AppShell.Navbar>
|
||||||
|
|
||||||
<AppShell.Main bg={mainBgColor}>
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
<KeuanganAnggaran />
|
<KeuanganAnggaran />
|
||||||
</AppShell.Main>
|
</AppShell.Main>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { Header } from "@/components/header";
|
import { Header } from "@/components/header";
|
||||||
import KinerjaDivisi from "@/components/kinerja-divisi";
|
import KinerjaDivisi from "@/components/kinerja-divisi";
|
||||||
import { Sidebar } from "@/components/sidebar";
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
|
||||||
export const Route = createFileRoute("/kinerja-divisi")({
|
export const Route = createFileRoute("/kinerja-divisi")({
|
||||||
component: KinerjaDivisiPage,
|
component: KinerjaDivisiPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
function KinerjaDivisiPage() {
|
function KinerjaDivisiPage() {
|
||||||
const [opened, { toggle }] = useDisclosure();
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||||
@@ -22,14 +28,19 @@ function KinerjaDivisiPage() {
|
|||||||
navbar={{
|
navbar={{
|
||||||
width: 300,
|
width: 300,
|
||||||
breakpoint: "sm",
|
breakpoint: "sm",
|
||||||
collapsed: { mobile: !opened },
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
}}
|
}}
|
||||||
padding="md"
|
padding="md"
|
||||||
>
|
>
|
||||||
<AppShell.Header bg={headerBgColor}>
|
<AppShell.Header bg={headerBgColor}>
|
||||||
<Group h="100%" px="md">
|
<Group h="100%" px="md">
|
||||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
<Burger
|
||||||
<Header />
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
</Group>
|
</Group>
|
||||||
</AppShell.Header>
|
</AppShell.Header>
|
||||||
|
|
||||||
@@ -43,7 +54,11 @@ function KinerjaDivisiPage() {
|
|||||||
</div>
|
</div>
|
||||||
</AppShell.Navbar>
|
</AppShell.Navbar>
|
||||||
|
|
||||||
<AppShell.Main bg={mainBgColor}>
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
<KinerjaDivisi />
|
<KinerjaDivisi />
|
||||||
</AppShell.Main>
|
</AppShell.Main>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { Header } from "@/components/header";
|
import { Header } from "@/components/header";
|
||||||
import PengaduanLayananPublik from "@/components/pengaduan-layanan-publik";
|
import PengaduanLayananPublik from "@/components/pengaduan-layanan-publik";
|
||||||
import { Sidebar } from "@/components/sidebar";
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
|
||||||
export const Route = createFileRoute("/pengaduan-layanan-publik")({
|
export const Route = createFileRoute("/pengaduan-layanan-publik")({
|
||||||
component: PengaduanLayananPublikPage,
|
component: PengaduanLayananPublikPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
function PengaduanLayananPublikPage() {
|
function PengaduanLayananPublikPage() {
|
||||||
const [opened, { toggle }] = useDisclosure();
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||||
@@ -22,14 +28,19 @@ function PengaduanLayananPublikPage() {
|
|||||||
navbar={{
|
navbar={{
|
||||||
width: 300,
|
width: 300,
|
||||||
breakpoint: "sm",
|
breakpoint: "sm",
|
||||||
collapsed: { mobile: !opened },
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
}}
|
}}
|
||||||
padding="md"
|
padding="md"
|
||||||
>
|
>
|
||||||
<AppShell.Header bg={headerBgColor}>
|
<AppShell.Header bg={headerBgColor}>
|
||||||
<Group h="100%" px="md">
|
<Group h="100%" px="md">
|
||||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
<Burger
|
||||||
<Header />
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
</Group>
|
</Group>
|
||||||
</AppShell.Header>
|
</AppShell.Header>
|
||||||
|
|
||||||
@@ -43,7 +54,11 @@ function PengaduanLayananPublikPage() {
|
|||||||
</div>
|
</div>
|
||||||
</AppShell.Navbar>
|
</AppShell.Navbar>
|
||||||
|
|
||||||
<AppShell.Main bg={mainBgColor}>
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
<PengaduanLayananPublik />
|
<PengaduanLayananPublik />
|
||||||
</AppShell.Main>
|
</AppShell.Main>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import AksesDanTimSettings from "@/components/pengaturan/akses-dan-tim";
|
||||||
|
|
||||||
export const Route = createFileRoute("/pengaturan/akses-dan-tim")({
|
export const Route = createFileRoute("/pengaturan/akses-dan-tim")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
return <div>Hello "/pengaturan/akses-dan-tim"!</div>;
|
return <AksesDanTimSettings />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import KeamananSettings from "@/components/pengaturan/keamanan";
|
||||||
|
|
||||||
export const Route = createFileRoute("/pengaturan/keamanan")({
|
export const Route = createFileRoute("/pengaturan/keamanan")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
return <div>Hello "/pengaturan/keamanan"!</div>;
|
return <KeamananSettings />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import NotifikasiSettings from "@/components/pengaturan/notifikasi";
|
||||||
|
|
||||||
export const Route = createFileRoute("/pengaturan/notifikasi")({
|
export const Route = createFileRoute("/pengaturan/notifikasi")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
return <div>Hello "/pengaturan/notifikasi"!</div>;
|
return <NotifikasiSettings />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
import { useDisclosure, useMediaQuery } from "@mantine/hooks";
|
import { useMediaQuery } from "@mantine/hooks";
|
||||||
import {
|
import {
|
||||||
createFileRoute,
|
createFileRoute,
|
||||||
Outlet,
|
Outlet,
|
||||||
@@ -8,13 +8,20 @@ import {
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Header } from "@/components/header";
|
import { Header } from "@/components/header";
|
||||||
import { Sidebar } from "@/components/sidebar";
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
|
||||||
export const Route = createFileRoute("/pengaturan")({
|
export const Route = createFileRoute("/pengaturan")({
|
||||||
component: PengaturanLayout,
|
component: PengaturanLayout,
|
||||||
});
|
});
|
||||||
|
|
||||||
function PengaturanLayout() {
|
function PengaturanLayout() {
|
||||||
const [opened, { toggle, close }] = useDisclosure();
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
|
||||||
const isMobile = useMediaQuery("(max-width: 48em)");
|
const isMobile = useMediaQuery("(max-width: 48em)");
|
||||||
@@ -27,9 +34,9 @@ function PengaturanLayout() {
|
|||||||
// Auto close navbar on route change (mobile only)
|
// Auto close navbar on route change (mobile only)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMobile && opened) {
|
if (isMobile && opened) {
|
||||||
close();
|
toggleMobile();
|
||||||
}
|
}
|
||||||
}, [routerState.location.pathname, isMobile, opened, close]);
|
}, [routerState.location.pathname, isMobile, opened, toggleMobile]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
@@ -37,14 +44,19 @@ function PengaturanLayout() {
|
|||||||
navbar={{
|
navbar={{
|
||||||
width: 300,
|
width: 300,
|
||||||
breakpoint: "sm",
|
breakpoint: "sm",
|
||||||
collapsed: { mobile: !opened },
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
}}
|
}}
|
||||||
padding="md"
|
padding="md"
|
||||||
>
|
>
|
||||||
<AppShell.Header bg={headerBgColor}>
|
<AppShell.Header bg={headerBgColor}>
|
||||||
<Group h="100%" px="lg" align="center" wrap="nowrap">
|
<Group h="100%" px="lg" align="center" wrap="nowrap">
|
||||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
<Burger
|
||||||
<Header />
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
</Group>
|
</Group>
|
||||||
</AppShell.Header>
|
</AppShell.Header>
|
||||||
|
|
||||||
@@ -58,7 +70,11 @@ function PengaturanLayout() {
|
|||||||
</div>
|
</div>
|
||||||
</AppShell.Navbar>
|
</AppShell.Navbar>
|
||||||
|
|
||||||
<AppShell.Main bg={mainBgColor}>
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,66 @@
|
|||||||
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { Header } from "@/components/header";
|
||||||
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import SosialPage from "@/components/sosial-page";
|
||||||
|
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
|
||||||
|
|
||||||
export const Route = createFileRoute("/sosial")({
|
export const Route = createFileRoute("/sosial")({
|
||||||
component: RouteComponent,
|
component: SosialRoute,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function SosialRoute() {
|
||||||
return <div>Hello "/sosial"!</div>;
|
const {
|
||||||
|
opened,
|
||||||
|
toggleMobile,
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
handleMainClick,
|
||||||
|
} = useSidebarFullscreen();
|
||||||
|
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 }}
|
||||||
|
navbar={{
|
||||||
|
width: 300,
|
||||||
|
breakpoint: "sm",
|
||||||
|
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
|
||||||
|
}}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Header bg={headerBgColor}>
|
||||||
|
<Group h="100%" px="md">
|
||||||
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={toggleMobile}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Header onSidebarToggle={toggleSidebar} />
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
|
<AppShell.Navbar
|
||||||
|
p="md"
|
||||||
|
bg={navbarBgColor}
|
||||||
|
style={{ display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
|
||||||
|
<AppShell.Main
|
||||||
|
bg={mainBgColor}
|
||||||
|
onClick={handleMainClick}
|
||||||
|
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
|
||||||
|
>
|
||||||
|
<SosialPage />
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/store/sosial.ts
Normal file
30
src/store/sosial.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { proxy } from "valtio";
|
||||||
|
|
||||||
|
type SelectedYear = string;
|
||||||
|
|
||||||
|
interface SosialState {
|
||||||
|
selectedYear: SelectedYear;
|
||||||
|
filters: {
|
||||||
|
dusun: string | null;
|
||||||
|
kategori: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sosialStore = proxy<SosialState>({
|
||||||
|
selectedYear: new Date().getFullYear().toString(),
|
||||||
|
filters: {
|
||||||
|
dusun: null,
|
||||||
|
kategori: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setYear = (year: SelectedYear) => {
|
||||||
|
sosialStore.selectedYear = year;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setFilter = (
|
||||||
|
key: keyof SosialState["filters"],
|
||||||
|
value: string | null,
|
||||||
|
) => {
|
||||||
|
sosialStore.filters[key] = value;
|
||||||
|
};
|
||||||
30
src/store/umkm.ts
Normal file
30
src/store/umkm.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { proxy } from "valtio";
|
||||||
|
|
||||||
|
type TimeRange = "minggu" | "bulan";
|
||||||
|
|
||||||
|
interface UmkmState {
|
||||||
|
selectedRange: TimeRange;
|
||||||
|
filters: {
|
||||||
|
kategori: string | null;
|
||||||
|
umkm: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const umkmStore = proxy<UmkmState>({
|
||||||
|
selectedRange: "bulan",
|
||||||
|
filters: {
|
||||||
|
kategori: null,
|
||||||
|
umkm: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setRange = (range: TimeRange) => {
|
||||||
|
umkmStore.selectedRange = range;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setFilter = (
|
||||||
|
key: keyof UmkmState["filters"],
|
||||||
|
value: string | null,
|
||||||
|
) => {
|
||||||
|
umkmStore.filters[key] = value;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user