Refactor New Ui Bumdes
This commit is contained in:
@@ -1,385 +1,38 @@
|
|||||||
import {
|
import { Grid, GridCol, Stack } from "@mantine/core";
|
||||||
Badge,
|
import { HeaderToggle } from "./umkm/header-toggle";
|
||||||
Button,
|
import { ProdukUnggulan } from "./umkm/produk-unggulan";
|
||||||
Card,
|
import type { SalesData } from "./umkm/sales-table";
|
||||||
Grid,
|
import { SalesTable } from "./umkm/sales-table";
|
||||||
GridCol,
|
import { SummaryCards } from "./umkm/summary-cards";
|
||||||
Group,
|
import { TopProducts } from "./umkm/top-products";
|
||||||
Select,
|
|
||||||
Stack,
|
|
||||||
Table,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
useMantineColorScheme,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import {
|
|
||||||
IconBuildingStore,
|
|
||||||
IconCategory,
|
|
||||||
IconCurrency,
|
|
||||||
IconUsers,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
const BumdesPage = () => {
|
const BumdesPage = () => {
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const handleDetailClick = (product: SalesData) => {
|
||||||
const dark = colorScheme === "dark";
|
console.log("Detail clicked for:", product);
|
||||||
|
// TODO: Open modal or navigate to detail page
|
||||||
const [timeFilter, setTimeFilter] = useState<string>("bulan");
|
};
|
||||||
|
|
||||||
// Sample data for KPI cards
|
|
||||||
const kpiData = [
|
|
||||||
{
|
|
||||||
title: "UMKM Aktif",
|
|
||||||
value: 45,
|
|
||||||
icon: <IconUsers size={24} />,
|
|
||||||
color: "darmasaba-blue",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "UMKM Terdaftar",
|
|
||||||
value: 68,
|
|
||||||
icon: <IconBuildingStore size={24} />,
|
|
||||||
color: "darmasaba-success",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Omzet",
|
|
||||||
value: "Rp 48.000.000",
|
|
||||||
icon: <IconCurrency size={24} />,
|
|
||||||
color: "darmasaba-warning",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Kategori UMKM",
|
|
||||||
value: 34,
|
|
||||||
icon: <IconCategory size={24} />,
|
|
||||||
color: "darmasaba-danger",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Sample data for top products
|
|
||||||
const topProducts = [
|
|
||||||
{
|
|
||||||
rank: 1,
|
|
||||||
name: "Beras Premium Organik",
|
|
||||||
umkmOwner: "Warung Pak Joko",
|
|
||||||
growth: "+12%",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rank: 2,
|
|
||||||
name: "Keripik Singkong",
|
|
||||||
umkmOwner: "Ibu Sari Snack",
|
|
||||||
growth: "+8%",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rank: 3,
|
|
||||||
name: "Madu Alami",
|
|
||||||
umkmOwner: "Peternakan Lebah",
|
|
||||||
growth: "+5%",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Sample data for product sales
|
|
||||||
const productSales = [
|
|
||||||
{
|
|
||||||
produk: "Beras Premium Organik",
|
|
||||||
penjualanBulanIni: "Rp 8.500.000",
|
|
||||||
bulanLalu: "Rp 8.500.000",
|
|
||||||
trend: 10,
|
|
||||||
volume: "650 Kg",
|
|
||||||
stok: "850 Kg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
produk: "Keripik Singkong",
|
|
||||||
penjualanBulanIni: "Rp 4.200.000",
|
|
||||||
bulanLalu: "Rp 3.800.000",
|
|
||||||
trend: 10,
|
|
||||||
volume: "320 Kg",
|
|
||||||
stok: "120 Kg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
produk: "Madu Alami",
|
|
||||||
penjualanBulanIni: "Rp 3.750.000",
|
|
||||||
bulanLalu: "Rp 4.100.000",
|
|
||||||
trend: -8,
|
|
||||||
volume: "150 Liter",
|
|
||||||
stok: "45 Liter",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
produk: "Kecap Tradisional",
|
|
||||||
penjualanBulanIni: "Rp 2.800.000",
|
|
||||||
bulanLalu: "Rp 2.500.000",
|
|
||||||
trend: 12,
|
|
||||||
volume: "280 Botol",
|
|
||||||
stok: "95 Botol",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
{/* KPI Cards */}
|
{/* KPI Summary Cards */}
|
||||||
<Grid gutter="md">
|
<SummaryCards />
|
||||||
{kpiData.map((kpi, index) => (
|
|
||||||
<GridCol key={index} span={{ base: 12, sm: 6, md: 3 }}>
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Stack gap={0}>
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
{kpi.title}
|
|
||||||
</Text>
|
|
||||||
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
{typeof kpi.value === "number"
|
|
||||||
? kpi.value.toLocaleString()
|
|
||||||
: kpi.value}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<Badge variant="light" color={kpi.color} p={8} radius="md">
|
|
||||||
{kpi.icon}
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Update Penjualan Produk Header */}
|
{/* Header with Time Range Toggle */}
|
||||||
<Card
|
<HeaderToggle />
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" align="center" px="md" py="xs">
|
|
||||||
<Title order={3} c={dark ? "dark.0" : "black"}>
|
|
||||||
Update Penjualan Produk
|
|
||||||
</Title>
|
|
||||||
<Group>
|
|
||||||
<Button
|
|
||||||
variant={timeFilter === "minggu" ? "filled" : "light"}
|
|
||||||
onClick={() => setTimeFilter("minggu")}
|
|
||||||
color="darmasaba-blue"
|
|
||||||
>
|
|
||||||
Minggu ini
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={timeFilter === "bulan" ? "filled" : "light"}
|
|
||||||
onClick={() => setTimeFilter("bulan")}
|
|
||||||
color="darmasaba-blue"
|
|
||||||
>
|
|
||||||
Bulan ini
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
|
{/* Main Content - 2 Column Layout */}
|
||||||
<Grid gutter="md">
|
<Grid gutter="md">
|
||||||
{/* Produk Unggulan (Left Column) */}
|
{/* Left Panel - Produk Unggulan */}
|
||||||
<GridCol span={{ base: 12, lg: 4 }}>
|
<GridCol span={{ base: 12, lg: 4 }}>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{/* Total Penjualan, Produk Aktif, Total Transaksi */}
|
<ProdukUnggulan />
|
||||||
<Card
|
<TopProducts />
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Stack gap="md">
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
Total Penjualan
|
|
||||||
</Text>
|
|
||||||
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
Rp 28.500.000
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
Produk Aktif
|
|
||||||
</Text>
|
|
||||||
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
124 Produk
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
Total Transaksi
|
|
||||||
</Text>
|
|
||||||
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>
|
|
||||||
1.240 Transaksi
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Top 3 Produk Terlaris */}
|
|
||||||
<Card
|
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Title order={4} mb="md" c={dark ? "dark.0" : "black"}>
|
|
||||||
Top 3 Produk Terlaris
|
|
||||||
</Title>
|
|
||||||
<Stack gap="sm">
|
|
||||||
{topProducts.map((product) => (
|
|
||||||
<Group
|
|
||||||
key={product.rank}
|
|
||||||
justify="space-between"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
<Group gap="sm">
|
|
||||||
<Badge
|
|
||||||
variant="filled"
|
|
||||||
color={
|
|
||||||
product.rank === 1
|
|
||||||
? "gold"
|
|
||||||
: product.rank === 2
|
|
||||||
? "gray"
|
|
||||||
: "bronze"
|
|
||||||
}
|
|
||||||
radius="xl"
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
{product.rank}
|
|
||||||
</Badge>
|
|
||||||
<Stack gap={0}>
|
|
||||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
|
||||||
{product.name}
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
|
||||||
{product.umkmOwner}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
|
||||||
<Badge
|
|
||||||
variant="light"
|
|
||||||
color={product.growth.startsWith("+") ? "green" : "red"}
|
|
||||||
>
|
|
||||||
{product.growth}
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
|
|
||||||
{/* Detail Penjualan Produk (Right Column) */}
|
{/* Right Panel - Detail Penjualan Produk */}
|
||||||
<GridCol span={{ base: 12, lg: 8 }}>
|
<GridCol span={{ base: 12, lg: 8 }}>
|
||||||
<Card
|
<SalesTable onDetailClick={handleDetailClick} />
|
||||||
p="md"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg={dark ? "#141D34" : "white"}
|
|
||||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" mb="md">
|
|
||||||
<Title order={4} c={dark ? "dark.0" : "black"}>
|
|
||||||
Detail Penjualan Produk
|
|
||||||
</Title>
|
|
||||||
<Select
|
|
||||||
placeholder="Filter kategori"
|
|
||||||
data={[
|
|
||||||
{ value: "semua", label: "Semua Kategori" },
|
|
||||||
{ value: "makanan", label: "Makanan" },
|
|
||||||
{ value: "minuman", label: "Minuman" },
|
|
||||||
{ value: "kerajinan", label: "Kerajinan" },
|
|
||||||
]}
|
|
||||||
defaultValue="semua"
|
|
||||||
w={200}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Table striped highlightOnHover withColumnBorders>
|
|
||||||
<Table.Thead>
|
|
||||||
<Table.Tr>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>Produk</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>
|
|
||||||
Penjualan Bulan Ini
|
|
||||||
</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>Bulan Lalu</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>Trend</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>Volume</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>Stok</Text>
|
|
||||||
</Table.Th>
|
|
||||||
<Table.Th>
|
|
||||||
<Text c={dark ? "white" : "dimmed"}>Aksi</Text>
|
|
||||||
</Table.Th>
|
|
||||||
</Table.Tr>
|
|
||||||
</Table.Thead>
|
|
||||||
<Table.Tbody>
|
|
||||||
{productSales.map((product, index) => (
|
|
||||||
<Table.Tr key={index}>
|
|
||||||
<Table.Td>
|
|
||||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
|
||||||
{product.produk}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Text fz={"sm"} c={dark ? "dark.0" : "black"}>
|
|
||||||
{product.penjualanBulanIni}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Text fz={"sm"} c={dark ? "white" : "dimmed"}>
|
|
||||||
{product.bulanLalu}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Group gap="xs">
|
|
||||||
<Text c={product.trend >= 0 ? "green" : "red"}>
|
|
||||||
{product.trend >= 0 ? "↑" : "↓"}{" "}
|
|
||||||
{Math.abs(product.trend)}%
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Text fz={"sm"} c={dark ? "dark.0" : "black"}>
|
|
||||||
{product.volume}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Badge
|
|
||||||
variant="light"
|
|
||||||
color={
|
|
||||||
parseInt(product.stok) > 200 ? "green" : "yellow"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{product.stok}
|
|
||||||
</Badge>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
size="compact-sm"
|
|
||||||
color="darmasaba-blue"
|
|
||||||
>
|
|
||||||
Detail
|
|
||||||
</Button>
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
))}
|
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ import {
|
|||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
Users,
|
|
||||||
Home,
|
|
||||||
Baby,
|
Baby,
|
||||||
TrendingDown,
|
|
||||||
BarChart3,
|
BarChart3,
|
||||||
PieChart as PieChartIcon,
|
|
||||||
Building2,
|
Building2,
|
||||||
|
Home,
|
||||||
|
PieChart as PieChartIcon,
|
||||||
|
TrendingDown,
|
||||||
|
Users,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Bar,
|
Bar,
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import {
|
|||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
MessageCircle,
|
|
||||||
CheckCircle,
|
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
|
CheckCircle,
|
||||||
Clock,
|
Clock,
|
||||||
|
MessageCircle,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ import {
|
|||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
Coins,
|
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
TrendingUp,
|
Coins,
|
||||||
TrendingDown,
|
|
||||||
PieChart as PieChartIcon,
|
PieChart as PieChartIcon,
|
||||||
Receipt,
|
Receipt,
|
||||||
|
TrendingDown,
|
||||||
|
TrendingUp,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Bar,
|
Bar,
|
||||||
@@ -144,18 +144,10 @@ const KeuanganAnggaran = () => {
|
|||||||
{item.value}
|
{item.value}
|
||||||
</Text>
|
</Text>
|
||||||
<Group gap={4} align="flex-start">
|
<Group gap={4} align="flex-start">
|
||||||
{item.trend && (
|
{item.trend && <TrendingUp size={14} color="#22C55E" />}
|
||||||
<TrendingUp size={14} color="#22C55E" />
|
|
||||||
)}
|
|
||||||
<Text
|
<Text
|
||||||
size="xs"
|
size="xs"
|
||||||
c={
|
c={item.trend ? "green" : dark ? "gray.4" : "gray.5"}
|
||||||
item.trend
|
|
||||||
? "green"
|
|
||||||
: dark
|
|
||||||
? "gray.4"
|
|
||||||
: "gray.5"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{item.subtitle}
|
{item.subtitle}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -313,7 +305,10 @@ const KeuanganAnggaran = () => {
|
|||||||
borderColor: dark ? "#334155" : "#e5e7eb",
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
}}
|
}}
|
||||||
formatter={(value: number | undefined) => [`Rp ${value}jt`, "Jumlah"]}
|
formatter={(value: number | undefined) => [
|
||||||
|
`Rp ${value}jt`,
|
||||||
|
"Jumlah",
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
<Bar
|
<Bar
|
||||||
dataKey="amount"
|
dataKey="amount"
|
||||||
@@ -354,11 +349,7 @@ const KeuanganAnggaran = () => {
|
|||||||
<Grid gutter="md">
|
<Grid gutter="md">
|
||||||
{/* Pendapatan */}
|
{/* Pendapatan */}
|
||||||
<Grid.Col span={6}>
|
<Grid.Col span={6}>
|
||||||
<Card
|
<Card p="sm" radius="lg" bg={dark ? "#064E3B" : "#DCFCE7"}>
|
||||||
p="sm"
|
|
||||||
radius="lg"
|
|
||||||
bg={dark ? "#064E3B" : "#DCFCE7"}
|
|
||||||
>
|
|
||||||
<Title order={5} c="#22C55E" mb="sm">
|
<Title order={5} c="#22C55E" mb="sm">
|
||||||
Pendapatan
|
Pendapatan
|
||||||
</Title>
|
</Title>
|
||||||
@@ -394,11 +385,7 @@ const KeuanganAnggaran = () => {
|
|||||||
|
|
||||||
{/* Belanja */}
|
{/* Belanja */}
|
||||||
<Grid.Col span={6}>
|
<Grid.Col span={6}>
|
||||||
<Card
|
<Card p="sm" radius="lg" bg={dark ? "#7F1D1D" : "#FEE2E2"}>
|
||||||
p="sm"
|
|
||||||
radius="lg"
|
|
||||||
bg={dark ? "#7F1D1D" : "#FEE2E2"}
|
|
||||||
>
|
|
||||||
<Title order={5} c="#EF4444" mb="sm">
|
<Title order={5} c="#EF4444" mb="sm">
|
||||||
Belanja
|
Belanja
|
||||||
</Title>
|
</Title>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { Grid, Stack } from "@mantine/core";
|
import { Grid, Stack } from "@mantine/core";
|
||||||
import { ActivityCard } from "./kinerja-divisi/activity-card";
|
import { ActivityCard } from "./kinerja-divisi/activity-card";
|
||||||
|
import { ArchiveCard } from "./kinerja-divisi/archive-card";
|
||||||
|
import { DiscussionPanel } from "./kinerja-divisi/discussion-panel";
|
||||||
import { DivisionList } from "./kinerja-divisi/division-list";
|
import { DivisionList } from "./kinerja-divisi/division-list";
|
||||||
import { DocumentChart } from "./kinerja-divisi/document-chart";
|
import { DocumentChart } from "./kinerja-divisi/document-chart";
|
||||||
import { ProgressChart } from "./kinerja-divisi/progress-chart";
|
|
||||||
import { DiscussionPanel } from "./kinerja-divisi/discussion-panel";
|
|
||||||
import { EventCard } from "./kinerja-divisi/event-card";
|
import { EventCard } from "./kinerja-divisi/event-card";
|
||||||
import { ArchiveCard } from "./kinerja-divisi/archive-card";
|
import { ProgressChart } from "./kinerja-divisi/progress-chart";
|
||||||
|
|
||||||
|
|
||||||
// Data for program kegiatan (Section 1)
|
// Data for program kegiatan (Section 1)
|
||||||
const programKegiatanData = [
|
const programKegiatanData = [
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Card, Text, Progress, Group, Box } from "@mantine/core";
|
import { Box, Card, Group, Progress, Text } from "@mantine/core";
|
||||||
|
|
||||||
interface ActivityCardProps {
|
interface ActivityCardProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
useMantineColorScheme
|
useMantineColorScheme,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { CheckCircle, Clock, FileText, MessageCircle } from "lucide-react";
|
import { CheckCircle, Clock, FileText, MessageCircle } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -61,10 +61,7 @@ export function Sidebar({ className }: SidebarProps) {
|
|||||||
return (
|
return (
|
||||||
<Box className={className}>
|
<Box className={className}>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Image
|
<Image src={dark ? "/white.png" : "/light-mode.png"} alt="Logo" />
|
||||||
src={dark ? "/white.png" : "/light-mode.png"}
|
|
||||||
alt="Logo"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<Box p="md">
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,8 +2,8 @@ import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
|||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { Header } from "@/components/header";
|
import { Header } from "@/components/header";
|
||||||
import { Sidebar } from "@/components/sidebar";
|
|
||||||
import HelpPage from "@/components/help-page";
|
import HelpPage from "@/components/help-page";
|
||||||
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
|
||||||
export const Route = createFileRoute("/bantuan")({
|
export const Route = createFileRoute("/bantuan")({
|
||||||
component: BantuanRoute,
|
component: BantuanRoute,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import BumdesPage from "@/components/bumdes-page";
|
||||||
import { Header } from "@/components/header";
|
import { Header } from "@/components/header";
|
||||||
import { Sidebar } from "@/components/sidebar";
|
import { Sidebar } from "@/components/sidebar";
|
||||||
import BumdesPage from "@/components/bumdes-page";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/bumdes")({
|
export const Route = createFileRoute("/bumdes")({
|
||||||
component: BumdesRoute,
|
component: BumdesRoute,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
|||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { Header } from "@/components/header";
|
import { Header } from "@/components/header";
|
||||||
import { Sidebar } from "@/components/sidebar";
|
|
||||||
import KeamananPage from "@/components/keamanan-page";
|
import KeamananPage from "@/components/keamanan-page";
|
||||||
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
|
||||||
export const Route = createFileRoute("/keamanan")({
|
export const Route = createFileRoute("/keamanan")({
|
||||||
component: KeamananRoute,
|
component: KeamananRoute,
|
||||||
|
|||||||
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