Compare commits

...

17 Commits

Author SHA1 Message Date
158a2db435 Fix New UI Pengaduan 2026-03-17 21:41:03 +07:00
2d68d4dc06 Fix New UI Kinerja Divisi 2026-03-17 21:19:10 +07:00
97e6caa332 Fix UI Beranda 2026-03-17 21:03:36 +07:00
f0c37272b9 Progress Tampilan UI Dashboard Desa Plus NOC 2026-03-17 20:53:33 +07:00
8c35d58b38 refactor: modularize kinerja-divisi components per PromptDashboard.md
- Create ActivityCard component for program kegiatan cards
- Create DivisionList component for divisi teraktif with arrow icons
- Create DocumentChart component for bar chart (jumlah dokumen)
- Create ProgressChart component for pie chart (progres kegiatan)
- Create DiscussionPanel component for diskusi internal
- Create EventCard component for agenda hari ini
- Create ArchiveCard component for arsip digital perangkat desa
- Refactor main KinerjaDivisi component to use new modular components
- Implement responsive 3-column grid layout
- Add proper dark mode support with specified colors
- Add hover effects and smooth animations

New components structure:
  src/components/kinerja-divisi/
    - activity-card.tsx
    - archive-card.tsx
    - discussion-panel.tsx
    - division-list.tsx
    - document-chart.tsx
    - event-card.tsx
    - progress-chart.tsx
    - index.ts (exports)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-13 16:47:19 +08:00
952f7ecb16 refactor: modularize dashboard components per PromptDashboard.md
- Create reusable StatCard component for header metrics
- Create ChartSurat component for bar chart (surat statistics)
- Create DivisionProgress component for divisi teraktif
- Create ChartAPBDes component for APBDes horizontal bar chart
- Create ActivityList component for calendar events
- Create SatisfactionChart component for donut chart
- Create SDGSCard component for SDGs metrics
- Refactor DashboardContent to use new modular components
- Add proper dark mode support with specified colors
- Implement responsive grid layout (12/6/1 columns)
- Add custom SDGs icons (Energy, Peace, Health, Poverty, Ocean)

New components structure:
  src/components/dashboard/
    - stat-card.tsx
    - chart-surat.tsx
    - chart-apbdes.tsx
    - division-progress.tsx
    - activity-list.tsx
    - satisfaction-chart.tsx
    - sdgs-card.tsx
    - index.ts (exports)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-13 15:36:31 +08:00
a74e0c02e5 fix: resolve merge conflict in pengaturan/route.tsx
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-13 14:41:22 +08:00
17ecd3feca fix: make dashboard public and remove admin-only restriction from main pages
- Make homepage (/) accessible without authentication
- Allow all authenticated users (user & admin) to access main pages:
  - /kinerja-divisi, /pengaduan, /jenna, /demografi
  - /keuangan, /bumdes, /sosial, /keamanan
  - /bantuan, /pengaturan
- Reserve admin-only access for /admin/* routes
- Update auth middleware to handle public routes properly

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-13 14:21:30 +08:00
d88cf2b100 Merge pull request #5 from bipprojectbali/nico/26-feb-26/fix-seed-2
Fix Gambar
2026-03-12 15:21:17 +08:00
e0955ed2c4 Merge branch 'stg' into nico/26-feb-26/fix-seed-2 2026-03-12 15:21:05 +08:00
918399bf62 Fix Gambar 2026-03-12 15:16:41 +08:00
7ce2eb6ae8 Merge pull request #4 from bipprojectbali/nico/27-feb-26/fix-gambar
Add Deploy
2026-03-12 14:47:12 +08:00
40772859f9 Merge branch 'stg' into nico/27-feb-26/fix-gambar 2026-03-12 14:47:03 +08:00
c7b34b8c28 Add Deploy 2026-03-12 14:40:15 +08:00
9bf73a305c Merge pull request #3 from bipprojectbali/fix-ui-berantakan
fix: production build CSS dan responsive layout untuk staging
2026-03-12 12:19:33 +08:00
947adc1537 fix: production build CSS dan responsive layout untuk staging
- Tambah scripts/build.ts untuk build CSS via PostCSS/Tailwind
- Update package.json build script untuk gunakan build script baru
- Fix responsive grid di sosial-page (lg -> md breakpoint)
- Tambah padding responsive untuk mobile display
- Convert inline styles ke Tailwind classes untuk konsistensi
- Update tailwind.config.js content paths
- Tambah CSS variables di index.css untuk color palette
- Update Dockerfile untuk gunakan build script baru

Fixes: tampilan berantakan di staging karena CSS tidak ter-build dengan benar

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-12 12:16:57 +08:00
9086e28961 Merge pull request #2 from bipprojectbali/nico/11-mar-26/fix-ui-to-figma
Nico/11 mar 26/fix UI to figma
2026-03-11 15:29:54 +08:00
68 changed files with 4810 additions and 3809 deletions

View File

@@ -29,7 +29,7 @@ RUN bun x prisma generate
# Generate API types
RUN bun run gen:api
# Build the application frontend
# Build the application frontend using our custom build script
RUN bun run build
# Stage 2: Runtime

139
Kinerja-Divisi-New.md Normal file
View File

@@ -0,0 +1,139 @@
Create a modern admin dashboard UI for a village management system using React 19 + Vite + TailwindCSS + Mantine components + Recharts.
Design style:
- Clean, soft UI with rounded corners (2xl)
- Light gray background (#f5f6f8)
- Card-based layout with subtle shadow
- Smooth spacing and consistent padding
- Professional government-style but still modern
- Use Inter or system font
- Primary color: dark blue
- Accent color: orange for progress
- Success color: green
- Use Mantine components where possible
Layout:
- Responsive grid layout (desktop-first)
- 4 summary cards on top (horizontal)
- 2 columns main content below
- Left sidebar for division list
- Right content for charts and activity
---
## 🔹 TOP CARDS (4 ITEMS)
Each card contains:
- Title (e.g: "Rakor 2025")
- Progress bar (orange)
- Date (small text)
- Status badge "Selesai" (green)
Use:
- Mantine Card
- Mantine Progress
- Mantine Badge
---
## 🔹 LEFT PANEL - "Divisi teraktif"
Vertical list of divisions:
- Each item is clickable
- Show division name + number of activities
- Rounded container with hover effect
- Chevron icon on right
Example items:
- Kesejahteraan (37 kegiatan)
- Pemerintahan (26 kegiatan)
- Keuangan (17 kegiatan)
- etc
Use:
- Scrollable container
- Soft border + hover highlight
---
## 🔹 CENTER - BAR CHART (Jumlah Dokumen)
- Use Recharts
- Two bars:
- Gambar
- Dokumen
- Color:
- Yellow/orange
- Green
- Show Y axis scale (0400)
---
## 🔹 RIGHT - PIE CHART (Progres Kegiatan)
- Use Recharts PieChart
- Segments:
- Selesai (green ~83%)
- Dikerjakan (orange ~16%)
- Segera dikerjakan (blue)
- Dibatalkan (red)
- Include legend below
---
## 🔹 BOTTOM LEFT - Diskusi Panel
- List of discussion messages
- Each item:
- Title
- Sender name
- Date
- Styled like notification cards
- Compact and clean
---
## 🔹 BOTTOM RIGHT - "Acara Hari Ini"
- Empty state
- Show text: "Tidak ada acara hari ini"
- Centered, muted text
---
## ⚙️ TECH REQUIREMENTS
- Use React functional components
- Use TanStack Router (file-based or route config)
- Use Mantine for UI components
- Use Tailwind for layout and spacing
- Use Recharts for charts
- State management: Valtio (simple global state)
- Date formatting: dayjs
- Icons: lucide-react
---
## 📁 COMPONENT STRUCTURE
- components/
- DashboardCard.tsx
- DivisionList.tsx
- BarChartCard.tsx
- PieChartCard.tsx
- DiscussionList.tsx
- EmptyState.tsx
- routes/
- dashboard.tsx
---
## ✨ EXTRA (IMPORTANT FOR VIBE CODING)
- Add subtle hover animations (scale 1.02)
- Smooth transitions (150200ms)
- Keep spacing consistent (gap-4 / gap-6)
- Avoid clutter, prioritize readability
- Make it feel "calm and productive"
---
Output:
- Full React component code (modular, not monolithic)
- Clean, readable, production-ready
- No unnecessary comments

168
Pengaduan-New.md Normal file
View 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: 16px24px (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: 3060
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

302
PromptDashboard.md Normal file
View File

@@ -0,0 +1,302 @@
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

58
scripts/build.ts Normal file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bun
/**
* Build script for production
* 1. Build CSS with PostCSS/Tailwind
* 2. Bundle JS with Bun (without CSS)
* 3. Replace CSS reference in HTML
*/
import { $ } from "bun";
import fs from "node:fs";
import postcss from "postcss";
import tailwindcss from "@tailwindcss/postcss";
import autoprefixer from "autoprefixer";
console.log("🔨 Starting production build...");
// Ensure dist directory exists
if (!fs.existsSync("./dist")) {
fs.mkdirSync("./dist", { recursive: true });
}
// Step 1: Build CSS with PostCSS
console.log("🎨 Building CSS...");
const cssInput = fs.readFileSync("./src/index.css", "utf-8");
const cssResult = await postcss([tailwindcss(), autoprefixer()]).process(
cssInput,
{
from: "./src/index.css",
to: "./dist/index.css",
},
);
fs.writeFileSync("./dist/index.css", cssResult.css);
console.log("✅ CSS built successfully!");
// Step 2: Build JS with Bun (build HTML too, we'll fix CSS link later)
console.log("📦 Bundling JavaScript...");
await $`bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='"production"' --env='VITE_*'`;
// Step 3: Copy public assets
console.log("📁 Copying public assets...");
if (fs.existsSync("./public")) {
await $`cp -r public/* dist/ 2>/dev/null || true`;
}
// Step 4: Ensure HTML references the correct CSS
// Bun build might have renamed the CSS, we want to use our own index.css
console.log("🔧 Fixing HTML CSS reference...");
const htmlPath = "./dist/index.html";
if (fs.existsSync(htmlPath)) {
let html = fs.readFileSync(htmlPath, "utf-8");
// Replace any bundled CSS reference with our index.css
html = html.replace(/href="[^"]*\.css"/g, 'href="/index.css"');
fs.writeFileSync(htmlPath, html);
}
console.log("✅ Build completed successfully!");

View File

@@ -1,13 +1,23 @@
import {
Badge,
Button,
Card,
Grid,
GridCol,
Group,
Select,
Stack,
Table,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import {
IconBuildingStore,
IconCategory,
IconCurrency,
IconUsers,
IconTrendingUp,
IconTrendingDown,
IconChevronDown,
} from "@tabler/icons-react";
import { useMantineColorScheme } from "@mantine/core";
import { useState } from "react";
const BumdesPage = () => {
@@ -15,98 +25,69 @@ const BumdesPage = () => {
const dark = colorScheme === "dark";
const [timeFilter, setTimeFilter] = useState<string>("bulan");
const [categoryFilter, setCategoryFilter] = useState<string>("semua");
// KPI Data
// Sample data for KPI cards
const kpiData = [
{
title: "UMKM Aktif",
value: "45",
subtitle: "Beroperasi",
icon: IconUsers,
value: 45,
icon: <IconUsers size={24} />,
color: "darmasaba-blue",
},
{
title: "UMKM Terdaftar",
value: "68",
subtitle: "Total terdaftar",
icon: IconBuildingStore,
value: 68,
icon: <IconBuildingStore size={24} />,
color: "darmasaba-success",
},
{
title: "Omzet",
value: "48 JT",
subtitle: "Bulan ini",
icon: IconCurrency,
value: "Rp 48.000.000",
icon: <IconCurrency size={24} />,
color: "darmasaba-warning",
},
{
title: "Kategori UMKM",
value: "34",
subtitle: "Jenis produk",
icon: IconCategory,
value: 34,
icon: <IconCategory size={24} />,
color: "darmasaba-danger",
},
];
// Mini stats data
const miniStats = [
{
title: "Total Penjualan",
value: "Rp 30.900.000",
subtitle: "+18% vs bulan lalu",
isPositive: true,
},
{
title: "Produk Aktif",
value: "7",
subtitle: "Kategori produk",
},
{
title: "Total Transaksi",
value: "500",
subtitle: "Transaksi bulan ini",
},
];
// Top 3 products data
// Sample data for top products
const topProducts = [
{
rank: 1,
name: "Beras Premium Organik",
umkmOwner: "Kelompok Tani Subak",
sales: "Rp 8.500.000",
volume: "650 Kg Terjual",
growth: "+15%",
umkmOwner: "Warung Pak Joko",
growth: "+12%",
},
{
rank: 2,
name: "Keripik Singkong",
umkmOwner: "Ibu Sari Snack",
sales: "Rp 4.200.000",
volume: "320 Kg Terjual",
growth: "+8%",
},
{
rank: 3,
name: "Madu Alami",
umkmOwner: "Peternakan Lebah",
sales: "Rp 3.750.000",
volume: "150 Liter Terjual",
growth: "+5%",
},
];
// Product sales data
// Sample data for product sales
const productSales = [
{
produk: "Beras Premium Organik",
umkm: "Kelompok Tani Subak",
penjualanBulanIni: "Rp 8.500.000",
bulanLalu: "Rp 7.400.000",
trend: 15,
bulanLalu: "Rp 8.500.000",
trend: 10,
volume: "650 Kg",
stok: "850 Kg",
},
{
produk: "Keripik Singkong",
umkm: "Ibu Sari Snack",
penjualanBulanIni: "Rp 4.200.000",
bulanLalu: "Rp 3.800.000",
trend: 10,
@@ -115,7 +96,6 @@ const BumdesPage = () => {
},
{
produk: "Madu Alami",
umkm: "Peternakan Lebah",
penjualanBulanIni: "Rp 3.750.000",
bulanLalu: "Rp 4.100.000",
trend: -8,
@@ -124,7 +104,6 @@ const BumdesPage = () => {
},
{
produk: "Kecap Tradisional",
umkm: "Bu Darmi",
penjualanBulanIni: "Rp 2.800.000",
bulanLalu: "Rp 2.500.000",
trend: 12,
@@ -133,414 +112,277 @@ const BumdesPage = () => {
},
];
const cardStyle = {
backgroundColor: dark ? "#1E293B" : "white",
border: `1px solid ${dark ? "#1E293B" : "white"}`,
};
const textStyle = {
color: dark ? "white" : "#1F2937",
};
const subtitleStyle = {
color: dark ? "#9CA3AF" : "#6B7280",
};
return (
<div
className="min-h-screen"
style={{ backgroundColor: dark ? "#0F172A" : "#F3F4F6" }}
>
<div className="max-w-7xl mx-auto">
{/* Row 1: Top 4 Metrics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
{kpiData.map((kpi, index) => (
<div
key={index}
className="rounded-xl shadow-sm p-6"
style={cardStyle}
<Stack gap="lg">
{/* KPI Cards */}
<Grid gutter="md">
{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" }}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<h3
className="text-sm font-medium mb-1"
style={subtitleStyle}
>
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{kpi.title}
</h3>
<p
className="text-3xl font-bold mb-1"
style={textStyle}
>
{kpi.value}
</p>
<p
className="text-xs"
style={subtitleStyle}
>
{kpi.subtitle}
</p>
</div>
<div className="flex-shrink-0 ml-4">
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-white"
style={{ backgroundColor: "#1F3A5F" }}
>
<kpi.icon size={24} />
</div>
</div>
</div>
</div>
))}
</div>
</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>
{/* Row 2: Sales Update Header */}
<div
className="rounded-xl shadow-sm mb-6 overflow-hidden"
style={cardStyle}
>
<div
className="px-6 py-4 flex items-center justify-between"
style={{ backgroundColor: "#1F3A5F" }}
>
<h3 className="text-lg font-semibold text-white">
Update Penjualan Produk
</h3>
<div className="flex items-center gap-2">
<button
onClick={() => setTimeFilter("minggu")}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
timeFilter === "minggu"
? "bg-white text-[#1F3A5F]"
: "bg-white/20 text-white hover:bg-white/30"
}`}
>
Minggu ini
</button>
<button
onClick={() => setTimeFilter("bulan")}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
timeFilter === "bulan"
? "bg-white text-[#1F3A5F]"
: "bg-white/20 text-white hover:bg-white/30"
}`}
>
Bulan ini
</button>
</div>
</div>
</div>
{/* Row 3: Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-10 gap-6">
{/* Left Column (30%) */}
<div className="lg:col-span-3 space-y-6">
{/* Produk Unggulan Section */}
<div
className="rounded-xl shadow-sm p-6"
style={cardStyle}
{/* Update Penjualan Produk Header */}
<Card
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"
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Produk Unggulan
</h3>
Minggu ini
</Button>
<Button
variant={timeFilter === "bulan" ? "filled" : "light"}
onClick={() => setTimeFilter("bulan")}
color="darmasaba-blue"
>
Bulan ini
</Button>
</Group>
</Group>
</Card>
{/* Mini Stats Cards */}
<div className="space-y-4 mb-6">
{miniStats.map((stat, index) => (
<div
key={index}
className="p-4 rounded-lg"
style={{
backgroundColor: dark ? "#334155" : "#F9FAFB",
}}
<Grid gutter="md">
{/* Produk Unggulan (Left Column) */}
<GridCol span={{ base: 12, lg: 4 }}>
<Stack gap="md">
{/* Total Penjualan, Produk Aktif, Total Transaksi */}
<Card
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"
>
<p
className="text-sm font-medium mb-1"
style={subtitleStyle}
<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"}
>
{stat.title}
</p>
<p
className="text-xl font-bold"
style={textStyle}
>
{stat.value}
</p>
{stat.subtitle && (
<p
className={`text-xs mt-1 ${
stat.isPositive
? "text-green-500"
: subtitleStyle.color
}`}
style={
stat.isPositive
? { color: "#22C55E" }
: subtitleStyle
{product.growth}
</Badge>
</Group>
))}
</Stack>
</Card>
</Stack>
</GridCol>
{/* Detail Penjualan Produk (Right Column) */}
<GridCol span={{ base: 12, lg: 8 }}>
<Card
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"
}
>
{stat.subtitle}
</p>
)}
</div>
{product.stok}
</Badge>
</Table.Td>
<Table.Td>
<Button
variant="subtle"
size="compact-sm"
color="darmasaba-blue"
>
Detail
</Button>
</Table.Td>
</Table.Tr>
))}
</div>
{/* Top 3 Products */}
<h4
className="text-base font-semibold mb-4"
style={textStyle}
>
Top 3 Produk Terlaris
</h4>
<div className="space-y-4">
{topProducts.map((product) => (
<div
key={product.rank}
className="flex items-start gap-3 p-3 rounded-lg"
style={{
backgroundColor: dark ? "#334155" : "#F9FAFB",
}}
>
<div
className="w-8 h-8 rounded-full flex items-center justify-center text-white font-bold text-sm flex-shrink-0"
style={{
backgroundColor:
product.rank === 1
? "#FFD700"
: product.rank === 2
? "#C0C0C0"
: "#CD7F32",
}}
>
#{product.rank}
</div>
<div className="flex-1">
<p
className="text-sm font-medium"
style={textStyle}
>
{product.name}
</p>
<p
className="text-xs mt-1"
style={subtitleStyle}
>
{product.umkmOwner}
</p>
<div className="flex items-center justify-between mt-2">
<p className="text-xs font-medium text-green-500">
{product.sales}
</p>
<p
className="text-xs"
style={{ color: "#22C55E" }}
>
{product.growth}
</p>
</div>
<p className="text-xs mt-1" style={subtitleStyle}>
{product.volume}
</p>
</div>
</div>
))}
</div>
</div>
</div>
{/* Right Column (70%) */}
<div className="lg:col-span-7">
<div
className="rounded-xl shadow-sm p-6"
style={cardStyle}
>
<div className="flex items-center justify-between mb-6">
<h3
className="text-lg font-semibold"
style={textStyle}
>
Detail Penjualan Produk
</h3>
<div className="relative">
<select
value={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value)}
className="appearance-none px-4 py-2 pr-8 rounded-lg text-sm font-medium border-0 focus:ring-2 focus:ring-[#1F3A5F] cursor-pointer"
style={{
backgroundColor: dark ? "#334155" : "#F9FAFB",
color: dark ? "white" : "#1F2937",
}}
>
<option value="semua">Semua Kategori</option>
<option value="makanan">Makanan</option>
<option value="minuman">Minuman</option>
<option value="kerajinan">Kerajinan</option>
</select>
<IconChevronDown
size={16}
className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none"
style={{ color: dark ? "#9CA3AF" : "#6B7280" }}
/>
</div>
</div>
{/* Data Table */}
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr
style={{
borderBottom: `2px solid ${dark ? "#334155" : "#E5E7EB"}`,
}}
>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Produk
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Penjualan Bulan Ini
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Bulan Lalu
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Trend
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Volume
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Stok
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Aksi
</th>
</tr>
</thead>
<tbody>
{productSales.map((product, index) => (
<tr
key={index}
style={{
borderBottom: `1px solid ${dark ? "#334155" : "#F3F4F6"}`,
}}
>
<td className="py-4 px-4">
<p
className="text-sm font-medium"
style={textStyle}
>
{product.produk}
</p>
<p
className="text-xs mt-1"
style={subtitleStyle}
>
{product.umkm}
</p>
</td>
<td className="py-4 px-4">
<p
className="text-sm font-medium"
style={textStyle}
>
{product.penjualanBulanIni}
</p>
</td>
<td className="py-4 px-4">
<p
className="text-sm"
style={subtitleStyle}
>
{product.bulanLalu}
</p>
</td>
<td className="py-4 px-4">
<div
className="flex items-center gap-1 text-sm font-medium"
style={{
color: product.trend >= 0 ? "#22C55E" : "#EF4444",
}}
>
{product.trend >= 0 ? (
<IconTrendingUp size={16} />
) : (
<IconTrendingDown size={16} />
)}
{product.trend >= 0 ? "+" : ""}
{product.trend}%
</div>
</td>
<td className="py-4 px-4">
<p
className="text-sm"
style={textStyle}
>
{product.volume}
</p>
</td>
<td className="py-4 px-4">
<span
className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${
parseInt(product.stok) > 200
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{product.stok}
</span>
</td>
<td className="py-4 px-4">
<button
className="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
style={{
backgroundColor: "#1F3A5F",
color: "white",
}}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = "#2d4a6f")
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = "#1F3A5F")
}
>
Detail
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</Table.Tbody>
</Table>
</Card>
</GridCol>
</Grid>
</Stack>
);
};

View File

