Compare commits

...

2 Commits

Author SHA1 Message Date
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
27 changed files with 1081 additions and 483 deletions

View File

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

View File

@@ -13,13 +13,13 @@ import {
useMantineColorScheme,
} from "@mantine/core";
import {
Users,
Home,
Baby,
TrendingDown,
BarChart3,
PieChart as PieChartIcon,
Building2,
Home,
PieChart as PieChartIcon,
TrendingDown,
Users,
} from "lucide-react";
import {
Bar,

View File

@@ -9,11 +9,18 @@ import {
Title,
useMantineColorScheme,
} from "@mantine/core";
import { IconUserShield } from "@tabler/icons-react";
import {
IconLayoutSidebarLeftCollapse,
IconUserShield,
} from "@tabler/icons-react";
import { useLocation, useNavigate } from "@tanstack/react-router";
import { Bell, Moon, Sun, User as UserIcon } from "lucide-react"; // Renamed User to UserIcon to avoid conflict with Mantine's User component if it exists
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 { colorScheme, toggleColorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
@@ -56,9 +63,24 @@ export function Header() {
return (
<Group justify="space-between" w="100%">
{/* Title */}
<Title order={3} c={"white"}>
{getPageTitle()}
</Title>
<Group gap="md">
<ActionIcon
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 */}
<Group gap="md">

View File

@@ -13,10 +13,10 @@ import {
useMantineColorScheme,
} from "@mantine/core";
import {
MessageCircle,
CheckCircle,
AlertTriangle,
CheckCircle,
Clock,
MessageCircle,
TrendingUp,
} from "lucide-react";
import {

View File

@@ -12,12 +12,12 @@ import {
useMantineColorScheme,
} from "@mantine/core";
import {
Coins,
CheckCircle,
TrendingUp,
TrendingDown,
Coins,
PieChart as PieChartIcon,
Receipt,
TrendingDown,
TrendingUp,
} from "lucide-react";
import {
Bar,
@@ -144,18 +144,10 @@ const KeuanganAnggaran = () => {
{item.value}
</Text>
<Group gap={4} align="flex-start">
{item.trend && (
<TrendingUp size={14} color="#22C55E" />
)}
{item.trend && <TrendingUp size={14} color="#22C55E" />}
<Text
size="xs"
c={
item.trend
? "green"
: dark
? "gray.4"
: "gray.5"
}
c={item.trend ? "green" : dark ? "gray.4" : "gray.5"}
>
{item.subtitle}
</Text>
@@ -313,7 +305,10 @@ const KeuanganAnggaran = () => {
borderColor: dark ? "#334155" : "#e5e7eb",
borderRadius: "8px",
}}
formatter={(value: number | undefined) => [`Rp ${value}jt`, "Jumlah"]}
formatter={(value: number | undefined) => [
`Rp ${value}jt`,
"Jumlah",
]}
/>
<Bar
dataKey="amount"
@@ -354,11 +349,7 @@ const KeuanganAnggaran = () => {
<Grid gutter="md">
{/* Pendapatan */}
<Grid.Col span={6}>
<Card
p="sm"
radius="lg"
bg={dark ? "#064E3B" : "#DCFCE7"}
>
<Card p="sm" radius="lg" bg={dark ? "#064E3B" : "#DCFCE7"}>
<Title order={5} c="#22C55E" mb="sm">
Pendapatan
</Title>
@@ -394,11 +385,7 @@ const KeuanganAnggaran = () => {
{/* Belanja */}
<Grid.Col span={6}>
<Card
p="sm"
radius="lg"
bg={dark ? "#7F1D1D" : "#FEE2E2"}
>
<Card p="sm" radius="lg" bg={dark ? "#7F1D1D" : "#FEE2E2"}>
<Title order={5} c="#EF4444" mb="sm">
Belanja
</Title>

View File

@@ -1,12 +1,11 @@
import { Grid, Stack } from "@mantine/core";
import { ActivityCard } from "./kinerja-divisi/activity-card";
import { ArchiveCard } from "./kinerja-divisi/archive-card";
import { DiscussionPanel } from "./kinerja-divisi/discussion-panel";
import { DivisionList } from "./kinerja-divisi/division-list";
import { DocumentChart } from "./kinerja-divisi/document-chart";
import { ProgressChart } from "./kinerja-divisi/progress-chart";
import { DiscussionPanel } from "./kinerja-divisi/discussion-panel";
import { EventCard } from "./kinerja-divisi/event-card";
import { ArchiveCard } from "./kinerja-divisi/archive-card";
import { ProgressChart } from "./kinerja-divisi/progress-chart";
// Data for program kegiatan (Section 1)
const programKegiatanData = [

View File

@@ -1,4 +1,4 @@
import { Card, Text, Progress, Group, Box } from "@mantine/core";
import { Box, Card, Group, Progress, Text } from "@mantine/core";
interface ActivityCardProps {
title: string;
@@ -86,4 +86,4 @@ export function ActivityCard({
</Box>
</Card>
);
}
}

View File

@@ -8,7 +8,7 @@ import {
Text,
ThemeIcon,
Title,
useMantineColorScheme
useMantineColorScheme,
} from "@mantine/core";
import { CheckCircle, Clock, FileText, MessageCircle } from "lucide-react";
import {

View File

@@ -61,10 +61,7 @@ export function Sidebar({ className }: SidebarProps) {
return (
<Box className={className}>
{/* Logo */}
<Image
src={dark ? "/white.png" : "/light-mode.png"}
alt="Logo"
/>
<Image src={dark ? "/white.png" : "/light-mode.png"} alt="Logo" />
{/* Search */}
<Box p="md">

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,148 @@
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,25 @@
import { useDisclosure } from "@mantine/hooks";
export function useSidebarFullscreen() {
const [opened, { toggle: toggleMobile }] = useDisclosure();
const [sidebarCollapsed, setSidebarCollapsed] = useDisclosure(false);
const toggleSidebar = () => {
setSidebarCollapsed.toggle();
};
const handleMainClick = () => {
if (!sidebarCollapsed) {
toggleSidebar();
}
};
return {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
isCollapsed: sidebarCollapsed,
};
}

View File

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

View File

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

View File

@@ -11,25 +11,32 @@ export const Route = createFileRoute("/")({
function DashboardPage() {
const [opened, { toggle }] = useDisclosure();
const [sidebarCollapsed, setSidebarCollapsed] = useDisclosure(false);
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
const handleMainClick = () => {
if (!sidebarCollapsed) {
setSidebarCollapsed.toggle();
}
};
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened },
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
<Header />
<Header onSidebarToggle={setSidebarCollapsed.toggle} />
</Group>
</AppShell.Header>
@@ -43,7 +50,11 @@ function DashboardPage() {
</div>
</AppShell.Navbar>
<AppShell.Main bg={mainBgColor}>
<AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<DashboardContent />
</AppShell.Main>
</AppShell>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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;
};