Compare commits

...

13 Commits

Author SHA1 Message Date
7c8012d277 Fix Lint-1 2026-03-25 15:07:10 +08:00
687ce11a81 Refactor New Ui Semua Pengaturan 2026-03-25 14:38:37 +08:00
1ba4643e23 Refactor New Ui Pengaturan Umum 2026-03-25 11:56:22 +08:00
113dd7ba6f Refactor New Ui Sosial, Keamanan, & Bantuan 2026-03-25 11:10:50 +08:00
71a305cd4b Refactor New Ui Bumdes 02 2026-03-25 10:32:31 +08:00
84b96ca3be Refactor New Ui Bumdes 2026-03-25 00:09:38 +08:00
8159216a2c Refactor ui keuangan 2026-03-24 23:17:23 +08:00
d714c09efc Fix New UI Pengaduan 2026-03-18 00:43:44 +07:00
0a97e31416 Fix New UI Pengaduan 2026-03-18 00:34:53 +07:00
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
67 changed files with 4390 additions and 3419 deletions

3
.gitignore vendored
View File

@@ -37,6 +37,9 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Dashboard-MD # Dashboard-MD
Dashboard-MD Dashboard-MD
# md
*.md
# Playwright artifacts # Playwright artifacts
test-results/ test-results/
playwright-report/ playwright-report/

168
Pengaduan-New.md Normal file
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

BIN
public/light-mode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
public/white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

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

View File

