export base44 - Nov 18
This commit is contained in:
@@ -1,282 +1,508 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import React, { useState, useMemo } from "react";
|
||||
import { base44 } from "@/api/base44Client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Calendar as CalendarIcon, MapPin, Users, Clock, DollarSign, FileText, Plus, RefreshCw, Zap } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { createPageUrl } from "@/utils";
|
||||
import { format, addDays } from "date-fns";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Tabs, // New import
|
||||
TabsList, // New import
|
||||
TabsTrigger, // New import
|
||||
} from "@/components/ui/tabs"; // New import
|
||||
import {
|
||||
Search, Calendar, MapPin, Users, Eye, Edit, X, Trash2, FileText, // Edit instead of Edit2
|
||||
Clock, DollarSign, Package, CheckCircle, AlertTriangle, Grid, List, Zap, Plus
|
||||
} from "lucide-react";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import QuickReorderModal from "@/components/events/QuickReorderModal";
|
||||
import { format, parseISO, isValid } from "date-fns";
|
||||
|
||||
const safeParseDate = (dateString) => {
|
||||
if (!dateString) return null;
|
||||
try {
|
||||
const date = typeof dateString === 'string' ? parseISO(dateString) : new Date(dateString);
|
||||
return isValid(date) ? date : null;
|
||||
} catch { return null; }
|
||||
};
|
||||
|
||||
const safeFormatDate = (dateString, formatString) => {
|
||||
const date = safeParseDate(dateString);
|
||||
return date ? format(date, formatString) : '—';
|
||||
};
|
||||
|
||||
const convertTo12Hour = (time24) => {
|
||||
if (!time24) return "-";
|
||||
try {
|
||||
const [hours, minutes] = time24.split(':');
|
||||
const hour = parseInt(hours);
|
||||
const ampm = hour >= 12 ? 'PM' : 'AM';
|
||||
const hour12 = hour % 12 || 12;
|
||||
return `${hour12}:${minutes} ${ampm}`;
|
||||
} catch {
|
||||
return time24;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (event) => {
|
||||
if (event.is_rapid) {
|
||||
return (
|
||||
<div className="inline-flex items-center gap-2 bg-red-500 text-white px-4 py-2 rounded-lg font-semibold text-xs shadow-md">
|
||||
<Zap className="w-3.5 h-3.5 fill-white" />
|
||||
RAPID
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const statusConfig = {
|
||||
'Draft': { bg: 'bg-slate-500', icon: FileText },
|
||||
'Pending': { bg: 'bg-amber-500', icon: Clock },
|
||||
'Partial Staffed': { bg: 'bg-orange-500', icon: AlertTriangle },
|
||||
'Fully Staffed': { bg: 'bg-emerald-500', icon: CheckCircle },
|
||||
'Active': { bg: 'bg-blue-500', icon: Users },
|
||||
'Completed': { bg: 'bg-slate-400', icon: CheckCircle },
|
||||
'Canceled': { bg: 'bg-red-500', icon: X },
|
||||
};
|
||||
|
||||
const config = statusConfig[event.status] || { bg: 'bg-slate-400', icon: Clock };
|
||||
const Icon = config.icon;
|
||||
|
||||
return (
|
||||
<div className={`inline-flex items-center gap-2 ${config.bg} text-white px-4 py-2 rounded-lg font-semibold text-xs shadow-md`}>
|
||||
<Icon className="w-3.5 h-3.5" />
|
||||
{event.status}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function ClientOrders() {
|
||||
const navigate = useNavigate();
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
const [reorderModalOpen, setReorderModalOpen] = useState(false);
|
||||
const [selectedEvent, setSelectedEvent] = useState(null);
|
||||
const queryClient = useQueryClient();
|
||||
const { toast } = useToast();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("all"); // Updated values for Tabs
|
||||
const [cancelDialogOpen, setCancelDialogOpen] = useState(false); // Changed from cancelDialog.open
|
||||
const [orderToCancel, setOrderToCancel] = useState(null); // Changed from cancelDialog.order
|
||||
|
||||
const { data: user } = useQuery({
|
||||
queryKey: ['current-user'],
|
||||
queryKey: ['current-user-client-orders'],
|
||||
queryFn: () => base44.auth.me(),
|
||||
});
|
||||
|
||||
const { data: events } = useQuery({
|
||||
queryKey: ['client-events'],
|
||||
const { data: allEvents = [] } = useQuery({
|
||||
queryKey: ['all-events-client'],
|
||||
queryFn: () => base44.entities.Event.list('-date'),
|
||||
initialData: [],
|
||||
});
|
||||
|
||||
// Filter events by current client
|
||||
const clientEvents = events.filter(e =>
|
||||
e.client_email === user?.email || e.created_by === user?.email
|
||||
);
|
||||
const clientEvents = useMemo(() => {
|
||||
return allEvents.filter(e =>
|
||||
e.client_email === user?.email ||
|
||||
e.business_name === user?.company_name ||
|
||||
e.created_by === user?.email
|
||||
);
|
||||
}, [allEvents, user]);
|
||||
|
||||
const filteredEvents = statusFilter === "all"
|
||||
? clientEvents
|
||||
: clientEvents.filter(e => {
|
||||
if (statusFilter === "rapid_request") return e.is_rapid_request;
|
||||
if (statusFilter === "pending") return e.status?.toLowerCase() === "pending" || e.status?.toLowerCase() === "draft";
|
||||
return e.status?.toLowerCase() === statusFilter;
|
||||
const cancelOrderMutation = useMutation({
|
||||
mutationFn: (orderId) => base44.entities.Event.update(orderId, { status: "Canceled" }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['all-events-client'] });
|
||||
toast({
|
||||
title: "✅ Order Canceled",
|
||||
description: "Your order has been canceled successfully",
|
||||
});
|
||||
setCancelDialogOpen(false); // Updated
|
||||
setOrderToCancel(null); // Updated
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: "❌ Failed to Cancel",
|
||||
description: "Could not cancel order. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
'pending': 'bg-yellow-100 text-yellow-700',
|
||||
'draft': 'bg-gray-100 text-gray-700',
|
||||
'confirmed': 'bg-green-100 text-green-700',
|
||||
'active': 'bg-blue-100 text-blue-700',
|
||||
'completed': 'bg-slate-100 text-slate-700',
|
||||
'canceled': 'bg-red-100 text-red-700',
|
||||
'cancelled': 'bg-red-100 text-red-700',
|
||||
const filteredOrders = useMemo(() => { // Renamed from filteredEvents
|
||||
let filtered = clientEvents;
|
||||
|
||||
if (searchTerm) {
|
||||
const lower = searchTerm.toLowerCase();
|
||||
filtered = filtered.filter(e =>
|
||||
e.event_name?.toLowerCase().includes(lower) ||
|
||||
e.business_name?.toLowerCase().includes(lower) ||
|
||||
e.hub?.toLowerCase().includes(lower) ||
|
||||
e.event_location?.toLowerCase().includes(lower) // Added event_location to search
|
||||
);
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
// Reset time for comparison to only compare dates
|
||||
now.setHours(0, 0, 0, 0);
|
||||
|
||||
filtered = filtered.filter(e => {
|
||||
const eventDate = safeParseDate(e.date);
|
||||
const isCompleted = e.status === "Completed";
|
||||
const isCanceled = e.status === "Canceled";
|
||||
const isFutureOrPresent = eventDate && eventDate >= now;
|
||||
|
||||
if (statusFilter === "active") {
|
||||
return !isCompleted && !isCanceled && isFutureOrPresent;
|
||||
} else if (statusFilter === "completed") {
|
||||
return isCompleted;
|
||||
}
|
||||
return true; // For "all" or other statuses
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}, [clientEvents, searchTerm, statusFilter]);
|
||||
|
||||
const activeOrders = clientEvents.filter(e =>
|
||||
e.status !== "Completed" && e.status !== "Canceled"
|
||||
).length;
|
||||
const completedOrders = clientEvents.filter(e => e.status === "Completed").length;
|
||||
const totalSpent = clientEvents
|
||||
.filter(e => e.status === "Completed")
|
||||
.reduce((sum, e) => sum + (e.total || 0), 0);
|
||||
|
||||
const handleCancelOrder = (order) => {
|
||||
setOrderToCancel(order); // Updated
|
||||
setCancelDialogOpen(true); // Updated
|
||||
};
|
||||
|
||||
const confirmCancel = () => {
|
||||
if (orderToCancel) { // Updated
|
||||
cancelOrderMutation.mutate(orderToCancel.id); // Updated
|
||||
}
|
||||
};
|
||||
|
||||
const canEditOrder = (order) => {
|
||||
const eventDate = safeParseDate(order.date);
|
||||
const now = new Date();
|
||||
return order.status !== "Completed" &&
|
||||
order.status !== "Canceled" &&
|
||||
eventDate && eventDate > now; // Ensure eventDate is valid before comparison
|
||||
};
|
||||
|
||||
const canCancelOrder = (order) => {
|
||||
return order.status !== "Completed" && order.status !== "Canceled";
|
||||
};
|
||||
|
||||
const getAssignmentStatus = (event) => {
|
||||
const totalRequested = event.shifts?.reduce((accShift, shift) => {
|
||||
return accShift + (shift.roles?.reduce((accRole, role) => accRole + (role.count || 0), 0) || 0);
|
||||
}, 0) || 0;
|
||||
|
||||
const assigned = event.assigned_staff?.length || 0;
|
||||
const percentage = totalRequested > 0 ? Math.round((assigned / totalRequested) * 100) : 0;
|
||||
|
||||
let badgeClass = 'bg-slate-100 text-slate-600'; // Default: no staff, or no roles requested
|
||||
if (assigned > 0 && assigned < totalRequested) {
|
||||
badgeClass = 'bg-orange-500 text-white'; // Partial Staffed
|
||||
} else if (assigned >= totalRequested && totalRequested > 0) {
|
||||
badgeClass = 'bg-emerald-500 text-white'; // Fully Staffed
|
||||
} else if (assigned === 0 && totalRequested > 0) {
|
||||
badgeClass = 'bg-red-500 text-white'; // Requested but 0 assigned
|
||||
} else if (assigned > 0 && totalRequested === 0) {
|
||||
badgeClass = 'bg-blue-500 text-white'; // Staff assigned but no roles explicitly requested (e.g., event set up, staff assigned, but roles not detailed or count is 0)
|
||||
}
|
||||
|
||||
return {
|
||||
badgeClass,
|
||||
assigned,
|
||||
requested: totalRequested,
|
||||
percentage,
|
||||
};
|
||||
return colors[status?.toLowerCase()] || 'bg-slate-100 text-slate-700';
|
||||
};
|
||||
|
||||
const stats = {
|
||||
total: clientEvents.length,
|
||||
rapidRequest: clientEvents.filter(e => e.is_rapid_request).length,
|
||||
pending: clientEvents.filter(e => e.status === 'Pending' || e.status === 'Draft').length,
|
||||
confirmed: clientEvents.filter(e => e.status === 'Confirmed').length,
|
||||
completed: clientEvents.filter(e => e.status === 'Completed').length,
|
||||
};
|
||||
const getEventTimes = (event) => {
|
||||
const firstShift = event.shifts?.[0];
|
||||
const rolesInFirstShift = firstShift?.roles || [];
|
||||
|
||||
const handleQuickReorder = (event) => {
|
||||
setSelectedEvent(event);
|
||||
setReorderModalOpen(true);
|
||||
let startTime = null;
|
||||
let endTime = null;
|
||||
|
||||
if (rolesInFirstShift.length > 0) {
|
||||
startTime = rolesInFirstShift[0].start_time || null;
|
||||
endTime = rolesInFirstShift[0].end_time || null;
|
||||
}
|
||||
|
||||
return {
|
||||
startTime: startTime ? convertTo12Hour(startTime) : "-",
|
||||
endTime: endTime ? convertTo12Hour(endTime) : "-"
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-8 bg-slate-50 min-h-screen">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-[#1C323E]">My Orders</h1>
|
||||
<p className="text-slate-500 mt-1">View and manage your event orders</p>
|
||||
<div className="max-w-[1800px] mx-auto space-y-6">
|
||||
<div className=""> {/* Removed mb-6 */}
|
||||
<h1 className="text-2xl font-bold text-slate-900">My Orders</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">View and manage all your orders</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> {/* Removed mb-6 from here as it's now part of space-y-6 */}
|
||||
<Card className="border border-blue-200 bg-blue-50">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center">
|
||||
<Package className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-blue-600 font-semibold uppercase">TOTAL</p>
|
||||
<p className="text-2xl font-bold text-blue-700">{clientEvents.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border border-orange-200 bg-orange-50">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-orange-500 rounded-lg flex items-center justify-center">
|
||||
<Clock className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-orange-600 font-semibold uppercase">ACTIVE</p>
|
||||
<p className="text-2xl font-bold text-orange-700">{activeOrders}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border border-green-200 bg-green-50">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-green-500 rounded-lg flex items-center justify-center">
|
||||
<CheckCircle className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-green-600 font-semibold uppercase">COMPLETED</p>
|
||||
<p className="text-2xl font-bold text-green-700">{completedOrders}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border border-purple-200 bg-purple-50">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-purple-500 rounded-lg flex items-center justify-center">
|
||||
<DollarSign className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-purple-600 font-semibold uppercase">TOTAL SPENT</p>
|
||||
<p className="text-2xl font-bold text-purple-700">${Math.round(totalSpent / 1000)}k</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-4 flex items-center gap-4 border shadow-sm">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" /> {/* Icon size updated */}
|
||||
<Input
|
||||
placeholder="Search orders..." // Placeholder text updated
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10 border-slate-300 h-10" // Class updated
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => navigate(createPageUrl("CreateEvent"))}
|
||||
className="bg-[#0A39DF] hover:bg-[#0A39DF]/90"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
New Order
|
||||
</Button>
|
||||
<Tabs value={statusFilter} onValueChange={setStatusFilter} className="w-fit"> {/* Replaced Select with Tabs */}
|
||||
<TabsList>
|
||||
<TabsTrigger value="all">All</TabsTrigger>
|
||||
<TabsTrigger value="active">Active</TabsTrigger>
|
||||
<TabsTrigger value="completed">Completed</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-6 mb-8">
|
||||
<Card className="border-slate-200">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<FileText className="w-8 h-8 text-[#0A39DF]" />
|
||||
</div>
|
||||
<p className="text-sm text-slate-500">Total Orders</p>
|
||||
<p className="text-3xl font-bold text-[#1C323E]">{stats.total}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-slate-200 shadow-sm"> {/* Card class updated */}
|
||||
<CardContent className="p-0"> {/* CardContent padding updated */}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-slate-50 hover:bg-slate-50"> {/* TableRow class updated */}
|
||||
<TableHead className="font-semibold text-slate-700">Order</TableHead> {/* Updated */}
|
||||
<TableHead className="font-semibold text-slate-700">Date</TableHead> {/* Updated */}
|
||||
<TableHead className="font-semibold text-slate-700">Location</TableHead> {/* Updated */}
|
||||
<TableHead className="font-semibold text-slate-700">Time</TableHead> {/* Updated */}
|
||||
<TableHead className="font-semibold text-slate-700">Status</TableHead> {/* Updated */}
|
||||
<TableHead className="font-semibold text-slate-700 text-center">Staff</TableHead> {/* Updated */}
|
||||
<TableHead className="font-semibold text-slate-700 text-center">Invoice</TableHead> {/* Updated */}
|
||||
<TableHead className="font-semibold text-slate-700 text-center">Actions</TableHead> {/* Updated */}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredOrders.length === 0 ? ( // Using filteredOrders
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-12 text-slate-500"> {/* Colspan updated */}
|
||||
<Package className="w-12 h-12 mx-auto mb-3 text-slate-300" /> {/* Icon updated */}
|
||||
<p className="font-medium">No orders found</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredOrders.map((order) => { // Using filteredOrders, renamed event to order
|
||||
const assignment = getAssignmentStatus(order);
|
||||
const { startTime, endTime } = getEventTimes(order);
|
||||
const invoiceReady = order.status === "Completed";
|
||||
// const eventDate = safeParseDate(order.date); // Not directly used here, safeFormatDate handles it.
|
||||
|
||||
<Card className="border-slate-200 bg-gradient-to-br from-red-50 to-white">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Zap className="w-8 h-8 text-red-600" />
|
||||
</div>
|
||||
<p className="text-sm text-slate-500">Rapid Requests</p>
|
||||
<p className="text-3xl font-bold text-red-600">{stats.rapidRequest}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-slate-200">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Clock className="w-8 h-8 text-yellow-600" />
|
||||
</div>
|
||||
<p className="text-sm text-slate-500">Pending</p>
|
||||
<p className="text-3xl font-bold text-yellow-600">{stats.pending}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-slate-200">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<CalendarIcon className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<p className="text-sm text-slate-500">Confirmed</p>
|
||||
<p className="text-3xl font-bold text-green-600">{stats.confirmed}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-slate-200">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Users className="w-8 h-8 text-blue-600" />
|
||||
</div>
|
||||
<p className="text-sm text-slate-500">Completed</p>
|
||||
<p className="text-3xl font-bold text-blue-600">{stats.completed}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
<div className="flex gap-2 mb-6 flex-wrap">
|
||||
<Button
|
||||
variant={statusFilter === "all" ? "default" : "outline"}
|
||||
onClick={() => setStatusFilter("all")}
|
||||
className={statusFilter === "all" ? "bg-[#0A39DF]" : ""}
|
||||
>
|
||||
All
|
||||
</Button>
|
||||
<Button
|
||||
variant={statusFilter === "rapid_request" ? "default" : "outline"}
|
||||
onClick={() => setStatusFilter("rapid_request")}
|
||||
className={statusFilter === "rapid_request" ? "bg-red-600 hover:bg-red-700" : ""}
|
||||
>
|
||||
<Zap className="w-4 h-4 mr-2" />
|
||||
Rapid Request
|
||||
</Button>
|
||||
<Button
|
||||
variant={statusFilter === "pending" ? "default" : "outline"}
|
||||
onClick={() => setStatusFilter("pending")}
|
||||
className={statusFilter === "pending" ? "bg-[#0A39DF]" : ""}
|
||||
>
|
||||
Pending
|
||||
</Button>
|
||||
<Button
|
||||
variant={statusFilter === "confirmed" ? "default" : "outline"}
|
||||
onClick={() => setStatusFilter("confirmed")}
|
||||
className={statusFilter === "confirmed" ? "bg-[#0A39DF]" : ""}
|
||||
>
|
||||
Confirmed
|
||||
</Button>
|
||||
<Button
|
||||
variant={statusFilter === "completed" ? "default" : "outline"}
|
||||
onClick={() => setStatusFilter("completed")}
|
||||
className={statusFilter === "completed" ? "bg-[#0A39DF]" : ""}
|
||||
>
|
||||
Completed
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Orders List */}
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{filteredEvents.length > 0 ? (
|
||||
filteredEvents.map((event) => (
|
||||
<Card key={event.id} className="border-slate-200 hover:shadow-lg transition-shadow">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<h3 className="text-xl font-bold text-[#1C323E]">{event.event_name}</h3>
|
||||
<Badge className={getStatusColor(event.status)}>
|
||||
{event.status}
|
||||
</Badge>
|
||||
{event.is_rapid_request && (
|
||||
<Badge className="bg-red-100 text-red-700 border-red-200 border">
|
||||
<Zap className="w-3 h-3 mr-1" />
|
||||
Rapid Request
|
||||
</Badge>
|
||||
)}
|
||||
{event.include_backup && (
|
||||
<Badge className="bg-green-100 text-green-700 border-green-200 border">
|
||||
🛡️ {event.backup_staff_count || 0} Backup Staff
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm text-slate-600 mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarIcon className="w-4 h-4" />
|
||||
<span>{event.date ? format(new Date(event.date), 'PPP') : 'Date TBD'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4" />
|
||||
<span>{event.event_location || event.hub || 'Location TBD'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4" />
|
||||
<span>{event.assigned || 0} of {event.requested || 0} staff</span>
|
||||
</div>
|
||||
{event.total && (
|
||||
<div className="flex items-center gap-2">
|
||||
<DollarSign className="w-4 h-4" />
|
||||
<span className="font-semibold">${event.total.toLocaleString()}</span>
|
||||
return (
|
||||
<TableRow key={order.id} className="hover:bg-slate-50">
|
||||
<TableCell> {/* Order cell */}
|
||||
<div>
|
||||
<p className="font-semibold text-slate-900">{order.event_name}</p>
|
||||
<p className="text-xs text-slate-500">{order.business_name || "—"}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => navigate(createPageUrl("EventDetail") + `?id=${event.id}`)}
|
||||
variant="outline"
|
||||
className="hover:bg-[#0A39DF] hover:text-white"
|
||||
>
|
||||
View Details
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleQuickReorder(event)}
|
||||
className="bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 text-white shadow-lg"
|
||||
size="lg"
|
||||
>
|
||||
<RefreshCw className="w-5 h-5 mr-2" />
|
||||
Reorder
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{event.notes && (
|
||||
<div className="mt-3 p-3 bg-slate-50 rounded-lg">
|
||||
<p className="text-sm text-slate-600">{event.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<Card className="border-slate-200">
|
||||
<CardContent className="p-12 text-center">
|
||||
<FileText className="w-16 h-16 mx-auto text-slate-300 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-slate-700 mb-2">No orders found</h3>
|
||||
<p className="text-slate-500 mb-6">Get started by creating your first order</p>
|
||||
<Button
|
||||
onClick={() => navigate(createPageUrl("CreateEvent"))}
|
||||
className="bg-[#0A39DF] hover:bg-[#0A39DF]/90"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create Order
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick Reorder Modal */}
|
||||
{selectedEvent && (
|
||||
<QuickReorderModal
|
||||
event={selectedEvent}
|
||||
open={reorderModalOpen}
|
||||
onOpenChange={setReorderModalOpen}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell> {/* Date cell */}
|
||||
<div>
|
||||
<p className="font-semibold text-slate-900">
|
||||
{safeFormatDate(order.date, 'MMM dd, yyyy')}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
{safeFormatDate(order.date, 'EEEE')}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell> {/* Location cell */}
|
||||
<div className="flex items-center gap-1.5 text-sm text-slate-600">
|
||||
<MapPin className="w-3.5 h-3.5 text-slate-400" />
|
||||
{order.hub || order.event_location || "—"}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell> {/* Time cell */}
|
||||
<div className="flex items-center gap-1 text-sm text-slate-600">
|
||||
<Clock className="w-3.5 h-3.5 text-slate-400" />
|
||||
{startTime} - {endTime}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell> {/* Status cell */}
|
||||
{getStatusBadge(order)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center"> {/* Staff cell */}
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<Badge className={assignment.badgeClass}>
|
||||
{assignment.assigned} / {assignment.requested}
|
||||
</Badge>
|
||||
<span className="text-[10px] text-slate-500 font-medium">
|
||||
{assignment.percentage}%
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-center"> {/* Invoice cell */}
|
||||
<div className="flex items-center justify-center">
|
||||
<Button // Changed from a div to a Button for better accessibility
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => invoiceReady && navigate(createPageUrl('Invoices'))}
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center ${invoiceReady ? 'bg-blue-100' : 'bg-slate-100'} ${invoiceReady ? 'cursor-pointer hover:bg-blue-200' : 'cursor-not-allowed opacity-50'}`}
|
||||
disabled={!invoiceReady}
|
||||
title={invoiceReady ? "View Invoice" : "Invoice not available"}
|
||||
>
|
||||
<FileText className={`w-5 h-5 ${invoiceReady ? 'text-blue-600' : 'text-slate-400'}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell> {/* Actions cell */}
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => navigate(createPageUrl(`EventDetail?id=${order.id}`))}
|
||||
className="hover:bg-slate-100"
|
||||
title="View details"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
{canEditOrder(order) && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => navigate(createPageUrl(`EditEvent?id=${order.id}`))}
|
||||
className="hover:bg-slate-100"
|
||||
title="Edit order"
|
||||
>
|
||||
<Edit className="w-4 h-4" /> {/* Changed from Edit2 */}
|
||||
</Button>
|
||||
)}
|
||||
{canCancelOrder(order) && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleCancelOrder(order)} // Updated
|
||||
className="hover:bg-red-50 hover:text-red-600"
|
||||
title="Cancel order"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Dialog open={cancelDialogOpen} onOpenChange={setCancelDialogOpen}> {/* Updated open and onOpenChange */}
|
||||
<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>
|
||||
|
||||
{orderToCancel && ( // Using orderToCancel
|
||||
<div className="bg-slate-50 rounded-lg p-4 space-y-2">
|
||||
<p className="font-bold text-slate-900">{orderToCancel.event_name}</p>
|
||||
<div className="flex items-center gap-2 text-sm text-slate-600">
|
||||
<Calendar className="w-4 h-4" />
|
||||
{orderToCancel.date ? format(new Date(orderToCancel.date), "MMMM d, yyyy") : "—"}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-slate-600">
|
||||
<MapPin className="w-4 h-4" />
|
||||
{orderToCancel.hub || orderToCancel.event_location}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCancelDialogOpen(false)} // Updated
|
||||
>
|
||||
Keep Order
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={confirmCancel}
|
||||
disabled={cancelOrderMutation.isPending}
|
||||
>
|
||||
{cancelOrderMutation.isPending ? "Canceling..." : "Yes, Cancel Order"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user