Files
Krow-workspace/frontend-web/src/pages/ClientDashboard.jsx
2025-11-26 13:05:42 -05:00

1273 lines
55 KiB
JavaScript

import React, { useState, useMemo, useEffect } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Link, useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Calendar, Plus, Clock, DollarSign, MessageSquare, RefreshCw, ArrowRight, Users, Package, AlertTriangle, Zap, CheckCircle, AlertCircle, Store, BarChart3, Sparkles, Edit2, X, MapPin, Grid, List, Eye, ChevronRight, FileText, Send, Copy, Award, Crown, Settings, GripVertical, Minus, Check, RotateCcw, TrendingUp } from "lucide-react";
import { format, parseISO, isToday, startOfDay, isSameDay } from "date-fns";
import { useToast } from "@/components/ui/use-toast";
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
const COLORS = ['#0A39DF', '#6366f1', '#8b5cf6', '#a855f7', '#c026d3', '#d946ef'];
const convertTo12Hour = (time24) => {
if (!time24 || time24 === "—") return time24;
try {
const parts = time24.split(':');
if (!parts || parts.length < 2) return time24;
const hours = parseInt(parts[0], 10);
const minutes = parseInt(parts[1], 10);
if (isNaN(hours) || isNaN(minutes)) return time24;
const period = hours >= 12 ? 'PM' : 'AM';
const hours12 = hours % 12 || 12;
const minutesStr = minutes.toString().padStart(2, '0');
return `${hours12}:${minutesStr} ${period}`;
} catch (error) {
console.error('Error converting time:', error);
return time24;
}
};
const isDateToday = (dateValue) => {
if (!dateValue) return false;
try {
let dateObj;
if (typeof dateValue === 'string') {
dateObj = parseISO(dateValue);
if (isNaN(dateObj.getTime())) {
dateObj = new Date(dateValue);
}
} else if (dateValue instanceof Date) {
dateObj = dateValue;
} else {
return false;
}
if (isNaN(dateObj.getTime())) {
return false;
}
const today = startOfDay(new Date());
const compareDate = startOfDay(dateObj);
return isSameDay(today, compareDate);
} catch (error) {
console.error('Error checking if date is today:', error, dateValue);
return false;
}
};
const AVAILABLE_WIDGETS = [
{
id: 'order-now',
title: 'Order Now',
description: 'Create new standard order',
category: 'Quick Actions',
categoryColor: 'bg-blue-100 text-blue-700',
},
{
id: 'rapid-order',
title: 'RAPID Order',
description: 'Emergency urgent orders',
category: 'Quick Actions',
categoryColor: 'bg-red-100 text-red-700',
},
{
id: 'today-count',
title: "Today's Count",
description: 'Orders scheduled for today',
category: 'Quick Stats',
categoryColor: 'bg-blue-100 text-blue-700',
},
{
id: 'in-progress',
title: 'In Progress',
description: 'Active orders count',
category: 'Quick Stats',
categoryColor: 'bg-green-100 text-green-700',
},
{
id: 'needs-attention',
title: 'Needs Attention',
description: 'Incomplete orders',
category: 'Quick Stats',
categoryColor: 'bg-amber-100 text-amber-700',
},
{
id: 'todays-orders',
title: "Today's Orders Table",
description: 'Full table of today\'s orders',
category: 'Orders',
categoryColor: 'bg-green-100 text-green-700',
},
{
id: 'labor-summary',
title: 'Labor Summary',
description: 'Detailed breakdown of labor costs',
category: 'Analytics',
categoryColor: 'bg-purple-100 text-purple-700',
},
{
id: 'sales-analytics',
title: 'Sales Analytics',
description: 'Visual spending breakdown',
category: 'Analytics',
categoryColor: 'bg-purple-100 text-purple-700',
},
{
id: 'vendor-marketplace',
title: 'Find Vendors',
description: 'Browse vendor marketplace',
category: 'Actions',
categoryColor: 'bg-blue-100 text-blue-700',
},
{
id: 'invoices',
title: 'Invoices',
description: 'View and manage invoices',
category: 'Actions',
categoryColor: 'bg-blue-100 text-blue-700',
}
];
export default function ClientDashboard() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { toast } = useToast();
const [cancelDialog, setCancelDialog] = useState({ open: false, order: null });
const [showAllToday, setShowAllToday] = useState(false);
const [widgetOrder, setWidgetOrder] = useState(AVAILABLE_WIDGETS.map(w => w.id));
const [hiddenWidgets, setHiddenWidgets] = useState([]);
const [isCustomizing, setIsCustomizing] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const { data: user } = useQuery({
queryKey: ['current-user'],
queryFn: () => base44.auth.me(),
});
useEffect(() => {
if (user?.dashboard_layout_client?.widgets) {
setWidgetOrder(user.dashboard_layout_client.widgets);
setHiddenWidgets(user.dashboard_layout_client.hidden_widgets || []);
}
}, [user]);
const { data: events } = useQuery({
queryKey: ['client-events'],
queryFn: async () => {
const allEvents = await base44.entities.Event.list('-date');
const clientEvents = allEvents.filter(e =>
e.client_email === user?.email ||
e.business_name === user?.company_name ||
e.created_by === user?.email
);
if (clientEvents.length === 0) {
return allEvents.filter(e => e.status === "Completed");
}
return clientEvents;
},
initialData: [],
enabled: !!user
});
const { data: invoices = [] } = useQuery({
queryKey: ['client-invoices-reminder'],
queryFn: () => base44.entities.Invoice.list(),
initialData: [],
enabled: !!user
});
const saveLayoutMutation = useMutation({
mutationFn: async (layoutData) => {
await base44.auth.updateMe({
dashboard_layout_client: layoutData
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['current-user'] });
toast({
title: "✅ Layout Saved",
description: "Your dashboard layout has been updated",
});
setHasChanges(false);
setIsCustomizing(false);
},
});
const cancelOrderMutation = useMutation({
mutationFn: (orderId) => base44.entities.Event.update(orderId, { status: "Canceled" }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['client-events'] });
toast({
title: "✅ Order Canceled",
description: "Your order has been canceled successfully",
});
setCancelDialog({ open: false, order: null });
},
});
const todayOrders = useMemo(() => {
return events.filter(e => {
if (e.status === "Canceled") return false;
return isDateToday(e.date);
});
}, [events]);
const inProgressOrders = useMemo(() => {
return events.filter(e =>
e.status === "Active" || e.status === "Confirmed" || e.status === "In Progress"
);
}, [events]);
const needsAttention = useMemo(() => {
return events.filter(e => {
const assignedCount = e.assigned_staff?.length || 0;
const requestedCount = e.requested || 0;
const isFuture = new Date(e.date) > new Date();
return isFuture && requestedCount > 0 && assignedCount < requestedCount;
});
}, [events]);
const rapidOrders = useMemo(() => {
return events.filter(e => {
const eventDate = new Date(e.date);
const now = new Date();
const hoursUntil = (eventDate - now) / (1000 * 60 * 60);
return hoursUntil > 0 && hoursUntil <= 24 &&
(e.status === "Active" || e.status === "Confirmed" || e.status === "Pending");
});
}, [events]);
const currentMonth = new Date().getMonth();
const currentYear = new Date().getFullYear();
const thisMonthOrders = events.filter(e => {
const eventDate = new Date(e.date);
return eventDate.getMonth() === currentMonth &&
eventDate.getFullYear() === currentYear &&
e.status === "Completed";
});
const laborByPosition = useMemo(() => {
const summary = {};
thisMonthOrders.forEach(order => {
if (order.shifts_data) {
order.shifts_data.forEach(shift => {
shift.roles?.forEach(role => {
const position = role.service || 'Other';
const count = parseInt(role.count) || 0;
const startTime = role.start_time || '00:00';
const endTime = role.end_time || '00:00';
const [startHour, startMin] = startTime.split(':').map(Number);
const [endHour, endMin] = endTime.split(':').map(Number);
const hours = (endHour * 60 + endMin - startHour * 60 - startMin) / 60;
const totalHours = hours * count;
const rate = role.bill_rate || role.pay_rate || 25;
const cost = totalHours * rate;
if (!summary[position]) {
summary[position] = {
position,
headcount: 0,
regHours: 0,
otHours: 0,
dtHours: 0,
regAmt: 0,
otAmt: 0,
dtAmt: 0,
totalPay: 0
};
}
summary[position].headcount += count;
summary[position].regHours += totalHours;
summary[position].regAmt += cost;
summary[position].totalPay += cost;
});
});
}
});
return Object.values(summary).sort((a, b) => b.totalPay - a.totalPay);
}, [thisMonthOrders]);
const totalLaborCost = laborByPosition.reduce((sum, p) => sum + p.totalPay, 0);
const totalHours = laborByPosition.reduce((sum, p) => sum + p.regHours + p.otHours + p.dtHours, 0);
const hour = new Date().getHours();
const greeting = hour < 12 ? "Good morning" : hour < 18 ? "Good afternoon" : "Good evening";
const pieChartData = laborByPosition.slice(0, 5).map(item => ({
name: item.position,
value: item.totalPay
}));
const pendingInvoices = useMemo(() => {
return invoices.filter(inv =>
(inv.business_name === user?.company_name || inv.created_by === user?.email) &&
(inv.status === 'Open' || inv.status === 'Overdue' || inv.status === 'Pending')
);
}, [invoices, user]);
const completedOrders = useMemo(() => {
return events.filter(e => e.status === 'Completed');
}, [events]);
const avgFulfillmentRate = useMemo(() => {
if (completedOrders.length === 0) return 0;
return (completedOrders.reduce((sum, e) => {
const assigned = e.assigned_staff?.length || 0;
const requested = e.requested || 1;
return sum + (assigned / requested);
}, 0) / completedOrders.length * 100);
}, [completedOrders]);
const handleCancelOrder = (order) => {
setCancelDialog({ open: true, order });
};
const confirmCancel = () => {
if (cancelDialog.order) {
cancelOrderMutation.mutate(cancelDialog.order.id);
}
};
const handleDragEnd = (result) => {
if (!result.destination) return;
const items = Array.from(widgetOrder);
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
setWidgetOrder(items);
setHasChanges(true);
};
const handleRemoveWidget = (widgetId) => {
setHiddenWidgets([...hiddenWidgets, widgetId]);
setHasChanges(true);
};
const handleAddWidget = (widgetId) => {
setHiddenWidgets(hiddenWidgets.filter(id => id !== widgetId));
setHasChanges(true);
};
const handleSaveLayout = () => {
saveLayoutMutation.mutate({
widgets: widgetOrder,
hidden_widgets: hiddenWidgets,
layout_version: "2.0"
});
};
const handleCancelCustomize = () => {
if (user?.dashboard_layout_client) {
setWidgetOrder(user.dashboard_layout_client.widgets || AVAILABLE_WIDGETS.map(w => w.id));
setHiddenWidgets(user.dashboard_layout_client.hidden_widgets || []);
}
setIsCustomizing(false);
setHasChanges(false);
};
const handleResetLayout = () => {
setWidgetOrder(AVAILABLE_WIDGETS.map(w => w.id));
setHiddenWidgets([]);
setHasChanges(true);
};
const getOrderStatusBadge = (order) => {
const assignedCount = order.assigned_staff?.length || 0;
const requestedCount = order.requested || 0;
if (order.is_rapid === true) {
return <Badge className="bg-red-600 text-white text-xs border-0 px-2.5 py-0.5 font-bold">URGENT</Badge>;
}
if (order.status === "Canceled") {
return <Badge className="bg-slate-400 text-white text-xs border-0 px-2.5 py-0.5">Canceled</Badge>;
}
if (assignedCount >= requestedCount && requestedCount > 0) {
return <Badge className="bg-green-600 text-white text-xs border-0 px-2.5 py-0.5">Fully Staffed</Badge>;
}
if (order.status === "Active" || order.status === "Confirmed") {
return <Badge className="bg-blue-600 text-white text-xs border-0 px-2.5 py-0.5">Active</Badge>;
}
if (order.status === "Pending") {
return <Badge className="bg-orange-600 text-white text-xs border-0 px-2.5 py-0.5">Pending</Badge>;
}
if (order.status === "Completed") {
return <Badge className="bg-slate-600 text-white text-xs border-0 px-2.5 py-0.5">Completed</Badge>;
}
return <Badge className="bg-slate-400 text-white text-xs border-0 px-2.5 py-0.5">{order.status || "Draft"}</Badge>;
};
const getShiftTimes = (order) => {
if (order.shifts && order.shifts.length > 0) {
const shift = order.shifts[0];
if (shift.roles && shift.roles.length > 0) {
const role = shift.roles[0];
const startTime = convertTo12Hour(role.start_time) || "—";
const endTime = convertTo12Hour(role.end_time) || "—";
return { startTime, endTime };
}
}
return { startTime: "—", endTime: "—" };
};
const renderOrderNow = () => (
<Card
className="bg-gradient-to-br from-[#0A39DF] to-blue-700 border-0 shadow-md hover:shadow-lg transition-all cursor-pointer group h-[120px] flex flex-col"
onClick={() => !isCustomizing && navigate(createPageUrl("CreateEvent"))}
>
<CardContent className="p-5 flex-1 flex flex-col justify-between">
<div className="flex items-center justify-between">
<div className="w-10 h-10 bg-white/20 rounded-lg flex items-center justify-center flex-shrink-0">
<Plus className="w-5 h-5 text-white" />
</div>
<ArrowRight className="w-5 h-5 text-white/60 group-hover:text-white group-hover:translate-x-1 transition-all" />
</div>
<div>
<p className="text-xs text-blue-100 mb-1">Create New</p>
<p className="text-lg font-bold text-white">Order Now</p>
</div>
</CardContent>
</Card>
);
const renderRapidOrder = () => (
<Card
className="bg-gradient-to-br from-red-50 to-orange-50 border-2 border-red-200 shadow-md hover:shadow-lg transition-all cursor-pointer group h-[120px] flex flex-col"
onClick={() => !isCustomizing && navigate(createPageUrl("RapidOrder"))}
>
<CardContent className="p-5 flex-1 flex flex-col justify-between">
<div className="flex items-center justify-between">
<p className="text-[10px] font-bold text-red-600 uppercase tracking-wider">ORDER TYPE</p>
{rapidOrders.length > 0 ? (
<Badge className="bg-red-600 text-white text-xs font-bold border-0 px-2.5 py-0.5">
{rapidOrders.length} Urgent
</Badge>
) : (
<Badge className="bg-red-600 text-white text-xs font-bold border-0 px-2.5 py-0.5 animate-flash">
Urgent
</Badge>
)}
</div>
<div className="flex items-center gap-3">
<Zap className="w-8 h-8 text-red-600 flex-shrink-0" />
<div>
<p className="text-2xl font-bold text-red-600 leading-none">RAPID</p>
<p className="text-xs text-red-500 mt-0.5">Click to order</p>
</div>
</div>
</CardContent>
</Card>
);
const renderTodayCount = () => (
<Card className="bg-white border border-slate-200 shadow-sm hover:shadow-md transition-all h-[120px] flex flex-col">
<CardContent className="p-5 flex-1 flex flex-col justify-between">
<div className="w-10 h-10 bg-blue-50 rounded-lg flex items-center justify-center flex-shrink-0">
<Calendar className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-xs text-slate-600 mb-1">Today's Orders</p>
<div className="flex items-baseline gap-2">
<p className="text-3xl font-bold text-slate-900 leading-none">{todayOrders.length}</p>
<Badge className="bg-blue-100 text-blue-700 text-xs border-0">Active</Badge>
</div>
</div>
</CardContent>
</Card>
);
const renderInProgress = () => (
<Card className="bg-white border border-slate-200 shadow-sm hover:shadow-md transition-all h-[120px] flex flex-col">
<CardContent className="p-5 flex-1 flex flex-col justify-between">
<div className="w-10 h-10 bg-green-50 rounded-lg flex items-center justify-center flex-shrink-0">
<Package className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-xs text-slate-600 mb-1">In Progress</p>
<div className="flex items-baseline gap-2">
<p className="text-3xl font-bold text-slate-900 leading-none">{inProgressOrders.length}</p>
<Badge className="bg-green-100 text-green-700 text-xs border-0">Active</Badge>
</div>
</div>
</CardContent>
</Card>
);
const renderNeedsAttention = () => (
<Card className="bg-gradient-to-br from-orange-500 to-orange-600 border-0 shadow-md hover:shadow-lg transition-all cursor-pointer group h-[120px] flex flex-col">
<CardContent className="p-5 flex-1 flex flex-col justify-between">
<div className="flex items-center justify-between">
<div className="w-10 h-10 bg-white/20 rounded-lg flex items-center justify-center flex-shrink-0">
<AlertTriangle className="w-5 h-5 text-white" />
</div>
<ArrowRight className="w-5 h-5 text-white/60 group-hover:text-white transition-colors" />
</div>
<div>
<p className="text-xs text-orange-50 mb-1">Needs Attention</p>
<p className="text-3xl font-bold text-white leading-none">{needsAttention.length}</p>
</div>
</CardContent>
</Card>
);
const renderTodaysOrders = () => {
return (
<Card className="bg-white border border-slate-200 shadow-sm">
<CardHeader className="border-b border-slate-100 pb-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center">
<Calendar className="w-5 h-5 text-white" />
</div>
<div>
<CardTitle className="text-lg font-bold text-slate-900">Today's Orders</CardTitle>
<p className="text-xs text-slate-500 mt-0.5">
{format(new Date(), 'EEEE, MMMM d, yyyy')}
</p>
</div>
</div>
<div className="flex items-center gap-3">
<Badge className="bg-blue-600 text-white text-sm px-3 py-1.5 font-semibold">
{todayOrders.length} Active
</Badge>
{user?.preferred_vendor_name && !isCustomizing && (
<Button
variant="outline"
size="sm"
onClick={() => navigate(createPageUrl("VendorMarketplace"))}
className="border-2 border-blue-300 hover:bg-blue-50 text-blue-700 gap-2"
>
<Crown className="w-4 h-4 text-yellow-500" />
<span className="font-semibold">{user.preferred_vendor_name}</span>
<Badge className="bg-blue-600 text-white text-xs px-2 py-0 border-0">PRIMARY</Badge>
</Button>
)}
</div>
</div>
</CardHeader>
<CardContent className="p-0">
{todayOrders.length > 0 ? (
<>
<div className="overflow-hidden">
<table className="w-full">
<thead className="bg-slate-50 border-b border-slate-200">
<tr>
<th className="text-left py-3 px-4 text-xs font-bold text-slate-600 uppercase tracking-wider">BUSINESS</th>
<th className="text-left py-3 px-4 text-xs font-bold text-slate-600 uppercase tracking-wider">HUB</th>
<th className="text-left py-3 px-4 text-xs font-bold text-slate-600 uppercase tracking-wider">EVENT NAME</th>
<th className="text-left py-3 px-4 text-xs font-bold text-slate-600 uppercase tracking-wider">STATUS</th>
<th className="text-left py-3 px-4 text-xs font-bold text-slate-600 uppercase tracking-wider">DATE</th>
<th className="text-left py-3 px-4 text-xs font-bold text-slate-600 uppercase tracking-wider">TIME</th>
<th className="text-center py-3 px-4 text-xs font-bold text-slate-600 uppercase tracking-wider">REQUESTED</th>
<th className="text-center py-3 px-4 text-xs font-bold text-slate-600 uppercase tracking-wider">ASSIGNED</th>
<th className="text-center py-3 px-4 text-xs font-bold text-slate-600 uppercase tracking-wider">INVOICE</th>
<th className="text-center py-3 px-4 text-xs font-bold text-slate-600 uppercase tracking-wider">ACTIONS</th>
</tr>
</thead>
<tbody className="bg-white">
{(showAllToday ? todayOrders : todayOrders.slice(0, 4)).map((order, index) => {
const assignedCount = order.assigned_staff?.length || 0;
const requestedCount = order.requested || 0;
const { startTime, endTime } = getShiftTimes(order);
const displayOrders = showAllToday ? todayOrders : todayOrders.slice(0, 4);
const isLastRow = index === displayOrders.length - 1;
return (
<tr key={order.id} className={`hover:bg-slate-50/50 transition-colors ${!isLastRow ? 'border-b border-slate-100' : ''}`}>
<td className="py-3 px-4">
<span className="text-sm text-slate-900">
{order.business_name || "Primary Location"}
</span>
</td>
<td className="py-3 px-4">
<span className="text-sm text-slate-700">
{order.hub || order.event_location || "Main Hub"}
</span>
</td>
<td className="py-3 px-4">
<span className="text-sm font-medium text-slate-900">
{order.event_name}
</span>
</td>
<td className="py-3 px-4">
{getOrderStatusBadge(order)}
</td>
<td className="py-3 px-4">
<span className="text-sm text-slate-700">
{order.date ? format(new Date(order.date), 'MM/dd/yy') : "—"}
</span>
</td>
<td className="py-3 px-4">
<div className="flex items-center gap-1.5 text-sm text-slate-700">
<Clock className="w-3.5 h-3.5 text-slate-400" />
<span>{startTime}</span>
<span className="text-slate-400"></span>
<span>{endTime}</span>
</div>
</td>
<td className="py-3 px-4 text-center">
<span className="text-sm font-medium text-slate-900">
{requestedCount}
</span>
</td>
<td className="py-3 px-4 text-center">
<span className="text-sm font-medium text-slate-900">
{assignedCount}
</span>
</td>
<td className="py-3 px-4 text-center">
<span className="text-sm text-slate-400"></span>
</td>
<td className="py-3 px-4">
<div className="flex items-center justify-center gap-1">
<button
onClick={() => navigate(createPageUrl(`EventDetail?id=${order.id}`))}
className="inline-flex items-center justify-center w-8 h-8 hover:bg-slate-100 rounded-md transition-colors"
title="View Details"
>
<Eye className="w-4 h-4 text-slate-500" />
</button>
<button
onClick={() => navigate(createPageUrl(`EditEvent?id=${order.id}`))}
className="inline-flex items-center justify-center w-8 h-8 hover:bg-slate-100 rounded-md transition-colors"
title="Edit Order"
>
<Edit2 className="w-4 h-4 text-slate-500" />
</button>
<button
onClick={() => handleCancelOrder(order)}
className="inline-flex items-center justify-center w-8 h-8 hover:bg-red-50 rounded-md transition-colors"
title="Cancel Order"
>
<X className="w-4 h-4 text-red-500" />
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{todayOrders.length > 4 && (
<div className="border-t border-slate-100 py-3 px-4 text-center bg-white">
<Button
variant="ghost"
onClick={() => setShowAllToday(!showAllToday)}
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50 text-sm font-medium"
>
{showAllToday ? "Show Less" : `Show All ${todayOrders.length} Orders`}
<ChevronRight className={`w-4 h-4 ml-1 transition-transform ${showAllToday ? 'rotate-90' : ''}`} />
</Button>
</div>
)}
</>
) : (
<div className="py-16 px-8">
{/* Main Empty State */}
<div className="text-center mb-10">
<div className="relative inline-flex mb-6">
<div className="w-24 h-24 bg-gradient-to-br from-blue-100 via-blue-50 to-slate-100 rounded-3xl flex items-center justify-center shadow-lg">
<Calendar className="w-12 h-12 text-blue-600" />
</div>
<div className="absolute -top-1 -right-1 w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center shadow-md">
<Sparkles className="w-4 h-4 text-white" />
</div>
</div>
<h3 className="font-bold text-slate-900 text-2xl mb-3">No orders scheduled for today</h3>
<p className="text-slate-600 text-base mb-8 max-w-lg mx-auto leading-relaxed">
Stay ahead of your operations create a new order or schedule one for today to keep your workforce running smoothly
</p>
{/* Action Buttons */}
<div className="flex items-center justify-center gap-4">
<button
onClick={() => navigate(createPageUrl("CreateEvent"))}
className="group relative inline-flex items-center gap-2.5 px-10 py-4 bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-bold text-base rounded-2xl shadow-xl hover:shadow-2xl transition-all transform hover:scale-105 overflow-hidden"
>
<div className="absolute inset-0 bg-gradient-to-r from-blue-400 to-blue-600 opacity-0 group-hover:opacity-100 transition-opacity" />
<Plus className="w-5 h-5 relative z-10" />
<span className="relative z-10">Create Order</span>
</button>
<button
onClick={() => navigate(createPageUrl("RapidOrder"))}
className="group relative inline-flex items-center gap-2.5 px-10 py-4 bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-700 hover:to-orange-700 text-white font-bold text-base rounded-2xl shadow-xl hover:shadow-2xl transition-all transform hover:scale-105 overflow-hidden"
>
<div className="absolute inset-0 bg-gradient-to-r from-red-400 to-orange-500 opacity-0 group-hover:opacity-100 transition-opacity" />
<Zap className="w-5 h-5 relative z-10 animate-pulse" />
<span className="relative z-10">RAPID Order</span>
</button>
</div>
</div>
{/* Smart Insights Section */}
<div className="max-w-4xl mx-auto">
<div className="flex items-center gap-2 mb-4">
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-slate-200 to-transparent" />
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Smart Insights</p>
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-slate-200 to-transparent" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{/* Invoice Reminder */}
{pendingInvoices.length > 0 && (
<div
className="group relative bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 border-2 border-amber-300/50 rounded-2xl p-6 cursor-pointer hover:shadow-2xl hover:border-amber-400 transition-all duration-300 transform hover:-translate-y-1 overflow-hidden"
onClick={() => navigate(createPageUrl("Invoices"))}
>
<div className="absolute top-0 right-0 w-32 h-32 bg-amber-200/30 rounded-full blur-3xl -mr-16 -mt-16" />
<div className="relative z-10">
<div className="flex items-start gap-4 mb-4">
<div className="w-14 h-14 bg-gradient-to-br from-amber-500 to-orange-500 rounded-2xl flex items-center justify-center flex-shrink-0 shadow-lg group-hover:scale-110 transition-transform">
<FileText className="w-7 h-7 text-white" />
</div>
<div className="flex-1 min-w-0">
<h4 className="font-bold text-amber-900 text-base mb-1.5">Action Required</h4>
<p className="text-sm text-amber-800 leading-relaxed">
{pendingInvoices.length} invoice{pendingInvoices.length !== 1 ? 's' : ''} requiring attention
</p>
</div>
</div>
<div className="flex items-center justify-between pt-3 border-t border-amber-200/50">
<Badge className="bg-amber-600 text-white text-sm px-3 py-1.5 shadow-md">
${pendingInvoices.reduce((sum, inv) => sum + (inv.amount || 0), 0).toLocaleString()}
</Badge>
<div className="flex items-center gap-1 text-amber-700 font-semibold text-sm group-hover:gap-2 transition-all">
<span>View Invoices</span>
<ArrowRight className="w-4 h-4" />
</div>
</div>
</div>
</div>
)}
{/* Order Health Report */}
<div className="group relative bg-gradient-to-br from-green-50 via-emerald-50 to-teal-50 border-2 border-green-300/50 rounded-2xl p-6 hover:shadow-2xl hover:border-green-400 transition-all duration-300 transform hover:-translate-y-1 overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-green-200/30 rounded-full blur-3xl -mr-16 -mt-16" />
<div className="relative z-10">
<div className="flex items-start gap-4 mb-4">
<div className="w-14 h-14 bg-gradient-to-br from-green-500 to-emerald-500 rounded-2xl flex items-center justify-center flex-shrink-0 shadow-lg group-hover:scale-110 transition-transform">
<TrendingUp className="w-7 h-7 text-white" />
</div>
<div className="flex-1">
<h4 className="font-bold text-green-900 text-base mb-1.5">Order Health Report</h4>
<p className="text-sm text-green-800 leading-relaxed">
Your fulfillment performance this month
</p>
</div>
</div>
<div className="space-y-3 pt-3 border-t border-green-200/50">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-green-800">Fulfillment Rate</span>
<span className="text-2xl font-bold text-green-900">{Math.round(avgFulfillmentRate)}%</span>
</div>
<div className="relative w-full bg-green-200 rounded-full h-3 overflow-hidden shadow-inner">
<div
className="absolute inset-y-0 left-0 bg-gradient-to-r from-green-500 to-emerald-500 rounded-full transition-all duration-1000 shadow-lg"
style={{ width: `${avgFulfillmentRate}%` }}
/>
</div>
<div className="flex items-center justify-between pt-2">
<span className="text-sm font-medium text-green-800">Completed Orders</span>
<Badge className="bg-green-600 text-white text-sm px-3 py-1 shadow-md">
{completedOrders.length}
</Badge>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)}
</CardContent>
</Card>
);
};
const renderLaborSummary = () => (
<Card className="bg-white border border-slate-200 shadow-sm">
<CardHeader className="border-b border-slate-100 pb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-slate-900 rounded-lg flex items-center justify-center">
<DollarSign className="w-5 h-5 text-white" />
</div>
<div>
<CardTitle className="text-base font-bold text-slate-900">Labor Summary</CardTitle>
<p className="text-xs text-slate-500 mt-0.5">Detailed breakdown for November 2025</p>
</div>
</div>
</CardHeader>
<CardContent className="p-6">
<div className="flex items-center gap-8 mb-6">
<div>
<p className="text-xs text-slate-500 mb-1">Total Amount</p>
<p className="text-2xl font-bold text-slate-900">
${totalLaborCost.toFixed(2)}
</p>
</div>
<div>
<p className="text-xs text-slate-500 mb-1">Total Hours</p>
<p className="text-2xl font-bold text-slate-900">
{totalHours.toFixed(2)} hours
</p>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="border-b border-slate-200">
<tr className="bg-slate-50">
<th className="text-left py-2 px-3 text-[10px] font-bold text-slate-600 uppercase tracking-wider">Job Title</th>
<th className="text-center py-2 px-3 text-[10px] font-bold text-slate-600 uppercase tracking-wider">Reg Hrs</th>
<th className="text-center py-2 px-3 text-[10px] font-bold text-slate-600 uppercase tracking-wider">OT Hrs</th>
<th className="text-center py-2 px-3 text-[10px] font-bold text-slate-600 uppercase tracking-wider">DT Hrs</th>
<th className="text-center py-2 px-3 text-[10px] font-bold text-slate-600 uppercase tracking-wider">Reg Amt</th>
<th className="text-center py-2 px-3 text-[10px] font-bold text-slate-600 uppercase tracking-wider">OT Amt</th>
<th className="text-center py-2 px-3 text-[10px] font-bold text-slate-600 uppercase tracking-wider">DT Amt</th>
<th className="text-right py-2 px-3 text-[10px] font-bold text-slate-600 uppercase tracking-wider">Total Pay</th>
</tr>
</thead>
<tbody>
{laborByPosition.length > 0 ? (
laborByPosition.map((item) => (
<tr key={item.position} className="border-b border-slate-100">
<td className="py-3 px-3 text-sm text-slate-900 font-medium">{item.position}</td>
<td className="py-3 px-3 text-center text-sm text-slate-700">{item.regHours.toFixed(1)}</td>
<td className="py-3 px-3 text-center text-sm text-slate-700">{item.otHours.toFixed(1)}</td>
<td className="py-3 px-3 text-center text-sm text-slate-700">{item.dtHours.toFixed(1)}</td>
<td className="py-3 px-3 text-center text-sm text-slate-700">${item.regAmt.toFixed(0)}</td>
<td className="py-3 px-3 text-center text-sm text-slate-700">${item.otAmt.toFixed(0)}</td>
<td className="py-3 px-3 text-center text-sm text-slate-700">${item.dtAmt.toFixed(0)}</td>
<td className="py-3 px-3 text-right text-sm font-bold text-slate-900">${item.totalPay.toFixed(0)}</td>
</tr>
))
) : (
<tr>
<td colSpan="8" className="py-12 text-center text-sm text-slate-400">
No labor data for this month
</td>
</tr>
)}
</tbody>
</table>
</div>
</CardContent>
</Card>
);
const renderSalesAnalytics = () => (
<Card className="bg-white border border-slate-200 shadow-sm">
<CardHeader className="border-b border-slate-100 pb-4">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-slate-900 rounded-lg flex items-center justify-center">
<BarChart3 className="w-4 h-4 text-white" />
</div>
<CardTitle className="text-base font-bold text-slate-900">Sales analytics</CardTitle>
</div>
</CardHeader>
<CardContent className="p-6">
{pieChartData.length > 0 ? (
<>
<ResponsiveContainer width="100%" height={180}>
<PieChart>
<Pie
data={pieChartData}
cx="50%"
cy="50%"
innerRadius={50}
outerRadius={70}
paddingAngle={4}
dataKey="value"
>
{pieChartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip formatter={(value) => `$${Math.round(value).toLocaleString()}`} />
</PieChart>
</ResponsiveContainer>
<div className="mt-4 space-y-2.5">
{pieChartData.map((item, idx) => (
<div key={item.name} className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: COLORS[idx % COLORS.length] }}
/>
<span className="text-slate-700 font-medium">{item.name}</span>
</div>
<span className="font-bold text-slate-900">
${Math.round(item.value).toLocaleString()}
</span>
</div>
))}
</div>
</>
) : (
<div className="text-center py-8 text-slate-400 text-sm">
No sales data available
</div>
)}
</CardContent>
</Card>
);
const renderVendorMarketplace = () => (
<Link to={createPageUrl("VendorMarketplace")}>
<Card className="bg-gradient-to-br from-purple-600 to-purple-700 border-0 shadow-md hover:shadow-lg transition-all cursor-pointer group">
<CardContent className="p-5 text-center">
<div className="w-12 h-12 mx-auto mb-3 bg-white/20 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
<Sparkles className="w-6 h-6 text-white" />
</div>
<p className="text-white font-bold text-base">Find Vendors</p>
<p className="text-purple-100 text-xs mt-1">Browse marketplace</p>
</CardContent>
</Card>
</Link>
);
const renderInvoices = () => (
<Link to={createPageUrl("Invoices")}>
<Card className="bg-white border-2 border-slate-900 shadow-md hover:shadow-lg transition-all cursor-pointer group">
<CardContent className="p-5 text-center">
<div className="w-12 h-12 mx-auto mb-3 bg-slate-900 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
<FileText className="w-6 h-6 text-white" />
</div>
<p className="text-slate-900 font-bold text-base">Invoices</p>
</CardContent>
</Card>
</Link>
);
const renderWidget = (widgetId) => {
switch (widgetId) {
case 'order-now':
return renderOrderNow();
case 'rapid-order':
return renderRapidOrder();
case 'today-count':
return renderTodayCount();
case 'in-progress':
return renderInProgress();
case 'needs-attention':
return renderNeedsAttention();
case 'todays-orders':
return renderTodaysOrders();
case 'labor-summary':
return renderLaborSummary();
case 'sales-analytics':
return renderSalesAnalytics();
case 'vendor-marketplace':
return renderVendorMarketplace();
case 'invoices':
return renderInvoices();
default:
return null;
}
};
const visibleWidgetIds = widgetOrder.filter(id => !hiddenWidgets.includes(id));
const availableToAdd = AVAILABLE_WIDGETS.filter(w => hiddenWidgets.includes(w.id));
const quickActionWidgets = ['order-now', 'rapid-order', 'today-count', 'in-progress', 'needs-attention'];
const gridPairWidgets = ['vendor-marketplace', 'invoices'];
const visibleQuickActions = visibleWidgetIds.filter(id => quickActionWidgets.includes(id));
const visibleOtherWidgets = visibleWidgetIds.filter(id => !quickActionWidgets.includes(id));
// Group grid pair widgets together
const gridPairVisible = visibleOtherWidgets.filter(id => gridPairWidgets.includes(id));
const otherWidgetsFullWidth = visibleOtherWidgets.filter(id => !gridPairWidgets.includes(id));
return (
<div className="min-h-screen bg-slate-50 p-6">
<style>{`
@keyframes flash {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.animate-flash {
animation: flash 1.5s ease-in-out infinite;
}
`}</style>
<div className="max-w-[1800px] mx-auto space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-slate-900">
{greeting} <span className="text-slate-500 font-normal">here's what matters today</span>
</h1>
<div className="flex items-center gap-2">
{isCustomizing && hasChanges && (
<Badge className="bg-orange-500 text-white animate-pulse">
Unsaved Changes
</Badge>
)}
{isCustomizing ? (
<>
<Button
variant="outline"
onClick={handleResetLayout}
className="gap-2"
>
<RotateCcw className="w-4 h-4" />
Reset
</Button>
<Button
variant="outline"
onClick={handleCancelCustomize}
className="gap-2"
>
<X className="w-4 h-4" />
Cancel
</Button>
<Button
onClick={handleSaveLayout}
disabled={!hasChanges || saveLayoutMutation.isPending}
className="bg-blue-600 hover:bg-blue-700 gap-2"
>
<Check className="w-4 h-4" />
{saveLayoutMutation.isPending ? "Saving..." : "Save Layout"}
</Button>
</>
) : (
<Button
onClick={() => setIsCustomizing(true)}
variant="outline"
className="gap-2 border-2 border-blue-200 hover:bg-blue-50 text-blue-600 font-semibold"
>
<Settings className="w-4 h-4" />
Customize
</Button>
)}
</div>
</div>
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="quick-actions" direction="horizontal" isDropDisabled={!isCustomizing}>
{(provided) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className="grid grid-cols-5 gap-4"
>
{visibleQuickActions.map((widgetId, index) => (
<Draggable
key={widgetId}
draggableId={widgetId}
index={index}
isDragDisabled={!isCustomizing}
>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className="relative group"
>
{isCustomizing && (
<button
onClick={() => handleRemoveWidget(widgetId)}
className="absolute -top-2 -left-2 w-6 h-6 bg-slate-500 hover:bg-slate-600 rounded-full flex items-center justify-center shadow-lg z-20 transition-all hover:scale-110"
title="Remove widget"
>
<Minus className="w-3.5 h-3.5 text-white" />
</button>
)}
<div className={`transition-all ${
snapshot.isDragging
? 'opacity-60 scale-105 shadow-2xl ring-4 ring-blue-400'
: isCustomizing
? 'cursor-move hover:shadow-lg hover:scale-102 ring-2 ring-slate-300'
: ''
}`}>
{renderWidget(widgetId)}
</div>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
<Droppable droppableId="other-widgets" isDropDisabled={!isCustomizing}>
{(provided) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className="space-y-6"
>
{otherWidgetsFullWidth.map((widgetId, index) => (
<Draggable
key={widgetId}
draggableId={widgetId}
index={index}
isDragDisabled={!isCustomizing}
>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className="relative group"
>
{isCustomizing && (
<button
onClick={() => handleRemoveWidget(widgetId)}
className="absolute -top-2 -left-2 w-6 h-6 bg-slate-500 hover:bg-slate-600 rounded-full flex items-center justify-center shadow-lg z-20 transition-all hover:scale-110"
title="Remove widget"
>
<Minus className="w-3.5 h-3.5 text-white" />
</button>
)}
<div className={`transition-all ${
snapshot.isDragging
? 'opacity-60 scale-105 rotate-1 shadow-2xl ring-4 ring-blue-400'
: isCustomizing
? 'cursor-move hover:shadow-lg hover:scale-[1.01] ring-2 ring-slate-300'
: ''
}`}>
{renderWidget(widgetId)}
</div>
</div>
)}
</Draggable>
))}
{gridPairVisible.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{gridPairVisible.map((widgetId) => (
<div key={widgetId} className="relative group">
{isCustomizing && (
<button
onClick={() => handleRemoveWidget(widgetId)}
className="absolute -top-2 -left-2 w-6 h-6 bg-slate-500 hover:bg-slate-600 rounded-full flex items-center justify-center shadow-lg z-20 transition-all hover:scale-110"
title="Remove widget"
>
<Minus className="w-3.5 h-3.5 text-white" />
</button>
)}
<div className={isCustomizing ? 'ring-2 ring-slate-300 rounded-lg' : ''}>
{renderWidget(widgetId)}
</div>
</div>
))}
</div>
)}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
{isCustomizing && availableToAdd.length > 0 && (
<div className="border-3 border-dashed border-blue-300 bg-blue-50/30 rounded-2xl p-6">
<div className="flex items-center gap-3 mb-4">
<Plus className="w-6 h-6 text-blue-600" />
<h3 className="font-bold text-lg text-slate-900">Add Widgets</h3>
<Badge className="bg-blue-100 text-blue-700">{availableToAdd.length} available</Badge>
</div>
<div className="grid grid-cols-5 gap-3">
{availableToAdd.map((widget) => (
<button
key={widget.id}
onClick={() => handleAddWidget(widget.id)}
className="p-4 bg-white border-2 border-dashed border-slate-300 hover:border-blue-400 hover:bg-blue-50 rounded-xl transition-all group text-left h-[110px] flex flex-col"
>
<div className="flex items-center gap-2 mb-2">
<Plus className="w-5 h-5 text-blue-600 group-hover:scale-110 transition-transform" />
<p className="font-bold text-sm text-slate-900">{widget.title}</p>
</div>
<p className="text-xs text-slate-500 flex-1">{widget.description}</p>
<Badge className={`${widget.categoryColor} text-xs self-start`}>
{widget.category}
</Badge>
</button>
))}
</div>
</div>
)}
</div>
<Dialog open={cancelDialog.open} onOpenChange={(open) => setCancelDialog({ open, order: null })}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-red-600">
<AlertTriangle className="w-5 h-5" />
Cancel Order?
</DialogTitle>
<DialogDescription>
Are you sure you want to cancel this order? This action cannot be undone.
</DialogDescription>
</DialogHeader>
{cancelDialog.order && (
<div className="bg-slate-50 rounded-lg p-4 space-y-2">
<p className="font-bold text-slate-900">{cancelDialog.order.event_name}</p>
<div className="flex items-center gap-2 text-sm text-slate-600">
<Calendar className="w-4 h-4" />
{cancelDialog.order.date ? format(new Date(cancelDialog.order.date), "MMMM d, yyyy") : "—"}
</div>
<div className="flex items-center gap-2 text-sm text-slate-600">
<MapPin className="w-4 h-4" />
{cancelDialog.order.hub || cancelDialog.order.event_location}
</div>
</div>
)}
<DialogFooter>
<Button
variant="outline"
onClick={() => setCancelDialog({ open: false, order: null })}
>
Keep Order
</Button>
<Button
variant="destructive"
onClick={confirmCancel}
disabled={cancelOrderMutation.isPending}
>
{cancelOrderMutation.isPending ? "Canceling..." : "Yes, Cancel Order"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}