1273 lines
55 KiB
JavaScript
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>
|
|
);
|
|
} |