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++) {
const product = products[Math.floor(Math.random() * products.length)];
const quantity = Math.floor(Math.random() * 4) + 1;
entries.push({
@ -26,13 +25,15 @@ export async function createSamplePurchase() {
if (entries.length === 0) throw new Error("No suitable product variants found");
const total = await entries.reduce(async (accP, entry) => {
const acc = await accP;
const product = await mockDB.products.get(entry.product_id);
return acc + ((product?.price ?? 0) * entry.quantity);
}, Promise.resolve(0));
let total = 0;
for (const entry of entries) {
const product = await mockDB.products.get(entry.product_id);
if (product) {
total += product.price * entry.quantity;
}
}
// Randomly apply a valid coupon
const coupons = await mockDB.coupons.toArray();
const validCoupons = coupons.filter(
(c) => new Date(c.valid_due) > new Date()
@ -46,18 +47,27 @@ export async function createSamplePurchase() {
? Math.max(0, total - chosenCoupon.discount_amount)
: 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(
{
user_id: user.id,
used_coupon_id: chosenCoupon?.id ?? null,
date_purchased: new Date().toISOString(),
date_purchased: isoDate,
total: discountedTotal,
},
entries,
);
console.log(
`Created mock purchase${chosenCoupon ? " with coupon" : ""}:`,
`Created mock purchase${chosenCoupon ? " with coupon" : ""} on ${date.toDateString()}:`,
purchase,
);
}

View File

@ -2,7 +2,7 @@ import
{
IconBuildingStore,
IconClipboardCheckFilled,
IconCoin, IconPackage,
IconCoin, IconLayoutDashboard, IconPackage,
IconPalette,
IconSettings,
IconTag,
@ -16,11 +16,11 @@ export const sidebarData: SidebarData = {
{
title: "Dashboard",
items: [
// {
// title: "Dashboard",
// url: "/dashboard",
// icon: IconLayoutDashboard,
// },
{
title: "Dashboard",
url: "/dashboard",
icon: IconLayoutDashboard,
},
{
title: "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 = [
{
name: "Jan",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Feb",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Mar",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Apr",
total: Math.floor(Math.random() * 5000) + 1000,
},
{
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,
},
];
interface OverviewProps {
data: { name: string; total: number }[];
}
export function Overview({ data }: OverviewProps) {
// Create labels for the past 12 months ending with current
const monthFormatter = new Intl.DateTimeFormat("en-US", { month: "short" });
const now = new Date();
const past12Months = Array.from({ length: 12 }).map((_, i) => {
const d = new Date(now.getFullYear(), now.getMonth() - (11 - i), 1);
return monthFormatter.format(d);
});
const chartData = past12Months.map((month) => ({
name: month,
total: data.find((d) => d.name === month)?.total ?? 0,
}));
export function Overview() {
return (
<ResponsiveContainer width="100%" height={350}>
<BarChart data={data}>
<BarChart data={chartData}>
<XAxis
dataKey="name"
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() {
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 (
<div className="space-y-8">
<div className="flex items-center gap-4">
<Avatar className="h-9 w-9">
<AvatarImage src="/avatars/01.png" alt="Avatar" />
<AvatarFallback>OM</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">Olivia Martin</p>
<p className="text-sm text-muted-foreground">
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>
{recent?.map((purchase) => {
const user = users?.find((u) => u.id === purchase.user_id);
const initials = user
? `${user.first_name?.[0] ?? "U"}${user.last_name?.[0] ?? ""}`
: "??";
const name = user
? `${user.first_name ?? "Unknown"} ${user.last_name ?? ""}`
: "Unknown User";
const email = user?.email ?? "unknown@email.com";
<div className="flex items-center gap-4">
return (
<div className="flex items-center gap-4" key={purchase.id}>
<Avatar className="h-9 w-9">
<AvatarImage src="/avatars/04.png" alt="Avatar" />
<AvatarFallback>WK</AvatarFallback>
<AvatarFallback>{initials}</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">William Kim</p>
<p className="text-sm text-muted-foreground">will@email.com</p>
</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>
<p className="text-sm font-medium leading-none">{name}</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>
);
}

View File

@ -3,7 +3,7 @@ import {
CardContent,
CardDescription,
CardHeader,
CardTitle,
CardTitle
} from "@/components/ui/card";
import { Tabs, TabsContent } from "@/components/ui/tabs";
import { Header } from "@/components/layout/header";
@ -13,11 +13,43 @@ import { Search } from "@/components/search";
import { ThemeSwitch } from "@/components/theme-switch";
import { Overview } from "./components/overview";
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() {
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 (
<>
{/* ===== Top Heading ===== */}
<Header>
<Search />
<div className="ml-auto flex items-center space-x-4">
@ -26,7 +58,6 @@ export default function Dashboard() {
</div>
</Header>
{/* ===== Main ===== */}
<Main>
<div className="mb-2 flex items-center justify-between space-y-2">
<h1 className="text-2xl font-bold tracking-tight">Dashboard</h1>
@ -34,10 +65,10 @@ export default function Dashboard() {
<Tabs
orientation="vertical"
defaultValue="overview"
className="space-y-4"
>
className="space-y-4">
<TabsContent value="overview" className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{/* Total Revenue */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
@ -51,22 +82,23 @@ export default function Dashboard() {
strokeLinecap="round"
strokeLinejoin="round"
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" />
</svg>
</CardHeader>
<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">
+20.1% from last month
Based on all time data
</p>
</CardContent>
</Card>
{/* Total Customers */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Subscriptions
Total Customers
</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -76,20 +108,21 @@ export default function Dashboard() {
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
className="h-4 w-4 text-muted-foreground">
<circle cx="12" cy="12" r="10" />
<path d="M16 14a4 4 0 0 0-8 0" />
<line x1="12" y1="6" x2="12" y2="6" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+2350</div>
<div className="text-2xl font-bold">{totalCustomers}</div>
<p className="text-xs text-muted-foreground">
+180.1% from last month
All-time registered customers
</p>
</CardContent>
</Card>
{/* Sales */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Sales</CardTitle>
@ -101,23 +134,24 @@ export default function Dashboard() {
strokeLinecap="round"
strokeLinejoin="round"
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" />
<path d="M2 10h20" />
</svg>
</CardHeader>
<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">
+19% from last month
Total purchases made
</p>
</CardContent>
</Card>
{/* Average Order Value */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Active Now
Average Order Value
</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -127,33 +161,39 @@ export default function Dashboard() {
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
className="h-4 w-4 text-muted-foreground">
<path d="M3 3v18h18" />
<path d="M7 12h10M7 16h10M7 8h10" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+573</div>
<div className="text-2xl font-bold">{avgFormatted}</div>
<p className="text-xs text-muted-foreground">
+201 since last hour
Across all sales
</p>
</CardContent>
</Card>
</div>
{/* Overview + Recent Sales */}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-7">
<Card className="col-span-1 lg:col-span-4">
<CardHeader>
<CardTitle>Overview</CardTitle>
</CardHeader>
<CardContent className="pl-2">
<Overview />
<Overview
data={Object.entries(
getMonthlyRevenue(purchases ?? [])
).map(([name, total]) => ({ name, total }))}
/>
</CardContent>
</Card>
<Card className="col-span-1 lg:col-span-3">
<CardHeader>
<CardTitle>Recent Sales</CardTitle>
<CardDescription>
You made 265 sales this month.
You made {totalSales} sales overall.
</CardDescription>
</CardHeader>
<CardContent>

View File

@ -6,6 +6,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { formatCurrency } from "@/utils/format-currency";
export function ProductList({
currency,
@ -47,7 +48,7 @@ export function ProductList({
{product.stock_quantity} in stock
</span>
<span className="rounded bg-background/70 px-2 py-1 font-semibold">
{product.price.toFixed(2)} {currency}
{formatCurrency(product.price, currency)}
</span>
</div>
</div>

View File

@ -7,7 +7,7 @@ export const Route = createFileRoute("/(auth)/sign-in")({
beforeLoad: async () => {
if (isLoggedIn()) {
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);
}