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++) {
|
||||
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); // 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(
|
||||
{
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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">
|
||||
<Avatar className="h-9 w-9">
|
||||
<AvatarImage src="/avatars/04.png" alt="Avatar" />
|
||||
<AvatarFallback>WK</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>
|
||||
return (
|
||||
<div className="flex items-center gap-4" key={purchase.id}>
|
||||
<Avatar className="h-9 w-9">
|
||||
<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">{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 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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -7,7 +7,7 @@ export const Route = createFileRoute("/(auth)/sign-in")({
|
||||
beforeLoad: async () => {
|
||||
if (isLoggedIn()) {
|
||||
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