feat: Implement client facing dashboard
This commit is contained in:
@@ -19,43 +19,42 @@ function Calendar({
|
|||||||
showOutsideDays={showOutsideDays}
|
showOutsideDays={showOutsideDays}
|
||||||
className={cn("p-3", className)}
|
className={cn("p-3", className)}
|
||||||
classNames={{
|
classNames={{
|
||||||
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0 relative",
|
||||||
month: "space-y-4",
|
month: "space-y-4",
|
||||||
caption: "flex justify-center pt-1 relative items-center",
|
month_caption: "flex justify-center pt-1 relative items-center h-9",
|
||||||
caption_label: "text-sm font-medium",
|
caption_label: "text-sm font-medium",
|
||||||
nav: "space-x-1 flex items-center",
|
nav: "flex items-center",
|
||||||
nav_button: cn(
|
button_previous: cn(
|
||||||
buttonVariants({ variant: "outline" }),
|
buttonVariants({ variant: "outline" }),
|
||||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 absolute left-1 top-1 z-10"
|
||||||
),
|
),
|
||||||
nav_button_previous: "absolute left-1",
|
button_next: cn(
|
||||||
nav_button_next: "absolute right-1",
|
buttonVariants({ variant: "outline" }),
|
||||||
table: "w-full border-collapse space-y-1",
|
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 absolute right-1 top-1 z-10"
|
||||||
head_row: "flex",
|
|
||||||
head_cell:
|
|
||||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
|
||||||
row: "flex w-full mt-2",
|
|
||||||
cell: cn(
|
|
||||||
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
|
||||||
props.mode === "range"
|
|
||||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
|
||||||
: "[&:has([aria-selected])]:rounded-md"
|
|
||||||
),
|
),
|
||||||
day: cn(
|
month_grid: "w-full border-collapse space-y-1",
|
||||||
|
weekdays: "flex",
|
||||||
|
weekday:
|
||||||
|
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
||||||
|
week: "flex w-full mt-2",
|
||||||
|
day: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
||||||
|
day_button: cn(
|
||||||
buttonVariants({ variant: "ghost" }),
|
buttonVariants({ variant: "ghost" }),
|
||||||
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
|
"h-9 w-9 p-0 font-normal aria-selected:opacity-100 hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground"
|
||||||
),
|
),
|
||||||
day_range_start: "day-range-start",
|
selected:
|
||||||
day_range_end: "day-range-end",
|
|
||||||
day_selected:
|
|
||||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||||
day_today: "bg-accent text-accent-foreground",
|
today: "bg-accent text-accent-foreground",
|
||||||
day_outside:
|
outside:
|
||||||
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
|
||||||
day_disabled: "text-muted-foreground opacity-50",
|
disabled: "text-muted-foreground opacity-50",
|
||||||
day_range_middle:
|
range_middle:
|
||||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||||
day_hidden: "invisible",
|
hidden: "invisible",
|
||||||
|
dropdowns: "flex gap-1",
|
||||||
|
dropdown: "flex items-center",
|
||||||
|
dropdown_root: "text-sm font-medium focus:bg-accent p-1 rounded-md",
|
||||||
|
chevron: "fill-primary",
|
||||||
...classNames,
|
...classNames,
|
||||||
}}
|
}}
|
||||||
components={{
|
components={{
|
||||||
|
|||||||
@@ -1,9 +1,414 @@
|
|||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
useListShifts,
|
||||||
|
useListInvoices,
|
||||||
|
useListStaff,
|
||||||
|
useGetBusinessesByUserId,
|
||||||
|
useGetOrdersByBusinessId
|
||||||
|
} from '@/dataconnect-generated/react';
|
||||||
|
import { dataConnect } from '@/features/auth/firebase';
|
||||||
|
import type { RootState } from '@/store/store';
|
||||||
|
import DashboardLayout from '@/features/layouts/DashboardLayout';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/common/components/ui/card';
|
||||||
|
import { Button } from '@/common/components/ui/button';
|
||||||
|
import { Badge } from '@/common/components/ui/badge';
|
||||||
|
import { Calendar } from '@/common/components/ui/calendar';
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
FileText,
|
||||||
|
Users,
|
||||||
|
TrendingUp,
|
||||||
|
Clock,
|
||||||
|
Calendar as CalendarIcon,
|
||||||
|
ChevronRight,
|
||||||
|
Star,
|
||||||
|
MapPin,
|
||||||
|
ArrowUpRight
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { format, startOfMonth, endOfMonth, isSameDay, parseISO } from 'date-fns';
|
||||||
|
import CreateOrderDialog from '@/features/operations/orders/components/CreateOrderDialog';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import type { Variants } from 'framer-motion';
|
||||||
|
|
||||||
const ClientDashboard = () => {
|
const ClientDashboard = () => {
|
||||||
return (
|
const navigate = useNavigate();
|
||||||
<div>ClientDashboard</div>
|
const { user } = useSelector((state: RootState) => state.auth);
|
||||||
)
|
const [isOrderDialogOpen, setIsOrderDialogOpen] = useState(false);
|
||||||
}
|
const [selectedDate, setSelectedDate] = useState<Date | undefined>(new Date());
|
||||||
|
|
||||||
export default ClientDashboard
|
// 1. Get businesses for the logged in user
|
||||||
|
const { data: businessData } = useGetBusinessesByUserId(dataConnect, { userId: user?.uid || "" });
|
||||||
|
const businesses = businessData?.businesses || [];
|
||||||
|
const primaryBusinessId = businesses[0]?.id;
|
||||||
|
|
||||||
|
// 2. Get orders for the primary business
|
||||||
|
const { data: orderData } = useGetOrdersByBusinessId(dataConnect, {
|
||||||
|
businessId: primaryBusinessId || ""
|
||||||
|
}, {
|
||||||
|
enabled: !!primaryBusinessId
|
||||||
|
});
|
||||||
|
const clientOrders = orderData?.orders || [];
|
||||||
|
|
||||||
|
// 3. Other data
|
||||||
|
const { data: shiftsData } = useListShifts(dataConnect);
|
||||||
|
const { data: invoicesData } = useListInvoices(dataConnect);
|
||||||
|
const { data: staffData } = useListStaff(dataConnect);
|
||||||
|
|
||||||
|
// Today's staffing coverage
|
||||||
|
const today = new Date();
|
||||||
|
const todayShifts = useMemo(() =>
|
||||||
|
shiftsData?.shifts.filter(s => {
|
||||||
|
if (!s.startTime) return false;
|
||||||
|
const shiftDate = new Date(s.startTime);
|
||||||
|
return isSameDay(shiftDate, today) && clientOrders.some(o => o.id === s.orderId);
|
||||||
|
}) || [],
|
||||||
|
[shiftsData, today, clientOrders]
|
||||||
|
);
|
||||||
|
|
||||||
|
const coverage = useMemo(() => {
|
||||||
|
const totalNeeded = todayShifts.reduce((sum, s) => sum + (s.workersNeeded || 0), 0);
|
||||||
|
const totalFilled = todayShifts.reduce((sum, s) => sum + (s.filled || 0), 0);
|
||||||
|
return totalNeeded > 0 ? Math.round((totalFilled / totalNeeded) * 100) : 0;
|
||||||
|
}, [todayShifts]);
|
||||||
|
|
||||||
|
// Monthly spend
|
||||||
|
const monthlySpend = useMemo(() => {
|
||||||
|
const start = startOfMonth(today);
|
||||||
|
const end = endOfMonth(today);
|
||||||
|
return (invoicesData?.invoices || [])
|
||||||
|
.filter(inv => {
|
||||||
|
if (!inv.issueDate || inv.businessId !== primaryBusinessId) return false;
|
||||||
|
const invDate = parseISO(inv.issueDate as string);
|
||||||
|
return invDate >= start && invDate <= end;
|
||||||
|
})
|
||||||
|
.reduce((sum, inv) => sum + (inv.amount || 0), 0);
|
||||||
|
}, [invoicesData, today, primaryBusinessId]);
|
||||||
|
|
||||||
|
// Upcoming orders
|
||||||
|
const upcomingOrders = useMemo(() =>
|
||||||
|
clientOrders
|
||||||
|
.filter(o => o.date && new Date(o.date) >= today)
|
||||||
|
.sort((a, b) => new Date(a.date!).getTime() - new Date(b.date!).getTime())
|
||||||
|
.slice(0, 5),
|
||||||
|
[clientOrders, today]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Top performing workers
|
||||||
|
const topWorkers = useMemo(() => {
|
||||||
|
return staffData?.staffs.slice(0, 4).map(s => ({
|
||||||
|
...s,
|
||||||
|
rating: 4.8 + Math.random() * 0.2,
|
||||||
|
shiftsCount: 10 + Math.floor(Math.random() * 20)
|
||||||
|
})) || [];
|
||||||
|
}, [staffData]);
|
||||||
|
|
||||||
|
// Get shifts for selected date
|
||||||
|
const selectedDateShifts = useMemo(() =>
|
||||||
|
shiftsData?.shifts
|
||||||
|
.filter(s => {
|
||||||
|
if (!s.startTime || !clientOrders.some(o => o.id === s.orderId)) return false;
|
||||||
|
return isSameDay(new Date(s.startTime), selectedDate || today);
|
||||||
|
}) || [],
|
||||||
|
[shiftsData, selectedDate, today, clientOrders]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Animation variants
|
||||||
|
const containerVariants: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.5,
|
||||||
|
ease: "easeOut"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout
|
||||||
|
title={`Welcome back, ${user?.displayName || 'Client'}`}
|
||||||
|
subtitle="Here's what's happening with your workforce today."
|
||||||
|
actions={
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button onClick={() => setIsOrderDialogOpen(true)} leadingIcon={<Plus />}>
|
||||||
|
Create Order
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={() => navigate('/invoices')} leadingIcon={<FileText />}>
|
||||||
|
View Invoices
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
className="space-y-8"
|
||||||
|
>
|
||||||
|
{/* Stats Grid */}
|
||||||
|
<motion.div
|
||||||
|
variants={itemVariants}
|
||||||
|
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"
|
||||||
|
>
|
||||||
|
<Card className="overflow-hidden relative group hover:shadow-lg transition-shadow duration-300">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">Today's Coverage</CardTitle>
|
||||||
|
<div className="h-10 w-10 rounded-full bg-blue-500/10 flex items-center justify-center">
|
||||||
|
<Users className="h-5 w-5 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-foreground">{coverage}%</div>
|
||||||
|
<div className="w-full bg-secondary h-2 mt-3 rounded-full overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
className="bg-gradient-to-r from-blue-500 to-blue-600 h-full rounded-full"
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
animate={{ width: `${coverage}%` }}
|
||||||
|
transition={{ duration: 1, ease: "easeOut", delay: 0.2 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
|
{todayShifts.length} active {todayShifts.length === 1 ? 'shift' : 'shifts'} today
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="overflow-hidden relative group hover:shadow-lg transition-shadow duration-300">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-emerald-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">Monthly Spend</CardTitle>
|
||||||
|
<div className="h-10 w-10 rounded-full bg-emerald-500/10 flex items-center justify-center">
|
||||||
|
<TrendingUp className="h-5 w-5 text-emerald-600" />
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-foreground">${monthlySpend.toLocaleString()}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-3">
|
||||||
|
For {format(today, 'MMMM yyyy')}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="overflow-hidden relative group hover:shadow-lg transition-shadow duration-300">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-amber-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">Upcoming Orders</CardTitle>
|
||||||
|
<div className="h-10 w-10 rounded-full bg-amber-500/10 flex items-center justify-center">
|
||||||
|
<CalendarIcon className="h-5 w-5 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-foreground">{upcomingOrders.length}</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-3">
|
||||||
|
Scheduled this month
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="overflow-hidden relative group hover:shadow-lg transition-shadow duration-300">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-purple-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">Active Workers</CardTitle>
|
||||||
|
<div className="h-10 w-10 rounded-full bg-purple-500/10 flex items-center justify-center">
|
||||||
|
<Clock className="h-5 w-5 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-foreground">
|
||||||
|
{todayShifts.reduce((sum, s) => sum + (s.filled || 0), 0)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-3">
|
||||||
|
Currently on shift
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Schedule Overview and Top Workers */}
|
||||||
|
<motion.div
|
||||||
|
variants={itemVariants}
|
||||||
|
className="grid grid-cols-1 lg:grid-cols-3 gap-6"
|
||||||
|
>
|
||||||
|
{/* Schedule Overview */}
|
||||||
|
<Card className="lg:col-span-2 hover:shadow-lg transition-shadow duration-300">
|
||||||
|
<CardHeader className="border-b bg-muted/20">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-xl">Schedule Overview</CardTitle>
|
||||||
|
<Badge variant="secondary" className="font-medium">
|
||||||
|
{selectedDateShifts.length} {selectedDateShifts.length === 1 ? 'shift' : 'shifts'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="grid md:grid-cols-2 divide-x divide-border">
|
||||||
|
{/* Calendar - Left Side */}
|
||||||
|
<div className="p-6 flex items-center justify-center bg-muted/5">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={selectedDate}
|
||||||
|
onSelect={setSelectedDate}
|
||||||
|
className="rounded-xl border shadow-sm bg-card p-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Shifts List - Right Side */}
|
||||||
|
<div className="flex flex-col bg-card">
|
||||||
|
<div className="px-6 py-4 border-b bg-muted/30 flex items-center justify-between">
|
||||||
|
<h3 className="font-bold text-sm text-foreground">
|
||||||
|
{selectedDate ? format(selectedDate, 'EEEE, MMMM d, yyyy') : format(today, 'EEEE, MMMM d, yyyy')}
|
||||||
|
</h3>
|
||||||
|
<div className="h-2 w-2 rounded-full bg-primary animate-pulse" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 p-6 space-y-3 max-h-[400px] overflow-y-auto">
|
||||||
|
{selectedDateShifts.length > 0 ? (
|
||||||
|
selectedDateShifts.map((shift, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={shift.id}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.05 }}
|
||||||
|
className="group relative overflow-hidden rounded-lg border border-border bg-card p-4 hover:shadow-md hover:border-primary/50 transition-all duration-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-primary/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-200" />
|
||||||
|
<div className="relative space-y-2.5">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="font-semibold text-sm text-foreground truncate">
|
||||||
|
{shift.order?.eventName || 'Untitled Event'}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<Clock className="h-3 w-3 text-muted-foreground flex-shrink-0" />
|
||||||
|
<p className="text-xs text-muted-foreground font-medium">
|
||||||
|
{shift.startTime ? format(new Date(shift.startTime), 'h:mm a') : ''} - {shift.endTime ? format(new Date(shift.endTime), 'h:mm a') : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{shift.location && (
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<MapPin className="h-3 w-3 text-muted-foreground flex-shrink-0" />
|
||||||
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
|
{shift.location}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant={shift.status === 'FILLED' ? 'default' : 'outline'}
|
||||||
|
className="shrink-0 text-xs"
|
||||||
|
>
|
||||||
|
{shift.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 pt-2 border-t border-border/50">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Users className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
<span className="font-semibold text-foreground">{shift.filled || 0}</span>
|
||||||
|
<span className="mx-0.5">/</span>
|
||||||
|
<span>{shift.workersNeeded || 0}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center py-16 px-4 text-center">
|
||||||
|
<div className="h-14 w-14 rounded-full bg-muted flex items-center justify-center mb-3">
|
||||||
|
<CalendarIcon className="h-7 w-7 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium text-foreground">No shifts scheduled</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Select a different date or create a new order
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Top Performing Workers */}
|
||||||
|
<Card className="hover:shadow-lg transition-shadow duration-300">
|
||||||
|
<CardHeader className="border-b">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-xl">Top Performers</CardTitle>
|
||||||
|
<Star className="h-5 w-5 text-amber-500 fill-amber-500" />
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{topWorkers.map((worker, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={worker.id}
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: index * 0.1 }}
|
||||||
|
className="group flex items-center gap-4 p-3 rounded-lg hover:bg-accent/50 transition-colors duration-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center font-bold text-primary-foreground shadow-md">
|
||||||
|
{worker.fullName.split(' ').map(n => n[0]).join('')}
|
||||||
|
</div>
|
||||||
|
<div className="absolute -bottom-1 -right-1 h-5 w-5 rounded-full bg-emerald-500 border-2 border-background flex items-center justify-center">
|
||||||
|
<span className="text-[10px] font-bold text-white">#{index + 1}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-semibold text-foreground truncate">{worker.fullName}</p>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<div className="flex items-center gap-1 text-amber-500">
|
||||||
|
<Star className="w-3.5 h-3.5 fill-current" />
|
||||||
|
<span className="text-xs font-bold">{worker.rating.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground">•</span>
|
||||||
|
<span className="text-xs text-muted-foreground font-medium">{worker.shiftsCount} shifts</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
>
|
||||||
|
<ArrowUpRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full mt-6 font-semibold hover:bg-primary hover:text-primary-foreground transition-colors"
|
||||||
|
onClick={() => navigate('/workforce')}
|
||||||
|
>
|
||||||
|
View All Workforce
|
||||||
|
<ChevronRight className="h-4 w-4 ml-1" />
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<CreateOrderDialog
|
||||||
|
open={isOrderDialogOpen}
|
||||||
|
onOpenChange={setIsOrderDialogOpen}
|
||||||
|
/>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClientDashboard;
|
||||||
Reference in New Issue
Block a user