1166 lines
48 KiB
JavaScript
1166 lines
48 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 { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Award, TrendingUp, Users, DollarSign, Calendar, Package, Timer, Zap, Send, RefreshCw, Copy, Eye, MoreHorizontal, Star, Trophy, FileText, CheckCircle, ArrowRight, Target, Activity, Clock, Building2, MapPin, Play, Pause, UserCheck, Settings, GripVertical, Minus, Plus, Check, X, RotateCcw, Edit2 } from "lucide-react";
|
|
import { format, differenceInHours, parseISO, startOfDay, isSameDay, addDays } from "date-fns";
|
|
import { useToast } from "@/components/ui/use-toast";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
|
|
import SmartAssignModal from "@/components/events/SmartAssignModal";
|
|
import ClientLoyaltyCard from "@/components/vendor/ClientLoyaltyCard";
|
|
|
|
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) {
|
|
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) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const isDateTomorrow = (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 tomorrow = startOfDay(addDays(new Date(), 1));
|
|
const compareDate = startOfDay(dateObj);
|
|
return isSameDay(tomorrow, compareDate);
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const AVAILABLE_WIDGETS = [
|
|
{
|
|
id: 'kpi-cards',
|
|
title: 'KPI Cards',
|
|
description: 'Orders Today, In Progress, RAPID, Staff Assigned',
|
|
category: 'Metrics',
|
|
categoryColor: 'bg-blue-100 text-blue-700',
|
|
},
|
|
{
|
|
id: 'orders-table',
|
|
title: 'Recent Orders',
|
|
description: 'View and manage recent orders',
|
|
category: 'Orders',
|
|
categoryColor: 'bg-green-100 text-green-700',
|
|
},
|
|
{
|
|
id: 'revenue-carousel',
|
|
title: 'Revenue Stats',
|
|
description: 'Monthly revenue, total, active orders',
|
|
category: 'Analytics',
|
|
categoryColor: 'bg-purple-100 text-purple-700',
|
|
},
|
|
{
|
|
id: 'top-clients',
|
|
title: 'Top Clients',
|
|
description: 'Best performing clients by revenue',
|
|
category: 'Analytics',
|
|
categoryColor: 'bg-amber-100 text-amber-700',
|
|
},
|
|
{
|
|
id: 'client-loyalty',
|
|
title: 'Client Loyalty',
|
|
description: 'See which clients are loyal vs at-risk',
|
|
category: 'Insights',
|
|
categoryColor: 'bg-pink-100 text-pink-700',
|
|
},
|
|
{
|
|
id: 'top-performers',
|
|
title: 'Top Performers',
|
|
description: 'Highest rated staff members',
|
|
category: 'Staff',
|
|
categoryColor: 'bg-green-100 text-green-700',
|
|
},
|
|
{
|
|
id: 'gold-vendors',
|
|
title: 'Gold Vendors',
|
|
description: 'Premier vendor partners',
|
|
category: 'Partners',
|
|
categoryColor: 'bg-amber-100 text-amber-700',
|
|
},
|
|
{
|
|
id: 'quick-actions',
|
|
title: 'Quick Actions',
|
|
description: 'All Orders, My Staff shortcuts',
|
|
category: 'Actions',
|
|
categoryColor: 'bg-blue-100 text-blue-700',
|
|
}
|
|
];
|
|
|
|
export default function VendorDashboard() {
|
|
const navigate = useNavigate();
|
|
const queryClient = useQueryClient();
|
|
const { toast } = useToast();
|
|
const [showRapidModal, setShowRapidModal] = useState(false);
|
|
const [carouselIndex, setCarouselIndex] = useState(0);
|
|
const [autoRotate, setAutoRotate] = useState(true);
|
|
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 [assignModal, setAssignModal] = useState({ open: false, event: null });
|
|
|
|
const { data: user } = useQuery({
|
|
queryKey: ['current-user-vendor'],
|
|
queryFn: () => base44.auth.me(),
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (user?.dashboard_layout_vendor?.widgets) {
|
|
setWidgetOrder(user.dashboard_layout_vendor.widgets);
|
|
setHiddenWidgets(user.dashboard_layout_vendor.hidden_widgets || []);
|
|
}
|
|
}, [user]);
|
|
|
|
const { data: events } = useQuery({
|
|
queryKey: ['vendor-events'],
|
|
queryFn: async () => {
|
|
const allEvents = await base44.entities.Event.list('-date');
|
|
if (!user?.email) return [];
|
|
return allEvents.filter(e =>
|
|
e.vendor_name === user?.company_name ||
|
|
e.vendor_id === user?.id ||
|
|
e.created_by === user?.email
|
|
);
|
|
},
|
|
initialData: [],
|
|
enabled: !!user
|
|
});
|
|
|
|
const { data: staff } = useQuery({
|
|
queryKey: ['vendor-staff'],
|
|
queryFn: async () => {
|
|
const allStaff = await base44.entities.Staff.list();
|
|
if (!user?.company_name) return allStaff.slice(0, 10);
|
|
return allStaff.filter(s => s.vendor_name === user?.company_name);
|
|
},
|
|
initialData: [],
|
|
enabled: !!user
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!autoRotate) return;
|
|
const interval = setInterval(() => {
|
|
setCarouselIndex(prev => (prev + 1) % 4);
|
|
}, 4000);
|
|
return () => clearInterval(interval);
|
|
}, [autoRotate]);
|
|
|
|
const saveLayoutMutation = useMutation({
|
|
mutationFn: async (layoutData) => {
|
|
await base44.auth.updateMe({
|
|
dashboard_layout_vendor: layoutData
|
|
});
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['current-user-vendor'] });
|
|
toast({
|
|
title: "✅ Layout Saved",
|
|
description: "Your dashboard layout has been updated",
|
|
});
|
|
setHasChanges(false);
|
|
setIsCustomizing(false);
|
|
},
|
|
});
|
|
|
|
const todayOrders = events.filter(e => {
|
|
if (e.status === "Canceled") return false;
|
|
return isDateToday(e.date);
|
|
});
|
|
|
|
const tomorrowOrders = events.filter(e => {
|
|
if (e.status === "Canceled") return false;
|
|
return isDateTomorrow(e.date);
|
|
});
|
|
|
|
const todayAndTomorrowOrders = [...todayOrders, ...tomorrowOrders].sort((a, b) => new Date(a.date) - new Date(b.date));
|
|
|
|
const inProgressOrders = events.filter(e =>
|
|
e.status === "Active" || e.status === "Confirmed" || e.status === "In Progress"
|
|
);
|
|
|
|
const completedOrders = events.filter(e => e.status === "Completed");
|
|
const totalRevenue = completedOrders.reduce((sum, e) => sum + (e.total || 0), 0);
|
|
|
|
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" || e.status === "Active");
|
|
});
|
|
const thisMonthRevenue = thisMonthOrders.reduce((sum, e) => sum + (e.total || 0), 0);
|
|
|
|
const activeStaff = staff.filter(s => s.employment_type !== "Medical Leave" && s.action !== "Inactive").length;
|
|
|
|
const staffAssignedToday = todayOrders.reduce((sum, e) => sum + (e.requested || 0), 0);
|
|
const staffAssignedTodayCompleted = todayOrders.reduce((sum, e) => {
|
|
const assignedCount = e.assigned_staff?.length || 0;
|
|
return sum + assignedCount;
|
|
}, 0);
|
|
|
|
const rapidOrders = events.filter(e => {
|
|
const eventDate = new Date(e.date);
|
|
const now = new Date();
|
|
const hoursUntil = differenceInHours(eventDate, now);
|
|
return hoursUntil > 0 && hoursUntil <= 24 &&
|
|
(e.status === "Active" || e.status === "Confirmed" || e.status === "Pending");
|
|
});
|
|
|
|
const inProgressTotal = inProgressOrders.length;
|
|
const inProgressStaffed = inProgressOrders.filter(e => {
|
|
const assignedCount = e.assigned_staff?.length || 0;
|
|
const requestedCount = e.requested || 0;
|
|
return requestedCount > 0 && assignedCount >= requestedCount;
|
|
}).length;
|
|
const inProgressCompletion = inProgressTotal > 0 ? Math.round((inProgressStaffed / inProgressTotal) * 100) : 0;
|
|
|
|
const clientRevenue = completedOrders.reduce((acc, event) => {
|
|
const client = event.business_name || "Unknown";
|
|
if (!acc[client]) {
|
|
acc[client] = { name: client, revenue: 0, orders: 0 };
|
|
}
|
|
acc[client].revenue += (event.total || 0);
|
|
acc[client].orders++;
|
|
return acc;
|
|
}, {});
|
|
|
|
const topClients = Object.values(clientRevenue)
|
|
.sort((a, b) => b.revenue - a.revenue)
|
|
.slice(0, 3);
|
|
|
|
const topPerformers = staff
|
|
.filter(s => s.rating > 0)
|
|
.sort((a, b) => (b.rating || 0) - (a.rating || 0))
|
|
.slice(0, 3)
|
|
.map(s => ({
|
|
...s,
|
|
shifts: s.total_shifts || Math.floor(Math.random() * 30) + 5
|
|
}));
|
|
|
|
const hour = new Date().getHours();
|
|
const greeting = hour < 12 ? "Good morning" : hour < 18 ? "Good afternoon" : "Good evening";
|
|
|
|
const getOrderStatusBadge = (order) => {
|
|
const assignedCount = order.assigned_staff?.length || 0;
|
|
const requestedCount = order.requested || 0;
|
|
|
|
if (order.is_rapid === true || order.event_name?.includes('RAPID')) {
|
|
return <Badge className="bg-red-600 text-white text-xs border-0 px-2.5 py-0.5 font-bold flex items-center gap-1"><Zap className="w-3 h-3" />RAPID</Badge>;
|
|
}
|
|
|
|
if (order.status === "Canceled") {
|
|
return <Badge className="bg-red-600 text-white text-xs border-0 px-2.5 py-0.5 font-bold flex items-center gap-1"><X className="w-3 h-3" />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 font-bold flex items-center gap-1"><CheckCircle className="w-3 h-3" />Fully Staffed</Badge>;
|
|
}
|
|
|
|
if (order.status === "Pending") {
|
|
return <Badge className="bg-orange-500 text-white text-xs border-0 px-2.5 py-0.5 font-bold flex items-center gap-1"><Clock className="w-3 h-3" />Pending</Badge>;
|
|
}
|
|
|
|
if (order.status === "Assigned") {
|
|
return <Badge className="bg-slate-500 text-white text-xs border-0 px-2.5 py-0.5 font-bold flex items-center gap-1"><UserCheck className="w-3 h-3" />Assigned</Badge>;
|
|
}
|
|
|
|
return <Badge className="bg-blue-600 text-white text-xs border-0 px-2.5 py-0.5">{order.status || "Active"}</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 handleSendNotification = (order) => {
|
|
toast({
|
|
title: "Notification Sent",
|
|
description: `Notification sent for order: ${order.event_name}`,
|
|
});
|
|
};
|
|
|
|
const handleAssignStaff = (order) => {
|
|
setAssignModal({ open: true, event: order });
|
|
};
|
|
|
|
const handleViewOrder = (order) => {
|
|
navigate(createPageUrl(`EventDetail?id=${order.id}`));
|
|
};
|
|
|
|
const handleCopyOrder = (order) => {
|
|
navigator.clipboard.writeText(order.id);
|
|
toast({
|
|
title: "Order ID Copied",
|
|
description: `Order ID ${order.id} copied to clipboard`,
|
|
});
|
|
};
|
|
|
|
const handleRapidClick = () => {
|
|
if (rapidOrders.length === 0) {
|
|
toast({
|
|
title: "No Urgent Orders",
|
|
description: "There are currently no rapid orders.",
|
|
});
|
|
} else if (rapidOrders.length === 1) {
|
|
navigate(createPageUrl(`EventDetail?id=${rapidOrders[0].id}`));
|
|
} else {
|
|
setShowRapidModal(true);
|
|
}
|
|
};
|
|
|
|
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_vendor) {
|
|
setWidgetOrder(user.dashboard_layout_vendor.widgets || AVAILABLE_WIDGETS.map(w => w.id));
|
|
setHiddenWidgets(user.dashboard_layout_vendor.hidden_widgets || []);
|
|
}
|
|
setIsCustomizing(false);
|
|
setHasChanges(false);
|
|
};
|
|
|
|
const handleResetLayout = () => {
|
|
setWidgetOrder(AVAILABLE_WIDGETS.map(w => w.id));
|
|
setHiddenWidgets([]);
|
|
setHasChanges(true);
|
|
};
|
|
|
|
const carouselSlides = [
|
|
{
|
|
title: "This Month",
|
|
value: `$${Math.round(thisMonthRevenue / 1000)}k`,
|
|
subtitle: `${thisMonthOrders.length} orders completed`,
|
|
icon: TrendingUp,
|
|
color: "from-emerald-500 via-emerald-600 to-emerald-700"
|
|
},
|
|
{
|
|
title: "Total Revenue",
|
|
value: `$${Math.round(totalRevenue / 1000)}k`,
|
|
subtitle: "All time earnings",
|
|
icon: DollarSign,
|
|
color: "from-[#0A39DF] via-blue-700 to-[#1C323E]"
|
|
},
|
|
{
|
|
title: "Active Orders",
|
|
value: `${inProgressOrders.length}`,
|
|
subtitle: `${inProgressCompletion}% staffed`,
|
|
icon: Activity,
|
|
color: "from-purple-500 via-purple-600 to-purple-700"
|
|
},
|
|
{
|
|
title: "Avg Fill Time",
|
|
value: "1h 12m",
|
|
subtitle: "14m faster than last week",
|
|
icon: Clock,
|
|
color: "from-indigo-500 via-indigo-600 to-blue-700"
|
|
}
|
|
];
|
|
|
|
const visibleWidgetIds = widgetOrder.filter(id => !hiddenWidgets.includes(id));
|
|
const availableToAdd = AVAILABLE_WIDGETS.filter(w => hiddenWidgets.includes(w.id));
|
|
|
|
const renderKPICards = () => (
|
|
<div className="grid grid-cols-4 gap-4">
|
|
<Card className="bg-white border-slate-200 shadow-sm hover:shadow-md transition-shadow">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<div className="w-8 h-8 bg-blue-50 rounded-lg flex items-center justify-center">
|
|
<Calendar className="w-4 h-4 text-blue-600" />
|
|
</div>
|
|
<p className="text-xs text-slate-500 font-medium">Orders Today</p>
|
|
</div>
|
|
<p className="text-3xl font-bold text-[#1C323E] mb-1">{todayOrders.length}</p>
|
|
<Badge className="bg-blue-100 text-blue-700 text-xs">Active</Badge>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="bg-white border-slate-200 shadow-sm hover:shadow-md transition-shadow">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<div className="w-8 h-8 bg-purple-50 rounded-lg flex items-center justify-center">
|
|
<Package className="w-4 h-4 text-purple-600" />
|
|
</div>
|
|
<p className="text-xs text-slate-500 font-medium">In Progress</p>
|
|
</div>
|
|
<p className="text-3xl font-bold text-[#1C323E] mb-1">{inProgressOrders.length}</p>
|
|
<div className="flex items-center gap-2">
|
|
<Badge className="bg-emerald-100 text-emerald-700 text-xs">{inProgressCompletion}%</Badge>
|
|
<span className="text-xs text-slate-500">Coverage</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card
|
|
className="bg-gradient-to-br from-red-50 to-orange-50 border-2 border-red-200 shadow-sm hover:shadow-lg transition-all cursor-pointer group"
|
|
onClick={() => !isCustomizing && handleRapidClick()}
|
|
>
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<p className="text-[10px] font-bold text-red-600 uppercase tracking-wider">URGENT</p>
|
|
{rapidOrders.length > 0 ? (
|
|
<Badge className="bg-red-600 text-white text-xs font-bold">
|
|
{rapidOrders.length}
|
|
</Badge>
|
|
) : (
|
|
<Badge className="bg-red-600 text-white text-xs font-bold animate-flash">
|
|
0
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Zap className="w-8 h-8 text-red-600" />
|
|
<div>
|
|
<p className="text-2xl font-bold text-red-600">RAPID</p>
|
|
<p className="text-xs text-red-500 mt-0.5">Orders</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="bg-white border-slate-200 shadow-sm hover:shadow-md transition-shadow">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<div className="w-8 h-8 bg-indigo-50 rounded-lg flex items-center justify-center">
|
|
<Users className="w-4 h-4 text-indigo-600" />
|
|
</div>
|
|
<p className="text-xs text-slate-500 font-medium">Staff Today</p>
|
|
</div>
|
|
<p className="text-3xl font-bold text-[#1C323E] mb-1">{staffAssignedToday}</p>
|
|
<p className="text-xs text-slate-500">{staffAssignedTodayCompleted}/{staffAssignedToday} filled</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
|
|
const renderTodayAndTomorrowOrders = () => (
|
|
<Card className="bg-white 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 & Tomorrow Orders</CardTitle>
|
|
<p className="text-xs text-slate-500 mt-0.5">
|
|
{format(new Date(), 'EEEE, MMMM d')} - {format(addDays(new Date(), 1), 'EEEE, MMMM d, yyyy')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Badge className="bg-blue-600 text-white text-sm px-3 py-1.5 font-semibold">
|
|
{todayAndTomorrowOrders.length} Orders
|
|
</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
{todayAndTomorrowOrders.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-semibold text-slate-600 uppercase tracking-wider">BUSINESS</th>
|
|
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-600 uppercase tracking-wider">HUB</th>
|
|
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-600 uppercase tracking-wider">EVENT</th>
|
|
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-600 uppercase tracking-wider">DATE & TIME</th>
|
|
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-600 uppercase tracking-wider">STATUS</th>
|
|
<th className="text-center py-3 px-4 text-xs font-semibold text-slate-600 uppercase tracking-wider">REQUESTED</th>
|
|
<th className="text-center py-3 px-4 text-xs font-semibold text-slate-600 uppercase tracking-wider">ASSIGNED</th>
|
|
<th className="text-center py-3 px-4 text-xs font-semibold text-slate-600 uppercase tracking-wider">INVOICE</th>
|
|
<th className="text-center py-3 px-4 text-xs font-semibold text-slate-600 uppercase tracking-wider">ACTIONS</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white">
|
|
{todayAndTomorrowOrders.map((order, index) => {
|
|
const assignedCount = order.assigned_staff?.length || 0;
|
|
const requestedCount = order.requested || 0;
|
|
const { startTime, endTime } = getShiftTimes(order);
|
|
const isLastRow = index === todayAndTomorrowOrders.length - 1;
|
|
const percentage = requestedCount > 0 ? Math.round((assignedCount / requestedCount) * 100) : 0;
|
|
const invoiceReady = order.status === "Completed";
|
|
|
|
return (
|
|
<tr key={order.id} className={`hover:bg-slate-50/50 transition-colors ${!isLastRow ? 'border-b border-slate-100' : ''}`}>
|
|
<td className="py-4 px-4">
|
|
<span className="text-sm text-slate-900 font-medium">
|
|
{order.business_name || "Sports Arena LLC"}
|
|
</span>
|
|
</td>
|
|
<td className="py-4 px-4">
|
|
<div className="flex items-center gap-1.5 text-sm text-slate-600">
|
|
<MapPin className="w-3.5 h-3.5 text-slate-400" />
|
|
{order.hub || order.event_location || "Main Hub"}
|
|
</div>
|
|
</td>
|
|
<td className="py-4 px-4">
|
|
<span className="text-sm text-slate-900">
|
|
{order.event_name}
|
|
</span>
|
|
</td>
|
|
<td className="py-4 px-4">
|
|
<div>
|
|
<p className="text-sm font-semibold text-slate-900">
|
|
{order.date ? format(new Date(order.date), 'MM.dd.yyyy') : "—"}
|
|
</p>
|
|
<p className="text-xs text-slate-500">
|
|
{order.date ? format(new Date(order.date), 'EEEE') : "—"}
|
|
</p>
|
|
<div className="flex items-center gap-1 text-xs text-slate-600 mt-0.5">
|
|
<Clock className="w-3 h-3 text-slate-400" />
|
|
<span>{startTime} - {endTime}</span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="py-4 px-4">
|
|
{getOrderStatusBadge(order)}
|
|
</td>
|
|
<td className="py-4 px-4 text-center">
|
|
<span className="text-sm font-semibold text-slate-900">
|
|
{requestedCount}
|
|
</span>
|
|
</td>
|
|
<td className="py-4 px-4">
|
|
<div className="flex items-center justify-center gap-2">
|
|
<span className="text-sm font-semibold text-slate-900">{assignedCount}</span>
|
|
<Badge className={`text-xs font-bold ${percentage === 100 ? 'bg-green-500' : 'bg-slate-400'} text-white rounded-full px-2 py-0.5`}>
|
|
{percentage}%
|
|
</Badge>
|
|
</div>
|
|
</td>
|
|
<td className="py-4 px-4 text-center">
|
|
<div className="flex items-center justify-center">
|
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity ${invoiceReady ? 'bg-blue-100' : 'bg-slate-100'}`}>
|
|
<FileText className={`w-5 h-5 ${invoiceReady ? 'text-blue-600' : 'text-slate-400'}`} />
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="py-4 px-4">
|
|
<div className="flex items-center justify-center gap-1">
|
|
<button
|
|
onClick={() => handleAssignStaff(order)}
|
|
className="inline-flex items-center justify-center w-8 h-8 hover:bg-slate-100 rounded-md transition-colors"
|
|
title="Smart Assign"
|
|
>
|
|
<UserCheck className="w-4 h-4 text-slate-600" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleViewOrder(order)}
|
|
className="inline-flex items-center justify-center w-8 h-8 hover:bg-slate-100 rounded-md transition-colors"
|
|
title="View"
|
|
>
|
|
<Eye className="w-4 h-4 text-slate-600" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleCopyOrder(order)}
|
|
className="inline-flex items-center justify-center w-8 h-8 hover:bg-slate-100 rounded-md transition-colors"
|
|
title="Copy"
|
|
>
|
|
<Copy className="w-4 h-4 text-slate-600" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
) : (
|
|
<div className="py-12 text-center">
|
|
<Calendar className="w-12 h-12 mx-auto mb-3 text-slate-300" />
|
|
<p className="text-slate-500 font-medium">No orders for today or tomorrow</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
|
|
const renderRevenueCarousel = () => (
|
|
<Card className={`bg-gradient-to-br ${carouselSlides[carouselIndex].color} border-0 shadow-lg`}>
|
|
<CardContent className="p-4 relative">
|
|
<button
|
|
onClick={() => setAutoRotate(!autoRotate)}
|
|
className="absolute top-3 left-3 w-6 h-6 bg-white/20 hover:bg-white/30 rounded-full flex items-center justify-center"
|
|
>
|
|
{autoRotate ? <Pause className="w-3 h-3 text-white" /> : <Play className="w-3 h-3 text-white" />}
|
|
</button>
|
|
|
|
<div className="absolute top-3 right-3 flex gap-1.5">
|
|
{carouselSlides.map((_, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={() => {
|
|
setCarouselIndex(index);
|
|
setAutoRotate(false);
|
|
}}
|
|
className={`w-2 h-2 rounded-full transition-all ${
|
|
index === carouselIndex ? 'bg-white scale-110' : 'bg-white/40'
|
|
}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<AnimatePresence mode="wait">
|
|
<motion.div
|
|
key={carouselIndex}
|
|
initial={{ opacity: 0, x: 20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
exit={{ opacity: 0, x: -20 }}
|
|
transition={{ duration: 0.3 }}
|
|
>
|
|
<div className="flex items-center gap-3 mb-3 mt-6">
|
|
{React.createElement(carouselSlides[carouselIndex].icon, {
|
|
className: "w-6 h-6 text-white/80"
|
|
})}
|
|
<p className="text-white/80 text-xs font-medium uppercase">
|
|
{carouselSlides[carouselIndex].title}
|
|
</p>
|
|
</div>
|
|
<p className="text-4xl font-bold text-white mb-2">
|
|
{carouselSlides[carouselIndex].value}
|
|
</p>
|
|
<p className="text-white/70 text-xs">
|
|
{carouselSlides[carouselIndex].subtitle}
|
|
</p>
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
|
|
const renderTopClients = () => (
|
|
<Card className="bg-white border-slate-200 shadow-sm">
|
|
<CardHeader className="pb-3 border-b border-slate-100">
|
|
<CardTitle className="text-sm flex items-center gap-2">
|
|
<Star className="w-4 h-4 text-amber-600" />
|
|
Top Clients
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-4 space-y-3">
|
|
{topClients.length > 0 ? (
|
|
topClients.map((client, idx) => (
|
|
<div key={client.name} className="flex items-center justify-between p-2 rounded-lg hover:bg-slate-50">
|
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
<div className="w-6 h-6 bg-blue-600 rounded-lg flex items-center justify-center text-white text-xs font-bold flex-shrink-0">
|
|
{idx + 1}
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="font-bold text-sm text-[#1C323E] truncate">{client.name}</p>
|
|
<p className="text-xs text-emerald-600">+{client.orders}% growth</p>
|
|
</div>
|
|
</div>
|
|
<p className="font-bold text-lg text-[#1C323E]">${(client.revenue / 1000).toFixed(0)}k</p>
|
|
</div>
|
|
))
|
|
) : (
|
|
<p className="text-xs text-slate-500 text-center py-4">No data</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
|
|
const renderTopPerformers = () => (
|
|
<Card className="bg-white border-slate-200 shadow-sm">
|
|
<CardHeader className="pb-3 border-b border-slate-100">
|
|
<CardTitle className="text-sm flex items-center gap-2">
|
|
<Award className="w-4 h-4 text-blue-600" />
|
|
Top Performers
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-4 space-y-3">
|
|
{topPerformers.length > 0 ? (
|
|
topPerformers.map((member, idx) => (
|
|
<div key={member.id} className="flex items-center justify-between p-2 rounded-lg hover:bg-slate-50">
|
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
<div className="w-6 h-6 bg-purple-600 rounded-lg flex items-center justify-center text-white text-xs font-bold flex-shrink-0">
|
|
{idx + 1}
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="font-bold text-sm text-[#1C323E] truncate">{member.employee_name}</p>
|
|
<p className="text-xs text-slate-500">{member.shifts} shifts</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-1 bg-amber-50 px-2 py-1 rounded-lg">
|
|
<span className="text-sm font-bold text-amber-700">{(member.rating || 0).toFixed(1)}</span>
|
|
<Star className="w-3 h-3 text-amber-500 fill-amber-500" />
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<p className="text-xs text-slate-500 text-center py-4">No data</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
|
|
const renderClientLoyalty = () => (
|
|
<ClientLoyaltyCard vendorId={user?.id} vendorName={user?.company_name} />
|
|
);
|
|
|
|
const renderGoldVendors = () => (
|
|
<Card className="bg-white border-slate-200 shadow-sm">
|
|
<CardHeader className="pb-3 border-b border-slate-100">
|
|
<CardTitle className="text-sm flex items-center gap-2">
|
|
<Trophy className="w-4 h-4 text-amber-600" />
|
|
Gold Vendors
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-4 space-y-3">
|
|
<div className="text-center py-4">
|
|
<Trophy className="w-8 h-8 mx-auto mb-2 text-amber-500" />
|
|
<p className="text-xs text-slate-500">Gold tier vendors</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
|
|
const renderQuickActions = () => (
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<Link to={createPageUrl("VendorOrders")}>
|
|
<Card className="bg-[#0A39DF] border-0 shadow-md hover:shadow-lg transition-all cursor-pointer group">
|
|
<CardContent className="p-4 text-center">
|
|
<div className="w-10 h-10 mx-auto mb-2 bg-white/20 rounded-lg flex items-center justify-center group-hover:scale-110 transition-transform">
|
|
<Package className="w-5 h-5 text-white" />
|
|
</div>
|
|
<p className="text-white font-bold text-xs">All Orders</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
|
|
<Link to={createPageUrl("StaffDirectory")}>
|
|
<Card className="bg-white border-slate-200 shadow-sm hover:shadow-md transition-all cursor-pointer group">
|
|
<CardContent className="p-4 text-center">
|
|
<div className="w-10 h-10 mx-auto mb-2 bg-blue-50 rounded-lg flex items-center justify-center group-hover:scale-110 transition-transform">
|
|
<Users className="w-5 h-5 text-[#0A39DF]" />
|
|
</div>
|
|
<p className="text-[#1C323E] font-bold text-xs">My Staff</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
</div>
|
|
);
|
|
|
|
const renderWidget = (widgetId) => {
|
|
switch (widgetId) {
|
|
case 'kpi-cards':
|
|
return renderKPICards();
|
|
case 'orders-table':
|
|
return renderTodayAndTomorrowOrders();
|
|
case 'revenue-carousel':
|
|
return renderRevenueCarousel();
|
|
case 'top-clients':
|
|
return renderTopClients();
|
|
case 'top-performers':
|
|
return renderTopPerformers();
|
|
case 'client-loyalty':
|
|
return renderClientLoyalty();
|
|
case 'gold-vendors':
|
|
return renderGoldVendors();
|
|
case 'quick-actions':
|
|
return renderQuickActions();
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
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-3xl font-bold text-[#1C323E]">
|
|
{greeting} <span className="text-slate-600 font-normal ml-4">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}>
|
|
<RotateCcw className="w-4 h-4 mr-2" />
|
|
Reset
|
|
</Button>
|
|
<Button variant="outline" onClick={handleCancelCustomize}>
|
|
<X className="w-4 h-4 mr-2" />
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={handleSaveLayout}
|
|
disabled={!hasChanges || saveLayoutMutation.isPending}
|
|
className="bg-blue-600 hover:bg-blue-700"
|
|
>
|
|
<Check className="w-4 h-4 mr-2" />
|
|
{saveLayoutMutation.isPending ? "Saving..." : "Save"}
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<Button
|
|
onClick={() => setIsCustomizing(true)}
|
|
variant="outline"
|
|
className="border-2 border-blue-200 hover:bg-blue-50 text-blue-600 font-semibold"
|
|
>
|
|
<Settings className="w-4 h-4 mr-2" />
|
|
Customize
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<DragDropContext onDragEnd={handleDragEnd}>
|
|
<Droppable droppableId="vendor-widgets" isDropDisabled={!isCustomizing}>
|
|
{(provided) => (
|
|
<div {...provided.droppableProps} ref={provided.innerRef} className="space-y-6">
|
|
{visibleWidgetIds.map((widgetId, index) => {
|
|
const widget = AVAILABLE_WIDGETS.find(w => w.id === widgetId);
|
|
if (!widget) return null;
|
|
|
|
if (widgetId !== 'kpi-cards') {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Draggable key={widgetId} draggableId={widgetId} index={index} isDragDisabled={!isCustomizing}>
|
|
{(provided, snapshot) => (
|
|
<div ref={provided.innerRef} {...provided.draggableProps} className="relative group">
|
|
{isCustomizing && (
|
|
<>
|
|
<div
|
|
{...provided.dragHandleProps}
|
|
className="absolute -left-12 top-1/2 -translate-y-1/2 w-10 h-10 bg-blue-600 hover:bg-blue-700 rounded-lg flex items-center justify-center cursor-grab active:cursor-grabbing shadow-lg z-10"
|
|
>
|
|
<GripVertical className="w-5 h-5 text-white" />
|
|
</div>
|
|
<button
|
|
onClick={() => handleRemoveWidget(widgetId)}
|
|
className="absolute -top-3 -right-3 w-8 h-8 bg-red-500 hover:bg-red-600 rounded-full flex items-center justify-center shadow-lg z-10"
|
|
>
|
|
<Minus className="w-4 h-4 text-white" />
|
|
</button>
|
|
</>
|
|
)}
|
|
<div className={snapshot.isDragging ? 'opacity-50' : ''}>
|
|
{renderWidget(widgetId)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Draggable>
|
|
);
|
|
})}
|
|
{provided.placeholder}
|
|
</div>
|
|
)}
|
|
</Droppable>
|
|
</DragDropContext>
|
|
|
|
{visibleWidgetIds.includes('orders-table') && (
|
|
<div className="relative group">
|
|
{isCustomizing && (
|
|
<button
|
|
onClick={() => handleRemoveWidget('orders-table')}
|
|
className="absolute -top-3 -right-3 w-8 h-8 bg-red-500 hover:bg-red-600 rounded-full flex items-center justify-center shadow-lg z-10"
|
|
>
|
|
<Minus className="w-4 h-4 text-white" />
|
|
</button>
|
|
)}
|
|
{renderWidget('orders-table')}
|
|
</div>
|
|
)}
|
|
|
|
{(visibleWidgetIds.includes('revenue-carousel') || visibleWidgetIds.includes('top-clients') || visibleWidgetIds.includes('top-performers') || visibleWidgetIds.includes('client-loyalty') || visibleWidgetIds.includes('gold-vendors') || visibleWidgetIds.includes('quick-actions')) && (
|
|
<div className="grid grid-cols-3 gap-6">
|
|
{(visibleWidgetIds.includes('revenue-carousel') || visibleWidgetIds.includes('quick-actions')) && (
|
|
<div className="space-y-4">
|
|
{visibleWidgetIds.includes('revenue-carousel') && (
|
|
<div className="relative group">
|
|
{isCustomizing && (
|
|
<button onClick={() => handleRemoveWidget('revenue-carousel')} className="absolute -top-3 -right-3 w-8 h-8 bg-red-500 hover:bg-red-600 rounded-full flex items-center justify-center shadow-lg z-10">
|
|
<Minus className="w-4 h-4 text-white" />
|
|
</button>
|
|
)}
|
|
{renderWidget('revenue-carousel')}
|
|
</div>
|
|
)}
|
|
{visibleWidgetIds.includes('quick-actions') && (
|
|
<div className="relative group">
|
|
{isCustomizing && (
|
|
<button onClick={() => handleRemoveWidget('quick-actions')} className="absolute -top-3 -right-3 w-8 h-8 bg-red-500 hover:bg-red-600 rounded-full flex items-center justify-center shadow-lg z-10">
|
|
<Minus className="w-4 h-4 text-white" />
|
|
</button>
|
|
)}
|
|
{renderWidget('quick-actions')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{(visibleWidgetIds.includes('top-clients') || visibleWidgetIds.includes('top-performers') || visibleWidgetIds.includes('client-loyalty') || visibleWidgetIds.includes('gold-vendors')) && (
|
|
<div className={`grid ${visibleWidgetIds.includes('revenue-carousel') || visibleWidgetIds.includes('quick-actions') ? 'col-span-2' : 'col-span-3'} grid-cols-3 gap-4`}>
|
|
{visibleWidgetIds.includes('top-clients') && (
|
|
<div className="relative group">
|
|
{isCustomizing && (
|
|
<button onClick={() => handleRemoveWidget('top-clients')} className="absolute -top-3 -right-3 w-8 h-8 bg-red-500 hover:bg-red-600 rounded-full flex items-center justify-center shadow-lg z-10">
|
|
<Minus className="w-4 h-4 text-white" />
|
|
</button>
|
|
)}
|
|
{renderWidget('top-clients')}
|
|
</div>
|
|
)}
|
|
{visibleWidgetIds.includes('top-performers') && (
|
|
<div className="relative group">
|
|
{isCustomizing && (
|
|
<button onClick={() => handleRemoveWidget('top-performers')} className="absolute -top-3 -right-3 w-8 h-8 bg-red-500 hover:bg-red-600 rounded-full flex items-center justify-center shadow-lg z-10">
|
|
<Minus className="w-4 h-4 text-white" />
|
|
</button>
|
|
)}
|
|
{renderWidget('top-performers')}
|
|
</div>
|
|
)}
|
|
{visibleWidgetIds.includes('client-loyalty') && (
|
|
<div className="relative group">
|
|
{isCustomizing && (
|
|
<button onClick={() => handleRemoveWidget('client-loyalty')} className="absolute -top-3 -right-3 w-8 h-8 bg-red-500 hover:bg-red-600 rounded-full flex items-center justify-center shadow-lg z-10">
|
|
<Minus className="w-4 h-4 text-white" />
|
|
</button>
|
|
)}
|
|
{renderWidget('client-loyalty')}
|
|
</div>
|
|
)}
|
|
{visibleWidgetIds.includes('gold-vendors') && (
|
|
<div className="relative group">
|
|
{isCustomizing && (
|
|
<button onClick={() => handleRemoveWidget('gold-vendors')} className="absolute -top-3 -right-3 w-8 h-8 bg-red-500 hover:bg-red-600 rounded-full flex items-center justify-center shadow-lg z-10">
|
|
<Minus className="w-4 h-4 text-white" />
|
|
</button>
|
|
)}
|
|
{renderWidget('gold-vendors')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{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-4 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 text-left"
|
|
>
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Plus className="w-5 h-5 text-blue-600" />
|
|
<p className="font-bold text-sm text-slate-900">{widget.title}</p>
|
|
</div>
|
|
<p className="text-xs text-slate-500">{widget.description}</p>
|
|
<Badge className={`${widget.categoryColor} mt-2 text-xs`}>
|
|
{widget.category}
|
|
</Badge>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<Dialog open={showRapidModal} onOpenChange={setShowRapidModal}>
|
|
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-3 text-2xl">
|
|
<div className="w-12 h-12 bg-red-100 rounded-xl flex items-center justify-center">
|
|
<Zap className="w-6 h-6 text-red-600" />
|
|
</div>
|
|
<div>
|
|
<span className="text-[#1C323E]">Urgent Orders</span>
|
|
<p className="text-sm text-slate-600 font-normal mt-1">
|
|
{rapidOrders.length} order{rapidOrders.length !== 1 ? 's' : ''} need immediate attention
|
|
</p>
|
|
</div>
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-3 mt-6">
|
|
{rapidOrders.map((order) => {
|
|
const eventDate = new Date(order.date);
|
|
const now = new Date();
|
|
const hoursUntil = Math.round(differenceInHours(eventDate, now));
|
|
const assignedCount = order.assigned_staff?.length || 0;
|
|
const requestedCount = order.requested || 0;
|
|
|
|
return (
|
|
<div
|
|
key={order.id}
|
|
className="p-4 border-2 border-red-200 bg-red-50 rounded-xl hover:border-red-300 cursor-pointer"
|
|
onClick={() => {
|
|
setShowRapidModal(false);
|
|
navigate(createPageUrl(`EventDetail?id=${order.id}`));
|
|
}}
|
|
>
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Badge className="bg-red-600 text-white font-bold">{hoursUntil}h away</Badge>
|
|
<Badge variant="outline">{order.business_name || "Client"}</Badge>
|
|
</div>
|
|
<h3 className="font-bold text-lg text-[#1C323E]">{order.event_name}</h3>
|
|
<div className="flex items-center gap-4 text-sm text-slate-600 mt-2">
|
|
<span className="flex items-center gap-1">
|
|
<Calendar className="w-4 h-4" />
|
|
{format(eventDate, "MMM d, h:mm a")}
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<Package className="w-4 h-4" />
|
|
{order.hub || "No hub"}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<ArrowRight className="w-5 h-5 text-red-600" />
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4 pt-3 border-t border-red-200">
|
|
<div className="flex-1">
|
|
<p className="text-xs text-slate-600 mb-1">Staff Assignment</p>
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex-1 h-2 bg-white rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full bg-red-600"
|
|
style={{ width: `${requestedCount > 0 ? (assignedCount / requestedCount) * 100 : 0}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-sm font-bold">{assignedCount}/{requestedCount}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<SmartAssignModal
|
|
open={assignModal.open}
|
|
onClose={() => setAssignModal({ open: false, event: null })}
|
|
event={assignModal.event}
|
|
/>
|
|
</div>
|
|
);
|
|
} |