Dashboard fix
This commit is contained in:
1 parent
6903a8deb0
commit
7db93e9e8a
8 files changed
+164
-172
No files matched your search
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user