feat(Makefile): patch Layout.jsx queryKey for local development feat(frontend-web): mock base44 client for local development with role switching feat(frontend-web): add event assignment modal with conflict detection and bulk assign feat(frontend-web): add client dashboard with key metrics and quick actions feat(frontend-web): add layout component with role-based navigation feat(frontend-web): update various pages to use "@/components" alias feat(frontend-web): update create event page with ai assistant toggle feat(frontend-web): update dashboard page with new components feat(frontend-web): update events page with quick assign popover feat(frontend-web): update invite vendor page with hover card feat(frontend-web): update messages page with conversation list and message thread feat(frontend-web): update operator dashboard page with new components feat(frontend-web): update partner management page with new components feat(frontend-web): update permissions page with new components feat(frontend-web): update procurement dashboard page with new components feat(frontend-web): update smart vendor onboarding page with new components feat(frontend-web): update staff directory page with new components feat(frontend-web): update teams page with new components feat(frontend-web): update user management page with new components feat(frontend-web): update vendor compliance page with new components feat(frontend-web): update main.jsx to include react query provider feat: add vendor marketplace page feat: add global import fix to prepare-export script feat: add patch-layout-query-key script to fix query key feat: update patch-base44-client script to use a more robust method
829 lines
40 KiB
JavaScript
829 lines
40 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 } from "lucide-react";
|
|
import { format, differenceInHours, parseISO } from "date-fns";
|
|
import { useToast } from "@/components/ui/use-toast";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
|
|
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 { data: user } = useQuery({
|
|
queryKey: ['current-user-vendor'],
|
|
queryFn: () => base44.auth.me(),
|
|
});
|
|
|
|
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 todayOrders = events.filter(e => {
|
|
const eventDate = new Date(e.date);
|
|
const today = new Date();
|
|
return eventDate.toDateString() === today.toDateString();
|
|
});
|
|
|
|
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 thisMonthPayroll = thisMonthRevenue * 0.88;
|
|
|
|
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 todayStaffCompletion = staffAssignedToday > 0 ? Math.round((staffAssignedTodayCompleted / staffAssignedToday) * 100) : 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 recentOrders = events
|
|
.filter(e => e.status !== "Completed" && e.status !== "Canceled")
|
|
.sort((a, b) => new Date(b.date) - new Date(a.date))
|
|
.slice(0, 7);
|
|
|
|
const coverageRate = events.reduce((sum, e) => sum + (e.requested || 0), 0) > 0
|
|
? Math.round((events.reduce((sum, e) => sum + (e.assigned_staff?.length || 0), 0) / events.reduce((sum, e) => sum + (e.requested || 0), 0)) * 100)
|
|
: 0;
|
|
|
|
const todayOrdersTotal = todayOrders.length;
|
|
const todayOrdersCompleted = todayOrders.filter(e => e.status === "Completed").length;
|
|
const todayOrdersCompletion = todayOrdersTotal > 0 ? Math.round((todayOrdersCompleted / todayOrdersTotal) * 100) : 0;
|
|
|
|
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 getStatusBadge = (status, isFull) => {
|
|
if (isFull) {
|
|
return {
|
|
label: "Fully Staffed",
|
|
className: "bg-emerald-500 text-white border-0",
|
|
dotColor: "bg-emerald-400"
|
|
};
|
|
}
|
|
const badges = {
|
|
"Active": {
|
|
label: "Active",
|
|
className: "bg-green-500 text-white border-0",
|
|
dotColor: "bg-green-400"
|
|
},
|
|
"Pending": {
|
|
label: "Pending",
|
|
className: "bg-orange-500 text-white border-0",
|
|
dotColor: "bg-orange-400"
|
|
},
|
|
"Confirmed": {
|
|
label: "Confirmed",
|
|
className: "bg-blue-500 text-white border-0",
|
|
dotColor: "bg-blue-400"
|
|
},
|
|
"Draft": {
|
|
label: "Draft",
|
|
className: "bg-slate-400 text-white border-0",
|
|
dotColor: "bg-slate-300"
|
|
}
|
|
};
|
|
return badges[status] || badges["Draft"];
|
|
};
|
|
|
|
const handleSendNotification = (order) => {
|
|
toast({
|
|
title: "Notification Sent",
|
|
description: `Notification sent for order: ${order.event_name}`,
|
|
});
|
|
};
|
|
|
|
const handleAssignStaff = (order) => {
|
|
navigate(createPageUrl(`EventDetail?id=${order.id}`));
|
|
};
|
|
|
|
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 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" // Changed color
|
|
}
|
|
];
|
|
|
|
return (
|
|
<div className="min-h-screen bg-slate-50 p-6">
|
|
<style>{`
|
|
@keyframes blink {
|
|
0%, 50%, 100% { opacity: 1; }
|
|
25%, 75% { opacity: 0.3; }
|
|
}
|
|
.blink-animation {
|
|
animation: blink 1.5s ease-in-out infinite;
|
|
}
|
|
`}</style>
|
|
|
|
<div className="max-w-[1800px] mx-auto space-y-6">
|
|
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-[#1C323E] mb-1">
|
|
{greeting} <span className="text-slate-600 font-normal ml-4">here's what matters today</span>
|
|
</h1>
|
|
</div>
|
|
|
|
{/* Top 4 KPI Cards */}
|
|
<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>
|
|
<div className="flex items-center gap-2">
|
|
<Badge className="bg-blue-100 text-blue-700 text-xs">Active</Badge>
|
|
</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-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 Rate</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card
|
|
className="bg-gradient-to-br from-red-50 to-orange-50 border-red-200 shadow-sm hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
|
|
onClick={handleRapidClick}
|
|
>
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<p className="text-xs text-red-600 font-semibold uppercase tracking-wide">Order Type</p>
|
|
{rapidOrders.length > 0 && (
|
|
<Badge className="bg-red-600 text-white font-bold blink-animation">
|
|
{rapidOrders.length} Urgent
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-12 h-12 bg-white rounded-xl flex items-center justify-center shadow-sm">
|
|
<Zap className="w-6 h-6 text-red-600" />
|
|
</div>
|
|
<div>
|
|
<p className="text-2xl font-bold text-red-600">RAPID</p>
|
|
<p className="text-xs text-red-500">Click to view</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"> {/* Changed bg-amber-50 to bg-indigo-50 */}
|
|
<Users className="w-4 h-4 text-indigo-600" /> {/* Changed text-amber-600 to text-indigo-600 */}
|
|
</div>
|
|
<p className="text-xs text-slate-500 font-medium">Staff Assigned</p>
|
|
<Badge className="bg-indigo-100 text-indigo-700 text-xs font-bold ml-auto">Today</Badge> {/* Changed bg-amber-100 to bg-indigo-100 and text-amber-700 to text-indigo-700 */}
|
|
</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>
|
|
|
|
{/* Main Content Grid */}
|
|
<div className="grid grid-cols-3 gap-6">
|
|
|
|
{/* Orders Table (2 cols) */}
|
|
<div className="col-span-2 space-y-4">
|
|
<Card className="bg-white border-slate-200 shadow-sm">
|
|
<CardContent className="p-0">
|
|
<div className="overflow-hidden">
|
|
<table className="w-full table-fixed">
|
|
<colgroup>
|
|
<col style={{ width: '14%' }} />
|
|
<col style={{ width: '12%' }} />
|
|
<col style={{ width: '18%' }} />
|
|
<col style={{ width: '14%' }} />
|
|
<col style={{ width: '10%' }} />
|
|
<col style={{ width: '8%' }} />
|
|
<col style={{ width: '8%' }} />
|
|
<col style={{ width: '8%' }} />
|
|
<col style={{ width: '8%' }} />
|
|
</colgroup>
|
|
<thead>
|
|
<tr className="border-b-2 border-slate-200 bg-gradient-to-r from-slate-50 to-white">
|
|
<th className="text-left py-4 px-4 text-xs font-bold text-slate-600 uppercase tracking-wide">Business</th>
|
|
<th className="text-left py-4 px-4 text-xs font-bold text-slate-600 uppercase tracking-wide">Hub</th>
|
|
<th className="text-left py-4 px-4 text-xs font-bold text-slate-600 uppercase tracking-wide">Event Name</th>
|
|
<th className="text-left py-4 px-4 text-xs font-bold text-slate-600 uppercase tracking-wide">Status</th>
|
|
<th className="text-left py-4 px-4 text-xs font-bold text-slate-600 uppercase tracking-wide">Date</th>
|
|
<th className="text-center py-4 px-3 text-xs font-bold text-slate-600 uppercase tracking-wide">Requested</th>
|
|
<th className="text-center py-4 px-3 text-xs font-bold text-slate-600 uppercase tracking-wide">Assigned</th>
|
|
<th className="text-center py-4 px-3 text-xs font-bold text-slate-600 uppercase tracking-wide">Invoice</th>
|
|
<th className="text-center py-4 px-3 text-xs font-bold text-slate-600 uppercase tracking-wide">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{recentOrders.length > 0 ? (
|
|
recentOrders.map((order, index) => {
|
|
const assignedCount = order.assigned_staff?.length || 0;
|
|
const requestedCount = order.requested || 0;
|
|
const isFull = assignedCount >= requestedCount && requestedCount > 0;
|
|
const statusConfig = getStatusBadge(order.status, isFull);
|
|
|
|
return (
|
|
<tr
|
|
key={order.id}
|
|
className="border-b border-slate-100 hover:bg-gradient-to-r hover:from-blue-50/50 hover:to-transparent transition-all duration-200 group"
|
|
>
|
|
<td className="py-4 px-4">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-1.5 h-1.5 rounded-full bg-blue-500 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" />
|
|
<span className="text-sm font-bold text-[#1C323E] whitespace-nowrap overflow-hidden text-ellipsis block">
|
|
{order.business_name || "Sports Arena LLC"}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td className="py-4 px-4">
|
|
<span className="text-sm text-slate-600 whitespace-nowrap overflow-hidden text-ellipsis block">
|
|
{order.hub || "Downtown"}
|
|
</span>
|
|
</td>
|
|
<td className="py-4 px-4">
|
|
<span className="text-sm text-slate-700 font-medium whitespace-nowrap overflow-hidden text-ellipsis block">
|
|
{order.event_name}
|
|
</span>
|
|
</td>
|
|
<td className="py-4 px-4">
|
|
<Badge className={`${statusConfig.className} font-semibold px-3 py-1 shadow-sm whitespace-nowrap`}>
|
|
<div className={`w-1.5 h-1.5 rounded-full ${statusConfig.dotColor} mr-1.5 animate-pulse`} />
|
|
{statusConfig.label}
|
|
</Badge>
|
|
</td>
|
|
<td className="py-4 px-4">
|
|
<span className="text-sm text-slate-600 font-medium whitespace-nowrap">
|
|
{order.date ? format(new Date(order.date), "MM/dd/yy") : "-"}
|
|
</span>
|
|
</td>
|
|
<td className="py-4 px-3 text-center">
|
|
<span className="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-slate-100 text-sm font-bold text-[#1C323E]">
|
|
{requestedCount}
|
|
</span>
|
|
</td>
|
|
<td className="py-4 px-3 text-center">
|
|
<span className={`inline-flex items-center justify-center w-8 h-8 rounded-lg text-sm font-bold ${
|
|
isFull ? 'bg-emerald-100 text-emerald-700' :
|
|
assignedCount > 0 ? 'bg-blue-100 text-blue-700' :
|
|
'bg-slate-100 text-slate-600'
|
|
}`}>
|
|
{assignedCount}
|
|
</span>
|
|
</td>
|
|
<td className="py-4 px-3 text-center">
|
|
<Link
|
|
to={createPageUrl(`EventDetail?id=${order.id}`)}
|
|
className="inline-flex items-center justify-center w-8 h-8 hover:bg-blue-50 rounded-lg transition-colors group/invoice"
|
|
title={`INV-${order.id?.slice(-6) || "000000"}`}
|
|
>
|
|
<FileText className="w-4 h-4 text-blue-600 group-hover/invoice:scale-110 transition-transform" />
|
|
</Link>
|
|
</td>
|
|
<td className="py-4 px-3">
|
|
<div className="flex items-center justify-center gap-1">
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
className="h-8 w-8 bg-slate-50 hover:bg-blue-100 hover:text-blue-600 rounded-lg shadow-sm transition-all"
|
|
title="Send Notification"
|
|
onClick={() => handleSendNotification(order)}
|
|
>
|
|
<Send className="w-4 h-4" />
|
|
</Button>
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
className="h-8 w-8 bg-slate-50 hover:bg-purple-100 hover:text-purple-600 rounded-lg shadow-sm transition-all"
|
|
title="Assign Staff"
|
|
onClick={() => handleAssignStaff(order)}
|
|
>
|
|
<UserCheck className="w-4 h-4" />
|
|
</Button>
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
className="h-8 w-8 bg-slate-50 hover:bg-green-100 hover:text-green-600 rounded-lg shadow-sm transition-all"
|
|
title="View Order"
|
|
onClick={() => handleViewOrder(order)}
|
|
>
|
|
<Eye className="w-4 h-4" />
|
|
</Button>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
className="h-8 w-8 bg-slate-50 hover:bg-slate-200 rounded-lg shadow-sm transition-all"
|
|
title="More Options"
|
|
>
|
|
<MoreHorizontal className="w-4 h-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={() => handleViewOrder(order)}>
|
|
<Eye className="w-4 h-4 mr-2" />
|
|
View Details
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => handleCopyOrder(order)}>
|
|
<Copy className="w-4 h-4 mr-2" />
|
|
Copy ID
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})
|
|
) : (
|
|
<tr>
|
|
<td colSpan="9" className="py-16 text-center">
|
|
<Package className="w-12 h-12 mx-auto mb-3 text-slate-300" />
|
|
<p className="font-medium text-slate-600">No orders to display</p>
|
|
<p className="text-sm text-slate-400 mt-1">Your recent orders will appear here</p>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Bottom Stats Row */}
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<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 text-slate-700">
|
|
<div className="w-6 h-6 bg-amber-100 rounded-lg flex items-center justify-center">
|
|
<Star className="w-3.5 h-3.5 text-amber-600" />
|
|
</div>
|
|
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 transition-colors">
|
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
<div className="w-6 h-6 bg-gradient-to-br from-blue-500 to-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] flex-shrink-0 ml-2">${(client.revenue / 1000).toFixed(0)}k</p>
|
|
</div>
|
|
))
|
|
) : (
|
|
<p className="text-xs text-slate-500 text-center py-4">No client data</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<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 text-slate-700">
|
|
<div className="w-6 h-6 bg-blue-100 rounded-lg flex items-center justify-center">
|
|
<Award className="w-3.5 h-3.5 text-blue-600" />
|
|
</div>
|
|
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 transition-colors">
|
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
<div className="w-6 h-6 bg-gradient-to-br from-purple-500 to-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 flex-shrink-0 ml-2">
|
|
<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 staff data</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<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 text-slate-700">
|
|
<div className="w-6 h-6 bg-amber-100 rounded-lg flex items-center justify-center">
|
|
<Trophy className="w-3.5 h-3.5 text-amber-600" />
|
|
</div>
|
|
Gold Vendors
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-4 space-y-3">
|
|
<div className="flex items-center justify-between p-2 rounded-lg hover:bg-slate-50 transition-colors">
|
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
<div className="w-6 h-6 bg-gradient-to-br from-amber-400 to-amber-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
|
<Trophy className="w-3.5 h-3.5 text-white" />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="font-bold text-sm text-[#1C323E] truncate">Legendary Staffing</p>
|
|
<p className="text-xs text-slate-500">Premier vendor</p>
|
|
</div>
|
|
</div>
|
|
<div className="text-right flex-shrink-0 ml-2">
|
|
<p className="text-2xl font-bold text-amber-600">98</p>
|
|
<p className="text-xs text-slate-500">Score</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between p-2 rounded-lg hover:bg-slate-50 transition-colors">
|
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
<div className="w-6 h-6 bg-gradient-to-br from-slate-400 to-slate-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
|
<Trophy className="w-3.5 h-3.5 text-white" />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="font-bold text-sm text-[#1C323E] truncate">Epic Workforce</p>
|
|
<p className="text-xs text-slate-500">Gold tier</p>
|
|
</div>
|
|
</div>
|
|
<div className="text-right flex-shrink-0 ml-2">
|
|
<p className="text-2xl font-bold text-amber-600">96</p>
|
|
<p className="text-xs text-slate-500">Score</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Sidebar */}
|
|
<div className="space-y-4">
|
|
|
|
{/* Enhanced Carousel Card */}
|
|
<Card className={`bg-gradient-to-br ${carouselSlides[carouselIndex].color} border-0 shadow-lg overflow-hidden relative`}>
|
|
<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 transition-all"
|
|
title={autoRotate ? "Pause auto-rotate" : "Start auto-rotate"}
|
|
>
|
|
{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 hover:bg-white/60'
|
|
}`}
|
|
title={carouselSlides[index].title}
|
|
/>
|
|
))}
|
|
</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 tracking-wide">
|
|
{carouselSlides[carouselIndex].title}
|
|
</p>
|
|
</div>
|
|
<p className="text-4xl font-bold text-white leading-none mb-2">
|
|
{carouselSlides[carouselIndex].value}
|
|
</p>
|
|
<p className="text-white/70 text-xs">
|
|
{carouselSlides[carouselIndex].subtitle}
|
|
</p>
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Quick Action Buttons */}
|
|
<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 flex flex-col items-center justify-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>
|
|
<p className="text-white/70 text-[10px] mt-0.5">View & manage</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 flex flex-col items-center justify-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>
|
|
<p className="text-slate-500 text-[10px] mt-0.5">Manage staff</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Rapid Orders Modal */}
|
|
<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 hover:bg-red-100 transition-all cursor-pointer group"
|
|
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 text-xs">
|
|
{hoursUntil}h away
|
|
</Badge>
|
|
<Badge variant="outline" className="text-xs">
|
|
{order.business_name || "Client"}
|
|
</Badge>
|
|
</div>
|
|
<h3 className="font-bold text-lg text-[#1C323E] mb-1 group-hover:text-red-700 transition-colors">
|
|
{order.event_name}
|
|
</h3>
|
|
<div className="flex items-center gap-4 text-sm text-slate-600">
|
|
<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 group-hover:translate-x-1 transition-transform" />
|
|
</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-gradient-to-r from-red-500 to-red-600 transition-all duration-500"
|
|
style={{ width: `${requestedCount > 0 ? (assignedCount / requestedCount) * 100 : 0}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-sm font-bold text-[#1C323E]">
|
|
{assignedCount}/{requestedCount}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
className="bg-red-600 hover:bg-red-700 text-white"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setShowRapidModal(false);
|
|
navigate(createPageUrl(`EventDetail?id=${order.id}`));
|
|
}}
|
|
>
|
|
View Order
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|