Some checks failed
Build Frontend / build (push) Failing after 7s
Introduces a modular dashboard system with draggable, configurable widgets including revenue, low stock, recent customers, and pending chats. Adds a dashboard editor for layout customization, widget visibility, and settings. Refactors dashboard content to use the new widget system and improves UI consistency and interactivity.
191 lines
8.9 KiB
TypeScript
191 lines
8.9 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Skeleton } from "@/components/ui/skeleton"
|
|
import { TrendingUp, DollarSign, RefreshCcw } from "lucide-react"
|
|
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"
|
|
import { getRevenueTrendsWithStore, type RevenueData, formatGBP } from "@/lib/api"
|
|
import { useToast } from "@/components/ui/use-toast"
|
|
|
|
interface RevenueWidgetProps {
|
|
settings?: {
|
|
days?: number
|
|
showComparison?: boolean
|
|
}
|
|
}
|
|
|
|
interface ChartDataPoint {
|
|
date: string
|
|
revenue: number
|
|
orders: number
|
|
formattedDate: string
|
|
}
|
|
|
|
export default function RevenueWidget({ settings }: RevenueWidgetProps) {
|
|
const days = settings?.days || 7
|
|
const [data, setData] = useState<RevenueData[]>([])
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const { toast } = useToast()
|
|
|
|
const fetchRevenueData = async () => {
|
|
try {
|
|
setIsLoading(true)
|
|
setError(null)
|
|
const response = await getRevenueTrendsWithStore(days.toString())
|
|
setData(Array.isArray(response) ? response : [])
|
|
} catch (err) {
|
|
console.error("Error fetching revenue data:", err)
|
|
setError("Failed to load revenue data")
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchRevenueData()
|
|
}, [days])
|
|
|
|
const chartData: ChartDataPoint[] = data.map(item => {
|
|
const date = new Date(Date.UTC(item._id.year, item._id.month - 1, item._id.day))
|
|
return {
|
|
date: date.toISOString().split('T')[0],
|
|
revenue: item.revenue || 0,
|
|
orders: item.orders || 0,
|
|
formattedDate: date.toLocaleDateString('en-GB', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
timeZone: 'UTC'
|
|
})
|
|
}
|
|
})
|
|
|
|
// Summary stats
|
|
const totalRevenue = data.reduce((sum, item) => sum + (item.revenue || 0), 0)
|
|
const totalOrders = data.reduce((sum, item) => sum + (item.orders || 0), 0)
|
|
|
|
const CustomTooltip = ({ active, payload }: any) => {
|
|
if (active && payload && payload.length) {
|
|
const data = payload[0].payload
|
|
return (
|
|
<div className="bg-background/95 backdrop-blur-md p-3 border border-border shadow-xl rounded-xl">
|
|
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1">{data.formattedDate}</p>
|
|
<div className="space-y-1">
|
|
<p className="text-sm font-bold text-primary">
|
|
Revenue: {formatGBP(data.revenue)}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
Orders: <span className="font-medium text-foreground">{data.orders}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
return null
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Card className="border-border/40 bg-background/50 backdrop-blur-sm">
|
|
<CardHeader className="pb-2">
|
|
<Skeleton className="h-5 w-32 mb-1" />
|
|
<Skeleton className="h-4 w-48" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Skeleton className="h-[250px] w-full rounded-xl" />
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Card className="border-border/40 bg-background/50 backdrop-blur-sm overflow-hidden flex flex-col h-full">
|
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
<div className="space-y-1">
|
|
<CardTitle className="flex items-center gap-2">
|
|
<TrendingUp className="h-5 w-5 text-primary" />
|
|
Revenue Insights
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Performance over the last {days} days
|
|
</CardDescription>
|
|
</div>
|
|
{error && (
|
|
<Button variant="ghost" size="icon" onClick={fetchRevenueData} className="h-8 w-8">
|
|
<RefreshCcw className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</CardHeader>
|
|
<CardContent className="flex-grow pt-4">
|
|
{error ? (
|
|
<div className="h-[300px] flex flex-col items-center justify-center text-center p-6">
|
|
<DollarSign className="h-12 w-12 text-muted-foreground/20 mb-4" />
|
|
<p className="text-sm text-muted-foreground mb-4">Could not load revenue trends</p>
|
|
<Button variant="outline" size="sm" onClick={fetchRevenueData}>Retry</Button>
|
|
</div>
|
|
) : chartData.length === 0 ? (
|
|
<div className="h-[300px] flex flex-col items-center justify-center text-center p-6">
|
|
<DollarSign className="h-12 w-12 text-muted-foreground/20 mb-4" />
|
|
<h3 className="text-lg font-medium">No revenue data</h3>
|
|
<p className="text-sm text-muted-foreground max-w-xs mt-2">
|
|
Start making sales to see your revenue trends here.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-6">
|
|
<div className="h-[300px] w-full">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<AreaChart data={chartData} margin={{ top: 0, right: 0, left: -20, bottom: 0 }}>
|
|
<defs>
|
|
<linearGradient id="colorRevenueWidget" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor="hsl(var(--primary))" stopOpacity={0.3} />
|
|
<stop offset="95%" stopColor="hsl(var(--primary))" stopOpacity={0} />
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" opacity={0.5} />
|
|
<XAxis
|
|
dataKey="formattedDate"
|
|
tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }}
|
|
axisLine={false}
|
|
tickLine={false}
|
|
minTickGap={30}
|
|
/>
|
|
<YAxis
|
|
tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }}
|
|
axisLine={false}
|
|
tickLine={false}
|
|
tickFormatter={(value) => `£${value >= 1000 ? (value / 1000).toFixed(1) + 'k' : value}`}
|
|
/>
|
|
<Tooltip content={<CustomTooltip />} />
|
|
<Area
|
|
type="monotone"
|
|
dataKey="revenue"
|
|
stroke="hsl(var(--primary))"
|
|
fillOpacity={1}
|
|
fill="url(#colorRevenueWidget)"
|
|
strokeWidth={2.5}
|
|
activeDot={{ r: 6, strokeWidth: 0, fill: "hsl(var(--primary))" }}
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4 pb-2">
|
|
<div className="p-4 rounded-2xl bg-primary/5 border border-primary/10">
|
|
<div className="text-sm text-muted-foreground font-medium mb-1">Total Revenue</div>
|
|
<div className="text-2xl font-bold text-primary">{formatGBP(totalRevenue)}</div>
|
|
</div>
|
|
<div className="p-4 rounded-2xl bg-muted/50 border border-border">
|
|
<div className="text-sm text-muted-foreground font-medium mb-1">Total Orders</div>
|
|
<div className="text-2xl font-bold">{totalOrders}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|