Dashboard fix
This commit is contained in:
parent
6903a8deb0
commit
7db93e9e8a
@ -13,7 +13,6 @@ export async function createSamplePurchase() {
|
|||||||
|
|
||||||
for (let i = 0; i < Math.floor(Math.random() * 3) + 1; i++) {
|
for (let i = 0; i < Math.floor(Math.random() * 3) + 1; i++) {
|
||||||
const product = products[Math.floor(Math.random() * products.length)];
|
const product = products[Math.floor(Math.random() * products.length)];
|
||||||
|
|
||||||
const quantity = Math.floor(Math.random() * 4) + 1;
|
const quantity = Math.floor(Math.random() * 4) + 1;
|
||||||
|
|
||||||
entries.push({
|
entries.push({
|
||||||
@ -26,13 +25,15 @@ export async function createSamplePurchase() {
|
|||||||
|
|
||||||
if (entries.length === 0) throw new Error("No suitable product variants found");
|
if (entries.length === 0) throw new Error("No suitable product variants found");
|
||||||
|
|
||||||
const total = await entries.reduce(async (accP, entry) => {
|
let total = 0;
|
||||||
const acc = await accP;
|
|
||||||
const product = await mockDB.products.get(entry.product_id);
|
for (const entry of entries) {
|
||||||
return acc + ((product?.price ?? 0) * entry.quantity);
|
const product = await mockDB.products.get(entry.product_id);
|
||||||
}, Promise.resolve(0));
|
if (product) {
|
||||||
|
total += product.price * entry.quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Randomly apply a valid coupon
|
|
||||||
const coupons = await mockDB.coupons.toArray();
|
const coupons = await mockDB.coupons.toArray();
|
||||||
const validCoupons = coupons.filter(
|
const validCoupons = coupons.filter(
|
||||||
(c) => new Date(c.valid_due) > new Date()
|
(c) => new Date(c.valid_due) > new Date()
|
||||||
@ -46,18 +47,27 @@ export async function createSamplePurchase() {
|
|||||||
? Math.max(0, total - chosenCoupon.discount_amount)
|
? Math.max(0, total - chosenCoupon.discount_amount)
|
||||||
: total;
|
: total;
|
||||||
|
|
||||||
|
// ✅ Generate a random date within the past year
|
||||||
|
const now = new Date();
|
||||||
|
const monthsAgo = Math.floor(Math.random() * 12); // 0–11 months ago
|
||||||
|
const date = new Date(now.getFullYear(), now.getMonth() - monthsAgo, 1);
|
||||||
|
|
||||||
|
const randomDay = Math.floor(Math.random() * 28) + 1;
|
||||||
|
date.setDate(randomDay);
|
||||||
|
const isoDate = date.toISOString();
|
||||||
|
|
||||||
const purchase = await MockPurchaseAPI.createPurchase(
|
const purchase = await MockPurchaseAPI.createPurchase(
|
||||||
{
|
{
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
used_coupon_id: chosenCoupon?.id ?? null,
|
used_coupon_id: chosenCoupon?.id ?? null,
|
||||||
date_purchased: new Date().toISOString(),
|
date_purchased: isoDate,
|
||||||
total: discountedTotal,
|
total: discountedTotal,
|
||||||
},
|
},
|
||||||
entries,
|
entries,
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Created mock purchase${chosenCoupon ? " with coupon" : ""}:`,
|
`Created mock purchase${chosenCoupon ? " with coupon" : ""} on ${date.toDateString()}:`,
|
||||||
purchase,
|
purchase,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import
|
|||||||
{
|
{
|
||||||
IconBuildingStore,
|
IconBuildingStore,
|
||||||
IconClipboardCheckFilled,
|
IconClipboardCheckFilled,
|
||||||
IconCoin, IconPackage,
|
IconCoin, IconLayoutDashboard, IconPackage,
|
||||||
IconPalette,
|
IconPalette,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconTag,
|
IconTag,
|
||||||
@ -16,11 +16,11 @@ export const sidebarData: SidebarData = {
|
|||||||
{
|
{
|
||||||
title: "Dashboard",
|
title: "Dashboard",
|
||||||
items: [
|
items: [
|
||||||
// {
|
{
|
||||||
// title: "Dashboard",
|
title: "Dashboard",
|
||||||
// url: "/dashboard",
|
url: "/dashboard",
|
||||||
// icon: IconLayoutDashboard,
|
icon: IconLayoutDashboard,
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
title: "Shop",
|
title: "Shop",
|
||||||
url: "/dashboard/shop",
|
url: "/dashboard/shop",
|
||||||
|
@ -1,60 +1,32 @@
|
|||||||
import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts";
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
ResponsiveContainer,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
|
|
||||||
const data = [
|
interface OverviewProps {
|
||||||
{
|
data: { name: string; total: number }[];
|
||||||
name: "Jan",
|
}
|
||||||
total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
export function Overview({ data }: OverviewProps) {
|
||||||
{
|
// Create labels for the past 12 months ending with current
|
||||||
name: "Feb",
|
const monthFormatter = new Intl.DateTimeFormat("en-US", { month: "short" });
|
||||||
total: Math.floor(Math.random() * 5000) + 1000,
|
const now = new Date();
|
||||||
},
|
const past12Months = Array.from({ length: 12 }).map((_, i) => {
|
||||||
{
|
const d = new Date(now.getFullYear(), now.getMonth() - (11 - i), 1);
|
||||||
name: "Mar",
|
return monthFormatter.format(d);
|
||||||
total: Math.floor(Math.random() * 5000) + 1000,
|
});
|
||||||
},
|
|
||||||
{
|
const chartData = past12Months.map((month) => ({
|
||||||
name: "Apr",
|
name: month,
|
||||||
total: Math.floor(Math.random() * 5000) + 1000,
|
total: data.find((d) => d.name === month)?.total ?? 0,
|
||||||
},
|
}));
|
||||||
{
|
|
||||||
name: "May",
|
|
||||||
total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Jun",
|
|
||||||
total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Jul",
|
|
||||||
total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Aug",
|
|
||||||
total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Sep",
|
|
||||||
total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Oct",
|
|
||||||
total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Nov",
|
|
||||||
total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Dec",
|
|
||||||
total: Math.floor(Math.random() * 5000) + 1000,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Overview() {
|
|
||||||
return (
|
return (
|
||||||
<ResponsiveContainer width="100%" height={350}>
|
<ResponsiveContainer width="100%" height={350}>
|
||||||
<BarChart data={data}>
|
<BarChart data={chartData}>
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="name"
|
dataKey="name"
|
||||||
stroke="#888888"
|
stroke="#888888"
|
||||||
|
@ -1,83 +1,46 @@
|
|||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { usePurchases } from "@/hooks/usePurchases";
|
||||||
|
import { useUsers } from "@/hooks/useUsers";
|
||||||
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
|
import { formatCurrency } from "@/utils/format-currency";
|
||||||
|
import { useShop } from "@/hooks/useShop";
|
||||||
|
|
||||||
export function RecentSales() {
|
export function RecentSales() {
|
||||||
|
const { data: purchases } = usePurchases();
|
||||||
|
const {shop} = useShop();
|
||||||
|
const { data: users } = useUsers();
|
||||||
|
|
||||||
|
const recent = purchases
|
||||||
|
?.slice() // copy to avoid mutating original
|
||||||
|
.sort((a, b) => new Date(b.date_purchased).getTime() - new Date(a.date_purchased).getTime())
|
||||||
|
.slice(0, 5) // take 5 most recent
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="flex items-center gap-4">
|
{recent?.map((purchase) => {
|
||||||
<Avatar className="h-9 w-9">
|
const user = users?.find((u) => u.id === purchase.user_id);
|
||||||
<AvatarImage src="/avatars/01.png" alt="Avatar" />
|
const initials = user
|
||||||
<AvatarFallback>OM</AvatarFallback>
|
? `${user.first_name?.[0] ?? "U"}${user.last_name?.[0] ?? ""}`
|
||||||
</Avatar>
|
: "??";
|
||||||
<div className="flex flex-1 flex-wrap items-center justify-between">
|
const name = user
|
||||||
<div className="space-y-1">
|
? `${user.first_name ?? "Unknown"} ${user.last_name ?? ""}`
|
||||||
<p className="text-sm font-medium leading-none">Olivia Martin</p>
|
: "Unknown User";
|
||||||
<p className="text-sm text-muted-foreground">
|
const email = user?.email ?? "unknown@email.com";
|
||||||
olivia.martin@email.com
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="font-medium">+$1,999.00</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Avatar className="flex h-9 w-9 items-center justify-center space-y-0 border">
|
|
||||||
<AvatarImage src="/avatars/02.png" alt="Avatar" />
|
|
||||||
<AvatarFallback>JL</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex flex-1 flex-wrap items-center justify-between">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<p className="text-sm font-medium leading-none">Jackson Lee</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
jackson.lee@email.com
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="font-medium">+$39.00</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Avatar className="h-9 w-9">
|
|
||||||
<AvatarImage src="/avatars/03.png" alt="Avatar" />
|
|
||||||
<AvatarFallback>IN</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex flex-1 flex-wrap items-center justify-between">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<p className="text-sm font-medium leading-none">Isabella Nguyen</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
isabella.nguyen@email.com
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="font-medium">+$299.00</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
return (
|
||||||
<Avatar className="h-9 w-9">
|
<div className="flex items-center gap-4" key={purchase.id}>
|
||||||
<AvatarImage src="/avatars/04.png" alt="Avatar" />
|
<Avatar className="h-9 w-9">
|
||||||
<AvatarFallback>WK</AvatarFallback>
|
<AvatarFallback>{initials}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex flex-1 flex-wrap items-center justify-between">
|
<div className="flex flex-1 flex-wrap items-center justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm font-medium leading-none">William Kim</p>
|
<p className="text-sm font-medium leading-none">{name}</p>
|
||||||
<p className="text-sm text-muted-foreground">will@email.com</p>
|
<p className="text-sm text-muted-foreground">{email}</p>
|
||||||
|
</div>
|
||||||
|
<div className="font-medium">+{formatCurrency(purchase.total, shop?.currency ?? "USD")}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="font-medium">+$99.00</div>
|
);
|
||||||
</div>
|
})}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Avatar className="h-9 w-9">
|
|
||||||
<AvatarImage src="/avatars/05.png" alt="Avatar" />
|
|
||||||
<AvatarFallback>SD</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex flex-1 flex-wrap items-center justify-between">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<p className="text-sm font-medium leading-none">Sofia Davis</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
sofia.davis@email.com
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="font-medium">+$39.00</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import {
|
|||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
||||||
import { Header } from "@/components/layout/header";
|
import { Header } from "@/components/layout/header";
|
||||||
@ -13,11 +13,43 @@ import { Search } from "@/components/search";
|
|||||||
import { ThemeSwitch } from "@/components/theme-switch";
|
import { ThemeSwitch } from "@/components/theme-switch";
|
||||||
import { Overview } from "./components/overview";
|
import { Overview } from "./components/overview";
|
||||||
import { RecentSales } from "./components/recent-sales";
|
import { RecentSales } from "./components/recent-sales";
|
||||||
|
import { useUsers } from "@/hooks/useUsers";
|
||||||
|
import { usePurchases } from "@/hooks/usePurchases";
|
||||||
|
import { Purchase } from "@/api/mock/models";
|
||||||
|
import { formatCurrency } from "@/utils/format-currency";
|
||||||
|
import { useShop } from "@/hooks/useShop";
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
|
const { data: users } = useUsers();
|
||||||
|
const { shop } = useShop();
|
||||||
|
const { data: purchases } = usePurchases();
|
||||||
|
|
||||||
|
const totalRevenue = purchases?.reduce((sum, p) => sum + p.total, 0) ?? 0;
|
||||||
|
|
||||||
|
const totalCustomers =
|
||||||
|
users?.filter((u) => u.user_role === "customer").length ?? 0;
|
||||||
|
|
||||||
|
const totalSales = purchases?.length ?? 0;
|
||||||
|
|
||||||
|
const averageOrderValue = totalSales ? totalRevenue / totalSales : 0;
|
||||||
|
|
||||||
|
function getMonthlyRevenue(purchases: Purchase[]) {
|
||||||
|
|
||||||
|
const result: Record<string, number> = {};
|
||||||
|
purchases.forEach((p) => {
|
||||||
|
const date = new Date(p.date_purchased);
|
||||||
|
const month = date.toLocaleString("en-US", { month: "short" });
|
||||||
|
result[month] = (result[month] || 0) + p.total;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const revenueFormatted = formatCurrency(totalRevenue, shop?.currency ?? "USD");
|
||||||
|
|
||||||
|
const avgFormatted = formatCurrency(averageOrderValue, shop?.currency ?? "USD");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ===== Top Heading ===== */}
|
|
||||||
<Header>
|
<Header>
|
||||||
<Search />
|
<Search />
|
||||||
<div className="ml-auto flex items-center space-x-4">
|
<div className="ml-auto flex items-center space-x-4">
|
||||||
@ -26,7 +58,6 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
{/* ===== Main ===== */}
|
|
||||||
<Main>
|
<Main>
|
||||||
<div className="mb-2 flex items-center justify-between space-y-2">
|
<div className="mb-2 flex items-center justify-between space-y-2">
|
||||||
<h1 className="text-2xl font-bold tracking-tight">Dashboard</h1>
|
<h1 className="text-2xl font-bold tracking-tight">Dashboard</h1>
|
||||||
@ -34,10 +65,10 @@ export default function Dashboard() {
|
|||||||
<Tabs
|
<Tabs
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
defaultValue="overview"
|
defaultValue="overview"
|
||||||
className="space-y-4"
|
className="space-y-4">
|
||||||
>
|
|
||||||
<TabsContent value="overview" className="space-y-4">
|
<TabsContent value="overview" className="space-y-4">
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{/* Total Revenue */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">
|
<CardTitle className="text-sm font-medium">
|
||||||
@ -51,22 +82,23 @@ export default function Dashboard() {
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
className="h-4 w-4 text-muted-foreground"
|
className="h-4 w-4 text-muted-foreground">
|
||||||
>
|
|
||||||
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
|
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
|
||||||
</svg>
|
</svg>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">$45,231.89</div>
|
<div className="text-2xl font-bold">{revenueFormatted}</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
+20.1% from last month
|
Based on all time data
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Total Customers */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">
|
<CardTitle className="text-sm font-medium">
|
||||||
Subscriptions
|
Total Customers
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -76,20 +108,21 @@ export default function Dashboard() {
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
className="h-4 w-4 text-muted-foreground"
|
className="h-4 w-4 text-muted-foreground">
|
||||||
>
|
<circle cx="12" cy="12" r="10" />
|
||||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
<path d="M16 14a4 4 0 0 0-8 0" />
|
||||||
<circle cx="9" cy="7" r="4" />
|
<line x1="12" y1="6" x2="12" y2="6" />
|
||||||
<path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
|
|
||||||
</svg>
|
</svg>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">+2350</div>
|
<div className="text-2xl font-bold">{totalCustomers}</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
+180.1% from last month
|
All-time registered customers
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Sales */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">Sales</CardTitle>
|
<CardTitle className="text-sm font-medium">Sales</CardTitle>
|
||||||
@ -101,23 +134,24 @@ export default function Dashboard() {
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
className="h-4 w-4 text-muted-foreground"
|
className="h-4 w-4 text-muted-foreground">
|
||||||
>
|
|
||||||
<rect width="20" height="14" x="2" y="5" rx="2" />
|
<rect width="20" height="14" x="2" y="5" rx="2" />
|
||||||
<path d="M2 10h20" />
|
<path d="M2 10h20" />
|
||||||
</svg>
|
</svg>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">+12,234</div>
|
<div className="text-2xl font-bold">+{totalSales}</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
+19% from last month
|
Total purchases made
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Average Order Value */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">
|
<CardTitle className="text-sm font-medium">
|
||||||
Active Now
|
Average Order Value
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -127,33 +161,39 @@ export default function Dashboard() {
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
className="h-4 w-4 text-muted-foreground"
|
className="h-4 w-4 text-muted-foreground">
|
||||||
>
|
<path d="M3 3v18h18" />
|
||||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
<path d="M7 12h10M7 16h10M7 8h10" />
|
||||||
</svg>
|
</svg>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">+573</div>
|
<div className="text-2xl font-bold">{avgFormatted}</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
+201 since last hour
|
Across all sales
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Overview + Recent Sales */}
|
||||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-7">
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-7">
|
||||||
<Card className="col-span-1 lg:col-span-4">
|
<Card className="col-span-1 lg:col-span-4">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Overview</CardTitle>
|
<CardTitle>Overview</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pl-2">
|
<CardContent className="pl-2">
|
||||||
<Overview />
|
<Overview
|
||||||
|
data={Object.entries(
|
||||||
|
getMonthlyRevenue(purchases ?? [])
|
||||||
|
).map(([name, total]) => ({ name, total }))}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="col-span-1 lg:col-span-3">
|
<Card className="col-span-1 lg:col-span-3">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Recent Sales</CardTitle>
|
<CardTitle>Recent Sales</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
You made 265 sales this month.
|
You made {totalSales} sales overall.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
import { formatCurrency } from "@/utils/format-currency";
|
||||||
|
|
||||||
export function ProductList({
|
export function ProductList({
|
||||||
currency,
|
currency,
|
||||||
@ -47,7 +48,7 @@ export function ProductList({
|
|||||||
{product.stock_quantity} in stock
|
{product.stock_quantity} in stock
|
||||||
</span>
|
</span>
|
||||||
<span className="rounded bg-background/70 px-2 py-1 font-semibold">
|
<span className="rounded bg-background/70 px-2 py-1 font-semibold">
|
||||||
{product.price.toFixed(2)} {currency}
|
{formatCurrency(product.price, currency)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,7 @@ export const Route = createFileRoute("/(auth)/sign-in")({
|
|||||||
beforeLoad: async () => {
|
beforeLoad: async () => {
|
||||||
if (isLoggedIn()) {
|
if (isLoggedIn()) {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: "/dashboard/shop",
|
to: "/dashboard",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
6
frontend/src/utils/format-currency.ts
Normal file
6
frontend/src/utils/format-currency.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export function formatCurrency(amount: number, currency: string) {
|
||||||
|
return new Intl.NumberFormat("en-US", {
|
||||||
|
style: "currency",
|
||||||
|
currency,
|
||||||
|
}).format(amount);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user