@@ -1,516 +1,117 @@
import {
Briefcase,
Calendar,
CheckCircle,
FileText,
MessageCircle,
Users,
} from "lucide-react";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
Pie,
PieChart,
ResponsiveContainer,
Tooltip, // Added Tooltip import
XAxis,
YAxis,
} from "recharts";
// Import Mantine components
import {
ActionIcon,
Badge,
Box,
Card, // Added for icon containers
Grid,
Group,
Image,
Progress,
SimpleGrid,
Stack,
Text,
ThemeIcon,
Title,
useMantineColorScheme, // Add this import
} from "@mantine/core";
const barChartData = [
{ month: "Jan", value: 145 },
{ month: "Feb", value: 165 },
{ month: "Mar", value: 195 },
{ month: "Apr", value: 155 },
{ month: "Mei", value: 205 },
{ month: "Jun", value: 185 },
];
const pieChartData = [
{ name: "Puas", value: 25 },
{ name: "Cukup", value: 25 },
{ name: "Kurang", value: 25 },
{ name: "Sangat puas", value: 25 },
];
const COLORS = ["#4E5BA6", "#F4C542", "#8CC63F", "#E57373"];
const divisiData = [
{ name: "Kesejahteraan", value: 37 },
{ name: "Pemerintahan", value: 26 },
{ name: "Keuangan", value: 17 },
{ name: "Sekretaris Desa", value: 15 },
];
const eventData = [
{ date: "1 Oktober 2025", title: "Hari Kesaktian Pancasila" },
{ date: "15 Oktober 2025", title: "Davest" },
{ date: "19 Oktober 2025", title: "Rapat Koordinasi" },
];
const apbdesData = [
{ name: "Belanja", value: 390, label: "390M" },
{ name: "Pendapatan", value: 470, label: "470M" },
{ name: "Pembiayaan", value: 290, label: "290M" },
];
import { Grid, Image, Stack, useMantineColorScheme } from "@mantine/core";
import { CheckCircle, FileText, MessageCircle, Users } from "lucide-react";
import { ActivityList } from "./dashboard/activity-list";
import { ChartAPBDes } from "./dashboard/chart-apbdes";
import { ChartSurat } from "./dashboard/chart-surat";
import { DivisionProgress } from "./dashboard/division-progress";
import { SatisfactionChart } from "./dashboard/satisfaction-chart";
import { SDGSCard } from "./dashboard/sdgs-card";
import { StatCard } from "./dashboard/stat-card";
const sdgsData = [
{ label: "Desa Berenergi Bersih Dan Terbarukan", value: 99.64, image: "/SDGS-7.png" },
{ label: "Desa Damai Berkeadilan", value: 78.65, image: "/SDGS-16.png" },
{ label: "Desa Sehat Dan Sejahtera", value: 77.37, image: "/SDGS-3.png" },
{ label: "Desa Tanpa Kemiskinan", value: 52.62, image: "/SDGS-1.png" }
{
title: "Desa Berenergi Bersih dan Terbarukan",
score: 99.64,
image: "SDGS-7.png",
},
{
title: "Desa Damai Berkeadilan",
score: 78.65,
image: "SDGS-16.png",
},
{
title: "Desa Sehat dan Sejahtera",
score: 77.37,
image: "SDGS-3.png",
},
{
title: "Desa Tanpa Kemiskinan",
score: 52.62,
image: "SDGS-1.png",
},
];
export function DashboardContent() {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return (
<Stack gap="lg">
{/* Stats Cards */}
{/* Header Metrics - 4 Stat Cards */}
<Grid gutter="md">
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
<Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
h="100%"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Group justify="space-between" align="flex-start" w="100%">
<Box style={{ flex: 1 }}>
<Text size="sm" c="dimmed" mb="xs">
Surat Minggu Ini
</Text>
<Group align="baseline" gap="xs">
<Text size="xl" fw={700}>
99
</Text>
</Group>
<Text size="sm" c="dimmed" mt="xs">
14 baru, 14 diproses
</Text>
<Text size="sm" c="red" mt="xs">
12% dari minggu lalu +12%
</Text>
</Box>
<ThemeIcon
variant="filled"
size="xl"
radius="xl"
color={dark ? "gray" : "darmasaba-navy"}
>
<FileText style={{ width: "70%", height: "70%" }} />
</ThemeIcon>
</Group>
</Card>
<StatCard
title="Surat Minggu Ini"
value={99}
detail="14 baru, 14 diproses"
trend="12% dari minggu lalu ↗ +12%"
trendValue={12}
icon={<FileText style={{ width: "70%", height: "70%" }} />}
/>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
<Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
h="100%"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Group justify="space-between" align="flex-start" w="100%">
<Box style={{ flex: 1 }}>
<Text size="sm" c="dimmed" mb="xs">
Pengaduan Aktif
</Text>
<Group align="baseline" gap="xs">
<Text size="xl" fw={700}>
28
</Text>
</Group>
<Text size="sm" c="dimmed" mt="xs">
14 baru, 14 diproses
</Text>
</Box>
<ThemeIcon
variant="filled"
size="xl"
radius="xl"
color={dark ? "gray" : "darmasaba-navy"}
>
<MessageCircle style={{ width: "70%", height: "70%" }} />
</ThemeIcon>
</Group>
</Card>
<StatCard
title="Pengaduan Aktif"
value={28}
detail="14 baru, 14 diproses"
icon={<MessageCircle style={{ width: "70%", height: "70%" }} />}
/>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
<Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
h="100%"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Group justify="space-between" align="flex-start" w="100%">
<Box style={{ flex: 1 }}>
<Text size="sm" c="dimmed" mb="xs">
Layanan Selesai
</Text>
<Group align="baseline" gap="xs">
<Text size="xl" fw={700}>
156
</Text>
</Group>
<Text size="sm" c="dimmed" mt="xs">
bulan ini
</Text>
<Text size="sm" c="red" mt="xs">
+8%
</Text>
</Box>
<ThemeIcon
variant="filled"
size="xl"
radius="xl"
color={dark ? "gray" : "darmasaba-navy"}
>
<CheckCircle style={{ width: "70%", height: "70%" }} />
</ThemeIcon>
</Group>
</Card>
<StatCard
title="Layanan Selesai"
value={156}
detail="bulan ini"
trend="+8%"
trendValue={8}
icon={<CheckCircle style={{ width: "70%", height: "70%" }} />}
/>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
<Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
h="100%"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Group justify="space-between" align="flex-start" w="100%">
<Box style={{ flex: 1 }}>
<Text size="sm" c="dimmed" mb="xs">
Kepuasan Warga
</Text>
<Group align="baseline" gap="xs">
<Text size="xl" fw={700}>
87.2%
</Text>
</Group>
<Text size="sm" c="dimmed" mt="xs">
dari 482 responden
</Text>
</Box>
<ThemeIcon
variant="filled"
size="xl"
radius="xl"
color={dark ? "gray" : "darmasaba-navy"}
>
<Users style={{ width: "70%", height: "70%" }} />
</ThemeIcon>
</Group>
</Card>
<StatCard
title="Kepuasan Warga"
value="87.2%"
detail="dari 482 responden"
icon={<Users style={{ width: "70%", height: "70%" }} />}
/>
</Grid.Col>
</Grid>
{/* Section 2: Chart & Division Progress */}
<Grid gutter="lg">
{/* Bar Chart */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Group justify="space-between" mb="md">
<Box>
<Title order={4} mb={5}>
Statistik Pengajuan Surat
</Title>
<Text size="sm" c="dimmed">
Trend pengajuan surat 6 bulan terakhir
</Text>
</Box>
<ActionIcon variant="subtle" size="lg" radius="md">
{/* Original SVG converted to a generic Icon placeholder */}
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 5L13 10L8 15"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</ActionIcon>
</Group>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={barChartData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke="var(--mantine-color-gray-3)"
/>
<XAxis
dataKey="month"
axisLine={false}
tickLine={false}
tick={{ fill: "var(--mantine-color-text)" }}
/>
<YAxis
axisLine={false}
tickLine={false}
ticks={[0, 55, 110, 165, 220]}
tick={{ fill: "var(--mantine-color-text)" }}
/>
<Tooltip />
<Bar
dataKey="value"
fill="#1E3A5F"
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</Card>
<Grid.Col span={{ base: 12, lg: 7 }}>
<ChartSurat />
</Grid.Col>
{/* Pie Chart */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Title order={4} mb={5}>
Tingkat Kepuasan
</Title>
<Text size="sm" c="dimmed" mb="md">
Tingkat kepuasan layanan
</Text>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={pieChartData}
cx="50%"
cy="50%"
innerRadius={80}
outerRadius={120}
paddingAngle={2}
dataKey="value"
>
{pieChartData.map((_entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index]} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
<Group justify="center" gap="md" mt="md">
<Group gap="xs">
<Box
w={12}
h={12}
style={{ backgroundColor: COLORS[0], borderRadius: "50%" }}
/>
<Text size="sm">Sangat puas (0%)</Text>
</Group>
<Group gap="xs">
<Box
w={12}
h={12}
style={{ backgroundColor: COLORS[1], borderRadius: "50%" }}
/>
<Text size="sm">Puas (0%)</Text>
</Group>
<Group gap="xs">
<Box
w={12}
h={12}
style={{ backgroundColor: COLORS[2], borderRadius: "50%" }}
/>
<Text size="sm">Cukup (0%)</Text>
</Group>
<Group gap="xs">
<Box
w={12}
h={12}
style={{ backgroundColor: COLORS[3], borderRadius: "50%" }}
/>
<Text size="sm">Kurang (0%)</Text>
</Group>
</Group>
</Card>
<Grid.Col span={{ base: 12, lg: 5 }}>
<SatisfactionChart />
</Grid.Col>
</Grid>
{/* Bottom Section */}
{/* Section 3: APBDes Chart */}
<Grid gutter="lg">
{/* Divisi Teraktif */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Group gap="xs" mb="lg">
<Box>
{/* Original SVG icon */}
<Briefcase color="#1E3A5F" />
</Box>
<Title order={4}>Divisi Teraktif</Title>
</Group>
<Stack gap="sm">
{divisiData.map((divisi, index) => (
<Box key={index}>
<Group justify="space-between" mb={5}>
<Text size="sm" fw={500}>
{divisi.name}
</Text>
<Text size="sm" fw={600}>
{divisi.value} Kegiatan
</Text>
</Group>
<Progress
value={(divisi.value / 37) * 100}
size="sm"
radius="xl"
color="#1E3A5F"
/>
</Box>
))}
</Stack>
</Card>
<Grid.Col span={{ base: 12, lg: 7 }}>
<DivisionProgress />
</Grid.Col>
{/* Kalender */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Group gap="xs" mb="lg">
<Calendar style={{ width: 20, height: 20 }} />
<Title order={4}>Kalender & Kegiatan Mendatang</Title>
</Group>
<Stack gap="md">
{eventData.map((event, index) => (
<Box
key={index}
style={{
borderLeft: "4px solid #1E3A5F",
paddingLeft: 12,
}}
>
<Text size="sm" c="dimmed">
{event.date}
</Text>
<Text fw={500}>{event.title}</Text>
</Box>
))}
</Stack>
</Card>
<Grid.Col span={{ base: 12, lg: 5 }}>
<ActivityList />
{/* <SatisfactionChart /> */}
</Grid.Col>
</Grid>
{/* APBDes Chart */}
<Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Title order={4} mb="lg">
Grafik APBDes
</Title>
<Stack gap="xs">
{apbdesData.map((data, index) => (
<Grid key={index} align="center">
<Grid.Col span={3}>
<Text size="sm" fw={500}>
{data.name}
</Text>
</Grid.Col>
<Grid.Col span={7}>
<Progress
value={(data.value / 470) * 100}
size="lg"
radius="xl"
color="#1E3A5F"
/>
</Grid.Col>
<Grid.Col span={2}>
<Text size="sm" fw={600} ta="right">
{data.label}
</Text>
</Grid.Col>
</Grid>
))}
</Stack>
</Card>
<ChartAPBDes />
{/* SDGS Desa */}
<Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Title order={4} mb="lg">
SDGS Desa
</Title>
<SimpleGrid cols={{ base: 2, md: 5 }}>
{sdgsData.map((data, index) => (
<Card key={index} withBorder bg={dark ? "#141D34" : "white"} p="md">
<Group gap="sm" align="center">
<Image src={data.image} width={40} height={40} />
<Box>
<Text size="sm" ta={"center"} fw={500} lineClamp={2}>
{data.label}
</Text>
<Text size="sm" ta={"center"} fw={600} c="darmasaba-blue">
{data.value}
</Text>
</Box>
</Group>
</Card>
))}
</SimpleGrid>
</Card>
{/* Section 6: SDGs Desa Cards */}
<Grid gutter="md">
{sdgsData.map((sdg, index) => (
<Grid.Col key={index} span={{ base: 9, md: 3 }}>
<SDGSCard
image={<Image src={sdg.image} alt={sdg.title} />}
title={sdg.title}
score={sdg.score}
/>
</Grid.Col>
))}
</Grid>
</Stack>
);
}

View File

@@ -0,0 +1,70 @@
import {
Box,
Card,
Group,
Stack,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import { Calendar } from "lucide-react";
interface EventData {
date: string;
title: string;
}
const events: EventData[] = [
{ date: "1 Oktober 2025", title: "Hari Kesaktian Pancasila" },
{ date: "15 Oktober 2025", title: "Davest" },
{ date: "19 Oktober 2025", title: "Rapat Koordinasi" },
];
export function ActivityList() {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return (
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: dark
? "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">
<Calendar
style={{ width: 20, height: 20 }}
color={dark ? "#E2E8F0" : "#1E3A5F"}
/>
<Title order={4} c={dark ? "white" : "gray.9"}>
Kalender & Kegiatan Mendatang
</Title>
</Group>
<Stack gap="md">
{events.map((event, index) => (
<Box
key={index}
style={{
borderLeft: "4px solid var(--mantine-color-blue-filled)",
paddingLeft: 12,
}}
>
<Text size="sm" c="dimmed">
{event.date}
</Text>
<Text fw={500} c={dark ? "white" : "gray.9"}>
{event.title}
</Text>
</Box>
))}
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,75 @@
import {
Card,
Group,
Stack,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import {
Bar,
BarChart,
Cell,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
const apbdesData = [
{ name: "Belanja", value: 70, color: "#3B82F6" },
{ name: "Pangan", value: 45, color: "#22C55E" },
{ name: "Pembiayaan", value: 55, color: "#FACC15" },
{ name: "Pendapatan", value: 90, color: "#3B82F6" },
];
export function ChartAPBDes() {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return (
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: dark
? "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">
Grafik APBDes
</Title>
<Stack gap="xs">
{apbdesData.map((item, index) => (
<Group key={index} align="center" gap="md">
<Text size="sm" fw={500} w={100} c={dark ? "white" : "gray.7"}>
{item.name}
</Text>
<ResponsiveContainer width="100%" height={20}>
<BarChart layout="vertical" data={[item]}>
<XAxis type="number" hide domain={[0, 100]} />
<YAxis type="category" hide dataKey="name" />
<Tooltip
formatter={(value: number) => [`${value}%`, ""]}
contentStyle={{
backgroundColor: dark ? "#1E293B" : "white",
borderColor: dark ? "#334155" : "#e5e7eb",
borderRadius: "8px",
}}
/>
<Bar dataKey="value" radius={[4, 4, 4, 4]}>
<Cell fill={item.color} />
</Bar>
</BarChart>
</ResponsiveContainer>
</Group>
))}
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,111 @@
import {
ActionIcon,
Box,
Card,
Group,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import {
Bar,
BarChart,
CartesianGrid,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
const chartData = [
{ month: "Jan", value: 150 },
{ month: "Feb", value: 165 },
{ month: "Mar", value: 195 },
{ month: "Apr", value: 160 },
{ month: "Mei", value: 205 },
{ month: "Jun", value: 185 },
];
export function ChartSurat() {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return (
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: dark
? "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">
<Box>
<Title order={4} c={dark ? "white" : "gray.9"} mb={5}>
Statistik Pengajuan Surat
</Title>
<Text size="sm" c="dimmed">
Trend pengajuan surat 6 bulan terakhir
</Text>
</Box>
<ActionIcon variant="subtle" size="lg" radius="md">
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 5L13 10L8 15"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</ActionIcon>
</Group>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={chartData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#334155" : "#e5e7eb"}
/>
<XAxis
dataKey="month"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
/>
<YAxis
axisLine={false}
tickLine={false}
ticks={[0, 55, 110, 165, 220]}
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1E293B" : "white",
borderColor: dark ? "#334155" : "#e5e7eb",
borderRadius: "8px",
boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)",
}}
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
/>
<Bar
dataKey="value"
fill="var(--mantine-color-blue-filled)"
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</Card>
);
}

View File

@@ -0,0 +1,70 @@
import {
Box,
Card,
Group,
Progress,
Stack,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
interface DivisionData {
name: string;
value: number;
}
const divisionData: DivisionData[] = [
{ name: "Kesejahteraan", value: 37 },
{ name: "Pemberdayaan", value: 26 },
{ name: "Keuangan", value: 17 },
{ name: "Sekretaris Desa", value: 15 },
];
const max_value = 37;
export function DivisionProgress() {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return (
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: dark
? "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">
Divisi Teraktif
</Title>
<Stack gap="sm">
{divisionData.map((divisi, index) => (
<Box key={index}>
<Group justify="space-between" mb={5}>
<Text size="sm" fw={500} c={dark ? "white" : "gray.7"}>
{divisi.name}
</Text>
<Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
{divisi.value} Kegiatan
</Text>
</Group>
<Progress
value={(divisi.value / max_value) * 100}
size="sm"
radius="xl"
color="blue"
animated
/>
</Box>
))}
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,7 @@
export { ActivityList } from "./activity-list";
export { ChartAPBDes } from "./chart-apbdes";
export { ChartSurat } from "./chart-surat";
export { DivisionProgress } from "./division-progress";
export { SatisfactionChart } from "./satisfaction-chart";
export { SDGSCard } from "./sdgs-card";
export { StatCard } from "./stat-card";

View File

@@ -0,0 +1,83 @@
import {
Box,
Card,
Group,
Stack,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
const satisfactionData = [
{ name: "Sangat Puas", value: 25, color: "#4E5BA6" },
{ name: "Puas", value: 25, color: "#F4C542" },
{ name: "Cukup", value: 25, color: "#8CC63F" },
{ name: "Kurang", value: 25, color: "#E57373" },
];
export function SatisfactionChart() {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return (
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: dark
? "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}>
Tingkat Kepuasan
</Title>
<Text size="sm" c="dimmed" mb="md">
Tingkat kepuasan layanan
</Text>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={satisfactionData}
cx="50%"
cy="50%"
innerRadius={80}
outerRadius={120}
paddingAngle={2}
dataKey="value"
>
{satisfactionData.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>
<Group justify="center" gap="md" mt="md">
{satisfactionData.map((item, index) => (
<Group key={index} gap="xs">
<Box
w={12}
h={12}
style={{ backgroundColor: item.color, borderRadius: "50%" }}
/>
<Text size="sm" c={dark ? "white" : "gray.7"}>
{item.name}
</Text>
</Group>
))}
</Group>
</Card>
);
}

View File

@@ -0,0 +1,44 @@
import { Box, Card, Group, Text, useMantineColorScheme } from "@mantine/core";
import type { ReactNode } from "react";
interface SDGSCardProps {
title: string;
score: number;
image: ReactNode;
}
export function SDGSCard({ title, score, image }: SDGSCardProps) {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return (
<Card
p="md"
radius="xl"
withBorder
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}}
h="100%"
>
<Group justify="space-between" align="flex-start" w="100%">
<Box>{image}</Box>
<Box style={{ flex: 1 }}>
<Text
ta={"center"}
size="sm"
c={dark ? "white" : "gray.8"}
fw={500}
mb="xs"
>
{title}
</Text>
<Text ta={"center"} size="xl" c={dark ? "white" : "gray.8"} fw={700}>
{score.toFixed(2)}
</Text>
</Box>
</Group>
</Card>
);
}

View File

@@ -0,0 +1,86 @@
import {
Box,
Card,
Group,
Text,
ThemeIcon,
useMantineColorScheme,
} from "@mantine/core";
import type { ReactNode } from "react";
interface StatCardProps {
title: string;
value: string | number;
detail?: string;
trend?: string;
trendValue?: number;
icon: ReactNode;
iconColor?: string;
}
export function StatCard({
title,
value,
detail,
trend,
trendValue,
icon,
iconColor = "darmasaba-blue",
}: StatCardProps) {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
const isPositiveTrend = trendValue ? trendValue >= 0 : true;
return (
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: dark
? "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" align="flex-start" w="100%">
<Box style={{ flex: 1 }}>
<Text size="sm" c="dimmed" mb="xs">
{title}
</Text>
<Group align="baseline" gap="xs">
<Text size="xl" fw={700} c={dark ? "white" : "gray.9"}>
{value}
</Text>
</Group>
{detail && (
<Text size="sm" c="dimmed" mt="xs">
{detail}
</Text>
)}
{trend && (
<Text
size="sm"
c={isPositiveTrend ? "green" : "red"}
mt="xs"
fw={500}
>
{trend}
</Text>
)}
</Box>
<ThemeIcon
variant="filled"
size="xl"
radius="xl"
color={dark ? "gray" : iconColor}
>
{icon}
</ThemeIcon>
</Group>
</Card>
);
}

View File

@@ -1,469 +1,410 @@
import { BarChart, PieChart } from "@mantine/charts";
import {
Box,
Card,
Grid,
Group,
Stack,
Table,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import {
IconArrowDown,
IconArrowUp,
IconBabyCarriage,
IconSkull,
IconUsers,
IconHome,
IconExclamationCircle,
} from "@tabler/icons-react";
import { useMantineColorScheme } from "@mantine/core";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
Pie,
PieChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import React from "react";
// Sample Data
const kpiData = [
{
id: 1,
title: "Total Penduduk",
value: "5.634",
sub: "Aktif terdaftar",
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"
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,
title: "Kepala Keluarga",
value: "1.354",
sub: "Total KK",
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"
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,
title: "Kelahiran",
value: "23",
sub: "Tahun ini",
icon: (
<IconBabyCarriage
className="h-6 w-6 text-muted-foreground"
role="img"
aria-label="Icon kelahiran"
/>
),
},
{
id: 4,
title: "Kemiskinan",
value: "324",
delta: "-10% dari tahun lalu",
deltaType: "positive",
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"
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>
),
},
];
const ageDistributionData = [
{ ageRange: "17-25", total: 850 },
{ ageRange: "26-35", total: 1200 },
{ ageRange: "36-45", total: 1100 },
{ ageRange: "46-55", total: 950 },
{ ageRange: "56-65", total: 750 },
{ ageRange: "65+", total: 484 },
];
const jobDistributionData = [
{ job: "Sipil", total: 1200 },
{ job: "Guru", total: 850 },
{ job: "Petani", total: 950 },
{ job: "Pedagang", total: 750 },
{ job: "Wiraswasta", total: 984 },
];
const religionData = [
{ religion: "Hindu", total: 4234, color: "red" },
{ religion: "Islam", total: 856, color: "blue" },
{ religion: "Kristen", total: 412, color: "green" },
{ religion: "Buddha", total: 202, color: "yellow" },
];
const banjarData = [
{ banjar: "Banjar Darmasaba", population: 1200, kk: 300, poor: 45 },
{ banjar: "Banjar Manesa", population: 950, kk: 240, poor: 32 },
{ banjar: "Banjar Cabe", population: 800, kk: 200, poor: 28 },
{ banjar: "Banjar Penenjoan", population: 1100, kk: 280, poor: 38 },
{ banjar: "Banjar Baler Pasar", population: 984, kk: 250, poor: 42 },
{ banjar: "Banjar Bucu", population: 600, kk: 184, poor: 25 },
];
const dynamicStats = [
{
title: "Kelahiran",
value: "23",
icon: <IconBabyCarriage size={16} />,
color: "green",
},
{
title: "Kematian",
value: "12",
icon: <IconSkull size={16} />,
color: "red",
},
{
title: "Pindah Masuk",
value: "45",
icon: <IconArrowDown size={16} />,
color: "blue",
},
{
title: "Pindah Keluar",
value: "32",
icon: <IconArrowUp size={16} />,
color: "orange",
},
];
const DemografiPekerjaan = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
// KPI Data
const kpiData = [
{
id: 1,
title: "Total Penduduk",
value: "5.634",
subtitle: "Aktif terdaftar",
icon: IconUsers,
},
{
id: 2,
title: "Kepala Keluarga",
value: "1.354",
subtitle: "Total KK",
icon: IconHome,
},
{
id: 3,
title: "Kelahiran",
value: "23",
subtitle: "Tahun ini",
icon: IconBabyCarriage,
},
{
id: 4,
title: "Kemiskinan",
value: "324",
subtitle: "-10% dari tahun lalu",
icon: IconExclamationCircle,
},
];
// Age distribution data
const ageDistributionData = [
{ ageRange: "17-25", total: 850 },
{ ageRange: "26-35", total: 1200 },
{ ageRange: "36-45", total: 1100 },
{ ageRange: "46-55", total: 950 },
{ ageRange: "56-65", total: 750 },
{ ageRange: "65+", total: 484 },
];
// Job distribution data
const jobDistributionData = [
{ job: "Sipil", total: 1200 },
{ job: "Guru", total: 850 },
{ job: "Petani", total: 950 },
{ job: "Pedagang", total: 750 },
{ job: "Wiraswasta", total: 984 },
];
// Religion data
const religionData = [
{ religion: "Hindu", total: 4234, color: "#EF4444" },
{ religion: "Islam", total: 856, color: "#3B82F6" },
{ religion: "Kristen", total: 412, color: "#10B981" },
{ religion: "Buddha", total: 202, color: "#F59E0B" },
];
// Banjar data
const banjarData = [
{ banjar: "Banjar Darmasaba", population: 1200, kk: 300, poor: 45 },
{ banjar: "Banjar Manesa", population: 950, kk: 240, poor: 32 },
{ banjar: "Banjar Cabe", population: 800, kk: 200, poor: 28 },
{ banjar: "Banjar Penenjoan", population: 1100, kk: 280, poor: 38 },
{ banjar: "Banjar Baler Pasar", population: 984, kk: 250, poor: 42 },
{ banjar: "Banjar Bucu", population: 600, kk: 184, poor: 25 },
];
// Dynamic stats
const dynamicStats = [
{
title: "Kelahiran",
value: "23",
icon: IconBabyCarriage,
color: "#10B981",
},
{
title: "Kematian",
value: "12",
icon: IconSkull,
color: "#EF4444",
},
{
title: "Pindah Masuk",
value: "45",
icon: IconArrowDown,
color: "#3B82F6",
},
{
title: "Pindah Keluar",
value: "32",
icon: IconArrowUp,
color: "#F59E0B",
},
];
const COLORS = ["#1E3A5F", "#3B82F6", "#60A5FA", "#93C5FD", "#DBEAFE"];
const cardStyle = {
backgroundColor: dark ? "#141D34" : "white",
border: `1px solid ${dark ? "#141D34" : "white"}`,
};
const textStyle = {
color: dark ? "white" : "#1F2937",
};
const subtitleStyle = {
color: dark ? "#9CA3AF" : "#6B7280",
};
return (
<div
className="min-h-screen"
style={{ backgroundColor: dark ? "#10192D" : "#F3F4F6" }}
>
<div className="max-w-7xl mx-auto">
{/* Row 1: 4 Statistic Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<Box className="space-y-6">
<Stack gap="xl">
{/* KPI Cards */}
<Grid gutter="lg">
{kpiData.map((kpi) => (
<div
key={kpi.id}
className="rounded-xl shadow-sm p-6"
style={cardStyle}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<h3
className="text-sm font-medium mb-1"
style={subtitleStyle}
>
{kpi.title}
</h3>
<p
className="text-3xl font-bold mb-1"
style={textStyle}
>
{kpi.value}
</p>
<p
className="text-xs"
style={subtitleStyle}
>
{kpi.subtitle}
</p>
</div>
<div className="flex-shrink-0 ml-4">
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-white"
style={{ backgroundColor: "#1E3A5F" }}
>
<kpi.icon size={24} />
</div>
</div>
</div>
</div>
))}
</div>
{/* Row 2: 2 Chart Cards */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{/* Age Distribution Bar Chart */}
<div
className="rounded-xl shadow-sm p-6"
style={cardStyle}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Grafik Pengelompokan Umur
</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={ageDistributionData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#2d3748" : "#E5E7EB"}
/>
<XAxis
dataKey="ageRange"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Bar dataKey="total" radius={[4, 4, 0, 0]}>
{ageDistributionData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
{/* Job Distribution Bar Chart */}
<div
className="rounded-xl shadow-sm p-6"
style={cardStyle}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Demografi Pekerjaan
</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={jobDistributionData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#2d3748" : "#E5E7EB"}
/>
<XAxis
dataKey="job"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Bar dataKey="total" radius={[4, 4, 0, 0]}>
{jobDistributionData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
{/* Row 3: 3 Insight Cards */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
{/* Religion Distribution Pie Chart */}
<div
className="rounded-xl shadow-sm p-6"
style={cardStyle}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Distribusi Agama
</h3>
<ResponsiveContainer width="100%" height={250}>
<PieChart>
<Pie
data={religionData}
cx="50%"
cy="50%"
outerRadius={80}
dataKey="total"
nameKey="religion"
label={({ name, percent }) =>
`${name}: ${percent ? (percent * 100).toFixed(0) : 0}%`
}
>
{religionData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
</PieChart>
</ResponsiveContainer>
</div>
{/* Population per Banjar Table */}
<div
className="rounded-xl shadow-sm p-6 lg:col-span-2"
style={cardStyle}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Data per Banjar
</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr
style={{
borderBottom: `1px solid ${dark ? "#2d3748" : "#E5E7EB"}`,
}}
>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Banjar
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Penduduk
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
KK
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Miskin
</th>
</tr>
</thead>
<tbody>
{banjarData.map((item, index) => (
<tr
key={index}
style={{
borderBottom: `1px solid ${dark ? "#2d3748" : "#F3F4F6"}`,
}}
>
<td
className="py-3 px-4 text-sm"
style={textStyle}
>
{item.banjar}
</td>
<td
className="py-3 px-4 text-sm"
style={textStyle}
>
{item.population.toLocaleString()}
</td>
<td
className="py-3 px-4 text-sm"
style={textStyle}
>
{item.kk.toLocaleString()}
</td>
<td
className="py-3 px-4 text-sm"
style={{ color: "#EF4444" }}
>
{item.poor.toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
{/* Population Dynamics Stats */}
<div
className="rounded-xl shadow-sm p-6"
style={cardStyle}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Statistik Dinamika Penduduk
</h3>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{dynamicStats.map((stat, index) => (
<div
key={index}
className="p-4 rounded-lg"
style={{
backgroundColor: dark ? "#1F2937" : "#F9FAFB",
}}
<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" }}
>
<div className="flex items-center justify-between">
<div>
<p
className="text-sm font-medium mb-1"
style={subtitleStyle}
>
{stat.title}
</p>
<p
className="text-2xl font-bold"
style={{ color: stat.color }}
>
{stat.value}
</p>
</div>
<div
className="w-10 h-10 rounded-full flex items-center justify-center"
style={{
backgroundColor: `${stat.color}20`,
color: stat.color,
}}
<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}
>
<stat.icon size={20} />
</div>
</div>
</div>
{kpi.delta}
</Text>
)}
{kpi.sub && (
<Text size="xs" c={dark ? "dark.3" : "dimmed"} mt={2}>
{kpi.sub}
</Text>
)}
</Card>
</Grid.Col>
))}
</Grid>
{/* Charts Section */}
<Grid gutter="lg">
{/* Grafik Pengelompokan Umur */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={3} fw={500} mb="md">
Grafik Pengelompokan Umur
</Title>
<BarChart
h={300}
data={ageDistributionData}
dataKey="ageRange"
series={[{ name: "total", color: "darmasaba-navy" }]}
withLegend
/>
</Card>
</Grid.Col>
{/* Demografi Pekerjaan */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={3} fw={500} mb="md">
Demografi Pekerjaan
</Title>
<BarChart
h={300}
data={jobDistributionData}
dataKey="job"
series={[{ name: "total", color: "darmasaba-navy" }]}
withLegend
/>
</Card>
</Grid.Col>
</Grid>
{/* Agama & Data per Banjar */}
<Grid gutter="lg">
{/* Distribusi Agama */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={3} fw={500} mb="md">
Distribusi Agama
</Title>
<PieChart
h={300}
data={religionData.map((item) => ({
name: item.religion,
value: item.total,
color: item.color,
}))}
withLabels
withLabelsLine
labelsPosition="outside"
labelsType="percent"
/>
</Card>
</Grid.Col>
{/* Data per Banjar */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<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">
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}
</Text>
<Title order={4} fw={700} c={stat.color}>
{stat.value}
</Title>
</Box>
<Box c={stat.color}>{stat.icon}</Box>
</Group>
</Card>
</Grid.Col>
))}
</div>
</div>
</div>
</div>
</Grid>
</Card>
</Stack>
</Box>
);
};

View File

@@ -1,72 +1,121 @@
import {
ActionIcon,
Avatar,
Badge,
Box,
Divider,
Group,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import { useLocation } from "@tanstack/react-router";
import { Bell, Moon, Sun } from "lucide-react";
import { IconUserShield } from "@tabler/icons-react";
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
export function Header() {
const location = useLocation();
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
const navigate = useNavigate();
const title =
location.pathname === "/"
? "Desa Darmasaba"
: "Desa Darmasaba";
// Define page titles based on route
const getPageTitle = () => {
switch (location.pathname) {
case "/":
return "Beranda";
case "/kinerja-divisi":
return "Kinerja Divisi";
case "/pengaduan-layanan-publik":
return "Pengaduan & Layanan Publik";
case "/jenna-analytic":
return "Jenna Analytic";
case "/demografi-pekerjaan":
return "Demografi & Kependudukan";
case "/keuangan-anggaran":
return "Keuangan & Anggaran";
case "/bumdes":
return "Bumdes & UMKM Desa";
case "/sosial":
return "Sosial";
case "/keamanan":
return "Keamanan";
case "/bantuan":
return "Bantuan";
case "/pengaturan":
case "/pengaturan/umum":
case "/pengaturan/notifikasi":
case "/pengaturan/keamanan":
case "/pengaturan/akses-dan-tim":
return "Pengaturan";
default:
return "Desa Darmasaba";
}
};
return (
<Box
style={{
display: "grid",
gridTemplateColumns: "1fr auto 1fr",
alignItems: "center",
width: "100%",
}}
>
{/* LEFT SPACER (burger sudah di luar) */}
<Box />
<Group justify="space-between" w="100%">
{/* Title */}
<Title order={3} c={"white"}>
{getPageTitle()}
</Title>
{/* CENTER TITLE */}
<Text
c="white"
fw={600}
size="md"
style={{
textAlign: "center",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{title}
</Text>
{/* Right Section */}
<Group gap="md">
{/* User Info */}
<Group gap="sm">
<Box ta="right">
<Text c={"white"} size="sm" fw={500}>
I. B. Surya Prabhawa M...
</Text>
<Text c={"white"} size="xs">
Kepala Desa
</Text>
</Box>
<Avatar color="blue" radius="xl">
<UserIcon color="white" style={{ width: "70%", height: "70%" }} />
</Avatar>
</Group>
{/* RIGHT ICONS */}
<Group gap="xs" justify="flex-end">
<ActionIcon
onClick={toggleColorScheme}
variant="subtle"
radius="xl"
>
{dark ? <Sun size={18} /> : <Moon size={18} />}
</ActionIcon>
{/* Divider */}
<Divider orientation="vertical" h={30} />
<ActionIcon variant="subtle" radius="xl" pos="relative">
<Bell size={18} />
<Badge
size="xs"
color="red"
style={{ position: "absolute", top: -4, right: -4 }}
{/* Icons */}
<Group gap="sm">
<ActionIcon
onClick={() => toggleColorScheme()}
variant="subtle"
size="lg"
radius="xl"
aria-label="Toggle color scheme"
>
10
</Badge>
</ActionIcon>
{dark ? (
<Sun color="white" style={{ width: "70%", height: "70%" }} />
) : (
<Moon color="white" style={{ width: "70%", height: "70%" }} />
)}
</ActionIcon>
<ActionIcon variant="subtle" size="lg" radius="xl" pos="relative">
<Bell color="white" style={{ width: "70%", height: "70%" }} />
<Badge
size="xs"
color="red"
variant="filled"
style={{ position: "absolute", top: 0, right: 0 }}
radius={"xl"}
>
10
</Badge>
</ActionIcon>
<ActionIcon variant="subtle" size="lg" radius="xl">
<IconUserShield
color="white"
style={{ width: "70%", height: "70%" }}
onClick={() => navigate({ to: "/signin" })}
/>
</ActionIcon>
</Group>
</Group>
</Box>
</Group>
);
}

View File

@@ -142,7 +142,14 @@ const HelpPage = () => {
};
return (
<Container size="lg" py="lg">
<Container size="lg" py="xl">
<Title order={1} mb="xl" ta="center">
Pusat Bantuan
</Title>
<Text size="lg" color="dimmed" ta="center" mb="xl">
Temukan jawaban untuk pertanyaan Anda atau hubungi tim support kami
</Text>
{/* Statistics Section */}
<SimpleGrid cols={3} spacing="lg" mb="xl">
{stats.map((stat, index) => (

View File

@@ -1,281 +1,283 @@
import { BarChart } from "@mantine/charts";
import {
IconAlertTriangle,
IconClock,
IconMessageChatbot,
IconSparkles,
} from "@tabler/icons-react";
import { useMantineColorScheme } from "@mantine/core";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
Badge,
Box,
Button,
Card,
Grid,
Group,
Progress,
Stack,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import React from "react";
// Sample Data
const kpiData = [
{
id: 1,
title: "Interaksi Hari Ini",
value: "61",
delta: "+15% dari kemarin",
deltaType: "positive",
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="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,
title: "Jawaban Otomatis",
value: "87%",
sub: "53 dari 61 interaksi",
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="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,
title: "Belum Ditindak",
value: "8",
sub: "Perlu respon manual",
deltaType: "negative",
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,
title: "Waktu Respon",
value: "2.3 sec",
sub: "Rata-rata",
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 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
),
},
];
const chartData = [
{ day: "Sen", total: 100 },
{ day: "Sel", total: 120 },
{ day: "Rab", total: 90 },
{ day: "Kam", total: 150 },
{ day: "Jum", total: 110 },
{ day: "Sab", total: 80 },
{ day: "Min", total: 130 },
];
const topTopics = [
{ topic: "Cara mengurus KTP", count: 89 },
{ topic: "Syarat Kartu Keluarga", count: 76 },
{ topic: "Jadwal Posyandu", count: 64 },
{ topic: "Pengaduan jalan rusak", count: 52 },
{ topic: "Info program bansos", count: 48 },
];
const busyHours = [
{ period: "Pagi (0812)", percentage: 30 },
{ period: "Siang (1216)", percentage: 40 },
{ period: "Sore (1620)", percentage: 20 },
{ period: "Malam (2008)", percentage: 10 },
];
const JennaAnalytic = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
// KPI Data
const kpiData = [
{
id: 1,
title: "Interaksi Hari Ini",
value: "61",
subtitle: "+15% dari kemarin",
icon: IconMessageChatbot,
},
{
id: 2,
title: "Jawaban Otomatis",
value: "87%",
subtitle: "53 dari 61 interaksi",
icon: IconSparkles,
},
{
id: 3,
title: "Belum Ditindak",
value: "8",
subtitle: "Perlu respon manual",
icon: IconAlertTriangle,
},
{
id: 4,
title: "Waktu Respon",
value: "2.3s",
subtitle: "Rata-rata",
icon: IconClock,
},
];
// Weekly chatbot interaction data
const weeklyData = [
{ day: "Sen", interactions: 100 },
{ day: "Sel", interactions: 120 },
{ day: "Rab", interactions: 90 },
{ day: "Kam", interactions: 150 },
{ day: "Jum", interactions: 110 },
{ day: "Sab", interactions: 80 },
{ day: "Min", interactions: 130 },
];
// Top topics data
const topTopics = [
{ topic: "Cara mengurus KTP", count: 89 },
{ topic: "Syarat Kartu Keluarga", count: 76 },
{ topic: "Jadwal Posyandu", count: 64 },
{ topic: "Pengaduan jalan rusak", count: 52 },
{ topic: "Info program bansos", count: 48 },
];
// Busy hour distribution
const busyHours = [
{ period: "Pagi (0812)", percentage: 30 },
{ period: "Siang (1216)", percentage: 40 },
{ period: "Sore (1620)", percentage: 20 },
{ period: "Malam (2008)", percentage: 10 },
];
const COLORS = ["#1E3A5F", "#3B82F6", "#60A5FA", "#93C5FD"];
const cardStyle = {
backgroundColor: dark ? "#141D34" : "white",
border: `1px solid ${dark ? "#141D34" : "white"}`,
};
const textStyle = {
color: dark ? "white" : "#1F2937",
};
const subtitleStyle = {
color: dark ? "#9CA3AF" : "#6B7280",
};
return (
<div
className="min-h-screen"
style={{ backgroundColor: dark ? "#10192D" : "#F3F4F6" }}
>
<div className="max-w-7xl mx-auto">
{/* Row 1: 4 Statistic Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<Box className="space-y-6">
<Stack gap="xl">
{/* KPI Cards */}
<Grid gutter="lg">
{kpiData.map((kpi) => (
<div
key={kpi.id}
className="rounded-xl shadow-sm p-6"
style={cardStyle}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<h3
className="text-sm font-medium mb-1"
style={subtitleStyle}
>
<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}
</h3>
<p
className="text-3xl font-bold mb-1"
style={textStyle}
</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.value}
</p>
<p
className="text-xs"
style={subtitleStyle}
>
{kpi.subtitle}
</p>
</div>
<div className="flex-shrink-0 ml-4">
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-white"
style={{ backgroundColor: "#1E3A5F" }}
>
<kpi.icon size={24} />
</div>
</div>
</div>
</div>
{kpi.delta}
</Text>
)}
{kpi.sub && (
<Text size="xs" c="dimmed" mt={2}>
{kpi.sub}
</Text>
)}
</Card>
</Grid.Col>
))}
</div>
</Grid>
{/* Row 2: Full Width Weekly Bar Chart */}
<div
className="rounded-xl shadow-sm p-6 mb-6"
style={cardStyle}
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Interaksi Chatbot Mingguan
</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={weeklyData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#2d3748" : "#E5E7EB"}
/>
<XAxis
dataKey="day"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Bar dataKey="interactions" radius={[4, 4, 0, 0]}>
{weeklyData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
<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
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
h="100%"
>
<Title order={3} fw={500} mb="md">
Jam Tersibuk
</Title>
<Stack gap="sm">
{busyHours.map((item, index) => (
<Box key={index}>
<Text size="sm">{item.period}</Text>
<Group align="center">
<Progress value={item.percentage} flex={1} />
<Text size="sm" fw={500}>
{item.percentage}%
</Text>
</Group>
</Box>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</Stack>
</Card>
</Grid.Col>
{/* Row 3: Two Insight Cards */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left: Frequently Asked Topics */}
<div
className="rounded-xl shadow-sm p-6"
style={cardStyle}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Topik Pertanyaan Terbanyak
</h3>
<div className="space-y-3">
{topTopics.map((item, index) => (
<div
key={index}
className="flex items-center justify-between py-3"
style={{
borderBottom: `1px solid ${dark ? "#2d3748" : "#E5E7EB"}`,
}}
>
<span
className="text-sm font-medium"
style={textStyle}
>
{item.topic}
</span>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{item.count}x
</span>
</div>
))}
</div>
</div>
{/* Topik Pertanyaan Terbanyak & Jam Tersibuk */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Stack gap="lg">
{/* Topik Pertanyaan Terbanyak */}
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
h="100%"
>
<Title order={3} fw={500} mb="md">
Topik Pertanyaan Terbanyak
</Title>
<Stack gap="xs">
{topTopics.map((item, index) => (
<Group
key={index}
justify="space-between"
align="center"
p="xs"
>
<Text size="sm" fw={500}>
{item.topic}
</Text>
<Badge variant="light" color="gray">
{item.count}x
</Badge>
</Group>
))}
</Stack>
</Card>
{/* Right: Busy Hour Distribution */}
<div
className="rounded-xl shadow-sm p-6"
style={cardStyle}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Distribusi Jam Tersibuk
</h3>
<div className="space-y-4">
{busyHours.map((item, index) => (
<div key={index}>
<div className="flex items-center justify-between mb-1">
<span
className="text-sm font-medium"
style={textStyle}
>
{item.period}
</span>
<span
className="text-sm font-semibold"
style={textStyle}
>
{item.percentage}%
</span>
</div>
<div
className="w-full rounded-full h-2"
style={{ backgroundColor: dark ? "#2d3748" : "#E5E7EB" }}
>
<div
className="h-2 rounded-full transition-all"
style={{
width: `${item.percentage}%`,
backgroundColor: COLORS[index % COLORS.length],
}}
/>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
{/* Jam Tersibuk */}
</Stack>
</Grid.Col>
</Grid>
</Stack>
</Box>
);
};
export default JennaAnalytic;

View File

@@ -118,6 +118,13 @@ const KeamananPage = () => {
return (
<Stack gap="lg">
{/* Page Header */}
<Group justify="space-between" align="center">
<Title order={2} c={dark ? "dark.0" : "black"}>
Keamanan Lingkungan Desa
</Title>
</Group>
{/* KPI Cards */}
<Grid gutter="md">
{kpiData.map((kpi, index) => (

View File

@@ -1,507 +1,356 @@
import { BarChart } from "@mantine/charts";
import {
Badge,
Box,
Button,
Card,
Grid,
Group,
Progress,
Stack,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import {
IconCurrency,
IconTrendingDown,
IconTrendingUp,
IconCheck,
IconClock,
} from "@tabler/icons-react";
import { useMantineColorScheme } from "@mantine/core";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import React from "react";
// Sample Data
const kpiData = [
{
id: 1,
title: "Total APBDes",
value: "Rp 5.2M",
sub: "Tahun 2025",
icon: <IconCurrency className="h-6 w-6 text-muted-foreground" />,
},
{
id: 2,
title: "Realisasi",
value: "68%",
sub: "Rp 3.5M dari 5.2M",
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="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,
title: "Pemasukan",
value: "Rp 580jt",
sub: "Bulan ini",
delta: "+8%",
deltaType: "positive",
icon: <IconTrendingUp className="h-6 w-6 text-muted-foreground" />,
},
{
id: 4,
title: "Pengeluaran",
value: "Rp 520jt",
sub: "Bulan ini",
icon: <IconTrendingDown className="h-6 w-6 text-muted-foreground" />,
},
];
const incomeExpenseData = [
{ month: "Apr", income: 450, expense: 380 },
{ month: "Mei", income: 520, expense: 420 },
{ month: "Jun", income: 480, expense: 500 },
{ month: "Jul", income: 580, expense: 450 },
{ month: "Agu", income: 550, expense: 520 },
{ month: "Sep", income: 600, expense: 480 },
{ month: "Okt", income: 580, expense: 520 },
];
const allocationData = [
{ sector: "Pembangunan", amount: 1200 },
{ sector: "Kesehatan", amount: 800 },
{ sector: "Pendidikan", amount: 650 },
{ sector: "Sosial", amount: 550 },
{ sector: "Kebudayaan", amount: 400 },
{ sector: "Teknologi", amount: 300 },
];
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 apbdReport = {
income: [
{ category: "Dana Desa", amount: 1800 },
{ category: "Alokasi Dana Desa", amount: 480 },
{ category: "Bagi Hasil Pajak & Retribusi", amount: 300 },
{ category: "Pendapatan Asli Desa", amount: 200 },
{ category: "Hibah Bantuan", amount: 300 },
],
expenses: [
{ category: "Penyelenggaraan Pemerintah", amount: 425 },
{ category: "Pembangunan Desa", amount: 850 },
{ category: "Pembinaan Kemasyarakatan", amount: 320 },
{ category: "Pemberdayaan Masyarakat", amount: 380 },
{ category: "Penanggulangan Bencana", amount: 180 },
],
totalIncome: 3080,
totalExpenses: 2155,
};
const KeuanganAnggaran = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
// KPI Data
const kpiData = [
{
id: 1,
title: "Total APBDes",
value: "Rp 5.2M",
subtitle: "Tahun 2025",
icon: IconCurrency,
},
{
id: 2,
title: "Realisasi",
value: "68%",
subtitle: "Rp 3.5M dari 5.2M",
icon: IconCheck,
},
{
id: 3,
title: "Pemasukan",
value: "Rp 580jt",
subtitle: "Bulan ini",
delta: "+8%",
icon: IconTrendingUp,
},
{
id: 4,
title: "Pengeluaran",
value: "Rp 520jt",
subtitle: "Bulan ini",
icon: IconTrendingDown,
},
];
// Income vs Expense data
const incomeExpenseData = [
{ month: "Apr", income: 450, expense: 380 },
{ month: "Mei", income: 520, expense: 420 },
{ month: "Jun", income: 480, expense: 500 },
{ month: "Jul", income: 580, expense: 450 },
{ month: "Agu", income: 550, expense: 520 },
{ month: "Sep", income: 600, expense: 480 },
{ month: "Okt", income: 580, expense: 520 },
];
// Allocation data
const allocationData = [
{ sector: "Pembangunan", amount: 1200 },
{ sector: "Kesehatan", amount: 800 },
{ sector: "Pendidikan", amount: 650 },
{ sector: "Sosial", amount: 550 },
{ sector: "Kebudayaan", amount: 400 },
{ sector: "Teknologi", amount: 300 },
];
// Assistance fund 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" },
];
// APBDes Report data
const apbdReport = {
income: [
{ category: "Dana Desa", amount: 1800 },
{ category: "Alokasi Dana Desa", amount: 480 },
{ category: "Bagi Hasil Pajak & Retribusi", amount: 300 },
{ category: "Pendapatan Asli Desa", amount: 200 },
{ category: "Hibah Bantuan", amount: 300 },
],
expenses: [
{ category: "Penyelenggaraan Pemerintah", amount: 425 },
{ category: "Pembangunan Desa", amount: 850 },
{ category: "Pembinaan Kemasyarakatan", amount: 320 },
{ category: "Pemberdayaan Masyarakat", amount: 380 },
{ category: "Penanggulangan Bencana", amount: 180 },
],
totalIncome: 3080,
totalExpenses: 2155,
};
const COLORS = ["#1E3A5F", "#3B82F6", "#60A5FA", "#93C5FD", "#DBEAFE"];
const cardStyle = {
backgroundColor: dark ? "#1E293B" : "white",
border: `1px solid ${dark ? "#1E293B" : "white"}`,
};
const textStyle = {
color: dark ? "white" : "#1F2937",
};
const subtitleStyle = {
color: dark ? "#9CA3AF" : "#6B7280",
};
return (
<div
className="min-h-screen"
style={{ backgroundColor: dark ? "#0F172A" : "#F3F4F6" }}
>
<div className="max-w-7xl mx-auto">
{/* Row 1: 4 Summary Metrics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<Box>
<Stack gap="xl">
{/* KPI Cards */}
<Grid gutter="lg">
{kpiData.map((kpi) => (
<div
key={kpi.id}
className="rounded-xl shadow-sm p-6"
style={cardStyle}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<h3
className="text-sm font-medium mb-1"
style={subtitleStyle}
>
<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}
</h3>
<p
className="text-3xl font-bold mb-1"
style={textStyle}
</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.value}
</p>
<p
className="text-xs"
style={subtitleStyle}
>
{kpi.subtitle}
</p>
{kpi.delta && (
<p className="text-xs text-green-500 mt-1">
{kpi.delta}
</p>
)}
</div>
<div className="flex-shrink-0 ml-4">
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-white"
style={{ backgroundColor: "#1F3A5F" }}
>
<kpi.icon size={24} />
</div>
</div>
</div>
</div>
{kpi.delta}
</Text>
)}
{kpi.sub && (
<Text size="xs" c="dimmed" mt="auto">
{kpi.sub}
</Text>
)}
</Card>
</Grid.Col>
))}
</div>
</Grid>
{/* Row 2: Line Chart Section */}
<div
className="rounded-xl shadow-sm p-6 mb-6"
style={cardStyle}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Pemasukan vs Pengeluaran
</h3>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={incomeExpenseData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#334155" : "#E5E7EB"}
/>
<XAxis
{/* Charts Section */}
<Grid gutter="lg">
{/* Grafik Pemasukan vs Pengeluaran */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={3} fw={500} mb="md">
Pemasukan vs Pengeluaran
</Title>
<BarChart
h={300}
data={incomeExpenseData}
dataKey="month"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
series={[
{ name: "income", color: "green", label: "Pemasukan" },
{ name: "expense", color: "red", label: "Pengeluaran" },
]}
withLegend
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
tickFormatter={(value) => `${value}jt`}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Line
type="monotone"
dataKey="income"
stroke="#22C55E"
strokeWidth={3}
dot={{ fill: "#22C55E", strokeWidth: 2, r: 5 }}
activeDot={{ r: 7 }}
name="Pemasukan"
/>
<Line
type="monotone"
dataKey="expense"
stroke="#EF4444"
strokeWidth={3}
dot={{ fill: "#EF4444", strokeWidth: 2, r: 5 }}
activeDot={{ r: 7 }}
name="Pengeluaran"
/>
</LineChart>
</ResponsiveContainer>
</Card>
</Grid.Col>
{/* Legend */}
<div className="flex items-center gap-6 mt-4">
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: "#22C55E" }}
/>
<span className="text-sm" style={subtitleStyle}>
Pemasukan
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: "#EF4444" }}
/>
<span className="text-sm" style={subtitleStyle}>
Pengeluaran
</span>
</div>
</div>
</div>
{/* Row 3: Analytics Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{/* Left: Horizontal Bar Chart */}
<div
className="rounded-xl shadow-sm p-6"
style={cardStyle}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
{/* Alokasi Anggaran Per Sektor */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
Alokasi Anggaran Per Sektor
</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={allocationData} layout="vertical">
<CartesianGrid
strokeDasharray="3 3"
horizontal={false}
stroke={dark ? "#334155" : "#E5E7EB"}
/>
<XAxis
type="number"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
tickFormatter={(value) => `${value}jt`}
/>
<YAxis
dataKey="sector"
type="category"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#374151" }}
width={120}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Bar dataKey="amount" radius={[0, 4, 4, 0]}>
{allocationData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
<Title order={3} fw={500} mb="md">
Alokasi Anggaran Per Sektor
</Title>
<BarChart
h={300}
data={allocationData}
dataKey="sector"
series={[
{ name: "amount", color: "darmasaba-navy", label: "Jumlah" },
]}
withLegend
orientation="horizontal"
/>
</Card>
</Grid.Col>
</Grid>
<Grid gutter="lg">
{/* Dana Bantuan & Hibah */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={3} fw={500} mb="md">
Dana Bantuan & Hibah
</Title>
<Stack gap="sm">
{assistanceFundData.map((fund, index) => (
<Group
key={index}
justify="space-between"
align="center"
p="sm"
style={{
border: "1px solid var(--mantine-color-gray-3)",
borderRadius: "var(--mantine-radius-sm)",
}}
>
<Box>
<Text size="sm" fw={500}>
{fund.source}
</Text>
<Text size="sm" c="dimmed">
Rp {fund.amount.toLocaleString()}jt
</Text>
</Box>
<Badge
variant="light"
color={fund.status === "cair" ? "green" : "yellow"}
>
{fund.status}
</Badge>
</Group>
))}
</Stack>
</Card>
</Grid.Col>
{/* Laporan APBDes */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<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>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
{/* Right: Assistance Funds List */}
<div
className="rounded-xl shadow-sm p-6"
style={cardStyle}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Dana Bantuan dan Hibah
</h3>
<div className="space-y-3">
{assistanceFundData.map((fund, index) => (
<div
key={index}
className="flex items-center justify-between p-4 rounded-lg"
style={{
backgroundColor: dark ? "#334155" : "#F9FAFB",
}}
>
<div>
<p
className="text-sm font-medium"
style={textStyle}
>
{fund.source}
</p>
<p
className="text-xs mt-1"
style={subtitleStyle}
>
Rp {fund.amount.toLocaleString()}jt
</p>
</div>
<span
className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium"
style={{
backgroundColor: fund.status === "cair" ? "#DCFCE7" : "#FEF3C7",
color: fund.status === "cair" ? "#166534" : "#92400E",
}}
>
{fund.status === "cair" ? (
<IconCheck size={14} className="mr-1" />
) : (
<IconClock size={14} className="mr-1" />
)}
{fund.status}
</span>
</div>
))}
</div>
</div>
<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>
</div>
{/* Row 4: Report Section */}
<div
className="rounded-xl shadow-sm p-6"
style={cardStyle}
>
<h3
className="text-lg font-semibold mb-6"
style={textStyle}
>
Laporan APBDes
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Left: Pendapatan */}
<div>
<h4
className="text-base font-semibold mb-4"
style={textStyle}
<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)" }}
>
Pendapatan
</h4>
<div className="space-y-3">
{apbdReport.income.map((item, index) => (
<div
key={index}
className="flex items-center justify-between py-2"
style={{
borderBottom: `1px solid ${dark ? "#334155" : "#F3F4F6"}`,
}}
<Group justify="space-between">
<Text fw={700}>Saldo:</Text>
<Text
fw={700}
c={
apbdReport.totalIncome > apbdReport.totalExpenses
? "green"
: "red"
}
>
<span className="text-sm" style={subtitleStyle}>
{item.category}
</span>
<span
className="text-sm font-medium"
style={{ color: "#22C55E" }}
>
Rp {item.amount.toLocaleString()}jt
</span>
</div>
))}
<div
className="flex items-center justify-between py-3 mt-4 pt-4"
style={{
borderTop: `2px solid ${dark ? "#334155" : "#E5E7EB"}`,
}}
>
<span className="text-base font-bold" style={textStyle}>
Total Pendapatan
</span>
<span
className="text-base font-bold"
style={{ color: "#22C55E" }}
>
Rp {apbdReport.totalIncome.toLocaleString()}jt
</span>
</div>
</div>
</div>
{/* Right: Belanja */}
<div>
<h4
className="text-base font-semibold mb-4"
style={textStyle}
>
Belanja
</h4>
<div className="space-y-3">
{apbdReport.expenses.map((item, index) => (
<div
key={index}
className="flex items-center justify-between py-2"
style={{
borderBottom: `1px solid ${dark ? "#334155" : "#F3F4F6"}`,
}}
>
<span className="text-sm" style={subtitleStyle}>
{item.category}
</span>
<span
className="text-sm font-medium"
style={{ color: "#EF4444" }}
>
Rp {item.amount.toLocaleString()}jt
</span>
</div>
))}
<div
className="flex items-center justify-between py-3 mt-4 pt-4"
style={{
borderTop: `2px solid ${dark ? "#334155" : "#E5E7EB"}`,
}}
>
<span className="text-base font-bold" style={textStyle}>
Total Belanja
</span>
<span
className="text-base font-bold"
style={{ color: "#EF4444" }}
>
Rp {apbdReport.totalExpenses.toLocaleString()}jt
</span>
</div>
</div>
</div>
</div>
{/* Footer: Balance */}
<div
className="flex items-center justify-between py-4 mt-6 pt-6"
style={{
borderTop: `2px solid ${dark ? "#334155" : "#E5E7EB"}`,
}}
>
<span className="text-lg font-bold" style={textStyle}>
Saldo
</span>
<span
className="text-lg font-bold"
style={{
color:
apbdReport.totalIncome > apbdReport.totalExpenses
? "#22C55E"
: "#EF4444",
}}
>
Rp{" "}
{(
apbdReport.totalIncome - apbdReport.totalExpenses
).toLocaleString()}
jt
</span>
</div>
</div>
</div>
</div>
Rp{" "}
{(
apbdReport.totalIncome - apbdReport.totalExpenses
).toLocaleString()}
jt
</Text>
</Group>
</Box>
</Card>
</Grid.Col>
</Grid>
</Stack>
</Box>
);
};

View File

@@ -1,355 +1,99 @@
import {
Bar,
BarChart,
CartesianGrid,
Cell,
Pie,
PieChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { useMantineColorScheme } from "@mantine/core";
import { Grid, Stack } from "@mantine/core";
import { ActivityCard } from "./kinerja-divisi/activity-card";
import { DivisionList } from "./kinerja-divisi/division-list";
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 { ArchiveCard } from "./kinerja-divisi/archive-card";
// Data for program kegiatan (Section 1)
const programKegiatanData = [
{
title: "Rakor 2025",
date: "3 Juli 2025",
progress: 90,
status: "Selesai" as const,
},
{
title: "Pemutakhiran Indeks Desa",
date: "3 Juli 2025",
progress: 85,
status: "Selesai" as const,
},
{
title: "Mengurus Akta Cerai Warga",
date: "3 Juli 2025",
progress: 80,
status: "Selesai" as const,
},
{
title: "Pasek 7 Desa Adat",
date: "3 Juli 2025",
progress: 92,
status: "Selesai" as const,
},
];
// Data for arsip digital (Section 5)
const archiveData = [
{ name: "Surat Keputusan" },
{ name: "Dokumentasi" },
{ name: "Laporan Keuangan" },
{ name: "Notulensi Rapat" },
];
const KinerjaDivisi = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
// Top row - 4 activity cards
const activities = [
{
title: "Rakor 2025",
progress: 100,
date: "15 Jan 2025",
},
{
title: "Pemutakhiran Indeks Desa",
progress: 100,
date: "20 Feb 2025",
},
{
title: "Mengurus akta cerai warga",
progress: 100,
date: "5 Mar 2025",
},
{
title: "Pasek 7 desa adat",
progress: 100,
date: "10 Mar 2025",
},
];
// Document statistics
const documentStats = [
{ name: "Gambar", value: 300, color: "#FAC858" },
{ name: "Dokumen", value: 310, color: "#92CC76" },
];
// Activity progress statistics
const activityProgressStats = [
{ name: "Selesai", value: 83.33, fill: "#92CC76" },
{ name: "Dikerjakan", value: 16.67, fill: "#FAC858" },
{ name: "Segera Dikerjakan", value: 0, fill: "#5470C6" },
{ name: "Dibatalkan", value: 0, fill: "#EE6767" },
];
// Discussion data
const discussions = [
{
title: "Pembahasan APBDes 2026",
sender: "Kepala Desa",
date: "10 Mar 2025",
},
{
title: "Kegiatan Posyandu",
sender: "Divisi Sosial",
date: "9 Mar 2025",
},
{
title: "Festival Budaya",
sender: "Divisi Humas",
date: "8 Mar 2025",
},
];
return (
<div
className="min-h-screen"
style={{ backgroundColor: dark ? "#10192D" : "#F3F4F6" }}
>
{/* Top Row - 4 Activity Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
{activities.map((activity, index) => (
<div
key={index}
className="rounded-xl shadow-sm p-5"
style={{
backgroundColor: dark ? "#141D34" : "white",
border: `1px solid ${dark ? "#141D34" : "white"}`,
}}
>
{/* Dark blue title bar */}
<div
className="text-white px-3 py-2 rounded-t-lg -mx-5 -mt-5 mb-4"
style={{ backgroundColor: "#1E3A5F" }}
>
<h3 className="text-sm font-semibold">{activity.title}</h3>
</div>
{/* Orange progress bar */}
<div
className="w-full rounded-full h-2 mb-3"
style={{ backgroundColor: dark ? "#2d3748" : "#E5E7EB" }}
>
<div
className="bg-orange-500 h-2 rounded-full"
style={{ width: `${activity.progress}%` }}
/>
</div>
{/* Date and badge */}
<div className="flex justify-between items-center">
<span
className="text-xs"
style={{ color: dark ? "#9CA3AF" : "#6B7280" }}
>
{activity.date}
</span>
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full font-medium">
Selesai
</span>
</div>
</div>
<Stack gap="lg">
{/* SECTION 1 — PROGRAM KEGIATAN */}
<Grid gutter="md">
{programKegiatanData.map((kegiatan, index) => (
<Grid.Col key={index} span={{ base: 12, md: 6, lg: 3 }}>
<ActivityCard
title={kegiatan.title}
date={kegiatan.date}
progress={kegiatan.progress}
status={kegiatan.status}
/>
</Grid.Col>
))}
</div>
</Grid>
{/* Second Row - Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{/* Left Card - Jumlah Dokumen (Bar Chart) */}
<div
className="rounded-xl shadow-sm p-5"
style={{
backgroundColor: dark ? "#141D34" : "white",
border: `1px solid ${dark ? "#141D34" : "white"}`,
}}
>
<h3
className="text-lg font-semibold mb-4"
style={{ color: dark ? "white" : "#1F2937" }}
>
Jumlah Dokumen
</h3>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={documentStats}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#2d3748" : "#E5E7EB"}
/>
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Bar dataKey="value" radius={[4, 4, 0, 0]}>
{documentStats.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
{/* SECTION 2 — GRID DASHBOARD (3 Columns) */}
<Grid gutter="lg">
{/* Left Column - Division List */}
<Grid.Col span={{ base: 12, lg: 3 }}>
<DivisionList />
</Grid.Col>
{/* Right Card - Progres Kegiatan (Pie Chart) */}
<div
className="rounded-xl shadow-sm p-5"
style={{
backgroundColor: dark ? "#141D34" : "white",
border: `1px solid ${dark ? "#141D34" : "white"}`,
}}
>
<h3
className="text-lg font-semibold mb-4"
style={{ color: dark ? "white" : "#1F2937" }}
>
Progres Kegiatan
</h3>
<ResponsiveContainer width="100%" height={250}>
<PieChart>
<Pie
data={activityProgressStats.filter(item => item.value > 0)}
cx="50%"
cy="50%"
outerRadius={80}
dataKey="value"
label={({ name, percent }) =>
`${name}: ${percent ? (percent * 100).toFixed(0) : 0}%`
}
>
{activityProgressStats
.filter(item => item.value > 0)
.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.fill} />
))}
</Pie>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
</PieChart>
</ResponsiveContainer>
{/* Middle Column - Document Chart */}
<Grid.Col span={{ base: 12, lg: 5 }}>
<DocumentChart />
</Grid.Col>
{/* Legend */}
<div className="mt-4 space-y-2">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-blue-500"></div>
<span
className="text-sm"
style={{ color: dark ? "#9CA3AF" : "#4B5563" }}
>
Segera Dikerjakan
</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
<span
className="text-sm"
style={{ color: dark ? "#9CA3AF" : "#4B5563" }}
>
Dikerjakan
</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-green-500"></div>
<span
className="text-sm"
style={{ color: dark ? "#9CA3AF" : "#4B5563" }}
>
Selesai
</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-red-500"></div>
<span
className="text-sm"
style={{ color: dark ? "#9CA3AF" : "#4B5563" }}
>
Dibatalkan
</span>
</div>
</div>
</div>
</div>
{/* Right Column - Progress Chart */}
<Grid.Col span={{ base: 12, lg: 4 }}>
<ProgressChart />
</Grid.Col>
</Grid>
{/* Bottom Row */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left Card - Diskusi */}
<div
className="rounded-xl shadow-sm p-5"
style={{
backgroundColor: dark ? "#141D34" : "white",
border: `1px solid ${dark ? "#141D34" : "white"}`,
}}
>
<h3
className="text-lg font-semibold mb-4"
style={{ color: dark ? "white" : "#1F2937" }}
>
Diskusi
</h3>
<div className="space-y-3">
{discussions.map((discussion, index) => (
<div
key={index}
className="flex items-start gap-3 p-3 rounded-lg transition-colors"
style={{
backgroundColor: dark ? "#1F2937" : "#F9FAFB",
}}
>
<div className="flex-shrink-0">
<div
className="w-8 h-8 rounded-full flex items-center justify-center"
style={{ backgroundColor: "#DBEAFE" }}
>
<svg
className="w-4 h-4"
style={{ color: "#1E3A5F" }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
</div>
</div>
<div className="flex-1">
<h4
className="text-sm font-medium"
style={{ color: dark ? "white" : "#1F2937" }}
>
{discussion.title}
</h4>
<p
className="text-xs mt-1"
style={{ color: dark ? "#9CA3AF" : "#6B7280" }}
>
{discussion.sender} {discussion.date}
</p>
</div>
</div>
))}
</div>
</div>
{/* SECTION 3 — DISCUSSION PANEL */}
<DiscussionPanel />
{/* Right Card - Acara Hari Ini */}
<div
className="rounded-xl shadow-sm p-5"
style={{
backgroundColor: dark ? "#141D34" : "white",
border: `1px solid ${dark ? "#141D34" : "white"}`,
}}
>
<h3
className="text-lg font-semibold mb-4"
style={{ color: dark ? "white" : "#1F2937" }}
>
Acara Hari Ini
</h3>
<div className="flex items-center justify-center h-32">
<p
className="text-sm"
style={{ color: dark ? "#9CA3AF" : "#6B7280" }}
>
Tidak ada acara hari ini
</p>
</div>
</div>
</div>
</div>
{/* SECTION 4 — ACARA HARI INI */}
<EventCard />
{/* SECTION 5 — ARSIP DIGITAL PERANGKAT DESA */}
<Grid gutter="md">
{archiveData.map((item, index) => (
<Grid.Col key={index} span={{ base: 12, md: 6 }}>
<ArchiveCard item={item} />
</Grid.Col>
))}
</Grid>
</Stack>
);
};

View File

@@ -0,0 +1,89 @@
import { Card, Text, Progress, Group, Box } from "@mantine/core";
interface ActivityCardProps {
title: string;
date: string;
progress: number;
status: "Selesai" | "Berjalan" | "Tertunda";
}
export function ActivityCard({
title,
date,
progress,
status,
}: ActivityCardProps) {
const getStatusColor = () => {
switch (status) {
case "Selesai":
return "#22C55E";
case "Berjalan":
return "#3B82F6";
case "Tertunda":
return "#EF4444";
default:
return "#9CA3AF";
}
};
return (
<Card
radius="xl"
p={0}
withBorder={false}
style={{
backgroundColor: "#F3F4F6",
overflow: "hidden",
}}
>
{/* 🔵 HEADER */}
<Box
style={{
backgroundColor: "#1E3A5F",
padding: "16px",
textAlign: "center",
}}
>
<Text c="white" fw={700} size="md">
{title}
</Text>
</Box>
{/* CONTENT */}
<Box p="md">
{/* PROGRESS */}
<Progress
value={progress}
radius="xl"
size="lg"
color="orange"
styles={{
root: {
height: 16,
},
}}
/>
{/* FOOTER */}
<Group justify="space-between" mt="md">
<Text size="sm" fw={500}>
{date}
</Text>
<Box
style={{
backgroundColor: getStatusColor(),
color: "white",
padding: "4px 12px",
borderRadius: 999,
fontSize: 12,
fontWeight: 600,
}}
>
{status}
</Box>
</Group>
</Box>
</Card>
);
}

View File

@@ -0,0 +1,42 @@
import { Card, Group, Text, useMantineColorScheme } from "@mantine/core";
import { FileText } from "lucide-react";
interface ArchiveItem {
name: string;
}
interface ArchiveCardProps {
item: ArchiveItem;
onClick?: () => void;
}
export function ArchiveCard({ item, onClick }: ArchiveCardProps) {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return (
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "#e5e7eb",
boxShadow: dark
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
cursor: "pointer",
transition: "transform 0.2s, box-shadow 0.2s",
}}
h="100%"
onClick={onClick}
>
<Group gap="md">
<FileText size={32} color={dark ? "#60A5FA" : "#3B82F6"} />
<Text size="sm" fw={500} c={dark ? "white" : "#1E3A5F"}>
{item.name}
</Text>
</Group>
</Card>
);
}

View File

@@ -0,0 +1,93 @@
import {
Box,
Card,
Group,
Stack,
Text,
useMantineColorScheme,
} from "@mantine/core";
import { MessageCircle } from "lucide-react";
interface DiscussionItem {
message: string;
sender: string;
date: string;
}
const discussions: DiscussionItem[] = [
{
message: "Kepada Pelayanan, mohon di cek...",
sender: "I.B Surya Prabhawa Manu",
date: "12 Apr 2025",
},
{
message: "Kepada staf perencanaan @suar...",
sender: "Ni Nyoman Yuliani",
date: "14 Jun 2025",
},
{
message: "ijin atau mohon kepada KBD sar...",
sender: "Ni Wayan Martini",
date: "12 Apr 2025",
},
];
export function DiscussionPanel() {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return (
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: dark
? "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">
<MessageCircle size={20} color={dark ? "#E2E8F0" : "#1E3A5F"} />
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}>
Diskusi
</Text>
</Group>
<Stack gap="sm">
{discussions.map((discussion, index) => (
<Card
key={index}
p="sm"
radius="md"
withBorder
bg={dark ? "#334155" : "#F1F5F9"}
style={{
borderColor: dark ? "#334155" : "#F1F5F9",
}}
>
<Text
size="sm"
c={dark ? "white" : "#1E3A5F"}
fw={500}
mb="xs"
lineClamp={2}
>
{discussion.message}
</Text>
<Group justify="space-between">
<Text size="xs" c="dimmed">
{discussion.sender}
</Text>
<Text size="xs" c="dimmed">
{discussion.date}
</Text>
</Group>
</Card>
))}
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,77 @@
import {
Box,
Card,
Group,
Stack,
Text,
useMantineColorScheme,
} from "@mantine/core";
import { ChevronRight } from "lucide-react";
interface DivisionItem {
name: string;
count: number;
}
const divisionData: DivisionItem[] = [
{ name: "Kesejahteraan", count: 37 },
{ name: "Pemerintahan", count: 26 },
{ name: "Keuangan", count: 17 },
{ name: "Sekretaris Desa", count: 15 },
{ name: "Tata Usaha TK", count: 14 },
{ name: "Perangkat Kewilayahan", count: 12 },
{ name: "Pelayanan", count: 10 },
{ name: "Perencanaan", count: 9 },
{ name: "Tata Usaha & Umum", count: 7 },
];
export function DivisionList() {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return (
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: dark
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}}
h="100%"
>
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"} mb="md">
Divisi Teraktif
</Text>
<Stack gap="xs">
{divisionData.map((division, index) => (
<Group
key={index}
justify="space-between"
align="center"
style={{
padding: "8px 12px",
borderRadius: 8,
backgroundColor: dark ? "#334155" : "#F1F5F9",
transition: "background-color 0.2s",
cursor: "pointer",
}}
>
<Text size="sm" c={dark ? "white" : "#1E3A5F"}>
{division.name}
</Text>
<Group gap="xs">
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}>
{division.count}
</Text>
<ChevronRight size={16} color={dark ? "#94A3B8" : "#64748B"} />
</Group>
</Group>
))}
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,74 @@
import { Card, Text, useMantineColorScheme } from "@mantine/core";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
const documentData = [
{ name: "Gambar", jumlah: 300, color: "#FACC15" },
{ name: "Dokumen", jumlah: 310, color: "#22C55E" },
];
export function DocumentChart() {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return (
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: dark
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}}
h="100%"
>
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"} mb="md">
Jumlah Dokumen
</Text>
<ResponsiveContainer width="100%" height={200}>
<BarChart data={documentData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#334155" : "#e5e7eb"}
/>
<XAxis
dataKey="name"
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" }}
/>
<Bar dataKey="jumlah" radius={[4, 4, 0, 0]}>
{documentData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</Card>
);
}

View File

@@ -0,0 +1,66 @@
import {
Box,
Card,
Group,
Stack,
Text,
useMantineColorScheme,
} from "@mantine/core";
import { Calendar } from "lucide-react";
interface AgendaItem {
time: string;
event: string;
}
interface EventCardProps {
agendas?: AgendaItem[];
}
export function EventCard({ agendas = [] }: EventCardProps) {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return (
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: dark
? "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">
<Calendar size={20} color={dark ? "#E2E8F0" : "#1E3A5F"} />
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}>
Acara Hari Ini
</Text>
</Group>
{agendas.length > 0 ? (
<Stack gap="sm">
{agendas.map((agenda, index) => (
<Group key={index} align="flex-start" gap="md">
<Box w={60}>
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}>
{agenda.time}
</Text>
</Box>
<Text size="sm" c={dark ? "white" : "#1E3A5F"}>
{agenda.event}
</Text>
</Group>
))}
</Stack>
) : (
<Text c="dimmed" ta="center" py="md">
Tidak ada acara hari ini
</Text>
)}
</Card>
);
}

View File

@@ -0,0 +1,7 @@
export { ActivityCard } from "./activity-card";
export { ArchiveCard } from "./archive-card";
export { DiscussionPanel } from "./discussion-panel";
export { DivisionList } from "./division-list";
export { DocumentChart } from "./document-chart";
export { EventCard } from "./event-card";
export { ProgressChart } from "./progress-chart";

View File

@@ -0,0 +1,84 @@
import {
Box,
Card,
Group,
Stack,
Text,
useMantineColorScheme,
} from "@mantine/core";
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
const progressData = [
{ name: "Selesai", value: 83.33, color: "#22C55E" },
{ name: "Dikerjakan", value: 16.67, color: "#F59E0B" },
{ name: "Segera Dikerjakan", value: 0, color: "#3B82F6" },
{ name: "Dibatalkan", value: 0, color: "#EF4444" },
];
export function ProgressChart() {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return (
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: dark
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}}
h="100%"
>
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"} mb="md">
Progres Kegiatan
</Text>
<ResponsiveContainer width="100%" height={200}>
<PieChart>
<Pie
data={progressData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
paddingAngle={2}
dataKey="value"
>
{progressData.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">
{progressData.map((item, index) => (
<Group key={index} justify="space-between">
<Group gap="xs">
<Box
w={12}
h={12}
style={{ backgroundColor: item.color, borderRadius: 2 }}
/>
<Text size="sm" c={dark ? "white" : "gray.7"}>
{item.name}
</Text>
</Group>
<Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
{item.value.toFixed(2)}%
</Text>
</Group>
))}
</Stack>
</Card>
);
}

View File

@@ -1,16 +1,20 @@
import {
IconMessage,
IconAlertTriangle,
IconClock,
IconCheck,
IconChevronRight,
} from "@tabler/icons-react";
import { useMantineColorScheme } from "@mantine/core";
Badge,
Button,
Card,
Grid,
Group,
Stack,
Text,
ThemeIcon,
Title,
useMantineColorScheme
} from "@mantine/core";
import { CheckCircle, Clock, FileText, MessageCircle } from "lucide-react";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
Line,
LineChart,
ResponsiveContainer,
@@ -19,414 +23,408 @@ import {
YAxis,
} from "recharts";
// Summary data
const summaryData = [
{
title: "Total Pengaduan",
value: 42,
subtitle: "Bulan ini",
icon: MessageCircle,
color: "#1E3A5F",
},
{
title: "Baru",
value: 14,
subtitle: "Belum diproses",
icon: FileText,
color: "#1E3A5F",
},
{
title: "Diproses",
value: 14,
subtitle: "Sedang ditangani",
icon: Clock,
color: "#1E3A5F",
},
{
title: "Selesai",
value: 14,
subtitle: "Terselesaikan",
icon: CheckCircle,
color: "#1E3A5F",
},
];
// Tren pengaduan data
const trenData = [
{ bulan: "Apr", jumlah: 35 },
{ bulan: "Mei", jumlah: 48 },
{ bulan: "Jun", jumlah: 42 },
{ bulan: "Jul", jumlah: 55 },
{ bulan: "Agu", jumlah: 50 },
{ bulan: "Sep", jumlah: 58 },
{ bulan: "Okt", jumlah: 52 },
];
// Surat terbanyak data
const suratData = [
{ jenis: "KTP", jumlah: 24 },
{ jenis: "KK", jumlah: 18 },
{ jenis: "Domisili", jumlah: 15 },
{ jenis: "Usaha", jumlah: 12 },
{ jenis: "Lainnya", jumlah: 8 },
];
// Pengajuan terbaru data
const pengajuanTerbaru = [
{
nama: "Budi Santoso",
jenis: "Ketertiban Umum",
waktu: "2 jam yang lalu",
status: "baru",
},
{
nama: "Siti Rahayu",
jenis: "Pelayanan Kesehatan",
waktu: "5 jam yang lalu",
status: "proses",
},
{
nama: "Ahmad Fauzi",
jenis: "Infrastruktur",
waktu: "1 hari yang lalu",
status: "selesai",
},
{
nama: "Dewi Lestari",
jenis: "Administrasi",
waktu: "1 hari yang lalu",
status: "baru",
},
{
nama: "Joko Widodo",
jenis: "Keamanan",
waktu: "2 hari yang lalu",
status: "proses",
},
];
// Ide inovatif data
const ideInovatif = [
{
nama: "Andi Prasetyo",
judul: "Penerapan Smart Village",
waktu: "3 hari yang lalu",
kategori: "Teknologi",
},
{
nama: "Rina Kusuma",
judul: "Program Ekowisata Desa",
waktu: "5 hari yang lalu",
kategori: "Ekonomi",
},
{
nama: "Bambang Suryono",
judul: "Peningkatan Sanitasi",
waktu: "1 minggu yang lalu",
kategori: "Kesehatan",
},
{
nama: "Lina Marlina",
judul: "Pusat Kreatif Anak Muda",
waktu: "2 minggu yang lalu",
kategori: "Pendidikan",
},
];
const getStatusColor = (status: string) => {
switch (status) {
case "baru":
return "red";
case "proses":
return "blue";
case "selesai":
return "green";
default:
return "gray";
}
};
const PengaduanLayananPublik = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
// Statistic cards data
const statsData = [
{
title: "Total Pengaduan",
value: 156,
subtitle: "+12% dari bulan lalu",
icon: IconMessage,
},
{
title: "Pengaduan Baru",
value: 24,
subtitle: "Perlu tindakan segera",
icon: IconAlertTriangle,
},
{
title: "Sedang Diproses",
value: 48,
subtitle: "Dalam penanganan",
icon: IconClock,
},
{
title: "Selesai",
value: 84,
subtitle: "92% tingkat kepuasan",
icon: IconCheck,
},
];
// Line chart data for complaint trends
const trendData = [
{ month: "Jan", complaints: 32 },
{ month: "Feb", complaints: 45 },
{ month: "Mar", complaints: 38 },
{ month: "Apr", complaints: 52 },
{ month: "Mei", complaints: 48 },
{ month: "Jun", complaints: 61 },
];
// Horizontal bar chart data for most requested documents
const documentData = [
{ name: "KTP", count: 145 },
{ name: "Kartu Keluarga", count: 128 },
{ name: "Surat Domisili", count: 96 },
{ name: "Surat Usaha", count: 74 },
{ name: "SKCK", count: 52 },
];
// Recent applications data
const recentApplications = [
{
id: 1,
name: "Budi Santoso",
type: "KTP Elektronik",
date: "10 Mar 2025",
status: "Selesai",
statusColor: "green",
statusText: "bg-green-100 text-green-800",
},
{
id: 2,
name: "Siti Aminah",
type: "Surat Domisili",
date: "10 Mar 2025",
status: "Diproses",
statusColor: "yellow",
statusText: "bg-yellow-100 text-yellow-800",
},
{
id: 3,
name: "Ahmad Fauzi",
type: "Kartu Keluarga",
date: "9 Mar 2025",
status: "Baru",
statusColor: "blue",
statusText: "bg-blue-100 text-blue-800",
},
{
id: 4,
name: "Dewi Lestari",
type: "Surat Usaha",
date: "9 Mar 2025",
status: "Selesai",
statusColor: "green",
statusText: "bg-green-100 text-green-800",
},
{
id: 5,
name: "Joko Widodo",
type: "SKCK",
date: "8 Mar 2025",
status: "Diproses",
statusColor: "yellow",
statusText: "bg-yellow-100 text-yellow-800",
},
];
// Innovation ideas data
const innovationIdeas = [
{
id: 1,
title: "Sistem Antrian Online",
submitter: "Andi Prasetyo",
category: "Teknologi",
},
{
id: 2,
title: "Layanan Jemput Dokumen",
submitter: "Rina Kusuma",
category: "Pelayanan",
},
{
id: 3,
title: "Digitalisasi Arsip Desa",
submitter: "Bambang Suryono",
category: "Administrasi",
},
{
id: 4,
title: "Aplikasi Pengaduan Mobile",
submitter: "Lina Marlina",
category: "Teknologi",
},
];
const COLORS = ["#1E3A5F", "#3B82F6", "#60A5FA", "#93C5FD", "#DBEAFE"];
const cardStyle = {
backgroundColor: dark ? "#141D34" : "white",
border: `1px solid ${dark ? "#141D34" : "white"}`,
};
const textStyle = {
color: dark ? "white" : "#1F2937",
};
const subtitleStyle = {
color: dark ? "#9CA3AF" : "#6B7280",
};
return (
<div
className="min-h-screen"
style={{ backgroundColor: dark ? "#10192D" : "#F3F4F6" }}
>
<div className="max-w-7xl mx-auto">
{/* Row 1: 4 Statistic Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
{statsData.map((stat, index) => (
<div
key={index}
className="rounded-xl shadow-sm p-6"
style={cardStyle}
<Stack gap="lg">
{/* TOP SECTION - 4 STAT CARDS */}
<Grid gutter="md">
{summaryData.map((item, index) => (
<Grid.Col key={index} span={{ base: 12, sm: 6, lg: 3 }}>
<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)",
transition: "transform 0.15s ease, box-shadow 0.15s ease",
}}
h="100%"
>
<div className="flex items-center justify-between">
<div className="flex-1">
<h3
className="text-sm font-medium mb-1"
style={subtitleStyle}
>
{stat.title}
</h3>
<p
className="text-3xl font-bold mb-1"
style={textStyle}
>
{stat.value}
</p>
<p
className="text-xs"
style={subtitleStyle}
>
{stat.subtitle}
</p>
</div>
<div className="flex-shrink-0 ml-4">
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-white"
style={{ backgroundColor: "#1E3A5F" }}
>
<stat.icon size={24} />
</div>
</div>
</div>
</div>
))}
</div>
<Group justify="space-between" align="center" w="100%">
<Stack gap={2}>
<Text size="sm" c="dimmed">
{item.title}
</Text>
<Text size="xl" fw={700} c={dark ? "white" : "gray.9"}>
{item.value}
</Text>
<Text size="xs" c="dimmed">
{item.subtitle}
</Text>
</Stack>
<ThemeIcon
color={item.color}
variant="filled"
size="lg"
radius="xl"
style={{
transition: "transform 0.15s ease",
}}
>
<item.icon style={{ width: "60%", height: "60%" }} />
</ThemeIcon>
</Group>
</Card>
</Grid.Col>
))}
</Grid>
{/* Row 2: Full Width Line Chart */}
<div
className="rounded-xl shadow-sm p-6 mb-6"
style={cardStyle}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Tren Pengaduan Warga
</h3>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={trendData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#2d3748" : "#E5E7EB"}
/>
<XAxis
dataKey="month"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Line
type="monotone"
dataKey="complaints"
stroke="#1E3A5F"
strokeWidth={3}
dot={{ fill: "#1E3A5F", strokeWidth: 2, r: 5 }}
activeDot={{ r: 7 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
{/* MAIN CHART - TREN PENGADUAN */}
<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)",
}}
>
<Group justify="space-between" mb="md">
<Title order={4} c={dark ? "white" : "gray.9"}>
Tren Pengaduan
</Title>
</Group>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={trenData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#334155" : "#e5e7eb"}
/>
<XAxis
dataKey="bulan"
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" }}
/>
<Line
type="monotone"
dataKey="jumlah"
stroke="#1E3A5F"
strokeWidth={2}
dot={{
fill: "#1E3A5F",
strokeWidth: 2,
r: 4,
}}
activeDot={{ r: 6 }}
/>
</LineChart>
</ResponsiveContainer>
</Card>
{/* Row 3: 3 Column Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left: Most Requested Documents (Horizontal Bar Chart) */}
<div
className="rounded-xl shadow-sm p-6"
style={cardStyle}
{/* BOTTOM SECTION - 3 COLUMNS */}
<Grid gutter="md">
{/* LEFT: SURAT TERBANYAK */}
<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%"
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Dokumen Paling Banyak Diminta
</h3>
<ResponsiveContainer width="100%" height={280}>
<BarChart data={documentData} layout="vertical">
<Title order={4} c={dark ? "white" : "gray.9"} mb="md">
Surat Terbanyak
</Title>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={suratData} layout="vertical">
<CartesianGrid
strokeDasharray="3 3"
horizontal={false}
stroke={dark ? "#2d3748" : "#E5E7EB"}
stroke={dark ? "#334155" : "#e5e7eb"}
/>
<XAxis
type="number"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
/>
<YAxis
dataKey="name"
type="category"
dataKey="jenis"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#374151" }}
width={120}
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
width={80}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
backgroundColor: dark ? "#1E293B" : "white",
borderColor: dark ? "#334155" : "#e5e7eb",
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Bar dataKey="count" radius={[0, 4, 4, 0]}>
{documentData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
))}
</Bar>
<Bar dataKey="jumlah" fill="#1E3A5F" radius={[0, 4, 4, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</Card>
</Grid.Col>
{/* Middle: Recent Applications */}
<div
className="rounded-xl shadow-sm p-6"
style={cardStyle}
{/* CENTER: PENGAJUAN TERBARU */}
<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%"
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
<Title order={4} c={dark ? "white" : "gray.9"} mb="md">
Pengajuan Terbaru
</h3>
<div className="space-y-4">
{recentApplications.map((app) => (
<div
key={app.id}
className="flex items-center justify-between py-3"
</Title>
<Stack gap="sm">
{pengajuanTerbaru.map((item, index) => (
<Card
key={index}
p="sm"
radius="md"
withBorder
bg={dark ? "#334155" : "#F1F5F9"}
style={{
borderBottom: `1px solid ${dark ? "#2d3748" : "#E5E7EB"}`,
borderColor: "transparent",
transition: "background-color 0.15s ease",
}}
>
<div className="flex-1">
<p
className="text-sm font-medium"
style={textStyle}
>
{app.name}
</p>
<p
className="text-xs"
style={subtitleStyle}
>
{app.type}
</p>
</div>
<div className="text-right">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${app.statusText}`}
>
{app.status}
</span>
<p
className="text-xs mt-1"
style={subtitleStyle}
>
{app.date}
</p>
</div>
</div>
<Group justify="space-between">
<Stack gap={0}>
<Text fw={600} c={dark ? "white" : "gray.9"}>
{item.nama}
</Text>
<Text size="sm" c="dimmed">
{item.jenis}
</Text>
</Stack>
<Stack gap={0} align="flex-end">
<Badge
color={getStatusColor(item.status)}
variant="light"
radius="sm"
>
{item.status}
</Badge>
<Text size="xs" c="dimmed">
{item.waktu}
</Text>
</Stack>
</Group>
</Card>
))}
</div>
</div>
</Stack>
</Card>
</Grid.Col>
{/* Right: Innovation Ideas */}
<div
className="rounded-xl shadow-sm p-6"
style={cardStyle}
{/* RIGHT: AJUAN IDE INOVATIF */}
<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%"
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Ide Inovatif Warga
</h3>
<div className="space-y-4">
{innovationIdeas.map((idea) => (
<div
key={idea.id}
className="py-3"
<Title order={4} c={dark ? "white" : "gray.9"} mb="md">
Ajuan Ide Inovatif
</Title>
<Stack gap="sm">
{ideInovatif.map((item, index) => (
<Card
key={index}
p="sm"
radius="md"
withBorder
bg={dark ? "#334155" : "#F1F5F9"}
style={{
borderBottom: `1px solid ${dark ? "#2d3748" : "#E5E7EB"}`,
borderColor: "transparent",
transition: "background-color 0.15s ease",
}}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<p
className="text-sm font-medium"
style={textStyle}
>
{idea.title}
</p>
<p
className="text-xs mt-1"
style={subtitleStyle}
>
{idea.submitter}
</p>
</div>
<div className="flex items-center gap-2">
<span
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
style={{
backgroundColor: dark ? "#2d3748" : "#F3F4F6",
color: dark ? "#E5E7EB" : "#1F2937",
}}
>
{idea.category}
</span>
<button
className="p-1"
style={{
color: dark ? "#60A5FA" : "#2563EB",
}}
>
<IconChevronRight size={20} />
</button>
</div>
</div>
</div>
<Group justify="space-between">
<Stack gap={0}>
<Text fw={600} c={dark ? "white" : "gray.9"}>
{item.judul}
</Text>
<Text size="sm" c="dimmed">
{item.nama}
</Text>
<Text size="xs" c="dimmed">
{item.waktu}
</Text>
</Stack>
<Button
size="xs"
variant="light"
color="darmasaba-blue"
radius="md"
>
Detail
</Button>
</Group>
</Card>
))}
</div>
</div>
</div>
</div>
</div>
</Stack>
</Card>
</Grid.Col>
</Grid>
</Stack>
);
};

View File

@@ -1,11 +1,14 @@
import {
Badge,
Box,
Collapse,
Group,
Image,
Input,
NavLink as MantineNavLink,
Stack,
useMantineColorScheme
Text,
useMantineColorScheme,
} from "@mantine/core";
import { useLocation, useNavigate } from "@tanstack/react-router";
import { ChevronDown, ChevronUp, Search } from "lucide-react";
@@ -25,35 +28,29 @@ export function Sidebar({ className }: SidebarProps) {
// State for settings submenu collapse
const [settingsOpen, setSettingsOpen] = useState(
location.pathname.startsWith("/dashboard/pengaturan"),
location.pathname.startsWith("/pengaturan"),
);
// Define menu items with their paths
const menuItems = [
{ name: "Beranda", path: "/dashboard" },
{ name: "Kinerja Divisi", path: "/dashboard/kinerja-divisi" },
{
name: "Pengaduan & Layanan Publik",
path: "/dashboard/pengaduan-layanan-publik",
},
{ name: "Jenna Analytic", path: "/dashboard/jenna-analytic" },
{
name: "Demografi & Kependudukan",
path: "/dashboard/demografi-pekerjaan",
},
{ name: "Keuangan & Anggaran", path: "/dashboard/keuangan-anggaran" },
{ name: "Bumdes & UMKM Desa", path: "/dashboard/bumdes" },
{ name: "Sosial", path: "/dashboard/sosial" },
{ name: "Keamanan", path: "/dashboard/keamanan" },
{ name: "Bantuan", path: "/dashboard/bantuan" },
{ name: "Beranda", path: "/" },
{ name: "Kinerja Divisi", path: "/kinerja-divisi" },
{ name: "Pengaduan & Layanan Publik", path: "/pengaduan-layanan-publik" },
{ name: "Jenna Analytic", path: "/jenna-analytic" },
{ name: "Demografi & Kependudukan", path: "/demografi-pekerjaan" },
{ name: "Keuangan & Anggaran", path: "/keuangan-anggaran" },
{ name: "Bumdes & UMKM Desa", path: "/bumdes" },
{ name: "Sosial", path: "/sosial" },
{ name: "Keamanan", path: "/keamanan" },
{ name: "Bantuan", path: "/bantuan" },
];
// Settings submenu items
const settingsItems = [
{ name: "Umum", path: "/dashboard/pengaturan/umum" },
{ name: "Notifikasi", path: "/dashboard/pengaturan/notifikasi" },
{ name: "Keamanan", path: "/dashboard/pengaturan/keamanan" },
{ name: "Akses & Tim", path: "/dashboard/pengaturan/akses-dan-tim" },
{ name: "Umum", path: "/pengaturan/umum" },
{ name: "Notifikasi", path: "/pengaturan/notifikasi" },
{ name: "Keamanan", path: "/pengaturan/keamanan" },
{ name: "Akses & Tim", path: "/pengaturan/akses-dan-tim" },
];
// Check if any settings submenu is active
@@ -61,12 +58,10 @@ export function Sidebar({ className }: SidebarProps) {
(item) => location.pathname === item.path,
);
const headerBgColor = colorScheme === "dark" ? "#ebedf0ff" : "#19355E";
return (
<Box className={className}>
{/* Logo */}
<Image src={"/logo-desa-plus.png"} width={201} height={84} />
<Image src="/logo-desa-plus.png" alt="Logo" />
{/* Search */}
<Box p="md">
@@ -94,7 +89,7 @@ export function Sidebar({ className }: SidebarProps) {
label={item.name}
active={isActive}
variant="subtle"
color={headerBgColor}
color="blue"
style={{
background: isActive ? isActiveBg : "transparent",
fontWeight: isActive ? "bold" : "normal",
@@ -186,6 +181,5 @@ export function Sidebar({ className }: SidebarProps) {
</Box>
</Stack>
</Box>
);
}

View File

@@ -1,481 +1,464 @@
import {
Badge,
Card,
Grid,
GridCol,
Group,
List,
Progress,
Stack,
Text,
ThemeIcon,
Title,
useMantineColorScheme,
} from "@mantine/core";
import {
IconAward,
IconBabyCarriage,
IconBook,
IconCalendarEvent,
IconHeartbeat,
IconMedicalCross,
IconSchool,
IconStethoscope,
IconUsers,
} from "@tabler/icons-react";
import { useMantineColorScheme } from "@mantine/core";
import { useState } from "react";
const SosialPage = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
// Health statistics data
const healthStats = [
{
title: "Ibu Hamil Aktif",
value: "87",
subtitle: "Aktif",
icon: IconHeartbeat,
},
{
title: "Balita Terdaftar",
value: "342",
subtitle: "Terdaftar",
icon: IconBabyCarriage,
},
{
title: "Alert Stunting",
value: "12",
subtitle: "Perlu perhatian",
icon: IconStethoscope,
alert: true,
},
{
title: "Posyandu Aktif",
value: "8",
subtitle: "Beroperasi",
icon: IconMedicalCross,
},
];
// Sample data for health statistics
const healthStats = {
ibuHamil: 87,
balita: 342,
alertStunting: 12,
posyanduAktif: 8,
};
// Health progress data
// Sample data for health progress
const healthProgress = [
{ label: "Imunisasi Lengkap", value: 92 },
{ label: "Pemeriksaan Rutin", value: 88 },
{ label: "Gizi Baik", value: 86 },
{ label: "Target Stunting", value: 14, isAlert: true },
{ 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" },
];
// Posyandu schedule data
// Sample data for posyandu schedule
const posyanduSchedule = [
{
nama: "Posyandu Barat",
tanggal: "5 Oktober 2025",
nama: "Posyandu Mawar",
tanggal: "Senin, 15 Feb 2026",
jam: "08:00 - 11:00",
},
{
nama: "Posyandu Timur",
tanggal: "6 Oktober 2025",
nama: "Posyandu Melati",
tanggal: "Selasa, 16 Feb 2026",
jam: "08:00 - 11:00",
},
{
nama: "Posyandu Utara",
tanggal: "7 Oktober 2025",
nama: "Posyandu Dahlia",
tanggal: "Rabu, 17 Feb 2026",
jam: "08:00 - 11:00",
},
{
nama: "Posyandu Selatan",
tanggal: "8 Oktober 2025",
nama: "Posyandu Anggrek",
tanggal: "Kamis, 18 Feb 2026",
jam: "08:00 - 11:00",
},
];
// Education stats data
const educationStats = [
{ level: "TK / PAUD", value: "500" },
{ level: "Siswa SD", value: "458" },
{ level: "Siswa SMP", value: "234" },
{ level: "Siswa SMA", value: "189" },
];
// Sample data for education stats
const educationStats = {
siswa: {
tk: 125,
sd: 480,
smp: 210,
sma: 150,
},
sekolah: {
jumlah: 8,
guru: 42,
},
};
// School info data
const schoolInfo = [
{ label: "Lembaga Pendidikan", value: "10" },
{ label: "Tenaga Pengajar", value: "3" },
];
// Scholarship data
// Sample data for scholarships
const scholarshipData = {
penerima: "250+",
dana: "1.5M",
penerima: 45,
dana: "Rp 1.200.000.000",
tahunAjaran: "2025/2026",
};
// Cultural events data
// Sample data for cultural events
const culturalEvents = [
{
nama: "Lomba Baris Berbaris",
tanggal: "1 Desember 2025",
nama: "Hari Kesaktian Pancasila",
tanggal: "1 Oktober 2025",
lokasi: "Balai Desa",
},
{
nama: "Festival Budaya Desa",
tanggal: "20 Mei 2026",
lokasi: "Lapangan Desa",
},
{
nama: "Lomba Tari Tradisional",
tanggal: "10 Desember 2025",
lokasi: "Banjar Desa",
},
{
nama: "Davoz",
tanggal: "20 Desember 2025",
lokasi: "Kantor Desa",
nama: "Perayaan HUT Desa",
tanggal: "17 Agustus 2026",
lokasi: "Balai Desa",
},
];
const cardStyle = {
backgroundColor: dark ? "#1E293B" : "white",
border: `1px solid ${dark ? "#1E293B" : "white"}`,
};
const textStyle = {
color: dark ? "white" : "#1F2937",
};
const subtitleStyle = {
color: dark ? "#9CA3AF" : "#6B7280",
};
return (
<div
className="min-h-screen"
style={{ backgroundColor: dark ? "#0F172A" : "#F3F4F6" }}
>
<div className="max-w-7xl mx-auto">
{/* Row 1: Top 4 Metrics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
{healthStats.map((stat, index) => (
<div
key={index}
className="rounded-xl shadow-sm p-6"
style={cardStyle}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<h3
className="text-sm font-medium mb-1"
style={subtitleStyle}
>
{stat.title}
</h3>
<p
className="text-3xl font-bold mb-1"
style={
stat.alert ? { color: "#EF4444" } : textStyle
}
>
{stat.value}
</p>
<p
className="text-xs"
style={subtitleStyle}
>
{stat.subtitle}
</p>
</div>
<div className="flex-shrink-0 ml-4">
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-white"
style={{
backgroundColor: stat.alert ? "#EF4444" : "#1F3A5F",
}}
>
<stat.icon size={24} />
</div>
</div>
</div>
<Stack gap="lg">
{/* Health Statistics Cards */}
<Grid gutter="md">
<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"}>
Ibu Hamil Aktif
</Text>
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
{healthStats.ibuHamil}
</Text>
</Stack>
<ThemeIcon
variant="light"
color="darmasaba-blue"
size="xl"
radius="xl"
>
<IconHeartbeat size={24} />
</ThemeIcon>
</Group>
</Card>
</GridCol>
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
<Card
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"}>
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>
</Grid>
{/* Health Progress Bars */}
<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>
))}
</div>
</Stack>
</Card>
{/* Row 2: Statistik Kesehatan */}
<div
className="rounded-xl shadow-sm p-6 mb-6"
style={cardStyle}
>
<h3
className="text-lg font-semibold mb-6"
style={textStyle}
<Grid gutter="md">
{/* Jadwal Posyandu */}
<GridCol span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
Statistik Kesehatan
</h3>
<div className="space-y-4">
{healthProgress.map((item, index) => (
<div key={index}>
<div className="flex items-center justify-between mb-2">
<span
className="text-sm font-medium"
style={textStyle}
>
{item.label}
</span>
<span
className="text-sm font-semibold"
style={
item.isAlert
? { color: "#EF4444" }
: textStyle
}
>
{item.value}%
</span>
</div>
<div
className="w-full rounded-full h-2"
style={{ backgroundColor: dark ? "#334155" : "#E5E7EB" }}
>
<div
className="h-2 rounded-full transition-all"
style={{
width: `${item.value}%`,
backgroundColor: item.isAlert
? "#EF4444"
: "#1F3A5F",
}}
/>
</div>
</div>
))}
</div>
</div>
{/* Row 3: Jadwal Posyandu & Pendidikan */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{/* Jadwal Posyandu */}
<div
className="rounded-xl shadow-sm p-6"
style={cardStyle}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
Jadwal Posyandu
</h3>
<div className="space-y-3">
</Title>
<Stack gap="sm">
{posyanduSchedule.map((item, index) => (
<div
<Card
key={index}
className="p-4 rounded-lg"
style={{
backgroundColor: dark ? "#334155" : "#F9FAFB",
}}
p="md"
radius="md"
withBorder
bg={dark ? "#263852ff" : "#F1F5F9"}
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
h="100%"
>
<div className="flex items-center justify-between">
<div>
<p
className="text-sm font-medium"
style={textStyle}
>
<Group justify="space-between">
<Stack gap={0}>
<Text fw={500} c={dark ? "dark.0" : "black"}>
{item.nama}
</p>
<p
className="text-xs mt-1"
style={subtitleStyle}
>
</Text>
<Text size="sm" c={dark ? "dark.0" : "black"}>
{item.tanggal}
</p>
</div>
<span
className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium"
style={{
backgroundColor: "#DBEAFE",
color: "#1E3A5F",
}}
>
</Text>
</Stack>
<Badge variant="light" color="darmasaba-blue">
{item.jam}
</span>
</div>
</div>
</Badge>
</Group>
</Card>
))}
</div>
</div>
</Stack>
</Card>
</GridCol>
{/* Pendidikan Section */}
<div
className="rounded-xl shadow-sm p-6"
style={cardStyle}
{/* Pendidikan */}
<GridCol span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
h="100%"
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
Pendidikan
</h3>
<div className="space-y-3 mb-6">
{educationStats.map((item, index) => (
<div
key={index}
className="flex items-center justify-between py-2"
style={{
borderBottom: `1px solid ${dark ? "#334155" : "#F3F4F6"}`,
}}
>
<span className="text-sm" style={subtitleStyle}>
{item.level}
</span>
<span
className="text-sm font-semibold"
style={textStyle}
>
{item.value}
</span>
</div>
))}
</div>
</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>
{/* Info Sekolah */}
<h4
className="text-base font-semibold mb-4"
style={textStyle}
>
Info Sekolah
</h4>
<div className="space-y-3">
{schoolInfo.map((item, index) => (
<div
key={index}
className="flex items-center justify-between py-3 px-4 rounded-lg"
style={{
backgroundColor: dark ? "#334155" : "#F9FAFB",
}}
>
<span className="text-sm" style={subtitleStyle}>
{item.label}
</span>
<span
className="text-lg font-bold"
style={textStyle}
>
{item.value}
</span>
</div>
))}
</div>
</div>
</div>
{/* Row 4: Beasiswa Desa & Kalender Event Budaya */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Beasiswa Desa */}
<div
className="rounded-xl shadow-sm p-6"
style={cardStyle}
>
<div className="flex items-center justify-between mb-6">
<h3
className="text-lg font-semibold"
style={textStyle}
<Card
withBorder
radius="md"
p="md"
mt="md"
bg={dark ? "#263852ff" : "#F1F5F9"}
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
>
Beasiswa Desa
</h3>
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-white"
style={{ backgroundColor: "#22C55E" }}
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "black"}>
Jumlah Lembaga Pendidikan
</Text>
<Text fw={700} c={dark ? "dark.0" : "black"}>
{educationStats.sekolah.jumlah}
</Text>
</Group>
<Group justify="space-between" mt="sm">
<Text fw={500} c={dark ? "dark.0" : "black"}>
Jumlah Tenaga Pengajar
</Text>
<Text fw={700} c={dark ? "dark.0" : "black"}>
{educationStats.sekolah.guru}
</Text>
</Group>
</Card>
</Stack>
</Card>
</GridCol>
</Grid>
<Grid gutter="md">
{/* Beasiswa Desa */}
<GridCol span={{ base: 12, lg: 6 }}>
<Card
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} />
</div>
</div>
</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>
{/* Two centered metrics */}
<div className="grid grid-cols-2 gap-4 mb-6">
<div
className="p-4 rounded-lg text-center"
style={{
backgroundColor: dark ? "#334155" : "#F9FAFB",
}}
>
<p
className="text-3xl font-bold mb-1"
style={textStyle}
>
{scholarshipData.penerima}
</p>
<p
className="text-xs"
style={subtitleStyle}
>
Penerima Beasiswa
</p>
</div>
<div
className="p-4 rounded-lg text-center"
style={{
backgroundColor: dark ? "#334155" : "#F9FAFB",
}}
>
<p
className="text-3xl font-bold mb-1"
style={{ color: "#22C55E" }}
>
{scholarshipData.dana}
</p>
<p
className="text-xs"
style={subtitleStyle}
>
Dana Tersalurkan
</p>
</div>
</div>
{/* Footer text */}
<p
className="text-center text-sm"
style={subtitleStyle}
>
Tahun Ajaran {scholarshipData.tahunAjaran}
</p>
</div>
{/* Kalender Event Budaya */}
<div
className="rounded-xl shadow-sm p-6"
style={cardStyle}
{/* 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%"
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
Kalender Event Budaya
</h3>
<div className="space-y-4">
</Title>
<List spacing="sm">
{culturalEvents.map((event, index) => (
<div
<List.Item
key={index}
className="flex items-start gap-3 p-4 rounded-lg"
style={{
backgroundColor: dark ? "#334155" : "#F9FAFB",
}}
icon={
<ThemeIcon color="darmasaba-blue" size={24} radius="xl">
<IconCalendarEvent size={12} />
</ThemeIcon>
}
>
<div
className="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0"
style={{
backgroundColor: "#DBEAFE",
color: "#1E3A5F",
}}
>
<IconCalendarEvent size={20} />
</div>
<div className="flex-1">
<p
className="text-sm font-medium"
style={textStyle}
>
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "black"}>
{event.nama}
</p>
<p
className="text-xs mt-1"
style={subtitleStyle}
>
{event.tanggal}
</p>
<p
className="text-xs mt-1"
style={subtitleStyle}
>
Location: {event.lokasi}
</p>
</div>
</div>
</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>
))}
</div>
</div>
</div>
</div>
</div>
</List>
</Card>
</GridCol>
</Grid>
</Stack>
);
};

View File

@@ -110,12 +110,10 @@ if (!isProduction) {
end(data: any) {
// Handle potential Buffer or string data from Vite
let body = data;
// If we have collected chunks from write() calls, combine them
if (this._chunks && this._chunks.length > 0) {
body = Buffer.concat(this._chunks);
}
if (data instanceof Uint8Array) {
body = data;
} else if (typeof data === "string") {
@@ -251,4 +249,3 @@ console.log(
);
export type ApiApp = typeof app;

View File

@@ -60,13 +60,36 @@ type RouteRule = {
};
const routeRules: RouteRule[] = [
// Public routes - no auth required
{
match: (p) => p === "/" || p === "/signin" || p === "/signup",
requireAuth: false,
},
// Profile routes - auth required for all roles
{
match: (p) => p === "/profile" || p.startsWith("/profile/"),
requireAuth: true,
redirectTo: "/signin",
},
// Dashboard and main pages - auth required for all roles (not just admin)
{
match: (p) => p === "/admin" || p.startsWith("/admin/"),
match: (p) =>
p.startsWith("/kinerja-divisi") ||
p.startsWith("/pengaduan") ||
p.startsWith("/jenna") ||
p.startsWith("/demografi") ||
p.startsWith("/keuangan") ||
p.startsWith("/bumdes") ||
p.startsWith("/sosial") ||
p.startsWith("/keamanan") ||
p.startsWith("/bantuan") ||
p.startsWith("/pengaturan"),
requireAuth: true,
redirectTo: "/signin",
},
// Admin routes - auth required with admin role only
{
match: (p) => p.startsWith("/admin"),
requireAuth: true,
requiredRole: "admin",
redirectTo: "/signin",
@@ -98,15 +121,22 @@ export function createProtectedRoute(options: ProtectedRouteOptions = {}) {
location: { pathname: string; href: string };
}) => {
const rule = findRouteRule(location.pathname);
// If no rule matches, allow access by default
if (!rule) return;
// If route explicitly doesn't require auth, allow access
if (rule.requireAuth === false) return;
const session = await fetchSession();
const user = session?.user;
// If auth is required but user is not logged in, redirect to login
if (rule.requireAuth && !user) {
redirectToLogin(rule.redirectTo ?? redirectTo, location.href);
}
// If specific role is required, check it
if (rule.requiredRole && user?.role !== rule.requiredRole) {
redirectToLogin(rule.redirectTo ?? redirectTo, location.href);
}

View File

@@ -9,35 +9,38 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as SosialRouteImport } from './routes/sosial'
import { Route as SignupRouteImport } from './routes/signup'
import { Route as SigninRouteImport } from './routes/signin'
import { Route as DashboardRouteRouteImport } from './routes/dashboard/route'
import { Route as PengaduanLayananPublikRouteImport } from './routes/pengaduan-layanan-publik'
import { Route as KinerjaDivisiRouteImport } from './routes/kinerja-divisi'
import { Route as KeuanganAnggaranRouteImport } from './routes/keuangan-anggaran'
import { Route as KeamananRouteImport } from './routes/keamanan'
import { Route as JennaAnalyticRouteImport } from './routes/jenna-analytic'
import { Route as DemografiPekerjaanRouteImport } from './routes/demografi-pekerjaan'
import { Route as BumdesRouteImport } from './routes/bumdes'
import { Route as BantuanRouteImport } from './routes/bantuan'
import { Route as PengaturanRouteRouteImport } from './routes/pengaturan/route'
import { Route as AdminRouteRouteImport } from './routes/admin/route'
import { Route as IndexRouteImport } from './routes/index'
import { Route as UsersIndexRouteImport } from './routes/users/index'
import { Route as ProfileIndexRouteImport } from './routes/profile/index'
import { Route as DashboardIndexRouteImport } from './routes/dashboard/index'
import { Route as AdminIndexRouteImport } from './routes/admin/index'
import { Route as UsersIdRouteImport } from './routes/users/$id'
import { Route as ProfileEditRouteImport } from './routes/profile/edit'
import { Route as DashboardSosialRouteImport } from './routes/dashboard/sosial'
import { Route as DashboardPengaduanLayananPublikRouteImport } from './routes/dashboard/pengaduan-layanan-publik'
import { Route as DashboardKinerjaDivisiRouteImport } from './routes/dashboard/kinerja-divisi'
import { Route as DashboardKeuanganAnggaranRouteImport } from './routes/dashboard/keuangan-anggaran'
import { Route as DashboardKeamananRouteImport } from './routes/dashboard/keamanan'
import { Route as DashboardJennaAnalyticRouteImport } from './routes/dashboard/jenna-analytic'
import { Route as DashboardDemografiPekerjaanRouteImport } from './routes/dashboard/demografi-pekerjaan'
import { Route as DashboardBumdesRouteImport } from './routes/dashboard/bumdes'
import { Route as DashboardBantuanRouteImport } from './routes/dashboard/bantuan'
import { Route as PengaturanUmumRouteImport } from './routes/pengaturan/umum'
import { Route as PengaturanNotifikasiRouteImport } from './routes/pengaturan/notifikasi'
import { Route as PengaturanKeamananRouteImport } from './routes/pengaturan/keamanan'
import { Route as PengaturanAksesDanTimRouteImport } from './routes/pengaturan/akses-dan-tim'
import { Route as AdminUsersRouteImport } from './routes/admin/users'
import { Route as AdminSettingsRouteImport } from './routes/admin/settings'
import { Route as AdminApikeyRouteImport } from './routes/admin/apikey'
import { Route as DashboardPengaturanRouteRouteImport } from './routes/dashboard/pengaturan/route'
import { Route as DashboardPengaturanUmumRouteImport } from './routes/dashboard/pengaturan/umum'
import { Route as DashboardPengaturanNotifikasiRouteImport } from './routes/dashboard/pengaturan/notifikasi'
import { Route as DashboardPengaturanKeamananRouteImport } from './routes/dashboard/pengaturan/keamanan'
import { Route as DashboardPengaturanAksesDanTimRouteImport } from './routes/dashboard/pengaturan/akses-dan-tim'
const SosialRoute = SosialRouteImport.update({
id: '/sosial',
path: '/sosial',
getParentRoute: () => rootRouteImport,
} as any)
const SignupRoute = SignupRouteImport.update({
id: '/signup',
path: '/signup',
@@ -48,9 +51,49 @@ const SigninRoute = SigninRouteImport.update({
path: '/signin',
getParentRoute: () => rootRouteImport,
} as any)
const DashboardRouteRoute = DashboardRouteRouteImport.update({
id: '/dashboard',
path: '/dashboard',
const PengaduanLayananPublikRoute = PengaduanLayananPublikRouteImport.update({
id: '/pengaduan-layanan-publik',
path: '/pengaduan-layanan-publik',
getParentRoute: () => rootRouteImport,
} as any)
const KinerjaDivisiRoute = KinerjaDivisiRouteImport.update({
id: '/kinerja-divisi',
path: '/kinerja-divisi',
getParentRoute: () => rootRouteImport,
} as any)
const KeuanganAnggaranRoute = KeuanganAnggaranRouteImport.update({
id: '/keuangan-anggaran',
path: '/keuangan-anggaran',
getParentRoute: () => rootRouteImport,
} as any)
const KeamananRoute = KeamananRouteImport.update({
id: '/keamanan',
path: '/keamanan',
getParentRoute: () => rootRouteImport,
} as any)
const JennaAnalyticRoute = JennaAnalyticRouteImport.update({
id: '/jenna-analytic',
path: '/jenna-analytic',
getParentRoute: () => rootRouteImport,
} as any)
const DemografiPekerjaanRoute = DemografiPekerjaanRouteImport.update({
id: '/demografi-pekerjaan',
path: '/demografi-pekerjaan',
getParentRoute: () => rootRouteImport,
} as any)
const BumdesRoute = BumdesRouteImport.update({
id: '/bumdes',
path: '/bumdes',
getParentRoute: () => rootRouteImport,
} as any)
const BantuanRoute = BantuanRouteImport.update({
id: '/bantuan',
path: '/bantuan',
getParentRoute: () => rootRouteImport,
} as any)
const PengaturanRouteRoute = PengaturanRouteRouteImport.update({
id: '/pengaturan',
path: '/pengaturan',
getParentRoute: () => rootRouteImport,
} as any)
const AdminRouteRoute = AdminRouteRouteImport.update({
@@ -73,11 +116,6 @@ const ProfileIndexRoute = ProfileIndexRouteImport.update({
path: '/profile/',
getParentRoute: () => rootRouteImport,
} as any)
const DashboardIndexRoute = DashboardIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => DashboardRouteRoute,
} as any)
const AdminIndexRoute = AdminIndexRouteImport.update({
id: '/',
path: '/',
@@ -93,53 +131,25 @@ const ProfileEditRoute = ProfileEditRouteImport.update({
path: '/profile/edit',
getParentRoute: () => rootRouteImport,
} as any)
const DashboardSosialRoute = DashboardSosialRouteImport.update({
id: '/sosial',
path: '/sosial',
getParentRoute: () => DashboardRouteRoute,
const PengaturanUmumRoute = PengaturanUmumRouteImport.update({
id: '/umum',
path: '/umum',
getParentRoute: () => PengaturanRouteRoute,
} as any)
const DashboardPengaduanLayananPublikRoute =
DashboardPengaduanLayananPublikRouteImport.update({
id: '/pengaduan-layanan-publik',
path: '/pengaduan-layanan-publik',
getParentRoute: () => DashboardRouteRoute,
} as any)
const DashboardKinerjaDivisiRoute = DashboardKinerjaDivisiRouteImport.update({
id: '/kinerja-divisi',
path: '/kinerja-divisi',
getParentRoute: () => DashboardRouteRoute,
const PengaturanNotifikasiRoute = PengaturanNotifikasiRouteImport.update({
id: '/notifikasi',
path: '/notifikasi',
getParentRoute: () => PengaturanRouteRoute,
} as any)
const DashboardKeuanganAnggaranRoute =
DashboardKeuanganAnggaranRouteImport.update({
id: '/keuangan-anggaran',
path: '/keuangan-anggaran',
getParentRoute: () => DashboardRouteRoute,
} as any)
const DashboardKeamananRoute = DashboardKeamananRouteImport.update({
const PengaturanKeamananRoute = PengaturanKeamananRouteImport.update({
id: '/keamanan',
path: '/keamanan',
getParentRoute: () => DashboardRouteRoute,
getParentRoute: () => PengaturanRouteRoute,
} as any)
const DashboardJennaAnalyticRoute = DashboardJennaAnalyticRouteImport.update({
id: '/jenna-analytic',
path: '/jenna-analytic',
getParentRoute: () => DashboardRouteRoute,
} as any)
const DashboardDemografiPekerjaanRoute =
DashboardDemografiPekerjaanRouteImport.update({
id: '/demografi-pekerjaan',
path: '/demografi-pekerjaan',
getParentRoute: () => DashboardRouteRoute,
} as any)
const DashboardBumdesRoute = DashboardBumdesRouteImport.update({
id: '/bumdes',
path: '/bumdes',
getParentRoute: () => DashboardRouteRoute,
} as any)
const DashboardBantuanRoute = DashboardBantuanRouteImport.update({
id: '/bantuan',
path: '/bantuan',
getParentRoute: () => DashboardRouteRoute,
const PengaturanAksesDanTimRoute = PengaturanAksesDanTimRouteImport.update({
id: '/akses-dan-tim',
path: '/akses-dan-tim',
getParentRoute: () => PengaturanRouteRoute,
} as any)
const AdminUsersRoute = AdminUsersRouteImport.update({
id: '/users',
@@ -156,222 +166,192 @@ const AdminApikeyRoute = AdminApikeyRouteImport.update({
path: '/apikey',
getParentRoute: () => AdminRouteRoute,
} as any)
const DashboardPengaturanRouteRoute =
DashboardPengaturanRouteRouteImport.update({
id: '/pengaturan',
path: '/pengaturan',
getParentRoute: () => DashboardRouteRoute,
} as any)
const DashboardPengaturanUmumRoute = DashboardPengaturanUmumRouteImport.update({
id: '/umum',
path: '/umum',
getParentRoute: () => DashboardPengaturanRouteRoute,
} as any)
const DashboardPengaturanNotifikasiRoute =
DashboardPengaturanNotifikasiRouteImport.update({
id: '/notifikasi',
path: '/notifikasi',
getParentRoute: () => DashboardPengaturanRouteRoute,
} as any)
const DashboardPengaturanKeamananRoute =
DashboardPengaturanKeamananRouteImport.update({
id: '/keamanan',
path: '/keamanan',
getParentRoute: () => DashboardPengaturanRouteRoute,
} as any)
const DashboardPengaturanAksesDanTimRoute =
DashboardPengaturanAksesDanTimRouteImport.update({
id: '/akses-dan-tim',
path: '/akses-dan-tim',
getParentRoute: () => DashboardPengaturanRouteRoute,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/admin': typeof AdminRouteRouteWithChildren
'/dashboard': typeof DashboardRouteRouteWithChildren
'/pengaturan': typeof PengaturanRouteRouteWithChildren
'/bantuan': typeof BantuanRoute
'/bumdes': typeof BumdesRoute
'/demografi-pekerjaan': typeof DemografiPekerjaanRoute
'/jenna-analytic': typeof JennaAnalyticRoute
'/keamanan': typeof KeamananRoute
'/keuangan-anggaran': typeof KeuanganAnggaranRoute
'/kinerja-divisi': typeof KinerjaDivisiRoute
'/pengaduan-layanan-publik': typeof PengaduanLayananPublikRoute
'/signin': typeof SigninRoute
'/signup': typeof SignupRoute
'/dashboard/pengaturan': typeof DashboardPengaturanRouteRouteWithChildren
'/sosial': typeof SosialRoute
'/admin/apikey': typeof AdminApikeyRoute
'/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/dashboard/bantuan': typeof DashboardBantuanRoute
'/dashboard/bumdes': typeof DashboardBumdesRoute
'/dashboard/demografi-pekerjaan': typeof DashboardDemografiPekerjaanRoute
'/dashboard/jenna-analytic': typeof DashboardJennaAnalyticRoute
'/dashboard/keamanan': typeof DashboardKeamananRoute
'/dashboard/keuangan-anggaran': typeof DashboardKeuanganAnggaranRoute
'/dashboard/kinerja-divisi': typeof DashboardKinerjaDivisiRoute
'/dashboard/pengaduan-layanan-publik': typeof DashboardPengaduanLayananPublikRoute
'/dashboard/sosial': typeof DashboardSosialRoute
'/pengaturan/akses-dan-tim': typeof PengaturanAksesDanTimRoute
'/pengaturan/keamanan': typeof PengaturanKeamananRoute
'/pengaturan/notifikasi': typeof PengaturanNotifikasiRoute
'/pengaturan/umum': typeof PengaturanUmumRoute
'/profile/edit': typeof ProfileEditRoute
'/users/$id': typeof UsersIdRoute
'/admin/': typeof AdminIndexRoute
'/dashboard/': typeof DashboardIndexRoute
'/profile/': typeof ProfileIndexRoute
'/users/': typeof UsersIndexRoute
'/dashboard/pengaturan/akses-dan-tim': typeof DashboardPengaturanAksesDanTimRoute
'/dashboard/pengaturan/keamanan': typeof DashboardPengaturanKeamananRoute
'/dashboard/pengaturan/notifikasi': typeof DashboardPengaturanNotifikasiRoute
'/dashboard/pengaturan/umum': typeof DashboardPengaturanUmumRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/pengaturan': typeof PengaturanRouteRouteWithChildren
'/bantuan': typeof BantuanRoute
'/bumdes': typeof BumdesRoute
'/demografi-pekerjaan': typeof DemografiPekerjaanRoute
'/jenna-analytic': typeof JennaAnalyticRoute
'/keamanan': typeof KeamananRoute
'/keuangan-anggaran': typeof KeuanganAnggaranRoute
'/kinerja-divisi': typeof KinerjaDivisiRoute
'/pengaduan-layanan-publik': typeof PengaduanLayananPublikRoute
'/signin': typeof SigninRoute
'/signup': typeof SignupRoute
'/dashboard/pengaturan': typeof DashboardPengaturanRouteRouteWithChildren
'/sosial': typeof SosialRoute
'/admin/apikey': typeof AdminApikeyRoute
'/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/dashboard/bantuan': typeof DashboardBantuanRoute
'/dashboard/bumdes': typeof DashboardBumdesRoute
'/dashboard/demografi-pekerjaan': typeof DashboardDemografiPekerjaanRoute
'/dashboard/jenna-analytic': typeof DashboardJennaAnalyticRoute
'/dashboard/keamanan': typeof DashboardKeamananRoute
'/dashboard/keuangan-anggaran': typeof DashboardKeuanganAnggaranRoute
'/dashboard/kinerja-divisi': typeof DashboardKinerjaDivisiRoute
'/dashboard/pengaduan-layanan-publik': typeof DashboardPengaduanLayananPublikRoute
'/dashboard/sosial': typeof DashboardSosialRoute
'/pengaturan/akses-dan-tim': typeof PengaturanAksesDanTimRoute
'/pengaturan/keamanan': typeof PengaturanKeamananRoute
'/pengaturan/notifikasi': typeof PengaturanNotifikasiRoute
'/pengaturan/umum': typeof PengaturanUmumRoute
'/profile/edit': typeof ProfileEditRoute
'/users/$id': typeof UsersIdRoute
'/admin': typeof AdminIndexRoute
'/dashboard': typeof DashboardIndexRoute
'/profile': typeof ProfileIndexRoute
'/users': typeof UsersIndexRoute
'/dashboard/pengaturan/akses-dan-tim': typeof DashboardPengaturanAksesDanTimRoute
'/dashboard/pengaturan/keamanan': typeof DashboardPengaturanKeamananRoute
'/dashboard/pengaturan/notifikasi': typeof DashboardPengaturanNotifikasiRoute
'/dashboard/pengaturan/umum': typeof DashboardPengaturanUmumRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/admin': typeof AdminRouteRouteWithChildren
'/dashboard': typeof DashboardRouteRouteWithChildren
'/pengaturan': typeof PengaturanRouteRouteWithChildren
'/bantuan': typeof BantuanRoute
'/bumdes': typeof BumdesRoute
'/demografi-pekerjaan': typeof DemografiPekerjaanRoute
'/jenna-analytic': typeof JennaAnalyticRoute
'/keamanan': typeof KeamananRoute
'/keuangan-anggaran': typeof KeuanganAnggaranRoute
'/kinerja-divisi': typeof KinerjaDivisiRoute
'/pengaduan-layanan-publik': typeof PengaduanLayananPublikRoute
'/signin': typeof SigninRoute
'/signup': typeof SignupRoute
'/dashboard/pengaturan': typeof DashboardPengaturanRouteRouteWithChildren
'/sosial': typeof SosialRoute
'/admin/apikey': typeof AdminApikeyRoute
'/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/dashboard/bantuan': typeof DashboardBantuanRoute
'/dashboard/bumdes': typeof DashboardBumdesRoute
'/dashboard/demografi-pekerjaan': typeof DashboardDemografiPekerjaanRoute
'/dashboard/jenna-analytic': typeof DashboardJennaAnalyticRoute
'/dashboard/keamanan': typeof DashboardKeamananRoute
'/dashboard/keuangan-anggaran': typeof DashboardKeuanganAnggaranRoute
'/dashboard/kinerja-divisi': typeof DashboardKinerjaDivisiRoute
'/dashboard/pengaduan-layanan-publik': typeof DashboardPengaduanLayananPublikRoute
'/dashboard/sosial': typeof DashboardSosialRoute
'/pengaturan/akses-dan-tim': typeof PengaturanAksesDanTimRoute
'/pengaturan/keamanan': typeof PengaturanKeamananRoute
'/pengaturan/notifikasi': typeof PengaturanNotifikasiRoute
'/pengaturan/umum': typeof PengaturanUmumRoute
'/profile/edit': typeof ProfileEditRoute
'/users/$id': typeof UsersIdRoute
'/admin/': typeof AdminIndexRoute
'/dashboard/': typeof DashboardIndexRoute
'/profile/': typeof ProfileIndexRoute
'/users/': typeof UsersIndexRoute
'/dashboard/pengaturan/akses-dan-tim': typeof DashboardPengaturanAksesDanTimRoute
'/dashboard/pengaturan/keamanan': typeof DashboardPengaturanKeamananRoute
'/dashboard/pengaturan/notifikasi': typeof DashboardPengaturanNotifikasiRoute
'/dashboard/pengaturan/umum': typeof DashboardPengaturanUmumRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/admin'
| '/dashboard'
| '/pengaturan'
| '/bantuan'
| '/bumdes'
| '/demografi-pekerjaan'
| '/jenna-analytic'
| '/keamanan'
| '/keuangan-anggaran'
| '/kinerja-divisi'
| '/pengaduan-layanan-publik'
| '/signin'
| '/signup'
| '/dashboard/pengaturan'
| '/sosial'
| '/admin/apikey'
| '/admin/settings'
| '/admin/users'
| '/dashboard/bantuan'
| '/dashboard/bumdes'
| '/dashboard/demografi-pekerjaan'
| '/dashboard/jenna-analytic'
| '/dashboard/keamanan'
| '/dashboard/keuangan-anggaran'
| '/dashboard/kinerja-divisi'
| '/dashboard/pengaduan-layanan-publik'
| '/dashboard/sosial'
| '/pengaturan/akses-dan-tim'
| '/pengaturan/keamanan'
| '/pengaturan/notifikasi'
| '/pengaturan/umum'
| '/profile/edit'
| '/users/$id'
| '/admin/'
| '/dashboard/'
| '/profile/'
| '/users/'
| '/dashboard/pengaturan/akses-dan-tim'
| '/dashboard/pengaturan/keamanan'
| '/dashboard/pengaturan/notifikasi'
| '/dashboard/pengaturan/umum'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/pengaturan'
| '/bantuan'
| '/bumdes'
| '/demografi-pekerjaan'
| '/jenna-analytic'
| '/keamanan'
| '/keuangan-anggaran'
| '/kinerja-divisi'
| '/pengaduan-layanan-publik'
| '/signin'
| '/signup'
| '/dashboard/pengaturan'
| '/sosial'
| '/admin/apikey'
| '/admin/settings'
| '/admin/users'
| '/dashboard/bantuan'
| '/dashboard/bumdes'
| '/dashboard/demografi-pekerjaan'
| '/dashboard/jenna-analytic'
| '/dashboard/keamanan'
| '/dashboard/keuangan-anggaran'
| '/dashboard/kinerja-divisi'
| '/dashboard/pengaduan-layanan-publik'
| '/dashboard/sosial'
| '/pengaturan/akses-dan-tim'
| '/pengaturan/keamanan'
| '/pengaturan/notifikasi'
| '/pengaturan/umum'
| '/profile/edit'
| '/users/$id'
| '/admin'
| '/dashboard'
| '/profile'
| '/users'
| '/dashboard/pengaturan/akses-dan-tim'
| '/dashboard/pengaturan/keamanan'
| '/dashboard/pengaturan/notifikasi'
| '/dashboard/pengaturan/umum'
id:
| '__root__'
| '/'
| '/admin'
| '/dashboard'
| '/pengaturan'
| '/bantuan'
| '/bumdes'
| '/demografi-pekerjaan'
| '/jenna-analytic'
| '/keamanan'
| '/keuangan-anggaran'
| '/kinerja-divisi'
| '/pengaduan-layanan-publik'
| '/signin'
| '/signup'
| '/dashboard/pengaturan'
| '/sosial'
| '/admin/apikey'
| '/admin/settings'
| '/admin/users'
| '/dashboard/bantuan'
| '/dashboard/bumdes'
| '/dashboard/demografi-pekerjaan'
| '/dashboard/jenna-analytic'
| '/dashboard/keamanan'
| '/dashboard/keuangan-anggaran'
| '/dashboard/kinerja-divisi'
| '/dashboard/pengaduan-layanan-publik'
| '/dashboard/sosial'
| '/pengaturan/akses-dan-tim'
| '/pengaturan/keamanan'
| '/pengaturan/notifikasi'
| '/pengaturan/umum'
| '/profile/edit'
| '/users/$id'
| '/admin/'
| '/dashboard/'
| '/profile/'
| '/users/'
| '/dashboard/pengaturan/akses-dan-tim'
| '/dashboard/pengaturan/keamanan'
| '/dashboard/pengaturan/notifikasi'
| '/dashboard/pengaturan/umum'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AdminRouteRoute: typeof AdminRouteRouteWithChildren
DashboardRouteRoute: typeof DashboardRouteRouteWithChildren
PengaturanRouteRoute: typeof PengaturanRouteRouteWithChildren
BantuanRoute: typeof BantuanRoute
BumdesRoute: typeof BumdesRoute
DemografiPekerjaanRoute: typeof DemografiPekerjaanRoute
JennaAnalyticRoute: typeof JennaAnalyticRoute
KeamananRoute: typeof KeamananRoute
KeuanganAnggaranRoute: typeof KeuanganAnggaranRoute
KinerjaDivisiRoute: typeof KinerjaDivisiRoute
PengaduanLayananPublikRoute: typeof PengaduanLayananPublikRoute
SigninRoute: typeof SigninRoute
SignupRoute: typeof SignupRoute
SosialRoute: typeof SosialRoute
ProfileEditRoute: typeof ProfileEditRoute
UsersIdRoute: typeof UsersIdRoute
ProfileIndexRoute: typeof ProfileIndexRoute
@@ -380,6 +360,13 @@ export interface RootRouteChildren {
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/sosial': {
id: '/sosial'
path: '/sosial'
fullPath: '/sosial'
preLoaderRoute: typeof SosialRouteImport
parentRoute: typeof rootRouteImport
}
'/signup': {
id: '/signup'
path: '/signup'
@@ -394,11 +381,67 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SigninRouteImport
parentRoute: typeof rootRouteImport
}
'/dashboard': {
id: '/dashboard'
path: '/dashboard'
fullPath: '/dashboard'
preLoaderRoute: typeof DashboardRouteRouteImport
'/pengaduan-layanan-publik': {
id: '/pengaduan-layanan-publik'
path: '/pengaduan-layanan-publik'
fullPath: '/pengaduan-layanan-publik'
preLoaderRoute: typeof PengaduanLayananPublikRouteImport
parentRoute: typeof rootRouteImport
}
'/kinerja-divisi': {
id: '/kinerja-divisi'
path: '/kinerja-divisi'
fullPath: '/kinerja-divisi'
preLoaderRoute: typeof KinerjaDivisiRouteImport
parentRoute: typeof rootRouteImport
}
'/keuangan-anggaran': {
id: '/keuangan-anggaran'
path: '/keuangan-anggaran'
fullPath: '/keuangan-anggaran'
preLoaderRoute: typeof KeuanganAnggaranRouteImport
parentRoute: typeof rootRouteImport
}
'/keamanan': {
id: '/keamanan'
path: '/keamanan'
fullPath: '/keamanan'
preLoaderRoute: typeof KeamananRouteImport
parentRoute: typeof rootRouteImport
}
'/jenna-analytic': {
id: '/jenna-analytic'
path: '/jenna-analytic'
fullPath: '/jenna-analytic'
preLoaderRoute: typeof JennaAnalyticRouteImport
parentRoute: typeof rootRouteImport
}
'/demografi-pekerjaan': {
id: '/demografi-pekerjaan'
path: '/demografi-pekerjaan'
fullPath: '/demografi-pekerjaan'
preLoaderRoute: typeof DemografiPekerjaanRouteImport
parentRoute: typeof rootRouteImport
}
'/bumdes': {
id: '/bumdes'
path: '/bumdes'
fullPath: '/bumdes'
preLoaderRoute: typeof BumdesRouteImport
parentRoute: typeof rootRouteImport
}
'/bantuan': {
id: '/bantuan'
path: '/bantuan'
fullPath: '/bantuan'
preLoaderRoute: typeof BantuanRouteImport
parentRoute: typeof rootRouteImport
}
'/pengaturan': {
id: '/pengaturan'
path: '/pengaturan'
fullPath: '/pengaturan'
preLoaderRoute: typeof PengaturanRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/admin': {
@@ -429,13 +472,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ProfileIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/dashboard/': {
id: '/dashboard/'
path: '/'
fullPath: '/dashboard/'
preLoaderRoute: typeof DashboardIndexRouteImport
parentRoute: typeof DashboardRouteRoute
}
'/admin/': {
id: '/admin/'
path: '/'
@@ -457,68 +493,33 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ProfileEditRouteImport
parentRoute: typeof rootRouteImport
}
'/dashboard/sosial': {
id: '/dashboard/sosial'
path: '/sosial'
fullPath: '/dashboard/sosial'
preLoaderRoute: typeof DashboardSosialRouteImport
parentRoute: typeof DashboardRouteRoute
'/pengaturan/umum': {
id: '/pengaturan/umum'
path: '/umum'
fullPath: '/pengaturan/umum'
preLoaderRoute: typeof PengaturanUmumRouteImport
parentRoute: typeof PengaturanRouteRoute
}
'/dashboard/pengaduan-layanan-publik': {
id: '/dashboard/pengaduan-layanan-publik'
path: '/pengaduan-layanan-publik'
fullPath: '/dashboard/pengaduan-layanan-publik'
preLoaderRoute: typeof DashboardPengaduanLayananPublikRouteImport
parentRoute: typeof DashboardRouteRoute
'/pengaturan/notifikasi': {
id: '/pengaturan/notifikasi'
path: '/notifikasi'
fullPath: '/pengaturan/notifikasi'
preLoaderRoute: typeof PengaturanNotifikasiRouteImport
parentRoute: typeof PengaturanRouteRoute
}
'/dashboard/kinerja-divisi': {
id: '/dashboard/kinerja-divisi'
path: '/kinerja-divisi'
fullPath: '/dashboard/kinerja-divisi'
preLoaderRoute: typeof DashboardKinerjaDivisiRouteImport
parentRoute: typeof DashboardRouteRoute
}
'/dashboard/keuangan-anggaran': {
id: '/dashboard/keuangan-anggaran'
path: '/keuangan-anggaran'
fullPath: '/dashboard/keuangan-anggaran'
preLoaderRoute: typeof DashboardKeuanganAnggaranRouteImport
parentRoute: typeof DashboardRouteRoute
}
'/dashboard/keamanan': {
id: '/dashboard/keamanan'
'/pengaturan/keamanan': {
id: '/pengaturan/keamanan'
path: '/keamanan'
fullPath: '/dashboard/keamanan'
preLoaderRoute: typeof DashboardKeamananRouteImport
parentRoute: typeof DashboardRouteRoute
fullPath: '/pengaturan/keamanan'
preLoaderRoute: typeof PengaturanKeamananRouteImport
parentRoute: typeof PengaturanRouteRoute
}
'/dashboard/jenna-analytic': {
id: '/dashboard/jenna-analytic'
path: '/jenna-analytic'
fullPath: '/dashboard/jenna-analytic'
preLoaderRoute: typeof DashboardJennaAnalyticRouteImport
parentRoute: typeof DashboardRouteRoute
}
'/dashboard/demografi-pekerjaan': {
id: '/dashboard/demografi-pekerjaan'
path: '/demografi-pekerjaan'
fullPath: '/dashboard/demografi-pekerjaan'
preLoaderRoute: typeof DashboardDemografiPekerjaanRouteImport
parentRoute: typeof DashboardRouteRoute
}
'/dashboard/bumdes': {
id: '/dashboard/bumdes'
path: '/bumdes'
fullPath: '/dashboard/bumdes'
preLoaderRoute: typeof DashboardBumdesRouteImport
parentRoute: typeof DashboardRouteRoute
}
'/dashboard/bantuan': {
id: '/dashboard/bantuan'
path: '/bantuan'
fullPath: '/dashboard/bantuan'
preLoaderRoute: typeof DashboardBantuanRouteImport
parentRoute: typeof DashboardRouteRoute
'/pengaturan/akses-dan-tim': {
id: '/pengaturan/akses-dan-tim'
path: '/akses-dan-tim'
fullPath: '/pengaturan/akses-dan-tim'
preLoaderRoute: typeof PengaturanAksesDanTimRouteImport
parentRoute: typeof PengaturanRouteRoute
}
'/admin/users': {
id: '/admin/users'
@@ -541,41 +542,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AdminApikeyRouteImport
parentRoute: typeof AdminRouteRoute
}
'/dashboard/pengaturan': {
id: '/dashboard/pengaturan'
path: '/pengaturan'
fullPath: '/dashboard/pengaturan'
preLoaderRoute: typeof DashboardPengaturanRouteRouteImport
parentRoute: typeof DashboardRouteRoute
}
'/dashboard/pengaturan/umum': {
id: '/dashboard/pengaturan/umum'
path: '/umum'
fullPath: '/dashboard/pengaturan/umum'
preLoaderRoute: typeof DashboardPengaturanUmumRouteImport
parentRoute: typeof DashboardPengaturanRouteRoute
}
'/dashboard/pengaturan/notifikasi': {
id: '/dashboard/pengaturan/notifikasi'
path: '/notifikasi'
fullPath: '/dashboard/pengaturan/notifikasi'
preLoaderRoute: typeof DashboardPengaturanNotifikasiRouteImport
parentRoute: typeof DashboardPengaturanRouteRoute
}
'/dashboard/pengaturan/keamanan': {
id: '/dashboard/pengaturan/keamanan'
path: '/keamanan'
fullPath: '/dashboard/pengaturan/keamanan'
preLoaderRoute: typeof DashboardPengaturanKeamananRouteImport
parentRoute: typeof DashboardPengaturanRouteRoute
}
'/dashboard/pengaturan/akses-dan-tim': {
id: '/dashboard/pengaturan/akses-dan-tim'
path: '/akses-dan-tim'
fullPath: '/dashboard/pengaturan/akses-dan-tim'
preLoaderRoute: typeof DashboardPengaturanAksesDanTimRouteImport
parentRoute: typeof DashboardPengaturanRouteRoute
}
}
}
@@ -597,64 +563,39 @@ const AdminRouteRouteWithChildren = AdminRouteRoute._addFileChildren(
AdminRouteRouteChildren,
)
interface DashboardPengaturanRouteRouteChildren {
DashboardPengaturanAksesDanTimRoute: typeof DashboardPengaturanAksesDanTimRoute
DashboardPengaturanKeamananRoute: typeof DashboardPengaturanKeamananRoute
DashboardPengaturanNotifikasiRoute: typeof DashboardPengaturanNotifikasiRoute
DashboardPengaturanUmumRoute: typeof DashboardPengaturanUmumRoute
interface PengaturanRouteRouteChildren {
PengaturanAksesDanTimRoute: typeof PengaturanAksesDanTimRoute
PengaturanKeamananRoute: typeof PengaturanKeamananRoute
PengaturanNotifikasiRoute: typeof PengaturanNotifikasiRoute
PengaturanUmumRoute: typeof PengaturanUmumRoute
}
const DashboardPengaturanRouteRouteChildren: DashboardPengaturanRouteRouteChildren =
{
DashboardPengaturanAksesDanTimRoute: DashboardPengaturanAksesDanTimRoute,
DashboardPengaturanKeamananRoute: DashboardPengaturanKeamananRoute,
DashboardPengaturanNotifikasiRoute: DashboardPengaturanNotifikasiRoute,
DashboardPengaturanUmumRoute: DashboardPengaturanUmumRoute,
}
const DashboardPengaturanRouteRouteWithChildren =
DashboardPengaturanRouteRoute._addFileChildren(
DashboardPengaturanRouteRouteChildren,
)
interface DashboardRouteRouteChildren {
DashboardPengaturanRouteRoute: typeof DashboardPengaturanRouteRouteWithChildren
DashboardBantuanRoute: typeof DashboardBantuanRoute
DashboardBumdesRoute: typeof DashboardBumdesRoute
DashboardDemografiPekerjaanRoute: typeof DashboardDemografiPekerjaanRoute
DashboardJennaAnalyticRoute: typeof DashboardJennaAnalyticRoute
DashboardKeamananRoute: typeof DashboardKeamananRoute
DashboardKeuanganAnggaranRoute: typeof DashboardKeuanganAnggaranRoute
DashboardKinerjaDivisiRoute: typeof DashboardKinerjaDivisiRoute
DashboardPengaduanLayananPublikRoute: typeof DashboardPengaduanLayananPublikRoute
DashboardSosialRoute: typeof DashboardSosialRoute
DashboardIndexRoute: typeof DashboardIndexRoute
const PengaturanRouteRouteChildren: PengaturanRouteRouteChildren = {
PengaturanAksesDanTimRoute: PengaturanAksesDanTimRoute,
PengaturanKeamananRoute: PengaturanKeamananRoute,
PengaturanNotifikasiRoute: PengaturanNotifikasiRoute,
PengaturanUmumRoute: PengaturanUmumRoute,
}
const DashboardRouteRouteChildren: DashboardRouteRouteChildren = {
DashboardPengaturanRouteRoute: DashboardPengaturanRouteRouteWithChildren,
DashboardBantuanRoute: DashboardBantuanRoute,
DashboardBumdesRoute: DashboardBumdesRoute,
DashboardDemografiPekerjaanRoute: DashboardDemografiPekerjaanRoute,
DashboardJennaAnalyticRoute: DashboardJennaAnalyticRoute,
DashboardKeamananRoute: DashboardKeamananRoute,
DashboardKeuanganAnggaranRoute: DashboardKeuanganAnggaranRoute,
DashboardKinerjaDivisiRoute: DashboardKinerjaDivisiRoute,
DashboardPengaduanLayananPublikRoute: DashboardPengaduanLayananPublikRoute,
DashboardSosialRoute: DashboardSosialRoute,
DashboardIndexRoute: DashboardIndexRoute,
}
const DashboardRouteRouteWithChildren = DashboardRouteRoute._addFileChildren(
DashboardRouteRouteChildren,
const PengaturanRouteRouteWithChildren = PengaturanRouteRoute._addFileChildren(
PengaturanRouteRouteChildren,
)
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AdminRouteRoute: AdminRouteRouteWithChildren,
DashboardRouteRoute: DashboardRouteRouteWithChildren,
PengaturanRouteRoute: PengaturanRouteRouteWithChildren,
BantuanRoute: BantuanRoute,
BumdesRoute: BumdesRoute,
DemografiPekerjaanRoute: DemografiPekerjaanRoute,
JennaAnalyticRoute: JennaAnalyticRoute,
KeamananRoute: KeamananRoute,
KeuanganAnggaranRoute: KeuanganAnggaranRoute,
KinerjaDivisiRoute: KinerjaDivisiRoute,
PengaduanLayananPublikRoute: PengaduanLayananPublikRoute,
SigninRoute: SigninRoute,
SignupRoute: SignupRoute,
SosialRoute: SosialRoute,
ProfileEditRoute: ProfileEditRoute,
UsersIdRoute: UsersIdRoute,
ProfileIndexRoute: ProfileIndexRoute,

View File

@@ -1,4 +1,5 @@
/** biome-ignore-all lint/suspicious/noExplicitAny: <explanation */
import { protectedRouteMiddleware } from "@/middleware/authMiddleware";
import { authStore } from "@/store/auth";
import "@mantine/core/styles.css";
import "@mantine/dates/styles.css";
@@ -6,22 +7,22 @@ import { createRootRoute, Outlet } from "@tanstack/react-router";
export const Route = createRootRoute({
component: RootComponent,
beforeLoad: async () => {
// Fetch session but don't block navigation
try {
const res = await fetch("/api/session", {
method: "GET",
credentials: "include",
});
if (res.ok) {
const { data } = await res.json();
authStore.user = data?.user;
authStore.session = data;
}
} catch {
// Ignore errors, allow public access
beforeLoad: async ({ location }) => {
// Only apply auth middleware for routes that need it
// Public routes: /, /signin, /signup
const isPublicRoute =
location.pathname === "/" ||
location.pathname === "/signin" ||
location.pathname === "/signup";
if (isPublicRoute) {
return;
}
return {};
// Apply protected route middleware for all other routes
const context = await protectedRouteMiddleware({ location });
authStore.user = context?.user as any;
authStore.session = context?.session as any;
},
});

51
src/routes/bantuan.tsx Normal file
View File

@@ -0,0 +1,51 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header";
import HelpPage from "@/components/help-page";
import { Sidebar } from "@/components/sidebar";
export const Route = createFileRoute("/bantuan")({
component: BantuanPage,
});
function BantuanPage() {
const [opened, { toggle }] = useDisclosure();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
<Header />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main bg={mainBgColor}>
<HelpPage />
</AppShell.Main>
</AppShell>
);
}

9
src/routes/bumdes.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/bumdes")({
component: RouteComponent,
});
function RouteComponent() {
return <div>Hello "/bumdes"!</div>;
}

View File

@@ -1,6 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import HelpPage from "@/components/help-page";
export const Route = createFileRoute("/dashboard/bantuan")({
component: HelpPage,
});

View File

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

View File

@@ -1,6 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import DemografiPekerjaan from "../../components/demografi-pekerjaan";
export const Route = createFileRoute("/dashboard/demografi-pekerjaan")({
component: DemografiPekerjaan,
});

View File

@@ -1,5 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { DashboardContent } from "@/components/dashboard-content";
export const Route = createFileRoute("/dashboard/")({
component: DashboardContent,
});

View File

@@ -1,6 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import JennaAnalytic from "@/components/jenna-analytic";
export const Route = createFileRoute("/dashboard/jenna-analytic")({
component: JennaAnalytic,
});

View File

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

View File

@@ -1,6 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import KeuanganAnggaran from "@/components/keuangan-anggaran";
export const Route = createFileRoute("/dashboard/keuangan-anggaran")({
component: KeuanganAnggaran,
});

View File

@@ -1,5 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import KinerjaDivisi from "@/components/kinerja-divisi";
export const Route = createFileRoute("/dashboard/kinerja-divisi")({
component: KinerjaDivisi,
});

View File

@@ -1,5 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import PengaduanLayananPublik from "@/components/pengaduan-layanan-publik";
export const Route = createFileRoute("/dashboard/pengaduan-layanan-publik")({
component: PengaduanLayananPublik,
});

View File

@@ -1,6 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import AksesDanTimSettings from "@/components/pengaturan/akses-dan-tim";
export const Route = createFileRoute("/dashboard/pengaturan/akses-dan-tim")({
component: AksesDanTimSettings,
});

View File

@@ -1,6 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import KeamananSettings from "@/components/pengaturan/keamanan";
export const Route = createFileRoute("/dashboard/pengaturan/keamanan")({
component: KeamananSettings,
});

View File

@@ -1,6 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import NotifikasiSettings from "@/components/pengaturan/notifikasi";
export const Route = createFileRoute("/dashboard/pengaturan/notifikasi")({
component: NotifikasiSettings,
});

View File

@@ -1,9 +0,0 @@
import { createFileRoute, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute("/dashboard/pengaturan")({
component: () => (
<div className="p-2">
<Outlet />
</div>
),
});

View File

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

View File

@@ -0,0 +1,51 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header";
import { Sidebar } from "@/components/sidebar";
import DemografiPekerjaan from "../components/demografi-pekerjaan";
export const Route = createFileRoute("/demografi-pekerjaan")({
component: DemografiPekerjaanPage,
});
function DemografiPekerjaanPage() {
const [opened, { toggle }] = useDisclosure();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
<Header />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main bg={mainBgColor}>
<DemografiPekerjaan />
</AppShell.Main>
</AppShell>
);
}

View File

@@ -1,7 +1,51 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { createFileRoute } from "@tanstack/react-router";
import { DashboardContent } from "@/components/dashboard-content";
import { Header } from "@/components/header";
import { Sidebar } from "@/components/sidebar";
export const Route = createFileRoute("/")({
beforeLoad: () => {
throw redirect({ to: "/dashboard" });
},
component: DashboardPage,
});
function DashboardPage() {
const [opened, { toggle }] = useDisclosure();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
<Header />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main bg={mainBgColor}>
<DashboardContent />
</AppShell.Main>
</AppShell>
);
}

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/jenna-analytic")({
component: RouteComponent,
});
function RouteComponent() {
return <div>Hello "/jenna-analytic"!</div>;
}

9
src/routes/keamanan.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/keamanan")({
component: RouteComponent,
});
function RouteComponent() {
return <div>Hello "/keamanan"!</div>;
}

View File

@@ -0,0 +1,51 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header";
import KeuanganAnggaran from "@/components/keuangan-anggaran";
import { Sidebar } from "@/components/sidebar";
export const Route = createFileRoute("/keuangan-anggaran")({
component: KeuanganAnggaranPage,
});
function KeuanganAnggaranPage() {
const [opened, { toggle }] = useDisclosure();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
<Header />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main bg={mainBgColor}>
<KeuanganAnggaran />
</AppShell.Main>
</AppShell>
);
}

View File

@@ -0,0 +1,51 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header";
import KinerjaDivisi from "@/components/kinerja-divisi";
import { Sidebar } from "@/components/sidebar";
export const Route = createFileRoute("/kinerja-divisi")({
component: KinerjaDivisiPage,
});
function KinerjaDivisiPage() {
const [opened, { toggle }] = useDisclosure();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
<Header />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main bg={mainBgColor}>
<KinerjaDivisi />
</AppShell.Main>
</AppShell>
);
}

View File

@@ -0,0 +1,51 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header";
import PengaduanLayananPublik from "@/components/pengaduan-layanan-publik";
import { Sidebar } from "@/components/sidebar";
export const Route = createFileRoute("/pengaduan-layanan-publik")({
component: PengaduanLayananPublikPage,
});
function PengaduanLayananPublikPage() {
const [opened, { toggle }] = useDisclosure();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
<Header />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main bg={mainBgColor}>
<PengaduanLayananPublik />
</AppShell.Main>
</AppShell>
);
}

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/pengaturan/akses-dan-tim")({
component: RouteComponent,
});
function RouteComponent() {
return <div>Hello "/pengaturan/akses-dan-tim"!</div>;
}

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/pengaturan/keamanan")({
component: RouteComponent,
});
function RouteComponent() {
return <div>Hello "/pengaturan/keamanan"!</div>;
}

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/pengaturan/notifikasi")({
component: RouteComponent,
});
function RouteComponent() {
return <div>Hello "/pengaturan/notifikasi"!</div>;
}

View File

@@ -1,38 +1,35 @@
import {
AppShell,
Burger,
Group,
useMantineColorScheme,
useMantineTheme,
} from "@mantine/core";
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { useDisclosure, useMediaQuery } from "@mantine/hooks";
import { createFileRoute, Outlet, useRouterState } from "@tanstack/react-router";
import {
createFileRoute,
Outlet,
useRouterState,
} from "@tanstack/react-router";
import { useEffect } from "react";
import { Header } from "@/components/header";
import { Sidebar } from "@/components/sidebar";
export const Route = createFileRoute("/dashboard")({
component: RouteComponent,
export const Route = createFileRoute("/pengaturan")({
component: PengaturanLayout,
});
function RouteComponent() {
function PengaturanLayout() {
const [opened, { toggle, close }] = useDisclosure();
const { colorScheme } = useMantineColorScheme();
const theme = useMantineTheme();
const routerState = useRouterState();
const isMobile = useMediaQuery("(max-width: 48em)");
const routerState = useRouterState();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
// ✅ AUTO CLOSE NAVBAR ON ROUTE CHANGE (MOBILE ONLY)
// Auto close navbar on route change (mobile only)
useEffect(() => {
if (isMobile && opened) {
close();
}
}, [routerState.location.pathname]);
}, [routerState.location.pathname, isMobile, opened, close]);
return (
<AppShell
@@ -45,19 +42,8 @@ function RouteComponent() {
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group
h="100%"
px="lg"
align="center"
wrap="nowrap"
>
<Burger
opened={opened}
onClick={toggle}
hiddenFrom="sm"
size="sm"
/>
<Group h="100%" px="lg" align="center" wrap="nowrap">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
<Header />
</Group>
</AppShell.Header>
@@ -73,7 +59,9 @@ function RouteComponent() {
</AppShell.Navbar>
<AppShell.Main bg={mainBgColor}>
<Outlet />
<div className="p-2">
<Outlet />
</div>
</AppShell.Main>
</AppShell>
);

View File

@@ -1,6 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import UmumSettings from "@/components/pengaturan/umum";
export const Route = createFileRoute("/dashboard/pengaturan/umum")({
export const Route = createFileRoute("/pengaturan/umum")({
component: UmumSettings,
});

9
src/routes/sosial.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/sosial")({
component: RouteComponent,
});
function RouteComponent() {
return <div>Hello "/sosial"!</div>;
}

View File

@@ -21,7 +21,7 @@ export const getEnv = (key: string, defaultValue = ""): string => {
};
export const VITE_PUBLIC_URL = (() => {
// Priority:
// Priority:
// 1. BETTER_AUTH_URL (standard for better-auth)
// 2. VITE_PUBLIC_URL (our app standard)
// 3. window.location.origin (browser fallback)

View File

@@ -1,11 +1,15 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
content: [
"./src/index.html",
"./public/**/*.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
"darmasaba-navy": {
DEFAULT: "#1E3A5F", // Primary navy color
DEFAULT: "#1E3A5F",
50: "#E1E4F2",
100: "#B9C2DD",
200: "#91A0C9",
@@ -18,7 +22,7 @@ module.exports = {
900: "#071833",
},
"darmasaba-blue": {
DEFAULT: "#3B82F6", // Primary blue color
DEFAULT: "#3B82F6",
50: "#E3F0FF",
100: "#B6D9FF",
200: "#89C2FF",