Compare commits
2 Commits
nico/18-ma
...
nico/25-ma
| Author | SHA1 | Date | |
|---|---|---|---|
| 84b96ca3be | |||
| 8159216a2c |
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -13,10 +13,10 @@ import {
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
MessageCircle,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
MessageCircle,
|
||||
TrendingUp,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
|
||||
@@ -1,73 +1,70 @@
|
||||
import { BarChart } from "@mantine/charts";
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
Progress,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconCurrency,
|
||||
IconTrendingDown,
|
||||
IconTrendingUp,
|
||||
} from "@tabler/icons-react";
|
||||
import React from "react";
|
||||
CheckCircle,
|
||||
Coins,
|
||||
PieChart as PieChartIcon,
|
||||
Receipt,
|
||||
TrendingDown,
|
||||
TrendingUp,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
// Sample Data
|
||||
// KPI Data
|
||||
const kpiData = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Total APBDes",
|
||||
value: "Rp 5.2M",
|
||||
sub: "Tahun 2025",
|
||||
icon: <IconCurrency className="h-6 w-6 text-muted-foreground" />,
|
||||
subtitle: "Tahun 2025",
|
||||
icon: Coins,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Realisasi",
|
||||
value: "68%",
|
||||
sub: "Rp 3.5M dari 5.2M",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="h-6 w-6 text-muted-foreground"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.473-1.688 3.342-.48.485-.926.97-1.378 1.44c-1.472 1.58-2.306 2.787-2.91 3.514-.15.18-.207.33-.207.33A.75.75 0 0 1 15 21h-3c-1.104 0-2.08-.542-2.657-1.455-.139-.201-.264-.406-.38-.614l-.014-.025C8.85 18.067 8.156 17.2 7.5 16.325.728 12.56.728 7.44 7.5 3.675c3.04-.482 5.584.47 7.042 1.956.674.672 1.228 1.462 1.696 2.307.426.786.793 1.582 1.113 2.392h.001Z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
subtitle: "Rp 3.5M dari 5.2M",
|
||||
icon: CheckCircle,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Pemasukan",
|
||||
value: "Rp 580jt",
|
||||
sub: "Bulan ini",
|
||||
delta: "+8%",
|
||||
deltaType: "positive",
|
||||
icon: <IconTrendingUp className="h-6 w-6 text-muted-foreground" />,
|
||||
subtitle: "Bulan ini",
|
||||
trend: "+8%",
|
||||
icon: TrendingUp,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Pengeluaran",
|
||||
value: "Rp 520jt",
|
||||
sub: "Bulan ini",
|
||||
icon: <IconTrendingDown className="h-6 w-6 text-muted-foreground" />,
|
||||
subtitle: "Bulan ini",
|
||||
icon: TrendingDown,
|
||||
},
|
||||
];
|
||||
|
||||
// Income & Expense Data
|
||||
const incomeExpenseData = [
|
||||
{ month: "Apr", income: 450, expense: 380 },
|
||||
{ month: "Mei", income: 520, expense: 420 },
|
||||
@@ -78,6 +75,7 @@ const incomeExpenseData = [
|
||||
{ month: "Okt", income: 580, expense: 520 },
|
||||
];
|
||||
|
||||
// Sector Allocation Data
|
||||
const allocationData = [
|
||||
{ sector: "Pembangunan", amount: 1200 },
|
||||
{ sector: "Kesehatan", amount: 800 },
|
||||
@@ -87,13 +85,7 @@ const allocationData = [
|
||||
{ sector: "Teknologi", amount: 300 },
|
||||
];
|
||||
|
||||
const assistanceFundData = [
|
||||
{ source: "Dana Desa (DD)", amount: 1800, status: "cair" },
|
||||
{ source: "Alokasi Dana Desa (ADD)", amount: 950, status: "cair" },
|
||||
{ source: "Bagi Hasil Pajak", amount: 450, status: "cair" },
|
||||
{ source: "Hibah Provinsi", amount: 300, status: "proses" },
|
||||
];
|
||||
|
||||
// APBDes Report Data
|
||||
const apbdReport = {
|
||||
income: [
|
||||
{ category: "Dana Desa", amount: 1800 },
|
||||
@@ -113,244 +105,410 @@ const apbdReport = {
|
||||
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 { colorScheme } = useMantineColorScheme();
|
||||
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 */}
|
||||
<Grid gutter="lg">
|
||||
{/* Grafik Pemasukan vs Pengeluaran */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
{/* TOP SECTION - 4 STAT CARDS */}
|
||||
<Grid gutter="md">
|
||||
{kpiData.map((item) => (
|
||||
<Grid.Col key={item.id} span={{ base: 12, sm: 6, lg: 3 }}>
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
transition: "transform 0.15s ease, box-shadow 0.15s ease",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Title order={3} fw={500} mb="md">
|
||||
Pemasukan vs Pengeluaran
|
||||
</Title>
|
||||
<BarChart
|
||||
h={300}
|
||||
data={incomeExpenseData}
|
||||
dataKey="month"
|
||||
series={[
|
||||
{ name: "income", color: "green", label: "Pemasukan" },
|
||||
{ name: "expense", color: "red", label: "Pengeluaran" },
|
||||
]}
|
||||
withLegend
|
||||
/>
|
||||
<Group justify="space-between" align="flex-start" w="100%">
|
||||
<Stack gap={2}>
|
||||
<Text size="sm" c="dimmed">
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text size="xl" fw={700} c={dark ? "white" : "gray.9"}>
|
||||
{item.value}
|
||||
</Text>
|
||||
<Group gap={4} align="flex-start">
|
||||
{item.trend && <TrendingUp size={14} color="#22C55E" />}
|
||||
<Text
|
||||
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>
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* Alokasi Anggaran Per Sektor */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Title order={3} fw={500} mb="md">
|
||||
{/* MAIN CHART SECTION */}
|
||||
<Grid gutter="lg">
|
||||
{/* LEFT: PEMASUKAN DAN PENGELUARAN (70%) */}
|
||||
<Grid.Col span={{ base: 12, lg: 8 }}>
|
||||
<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"}>
|
||||
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
|
||||
</Title>
|
||||
<BarChart
|
||||
h={300}
|
||||
data={allocationData}
|
||||
dataKey="sector"
|
||||
series={[
|
||||
{ name: "amount", color: "darmasaba-navy", label: "Jumlah" },
|
||||
]}
|
||||
withLegend
|
||||
orientation="horizontal"
|
||||
/>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Group>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={allocationData} layout="vertical">
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
horizontal={false}
|
||||
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||
/>
|
||||
<XAxis
|
||||
type="number"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{
|
||||
fill: dark ? "#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">
|
||||
{/* Dana Bantuan & Hibah */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Title order={3} fw={500} mb="md">
|
||||
Dana Bantuan & Hibah
|
||||
{/* BOTTOM SECTION */}
|
||||
<Grid gutter="lg">
|
||||
{/* LEFT: LAPORAN APBDES */}
|
||||
<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">
|
||||
<Receipt size={14} />
|
||||
</ThemeIcon>
|
||||
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||
Laporan APBDes
|
||||
</Title>
|
||||
<Stack gap="sm">
|
||||
{assistanceFundData.map((fund, index) => (
|
||||
<Group
|
||||
key={index}
|
||||
justify="space-between"
|
||||
align="center"
|
||||
p="sm"
|
||||
style={{
|
||||
border: "1px solid var(--mantine-color-gray-3)",
|
||||
borderRadius: "var(--mantine-radius-sm)",
|
||||
}}
|
||||
>
|
||||
</Group>
|
||||
|
||||
<Grid gutter="md">
|
||||
{/* Pendapatan */}
|
||||
<Grid.Col span={6}>
|
||||
<Card p="sm" radius="lg" bg={dark ? "#064E3B" : "#DCFCE7"}>
|
||||
<Title order={5} c="#22C55E" mb="sm">
|
||||
Pendapatan
|
||||
</Title>
|
||||
<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>
|
||||
<Text size="sm" fw={500}>
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
|
||||
{fund.source}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
<Text size="xs" c="dimmed">
|
||||
Rp {fund.amount.toLocaleString()}jt
|
||||
</Text>
|
||||
</Box>
|
||||
<Badge
|
||||
variant="light"
|
||||
color={fund.status === "cair" ? "green" : "yellow"}
|
||||
radius="sm"
|
||||
fw={600}
|
||||
>
|
||||
{fund.status}
|
||||
{fund.status === "cair" ? "Cair" : "Proses"}
|
||||
</Badge>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
|
||||
{/* Laporan APBDes */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Title order={3} fw={500} mb="md">
|
||||
Laporan APBDes
|
||||
</Title>
|
||||
|
||||
<Box mb="md">
|
||||
<Title order={4} mb="sm">
|
||||
Pendapatan
|
||||
</Title>
|
||||
<Stack gap="xs">
|
||||
{apbdReport.income.map((item, index) => (
|
||||
<Group key={index} justify="space-between">
|
||||
<Text size="sm">{item.category}</Text>
|
||||
<Text size="sm" c="green">
|
||||
Rp {item.amount.toLocaleString()}jt
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
<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>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
useMantineColorScheme
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { CheckCircle, Clock, FileText, MessageCircle } from "lucide-react";
|
||||
import {
|
||||
|
||||
@@ -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">
|
||||
|
||||
75
src/components/umkm/header-toggle.tsx
Normal file
75
src/components/umkm/header-toggle.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { useSnapshot } from "valtio";
|
||||
import { setRange, umkmStore } from "../../store/umkm";
|
||||
|
||||
type TimeRange = "minggu" | "bulan";
|
||||
|
||||
interface HeaderToggleProps {
|
||||
title?: string;
|
||||
onRangeChange?: (range: TimeRange) => void;
|
||||
}
|
||||
|
||||
export const HeaderToggle = ({
|
||||
title = "Update Penjualan Produk",
|
||||
onRangeChange,
|
||||
}: HeaderToggleProps) => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
const { selectedRange } = useSnapshot(umkmStore);
|
||||
|
||||
const handleRangeChange = (range: TimeRange) => {
|
||||
setRange(range);
|
||||
onRangeChange?.(range);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
shadow="sm"
|
||||
bg={dark ? "#1e3a5f" : "#1e3a5f"}
|
||||
style={{ borderColor: dark ? "#1e3a5f" : "#1e3a5f" }}
|
||||
>
|
||||
<Group justify="space-between" align="center" px="md" py="xs">
|
||||
<Title order={3} c="white">
|
||||
{title}
|
||||
</Title>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
variant={selectedRange === "minggu" ? "white" : "transparent"}
|
||||
onClick={() => handleRangeChange("minggu")}
|
||||
c={selectedRange === "minggu" ? "#1e3a5f" : "white"}
|
||||
fw={600}
|
||||
radius="xl"
|
||||
size="sm"
|
||||
style={{
|
||||
opacity: selectedRange === "minggu" ? 1 : 0.8,
|
||||
}}
|
||||
>
|
||||
Minggu ini
|
||||
</Button>
|
||||
<Button
|
||||
variant={selectedRange === "bulan" ? "white" : "transparent"}
|
||||
onClick={() => handleRangeChange("bulan")}
|
||||
c={selectedRange === "bulan" ? "#1e3a5f" : "white"}
|
||||
fw={600}
|
||||
radius="xl"
|
||||
size="sm"
|
||||
style={{
|
||||
opacity: selectedRange === "bulan" ? 1 : 0.8,
|
||||
}}
|
||||
>
|
||||
Bulan ini
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
100
src/components/umkm/produk-unggulan.tsx
Normal file
100
src/components/umkm/produk-unggulan.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { Card, Group, Stack, Text, useMantineColorScheme } from "@mantine/core";
|
||||
|
||||
interface MetricCardProps {
|
||||
title: string;
|
||||
value: string | number;
|
||||
trend?: {
|
||||
value: number;
|
||||
label: string;
|
||||
};
|
||||
}
|
||||
|
||||
const MetricCard = ({ title, value, trend }: MetricCardProps) => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Group justify="space-between" align="center">
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"} fw={500}>
|
||||
{title}
|
||||
</Text>
|
||||
<Stack gap={0} align="flex-end">
|
||||
<Text size="lg" fw={700} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||
{value}
|
||||
</Text>
|
||||
{trend && (
|
||||
<Text size="xs" c={trend.value >= 0 ? "green" : "red"} fw={600}>
|
||||
{trend.value >= 0 ? "↑" : "↓"} {Math.abs(trend.value)}%{" "}
|
||||
{trend.label}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
interface ProdukUnggulanProps {
|
||||
data?: {
|
||||
totalPenjualan: number;
|
||||
produkAktif: number;
|
||||
totalTransaksi: number;
|
||||
trend?: {
|
||||
value: number;
|
||||
label: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const ProdukUnggulan = ({ data }: ProdukUnggulanProps) => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
const defaultData = {
|
||||
totalPenjualan: 30900000,
|
||||
produkAktif: 7,
|
||||
totalTransaksi: 500,
|
||||
trend: {
|
||||
value: 18,
|
||||
label: "vs bulan lalu",
|
||||
},
|
||||
};
|
||||
|
||||
const displayData = data || defaultData;
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
if (value >= 1000000) {
|
||||
return `Rp ${(value / 1000000).toFixed(1)}M`;
|
||||
}
|
||||
if (value >= 1000) {
|
||||
return `Rp ${(value / 1000).toFixed(0)}K`;
|
||||
}
|
||||
return `Rp ${value.toLocaleString()}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
shadow="sm"
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||
>
|
||||
<Stack gap="lg">
|
||||
<MetricCard
|
||||
title="Total Penjualan"
|
||||
value={formatCurrency(displayData.totalPenjualan)}
|
||||
trend={displayData.trend}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Produk Aktif"
|
||||
value={`${displayData.produkAktif} kategori`}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Total Transaksi"
|
||||
value={`${displayData.totalTransaksi} transaksi`}
|
||||
/>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
260
src/components/umkm/sales-table.tsx
Normal file
260
src/components/umkm/sales-table.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Select,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { IconArrowDown, IconArrowUp } from "@tabler/icons-react";
|
||||
|
||||
export interface SalesData {
|
||||
id: string;
|
||||
produk: string;
|
||||
penjualanBulanIni: number;
|
||||
bulanLalu: number;
|
||||
trend: number;
|
||||
volume: string;
|
||||
stok: number;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
interface SalesTableProps {
|
||||
data?: SalesData[];
|
||||
onDetailClick?: (product: SalesData) => void;
|
||||
}
|
||||
|
||||
export const SalesTable = ({ data, onDetailClick }: SalesTableProps) => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
const defaultData: SalesData[] = [
|
||||
{
|
||||
id: "1",
|
||||
produk: "Beras Premium Organik",
|
||||
penjualanBulanIni: 8500000,
|
||||
bulanLalu: 7600000,
|
||||
trend: 12,
|
||||
volume: "650 Kg",
|
||||
stok: 850,
|
||||
unit: "Kg",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
produk: "Keripik Singkong",
|
||||
penjualanBulanIni: 4200000,
|
||||
bulanLalu: 3800000,
|
||||
trend: 11,
|
||||
volume: "320 Kg",
|
||||
stok: 120,
|
||||
unit: "Kg",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
produk: "Madu Alami",
|
||||
penjualanBulanIni: 3750000,
|
||||
bulanLalu: 4100000,
|
||||
trend: -9,
|
||||
volume: "150 Liter",
|
||||
stok: 45,
|
||||
unit: "Liter",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
produk: "Kecap Tradisional",
|
||||
penjualanBulanIni: 2800000,
|
||||
bulanLalu: 2500000,
|
||||
trend: 12,
|
||||
volume: "280 Botol",
|
||||
stok: 95,
|
||||
unit: "Botol",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
produk: "Sambal Bu Rudy",
|
||||
penjualanBulanIni: 2100000,
|
||||
bulanLalu: 2300000,
|
||||
trend: -9,
|
||||
volume: "180 Botol",
|
||||
stok: 35,
|
||||
unit: "Botol",
|
||||
},
|
||||
];
|
||||
|
||||
const displayData = data || defaultData;
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
if (value >= 1000000) {
|
||||
return `Rp ${(value / 1000000).toFixed(1)}M`;
|
||||
}
|
||||
if (value >= 1000) {
|
||||
return `Rp ${(value / 1000).toFixed(0)}K`;
|
||||
}
|
||||
return `Rp ${value.toLocaleString()}`;
|
||||
};
|
||||
|
||||
const getStockStatus = (stock: number) => {
|
||||
if (stock > 200) return { color: "green", label: "Aman" };
|
||||
if (stock > 50) return { color: "yellow", label: "Sedang" };
|
||||
return { color: "red", label: "Rendah" };
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
shadow="sm"
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||
>
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||
Detail Penjualan Produk
|
||||
</Title>
|
||||
<Group gap="xs">
|
||||
<Select
|
||||
placeholder="Filter kategori"
|
||||
data={[
|
||||
{ value: "semua", label: "Semua Kategori" },
|
||||
{ value: "makanan", label: "Makanan" },
|
||||
{ value: "minuman", label: "Minuman" },
|
||||
{ value: "kerajinan", label: "Kerajinan" },
|
||||
]}
|
||||
defaultValue="semua"
|
||||
w={180}
|
||||
size="sm"
|
||||
/>
|
||||
<Select
|
||||
placeholder="Filter UMKM"
|
||||
data={[
|
||||
{ value: "semua", label: "Semua UMKM" },
|
||||
{ value: "umkm1", label: "Warung Pak Joko" },
|
||||
{ value: "umkm2", label: "Ibu Sari Snack" },
|
||||
]}
|
||||
defaultValue="semua"
|
||||
w={180}
|
||||
size="sm"
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Table
|
||||
stickyHeader
|
||||
stickyHeaderOffset={60}
|
||||
highlightOnHover
|
||||
withRowBorders={false}
|
||||
verticalSpacing="sm"
|
||||
>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||
Produk
|
||||
</Text>
|
||||
</Table.Th>
|
||||
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||
Penjualan Bulan Ini
|
||||
</Text>
|
||||
</Table.Th>
|
||||
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||
Bulan Lalu
|
||||
</Text>
|
||||
</Table.Th>
|
||||
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||
Trend
|
||||
</Text>
|
||||
</Table.Th>
|
||||
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||
Volume
|
||||
</Text>
|
||||
</Table.Th>
|
||||
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||
Stok
|
||||
</Text>
|
||||
</Table.Th>
|
||||
<Table.Th style={{ backgroundColor: dark ? "#1e3a5f" : "#f8f9fa" }}>
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "dimmed"}>
|
||||
Aksi
|
||||
</Text>
|
||||
</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{displayData.map((product) => {
|
||||
const stockStatus = getStockStatus(product.stok);
|
||||
return (
|
||||
<Table.Tr
|
||||
key={product.id}
|
||||
style={{
|
||||
backgroundColor: dark ? "#141D34" : "white",
|
||||
}}
|
||||
>
|
||||
<Table.Td>
|
||||
<Text fw={600} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||
{product.produk}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm" fw={600} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||
{formatCurrency(product.penjualanBulanIni)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||
{formatCurrency(product.bulanLalu)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs">
|
||||
{product.trend >= 0 ? (
|
||||
<IconArrowUp size={16} color="green" />
|
||||
) : (
|
||||
<IconArrowDown size={16} color="red" />
|
||||
)}
|
||||
<Text
|
||||
size="sm"
|
||||
fw={600}
|
||||
c={product.trend >= 0 ? "green" : "red"}
|
||||
>
|
||||
{Math.abs(product.trend)}%
|
||||
</Text>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm" c={dark ? "dark.0" : "#1e3a5f"}>
|
||||
{product.volume}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge variant="light" color={stockStatus.color} size="sm">
|
||||
{product.stok} {product.unit} ({stockStatus.label})
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="compact-sm"
|
||||
color="darmasaba-blue"
|
||||
radius="xl"
|
||||
onClick={() => onDetailClick?.(product)}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
144
src/components/umkm/summary-cards.tsx
Normal file
144
src/components/umkm/summary-cards.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconBuildingStore,
|
||||
IconCategory,
|
||||
IconCurrency,
|
||||
IconCurrencyDollar,
|
||||
IconUsers,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
interface KpiCardProps {
|
||||
title: string;
|
||||
value: string | number;
|
||||
subtitle?: string;
|
||||
icon: React.ReactNode;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const KpiCard = ({ title, value, subtitle, icon, color }: 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>
|
||||
<Badge
|
||||
variant="light"
|
||||
color={color}
|
||||
p={10}
|
||||
radius="xl"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Badge>
|
||||
</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={20} />,
|
||||
color: "darmasaba-blue",
|
||||
},
|
||||
{
|
||||
title: "UMKM Terdaftar",
|
||||
value: displayData.umkmTerdaftar,
|
||||
subtitle: "Total registrasi",
|
||||
icon: <IconBuildingStore size={20} />,
|
||||
color: "darmasaba-success",
|
||||
},
|
||||
{
|
||||
title: "Omzet",
|
||||
value: displayData.omzet,
|
||||
subtitle: "Omzet BUMDes per bulan",
|
||||
icon: <IconCurrency size={20} />,
|
||||
color: "darmasaba-warning",
|
||||
},
|
||||
{
|
||||
title: "UMKM Terbanyak",
|
||||
value: displayData.kategoriTerbanyak.count,
|
||||
subtitle: `Kategori ${displayData.kategoriTerbanyak.name}`,
|
||||
icon: <IconCategory size={20} />,
|
||||
color: "darmasaba-danger",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Grid gutter="md">
|
||||
{kpiData.map((kpi, index) => (
|
||||
<GridCol key={index} span={{ base: 12, sm: 6, lg: 3 }}>
|
||||
<KpiCard {...kpi} />
|
||||
</GridCol>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
140
src/components/umkm/top-products.tsx
Normal file
140
src/components/umkm/top-products.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
|
||||
interface TopProduct {
|
||||
rank: number;
|
||||
name: string;
|
||||
umkmName: string;
|
||||
revenue: number;
|
||||
quantitySold: number;
|
||||
trend: number;
|
||||
}
|
||||
|
||||
interface TopProductsProps {
|
||||
products?: TopProduct[];
|
||||
}
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
if (value >= 1000000) {
|
||||
return `${(value / 1000000).toFixed(1)}M`;
|
||||
}
|
||||
if (value >= 1000) {
|
||||
return `${(value / 1000).toFixed(0)}K`;
|
||||
}
|
||||
return value.toString();
|
||||
};
|
||||
|
||||
const formatNumber = (value: number) => {
|
||||
if (value >= 1000) {
|
||||
return `${(value / 1000).toFixed(1)}K`;
|
||||
}
|
||||
return value.toString();
|
||||
};
|
||||
|
||||
export const TopProducts = ({ products }: TopProductsProps) => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
const defaultProducts: TopProduct[] = [
|
||||
{
|
||||
rank: 1,
|
||||
name: "Beras Premium Organik",
|
||||
umkmName: "Warung Pak Joko",
|
||||
revenue: 8500000,
|
||||
quantitySold: 650,
|
||||
trend: 12,
|
||||
},
|
||||
{
|
||||
rank: 2,
|
||||
name: "Keripik Singkong",
|
||||
umkmName: "Ibu Sari Snack",
|
||||
revenue: 4200000,
|
||||
quantitySold: 320,
|
||||
trend: 8,
|
||||
},
|
||||
{
|
||||
rank: 3,
|
||||
name: "Madu Alami",
|
||||
umkmName: "Peternakan Lebah",
|
||||
revenue: 3750000,
|
||||
quantitySold: 150,
|
||||
trend: 5,
|
||||
},
|
||||
];
|
||||
|
||||
const displayProducts = products || defaultProducts;
|
||||
|
||||
const getRankColor = (rank: number) => {
|
||||
if (rank === 1) return "yellow";
|
||||
if (rank === 2) return "gray";
|
||||
if (rank === 3) return "orange";
|
||||
return "blue";
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
shadow="sm"
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "#e5e7eb" }}
|
||||
>
|
||||
<Title order={4} mb="md" c={dark ? "dark.0" : "#1e3a5f"}>
|
||||
Top 3 Produk Terlaris
|
||||
</Title>
|
||||
<Stack gap="sm">
|
||||
{displayProducts.map((product) => (
|
||||
<Group key={product.rank} justify="space-between" align="center">
|
||||
<Group gap="sm">
|
||||
<Badge
|
||||
variant="filled"
|
||||
color={getRankColor(product.rank)}
|
||||
radius="xl"
|
||||
size="lg"
|
||||
w={30}
|
||||
h={30}
|
||||
>
|
||||
{product.rank}
|
||||
</Badge>
|
||||
<Stack gap={0}>
|
||||
<Text fw={600} c={dark ? "dark.0" : "#1e3a5f"}>
|
||||
{product.name}
|
||||
</Text>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||
{product.umkmName}
|
||||
</Text>
|
||||
<Group gap="xs" mt={2}>
|
||||
<Text size="xs" c={dark ? "dark.4" : "gray.6"}>
|
||||
Rp {formatCurrency(product.revenue)}
|
||||
</Text>
|
||||
<Text size="xs" c={dark ? "dark.4" : "gray.6"}>
|
||||
•
|
||||
</Text>
|
||||
<Text size="xs" c={dark ? "dark.4" : "gray.6"}>
|
||||
{formatNumber(product.quantitySold)} terjual
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Badge
|
||||
variant="light"
|
||||
color={product.trend >= 0 ? "green" : "red"}
|
||||
size="sm"
|
||||
>
|
||||
{product.trend >= 0 ? "+" : ""}
|
||||
{product.trend}%
|
||||
</Badge>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -6,10 +6,10 @@ import HelpPage from "@/components/help-page";
|
||||
import { Sidebar } from "@/components/sidebar";
|
||||
|
||||
export const Route = createFileRoute("/bantuan")({
|
||||
component: BantuanPage,
|
||||
component: BantuanRoute,
|
||||
});
|
||||
|
||||
function BantuanPage() {
|
||||
function BantuanRoute() {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||
|
||||
@@ -1,9 +1,51 @@
|
||||
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";
|
||||
|
||||
export const Route = createFileRoute("/bumdes")({
|
||||
component: RouteComponent,
|
||||
component: BumdesRoute,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/bumdes"!</div>;
|
||||
function BumdesRoute() {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: { mobile: !opened },
|
||||
}}
|
||||
padding="md"
|
||||
>
|
||||
<AppShell.Header bg={headerBgColor}>
|
||||
<Group h="100%" px="md">
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||
<Header />
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
|
||||
<AppShell.Navbar
|
||||
p="md"
|
||||
bg={navbarBgColor}
|
||||
style={{ display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||
<Sidebar />
|
||||
</div>
|
||||
</AppShell.Navbar>
|
||||
|
||||
<AppShell.Main bg={mainBgColor}>
|
||||
<BumdesPage />
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,51 @@
|
||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Header } from "@/components/header";
|
||||
import KeamananPage from "@/components/keamanan-page";
|
||||
import { Sidebar } from "@/components/sidebar";
|
||||
|
||||
export const Route = createFileRoute("/keamanan")({
|
||||
component: RouteComponent,
|
||||
component: KeamananRoute,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/keamanan"!</div>;
|
||||
function KeamananRoute() {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: { mobile: !opened },
|
||||
}}
|
||||
padding="md"
|
||||
>
|
||||
<AppShell.Header bg={headerBgColor}>
|
||||
<Group h="100%" px="md">
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||
<Header />
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
|
||||
<AppShell.Navbar
|
||||
p="md"
|
||||
bg={navbarBgColor}
|
||||
style={{ display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||
<Sidebar />
|
||||
</div>
|
||||
</AppShell.Navbar>
|
||||
|
||||
<AppShell.Main bg={mainBgColor}>
|
||||
<KeamananPage />
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,51 @@
|
||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Header } from "@/components/header";
|
||||
import { Sidebar } from "@/components/sidebar";
|
||||
import SosialPage from "@/components/sosial-page";
|
||||
|
||||
export const Route = createFileRoute("/sosial")({
|
||||
component: RouteComponent,
|
||||
component: SosialRoute,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/sosial"!</div>;
|
||||
function SosialRoute() {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: { mobile: !opened },
|
||||
}}
|
||||
padding="md"
|
||||
>
|
||||
<AppShell.Header bg={headerBgColor}>
|
||||
<Group h="100%" px="md">
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||
<Header />
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
|
||||
<AppShell.Navbar
|
||||
p="md"
|
||||
bg={navbarBgColor}
|
||||
style={{ display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||
<Sidebar />
|
||||
</div>
|
||||
</AppShell.Navbar>
|
||||
|
||||
<AppShell.Main bg={mainBgColor}>
|
||||
<SosialPage />
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
30
src/store/umkm.ts
Normal file
30
src/store/umkm.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { proxy } from "valtio";
|
||||
|
||||
type TimeRange = "minggu" | "bulan";
|
||||
|
||||
interface UmkmState {
|
||||
selectedRange: TimeRange;
|
||||
filters: {
|
||||
kategori: string | null;
|
||||
umkm: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export const umkmStore = proxy<UmkmState>({
|
||||
selectedRange: "bulan",
|
||||
filters: {
|
||||
kategori: null,
|
||||
umkm: null,
|
||||
},
|
||||
});
|
||||
|
||||
export const setRange = (range: TimeRange) => {
|
||||
umkmStore.selectedRange = range;
|
||||
};
|
||||
|
||||
export const setFilter = (
|
||||
key: keyof UmkmState["filters"],
|
||||
value: string | null,
|
||||
) => {
|
||||
umkmStore.filters[key] = value;
|
||||
};
|
||||
Reference in New Issue
Block a user