@@ -1,4 +1,4 @@
import { Grid, Stack, useMantineColorScheme } from "@mantine/core"; import { Grid, Image, Stack, useMantineColorScheme } from "@mantine/core";
import { CheckCircle, FileText, MessageCircle, Users } from "lucide-react"; import { CheckCircle, FileText, MessageCircle, Users } from "lucide-react";
import { ActivityList } from "./dashboard/activity-list"; import { ActivityList } from "./dashboard/activity-list";
import { ChartAPBDes } from "./dashboard/chart-apbdes"; import { ChartAPBDes } from "./dashboard/chart-apbdes";
@@ -8,149 +8,26 @@ import { SatisfactionChart } from "./dashboard/satisfaction-chart";
import { SDGSCard } from "./dashboard/sdgs-card"; import { SDGSCard } from "./dashboard/sdgs-card";
import { StatCard } from "./dashboard/stat-card"; import { StatCard } from "./dashboard/stat-card";
// SDGs Icons
function EnergyIcon() {
return (
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M24 4L14 24H22L20 44L34 20H26L24 4Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function PeaceIcon() {
return (
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="24" cy="24" r="20" stroke="currentColor" strokeWidth="2" />
<path
d="M24 4V44M24 24L10 38M24 24L38 38"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
);
}
function HealthIcon() {
return (
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M24 44C24 44 6 28 6 18C6 11.373 11.373 6 18 6C21.5 6 24.5 7.5 24 12C23.5 7.5 26.5 6 30 6C36.627 6 42 11.373 42 18C42 28 24 44 24 44Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="2"
strokeLinejoin="round"
/>
</svg>
);
}
function PovertyIcon() {
return (
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="6" y="18" width="36" height="26" rx="2" fill="currentColor" />
<path
d="M14 18V12C14 8.686 16.686 6 20 6H28C31.314 6 34 8.686 34 12V18"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
);
}
function OceanIcon() {
return (
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 30C6 30 10 26 14 30C18 34 22 30 26 30C30 30 34 34 38 30C42 26 46 30 46 30"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M6 38C6 38 10 34 14 38C18 42 22 38 26 38C30 38 34 42 38 38C42 34 46 38 46 38"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<circle cx="24" cy="16" r="6" fill="currentColor" />
</svg>
);
}
const sdgsData = [ const sdgsData = [
{ {
title: "Desa Berenergi Bersih dan Terbarukan", title: "Desa Berenergi Bersih dan Terbarukan",
score: 99.64, score: 99.64,
icon: <EnergyIcon />, image: "SDGS-7.png",
color: "#FACC15",
bgColor: "#FEF9C3",
}, },
{ {
title: "Desa Damai Berkeadilan", title: "Desa Damai Berkeadilan",
score: 78.65, score: 78.65,
icon: <PeaceIcon />, image: "SDGS-16.png",
color: "#3B82F6",
bgColor: "#DBEAFE",
}, },
{ {
title: "Desa Sehat dan Sejahtera", title: "Desa Sehat dan Sejahtera",
score: 77.37, score: 77.37,
icon: <HealthIcon />, image: "SDGS-3.png",
color: "#22C55E",
bgColor: "#DCFCE7",
}, },
{ {
title: "Desa Tanpa Kemiskinan", title: "Desa Tanpa Kemiskinan",
score: 52.62, score: 52.62,
icon: <PovertyIcon />, image: "SDGS-1.png",
color: "#EF4444",
bgColor: "#FEE2E2",
},
{
title: "Desa Peduli Lingkungan Laut",
score: 50.0,
icon: <OceanIcon />,
color: "#06B6D4",
bgColor: "#CFFAFE",
}, },
]; ];
@@ -202,37 +79,35 @@ export function DashboardContent() {
{/* Section 2: Chart & Division Progress */} {/* Section 2: Chart & Division Progress */}
<Grid gutter="lg"> <Grid gutter="lg">
<Grid.Col span={{ base: 12, lg: 8 }}> <Grid.Col span={{ base: 12, lg: 7 }}>
<ChartSurat /> <ChartSurat />
</Grid.Col> </Grid.Col>
<Grid.Col span={{ base: 12, lg: 4 }}> <Grid.Col span={{ base: 12, lg: 5 }}>
<DivisionProgress />
</Grid.Col>
</Grid>
{/* Section 3: APBDes Chart */}
<ChartAPBDes />
{/* Section 4 & 5: Activity List & Satisfaction Chart */}
<Grid gutter="lg">
<Grid.Col span={{ base: 12, lg: 6 }}>
<ActivityList />
</Grid.Col>
<Grid.Col span={{ base: 12, lg: 6 }}>
<SatisfactionChart /> <SatisfactionChart />
</Grid.Col> </Grid.Col>
</Grid> </Grid>
{/* Section 3: APBDes Chart */}
<Grid gutter="lg">
<Grid.Col span={{ base: 12, lg: 7 }}>
<DivisionProgress />
</Grid.Col>
<Grid.Col span={{ base: 12, lg: 5 }}>
<ActivityList />
{/* <SatisfactionChart /> */}
</Grid.Col>
</Grid>
<ChartAPBDes />
{/* Section 6: SDGs Desa Cards */} {/* Section 6: SDGs Desa Cards */}
<Grid gutter="md"> <Grid gutter="md">
{sdgsData.map((sdg, index) => ( {sdgsData.map((sdg, index) => (
<Grid.Col key={index} span={{ base: 12, sm: 6, md: 4, lg: 2.4 }}> <Grid.Col key={index} span={{ base: 9, md: 3 }}>
<SDGSCard <SDGSCard
image={<Image src={sdg.image} alt={sdg.title} />}
title={sdg.title} title={sdg.title}
score={sdg.score} score={sdg.score}
icon={sdg.icon}
color={sdg.color}
bgColor={sdg.bgColor}
/> />
</Grid.Col> </Grid.Col>
))} ))}

View File

@@ -36,6 +36,7 @@ export function ActivityList() {
? "0 1px 3px 0 rgb(0 0 0 / 0.1)" ? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
: "0 1px 3px 0 rgb(0 0 0 / 0.1)", : "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}} }}
h="100%"
> >
<Group gap="xs" mb="lg"> <Group gap="xs" mb="lg">
<Calendar <Calendar

View File

@@ -39,6 +39,7 @@ export function ChartAPBDes() {
? "0 1px 3px 0 rgb(0 0 0 / 0.1)" ? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
: "0 1px 3px 0 rgb(0 0 0 / 0.1)", : "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}} }}
h="100%"
> >
<Title order={4} c={dark ? "white" : "gray.9"} mb="lg"> <Title order={4} c={dark ? "white" : "gray.9"} mb="lg">
Grafik APBDes Grafik APBDes

View File

@@ -42,6 +42,7 @@ export function ChartSurat() {
? "0 1px 3px 0 rgb(0 0 0 / 0.1)" ? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
: "0 1px 3px 0 rgb(0 0 0 / 0.1)", : "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}} }}
h="100%"
> >
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Box> <Box>

View File

@@ -39,6 +39,7 @@ export function DivisionProgress() {
? "0 1px 3px 0 rgb(0 0 0 / 0.1)" ? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
: "0 1px 3px 0 rgb(0 0 0 / 0.1)", : "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}} }}
h="100%"
> >
<Title order={4} c={dark ? "white" : "gray.9"} mb="lg"> <Title order={4} c={dark ? "white" : "gray.9"} mb="lg">
Divisi Teraktif Divisi Teraktif

View File

@@ -32,6 +32,7 @@ export function SatisfactionChart() {
? "0 1px 3px 0 rgb(0 0 0 / 0.1)" ? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
: "0 1px 3px 0 rgb(0 0 0 / 0.1)", : "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}} }}
h="100%"
> >
<Title order={4} c={dark ? "white" : "gray.9"} mb={5}> <Title order={4} c={dark ? "white" : "gray.9"} mb={5}>
Tingkat Kepuasan Tingkat Kepuasan

View File

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

View File

@@ -25,7 +25,7 @@ export function StatCard({
trend, trend,
trendValue, trendValue,
icon, icon,
iconColor = "darmasaba-blue", iconColor = "#1E3A5F",
}: StatCardProps) { }: StatCardProps) {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; const dark = colorScheme === "dark";
@@ -77,6 +77,7 @@ export function StatCard({
size="xl" size="xl"
radius="xl" radius="xl"
color={dark ? "gray" : iconColor} color={dark ? "gray" : iconColor}
bg={dark ? "gray" : iconColor}
> >
{icon} {icon}
</ThemeIcon> </ThemeIcon>

View File

@@ -1,116 +1,73 @@
import { BarChart, PieChart } from "@mantine/charts";
import { import {
Badge,
Box, Box,
Card, Card,
Grid, Grid,
GridCol,
Group, Group,
Progress,
Stack, Stack,
Table,
Text, Text,
ThemeIcon,
Title, Title,
useMantineColorScheme, useMantineColorScheme,
} from "@mantine/core"; } from "@mantine/core";
import { import {
IconArrowDown, Baby,
IconArrowUp, BarChart3,
IconBabyCarriage, Building2,
IconSkull, Home,
} from "@tabler/icons-react"; PieChart as PieChartIcon,
import React from "react"; TrendingDown,
Users,
} from "lucide-react";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
Pie,
PieChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
// Sample Data // KPI Data
const kpiData = [ const kpiData = [
{ {
id: 1, id: 1,
title: "Total Penduduk", title: "Total Penduduk",
value: "5.634", value: "5.634",
sub: "Aktif terdaftar", subtitle: "Aktif terdaftar",
icon: ( icon: Users,
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
role="img"
aria-label="Icon penduduk"
>
<title>Total Penduduk</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"
/>
</svg>
),
}, },
{ {
id: 2, id: 2,
title: "Kepala Keluarga", title: "Kepala Keluarga",
value: "1.354", value: "1.354",
sub: "Total KK", subtitle: "Total KK",
icon: ( icon: Home,
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
role="img"
aria-label="Icon kepala keluarga"
>
<title>Kepala Keluarga</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
/>
</svg>
),
}, },
{ {
id: 3, id: 3,
title: "Kelahiran", title: "Kelahiran",
value: "23", value: "23",
sub: "Tahun ini", subtitle: "Tahun ini",
icon: ( icon: Baby,
<IconBabyCarriage
className="h-6 w-6 text-muted-foreground"
role="img"
aria-label="Icon kelahiran"
/>
),
}, },
{ {
id: 4, id: 4,
title: "Kemiskinan", title: "Kemiskinan",
value: "324", value: "324",
delta: "-10% dari tahun lalu", subtitle: "-10% dari tahun lalu",
deltaType: "positive", trend: "positive",
icon: ( icon: TrendingDown,
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
role="img"
aria-label="Icon kemiskinan"
>
<title>Kemiskinan</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
/>
</svg>
),
}, },
]; ];
// Age Distribution Data
const ageDistributionData = [ const ageDistributionData = [
{ ageRange: "17-25", total: 850 }, { ageRange: "17-25", total: 850 },
{ ageRange: "26-35", total: 1200 }, { ageRange: "26-35", total: 1200 },
@@ -120,6 +77,7 @@ const ageDistributionData = [
{ ageRange: "65+", total: 484 }, { ageRange: "65+", total: 484 },
]; ];
// Job Distribution Data
const jobDistributionData = [ const jobDistributionData = [
{ job: "Sipil", total: 1200 }, { job: "Sipil", total: 1200 },
{ job: "Guru", total: 850 }, { job: "Guru", total: 850 },
@@ -128,284 +86,598 @@ const jobDistributionData = [
{ job: "Wiraswasta", total: 984 }, { job: "Wiraswasta", total: 984 },
]; ];
// Religion Data
const religionData = [ const religionData = [
{ religion: "Hindu", total: 4234, color: "red" }, { name: "Hindu", value: 4234, color: "#EF4444" },
{ religion: "Islam", total: 856, color: "blue" }, { name: "Islam", value: 856, color: "#3B82F6" },
{ religion: "Kristen", total: 412, color: "green" }, { name: "Kristen", value: 412, color: "#22C55E" },
{ religion: "Buddha", total: 202, color: "yellow" }, { name: "Buddha", value: 202, color: "#FACC15" },
]; ];
// Banjar Data
const banjarData = [ const banjarData = [
{ banjar: "Banjar Darmasaba", population: 1200, kk: 300, poor: 45 }, { banjar: "Darmasaba", population: 1200, kk: 300, poor: 45 },
{ banjar: "Banjar Manesa", population: 950, kk: 240, poor: 32 }, { banjar: "Manesa", population: 950, kk: 240, poor: 32 },
{ banjar: "Banjar Cabe", population: 800, kk: 200, poor: 28 }, { banjar: "Cabe", population: 800, kk: 200, poor: 28 },
{ banjar: "Banjar Penenjoan", population: 1100, kk: 280, poor: 38 }, { banjar: "Penenjoan", population: 1100, kk: 280, poor: 38 },
{ banjar: "Banjar Baler Pasar", population: 984, kk: 250, poor: 42 }, { banjar: "Baler Pasar", population: 984, kk: 250, poor: 42 },
{ banjar: "Banjar Bucu", population: 600, kk: 184, poor: 25 }, { banjar: "Bucu", population: 600, kk: 184, poor: 25 },
]; ];
// Dynamic Stats Data
const dynamicStats = [ const dynamicStats = [
{ {
title: "Kelahiran", title: "Kelahiran",
value: "23", value: "23",
icon: <IconBabyCarriage size={16} />, icon: Baby,
color: "green", color: "#22C55E",
}, },
{ {
title: "Kematian", title: "Kematian",
value: "12", value: "12",
icon: <IconSkull size={16} />, icon: TrendingDown,
color: "red", color: "#EF4444",
}, },
{ {
title: "Pindah Masuk", title: "Pindah Masuk",
value: "45", value: "45",
icon: <IconArrowDown size={16} />, icon: Users,
color: "blue", color: "#3B82F6",
}, },
{ {
title: "Pindah Keluar", title: "Pindah Keluar",
value: "32", value: "32",
icon: <IconArrowUp size={16} />, icon: Users,
color: "orange", color: "#3B82F6",
}, },
]; ];
// Sektor Unggulan Data
const sektorUnggulanData = [
{ sektor: "Pertanian", value: 65 },
{ sektor: "Perdagangan", value: 45 },
{ sektor: "Industri", value: 38 },
{ sektor: "Jasa", value: 52 },
];
const DemografiPekerjaan = () => { const DemografiPekerjaan = () => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; const dark = colorScheme === "dark";
return (
<Box className="space-y-6">
<Stack gap="xl">
{/* KPI Cards */}
<Grid gutter="lg">
{kpiData.map((kpi) => (
<Grid.Col key={kpi.id} span={{ base: 12, md: 6, lg: 3 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Group justify="space-between" align="flex-start" mb="xs">
<Text size="sm" fw={500} c={dark ? "dark.3" : "dimmed"}>
{kpi.title}
</Text>
{React.cloneElement(kpi.icon, {
className: "h-6 w-6",
color: dark
? "var(--mantine-color-dark-3)"
: "var(--mantine-color-dimmed)",
})}
</Group>
<Title order={3} fw={700} c={dark ? "dark.0" : "black"} mt="xs">
{kpi.value}
</Title>
{kpi.delta && (
<Text
size="xs"
c={
kpi.deltaType === "positive"
? "green"
: kpi.deltaType === "negative"
? "red"
: dark
? "dark.3"
: "dimmed"
}
mt={4}
>
{kpi.delta}
</Text>
)}
{kpi.sub && (
<Text size="xs" c={dark ? "dark.3" : "dimmed"} mt={2}>
{kpi.sub}
</Text>
)}
</Card>
</Grid.Col>
))}
</Grid>
{/* Charts Section */} return (
<Grid gutter="lg"> <Stack gap="lg">
{/* Grafik Pengelompokan Umur */} {/* TOP SECTION - 4 STAT CARDS */}
<Grid.Col span={{ base: 12, lg: 6 }}> <Grid gutter="md">
{kpiData.map((item) => (
<Grid.Col key={item.id} span={{ base: 12, sm: 6, lg: 3 }}>
<Card <Card
p="md" p="md"
radius="md" radius="xl"
withBorder withBorder
bg={dark ? "#141D34" : "white"} bg={dark ? "#1E293B" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }} style={{
borderColor: dark ? "#334155" : "white",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
transition: "transform 0.15s ease, box-shadow 0.15s ease",
}}
h="100%"
> >
<Title order={3} fw={500} mb="md"> <Group justify="space-between" align="flex-start" w="100%">
Grafik Pengelompokan Umur <Stack gap={2}>
</Title> <Text size="sm" c="dimmed">
<BarChart {item.title}
h={300} </Text>
data={ageDistributionData} <Text size="xl" fw={700} c={dark ? "white" : "gray.9"}>
dataKey="ageRange" {item.value}
series={[{ name: "total", color: "darmasaba-navy" }]} </Text>
withLegend <Group gap={4} align="flex-start">
/> {item.trend === "positive" && (
<TrendingDown size={14} color="#22C55E" />
)}
<Text
size="xs"
c={
item.trend === "positive"
? "green"
: dark
? "gray.4"
: "gray.5"
}
>
{item.subtitle}
</Text>
</Group>
</Stack>
<ThemeIcon
color="#1E3A5F"
variant="filled"
size="lg"
radius="xl"
>
<item.icon style={{ width: "60%", height: "60%" }} />
</ThemeIcon>
</Group>
</Card> </Card>
</Grid.Col> </Grid.Col>
))}
</Grid>
{/* Demografi Pekerjaan */} {/* ROW 2 - 3 COLUMNS */}
<Grid.Col span={{ base: 12, lg: 6 }}> <Grid gutter="lg">
<Card {/* LEFT: PENGELOMPOKAN UMUR */}
p="md" <Grid.Col span={{ base: 12, lg: 4 }}>
radius="md" <Card
withBorder p="md"
bg={dark ? "#141D34" : "white"} radius="xl"
style={{ borderColor: dark ? "#141D34" : "white" }} withBorder
> bg={dark ? "#1E293B" : "white"}
<Title order={3} fw={500} mb="md"> style={{
borderColor: dark ? "#334155" : "white",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}}
h="100%"
>
<Group gap="xs" mb="md">
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
<BarChart3 size={14} />
</ThemeIcon>
<Title order={4} c={dark ? "white" : "gray.9"}>
Pengelompokan Umur
</Title>
</Group>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={ageDistributionData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#334155" : "#e5e7eb"}
/>
<XAxis
dataKey="ageRange"
axisLine={false}
tickLine={false}
tick={{
fill: dark ? "#E2E8F0" : "#374151",
fontSize: 12,
}}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{
fill: dark ? "#E2E8F0" : "#374151",
fontSize: 12,
}}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1E293B" : "white",
borderColor: dark ? "#334155" : "#e5e7eb",
borderRadius: "8px",
}}
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
/>
<Bar
dataKey="total"
fill="#1E3A5F"
radius={[8, 8, 0, 0]}
maxBarSize={40}
/>
</BarChart>
</ResponsiveContainer>
</Card>
</Grid.Col>
{/* CENTER: DEMOGRAFI PEKERJAAN */}
<Grid.Col span={{ base: 12, lg: 4 }}>
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}}
h="100%"
>
<Group gap="xs" mb="md">
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
<Building2 size={14} />
</ThemeIcon>
<Title order={4} c={dark ? "white" : "gray.9"}>
Demografi Pekerjaan Demografi Pekerjaan
</Title> </Title>
<BarChart </Group>
h={300} <ResponsiveContainer width="100%" height={250}>
data={jobDistributionData} <BarChart data={jobDistributionData} layout="vertical">
dataKey="job" <CartesianGrid
series={[{ name: "total", color: "darmasaba-navy" }]} strokeDasharray="3 3"
withLegend horizontal={false}
/> stroke={dark ? "#334155" : "#e5e7eb"}
</Card> />
</Grid.Col> <XAxis
</Grid> type="number"
axisLine={false}
tickLine={false}
tick={{
fill: dark ? "#E2E8F0" : "#374151",
fontSize: 12,
}}
/>
<YAxis
type="category"
dataKey="job"
axisLine={false}
tickLine={false}
tick={{
fill: dark ? "#E2E8F0" : "#374151",
fontSize: 12,
}}
width={90}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1E293B" : "white",
borderColor: dark ? "#334155" : "#e5e7eb",
borderRadius: "8px",
}}
/>
<Bar
dataKey="total"
fill="#1E3A5F"
radius={[0, 8, 8, 0]}
maxBarSize={30}
/>
</BarChart>
</ResponsiveContainer>
</Card>
</Grid.Col>
{/* Agama & Data per Banjar */} {/* RIGHT: STATISTIK DINAMIKA PENDUDUK */}
<Grid gutter="lg"> <Grid.Col span={{ base: 12, lg: 4 }}>
{/* Distribusi Agama */} <Card
<Grid.Col span={{ base: 12, lg: 6 }}> p="md"
<Card radius="xl"
p="md" withBorder
radius="md" bg={dark ? "#1E293B" : "white"}
withBorder style={{
bg={dark ? "#141D34" : "white"} borderColor: dark ? "#334155" : "white",
style={{ borderColor: dark ? "#141D34" : "white" }} boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
> }}
<Title order={3} fw={500} mb="md"> h="100%"
Distribusi Agama >
<Group gap="xs" mb="md">
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
<BarChart3 size={14} />
</ThemeIcon>
<Title order={4} c={dark ? "white" : "gray.9"}>
Dinamika Penduduk
</Title> </Title>
<PieChart </Group>
h={300} <Grid gutter="sm">
data={religionData.map((item) => ({ {dynamicStats.map((stat, index) => (
name: item.religion, <Grid.Col key={index} span={6}>
value: item.total, <Card
color: item.color, p="sm"
}))} radius="lg"
withLabels bg={dark ? "#334155" : "#F1F5F9"}
withLabelsLine style={{
labelsPosition="outside" transition: "transform 0.15s ease",
labelsType="percent" cursor: "pointer",
/> }}
</Card> >
</Grid.Col> <Stack gap={2} align="center">
<ThemeIcon
{/* Data per Banjar */} color={stat.color}
<Grid.Col span={{ base: 12, lg: 6 }}> variant="filled"
<Card size="md"
p="md" radius="lg"
radius="md" >
withBorder <stat.icon size={14} />
bg={dark ? "#141D34" : "white"} </ThemeIcon>
style={{ borderColor: dark ? "#141D34" : "white" }} <Text size="xs" c="dimmed" ta="center">
>
<Title order={3} fw={500} c={dark ? "dark.0" : "black"} mb="md">
Data per Banjar
</Title>
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>
<Text c={dark ? "dark.0" : "black"}>Banjar</Text>
</Table.Th>
<Table.Th>
<Text c={dark ? "dark.0" : "black"}>Penduduk</Text>
</Table.Th>
<Table.Th>
<Text c={dark ? "dark.0" : "black"}>KK</Text>
</Table.Th>
<Table.Th>
<Text c={dark ? "dark.0" : "black"}>Miskin</Text>
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{banjarData.map((item, index) => (
<Table.Tr key={`${item.banjar}-${index}`}>
<Table.Td>
<Text c={dark ? "dark.0" : "black"}>{item.banjar}</Text>
</Table.Td>
<Table.Td>
<Text c={dark ? "dark.0" : "black"}>
{item.population.toLocaleString()}
</Text>
</Table.Td>
<Table.Td>
<Text c={dark ? "dark.0" : "black"}>
{item.kk.toLocaleString()}
</Text>
</Table.Td>
<Table.Td>
<Text c={dark ? "red.4" : "red"}>
{item.poor.toLocaleString()}
</Text>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Card>
</Grid.Col>
</Grid>
{/* Statistik Dinamika Penduduk */}
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={3} fw={500} c={dark ? "dark.0" : "black"} mb="md">
Statistik Dinamika Penduduk
</Title>
<Grid gutter="md">
{dynamicStats.map((stat, index) => (
<Grid.Col
key={`${stat.title}-${index}`}
span={{ base: 12, md: 3 }}
>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Group justify="space-between" align="center">
<Box>
<Text size="sm" fw={500} c={dark ? "dark.3" : "dimmed"}>
{stat.title} {stat.title}
</Text> </Text>
<Title order={4} fw={700} c={stat.color}> <Text
size="lg"
fw={700}
c={stat.color}
style={{ lineHeight: 1 }}
>
{stat.value} {stat.value}
</Title> </Text>
</Box> </Stack>
<Box c={stat.color}>{stat.icon}</Box> </Card>
</Grid.Col>
))}
</Grid>
</Card>
</Grid.Col>
</Grid>
{/* ROW 3 - 3 COLUMNS */}
<Grid gutter="lg">
{/* LEFT: DISTRIBUSI AGAMA */}
<Grid.Col span={{ base: 12, lg: 4 }}>
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}}
h="100%"
>
<Group gap="xs" mb="md">
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
<PieChartIcon size={14} />
</ThemeIcon>
<Title order={4} c={dark ? "white" : "gray.9"}>
Distribusi Agama
</Title>
</Group>
<ResponsiveContainer width="100%" height={250}>
<PieChart>
<Pie
data={religionData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={90}
paddingAngle={2}
dataKey="value"
>
{religionData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1E293B" : "white",
borderColor: dark ? "#334155" : "#e5e7eb",
borderRadius: "8px",
}}
/>
</PieChart>
</ResponsiveContainer>
<Stack gap="xs" mt="md">
{religionData.map((item, index) => (
<Group key={index} justify="space-between">
<Group gap="xs">
<Box
w={10}
h={10}
style={{
backgroundColor: item.color,
borderRadius: 2,
}}
/>
<Text size="sm" c={dark ? "white" : "gray.7"}>
{item.name}
</Text>
</Group> </Group>
</Card> <Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
</Grid.Col> {item.value.toLocaleString()}
))} </Text>
</Grid> </Group>
</Card> ))}
</Stack> </Stack>
</Box> </Card>
</Grid.Col>
{/* CENTER: DATA PER BANJAR */}
<Grid.Col span={{ base: 12, lg: 4 }}>
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}}
h="100%"
>
<Group gap="xs" mb="md">
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
<Users size={14} />
</ThemeIcon>
<Title order={4} c={dark ? "white" : "gray.9"}>
Data per Banjar
</Title>
</Group>
<Box style={{ overflowX: "auto" }}>
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr>
<th
style={{
textAlign: "left",
padding: "8px",
fontSize: "12px",
fontWeight: 600,
color: dark ? "#94A3B8" : "#64748B",
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
}}
>
Banjar
</th>
<th
style={{
textAlign: "right",
padding: "8px",
fontSize: "12px",
fontWeight: 600,
color: dark ? "#94A3B8" : "#64748B",
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
}}
>
Penduduk
</th>
<th
style={{
textAlign: "right",
padding: "8px",
fontSize: "12px",
fontWeight: 600,
color: dark ? "#94A3B8" : "#64748B",
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
}}
>
KK
</th>
<th
style={{
textAlign: "right",
padding: "8px",
fontSize: "12px",
fontWeight: 600,
color: dark ? "#94A3B8" : "#64748B",
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
}}
>
Miskin
</th>
</tr>
</thead>
<tbody>
{banjarData.map((item, index) => (
<tr
key={index}
style={{
backgroundColor:
index % 2 === 0
? dark
? "#334155"
: "#F8FAFC"
: "transparent",
transition: "background-color 0.15s ease",
}}
>
<td
style={{
padding: "10px 8px",
fontSize: "13px",
fontWeight: 500,
color: dark ? "#E2E8F0" : "#1E293B",
}}
>
{item.banjar}
</td>
<td
style={{
padding: "10px 8px",
textAlign: "right",
fontSize: "13px",
color: dark ? "#E2E8F0" : "#1E293B",
}}
>
{item.population.toLocaleString()}
</td>
<td
style={{
padding: "10px 8px",
textAlign: "right",
fontSize: "13px",
color: dark ? "#E2E8F0" : "#1E293B",
}}
>
{item.kk.toLocaleString()}
</td>
<td
style={{
padding: "10px 8px",
textAlign: "right",
fontSize: "13px",
color: "#EF4444",
fontWeight: 600,
}}
>
{item.poor.toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
</Box>
</Card>
</Grid.Col>
{/* RIGHT: STATISTIK SEKTOR UNGGULAN */}
<Grid.Col span={{ base: 12, lg: 4 }}>
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}}
h="100%"
>
<Group gap="xs" mb="md">
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
<BarChart3 size={14} />
</ThemeIcon>
<Title order={4} c={dark ? "white" : "gray.9"}>
Sektor Unggulan
</Title>
</Group>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={sektorUnggulanData} layout="vertical">
<CartesianGrid
strokeDasharray="3 3"
horizontal={false}
stroke={dark ? "#334155" : "#e5e7eb"}
/>
<XAxis
type="number"
axisLine={false}
tickLine={false}
tick={{
fill: dark ? "#E2E8F0" : "#374151",
fontSize: 12,
}}
/>
<YAxis
type="category"
dataKey="sektor"
axisLine={false}
tickLine={false}
tick={{
fill: dark ? "#E2E8F0" : "#374151",
fontSize: 12,
}}
width={90}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1E293B" : "white",
borderColor: dark ? "#334155" : "#e5e7eb",
borderRadius: "8px",
}}
/>
<Bar
dataKey="value"
fill="#1E3A5F"
radius={[0, 8, 8, 0]}
maxBarSize={40}
>
{sektorUnggulanData.map((entry, index) => (
<Cell key={`cell-${index}`} fill="#1E3A5F" />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</Card>
</Grid.Col>
</Grid>
</Stack>
); );
}; };
export default DemografiPekerjaan; export default DemografiPekerjaan;

View File

@@ -9,11 +9,18 @@ import {
Title, Title,
useMantineColorScheme, useMantineColorScheme,
} from "@mantine/core"; } from "@mantine/core";
import { IconUserShield } from "@tabler/icons-react"; import {
IconLayoutSidebarLeftCollapse,
IconUserShield,
} from "@tabler/icons-react";
import { useLocation, useNavigate } from "@tanstack/react-router"; import { useLocation, useNavigate } from "@tanstack/react-router";
import { Bell, Moon, Sun, User as UserIcon } from "lucide-react"; // Renamed User to UserIcon to avoid conflict with Mantine's User component if it exists import { Bell, Moon, Sun, User as UserIcon } from "lucide-react";
export function Header() { interface HeaderProps {
onSidebarToggle?: () => void;
}
export function Header({ onSidebarToggle }: HeaderProps) {
const location = useLocation(); const location = useLocation();
const { colorScheme, toggleColorScheme } = useMantineColorScheme(); const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; const dark = colorScheme === "dark";
@@ -56,9 +63,24 @@ export function Header() {
return ( return (
<Group justify="space-between" w="100%"> <Group justify="space-between" w="100%">
{/* Title */} {/* Title */}
<Title order={3} c={"white"}> <Group gap="md">
{getPageTitle()} <ActionIcon
</Title> onClick={onSidebarToggle}
variant="subtle"
size="lg"
radius="xl"
visibleFrom="sm"
aria-label="Toggle sidebar"
>
<IconLayoutSidebarLeftCollapse
color="white"
style={{ width: "70%", height: "70%" }}
/>
</ActionIcon>
{/* <Title order={3} c={"white"}>
{getPageTitle()}
</Title> */}
</Group>
{/* Right Section */} {/* Right Section */}
<Group gap="md"> <Group gap="md">
@@ -118,4 +140,4 @@ export function Header() {
</Group> </Group>
</Group> </Group>
); );
} }

View File

@@ -181,7 +181,7 @@ const HelpPage = () => {
<HelpCard <HelpCard
style={{ borderColor: dark ? "#141D34" : "white" }} style={{ borderColor: dark ? "#141D34" : "white" }}
bg={dark ? "#141D34" : "white"} bg={dark ? "#141D34" : "white"}
icon={<IconBook size={24} />} icon={<IconBook size={24} color="white" />}
title="Panduan Memulai" title="Panduan Memulai"
h="100%" h="100%"
> >
@@ -211,7 +211,7 @@ const HelpPage = () => {
<HelpCard <HelpCard
style={{ borderColor: dark ? "#141D34" : "white" }} style={{ borderColor: dark ? "#141D34" : "white" }}
bg={dark ? "#141D34" : "white"} bg={dark ? "#141D34" : "white"}
icon={<IconVideo size={24} />} icon={<IconVideo size={24} color="white" />}
title="Video Tutorial" title="Video Tutorial"
h="100%" h="100%"
> >
@@ -241,7 +241,7 @@ const HelpPage = () => {
<HelpCard <HelpCard
style={{ borderColor: dark ? "#141D34" : "white" }} style={{ borderColor: dark ? "#141D34" : "white" }}
bg={dark ? "#141D34" : "white"} bg={dark ? "#141D34" : "white"}
icon={<IconHelpCircle size={24} />} icon={<IconHelpCircle size={24} color="white" />}
title="FAQ" title="FAQ"
h="100%" h="100%"
> >
@@ -273,7 +273,7 @@ const HelpPage = () => {
<HelpCard <HelpCard
style={{ borderColor: dark ? "#141D34" : "white" }} style={{ borderColor: dark ? "#141D34" : "white" }}
bg={dark ? "#141D34" : "white"} bg={dark ? "#141D34" : "white"}
icon={<IconHeadphones size={24} />} icon={<IconHeadphones size={24} color="white" />}
title="Hubungi Support" title="Hubungi Support"
h="100%" h="100%"
> >
@@ -308,7 +308,7 @@ const HelpPage = () => {
<HelpCard <HelpCard
style={{ borderColor: dark ? "#141D34" : "white" }} style={{ borderColor: dark ? "#141D34" : "white" }}
bg={dark ? "#141D34" : "white"} bg={dark ? "#141D34" : "white"}
icon={<IconFileText size={24} />} icon={<IconFileText size={24} color="white" />}
title="Dokumentasi" title="Dokumentasi"
h="100%" h="100%"
> >
@@ -340,7 +340,7 @@ const HelpPage = () => {
<HelpCard <HelpCard
style={{ borderColor: dark ? "#141D34" : "white" }} style={{ borderColor: dark ? "#141D34" : "white" }}
bg={dark ? "#141D34" : "white"} bg={dark ? "#141D34" : "white"}
icon={<IconMessage size={24} />} icon={<IconMessage size={24} color="white" />}
title="Jenna - Virtual Assistant" title="Jenna - Virtual Assistant"
h="100%" h="100%"
> >
@@ -432,4 +432,4 @@ const HelpPage = () => {
); );
}; };
export default HelpPage; export default HelpPage;

View File

@@ -1,123 +1,79 @@
import { BarChart } from "@mantine/charts";
import { import {
Badge, Badge,
Box, Box,
Button,
Card, Card,
Grid, Grid,
GridCol,
Group, Group,
Progress, Progress,
Stack, Stack,
Text, Text,
ThemeIcon,
Title, Title,
useMantineColorScheme, useMantineColorScheme,
} from "@mantine/core"; } from "@mantine/core";
import React from "react"; import {
AlertTriangle,
CheckCircle,
Clock,
MessageCircle,
TrendingUp,
} from "lucide-react";
import {
Bar,
BarChart,
CartesianGrid,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
// Sample Data // KPI Data
const kpiData = [ const kpiData = [
{ {
id: 1, id: 1,
title: "Interaksi Hari Ini", title: "Interaksi Hari Ini",
value: "61", value: "61",
delta: "+15% dari kemarin", subtitle: "+15% dari kemarin",
deltaType: "positive", trend: "positive",
icon: ( icon: MessageCircle,
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.625 12a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H16.5m-13.5 3h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Z"
/>
</svg>
),
}, },
{ {
id: 2, id: 2,
title: "Jawaban Otomatis", title: "Jawaban Otomatis",
value: "87%", value: "87%",
sub: "53 dari 61 interaksi", subtitle: "53 dari 61 interaksi",
icon: ( icon: CheckCircle,
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.473-1.688 3.342-.48.485-.926.97-1.378 1.44c-1.472 1.58-2.306 2.787-2.91 3.514-.15.18-.207.33-.207.33A.75.75 0 0 1 15 21h-3c-1.104 0-2.08-.542-2.657-1.455-.139-.201-.264-.406-.38-.614l-.014-.025C8.85 18.067 8.156 17.2 7.5 16.325.728 12.56.728 7.44 7.5 3.675c3.04-.482 5.584.47 7.042 1.956.674.672 1.228 1.462 1.696 2.307.426.786.793 1.582 1.113 2.392h.001Z"
/>
</svg>
),
}, },
{ {
id: 3, id: 3,
title: "Belum Ditindak", title: "Belum Ditindak",
value: "8", value: "8",
sub: "Perlu respon manual", subtitle: "Perlu respon manual",
deltaType: "negative", icon: AlertTriangle,
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
/>
</svg>
),
}, },
{ {
id: 4, id: 4,
title: "Waktu Respon", title: "Waktu Respon",
value: "2.3 sec", value: "2.3 sec",
sub: "Rata-rata", subtitle: "Rata-rata",
icon: ( icon: Clock,
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
),
}, },
]; ];
// Chart Data
const chartData = [ const chartData = [
{ day: "Sen", total: 100 }, { day: "Sen", total: 45 },
{ day: "Sel", total: 120 }, { day: "Sel", total: 62 },
{ day: "Rab", total: 90 }, { day: "Rab", total: 38 },
{ day: "Kam", total: 150 }, { day: "Kam", total: 75 },
{ day: "Jum", total: 110 }, { day: "Jum", total: 58 },
{ day: "Sab", total: 80 }, { day: "Sab", total: 32 },
{ day: "Min", total: 130 }, { day: "Min", total: 51 },
]; ];
// Top Topics Data
const topTopics = [ const topTopics = [
{ topic: "Cara mengurus KTP", count: 89 }, { topic: "Cara mengurus KTP", count: 89 },
{ topic: "Syarat Kartu Keluarga", count: 76 }, { topic: "Syarat Kartu Keluarga", count: 76 },
@@ -126,6 +82,7 @@ const topTopics = [
{ topic: "Info program bansos", count: 48 }, { topic: "Info program bansos", count: 48 },
]; ];
// Busy Hours Data
const busyHours = [ const busyHours = [
{ period: "Pagi (0812)", percentage: 30 }, { period: "Pagi (0812)", percentage: 30 },
{ period: "Siang (1216)", percentage: 40 }, { period: "Siang (1216)", percentage: 40 },
@@ -138,146 +95,206 @@ const JennaAnalytic = () => {
const dark = colorScheme === "dark"; const dark = colorScheme === "dark";
return ( return (
<Box className="space-y-6"> <Stack gap="lg">
<Stack gap="xl"> {/* TOP SECTION - 4 STAT CARDS */}
{/* KPI Cards */} <Grid gutter="md">
<Grid gutter="lg"> {kpiData.map((item) => (
{kpiData.map((kpi) => ( <Grid.Col key={item.id} span={{ base: 12, sm: 6, lg: 3 }}>
<Grid.Col key={kpi.id} span={{ base: 12, md: 6, lg: 3 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Group justify="space-between" align="flex-start" mb="xs">
<Text size="sm" fw={500} c="dimmed">
{kpi.title}
</Text>
{React.cloneElement(kpi.icon, {
className: "h-6 w-6", // Keeping classes for now, can be replaced by Mantine Icon component if available or styled with sx prop
color: "var(--mantine-color-dimmed)", // Set color via prop
})}
</Group>
<Title order={3} fw={700} mt="xs">
{kpi.value}
</Title>
{kpi.delta && (
<Text
size="xs"
c={
kpi.deltaType === "positive"
? "green"
: kpi.deltaType === "negative"
? "red"
: "dimmed"
}
mt={4}
>
{kpi.delta}
</Text>
)}
{kpi.sub && (
<Text size="xs" c="dimmed" mt={2}>
{kpi.sub}
</Text>
)}
</Card>
</Grid.Col>
))}
</Grid>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={3} fw={500} mb="md">
Interaksi Chatbot
</Title>
<BarChart
h={300}
data={chartData}
dataKey="day"
series={[{ name: "total", color: "blue" }]}
withLegend
/>
</Card>
{/* Charts and Lists Section */}
<Grid gutter="lg">
{/* Grafik Interaksi Chatbot (now Bar Chart) */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card <Card
p="md" p="md"
radius="md" radius="xl"
withBorder withBorder
bg={dark ? "#141D34" : "white"} bg={dark ? "#1E293B" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }} style={{
borderColor: dark ? "#334155" : "white",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
transition: "transform 0.15s ease, box-shadow 0.15s ease",
}}
h="100%" h="100%"
> >
<Title order={3} fw={500} mb="md"> <Group justify="space-between" align="flex-start" w="100%">
Jam Tersibuk <Stack gap={2}>
</Title> <Text size="sm" c="dimmed">
<Stack gap="sm"> {item.title}
{busyHours.map((item, index) => ( </Text>
<Box key={index}> <Text size="xl" fw={700} c={dark ? "white" : "gray.9"}>
<Text size="sm">{item.period}</Text> {item.value}
<Group align="center"> </Text>
<Progress value={item.percentage} flex={1} /> <Group gap={4} align="flex-start">
<Text size="sm" fw={500}> {item.trend === "positive" && (
{item.percentage}% <TrendingUp size={14} color="#22C55E" />
</Text> )}
</Group> <Text
</Box> size="xs"
))} c={
</Stack> item.trend === "positive"
? "green"
: dark
? "gray.4"
: "gray.5"
}
>
{item.subtitle}
</Text>
</Group>
</Stack>
<ThemeIcon
color="#1E3A5F"
variant="filled"
size="lg"
radius="xl"
>
<item.icon style={{ width: "60%", height: "60%" }} />
</ThemeIcon>
</Group>
</Card> </Card>
</Grid.Col> </Grid.Col>
))}
</Grid>
{/* Topik Pertanyaan Terbanyak & Jam Tersibuk */} {/* MAIN CHART - INTERAKSI CHATBOT */}
<Grid.Col span={{ base: 12, lg: 6 }}> <Card
<Stack gap="lg"> p="md"
{/* Topik Pertanyaan Terbanyak */} radius="xl"
<Card withBorder
p="md" bg={dark ? "#1E293B" : "white"}
radius="md" style={{
withBorder borderColor: dark ? "#334155" : "white",
bg={dark ? "#141D34" : "white"} boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
style={{ borderColor: dark ? "#141D34" : "white" }} }}
h="100%" >
> <Group justify="space-between" mb="md">
<Title order={3} fw={500} mb="md"> <Title order={4} c={dark ? "white" : "gray.9"}>
Topik Pertanyaan Terbanyak Interaksi Chatbot
</Title> </Title>
<Stack gap="xs"> </Group>
{topTopics.map((item, index) => ( <ResponsiveContainer width="100%" height={300}>
<Group <BarChart data={chartData}>
key={index} <CartesianGrid
justify="space-between" strokeDasharray="3 3"
align="center" vertical={false}
p="xs" stroke={dark ? "#334155" : "#e5e7eb"}
/>
<XAxis
dataKey="day"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1E293B" : "white",
borderColor: dark ? "#334155" : "#e5e7eb",
borderRadius: "8px",
}}
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
cursor={{ fill: dark ? "#334155" : "#f3f4f6" }}
/>
<Bar
dataKey="total"
fill="#1E3A5F"
radius={[8, 8, 0, 0]}
maxBarSize={60}
/>
</BarChart>
</ResponsiveContainer>
</Card>
{/* BOTTOM SECTION - 2 COLUMNS */}
<Grid gutter="lg">
{/* LEFT: TOPIK PERTANYAAN TERBANYAK */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}}
h="100%"
>
<Title order={4} c={dark ? "white" : "gray.9"} mb="md">
Topik Pertanyaan Terbanyak
</Title>
<Stack gap="xs">
{topTopics.map((item, index) => (
<Box
key={index}
p="sm"
bg={dark ? "#334155" : "#F1F5F9"}
style={{
transition: "background-color 0.15s ease",
cursor: "pointer",
}}
>
<Group justify="space-between">
<Text size="sm" fw={500} c={dark ? "white" : "gray.9"}>
{item.topic}
</Text>
<Badge
variant="light"
color="darmasaba-blue"
radius="sm"
fw={600}
> >
<Text size="sm" fw={500}> {item.count}x
{item.topic} </Badge>
</Text> </Group>
<Badge variant="light" color="gray"> </Box>
{item.count}x ))}
</Badge>
</Group>
))}
</Stack>
</Card>
{/* Jam Tersibuk */}
</Stack> </Stack>
</Grid.Col> </Card>
</Grid> </Grid.Col>
</Stack>
</Box> {/* RIGHT: JAM TERSIBUK */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}}
h="100%"
>
<Title order={4} c={dark ? "white" : "gray.9"} mb="md">
Jam Tersibuk
</Title>
<Stack gap="md">
{busyHours.map((item, index) => (
<Box key={index}>
<Group justify="space-between" mb={5}>
<Text size="sm" fw={500} c={dark ? "white" : "gray.9"}>
{item.period}
</Text>
<Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
{item.percentage}%
</Text>
</Group>
<Progress
value={item.percentage}
size="lg"
radius="xl"
color="#1E3A5F"
animated
/>
</Box>
))}
</Stack>
</Card>
</Grid.Col>
</Grid>
</Stack>
); );
}; };
export default JennaAnalytic;
export default JennaAnalytic;

View File

@@ -5,22 +5,18 @@ import {
Grid, Grid,
GridCol, GridCol,
Group, Group,
List,
Stack, Stack,
Text, Text,
ThemeIcon, ThemeIcon,
Title, Title,
useMantineColorScheme, useMantineColorScheme
} from "@mantine/core"; } from "@mantine/core";
import { import {
IconAlertTriangle, IconAlertTriangle,
IconCamera, IconCamera,
IconClock, IconClock,
IconEye, IconMapPin
IconMapPin,
IconShieldLock,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { useState } from "react";
const KeamananPage = () => { const KeamananPage = () => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
@@ -125,10 +121,53 @@ const KeamananPage = () => {
</Title> </Title>
</Group> </Group>
{/* KPI Cards */}
<Grid gutter="md"> <Grid gutter="md">
{kpiData.map((kpi, index) => ( {/* Peta Keamanan CCTV */}
<GridCol key={index} span={{ base: 12, sm: 6, md: 6 }}> <GridCol span={{ base: 12, lg: 6 }}>
<Stack gap={"xs"}>
{/* KPI Cards */}
<Grid gutter="md">
{kpiData.map((kpi, index) => (
<GridCol key={index} span={{ base: 12, sm: 6, md: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
h="100%"
>
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{kpi.subtitle}
</Text>
<Group gap="xs" align="center">
<Text
size="xl"
fw={700}
c={dark ? "dark.0" : "black"}
>
{kpi.value}
</Text>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{kpi.title}
</Text>
</Group>
</Stack>
<ThemeIcon
variant="light"
color={kpi.color}
size="xl"
radius="xl"
>
{kpi.icon}
</ThemeIcon>
</Group>
</Card>
</GridCol>
))}
</Grid>
<Card <Card
p="md" p="md"
radius="md" radius="md"
@@ -137,119 +176,81 @@ const KeamananPage = () => {
style={{ borderColor: dark ? "#141D34" : "white" }} style={{ borderColor: dark ? "#141D34" : "white" }}
h="100%" h="100%"
> >
<Group justify="space-between" align="center"> <Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
<Stack gap={0}> Peta Keamanan CCTV
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{kpi.subtitle}
</Text>
<Group gap="xs" align="center">
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
{kpi.value}
</Text>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{kpi.title}
</Text>
</Group>
</Stack>
<ThemeIcon
variant="light"
color={kpi.color}
size="xl"
radius="xl"
>
{kpi.icon}
</ThemeIcon>
</Group>
</Card>
</GridCol>
))}
</Grid>
<Grid gutter="md">
{/* Peta Keamanan CCTV */}
<GridCol span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
h="100%"
>
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
Peta Keamanan CCTV
</Title>
<Text size="sm" c={dark ? "dark.3" : "dimmed"} mb="md">
Titik Lokasi CCTV
</Text>
{/* Placeholder for map */}
<Box
style={{
backgroundColor: dark ? "#2d3748" : "#e2e8f0",
borderRadius: "8px",
height: "400px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Stack align="center">
<IconMapPin
size={48}
stroke={1.5}
color={dark ? "#94a3b8" : "#64748b"}
/>
<Text c={dark ? "dark.3" : "dimmed"}>Peta Lokasi CCTV</Text>
<Text size="sm" c={dark ? "dark.3" : "dimmed"} ta="center">
Integrasi dengan Google Maps atau Mapbox akan ditampilkan di
sini
</Text>
</Stack>
</Box>
{/* CCTV Locations List */}
<Stack mt="md" gap="sm">
<Title order={4} c={dark ? "dark.0" : "black"}>
Daftar CCTV
</Title> </Title>
{cctvLocations.map((cctv, index) => ( <Text size="sm" c={dark ? "dark.3" : "dimmed"} mb="md">
<Card Titik Lokasi CCTV
key={index} </Text>
p="md"
radius="md" {/* Placeholder for map */}
withBorder <Box
bg={dark ? "#263852ff" : "#F1F5F9"} style={{
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }} backgroundColor: dark ? "#2d3748" : "#e2e8f0",
> borderRadius: "8px",
<Group justify="space-between"> height: "400px",
<Stack gap={0}> display: "flex",
<Group gap="xs"> alignItems: "center",
<Text fw={500} c={dark ? "dark.0" : "black"}> justifyContent: "center",
{cctv.id} }}
>
<Stack align="center">
<IconMapPin
size={48}
stroke={1.5}
color={dark ? "#94a3b8" : "#64748b"}
/>
<Text c={dark ? "dark.3" : "dimmed"}>Peta Lokasi CCTV</Text>
<Text size="sm" c={dark ? "dark.3" : "dimmed"} ta="center">
Integrasi dengan Google Maps atau Mapbox akan ditampilkan di
sini
</Text>
</Stack>
</Box>
{/* CCTV Locations List */}
<Stack mt="md" gap="sm">
<Title order={4} c={dark ? "dark.0" : "black"}>
Daftar CCTV
</Title>
{cctvLocations.map((cctv, index) => (
<Card
key={index}
p="md"
radius="md"
withBorder
bg={dark ? "#263852ff" : "#F1F5F9"}
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
>
<Group justify="space-between">
<Stack gap={0}>
<Group gap="xs">
<Text fw={500} c={dark ? "dark.0" : "black"}>
{cctv.id}
</Text>
<Badge
variant="dot"
color={cctv.status === "active" ? "green" : "gray"}
>
{cctv.status === "active" ? "Online" : "Offline"}
</Badge>
</Group>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{cctv.location}
</Text>
</Stack>
<Group gap="xs">
<IconClock size={16} stroke={1.5} />
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{cctv.lastSeen}
</Text> </Text>
<Badge
variant="dot"
color={cctv.status === "active" ? "green" : "gray"}
>
{cctv.status === "active" ? "Online" : "Offline"}
</Badge>
</Group> </Group>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{cctv.location}
</Text>
</Stack>
<Group gap="xs">
<IconClock size={16} stroke={1.5} />
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{cctv.lastSeen}
</Text>
</Group> </Group>
</Group> </Card>
</Card> ))}
))} </Stack>
</Stack> </Card>
</Card> </Stack>
</GridCol> </GridCol>
{/* Daftar Laporan Keamanan */} {/* Daftar Laporan Keamanan */}
@@ -262,10 +263,6 @@ const KeamananPage = () => {
style={{ borderColor: dark ? "#141D34" : "white" }} style={{ borderColor: dark ? "#141D34" : "white" }}
h="100%" h="100%"
> >
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
Laporan Keamanan Lingkungan
</Title>
<Stack gap="sm"> <Stack gap="sm">
{securityReports.map((report, index) => ( {securityReports.map((report, index) => (
<Card <Card
@@ -322,4 +319,4 @@ const KeamananPage = () => {
); );
}; };
export default KeamananPage; export default KeamananPage;

View File

@@ -1,73 +1,70 @@
import { BarChart } from "@mantine/charts";
import { import {
Badge, Badge,
Box, Box,
Button,
Card, Card,
Grid, Grid,
GridCol,
Group, Group,
Progress,
Stack, Stack,
Text, Text,
ThemeIcon,
Title, Title,
useMantineColorScheme, useMantineColorScheme,
} from "@mantine/core"; } from "@mantine/core";
import { import {
IconCurrency, CheckCircle,
IconTrendingDown, Coins,
IconTrendingUp, PieChart as PieChartIcon,
} from "@tabler/icons-react"; Receipt,
import React from "react"; TrendingDown,
TrendingUp,
} from "lucide-react";
import {
Bar,
BarChart,
CartesianGrid,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
// Sample Data // KPI Data
const kpiData = [ const kpiData = [
{ {
id: 1, id: 1,
title: "Total APBDes", title: "Total APBDes",
value: "Rp 5.2M", value: "Rp 5.2M",
sub: "Tahun 2025", subtitle: "Tahun 2025",
icon: <IconCurrency className="h-6 w-6 text-muted-foreground" />, icon: Coins,
}, },
{ {
id: 2, id: 2,
title: "Realisasi", title: "Realisasi",
value: "68%", value: "68%",
sub: "Rp 3.5M dari 5.2M", subtitle: "Rp 3.5M dari 5.2M",
icon: ( icon: CheckCircle,
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.473-1.688 3.342-.48.485-.926.97-1.378 1.44c-1.472 1.58-2.306 2.787-2.91 3.514-.15.18-.207.33-.207.33A.75.75 0 0 1 15 21h-3c-1.104 0-2.08-.542-2.657-1.455-.139-.201-.264-.406-.38-.614l-.014-.025C8.85 18.067 8.156 17.2 7.5 16.325.728 12.56.728 7.44 7.5 3.675c3.04-.482 5.584.47 7.042 1.956.674.672 1.228 1.462 1.696 2.307.426.786.793 1.582 1.113 2.392h.001Z"
/>
</svg>
),
}, },
{ {
id: 3, id: 3,
title: "Pemasukan", title: "Pemasukan",
value: "Rp 580jt", value: "Rp 580jt",
sub: "Bulan ini", subtitle: "Bulan ini",
delta: "+8%", trend: "+8%",
deltaType: "positive", icon: TrendingUp,
icon: <IconTrendingUp className="h-6 w-6 text-muted-foreground" />,
}, },
{ {
id: 4, id: 4,
title: "Pengeluaran", title: "Pengeluaran",
value: "Rp 520jt", value: "Rp 520jt",
sub: "Bulan ini", subtitle: "Bulan ini",
icon: <IconTrendingDown className="h-6 w-6 text-muted-foreground" />, icon: TrendingDown,
}, },
]; ];
// Income & Expense Data
const incomeExpenseData = [ const incomeExpenseData = [
{ month: "Apr", income: 450, expense: 380 }, { month: "Apr", income: 450, expense: 380 },
{ month: "Mei", income: 520, expense: 420 }, { month: "Mei", income: 520, expense: 420 },
@@ -78,6 +75,7 @@ const incomeExpenseData = [
{ month: "Okt", income: 580, expense: 520 }, { month: "Okt", income: 580, expense: 520 },
]; ];
// Sector Allocation Data
const allocationData = [ const allocationData = [
{ sector: "Pembangunan", amount: 1200 }, { sector: "Pembangunan", amount: 1200 },
{ sector: "Kesehatan", amount: 800 }, { sector: "Kesehatan", amount: 800 },
@@ -87,13 +85,7 @@ const allocationData = [
{ sector: "Teknologi", amount: 300 }, { sector: "Teknologi", amount: 300 },
]; ];
const assistanceFundData = [ // APBDes Report Data
{ source: "Dana Desa (DD)", amount: 1800, status: "cair" },
{ source: "Alokasi Dana Desa (ADD)", amount: 950, status: "cair" },
{ source: "Bagi Hasil Pajak", amount: 450, status: "cair" },
{ source: "Hibah Provinsi", amount: 300, status: "proses" },
];
const apbdReport = { const apbdReport = {
income: [ income: [
{ category: "Dana Desa", amount: 1800 }, { category: "Dana Desa", amount: 1800 },
@@ -113,245 +105,411 @@ const apbdReport = {
totalExpenses: 2155, totalExpenses: 2155,
}; };
// Aid & Grants Data
const assistanceFundData = [
{ source: "Dana Desa (DD)", amount: 1800, status: "cair" },
{ source: "Alokasi Dana Desa (ADD)", amount: 950, status: "cair" },
{ source: "Bagi Hasil Pajak", amount: 450, status: "cair" },
{ source: "Hibah Provinsi", amount: 300, status: "proses" },
];
const KeuanganAnggaran = () => { const KeuanganAnggaran = () => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; const dark = colorScheme === "dark";
return (
<Box>
<Stack gap="xl">
{/* KPI Cards */}
<Grid gutter="lg">
{kpiData.map((kpi) => (
<Grid.Col key={kpi.id} span={{ base: 12, md: 6, lg: 3 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
h="100%"
>
<Group justify="space-between" align="flex-start" mb="xs">
<Text size="sm" fw={500} c="dimmed">
{kpi.title}
</Text>
{React.cloneElement(kpi.icon, {
className: "h-6 w-6",
color: "var(--mantine-color-dimmed)",
})}
</Group>
<Title order={3} fw={700} mt="xs">
{kpi.value}
</Title>
{kpi.delta && (
<Text
size="xs"
c={
kpi.deltaType === "positive"
? "green"
: kpi.deltaType === "negative"
? "red"
: "dimmed"
}
mt={4}
>
{kpi.delta}
</Text>
)}
{kpi.sub && (
<Text size="xs" c="dimmed" mt="auto">
{kpi.sub}
</Text>
)}
</Card>
</Grid.Col>
))}
</Grid>
{/* Charts Section */} return (
<Grid gutter="lg"> <Stack gap="lg">
{/* Grafik Pemasukan vs Pengeluaran */} {/* TOP SECTION - 4 STAT CARDS */}
<Grid.Col span={{ base: 12, lg: 6 }}> <Grid gutter="md">
{kpiData.map((item) => (
<Grid.Col key={item.id} span={{ base: 12, sm: 6, lg: 3 }}>
<Card <Card
p="md" p="md"
radius="md" radius="xl"
withBorder withBorder
bg={dark ? "#141D34" : "white"} bg={dark ? "#1E293B" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }} style={{
borderColor: dark ? "#334155" : "white",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
transition: "transform 0.15s ease, box-shadow 0.15s ease",
}}
h="100%"
> >
<Title order={3} fw={500} mb="md"> <Group justify="space-between" align="flex-start" w="100%">
Pemasukan vs Pengeluaran <Stack gap={2}>
</Title> <Text size="sm" c="dimmed">
<BarChart {item.title}
h={300} </Text>
data={incomeExpenseData} <Text size="xl" fw={700} c={dark ? "white" : "gray.9"}>
dataKey="month" {item.value}
series={[ </Text>
{ name: "income", color: "green", label: "Pemasukan" }, <Group gap={4} align="flex-start">
{ name: "expense", color: "red", label: "Pengeluaran" }, {item.trend && <TrendingUp size={14} color="#22C55E" />}
]} <Text
withLegend size="xs"
/> c={item.trend ? "green" : dark ? "gray.4" : "gray.5"}
>
{item.subtitle}
</Text>
</Group>
</Stack>
<ThemeIcon
color="#1E3A5F"
variant="filled"
size="lg"
radius="xl"
>
<item.icon style={{ width: "60%", height: "60%" }} />
</ThemeIcon>
</Group>
</Card> </Card>
</Grid.Col> </Grid.Col>
))}
</Grid>
{/* Alokasi Anggaran Per Sektor */} {/* MAIN CHART SECTION */}
<Grid.Col span={{ base: 12, lg: 6 }}> <Grid gutter="lg">
<Card {/* LEFT: PEMASUKAN DAN PENGELUARAN (70%) */}
p="md" <Grid.Col span={{ base: 12, lg: 8 }}>
radius="md" <Card
withBorder p="md"
bg={dark ? "#141D34" : "white"} radius="xl"
style={{ borderColor: dark ? "#141D34" : "white" }} withBorder
> bg={dark ? "#1E293B" : "white"}
<Title order={3} fw={500} mb="md"> style={{
borderColor: dark ? "#334155" : "white",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}}
h="100%"
>
<Group gap="xs" mb="md">
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
<PieChartIcon size={14} />
</ThemeIcon>
<Title order={4} c={dark ? "white" : "gray.9"}>
Pemasukan dan Pengeluaran
</Title>
</Group>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={incomeExpenseData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#334155" : "#e5e7eb"}
/>
<XAxis
dataKey="month"
axisLine={false}
tickLine={false}
tick={{
fill: dark ? "#E2E8F0" : "#374151",
fontSize: 12,
}}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{
fill: dark ? "#E2E8F0" : "#374151",
fontSize: 12,
}}
tickFormatter={(value) => `Rp ${value}jt`}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1E293B" : "white",
borderColor: dark ? "#334155" : "#e5e7eb",
borderRadius: "8px",
}}
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
formatter={(value: number | undefined) => [
`Rp ${value}jt`,
"",
]}
/>
<Line
type="monotone"
dataKey="income"
stroke="#22C55E"
strokeWidth={2}
dot={{ fill: "#22C55E", strokeWidth: 2, r: 4 }}
activeDot={{ r: 6 }}
name="Pemasukan"
/>
<Line
type="monotone"
dataKey="expense"
stroke="#EF4444"
strokeWidth={2}
dot={{ fill: "#EF4444", strokeWidth: 2, r: 4 }}
activeDot={{ r: 6 }}
name="Pengeluaran"
/>
</LineChart>
</ResponsiveContainer>
</Card>
</Grid.Col>
{/* RIGHT: ALOKASI ANGGARAN PER SEKTOR (30%) */}
<Grid.Col span={{ base: 12, lg: 4 }}>
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}}
h="100%"
>
<Group gap="xs" mb="md">
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
<PieChartIcon size={14} />
</ThemeIcon>
<Title order={4} c={dark ? "white" : "gray.9"}>
Alokasi Anggaran Per Sektor Alokasi Anggaran Per Sektor
</Title> </Title>
<BarChart </Group>
h={300} <ResponsiveContainer width="100%" height={300}>
data={allocationData} <BarChart data={allocationData} layout="vertical">
dataKey="sector" <CartesianGrid
series={[ strokeDasharray="3 3"
{ name: "amount", color: "darmasaba-navy", label: "Jumlah" }, horizontal={false}
]} stroke={dark ? "#334155" : "#e5e7eb"}
withLegend />
orientation="horizontal" <XAxis
/> type="number"
</Card> axisLine={false}
</Grid.Col> tickLine={false}
</Grid> tick={{
fill: dark ? "#E2E8F0" : "#374151",
fontSize: 12,
}}
tickFormatter={(value) => `${value}`}
/>
<YAxis
type="category"
dataKey="sector"
axisLine={false}
tickLine={false}
tick={{
fill: dark ? "#E2E8F0" : "#374151",
fontSize: 11,
}}
width={100}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1E293B" : "white",
borderColor: dark ? "#334155" : "#e5e7eb",
borderRadius: "8px",
}}
formatter={(value: number | undefined) => [
`Rp ${value}jt`,
"Jumlah",
]}
/>
<Bar
dataKey="amount"
fill="#1E3A5F"
radius={[0, 8, 8, 0]}
maxBarSize={30}
/>
</BarChart>
</ResponsiveContainer>
</Card>
</Grid.Col>
</Grid>
<Grid gutter="lg"> {/* BOTTOM SECTION */}
{/* Dana Bantuan & Hibah */} <Grid gutter="lg">
<Grid.Col span={{ base: 12, lg: 6 }}> {/* LEFT: LAPORAN APBDES */}
<Card <Grid.Col span={{ base: 12, lg: 6 }}>
p="md" <Card
radius="md" p="md"
withBorder radius="xl"
bg={dark ? "#141D34" : "white"} withBorder
style={{ borderColor: dark ? "#141D34" : "white" }} bg={dark ? "#1E293B" : "white"}
> style={{
<Title order={3} fw={500} mb="md"> borderColor: dark ? "#334155" : "white",
Dana Bantuan & Hibah boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}}
h="100%"
>
<Group gap="xs" mb="md">
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
<Receipt size={14} />
</ThemeIcon>
<Title order={4} c={dark ? "white" : "gray.9"}>
Laporan APBDes
</Title> </Title>
<Stack gap="sm"> </Group>
{assistanceFundData.map((fund, index) => (
<Group <Grid gutter="md">
key={index} {/* Pendapatan */}
justify="space-between" <Grid.Col span={6}>
align="center" <Card p="sm" radius="lg" bg={dark ? "#064E3B" : "#DCFCE7"}>
p="sm" <Title order={5} c="#22C55E" mb="sm">
style={{ Pendapatan
border: "1px solid var(--mantine-color-gray-3)", </Title>
borderRadius: "var(--mantine-radius-sm)", <Stack gap="xs">
}} {apbdReport.income.map((item, index) => (
> <Group key={index} justify="space-between">
<Text size="sm" c={dark ? "gray.3" : "gray.7"}>
{item.category}
</Text>
<Text size="sm" fw={600} c="#22C55E">
Rp {item.amount.toLocaleString()}jt
</Text>
</Group>
))}
<Group
justify="space-between"
mt="sm"
pt="sm"
style={{
borderTop: `1px solid ${dark ? "#065F46" : "#86EFAC"}`,
}}
>
<Text fw={700} c="#22C55E">
Total:
</Text>
<Text fw={700} c="#22C55E">
Rp {apbdReport.totalIncome.toLocaleString()}jt
</Text>
</Group>
</Stack>
</Card>
</Grid.Col>
{/* Belanja */}
<Grid.Col span={6}>
<Card p="sm" radius="lg" bg={dark ? "#7F1D1D" : "#FEE2E2"}>
<Title order={5} c="#EF4444" mb="sm">
Belanja
</Title>
<Stack gap="xs">
{apbdReport.expenses.map((item, index) => (
<Group key={index} justify="space-between">
<Text size="sm" c={dark ? "gray.3" : "gray.7"}>
{item.category}
</Text>
<Text size="sm" fw={600} c="#EF4444">
Rp {item.amount.toLocaleString()}jt
</Text>
</Group>
))}
<Group
justify="space-between"
mt="sm"
pt="sm"
style={{
borderTop: `1px solid ${dark ? "#991B1B" : "#FCA5A5"}`,
}}
>
<Text fw={700} c="#EF4444">
Total:
</Text>
<Text fw={700} c="#EF4444">
Rp {apbdReport.totalExpenses.toLocaleString()}jt
</Text>
</Group>
</Stack>
</Card>
</Grid.Col>
</Grid>
{/* Saldo */}
<Group
justify="space-between"
mt="md"
pt="md"
style={{
borderTop: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
}}
>
<Text fw={700} c={dark ? "white" : "gray.9"}>
Saldo:
</Text>
<Text
fw={700}
size="lg"
c={
apbdReport.totalIncome > apbdReport.totalExpenses
? "#22C55E"
: "#EF4444"
}
>
Rp{" "}
{(
apbdReport.totalIncome - apbdReport.totalExpenses
).toLocaleString()}
jt
</Text>
</Group>
</Card>
</Grid.Col>
{/* RIGHT: DANA BANTUAN DAN HIBAH */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}}
h="100%"
>
<Group gap="xs" mb="md">
<ThemeIcon color="#1E3A5F" variant="filled" size="sm" radius="sm">
<Coins size={14} />
</ThemeIcon>
<Title order={4} c={dark ? "white" : "gray.9"}>
Dana Bantuan dan Hibah
</Title>
</Group>
<Stack gap="sm">
{assistanceFundData.map((fund, index) => (
<Card
key={index}
p="sm"
radius="lg"
bg={dark ? "#334155" : "#F1F5F9"}
style={{
borderColor: "transparent",
transition: "background-color 0.15s ease",
}}
>
<Group justify="space-between" align="center">
<Box> <Box>
<Text size="sm" fw={500}> <Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
{fund.source} {fund.source}
</Text> </Text>
<Text size="sm" c="dimmed"> <Text size="xs" c="dimmed">
Rp {fund.amount.toLocaleString()}jt Rp {fund.amount.toLocaleString()}jt
</Text> </Text>
</Box> </Box>
<Badge <Badge
variant="light" variant="light"
color={fund.status === "cair" ? "green" : "yellow"} color={fund.status === "cair" ? "green" : "yellow"}
radius="sm"
fw={600}
> >
{fund.status} {fund.status === "cair" ? "Cair" : "Proses"}
</Badge> </Badge>
</Group> </Group>
))} </Card>
</Stack> ))}
</Card> </Stack>
</Grid.Col> </Card>
</Grid.Col>
{/* Laporan APBDes */} </Grid>
<Grid.Col span={{ base: 12, lg: 6 }}> </Stack>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={3} fw={500} mb="md">
Laporan APBDes
</Title>
<Box mb="md">
<Title order={4} mb="sm">
Pendapatan
</Title>
<Stack gap="xs">
{apbdReport.income.map((item, index) => (
<Group key={index} justify="space-between">
<Text size="sm">{item.category}</Text>
<Text size="sm" c="green">
Rp {item.amount.toLocaleString()}jt
</Text>
</Group>
))}
<Group justify="space-between" mt="sm">
<Text fw={700}>Total Pendapatan:</Text>
<Text fw={700} c="green">
Rp {apbdReport.totalIncome.toLocaleString()}jt
</Text>
</Group>
</Stack>
</Box>
<Box>
<Title order={4} mb="sm">
Belanja
</Title>
<Stack gap="xs">
{apbdReport.expenses.map((item, index) => (
<Group key={index} justify="space-between">
<Text size="sm">{item.category}</Text>
<Text size="sm" c="red">
Rp {item.amount.toLocaleString()}jt
</Text>
</Group>
))}
<Group justify="space-between" mt="sm">
<Text fw={700}>Total Belanja:</Text>
<Text fw={700} c="red">
Rp {apbdReport.totalExpenses.toLocaleString()}jt
</Text>
</Group>
</Stack>
</Box>
<Box
mt="md"
pt="md"
style={{ borderTop: "1px solid var(--mantine-color-gray-3)" }}
>
<Group justify="space-between">
<Text fw={700}>Saldo:</Text>
<Text
fw={700}
c={
apbdReport.totalIncome > apbdReport.totalExpenses
? "green"
: "red"
}
>
Rp{" "}
{(
apbdReport.totalIncome - apbdReport.totalExpenses
).toLocaleString()}
jt
</Text>
</Group>
</Box>
</Card>
</Grid.Col>
</Grid>
</Stack>
</Box>
); );
}; };
export default KeuanganAnggaran; export default KeuanganAnggaran;

View File

@@ -1,13 +1,11 @@
import { Grid, Stack } from "@mantine/core"; import { Grid, Stack } from "@mantine/core";
import { import { ActivityCard } from "./kinerja-divisi/activity-card";
ActivityCard, import { ArchiveCard } from "./kinerja-divisi/archive-card";
ArchiveCard, import { DiscussionPanel } from "./kinerja-divisi/discussion-panel";
DiscussionPanel, import { DivisionList } from "./kinerja-divisi/division-list";
DivisionList, import { DocumentChart } from "./kinerja-divisi/document-chart";
DocumentChart, import { EventCard } from "./kinerja-divisi/event-card";
EventCard, import { ProgressChart } from "./kinerja-divisi/progress-chart";
ProgressChart,
} from ".";
// Data for program kegiatan (Section 1) // Data for program kegiatan (Section 1)
const programKegiatanData = [ const programKegiatanData = [
@@ -15,25 +13,25 @@ const programKegiatanData = [
title: "Rakor 2025", title: "Rakor 2025",
date: "3 Juli 2025", date: "3 Juli 2025",
progress: 90, progress: 90,
status: "selesai" as const, status: "Selesai" as const,
}, },
{ {
title: "Pemutakhiran Indeks Desa", title: "Pemutakhiran Indeks Desa",
date: "3 Juli 2025", date: "3 Juli 2025",
progress: 85, progress: 85,
status: "selesai" as const, status: "Selesai" as const,
}, },
{ {
title: "Mengurus Akta Cerai Warga", title: "Mengurus Akta Cerai Warga",
date: "3 Juli 2025", date: "3 Juli 2025",
progress: 80, progress: 80,
status: "selesai" as const, status: "Selesai" as const,
}, },
{ {
title: "Pasek 7 Desa Adat", title: "Pasek 7 Desa Adat",
date: "3 Juli 2025", date: "3 Juli 2025",
progress: 92, progress: 92,
status: "selesai" as const, status: "Selesai" as const,
}, },
]; ];

View File

@@ -1,17 +1,10 @@
import { import { Box, Card, Group, Progress, Text } from "@mantine/core";
Box,
Card,
Group,
Progress,
Text,
useMantineColorScheme,
} from "@mantine/core";
interface ActivityCardProps { interface ActivityCardProps {
title: string; title: string;
date: string; date: string;
progress: number; progress: number;
status: "selesai" | "berjalan" | "tertunda"; status: "Selesai" | "Berjalan" | "Tertunda";
} }
export function ActivityCard({ export function ActivityCard({
@@ -20,16 +13,13 @@ export function ActivityCard({
progress, progress,
status, status,
}: ActivityCardProps) { }: ActivityCardProps) {
const { colorScheme } = useMantineColorScheme(); const getStatusColor = () => {
const dark = colorScheme === "dark"; switch (status) {
case "Selesai":
const getStatusColor = (s: string) => {
switch (s) {
case "selesai":
return "#22C55E"; return "#22C55E";
case "berjalan": case "Berjalan":
return "#3B82F6"; return "#3B82F6";
case "tertunda": case "Tertunda":
return "#EF4444"; return "#EF4444";
default: default:
return "#9CA3AF"; return "#9CA3AF";
@@ -38,58 +28,62 @@ export function ActivityCard({
return ( return (
<Card <Card
p="md"
radius="xl" radius="xl"
withBorder p={0}
bg={dark ? "#1E293B" : "white"} withBorder={false}
style={{ style={{
borderColor: dark ? "#334155" : "white", backgroundColor: "#F3F4F6",
boxShadow: dark overflow: "hidden",
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}} }}
> >
{/* 🔵 HEADER */}
<Box <Box
style={{ style={{
borderLeft: `4px solid #3B82F6`, backgroundColor: "#1E3A5F",
paddingLeft: 12, padding: "16px",
marginBottom: 12, textAlign: "center",
}} }}
> >
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}> <Text c="white" fw={700} size="md">
{title} {title}
</Text> </Text>
</Box> </Box>
<Group justify="space-between" mb="xs"> {/* CONTENT */}
<Text size="xs" c="dimmed"> <Box p="md">
{date} {/* PROGRESS */}
</Text> <Progress
<Box value={progress}
style={{ radius="xl"
backgroundColor: getStatusColor(status), size="lg"
color: "white", color="orange"
padding: "2px 8px", styles={{
borderRadius: 4, root: {
fontSize: 11, height: 16,
fontWeight: 600, },
}} }}
> />
{status.toUpperCase()}
</Box>
</Group>
<Progress {/* FOOTER */}
value={progress} <Group justify="space-between" mt="md">
size="sm" <Text size="sm" fw={500}>
radius="xl" {date}
color={progress === 100 ? "green" : "yellow"} </Text>
animated={progress < 100}
/>
<Text size="xs" c="dimmed" mt="xs" ta="right"> <Box
{progress}% style={{
</Text> backgroundColor: getStatusColor(),
color: "white",
padding: "4px 12px",
borderRadius: 999,
fontSize: 12,
fontWeight: 600,
}}
>
{status}
</Box>
</Group>
</Box>
</Card> </Card>
); );
} }

View File

@@ -28,6 +28,7 @@ export function ArchiveCard({ item, onClick }: ArchiveCardProps) {
cursor: "pointer", cursor: "pointer",
transition: "transform 0.2s, box-shadow 0.2s", transition: "transform 0.2s, box-shadow 0.2s",
}} }}
h="100%"
onClick={onClick} onClick={onClick}
> >
<Group gap="md"> <Group gap="md">

View File

@@ -48,6 +48,7 @@ export function DiscussionPanel() {
? "0 1px 3px 0 rgb(0 0 0 / 0.1)" ? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
: "0 1px 3px 0 rgb(0 0 0 / 0.1)", : "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}} }}
h="100%"
> >
<Group gap="xs" mb="md"> <Group gap="xs" mb="md">
<MessageCircle size={20} color={dark ? "#E2E8F0" : "#1E3A5F"} /> <MessageCircle size={20} color={dark ? "#E2E8F0" : "#1E3A5F"} />

View File

@@ -3,6 +3,7 @@ import {
Bar, Bar,
BarChart, BarChart,
CartesianGrid, CartesianGrid,
Cell,
ResponsiveContainer, ResponsiveContainer,
Tooltip, Tooltip,
XAxis, XAxis,
@@ -10,8 +11,8 @@ import {
} from "recharts"; } from "recharts";
const documentData = [ const documentData = [
{ name: "Gambar", value: 300 }, { name: "Gambar", jumlah: 300, color: "#FACC15" },
{ name: "Dokumen", value: 310 }, { name: "Dokumen", jumlah: 310, color: "#22C55E" },
]; ];
export function DocumentChart() { export function DocumentChart() {
@@ -61,7 +62,11 @@ export function DocumentChart() {
}} }}
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }} labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
/> />
<Bar dataKey="value" fill="#3B82F6" radius={[4, 4, 0, 0]} /> <Bar dataKey="jumlah" radius={[4, 4, 0, 0]}>
{documentData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Bar>
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</Card> </Card>

View File

@@ -33,6 +33,7 @@ export function EventCard({ agendas = [] }: EventCardProps) {
? "0 1px 3px 0 rgb(0 0 0 / 0.1)" ? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
: "0 1px 3px 0 rgb(0 0 0 / 0.1)", : "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}} }}
h="100%"
> >
<Group gap="xs" mb="md"> <Group gap="xs" mb="md">
<Calendar size={20} color={dark ? "#E2E8F0" : "#1E3A5F"} /> <Calendar size={20} color={dark ? "#E2E8F0" : "#1E3A5F"} />

View File

@@ -10,7 +10,7 @@ import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
const progressData = [ const progressData = [
{ name: "Selesai", value: 83.33, color: "#22C55E" }, { name: "Selesai", value: 83.33, color: "#22C55E" },
{ name: "Dikerjakan", value: 16.67, color: "#FACC15" }, { name: "Dikerjakan", value: 16.67, color: "#F59E0B" },
{ name: "Segera Dikerjakan", value: 0, color: "#3B82F6" }, { name: "Segera Dikerjakan", value: 0, color: "#3B82F6" },
{ name: "Dibatalkan", value: 0, color: "#EF4444" }, { name: "Dibatalkan", value: 0, color: "#EF4444" },
]; ];

File diff suppressed because it is too large Load Diff

View File

@@ -1,189 +1,78 @@
import { import { Box, Button, Group, Stack, Switch, Text, Title } from "@mantine/core";
ActionIcon,
Alert,
Button,
Card,
Group,
Modal,
Select,
Space,
Table,
Text,
TextInput,
Title,
useMantineColorScheme,
} from "@mantine/core";
import {
IconEdit,
IconInfoCircle,
IconTrash,
IconUser,
IconUserPlus,
} from "@tabler/icons-react";
import { useState } from "react";
const AksesDanTimSettings = () => { const AksesDanTimSettings = () => {
const [opened, setOpened] = useState(false);
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
// Sample team members data
const teamMembers = [
{
id: 1,
name: "Admin Utama",
email: "admin@desa.go.id",
role: "Administrator",
status: "Aktif",
},
{
id: 2,
name: "Operator Desa",
email: "operator@desa.go.id",
role: "Operator",
status: "Aktif",
},
{
id: 3,
name: "Staff Keuangan",
email: "keuangan@desa.go.id",
role: "Keuangan",
status: "Aktif",
},
{
id: 4,
name: "Staff Umum",
email: "umum@desa.go.id",
role: "Umum",
status: "Nonaktif",
},
];
const roles = [
{ value: "administrator", label: "Administrator" },
{ value: "operator", label: "Operator" },
{ value: "keuangan", label: "Keuangan" },
{ value: "umum", label: "Umum" },
{ value: "keamanan", label: "Keamanan" },
];
return ( return (
<Card <Stack pr={"50%"} gap={"xl"}>
withBorder <Box>
radius="md" <Stack gap={"xs"}>
p="xl" <Title order={2}>Manajemen Tim</Title>
bg={dark ? "#141D34" : "white"} <Button bg={"#1E3A5F"} radius={"md"} c={"white"} fullWidth>
style={{ borderColor: dark ? "#141D34" : "white" }} Undangan Anggota Baru
>
<Modal
opened={opened}
onClose={() => setOpened(false)}
title="Tambah Anggota Tim"
size="lg"
>
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap anggota tim"
mb="md"
/>
<TextInput
label="Alamat Email"
placeholder="Masukkan alamat email"
mb="md"
/>
<Select
label="Peran"
placeholder="Pilih peran anggota tim"
data={roles}
mb="md"
/>
<Group justify="flex-end" mt="xl">
<Button variant="outline" onClick={() => setOpened(false)}>
Batal
</Button> </Button>
<Button>Undang Anggota</Button> <Button bg={"#1E3A5F"} radius={"md"} c={"white"} fullWidth>
</Group> Kelola Role & Permission
</Modal> </Button>
<Group justify="space-between">
<Title order={2} mb="lg"> <Text fw={"bold"} fz={"sm"}>
Akses & Tim Daftar Anggota Teraktif
</Title> </Text>
<Text color="dimmed" mb="xl"> <Text fw={"bold"} fz={"sm"}>
Kelola akses dan anggota tim Anda 12 Anggota
</Text> </Text>
</Group>
<Space h="lg" /> </Stack>
</Box>
<Group justify="space-between" mb="md"> <Box>
<Title order={4}>Anggota Tim</Title> <Stack gap={"xs"}>
<Button <Title order={2}>Hak Akses</Title>
leftSection={<IconUserPlus size={16} />} <Group justify="space-between">
onClick={() => setOpened(true)} <Text fw={"bold"} fz={"sm"}>
> Administrator
Tambah Anggota </Text>
</Button> <Text fw={"bold"} fz={"sm"}>
</Group> 2 Orang
</Text>
<Table highlightOnHover> </Group>
<Table.Thead> <Group justify="space-between">
<Table.Tr> <Text fw={"bold"} fz={"sm"}>
<Table.Th>Nama</Table.Th> Editor
<Table.Th>Email</Table.Th> </Text>
<Table.Th>Peran</Table.Th> <Text fw={"bold"} fz={"sm"}>
<Table.Th>Status</Table.Th> 5 Orang
<Table.Th>Aksi</Table.Th> </Text>
</Table.Tr> </Group>
</Table.Thead> <Group justify="space-between">
<Table.Tbody> <Text fw={"bold"} fz={"sm"}>
{teamMembers.map((member) => ( Viewer
<Table.Tr key={member.id}> </Text>
<Table.Td> <Text fw={"bold"} fz={"sm"}>
<Group gap="sm"> 5 Orang
<IconUser size={20} /> </Text>
<Text>{member.name}</Text> </Group>
</Group> </Stack>
</Table.Td> </Box>
<Table.Td>{member.email}</Table.Td> <Box>
<Table.Td> <Stack gap={"xs"}>
<Text fw={500}>{member.role}</Text> <Title order={2}>Kolaborasi</Title>
</Table.Td> <Group mb="md" justify="space-between">
<Table.Td> <Text fw={"bold"} fz={"sm"}>
<Text c={member.status === "Aktif" ? "green" : "red"} fw={500}> Izin Export Data
{member.status} </Text>
</Text> <Switch defaultChecked />
</Table.Td> </Group>
<Table.Td> <Group mb="md" justify="space-between">
<Group> <Text fw={"bold"} fz={"sm"}>
<ActionIcon variant="subtle" color="blue"> Require Approval Untuk Perubahan
<IconEdit size={16} /> </Text>
</ActionIcon> <Switch defaultChecked />
<ActionIcon variant="subtle" color="red"> </Group>
<IconTrash size={16} /> </Stack>
</ActionIcon> </Box>
</Group> <Group justify="flex-start" mt="xl">
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
<Space h="xl" />
<Alert
icon={<IconInfoCircle size={16} />}
title="Informasi"
color="blue"
mb="md"
>
Administrator memiliki akses penuh ke semua fitur. Peran lainnya
memiliki akses terbatas sesuai kebutuhan.
</Alert>
<Group justify="flex-end" mt="xl">
<Button variant="outline">Batal</Button> <Button variant="outline">Batal</Button>
<Button>Simpan Perubahan</Button> <Button>Simpan Perubahan</Button>
</Group> </Group>
</Card> </Stack>
); );
}; };

View File

@@ -1,89 +1,64 @@
import { import { Box, Button, Group, Stack, Switch, Text, Title } from "@mantine/core";
Alert,
Button,
Card,
Group,
PasswordInput,
Space,
Switch,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import { IconInfoCircle, IconLock } from "@tabler/icons-react";
const KeamananSettings = () => { const KeamananSettings = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return ( return (
<Card <Stack pr={"50%"} gap={"xl"}>
withBorder <Box>
radius="md" <Stack gap={"xs"}>
p="xl" <Title order={2}>Autentikasi</Title>
bg={dark ? "#141D34" : "white"} <Group mb="md" justify="space-between">
style={{ borderColor: dark ? "#141D34" : "white" }} <Text fw={"bold"} fz={"sm"}>
> Two-Factor Authentication
<Title order={2} mb="lg"> </Text>
Pengaturan Keamanan <Switch defaultChecked />
</Title> </Group>
<Text color="dimmed" mb="xl"> <Group mb="md" justify="space-between">
Kelola keamanan akun Anda <Text fw={"bold"} fz={"sm"}>
</Text> Biometrik Login
</Text>
<Space h="lg" /> <Switch defaultChecked />
</Group>
<PasswordInput <Group mb="md" justify="space-between">
label="Kata Sandi Saat Ini" <Text fw={"bold"} fz={"sm"}>
placeholder="Masukkan kata sandi saat ini" IP Whitelist
mb="md" </Text>
/> <Switch defaultChecked />
</Group>
<PasswordInput </Stack>
label="Kata Sandi Baru" </Box>
placeholder="Masukkan kata sandi baru" <Box>
mb="md" <Stack gap={"xs"}>
/> <Title order={2}>Password</Title>
<Button bg={"#1E3A5F"} radius={"md"} c={"white"} fullWidth>
<PasswordInput Ubah Password
label="Konfirmasi Kata Sandi Baru" </Button>
placeholder="Konfirmasi kata sandi baru" <Button bg={"#1E3A5F"} radius={"md"} c={"white"} fullWidth>
mb="md" Riwayat Login
/> </Button>
<Button bg={"#1E3A5F"} radius={"md"} c={"white"} fullWidth>
<Space h="md" /> Perangkat Terdaftar
</Button>
<Group mb="md"> </Stack>
<Switch label="Verifikasi Dua Langkah" /> </Box>
<Switch label="Login Otentikasi Aplikasi" /> <Box>
</Group> <Stack gap={"xs"}>
<Title order={2}>Audit & Log</Title>
<Space h="md" /> <Group mb="md" justify="space-between">
<Text fw={"bold"} fz={"sm"}>
<Alert Log Aktivitas
icon={<IconLock size={16} />} </Text>
title="Keamanan" <Switch defaultChecked />
color="orange" </Group>
mb="md" <Button bg={"#1E3A5F"} radius={"md"} c={"white"} fullWidth>
> Download Log
Gunakan kata sandi yang kuat dan unik. Hindari menggunakan kata sandi </Button>
yang sama di banyak layanan. </Stack>
</Alert> </Box>
<Group justify="flex-start" mt="xl">
<Alert
icon={<IconInfoCircle size={16} />}
title="Informasi"
color="blue"
mb="md"
>
Setelah mengganti kata sandi, Anda akan diminta logout dari semua
perangkat.
</Alert>
<Group justify="flex-end" mt="xl">
<Button variant="outline">Batal</Button> <Button variant="outline">Batal</Button>
<Button>Perbarui Kata Sandi</Button> <Button>Simpan Perubahan</Button>
</Group> </Group>
</Card> </Stack>
); );
}; };

View File

@@ -1,10 +1,14 @@
import { import {
Alert, Alert,
Box,
Button, Button,
Card, Card,
Checkbox, Checkbox,
Grid,
GridCol,
Group, Group,
Space, Space,
Stack,
Switch, Switch,
Text, Text,
Title, Title,
@@ -16,70 +20,101 @@ const NotifikasiSettings = () => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; const dark = colorScheme === "dark";
return ( return (
<Card <Stack pr={"20%"} gap={"xs"}>
withBorder <Grid gutter={{ base: 5, xs: "md", md: "xl", xl: 50 }}>
radius="md" <GridCol span={6}>
p="xl" <Stack gap={"xs"}>
bg={dark ? "#141D34" : "white"} <Title order={3} mb="sm">
style={{ borderColor: dark ? "#141D34" : "white" }} Metode Notifikasi
> </Title>
<Title order={2} mb="lg"> <Group mb="md" justify="space-between">
Pengaturan Notifikasi <Text fw={"bold"} fz={"sm"}>
</Title> Laporan Harian
<Text color="dimmed" mb="xl"> </Text>
Kelola preferensi notifikasi Anda <Switch defaultChecked />
</Text> </Group>
<Group mb="md" justify="space-between">
<Space h="lg" /> <Text fw={"bold"} fz={"sm"}>
Alert Sistem
<Checkbox.Group defaultValue={["email", "push"]} mb="md"> </Text>
<Title order={4} mb="sm"> <Switch defaultChecked />
Metode Notifikasi </Group>
</Title> <Group mb="md" justify="space-between">
<Group> <Text fw={"bold"} fz={"sm"}>
<Checkbox value="email" label="Email" /> Update Keamanan
<Checkbox value="push" label="Notifikasi Push" /> </Text>
<Checkbox value="sms" label="SMS" /> <Switch defaultChecked />
</Group> </Group>
</Checkbox.Group> <Group mb="md" justify="space-between">
<Text fw={"bold"} fz={"sm"}>
<Space h="md" /> Newsletter Bulanan
</Text>
<Group mb="md"> <Switch defaultChecked />
<Switch label="Notifikasi Email" defaultChecked /> </Group>
<Switch label="Notifikasi Push" defaultChecked /> </Stack>
</Group> </GridCol>
<GridCol span={6}>
<Space h="md" /> <Stack gap={"xs"}>
<Title order={3} mb="sm">
<Title order={4} mb="sm"> Preferensi Alert
Jenis Notifikasi </Title>
</Title> <Group mb="md" justify="space-between">
<Group align="start"> <Text fw={"bold"} fz={"sm"}>
<Switch label="Pengaduan Baru" defaultChecked /> Treshold Memori
<Switch label="Update Status Pengaduan" defaultChecked /> </Text>
<Switch label="Laporan Mingguan" /> <Switch defaultChecked />
<Switch label="Pemberitahuan Keamanan" defaultChecked /> </Group>
<Switch label="Aktivitas Akun" defaultChecked /> <Group mb="md" justify="space-between">
</Group> <Text fw={"bold"} fz={"sm"}>
Treshold CPU
<Space h="md" /> </Text>
<Switch defaultChecked />
<Alert </Group>
icon={<IconInfoCircle size={16} />} <Group mb="md" justify="space-between">
title="Tip" <Text fw={"bold"} fz={"sm"}>
color="blue" Treshold Disk
mb="md" </Text>
> <Switch defaultChecked />
Anda dapat menyesuaikan frekuensi notifikasi mingguan sesuai kebutuhan </Group>
Anda. </Stack>
</Alert> </GridCol>
<GridCol span={6}>
<Group justify="flex-end" mt="xl"> <Stack gap={"xs"}>
<Title order={3} mb="sm">
Notifikasi Push
</Title>
<Group mb="md" justify="space-between">
<Text fw={"bold"} fz={"sm"}>
Alert Kritis
</Text>
<Switch defaultChecked />
</Group>
<Group mb="md" justify="space-between">
<Text fw={"bold"} fz={"sm"}>
Aktivitas Tim
</Text>
<Switch defaultChecked />
</Group>
<Group mb="md" justify="space-between">
<Text fw={"bold"} fz={"sm"}>
Komentar & Mention
</Text>
<Switch defaultChecked />
</Group>
<Group mb="md" justify="space-between">
<Text fw={"bold"} fz={"sm"}>
Bunyi Notifikasi
</Text>
<Switch defaultChecked />
</Group>
</Stack>
</GridCol>
</Grid>
<Group justify="flex-start" mt="xl">
<Button variant="outline">Batal</Button> <Button variant="outline">Batal</Button>
<Button>Simpan Preferensi</Button> <Button>Simpan Preferensi</Button>
</Group> </Group>
</Card> </Stack>
); );
}; };

View File

@@ -1,44 +1,12 @@
import { import { Box, Button, Group, Select, Switch, Text, Title } from "@mantine/core";
Alert, import { DateInput } from "@mantine/dates";
Button,
Card,
Group,
Select,
Space,
Switch,
Text,
TextInput,
Title,
useMantineColorScheme,
} from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react";
const UmumSettings = () => { const UmumSettings = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return ( return (
<Card <Box pr={"50%"}>
withBorder
radius="md"
p="xl"
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={2} mb="lg"> <Title order={2} mb="lg">
Pengaturan Umum Preferensi Tampilan
</Title> </Title>
<Text color="dimmed" mb="xl">
Kelola pengaturan umum aplikasi Anda
</Text>
<Space h="lg" />
<TextInput
label="Nama Aplikasi"
placeholder="Masukkan nama aplikasi"
defaultValue="Dashboard Desa Plus"
mb="md"
/>
<Select <Select
label="Bahasa Aplikasi" label="Bahasa Aplikasi"
@@ -61,25 +29,53 @@ const UmumSettings = () => {
mb="md" mb="md"
/> />
<Group mb="md"> <DateInput label="Format Tanggal" mb={"xl"} />
<Switch label="Notifikasi Email" defaultChecked />
<Title order={2} mb="lg">
Dashboard
</Title>
<Group mb="md" justify="space-between">
<Text fw={"bold"} fz={"sm"}>
Refresh Otomatis
</Text>
<Switch defaultChecked />
</Group> </Group>
<Alert <Group mb="md" justify="space-between">
icon={<IconInfoCircle size={16} />} <Text fw={"bold"} fz={"sm"}>
title="Informasi" Interval Refresh
color="blue" </Text>
mb="md" <Select
> data={[
Beberapa pengaturan mungkin memerlukan restart aplikasi untuk diterapkan { value: "1", label: "30d" },
sepenuhnya. { value: "2", label: "60d" },
</Alert> { value: "3", label: "90d" },
]}
defaultValue="1"
w={90}
/>
</Group>
<Group mb="md" justify="space-between">
<Text fw={"bold"} fz={"sm"}>
Tampilkan Grid
</Text>
<Switch defaultChecked />
</Group>
<Group mb="md" justify="space-between">
<Text fw={"bold"} fz={"sm"}>
Animasi Transisi
</Text>
<Switch defaultChecked />
</Group>
<Group justify="flex-end" mt="xl"> <Group justify="flex-end" mt="xl">
<Button variant="outline">Batal</Button> <Button variant="outline">Batal</Button>
<Button>Simpan Perubahan</Button> <Button>Simpan Perubahan</Button>
</Group> </Group>
</Card> </Box>
); );
}; };

View File

@@ -3,6 +3,7 @@ import {
Box, Box,
Collapse, Collapse,
Group, Group,
Image,
Input, Input,
NavLink as MantineNavLink, NavLink as MantineNavLink,
Stack, Stack,
@@ -60,30 +61,7 @@ export function Sidebar({ className }: SidebarProps) {
return ( return (
<Box className={className}> <Box className={className}>
{/* Logo */} {/* Logo */}
<Box <Image src={dark ? "/white.png" : "/light-mode.png"} alt="Logo" />
p="md"
style={{ borderBottom: "1px solid var(--mantine-color-gray-3)" }}
>
<Group gap="xs">
<Badge
color="dark"
variant="filled"
size="xl"
radius="md"
py="xs"
px="md"
style={{ fontSize: "1.5rem", fontWeight: "bold" }}
>
DESA
</Badge>
<Badge color="green" variant="filled" size="md" radius="md">
+
</Badge>
</Group>
<Text size="xs" c="dimmed" mt="xs">
Digitalisasi Desa Transparansi Kerja
</Text>
</Box>
{/* Search */} {/* Search */}
<Box p="md"> <Box p="md">
@@ -204,4 +182,4 @@ export function Sidebar({ className }: SidebarProps) {
</Stack> </Stack>
</Box> </Box>
); );
} }

View File

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

View File

@@ -0,0 +1,76 @@
import {
Card,
Group,
Stack,
Text,
ThemeIcon,
Title,
useMantineColorScheme,
} from "@mantine/core";
import { IconAward } from "@tabler/icons-react";
interface ScholarshipData {
penerima: number;
dana: string;
tahunAjaran: string;
}
interface BeasiswaProps {
data?: ScholarshipData;
}
export const Beasiswa = ({ data }: BeasiswaProps) => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
const defaultData: ScholarshipData = {
penerima: 45,
dana: "Rp 1.200.000.000",
tahunAjaran: "2025/2026",
};
const displayData = data || defaultData;
return (
<Card
p="md"
radius="xl"
withBorder
shadow="sm"
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
h={"100%"}
>
<Group justify="space-between" align="center">
<Stack gap={2}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"} fw={500}>
Beasiswa Desa
</Text>
<Text size="xl" fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
Penerima: {displayData.penerima}
</Text>
</Stack>
<ThemeIcon
variant="light"
color="darmasaba-success"
size="xl"
radius="xl"
>
<IconAward size={24} />
</ThemeIcon>
</Group>
<Stack gap="xs" mt="md">
<Group justify="space-between">
<Text c={dark ? "dark.3" : "dimmed"}>Dana Tersalurkan:</Text>
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
{displayData.dana}
</Text>
</Group>
<Group justify="space-between">
<Text c={dark ? "dark.3" : "dimmed"}>Tahun Ajaran:</Text>
<Text c={dark ? "dark.0" : "#1e3a5f"}>{displayData.tahunAjaran}</Text>
</Group>
</Stack>
</Card>
);
};

View File

@@ -0,0 +1,101 @@
import {
Card,
Group,
Stack,
Text,
ThemeIcon,
Title,
useMantineColorScheme,
} from "@mantine/core";
import { IconCalendarEvent } from "@tabler/icons-react";
interface EventItem {
id: string;
nama: string;
tanggal: string;
lokasi: string;
}
interface EventCalendarProps {
data?: EventItem[];
}
export const EventCalendar = ({ data }: EventCalendarProps) => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
const defaultData: EventItem[] = [
{
id: "1",
nama: "Hari Kesaktian Pancasila",
tanggal: "1 Oktober 2025",
lokasi: "Balai Desa",
},
{
id: "2",
nama: "Festival Budaya Desa",
tanggal: "20 Mei 2026",
lokasi: "Lapangan Desa",
},
{
id: "3",
nama: "Perayaan HUT Desa",
tanggal: "17 Agustus 2026",
lokasi: "Balai Desa",
},
];
const displayData = data || defaultData;
return (
<Card
p="md"
radius="xl"
withBorder
shadow="sm"
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
>
<Title order={3} mb="md" c={dark ? "dark.0" : "#1e3a5f"}>
Kalender Event Budaya
</Title>
<Stack gap="sm">
{displayData.map((event) => (
<Card
key={event.id}
p="md"
radius="md"
withBorder
bg={dark ? "#263852ff" : "#F1F5F9"}
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
hoverable
>
<Group justify="space-between" mb="xs">
<Group gap="sm" align="center">
<ThemeIcon
color="darmasaba-blue"
size="md"
radius="xl"
variant="light"
>
<IconCalendarEvent size={16} />
</ThemeIcon>
<Text fw={600} c={dark ? "dark.0" : "#1e3a5f"}>
{event.nama}
</Text>
</Group>
<Text size="sm" c={dark ? "dark.3" : "dimmed"} fw={500}>
{event.lokasi}
</Text>
</Group>
<Group pl={36}>
<Text size="sm" c={dark ? "dark.4" : "gray.6"}>
{event.tanggal}
</Text>
</Group>
</Card>
))}
</Stack>
</Card>
);
};

View File

@@ -0,0 +1,73 @@
import {
Card,
Group,
Progress,
Stack,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
interface HealthProgressItem {
label: string;
value: number;
color: string;
}
interface HealthStatsProps {
data?: HealthProgressItem[];
}
export const HealthStats = ({ data }: HealthStatsProps) => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
const defaultData: HealthProgressItem[] = [
{ label: "Imunisasi Lengkap", value: 92, color: "green" },
{ label: "Pemeriksaan Rutin", value: 88, color: "blue" },
{ label: "Gizi Baik", value: 86, color: "teal" },
{ label: "Target Stunting", value: 14, color: "red" },
];
const displayData = data || defaultData;
return (
<Card
p="md"
radius="xl"
withBorder
shadow="sm"
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
h={"100%"}
>
<Title order={3} mb="md" c={dark ? "dark.0" : "#1e3a5f"}>
Statistik Kesehatan
</Title>
<Stack gap="md">
{displayData.map((item, index) => (
<div key={index}>
<Group justify="space-between" mb={5}>
<Text size="sm" fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
{item.label}
</Text>
<Text
size="sm"
fw={600}
c={item.color === "red" ? "red" : dark ? "dark.0" : "#1e3a5f"}
>
{item.value}%
</Text>
</Group>
<Progress
value={item.value}
size="lg"
radius="xl"
color={item.color}
/>
</div>
))}
</Stack>
</Card>
);
};

View File

@@ -0,0 +1,120 @@
import {
Card,
Group,
Stack,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
interface EducationData {
siswa: {
tk: number;
sd: number;
smp: number;
sma: number;
};
sekolah: {
jumlah: number;
guru: number;
};
}
interface PendidikanProps {
data?: EducationData;
}
export const Pendidikan = ({ data }: PendidikanProps) => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
const defaultData: EducationData = {
siswa: {
tk: 125,
sd: 480,
smp: 210,
sma: 150,
},
sekolah: {
jumlah: 8,
guru: 42,
},
};
const displayData = data || defaultData;
return (
<Card
p="md"
radius="xl"
withBorder
shadow="sm"
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
>
<Title order={3} mb="md" c={dark ? "dark.0" : "#1e3a5f"}>
Pendidikan
</Title>
<Stack gap="md">
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
TK / PAUD
</Text>
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
{displayData.siswa.tk}
</Text>
</Group>
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
SD
</Text>
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
{displayData.siswa.sd}
</Text>
</Group>
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
SMP
</Text>
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
{displayData.siswa.smp}
</Text>
</Group>
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
SMA
</Text>
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
{displayData.siswa.sma}
</Text>
</Group>
<Card
withBorder
radius="md"
p="md"
mt="md"
bg={dark ? "#263852ff" : "#F1F5F9"}
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
>
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
Jumlah Lembaga Pendidikan
</Text>
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
{displayData.sekolah.jumlah}
</Text>
</Group>
<Group justify="space-between" mt="sm">
<Text fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
Jumlah Tenaga Pengajar
</Text>
<Text fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
{displayData.sekolah.guru}
</Text>
</Group>
</Card>
</Stack>
</Card>
);
};

View File

@@ -0,0 +1,96 @@
import {
Badge,
Card,
Group,
Stack,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
interface PosyanduItem {
id: string;
nama: string;
tanggal: string;
jam: string;
}
interface PosyanduScheduleProps {
data?: PosyanduItem[];
}
export const PosyanduSchedule = ({ data }: PosyanduScheduleProps) => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
const defaultData: PosyanduItem[] = [
{
id: "1",
nama: "Posyandu Mawar",
tanggal: "Senin, 15 Feb 2026",
jam: "08:00 - 11:00",
},
{
id: "2",
nama: "Posyandu Melati",
tanggal: "Selasa, 16 Feb 2026",
jam: "08:00 - 11:00",
},
{
id: "3",
nama: "Posyandu Dahlia",
tanggal: "Rabu, 17 Feb 2026",
jam: "08:00 - 11:00",
},
{
id: "4",
nama: "Posyandu Anggrek",
tanggal: "Kamis, 18 Feb 2026",
jam: "08:00 - 11:00",
},
];
const displayData = data || defaultData;
return (
<Card
p="md"
radius="xl"
withBorder
shadow="sm"
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
>
<Title order={3} mb="md" c={dark ? "dark.0" : "#1e3a5f"}>
Jadwal Posyandu
</Title>
<Stack gap="sm">
{displayData.map((item) => (
<Card
key={item.id}
p="md"
radius="md"
withBorder
bg={dark ? "#263852ff" : "#F1F5F9"}
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
hoverable
>
<Group justify="space-between">
<Stack gap={0}>
<Text fw={600} c={dark ? "dark.0" : "#1e3a5f"}>
{item.nama}
</Text>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{item.tanggal}
</Text>
</Stack>
<Badge variant="light" color="darmasaba-blue" size="md">
{item.jam}
</Badge>
</Group>
</Card>
))}
</Stack>
</Card>
);
};

View File

@@ -0,0 +1,140 @@
import {
Card,
Grid,
GridCol,
Group,
Stack,
Text,
ThemeIcon,
useMantineColorScheme,
} from "@mantine/core";
import {
IconBabyCarriage,
IconHeartbeat,
IconMedicalCross,
IconStethoscope,
} from "@tabler/icons-react";
interface SummaryCardProps {
title: string;
value: number;
subtitle?: string;
icon: React.ReactNode;
color: string;
highlight?: boolean;
backgroundColor: string;
}
const SummaryCard = ({
title,
value,
subtitle,
icon,
color,
highlight = false,
backgroundColor,
}: SummaryCardProps) => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return (
<Card
p="md"
radius="xl"
withBorder
shadow="sm"
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
>
<Group justify="space-between" align="center">
<Stack gap={2}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"} fw={500}>
{title}
</Text>
<Text
size="xl"
fw={700}
c={highlight ? "red" : dark ? "dark.0" : "#1e3a5f"}
>
{value}
</Text>
{subtitle && (
<Text size="xs" c={dark ? "dark.4" : "gray.6"}>
{subtitle}
</Text>
)}
</Stack>
<ThemeIcon bg={backgroundColor} color={color} size="xl" radius="xl">
{icon}
</ThemeIcon>
</Group>
</Card>
);
};
interface HealthSummaryData {
ibuHamil: number;
balita: number;
alertStunting: number;
posyanduAktif: number;
}
interface SummaryCardsProps {
data?: HealthSummaryData;
}
export const SummaryCards = ({ data }: SummaryCardsProps) => {
const defaultData: HealthSummaryData = {
ibuHamil: 87,
balita: 342,
alertStunting: 12,
posyanduAktif: 8,
};
const displayData = data || defaultData;
return (
<Grid gutter="md">
<GridCol span={{ base: 12, sm: 6, lg: 3 }}>
<SummaryCard
title="Ibu Hamil Aktif"
value={displayData.ibuHamil}
subtitle="Aktif"
icon={<IconHeartbeat size={20} />}
color="white"
backgroundColor="#1E3A5F"
/>
</GridCol>
<GridCol span={{ base: 12, sm: 6, lg: 3 }}>
<SummaryCard
title="Balita Terdaftar"
value={displayData.balita}
subtitle="Terdaftar"
icon={<IconBabyCarriage size={20} />}
color="white"
backgroundColor="#1E3A5F"
/>
</GridCol>
<GridCol span={{ base: 12, sm: 6, lg: 3 }}>
<SummaryCard
title="Alert Stunting"
value={displayData.alertStunting}
subtitle="Perhatian"
icon={<IconStethoscope size={20} />}
color="white"
backgroundColor="#1E3A5F"
/>
</GridCol>
<GridCol span={{ base: 12, sm: 6, lg: 3 }}>
<SummaryCard
title="Posyandu Aktif"
value={displayData.posyanduAktif}
subtitle="Aktif"
icon={<IconMedicalCross size={20} />}
color="white"
backgroundColor="#1E3A5F"
/>
</GridCol>
</Grid>
);
};

View File

@@ -51,9 +51,7 @@ export const HelpCard = ({
{icon && ( {icon && (
<div <div
style={{ style={{
backgroundColor: isDark backgroundColor: isDark ? "#263852ff" : "#1E3A5F",
? theme.colors.blue[8]
: theme.colors.blue[0],
borderRadius: "8px", borderRadius: "8px",
padding: "8px", padding: "8px",
display: "flex", display: "flex",

View File

@@ -0,0 +1,75 @@
import {
Button,
Card,
Group,
Title,
useMantineColorScheme,
} from "@mantine/core";
import { useSnapshot } from "valtio";
import { setRange, umkmStore } from "../../store/umkm";
type TimeRange = "minggu" | "bulan";
interface HeaderToggleProps {
title?: string;
onRangeChange?: (range: TimeRange) => void;
}
export const HeaderToggle = ({
title = "Update Penjualan Produk",
onRangeChange,
}: HeaderToggleProps) => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
const { selectedRange } = useSnapshot(umkmStore);
const handleRangeChange = (range: TimeRange) => {
setRange(range);
onRangeChange?.(range);
};
return (
<Card
p="md"
radius="xl"
withBorder
shadow="sm"
bg={dark ? "#1e3a5f" : "#1e3a5f"}
style={{ borderColor: dark ? "#1e3a5f" : "#1e3a5f" }}
>
<Group justify="space-between" align="center" px="md" py="xs">
<Title order={3} c="white">
{title}
</Title>
<Group gap="xs">
<Button
variant={selectedRange === "minggu" ? "white" : "transparent"}
onClick={() => handleRangeChange("minggu")}
c={selectedRange === "minggu" ? "#1e3a5f" : "white"}
fw={600}
radius="xl"
size="sm"
style={{
opacity: selectedRange === "minggu" ? 1 : 0.8,
}}
>
Minggu ini
</Button>
<Button
variant={selectedRange === "bulan" ? "white" : "transparent"}
onClick={() => handleRangeChange("bulan")}
c={selectedRange === "bulan" ? "#1e3a5f" : "white"}
fw={600}
radius="xl"
size="sm"
style={{
opacity: selectedRange === "bulan" ? 1 : 0.8,
}}
>
Bulan ini
</Button>
</Group>
</Group>
</Card>
);
};

View File

@@ -0,0 +1,100 @@
import { Card, Group, Stack, Text, useMantineColorScheme } from "@mantine/core";
interface MetricCardProps {
title: string;
value: string | number;
trend?: {
value: number;
label: string;
};
}
const MetricCard = ({ title, value, trend }: MetricCardProps) => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return (
<Group justify="space-between" align="center">
<Text size="sm" c={dark ? "dark.3" : "dimmed"} fw={500}>
{title}
</Text>
<Stack gap={0} align="flex-end">
<Text size="lg" fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
{value}
</Text>
{trend && (
<Text size="xs" c={trend.value >= 0 ? "green" : "red"} fw={600}>
{trend.value >= 0 ? "↑" : "↓"} {Math.abs(trend.value)}%{" "}
{trend.label}
</Text>
)}
</Stack>
</Group>
);
};
interface ProdukUnggulanProps {
data?: {
totalPenjualan: number;
produkAktif: number;
totalTransaksi: number;
trend?: {
value: number;
label: string;
};
};
}
export const ProdukUnggulan = ({ data }: ProdukUnggulanProps) => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
const defaultData = {
totalPenjualan: 30900000,
produkAktif: 7,
totalTransaksi: 500,
trend: {
value: 18,
label: "vs bulan lalu",
},
};
const displayData = data || defaultData;
const formatCurrency = (value: number) => {
if (value >= 1000000) {
return `Rp ${(value / 1000000).toFixed(1)}M`;
}
if (value >= 1000) {
return `Rp ${(value / 1000).toFixed(0)}K`;
}
return `Rp ${value.toLocaleString()}`;
};
return (
<Card
p="md"
radius="xl"
withBorder
shadow="sm"
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
>
<Stack gap="lg">
<MetricCard
title="Total Penjualan"
value={formatCurrency(displayData.totalPenjualan)}
trend={displayData.trend}
/>
<MetricCard
title="Produk Aktif"
value={`${displayData.produkAktif} kategori`}
/>
<MetricCard
title="Total Transaksi"
value={`${displayData.totalTransaksi} transaksi`}
/>
</Stack>
</Card>
);
};

View File

@@ -0,0 +1,260 @@
import {
Badge,
Button,
Card,
Group,
Select,
Table,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import { IconArrowDown, IconArrowUp } from "@tabler/icons-react";
export interface SalesData {
id: string;
produk: string;
penjualanBulanIni: number;
bulanLalu: number;
trend: number;
volume: string;
stok: number;
unit: string;
}
interface SalesTableProps {
data?: SalesData[];
onDetailClick?: (product: SalesData) => void;
}
export const SalesTable = ({ data, onDetailClick }: SalesTableProps) => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
const defaultData: SalesData[] = [
{
id: "1",
produk: "Beras Premium Organik",
penjualanBulanIni: 8500000,
bulanLalu: 7600000,
trend: 12,
volume: "650 Kg",
stok: 850,
unit: "Kg",
},
{
id: "2",
produk: "Keripik Singkong",
penjualanBulanIni: 4200000,
bulanLalu: 3800000,
trend: 11,
volume: "320 Kg",
stok: 120,
unit: "Kg",
},
{
id: "3",
produk: "Madu Alami",
penjualanBulanIni: 3750000,
bulanLalu: 4100000,
trend: -9,
volume: "150 Liter",
stok: 45,
unit: "Liter",
},
{
id: "4",
produk: "Kecap Tradisional",
penjualanBulanIni: 2800000,
bulanLalu: 2500000,
trend: 12,
volume: "280 Botol",
stok: 95,
unit: "Botol",
},
{
id: "5",
produk: "Sambal Bu Rudy",
penjualanBulanIni: 2100000,
bulanLalu: 2300000,
trend: -9,
volume: "180 Botol",
stok: 35,
unit: "Botol",
},
];
const displayData = data || defaultData;
const formatCurrency = (value: number) => {
if (value >= 1000000) {
return `Rp ${(value / 1000000).toFixed(1)}M`;
}
if (value >= 1000) {
return `Rp ${(value / 1000).toFixed(0)}K`;
}
return `Rp ${value.toLocaleString()}`;
};
const getStockStatus = (stock: number) => {
if (stock > 200) return { color: "green", label: "Aman" };
if (stock > 50) return { color: "yellow", label: "Sedang" };
return { color: "red", label: "Rendah" };
};
return (
<Card
p="md"
radius="xl"
withBorder
shadow="sm"
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
>
<Group justify="space-between" mb="md">
<Title order={4} c={dark ? "dark.0" : "#1e3a5f"}>
Detail Penjualan Produk
</Title>
<Group gap="xs">
<Select
placeholder="Filter kategori"
data={[
{ value: "semua", label: "Semua Kategori" },
{ value: "makanan", label: "Makanan" },
{ value: "minuman", label: "Minuman" },
{ value: "kerajinan", label: "Kerajinan" },
]}
defaultValue="semua"
w={180}
size="sm"
/>
<Select
placeholder="Filter UMKM"
data={[
{ value: "semua", label: "Semua UMKM" },
{ value: "umkm1", label: "Warung Pak Joko" },
{ value: "umkm2", label: "Ibu Sari Snack" },
]}
defaultValue="semua"
w={180}
size="sm"
/>
</Group>
</Group>
<Table
stickyHeader
stickyHeaderOffset={60}
highlightOnHover
withRowBorders={false}
verticalSpacing="sm"
>
<Table.Thead>
<Table.Tr>
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
Produk
</Text>
</Table.Th>
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
Penjualan Bulan Ini
</Text>
</Table.Th>
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
Bulan Lalu
</Text>
</Table.Th>
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
Trend
</Text>
</Table.Th>
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
Volume
</Text>
</Table.Th>
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
Stok
</Text>
</Table.Th>
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
Aksi
</Text>
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{displayData.map((product) => {
const stockStatus = getStockStatus(product.stok);
return (
<Table.Tr
key={product.id}
style={{
backgroundColor: dark ? "#141D34" : "white",
}}
>
<Table.Td>
<Text fw={600} c={dark ? "dark.0" : "#1e3a5f"}>
{product.produk}
</Text>
</Table.Td>
<Table.Td>
<Text size="sm" fw={600} c={dark ? "dark.0" : "#1e3a5f"}>
{formatCurrency(product.penjualanBulanIni)}
</Text>
</Table.Td>
<Table.Td>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{formatCurrency(product.bulanLalu)}
</Text>
</Table.Td>
<Table.Td>
<Group gap="xs">
{product.trend >= 0 ? (
<IconArrowUp size={16} color="green" />
) : (
<IconArrowDown size={16} color="red" />
)}
<Text
size="sm"
fw={600}
c={product.trend >= 0 ? "green" : "red"}
>
{Math.abs(product.trend)}%
</Text>
</Group>
</Table.Td>
<Table.Td>
<Text size="sm" c={dark ? "dark.0" : "#1e3a5f"}>
{product.volume}
</Text>
</Table.Td>
<Table.Td>
<Badge variant="light" color={stockStatus.color} size="sm">
{product.stok} {product.unit} ({stockStatus.label})
</Badge>
</Table.Td>
<Table.Td>
<Button
variant="subtle"
size="compact-sm"
color="darmasaba-blue"
radius="xl"
onClick={() => onDetailClick?.(product)}
>
Detail
</Button>
</Table.Td>
</Table.Tr>
);
})}
</Table.Tbody>
</Table>
</Card>
);
};

View File

@@ -0,0 +1,155 @@
import {
Avatar,
Card,
Grid,
GridCol,
Group,
Stack,
Text,
useMantineColorScheme,
} from "@mantine/core";
import {
IconCategory,
IconCurrencyDollar,
IconTrendingUp,
IconUsers,
} from "@tabler/icons-react";
interface KpiCardProps {
title: string;
value: string | number;
subtitle?: string;
icon: React.ReactNode;
color: string;
backgroundColor: string;
}
const KpiCard = ({
title,
value,
subtitle,
icon,
color,
backgroundColor,
}: KpiCardProps) => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
const formatValue = (val: string | number) => {
if (typeof val === "number") {
if (val >= 1000000) {
return `${(val / 1000000).toFixed(1)}M`;
}
if (val >= 1000) {
return `${(val / 1000).toFixed(1)}K`;
}
return val.toLocaleString();
}
return val;
};
return (
<Card
p="md"
radius="xl"
withBorder
shadow="sm"
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
>
<Group justify="space-between" align="center">
<Stack gap={2}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"} fw={500}>
{title}
</Text>
<Text size="xl" fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
{formatValue(value)}
</Text>
{subtitle && (
<Text size="xs" c={dark ? "dark.4" : "gray.6"}>
{subtitle}
</Text>
)}
</Stack>
<Avatar
color={color}
bg={backgroundColor}
size={40}
radius="xl"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{icon}
</Avatar>
</Group>
</Card>
);
};
interface SummaryCardsProps {
data?: {
umkmAktif: number;
umkmTerdaftar: number;
omzet: number;
kategoriTerbanyak: { count: number; name: string };
};
}
export const SummaryCards = ({ data }: SummaryCardsProps) => {
const defaultData = {
umkmAktif: 45,
umkmTerdaftar: 68,
omzet: 48000000,
kategoriTerbanyak: { count: 34, name: "Kuliner" },
};
const displayData = data || defaultData;
const kpiData: KpiCardProps[] = [
{
title: "UMKM Aktif",
value: displayData.umkmAktif,
subtitle: "Beroperasi",
icon: <IconCurrencyDollar size={25} />,
color: "white",
backgroundColor: "#1E3A5F",
},
{
title: "UMKM Terdaftar",
value: displayData.umkmTerdaftar,
subtitle: "Total registrasi",
icon: <IconUsers size={25} />,
color: "white",
backgroundColor: "#1E3A5F",
},
{
title: "Omzet",
value: displayData.omzet,
subtitle: "Omzet BUMDes per bulan",
icon: <IconTrendingUp size={25} />,
color: "white",
backgroundColor: "#1E3A5F",
},
{
title: "UMKM Terbanyak",
value: displayData.kategoriTerbanyak.count,
subtitle: `Kategori ${displayData.kategoriTerbanyak.name}`,
icon: <IconTrendingUp size={25} />,
color: "white",
backgroundColor: "#1E3A5F",
},
];
return (
<Grid gutter="md">
{kpiData.map((kpi, index) => (
<GridCol key={index} span={{ base: 12, sm: 6, lg: 3 }}>
<KpiCard {...kpi} />
</GridCol>
))}
</Grid>
);
};

View File

@@ -0,0 +1,140 @@
import {
Badge,
Card,
Group,
Stack,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
interface TopProduct {
rank: number;
name: string;
umkmName: string;
revenue: number;
quantitySold: number;
trend: number;
}
interface TopProductsProps {
products?: TopProduct[];
}
const formatCurrency = (value: number) => {
if (value >= 1000000) {
return `${(value / 1000000).toFixed(1)}M`;
}
if (value >= 1000) {
return `${(value / 1000).toFixed(0)}K`;
}
return value.toString();
};
const formatNumber = (value: number) => {
if (value >= 1000) {
return `${(value / 1000).toFixed(1)}K`;
}
return value.toString();
};
export const TopProducts = ({ products }: TopProductsProps) => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
const defaultProducts: TopProduct[] = [
{
rank: 1,
name: "Beras Premium Organik",
umkmName: "Warung Pak Joko",
revenue: 8500000,
quantitySold: 650,
trend: 12,
},
{
rank: 2,
name: "Keripik Singkong",
umkmName: "Ibu Sari Snack",
revenue: 4200000,
quantitySold: 320,
trend: 8,
},
{
rank: 3,
name: "Madu Alami",
umkmName: "Peternakan Lebah",
revenue: 3750000,
quantitySold: 150,
trend: 5,
},
];
const displayProducts = products || defaultProducts;
const getRankColor = (rank: number) => {
if (rank === 1) return "yellow";
if (rank === 2) return "gray";
if (rank === 3) return "orange";
return "blue";
};
return (
<Card
p="md"
radius="xl"
withBorder
shadow="sm"
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
>
<Title order={4} mb="md" c={dark ? "dark.0" : "#1e3a5f"}>
Top 3 Produk Terlaris
</Title>
<Stack gap="sm">
{displayProducts.map((product) => (
<Group key={product.rank} justify="space-between" align="center">
<Group gap="sm">
<Badge
variant="filled"
color={getRankColor(product.rank)}
radius="xl"
size="lg"
w={30}
h={30}
>
{product.rank}
</Badge>
<Stack gap={0}>
<Text fw={600} c={dark ? "dark.0" : "#1e3a5f"}>
{product.name}
</Text>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{product.umkmName}
</Text>
<Group gap="xs" mt={2}>
<Text size="xs" c={dark ? "dark.4" : "gray.6"}>
Rp {formatCurrency(product.revenue)}
</Text>
<Text size="xs" c={dark ? "dark.4" : "gray.6"}>
</Text>
<Text size="xs" c={dark ? "dark.4" : "gray.6"}>
{formatNumber(product.quantitySold)} terjual
</Text>
</Group>
</Stack>
</Group>
<Badge
variant="light"
color={product.trend >= 0 ? "green" : "red"}
size="sm"
>
{product.trend >= 0 ? "+" : ""}
{product.trend}%
</Badge>
</Group>
))}
</Stack>
</Card>
);
};

View File

@@ -0,0 +1,35 @@
import { useDisclosure } from "@mantine/hooks";
import { useState } from "react";
export function useSidebarFullscreen() {
const [opened, { toggle: toggleMobile }] = useDisclosure();
const [sidebarCollapsed, setSidebarCollapsed] = useDisclosure(false);
const [clickCount, setClickCount] = useState(0);
const toggleSidebar = () => {
setSidebarCollapsed.toggle();
setClickCount(0);
};
const handleMainClick = () => {
if (!sidebarCollapsed) {
const newCount = clickCount + 1;
setClickCount(newCount);
if (newCount === 2) {
toggleSidebar();
} else {
setTimeout(() => setClickCount(0), 300);
}
}
};
return {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
isCollapsed: sidebarCollapsed,
};
}

View File

@@ -1 +1 @@
@import "tailwindcss"; @import "tailwindcss";

View File

@@ -12,240 +12,240 @@ const isProduction = process.env.NODE_ENV === "production";
// Auto-seed database in production (ensure admin user exists) // Auto-seed database in production (ensure admin user exists)
if (isProduction && process.env.ADMIN_EMAIL) { if (isProduction && process.env.ADMIN_EMAIL) {
try { try {
console.log("🌱 Running database seed in production..."); console.log("🌱 Running database seed in production...");
const { runSeed } = await import("../prisma/seed.ts"); const { runSeed } = await import("../prisma/seed.ts");
await runSeed(); await runSeed();
} catch (error) { } catch (error) {
console.error("⚠️ Production seed failed:", error); console.error("⚠️ Production seed failed:", error);
// Don't crash the server if seed fails // Don't crash the server if seed fails
} }
} }
const app = new Elysia().use(api); const app = new Elysia().use(api);
if (!isProduction) { if (!isProduction) {
// Development: Use Vite middleware // Development: Use Vite middleware
const { createVite } = await import("./vite"); const { createVite } = await import("./vite");
const vite = await createVite(); const vite = await createVite();
// Serve PWA/TWA assets in dev (root and nested path support) // Serve PWA/TWA assets in dev (root and nested path support)
const _servePwaAsset = (srcPath: string) => () => Bun.file(srcPath); const _servePwaAsset = (srcPath: string) => () => Bun.file(srcPath);
app.post("/__open-in-editor", ({ body }) => { app.post("/__open-in-editor", ({ body }) => {
const { relativePath, lineNumber, columnNumber } = body as { const { relativePath, lineNumber, columnNumber } = body as {
relativePath: string; relativePath: string;
lineNumber: number; lineNumber: number;
columnNumber: number; columnNumber: number;
}; };
openInEditor(relativePath, { openInEditor(relativePath, {
line: lineNumber, line: lineNumber,
column: columnNumber, column: columnNumber,
editor: "antigravity", editor: "antigravity",
}); });
return { ok: true }; return { ok: true };
}); });
// Vite middleware for other requests // Vite middleware for other requests
app.all("*", async ({ request }) => { app.all("*", async ({ request }) => {
const url = new URL(request.url); const url = new URL(request.url);
const pathname = url.pathname; const pathname = url.pathname;
// Serve transformed index.html for root or any path that should be handled by the SPA // Serve transformed index.html for root or any path that should be handled by the SPA
if ( if (
pathname === "/" || pathname === "/" ||
(!pathname.includes(".") && (!pathname.includes(".") &&
!pathname.startsWith("/@") && !pathname.startsWith("/@") &&
!pathname.startsWith("/inspector") && !pathname.startsWith("/inspector") &&
!pathname.startsWith("/__open-stack-frame-in-editor") && !pathname.startsWith("/__open-stack-frame-in-editor") &&
!pathname.startsWith("/api")) !pathname.startsWith("/api"))
) { ) {
try { try {
const htmlPath = path.resolve("src/index.html"); const htmlPath = path.resolve("src/index.html");
let html = fs.readFileSync(htmlPath, "utf-8"); let html = fs.readFileSync(htmlPath, "utf-8");
html = await vite.transformIndexHtml(pathname, html); html = await vite.transformIndexHtml(pathname, html);
return new Response(html, { return new Response(html, {
headers: { "Content-Type": "text/html" }, headers: { "Content-Type": "text/html" },
}); });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
} }
return new Promise<Response>((resolve) => { return new Promise<Response>((resolve) => {
// Use a Proxy to mock Node.js req because Bun's Request is read-only // Use a Proxy to mock Node.js req because Bun's Request is read-only
const req = new Proxy(request, { const req = new Proxy(request, {
get(target, prop) { get(target, prop) {
if (prop === "url") return pathname + url.search; if (prop === "url") return pathname + url.search;
if (prop === "method") return request.method; if (prop === "method") return request.method;
if (prop === "headers") if (prop === "headers")
return Object.fromEntries(request.headers as any); return Object.fromEntries(request.headers as any);
return (target as any)[prop]; return (target as any)[prop];
}, },
}) as any; }) as any;
const res = { const res = {
statusCode: 200, statusCode: 200,
setHeader(name: string, value: string) { setHeader(name: string, value: string) {
this.headers[name.toLowerCase()] = value; this.headers[name.toLowerCase()] = value;
}, },
getHeader(name: string) { getHeader(name: string) {
return this.headers[name.toLowerCase()]; return this.headers[name.toLowerCase()];
}, },
writeHead(code: number, headers: Record<string, string>) { writeHead(code: number, headers: Record<string, string>) {
this.statusCode = code; this.statusCode = code;
Object.assign(this.headers, headers); Object.assign(this.headers, headers);
}, },
write(chunk: any, callback?: () => void) { write(chunk: any, callback?: () => void) {
// Collect chunks for streaming responses // Collect chunks for streaming responses
if (!this._chunks) this._chunks = []; if (!this._chunks) this._chunks = [];
this._chunks.push(chunk); this._chunks.push(chunk);
if (callback) callback(); if (callback) callback();
return true; // Indicate we can accept more data return true; // Indicate we can accept more data
}, },
headers: {} as Record<string, string>, headers: {} as Record<string, string>,
end(data: any) { end(data: any) {
// Handle potential Buffer or string data from Vite // Handle potential Buffer or string data from Vite
let body = data; let body = data;
// If we have collected chunks from write() calls, combine them // If we have collected chunks from write() calls, combine them
if (this._chunks && this._chunks.length > 0) { if (this._chunks && this._chunks.length > 0) {
body = Buffer.concat(this._chunks); body = Buffer.concat(this._chunks);
} }
if (data instanceof Uint8Array) { if (data instanceof Uint8Array) {
body = data; body = data;
} else if (typeof data === "string") { } else if (typeof data === "string") {
body = data; body = data;
} else if (data) { } else if (data) {
body = String(data); body = String(data);
} }
resolve( resolve(
new Response(body || "", { new Response(body || "", {
status: this.statusCode, status: this.statusCode,
headers: this.headers, headers: this.headers,
}), }),
); );
}, },
// Minimal event emitter mock // Minimal event emitter mock
once() { once() {
return this; return this;
}, },
on() { on() {
return this; return this;
}, },
emit() { emit() {
return this; return this;
}, },
removeListener() { removeListener() {
return this; return this;
}, },
} as any; } as any;
vite.middlewares(req, res, (err: any) => { vite.middlewares(req, res, (err: any) => {
if (err) { if (err) {
console.error("Vite middleware error:", err); console.error("Vite middleware error:", err);
resolve(new Response(err.stack || err.toString(), { status: 500 })); resolve(new Response(err.stack || err.toString(), { status: 500 }));
return; return;
} }
// If Vite doesn't handle it, return 404 // If Vite doesn't handle it, return 404
resolve(new Response("Not Found", { status: 404 })); resolve(new Response("Not Found", { status: 404 }));
}); });
}); });
}); });
} else { } else {
// Production: Final catch-all for static files and SPA fallback // Production: Final catch-all for static files and SPA fallback
app.all("*", async ({ request }) => { app.all("*", async ({ request }) => {
const url = new URL(request.url); const url = new URL(request.url);
const pathname = url.pathname; const pathname = url.pathname;
// 1. Try exact match in dist // 1. Try exact match in dist
let filePath = path.join( let filePath = path.join(
"dist", "dist",
pathname === "/" ? "index.html" : pathname, pathname === "/" ? "index.html" : pathname,
); );
// 1.1 Special handling for PWA/TWA assets that might not be in dist (since we use custom bun build) // 1.1 Special handling for PWA/TWA assets that might not be in dist (since we use custom bun build)
if (isProduction) { if (isProduction) {
const srcPath = path.join("src", pathname); const srcPath = path.join("src", pathname);
if (fs.existsSync(srcPath)) { if (fs.existsSync(srcPath)) {
filePath = srcPath; filePath = srcPath;
} }
// Check public folder for static assets // Check public folder for static assets
const publicPath = path.join("public", pathname); const publicPath = path.join("public", pathname);
if (fs.existsSync(publicPath)) { if (fs.existsSync(publicPath)) {
filePath = publicPath; filePath = publicPath;
} }
} }
// 2. If not found and looks like an asset (has extension), try root of dist or src // 2. If not found and looks like an asset (has extension), try root of dist or src
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
if (pathname.includes(".") && !pathname.endsWith("/")) { if (pathname.includes(".") && !pathname.endsWith("/")) {
const filename = path.basename(pathname); const filename = path.basename(pathname);
// Try root of dist // Try root of dist
const fallbackDistPath = path.join("dist", filename); const fallbackDistPath = path.join("dist", filename);
if ( if (
fs.existsSync(fallbackDistPath) && fs.existsSync(fallbackDistPath) &&
fs.statSync(fallbackDistPath).isFile() fs.statSync(fallbackDistPath).isFile()
) { ) {
filePath = fallbackDistPath; filePath = fallbackDistPath;
} }
// Try public folder // Try public folder
else { else {
const fallbackPublicPath = path.join("public", filename); const fallbackPublicPath = path.join("public", filename);
if ( if (
fs.existsSync(fallbackPublicPath) && fs.existsSync(fallbackPublicPath) &&
fs.statSync(fallbackPublicPath).isFile() fs.statSync(fallbackPublicPath).isFile()
) { ) {
filePath = fallbackPublicPath; filePath = fallbackPublicPath;
} }
} }
// Special handling for PWA files in src // Special handling for PWA files in src
if (pathname.includes("assetlinks.json")) { if (pathname.includes("assetlinks.json")) {
const srcFilename = pathname.includes("assetlinks.json") const srcFilename = pathname.includes("assetlinks.json")
? ".well-known/assetlinks.json" ? ".well-known/assetlinks.json"
: filename; : filename;
const fallbackSrcPath = path.join("src", srcFilename); const fallbackSrcPath = path.join("src", srcFilename);
if ( if (
fs.existsSync(fallbackSrcPath) && fs.existsSync(fallbackSrcPath) &&
fs.statSync(fallbackSrcPath).isFile() fs.statSync(fallbackSrcPath).isFile()
) { ) {
filePath = fallbackSrcPath; filePath = fallbackSrcPath;
} }
} }
} }
} }
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
const file = Bun.file(filePath); const file = Bun.file(filePath);
return new Response(file, { return new Response(file, {
headers: { headers: {
Vary: "Accept-Encoding", Vary: "Accept-Encoding",
}, },
}); });
} }
// 3. SPA Fallback: Serve index.html // 3. SPA Fallback: Serve index.html
const indexHtml = path.join("dist", "index.html"); const indexHtml = path.join("dist", "index.html");
if (fs.existsSync(indexHtml)) { if (fs.existsSync(indexHtml)) {
return new Response(Bun.file(indexHtml), { return new Response(Bun.file(indexHtml), {
headers: { headers: {
Vary: "Accept-Encoding", Vary: "Accept-Encoding",
}, },
}); });
} }
return new Response("Not Found", { status: 404 }); return new Response("Not Found", { status: 404 });
}); });
} }
app.listen(PORT); app.listen(PORT);
console.log( console.log(
`🚀 Server running at http://localhost:${PORT} in ${isProduction ? "production" : "development"} mode`, `🚀 Server running at http://localhost:${PORT} in ${isProduction ? "production" : "development"} mode`,
); );
export type ApiApp = typeof app; export type ApiApp = typeof app;

View File

@@ -152,4 +152,4 @@ export function createProtectedRoute(options: ProtectedRouteOptions = {}) {
* Default Middleware Export * Default Middleware Export
* ================================ */ * ================================ */
export const protectedRouteMiddleware = createProtectedRoute(); export const protectedRouteMiddleware = createProtectedRoute();

View File

@@ -28,4 +28,4 @@ export const Route = createRootRoute({
function RootComponent() { function RootComponent() {
return <Outlet />; return <Outlet />;
} }

View File

@@ -1,16 +1,22 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core"; import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header"; import { Header } from "@/components/header";
import HelpPage from "@/components/help-page"; import HelpPage from "@/components/help-page";
import { Sidebar } from "@/components/sidebar"; import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
export const Route = createFileRoute("/bantuan")({ export const Route = createFileRoute("/bantuan")({
component: BantuanPage, component: BantuanRoute,
}); });
function BantuanPage() { function BantuanRoute() {
const [opened, { toggle }] = useDisclosure(); const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E"; const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white"; const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
@@ -22,14 +28,19 @@ function BantuanPage() {
navbar={{ navbar={{
width: 300, width: 300,
breakpoint: "sm", breakpoint: "sm",
collapsed: { mobile: !opened }, collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}} }}
padding="md" padding="md"
> >
<AppShell.Header bg={headerBgColor}> <AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md"> <Group h="100%" px="md">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" /> <Burger
<Header /> opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group> </Group>
</AppShell.Header> </AppShell.Header>
@@ -43,7 +54,11 @@ function BantuanPage() {
</div> </div>
</AppShell.Navbar> </AppShell.Navbar>
<AppShell.Main bg={mainBgColor}> <AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<HelpPage /> <HelpPage />
</AppShell.Main> </AppShell.Main>
</AppShell> </AppShell>

View File

@@ -1,9 +1,66 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import BumdesPage from "@/components/bumdes-page";
import { Header } from "@/components/header";
import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
export const Route = createFileRoute("/bumdes")({ export const Route = createFileRoute("/bumdes")({
component: RouteComponent, component: BumdesRoute,
}); });
function RouteComponent() { function BumdesRoute() {
return <div>Hello "/bumdes"!</div>; const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger
opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<BumdesPage />
</AppShell.Main>
</AppShell>
);
} }

View File

@@ -1,8 +1,8 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core"; import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header"; import { Header } from "@/components/header";
import { Sidebar } from "@/components/sidebar"; import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
import DemografiPekerjaan from "../components/demografi-pekerjaan"; import DemografiPekerjaan from "../components/demografi-pekerjaan";
export const Route = createFileRoute("/demografi-pekerjaan")({ export const Route = createFileRoute("/demografi-pekerjaan")({
@@ -10,7 +10,13 @@ export const Route = createFileRoute("/demografi-pekerjaan")({
}); });
function DemografiPekerjaanPage() { function DemografiPekerjaanPage() {
const [opened, { toggle }] = useDisclosure(); const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E"; const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white"; const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
@@ -22,14 +28,19 @@ function DemografiPekerjaanPage() {
navbar={{ navbar={{
width: 300, width: 300,
breakpoint: "sm", breakpoint: "sm",
collapsed: { mobile: !opened }, collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}} }}
padding="md" padding="md"
> >
<AppShell.Header bg={headerBgColor}> <AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md"> <Group h="100%" px="md">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" /> <Burger
<Header /> opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group> </Group>
</AppShell.Header> </AppShell.Header>
@@ -43,7 +54,11 @@ function DemografiPekerjaanPage() {
</div> </div>
</AppShell.Navbar> </AppShell.Navbar>
<AppShell.Main bg={mainBgColor}> <AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<DemografiPekerjaan /> <DemografiPekerjaan />
</AppShell.Main> </AppShell.Main>
</AppShell> </AppShell>

View File

@@ -1,6 +1,7 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core"; import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { DashboardContent } from "@/components/dashboard-content"; import { DashboardContent } from "@/components/dashboard-content";
import { Header } from "@/components/header"; import { Header } from "@/components/header";
import { Sidebar } from "@/components/sidebar"; import { Sidebar } from "@/components/sidebar";
@@ -11,25 +12,41 @@ export const Route = createFileRoute("/")({
function DashboardPage() { function DashboardPage() {
const [opened, { toggle }] = useDisclosure(); const [opened, { toggle }] = useDisclosure();
const [sidebarCollapsed, setSidebarCollapsed] = useDisclosure(false);
const [clickCount, setClickCount] = useState(0);
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E"; const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white"; const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff"; const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
const handleMainClick = () => {
if (!sidebarCollapsed) {
const newCount = clickCount + 1;
setClickCount(newCount);
if (newCount === 2) {
setSidebarCollapsed.toggle();
setClickCount(0);
} else {
setTimeout(() => setClickCount(0), 300);
}
}
};
return ( return (
<AppShell <AppShell
header={{ height: 60 }} header={{ height: 60 }}
navbar={{ navbar={{
width: 300, width: 300,
breakpoint: "sm", breakpoint: "sm",
collapsed: { mobile: !opened }, collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}} }}
padding="md" padding="md"
> >
<AppShell.Header bg={headerBgColor}> <AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md"> <Group h="100%" px="md">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" /> <Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
<Header /> <Header onSidebarToggle={setSidebarCollapsed.toggle} />
</Group> </Group>
</AppShell.Header> </AppShell.Header>
@@ -43,9 +60,13 @@ function DashboardPage() {
</div> </div>
</AppShell.Navbar> </AppShell.Navbar>
<AppShell.Main bg={mainBgColor}> <AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<DashboardContent /> <DashboardContent />
</AppShell.Main> </AppShell.Main>
</AppShell> </AppShell>
); );
} }

View File

@@ -1,9 +1,66 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header";
import JennaAnalytic from "@/components/jenna-analytic";
import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
export const Route = createFileRoute("/jenna-analytic")({ export const Route = createFileRoute("/jenna-analytic")({
component: RouteComponent, component: JennaAnalyticPage,
}); });
function RouteComponent() { function JennaAnalyticPage() {
return <div>Hello "/jenna-analytic"!</div>; const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger
opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<JennaAnalytic />
</AppShell.Main>
</AppShell>
);
} }

View File

@@ -1,9 +1,66 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header";
import KeamananPage from "@/components/keamanan-page";
import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
export const Route = createFileRoute("/keamanan")({ export const Route = createFileRoute("/keamanan")({
component: RouteComponent, component: KeamananRoute,
}); });
function RouteComponent() { function KeamananRoute() {
return <div>Hello "/keamanan"!</div>; const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger
opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<KeamananPage />
</AppShell.Main>
</AppShell>
);
} }

View File

@@ -1,16 +1,22 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core"; import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header"; import { Header } from "@/components/header";
import KeuanganAnggaran from "@/components/keuangan-anggaran"; import KeuanganAnggaran from "@/components/keuangan-anggaran";
import { Sidebar } from "@/components/sidebar"; import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
export const Route = createFileRoute("/keuangan-anggaran")({ export const Route = createFileRoute("/keuangan-anggaran")({
component: KeuanganAnggaranPage, component: KeuanganAnggaranPage,
}); });
function KeuanganAnggaranPage() { function KeuanganAnggaranPage() {
const [opened, { toggle }] = useDisclosure(); const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E"; const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white"; const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
@@ -22,14 +28,19 @@ function KeuanganAnggaranPage() {
navbar={{ navbar={{
width: 300, width: 300,
breakpoint: "sm", breakpoint: "sm",
collapsed: { mobile: !opened }, collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}} }}
padding="md" padding="md"
> >
<AppShell.Header bg={headerBgColor}> <AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md"> <Group h="100%" px="md">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" /> <Burger
<Header /> opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group> </Group>
</AppShell.Header> </AppShell.Header>
@@ -43,7 +54,11 @@ function KeuanganAnggaranPage() {
</div> </div>
</AppShell.Navbar> </AppShell.Navbar>
<AppShell.Main bg={mainBgColor}> <AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<KeuanganAnggaran /> <KeuanganAnggaran />
</AppShell.Main> </AppShell.Main>
</AppShell> </AppShell>

View File

@@ -1,16 +1,22 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core"; import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header"; import { Header } from "@/components/header";
import KinerjaDivisi from "@/components/kinerja-divisi"; import KinerjaDivisi from "@/components/kinerja-divisi";
import { Sidebar } from "@/components/sidebar"; import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
export const Route = createFileRoute("/kinerja-divisi")({ export const Route = createFileRoute("/kinerja-divisi")({
component: KinerjaDivisiPage, component: KinerjaDivisiPage,
}); });
function KinerjaDivisiPage() { function KinerjaDivisiPage() {
const [opened, { toggle }] = useDisclosure(); const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E"; const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white"; const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
@@ -22,14 +28,19 @@ function KinerjaDivisiPage() {
navbar={{ navbar={{
width: 300, width: 300,
breakpoint: "sm", breakpoint: "sm",
collapsed: { mobile: !opened }, collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}} }}
padding="md" padding="md"
> >
<AppShell.Header bg={headerBgColor}> <AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md"> <Group h="100%" px="md">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" /> <Burger
<Header /> opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group> </Group>
</AppShell.Header> </AppShell.Header>
@@ -43,7 +54,11 @@ function KinerjaDivisiPage() {
</div> </div>
</AppShell.Navbar> </AppShell.Navbar>
<AppShell.Main bg={mainBgColor}> <AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<KinerjaDivisi /> <KinerjaDivisi />
</AppShell.Main> </AppShell.Main>
</AppShell> </AppShell>

View File

@@ -1,16 +1,22 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core"; import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header"; import { Header } from "@/components/header";
import PengaduanLayananPublik from "@/components/pengaduan-layanan-publik"; import PengaduanLayananPublik from "@/components/pengaduan-layanan-publik";
import { Sidebar } from "@/components/sidebar"; import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
export const Route = createFileRoute("/pengaduan-layanan-publik")({ export const Route = createFileRoute("/pengaduan-layanan-publik")({
component: PengaduanLayananPublikPage, component: PengaduanLayananPublikPage,
}); });
function PengaduanLayananPublikPage() { function PengaduanLayananPublikPage() {
const [opened, { toggle }] = useDisclosure(); const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E"; const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white"; const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
@@ -22,14 +28,19 @@ function PengaduanLayananPublikPage() {
navbar={{ navbar={{
width: 300, width: 300,
breakpoint: "sm", breakpoint: "sm",
collapsed: { mobile: !opened }, collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}} }}
padding="md" padding="md"
> >
<AppShell.Header bg={headerBgColor}> <AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md"> <Group h="100%" px="md">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" /> <Burger
<Header /> opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group> </Group>
</AppShell.Header> </AppShell.Header>
@@ -43,7 +54,11 @@ function PengaduanLayananPublikPage() {
</div> </div>
</AppShell.Navbar> </AppShell.Navbar>
<AppShell.Main bg={mainBgColor}> <AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<PengaduanLayananPublik /> <PengaduanLayananPublik />
</AppShell.Main> </AppShell.Main>
</AppShell> </AppShell>

View File

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

View File

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

View File

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

View File

@@ -1,21 +1,27 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { useMediaQuery } from "@mantine/hooks";
import { import {
AppShell, createFileRoute,
Burger, Outlet,
Group, useRouterState,
useMantineColorScheme, } from "@tanstack/react-router";
} from "@mantine/core";
import { useDisclosure, useMediaQuery } from "@mantine/hooks";
import { createFileRoute, Outlet, useRouterState } from "@tanstack/react-router";
import { useEffect } from "react"; import { useEffect } from "react";
import { Header } from "@/components/header"; import { Header } from "@/components/header";
import { Sidebar } from "@/components/sidebar"; import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
export const Route = createFileRoute("/pengaturan")({ export const Route = createFileRoute("/pengaturan")({
component: PengaturanLayout, component: PengaturanLayout,
}); });
function PengaturanLayout() { function PengaturanLayout() {
const [opened, { toggle, close }] = useDisclosure(); const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const isMobile = useMediaQuery("(max-width: 48em)"); const isMobile = useMediaQuery("(max-width: 48em)");
@@ -28,9 +34,9 @@ function PengaturanLayout() {
// Auto close navbar on route change (mobile only) // Auto close navbar on route change (mobile only)
useEffect(() => { useEffect(() => {
if (isMobile && opened) { if (isMobile && opened) {
close(); toggleMobile();
} }
}, [routerState.location.pathname, isMobile, opened, close]); }, [routerState.location.pathname, isMobile, opened, toggleMobile]);
return ( return (
<AppShell <AppShell
@@ -38,7 +44,7 @@ function PengaturanLayout() {
navbar={{ navbar={{
width: 300, width: 300,
breakpoint: "sm", breakpoint: "sm",
collapsed: { mobile: !opened }, collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}} }}
padding="md" padding="md"
> >
@@ -46,11 +52,11 @@ function PengaturanLayout() {
<Group h="100%" px="lg" align="center" wrap="nowrap"> <Group h="100%" px="lg" align="center" wrap="nowrap">
<Burger <Burger
opened={opened} opened={opened}
onClick={toggle} onClick={toggleMobile}
hiddenFrom="sm" hiddenFrom="sm"
size="sm" size="sm"
/> />
<Header /> <Header onSidebarToggle={toggleSidebar} />
</Group> </Group>
</AppShell.Header> </AppShell.Header>
@@ -64,7 +70,11 @@ function PengaturanLayout() {
</div> </div>
</AppShell.Navbar> </AppShell.Navbar>
<AppShell.Main bg={mainBgColor}> <AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<div className="p-2"> <div className="p-2">
<Outlet /> <Outlet />
</div> </div>

View File

@@ -1,9 +1,66 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header";
import { Sidebar } from "@/components/sidebar";
import SosialPage from "@/components/sosial-page";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
export const Route = createFileRoute("/sosial")({ export const Route = createFileRoute("/sosial")({
component: RouteComponent, component: SosialRoute,
}); });
function RouteComponent() { function SosialRoute() {
return <div>Hello "/sosial"!</div>; const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger
opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<SosialPage />
</AppShell.Main>
</AppShell>
);
} }

30
src/store/sosial.ts Normal file
View File

@@ -0,0 +1,30 @@
import { proxy } from "valtio";
type SelectedYear = string;
interface SosialState {
selectedYear: SelectedYear;
filters: {
dusun: string | null;
kategori: string | null;
};
}
export const sosialStore = proxy<SosialState>({
selectedYear: new Date().getFullYear().toString(),
filters: {
dusun: null,
kategori: null,
},
});
export const setYear = (year: SelectedYear) => {
sosialStore.selectedYear = year;
};
export const setFilter = (
key: keyof SosialState["filters"],
value: string | null,
) => {
sosialStore.filters[key] = value;
};

30
src/store/umkm.ts Normal file
View File

@@ -0,0 +1,30 @@
import { proxy } from "valtio";
type TimeRange = "minggu" | "bulan";
interface UmkmState {
selectedRange: TimeRange;
filters: {
kategori: string | null;
umkm: string | null;
};
}
export const umkmStore = proxy<UmkmState>({
selectedRange: "bulan",
filters: {
kategori: null,
umkm: null,
},
});
export const setRange = (range: TimeRange) => {
umkmStore.selectedRange = range;
};
export const setFilter = (
key: keyof UmkmState["filters"],
value: string | null,
) => {
umkmStore.filters[key] = value;
};

View File

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