Some checks failed
Build Frontend / build (push) Failing after 7s
Replaces imports from 'components/ui' with 'components/common' across the app and dashboard pages, and updates model and API imports to use new paths under 'lib'. Removes redundant authentication checks from several dashboard pages. Adds new dashboard components and utility files, and reorganizes hooks and services into the 'lib' directory for improved structure.
185 lines
8.3 KiB
TypeScript
185 lines
8.3 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card"
|
|
import { Button } from "@/components/common/button"
|
|
import { Skeleton } from "@/components/common/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/common/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 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">
|
|
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" />
|
|
Order 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 order 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 order data</h3>
|
|
<p className="text-sm text-muted-foreground max-w-xs mt-2">
|
|
Start making sales to see your order 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="colorOrderWidget" 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}`}
|
|
/>
|
|
<Tooltip content={<CustomTooltip />} />
|
|
<Area
|
|
type="monotone"
|
|
dataKey="orders"
|
|
stroke="hsl(var(--primary))"
|
|
fillOpacity={1}
|
|
fill="url(#colorOrderWidget)"
|
|
strokeWidth={2.5}
|
|
activeDot={{ r: 6, strokeWidth: 0, fill: "hsl(var(--primary))" }}
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
|
|
<div className="flex justify-center pb-2">
|
|
<div className="p-4 rounded-2xl bg-muted/50 border border-border w-full max-w-sm text-center">
|
|
<div className="text-sm text-muted-foreground font-medium mb-1">Total Orders</div>
|
|
<div className="text-3xl font-bold">{totalOrders}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
|