Dashboard fix

This commit is contained in:
Thastertyn 2025-04-22 10:09:40 +02:00
parent 6903a8deb0
commit 7db93e9e8a
8 changed files with 165 additions and 173 deletions

View File

@ -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); // 011 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,
); );
} }

View File

@ -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",

View File

@ -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"

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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",
}); });
} }
}, },

View File

@ -0,0 +1,6 @@
export function formatCurrency(amount: number, currency: string) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency,
}).format(amount);
}