Files
ember-market-frontend/components/dashboard/revenue-widget.tsx
g 318927cd0c
Some checks failed
Build Frontend / build (push) Failing after 7s
Add modular dashboard widgets and layout editor
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.
2026-01-12 10:39:50 +00:00

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