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
726 lines
32 KiB
JavaScript
726 lines
32 KiB
JavaScript
import React, { useState, useMemo } 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 { Calendar, Plus, Clock, DollarSign, MessageSquare, RefreshCw, ArrowRight, Users, TrendingUp, TrendingDown, BarChart3, Sparkles, Zap, CheckCircle, AlertCircle, Coffee, ChevronRight, User } from "lucide-react";
|
|
import { format, parseISO, startOfMonth, endOfMonth, isToday, isTomorrow, addDays, differenceInDays } from "date-fns";
|
|
import { useToast } from "@/components/ui/use-toast";
|
|
import { PieChart, Pie, Cell, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, Legend } from 'recharts';
|
|
|
|
const COLORS = ['#0A39DF', '#6366f1', '#8b5cf6', '#a855f7', '#c026d3', '#d946ef'];
|
|
|
|
export default function ClientDashboard() {
|
|
const navigate = useNavigate();
|
|
const queryClient = useQueryClient();
|
|
const { toast } = useToast();
|
|
const [reorderingId, setReorderingId] = useState(null);
|
|
|
|
const { data: user } = useQuery({
|
|
queryKey: ['current-user'],
|
|
queryFn: () => base44.auth.me(),
|
|
});
|
|
|
|
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 createEventMutation = useMutation({
|
|
mutationFn: (eventData) => base44.entities.Event.create(eventData),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['client-events'] });
|
|
toast({
|
|
title: "✅ Order Created",
|
|
description: "Your reorder has been created successfully",
|
|
});
|
|
setReorderingId(null);
|
|
},
|
|
onError: () => {
|
|
toast({
|
|
title: "❌ Failed to Create Order",
|
|
description: "Please try again",
|
|
variant: "destructive",
|
|
});
|
|
setReorderingId(null);
|
|
},
|
|
});
|
|
|
|
// Today's orders
|
|
const todayOrders = useMemo(() => {
|
|
return events.filter(e => {
|
|
const eventDate = new Date(e.date);
|
|
return isToday(eventDate);
|
|
});
|
|
}, [events]);
|
|
|
|
// Upcoming orders (next 7 days)
|
|
const upcomingOrders = useMemo(() => {
|
|
return events
|
|
.filter(e => {
|
|
const eventDate = new Date(e.date);
|
|
const today = new Date();
|
|
const daysUntil = differenceInDays(eventDate, today);
|
|
return daysUntil > 0 && daysUntil <= 7 && e.status !== "Canceled";
|
|
})
|
|
.sort((a, b) => new Date(a.date) - new Date(b.date))
|
|
.slice(0, 5);
|
|
}, [events]);
|
|
|
|
// Completed orders for analytics
|
|
const completedOrders = events.filter(e => e.status === "Completed");
|
|
|
|
// Current month data
|
|
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";
|
|
});
|
|
|
|
// Calculate labor summary by position
|
|
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';
|
|
|
|
// Calculate hours
|
|
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,
|
|
hours: 0,
|
|
cost: 0
|
|
};
|
|
}
|
|
|
|
summary[position].headcount += count;
|
|
summary[position].hours += totalHours;
|
|
summary[position].cost += cost;
|
|
});
|
|
});
|
|
} else if (order.requested) {
|
|
const position = 'Staff';
|
|
const count = order.requested;
|
|
const hours = 8 * count;
|
|
const cost = hours * 25;
|
|
|
|
if (!summary[position]) {
|
|
summary[position] = {
|
|
position,
|
|
headcount: 0,
|
|
hours: 0,
|
|
cost: 0
|
|
};
|
|
}
|
|
|
|
summary[position].headcount += count;
|
|
summary[position].hours += hours;
|
|
summary[position].cost += cost;
|
|
}
|
|
});
|
|
|
|
return Object.values(summary).sort((a, b) => b.cost - a.cost);
|
|
}, [thisMonthOrders]);
|
|
|
|
// Cost breakdown
|
|
const totalLaborCost = laborByPosition.reduce((sum, p) => sum + p.cost, 0);
|
|
const totalHours = laborByPosition.reduce((sum, p) => sum + p.hours, 0);
|
|
const totalHeadcount = laborByPosition.reduce((sum, p) => sum + p.headcount, 0);
|
|
const avgCostPerHour = totalHours > 0 ? totalLaborCost / totalHours : 0;
|
|
|
|
// Last month comparison
|
|
const lastMonth = new Date();
|
|
lastMonth.setMonth(lastMonth.getMonth() - 1);
|
|
const lastMonthOrders = events.filter(e => {
|
|
const eventDate = new Date(e.date);
|
|
return eventDate.getMonth() === lastMonth.getMonth() &&
|
|
eventDate.getFullYear() === lastMonth.getFullYear() &&
|
|
e.status === "Completed";
|
|
});
|
|
const lastMonthCost = lastMonthOrders.reduce((sum, e) => sum + (e.total || 0), 0);
|
|
const costChange = lastMonthCost > 0 ? ((totalLaborCost - lastMonthCost) / lastMonthCost) * 100 : 0;
|
|
|
|
// Frequent orders for quick reorder
|
|
const pastOrders = events.filter(e => e.status === "Completed");
|
|
const orderFrequency = pastOrders.reduce((acc, event) => {
|
|
const key = event.event_name;
|
|
if (!acc[key]) {
|
|
acc[key] = {
|
|
event,
|
|
count: 0,
|
|
lastOrdered: event.date,
|
|
totalCost: 0
|
|
};
|
|
}
|
|
acc[key].count++;
|
|
acc[key].totalCost += (event.total || 0);
|
|
if (new Date(event.date) > new Date(acc[key].lastOrdered)) {
|
|
acc[key].lastOrdered = event.date;
|
|
acc[key].event = event;
|
|
}
|
|
return acc;
|
|
}, {});
|
|
|
|
const favoriteOrders = Object.values(orderFrequency)
|
|
.sort((a, b) => b.count - a.count)
|
|
.slice(0, 4);
|
|
|
|
const handleQuickReorder = (event) => {
|
|
setReorderingId(event.id);
|
|
const reorderData = {
|
|
event_name: event.event_name,
|
|
business_id: event.business_id,
|
|
business_name: event.business_name,
|
|
hub: event.hub,
|
|
status: "Draft",
|
|
requested: event.requested,
|
|
shifts: event.shifts,
|
|
notes: `Reorder of: ${event.event_name}`,
|
|
};
|
|
createEventMutation.mutate(reorderData);
|
|
};
|
|
|
|
const handleRapidOrder = () => {
|
|
navigate(createPageUrl("CreateEvent") + "?rapid=true");
|
|
};
|
|
|
|
const hour = new Date().getHours();
|
|
const greeting = hour < 12 ? "Good morning" : hour < 18 ? "Good afternoon" : "Good evening";
|
|
|
|
// Prepare data for pie chart
|
|
const pieChartData = laborByPosition.slice(0, 5).map(item => ({
|
|
name: item.position,
|
|
value: item.cost
|
|
}));
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-slate-50 p-6">
|
|
<div className="max-w-[1800px] mx-auto space-y-6">
|
|
|
|
{/* Hero Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<h1 className="text-4xl font-bold text-[#1C323E]">
|
|
{greeting}, {user?.full_name?.split(' ')[0] || 'there'}
|
|
</h1>
|
|
<Coffee className="w-8 h-8 text-amber-600" />
|
|
</div>
|
|
<p className="text-lg text-slate-600">Your staffing operations at a glance</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Link to={createPageUrl("ClientOrders")}>
|
|
<Button variant="outline" size="lg" className="gap-2">
|
|
<BarChart3 className="w-5 h-5" />
|
|
All Orders
|
|
</Button>
|
|
</Link>
|
|
<Button
|
|
size="lg"
|
|
onClick={handleRapidOrder}
|
|
className="bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-700 hover:to-orange-700 text-white font-bold shadow-lg gap-2 px-6"
|
|
>
|
|
<Zap className="w-5 h-5" />
|
|
Rapid Order
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Today's Orders - Prominent Section */}
|
|
{todayOrders.length > 0 && (
|
|
<Card className="border-2 border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 shadow-lg">
|
|
<CardHeader className="border-b border-blue-200 bg-white/50 pb-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-12 h-12 bg-blue-600 rounded-xl flex items-center justify-center">
|
|
<Calendar className="w-6 h-6 text-white" />
|
|
</div>
|
|
<div>
|
|
<CardTitle className="text-2xl text-[#1C323E]">Today's Orders</CardTitle>
|
|
<p className="text-sm text-slate-600 mt-1">
|
|
{format(new Date(), 'EEEE, MMMM d, yyyy')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Badge className="bg-blue-600 text-white text-lg px-4 py-2">
|
|
{todayOrders.length} Active
|
|
</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="p-6">
|
|
<div className="grid grid-cols-3 gap-4">
|
|
{todayOrders.map((order) => {
|
|
const assignedCount = order.assigned_staff?.length || 0;
|
|
const requestedCount = order.requested || 0;
|
|
const fillPercent = requestedCount > 0 ? Math.round((assignedCount / requestedCount) * 100) : 0;
|
|
|
|
return (
|
|
<Link
|
|
key={order.id}
|
|
to={createPageUrl(`EventDetail?id=${order.id}`)}
|
|
className="block"
|
|
>
|
|
<Card className="bg-white border-2 border-slate-200 hover:border-blue-500 hover:shadow-lg transition-all group">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<h3 className="font-bold text-[#1C323E] text-lg group-hover:text-blue-600 transition-colors">
|
|
{order.event_name}
|
|
</h3>
|
|
{fillPercent >= 100 ? (
|
|
<CheckCircle className="w-5 h-5 text-green-600" />
|
|
) : (
|
|
<AlertCircle className="w-5 h-5 text-orange-600" />
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2 mb-4">
|
|
<div className="flex items-center gap-2 text-sm text-slate-600">
|
|
<Clock className="w-4 h-4" />
|
|
<span>{order.shifts?.[0]?.roles?.[0]?.start_time || 'Time TBD'}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm text-slate-600">
|
|
<Users className="w-4 h-4" />
|
|
<span>{assignedCount}/{requestedCount} Staff</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between text-xs text-slate-600">
|
|
<span>Staffing Progress</span>
|
|
<span className="font-bold">{fillPercent}%</span>
|
|
</div>
|
|
<div className="w-full h-2 bg-slate-200 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full transition-all ${
|
|
fillPercent >= 100 ? 'bg-green-500' :
|
|
fillPercent >= 50 ? 'bg-blue-500' :
|
|
'bg-orange-500'
|
|
}`}
|
|
style={{ width: `${fillPercent}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Main Dashboard Grid */}
|
|
<div className="grid grid-cols-3 gap-6">
|
|
|
|
{/* Left Column - Labor & Cost Analytics */}
|
|
<div className="col-span-2 space-y-6">
|
|
|
|
{/* Labor Summary */}
|
|
<Card className="border-slate-200 shadow-sm">
|
|
<CardHeader className="border-b border-slate-100 bg-gradient-to-r from-slate-50 to-white pb-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-purple-100 rounded-xl flex items-center justify-center">
|
|
<Users className="w-5 h-5 text-purple-600" />
|
|
</div>
|
|
<div>
|
|
<CardTitle className="text-xl">Labor Summary</CardTitle>
|
|
<p className="text-sm text-slate-500 mt-1">This month breakdown</p>
|
|
</div>
|
|
</div>
|
|
<Badge className="bg-purple-100 text-purple-700 text-sm px-3 py-1">
|
|
{thisMonthOrders.length} Orders
|
|
</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="p-6">
|
|
<div className="overflow-hidden">
|
|
<table className="w-full">
|
|
<thead className="border-b-2 border-slate-200">
|
|
<tr className="bg-slate-50">
|
|
<th className="text-left py-3 px-4 text-xs font-bold text-slate-600 uppercase">Position</th>
|
|
<th className="text-center py-3 px-4 text-xs font-bold text-slate-600 uppercase">Headcount</th>
|
|
<th className="text-center py-3 px-4 text-xs font-bold text-slate-600 uppercase">Hours</th>
|
|
<th className="text-right py-3 px-4 text-xs font-bold text-slate-600 uppercase">Total Cost</th>
|
|
<th className="text-right py-3 px-4 text-xs font-bold text-slate-600 uppercase">Avg/Hour</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{laborByPosition.length > 0 ? (
|
|
laborByPosition.map((item, idx) => (
|
|
<tr key={item.position} className="border-b border-slate-100 hover:bg-blue-50 transition-colors">
|
|
<td className="py-4 px-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`w-3 h-3 rounded-full`} style={{ backgroundColor: COLORS[idx % COLORS.length] }} />
|
|
<span className="font-semibold text-[#1C323E]">{item.position}</span>
|
|
</div>
|
|
</td>
|
|
<td className="py-4 px-4 text-center">
|
|
<Badge variant="outline" className="font-bold">
|
|
{item.headcount}
|
|
</Badge>
|
|
</td>
|
|
<td className="py-4 px-4 text-center">
|
|
<span className="font-semibold text-slate-700">
|
|
{Math.round(item.hours)}h
|
|
</span>
|
|
</td>
|
|
<td className="py-4 px-4 text-right">
|
|
<span className="font-bold text-lg text-[#1C323E]">
|
|
${Math.round(item.cost).toLocaleString()}
|
|
</span>
|
|
</td>
|
|
<td className="py-4 px-4 text-right">
|
|
<span className="text-sm text-slate-600">
|
|
${Math.round(item.cost / item.hours)}/hr
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
))
|
|
) : (
|
|
<tr>
|
|
<td colSpan="5" className="py-8 text-center text-slate-500">
|
|
No labor data for this month
|
|
</td>
|
|
</tr>
|
|
)}
|
|
{laborByPosition.length > 0 && (
|
|
<tr className="bg-slate-50 font-bold border-t-2 border-slate-300">
|
|
<td className="py-4 px-4 text-[#1C323E]">TOTAL</td>
|
|
<td className="py-4 px-4 text-center text-[#1C323E]">{totalHeadcount}</td>
|
|
<td className="py-4 px-4 text-center text-[#1C323E]">{Math.round(totalHours)}h</td>
|
|
<td className="py-4 px-4 text-right text-xl text-[#1C323E]">
|
|
${Math.round(totalLaborCost).toLocaleString()}
|
|
</td>
|
|
<td className="py-4 px-4 text-right text-[#1C323E]">
|
|
${Math.round(avgCostPerHour)}/hr
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Cost Analysis */}
|
|
<div className="grid grid-cols-2 gap-6">
|
|
{/* Pie Chart */}
|
|
<Card className="border-slate-200 shadow-sm">
|
|
<CardHeader className="border-b border-slate-100 pb-4">
|
|
<CardTitle className="text-lg">Cost Distribution</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-6">
|
|
{pieChartData.length > 0 ? (
|
|
<>
|
|
<ResponsiveContainer width="100%" height={200}>
|
|
<PieChart>
|
|
<Pie
|
|
data={pieChartData}
|
|
cx="50%"
|
|
cy="50%"
|
|
innerRadius={60}
|
|
outerRadius={80}
|
|
paddingAngle={5}
|
|
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">
|
|
{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">{item.name}</span>
|
|
</div>
|
|
<span className="font-bold text-[#1C323E]">
|
|
${Math.round(item.value).toLocaleString()}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="text-center py-8 text-slate-500">
|
|
No cost data available
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Cost Summary Cards */}
|
|
<div className="space-y-4">
|
|
<Card className="border-slate-200 shadow-sm bg-gradient-to-br from-blue-50 to-indigo-50">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<div className="w-10 h-10 bg-blue-600 rounded-xl flex items-center justify-center">
|
|
<DollarSign className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-slate-600 font-medium">This Month</p>
|
|
<p className="text-2xl font-bold text-[#1C323E]">
|
|
${Math.round(totalLaborCost / 1000)}k
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{costChange >= 0 ? (
|
|
<TrendingUp className="w-4 h-4 text-red-600" />
|
|
) : (
|
|
<TrendingDown className="w-4 h-4 text-green-600" />
|
|
)}
|
|
<span className={`text-sm font-semibold ${
|
|
costChange >= 0 ? 'text-red-600' : 'text-green-600'
|
|
}`}>
|
|
{Math.abs(costChange).toFixed(1)}% vs last month
|
|
</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border-slate-200 shadow-sm">
|
|
<CardContent className="p-5">
|
|
<p className="text-xs text-slate-600 mb-2">Avg Cost per Hour</p>
|
|
<p className="text-3xl font-bold text-[#1C323E] mb-1">
|
|
${Math.round(avgCostPerHour)}
|
|
</p>
|
|
<p className="text-xs text-slate-500">
|
|
Across {totalHours.toFixed(0)} hours
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border-slate-200 shadow-sm">
|
|
<CardContent className="p-5">
|
|
<p className="text-xs text-slate-600 mb-2">Total Staff Hired</p>
|
|
<p className="text-3xl font-bold text-[#1C323E] mb-1">
|
|
{totalHeadcount}
|
|
</p>
|
|
<p className="text-xs text-slate-500">
|
|
This month
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Column - Quick Actions */}
|
|
<div className="space-y-6">
|
|
|
|
{/* Quick Reorder - DoorDash Style */}
|
|
<Card className="border-slate-200 shadow-sm">
|
|
<CardHeader className="border-b border-slate-100 pb-4 bg-gradient-to-r from-green-50 to-emerald-50">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-green-600 rounded-xl flex items-center justify-center">
|
|
<RefreshCw className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<CardTitle className="text-lg">Reorder Favorites</CardTitle>
|
|
<p className="text-xs text-slate-600 mt-1">One tap to reorder</p>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="p-4 space-y-3">
|
|
{favoriteOrders.length > 0 ? (
|
|
favoriteOrders.map((item) => {
|
|
const { event, count } = item;
|
|
const isReordering = reorderingId === event.id;
|
|
|
|
return (
|
|
<div
|
|
key={event.id}
|
|
className="p-4 rounded-xl bg-white border-2 border-slate-200 hover:border-green-500 hover:shadow-md transition-all group"
|
|
>
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<Badge className="bg-green-100 text-green-700 text-xs">
|
|
Ordered {count}x
|
|
</Badge>
|
|
</div>
|
|
<h4 className="font-bold text-[#1C323E] group-hover:text-green-600 transition-colors">
|
|
{event.event_name}
|
|
</h4>
|
|
<p className="text-xs text-slate-500 mt-1">
|
|
{event.hub || 'No location'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between pt-3 border-t border-slate-100">
|
|
<div className="text-sm">
|
|
<span className="text-slate-600">Last: </span>
|
|
<span className="font-semibold text-slate-700">
|
|
{format(parseISO(event.date), "MMM d")}
|
|
</span>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
disabled={isReordering}
|
|
onClick={() => handleQuickReorder(event)}
|
|
className="bg-green-600 hover:bg-green-700 text-white"
|
|
>
|
|
{isReordering ? (
|
|
<>
|
|
<RefreshCw className="w-3 h-3 mr-2 animate-spin" />
|
|
Ordering...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Plus className="w-3 h-3 mr-2" />
|
|
Reorder
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<RefreshCw className="w-12 h-12 mx-auto text-slate-300 mb-3" />
|
|
<p className="text-sm text-slate-600 font-medium">No previous orders</p>
|
|
<p className="text-xs text-slate-400 mt-1">Your favorites will appear here</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Upcoming Orders */}
|
|
<Card className="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-indigo-100 rounded-xl flex items-center justify-center">
|
|
<Calendar className="w-5 h-5 text-indigo-600" />
|
|
</div>
|
|
<div>
|
|
<CardTitle className="text-lg">Coming Up</CardTitle>
|
|
<p className="text-xs text-slate-600 mt-1">Next 7 days</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="p-4 space-y-3">
|
|
{upcomingOrders.length > 0 ? (
|
|
upcomingOrders.map((order) => {
|
|
const eventDate = new Date(order.date);
|
|
const daysUntil = differenceInDays(eventDate, new Date());
|
|
const isUrgent = daysUntil <= 2;
|
|
|
|
return (
|
|
<Link
|
|
key={order.id}
|
|
to={createPageUrl(`EventDetail?id=${order.id}`)}
|
|
className="block"
|
|
>
|
|
<div className={`p-4 rounded-xl border-2 transition-all hover:shadow-md ${
|
|
isUrgent
|
|
? 'bg-orange-50 border-orange-200 hover:border-orange-400'
|
|
: 'bg-white border-slate-200 hover:border-blue-400'
|
|
}`}>
|
|
<div className="flex items-start justify-between mb-2">
|
|
<h4 className="font-bold text-[#1C323E] text-sm">
|
|
{order.event_name}
|
|
</h4>
|
|
{isUrgent && (
|
|
<Badge className="bg-orange-600 text-white text-xs">
|
|
{daysUntil === 0 ? 'Today' : daysUntil === 1 ? 'Tomorrow' : `${daysUntil}d`}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center justify-between text-xs text-slate-600">
|
|
<span>{format(eventDate, "MMM d, h:mm a")}</span>
|
|
<ChevronRight className="w-4 h-4" />
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
);
|
|
})
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<Calendar className="w-12 h-12 mx-auto text-slate-300 mb-3" />
|
|
<p className="text-sm text-slate-600">No upcoming orders</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Quick Actions */}
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<Link to={createPageUrl("VendorMarketplace")}>
|
|
<Card className="bg-gradient-to-br from-purple-500 to-purple-600 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-sm">Find Vendors</p>
|
|
<p className="text-white/70 text-xs mt-1">Browse marketplace</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
|
|
<Link to={createPageUrl("Messages")}>
|
|
<Card className="bg-white border-slate-200 shadow-sm hover:shadow-md transition-all cursor-pointer group">
|
|
<CardContent className="p-5 text-center">
|
|
<div className="w-12 h-12 mx-auto mb-3 bg-blue-50 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
|
<MessageSquare className="w-6 h-6 text-blue-600" />
|
|
</div>
|
|
<p className="text-[#1C323E] font-bold text-sm">Messages</p>
|
|
<p className="text-slate-500 text-xs mt-1">Chat with vendors</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |