Refactor New Ui Bumdes
This commit is contained in:
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user