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.
155 lines
6.9 KiB
TypeScript
155 lines
6.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 { Users, User, ArrowRight, DollarSign } from "lucide-react"
|
|
import { getCustomerInsightsWithStore, formatGBP } from "@/lib/api"
|
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
|
import Link from "next/link"
|
|
|
|
interface RecentCustomersWidgetProps {
|
|
settings?: {
|
|
itemCount?: number
|
|
showSpent?: boolean
|
|
}
|
|
}
|
|
|
|
interface Customer {
|
|
id: string
|
|
name: string
|
|
username?: string
|
|
orderCount: number
|
|
totalSpent: number
|
|
}
|
|
|
|
export default function RecentCustomersWidget({ settings }: RecentCustomersWidgetProps) {
|
|
const itemCount = settings?.itemCount || 5
|
|
const showSpent = settings?.showSpent !== false
|
|
const [customers, setCustomers] = useState<Customer[]>([])
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
const fetchCustomers = async () => {
|
|
try {
|
|
setIsLoading(true)
|
|
setError(null)
|
|
// The API returns topCustomers, but we'll use 'recent' sorting to show new engagement
|
|
const response = await getCustomerInsightsWithStore(1, itemCount, "recent")
|
|
|
|
const mappedCustomers = (response.topCustomers || []).map((c: any) => ({
|
|
id: c._id,
|
|
name: c.displayName || c.username || `Customer ${c._id.slice(-4)}`,
|
|
username: c.username,
|
|
orderCount: c.orderCount || 0,
|
|
totalSpent: c.totalSpent || 0
|
|
}))
|
|
|
|
setCustomers(mappedCustomers)
|
|
} catch (err) {
|
|
console.error("Error fetching customers:", err)
|
|
setError("Failed to load customer data")
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchCustomers()
|
|
}, [itemCount])
|
|
|
|
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 className="space-y-4">
|
|
{[1, 2, 3].map((i) => (
|
|
<div key={i} className="flex items-center gap-4">
|
|
<Skeleton className="h-10 w-10 rounded-full" />
|
|
<div className="space-y-2 flex-1">
|
|
<Skeleton className="h-4 w-3/4" />
|
|
<Skeleton className="h-3 w-1/4" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</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">
|
|
<Users className="h-5 w-5 text-indigo-500" />
|
|
Recent Customers
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Latest and newest connections
|
|
</CardDescription>
|
|
</div>
|
|
<Link href="/dashboard/customers">
|
|
<Button variant="ghost" size="sm" className="h-8 gap-1.5 text-xs">
|
|
View All
|
|
<ArrowRight className="h-3 w-3" />
|
|
</Button>
|
|
</Link>
|
|
</CardHeader>
|
|
<CardContent className="pt-4 flex-grow">
|
|
{error ? (
|
|
<div className="flex flex-col items-center justify-center py-10 text-center">
|
|
<User className="h-10 w-10 text-muted-foreground/20 mb-3" />
|
|
<p className="text-sm text-muted-foreground">{error}</p>
|
|
</div>
|
|
) : customers.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
<div className="h-12 w-12 rounded-full bg-indigo-500/10 flex items-center justify-center mb-4">
|
|
<Users className="h-6 w-6 text-indigo-500" />
|
|
</div>
|
|
<h3 className="font-medium">No customers yet</h3>
|
|
<p className="text-sm text-muted-foreground mt-1 max-w-xs">
|
|
This widget will populate once people start browsing and buying.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-1">
|
|
{customers.map((customer) => (
|
|
<div
|
|
key={customer.id}
|
|
className="flex items-center gap-4 p-3 rounded-xl hover:bg-muted/50 transition-colors group"
|
|
>
|
|
<Avatar className="h-10 w-10 border shadow-sm group-hover:scale-105 transition-transform">
|
|
<AvatarFallback className="bg-indigo-500/10 text-indigo-600 text-xs font-bold">
|
|
{customer.name.slice(0, 2).toUpperCase()}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex-grow min-w-0">
|
|
<h4 className="font-semibold text-sm truncate group-hover:text-primary transition-colors">{customer.name}</h4>
|
|
<div className="flex items-center gap-2 mt-0.5">
|
|
<span className="text-xs text-muted-foreground">
|
|
{customer.orderCount} order{customer.orderCount !== 1 ? 's' : ''}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{showSpent && (
|
|
<div className="text-right">
|
|
<div className="text-sm font-bold text-foreground">
|
|
{formatGBP(customer.totalSpent)}
|
|
</div>
|
|
<div className="text-[10px] text-muted-foreground font-medium uppercase tracking-tighter">Total Spent</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|