505 lines
24 KiB
JavaScript
505 lines
24 KiB
JavaScript
|
|
import React, { useState, useMemo } from "react";
|
|
import { base44 } from "@/api/base44Client";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { createPageUrl } from "@/utils";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { Search, Eye, Edit, Copy, UserCheck, Zap, Clock, Users, RefreshCw, Calendar as CalendarIcon, AlertTriangle, List, LayoutGrid, CheckCircle, FileText, X, MapPin } from "lucide-react";
|
|
import { useToast } from "@/components/ui/use-toast";
|
|
import { format, parseISO, isValid } from "date-fns";
|
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
import SmartAssignModal from "@/components/events/SmartAssignModal";
|
|
import { autoFillShifts } from "@/components/scheduling/SmartAssignmentEngine";
|
|
import { detectAllConflicts, ConflictAlert } from "@/components/scheduling/ConflictDetection";
|
|
|
|
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, formatStr) => {
|
|
const date = safeParseDate(dateString);
|
|
if (!date) return "-";
|
|
try { return format(date, formatStr); } catch { return "-"; }
|
|
};
|
|
|
|
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, hasConflicts) => {
|
|
if (event.is_rapid) {
|
|
return (
|
|
<div className="relative 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
|
|
{hasConflicts && (
|
|
<AlertTriangle className="w-3 h-3 absolute -top-1 -right-1 text-orange-500 bg-white rounded-full p-0.5" />
|
|
)}
|
|
</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={`relative 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}
|
|
{hasConflicts && (
|
|
<AlertTriangle className="w-3 h-3 absolute -top-1 -right-1 text-orange-500 bg-white rounded-full p-0.5" />
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default function VendorOrders() {
|
|
const navigate = useNavigate();
|
|
const queryClient = useQueryClient();
|
|
const { toast } = useToast();
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [activeTab, setActiveTab] = useState("all");
|
|
const [viewMode, setViewMode] = useState("table");
|
|
const [showConflicts, setShowConflicts] = useState(true);
|
|
const [assignModal, setAssignModal] = useState({ open: false, event: null });
|
|
|
|
const [assignmentOptions] = useState({
|
|
prioritizeSkill: true,
|
|
prioritizeReliability: true,
|
|
prioritizeVendor: true,
|
|
prioritizeFatigue: true,
|
|
prioritizeCompliance: true,
|
|
prioritizeProximity: true,
|
|
prioritizeCost: false,
|
|
});
|
|
|
|
const { data: user } = useQuery({
|
|
queryKey: ['current-user-vendor-orders'],
|
|
queryFn: () => base44.auth.me(),
|
|
});
|
|
|
|
const { data: allEvents = [] } = useQuery({
|
|
queryKey: ['all-events-vendor'],
|
|
queryFn: () => base44.entities.Event.list('-date'),
|
|
});
|
|
|
|
const { data: allStaff = [] } = useQuery({
|
|
queryKey: ['staff-for-auto-assign'],
|
|
queryFn: () => base44.entities.Staff.list(),
|
|
});
|
|
|
|
const { data: vendorRates = [] } = useQuery({
|
|
queryKey: ['vendor-rates-auto-assign'],
|
|
queryFn: () => base44.entities.VendorRate.list(),
|
|
initialData: [],
|
|
});
|
|
|
|
const updateEventMutation = useMutation({
|
|
mutationFn: ({ id, data }) => base44.entities.Event.update(id, data),
|
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['all-events-vendor'] }),
|
|
});
|
|
|
|
const autoAssignMutation = useMutation({
|
|
mutationFn: async (event) => {
|
|
const assignments = await autoFillShifts(event, allStaff, vendorEvents, vendorRates, assignmentOptions);
|
|
if (assignments.length === 0) throw new Error("No suitable staff found");
|
|
|
|
const updatedAssignedStaff = [...(event.assigned_staff || []), ...assignments];
|
|
const updatedShifts = (event.shifts || []).map(shift => {
|
|
const updatedRoles = (shift.roles || []).map(role => {
|
|
const roleAssignments = assignments.filter(a => a.role === role.role);
|
|
return { ...role, assigned: (role.assigned || 0) + roleAssignments.length };
|
|
});
|
|
return { ...shift, roles: updatedRoles };
|
|
});
|
|
|
|
const totalRequested = updatedShifts.reduce((accShift, shift) => {
|
|
return accShift + (shift.roles?.reduce((accRole, role) => accRole + (role.count || 0), 0) || 0);
|
|
}, 0);
|
|
|
|
const totalAssigned = updatedAssignedStaff.length;
|
|
let newStatus = event.status;
|
|
|
|
if (totalAssigned >= totalRequested && totalRequested > 0) {
|
|
newStatus = 'Fully Staffed';
|
|
} else if (totalAssigned > 0 && totalAssigned < totalRequested) {
|
|
newStatus = 'Partial Staffed';
|
|
} else if (totalAssigned === 0) {
|
|
newStatus = 'Pending';
|
|
}
|
|
|
|
await base44.entities.Event.update(event.id, {
|
|
assigned_staff: updatedAssignedStaff,
|
|
shifts: updatedShifts,
|
|
requested: (event.requested || 0) + assignments.length,
|
|
status: newStatus,
|
|
});
|
|
|
|
return assignments.length;
|
|
},
|
|
onSuccess: (count) => {
|
|
queryClient.invalidateQueries({ queryKey: ['all-events-vendor'] });
|
|
toast({ title: "✅ Auto-Assigned", description: `Assigned ${count} staff automatically` });
|
|
},
|
|
onError: (error) => {
|
|
toast({ title: "⚠️ Auto-Assign Failed", description: error.message, variant: "destructive" });
|
|
},
|
|
});
|
|
|
|
const vendorEvents = useMemo(() => {
|
|
return allEvents.filter(e =>
|
|
e.vendor_name === user?.company_name ||
|
|
e.vendor_id === user?.id ||
|
|
e.created_by === user?.email
|
|
);
|
|
}, [allEvents, user]);
|
|
|
|
const eventsWithConflicts = useMemo(() => {
|
|
return vendorEvents.map(event => {
|
|
const conflicts = detectAllConflicts(event, vendorEvents);
|
|
return { ...event, detected_conflicts: conflicts };
|
|
});
|
|
}, [vendorEvents]);
|
|
|
|
const totalConflicts = eventsWithConflicts.reduce((sum, e) => sum + (e.detected_conflicts?.length || 0), 0);
|
|
|
|
const filteredEvents = useMemo(() => {
|
|
let filtered = eventsWithConflicts;
|
|
|
|
if (activeTab === "upcoming") filtered = filtered.filter(e => { const eventDate = safeParseDate(e.date); return eventDate && eventDate > new Date(); });
|
|
else if (activeTab === "active") filtered = filtered.filter(e => e.status === "Active");
|
|
else if (activeTab === "past") filtered = filtered.filter(e => e.status === "Completed");
|
|
else if (activeTab === "conflicts") filtered = filtered.filter(e => e.detected_conflicts && e.detected_conflicts.length > 0);
|
|
|
|
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)
|
|
);
|
|
}
|
|
|
|
return filtered;
|
|
}, [eventsWithConflicts, searchTerm, activeTab]);
|
|
|
|
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 fillPercent = totalRequested > 0 ? Math.round((assigned / totalRequested) * 100) : 0;
|
|
|
|
if (assigned === 0) return { color: 'bg-slate-100 text-slate-600', text: '0', percent: '0%', status: 'empty' };
|
|
if (totalRequested > 0 && assigned >= totalRequested) return { color: 'bg-emerald-500 text-white', text: assigned, percent: '100%', status: 'full' };
|
|
if (totalRequested > 0 && assigned < totalRequested) return { color: 'bg-orange-500 text-white', text: assigned, percent: `${fillPercent}%`, status: 'partial' };
|
|
return { color: 'bg-slate-500 text-white', text: assigned, percent: '0%', status: 'partial' };
|
|
};
|
|
|
|
const getTabCount = (tab) => {
|
|
if (tab === "all") return vendorEvents.length;
|
|
if (tab === "conflicts") return eventsWithConflicts.filter(e => e.detected_conflicts && e.detected_conflicts.length > 0).length;
|
|
if (tab === "upcoming") return vendorEvents.filter(e => { const eventDate = safeParseDate(e.date); return eventDate && eventDate > new Date(); }).length;
|
|
if (tab === "active") return vendorEvents.filter(e => e.status === "Active").length;
|
|
if (tab === "past") return vendorEvents.filter(e => e.status === "Completed").length;
|
|
return 0;
|
|
};
|
|
|
|
// The original handleAutoAssignEvent is removed as the button now opens the modal directly.
|
|
// const handleAutoAssignEvent = (event) => autoAssignMutation.mutate(event);
|
|
|
|
const getEventTimes = (event) => {
|
|
const firstShift = event.shifts?.[0];
|
|
const rolesInFirstShift = firstShift?.roles || [];
|
|
|
|
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-[1800px] mx-auto">
|
|
<div className="mb-6">
|
|
<h1 className="text-2xl font-bold text-slate-900">Order Management</h1>
|
|
<p className="text-sm text-slate-500 mt-1">View, assign, and track all your orders</p>
|
|
</div>
|
|
|
|
{showConflicts && totalConflicts > 0 && (
|
|
<Alert className="mb-6 border-2 border-orange-500 bg-orange-50">
|
|
<div className="flex items-start gap-3">
|
|
<AlertTriangle className="w-5 h-5 text-orange-600 flex-shrink-0 mt-0.5" />
|
|
<div className="flex-1">
|
|
<AlertDescription className="font-semibold text-orange-900">
|
|
{totalConflicts} scheduling conflict{totalConflicts !== 1 ? 's' : ''} detected
|
|
</AlertDescription>
|
|
</div>
|
|
<Button variant="ghost" size="icon" onClick={() => setShowConflicts(false)} className="flex-shrink-0">
|
|
<RefreshCw className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</Alert>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
|
<Card className="border border-red-200 bg-red-50">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-red-500 rounded-lg flex items-center justify-center">
|
|
<Zap className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-red-600 font-semibold uppercase">RAPID</p>
|
|
<p className="text-2xl font-bold text-red-700">{vendorEvents.filter(e => e.is_rapid).length}</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border border-amber-200 bg-amber-50">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-amber-500 rounded-lg flex items-center justify-center">
|
|
<Clock className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-amber-600 font-semibold uppercase">REQUESTED</p>
|
|
<p className="text-2xl font-bold text-amber-700">{vendorEvents.filter(e => e.status === 'Pending' || e.status === 'Draft').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">
|
|
<AlertTriangle className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-orange-600 font-semibold uppercase">PARTIAL</p>
|
|
<p className="text-2xl font-bold text-orange-700">{vendorEvents.filter(e => {
|
|
const status = getAssignmentStatus(e);
|
|
return status.status === 'partial';
|
|
}).length}</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border border-emerald-200 bg-emerald-50">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-emerald-500 rounded-lg flex items-center justify-center">
|
|
<CheckCircle className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-emerald-600 font-semibold uppercase">FULLY STAFFED</p>
|
|
<p className="text-2xl font-bold text-emerald-700">{vendorEvents.filter(e => {
|
|
const status = getAssignmentStatus(e);
|
|
return status.status === 'full';
|
|
}).length}</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-xl p-4 mb-6 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-4 h-4 text-slate-400" />
|
|
<Input placeholder="Search by event, business, or location..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="pl-10 border-slate-200 h-10" />
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button variant={viewMode === "table" ? "default" : "outline"} size="sm" onClick={() => setViewMode("table")} className={viewMode === "table" ? "bg-[#0A39DF]" : ""}>
|
|
<List className="w-4 h-4" />
|
|
</Button>
|
|
<Button variant={viewMode === "scheduler" ? "default" : "outline"} size="sm" onClick={() => setViewMode("scheduler")} className={viewMode === "scheduler" ? "bg-[#0A39DF]" : ""}>
|
|
<LayoutGrid className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="mb-6">
|
|
<TabsList className="bg-white border">
|
|
<TabsTrigger value="all">All ({getTabCount("all")})</TabsTrigger>
|
|
<TabsTrigger value="conflicts" className="data-[state=active]:bg-orange-500 data-[state=active]:text-white">
|
|
<AlertTriangle className="w-4 h-4 mr-2" />
|
|
Conflicts ({getTabCount("conflicts")})
|
|
</TabsTrigger>
|
|
<TabsTrigger value="upcoming">Upcoming ({getTabCount("upcoming")})</TabsTrigger>
|
|
<TabsTrigger value="active">Active ({getTabCount("active")})</TabsTrigger>
|
|
<TabsTrigger value="past">Past ({getTabCount("past")})</TabsTrigger>
|
|
</TabsList>
|
|
</Tabs>
|
|
|
|
<div className="bg-white rounded-xl shadow-sm border overflow-hidden">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="bg-slate-50 hover:bg-slate-50 border-b">
|
|
<TableHead className="font-semibold text-slate-600 uppercase text-[10px] tracking-wide h-10">BUSINESS</TableHead>
|
|
<TableHead className="font-semibold text-slate-600 uppercase text-[10px] tracking-wide">HUB</TableHead>
|
|
<TableHead className="font-semibold text-slate-600 uppercase text-[10px] tracking-wide">EVENT</TableHead>
|
|
<TableHead className="font-semibold text-slate-600 uppercase text-[10px] tracking-wide">DATE & TIME</TableHead>
|
|
<TableHead className="font-semibold text-slate-600 uppercase text-[10px] tracking-wide">STATUS</TableHead>
|
|
<TableHead className="font-semibold text-slate-600 uppercase text-[10px] tracking-wide text-center">REQUESTED</TableHead>
|
|
<TableHead className="font-semibold text-slate-600 uppercase text-[10px] tracking-wide text-center">ASSIGNED</TableHead>
|
|
<TableHead className="font-semibold text-slate-600 uppercase text-[10px] tracking-wide text-center">INVOICE</TableHead>
|
|
<TableHead className="font-semibold text-slate-600 uppercase text-[10px] tracking-wide text-center">ACTIONS</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredEvents.length === 0 ? (
|
|
<TableRow><TableCell colSpan={9} className="text-center py-12 text-slate-500"><CalendarIcon className="w-12 h-12 mx-auto mb-3 text-slate-300" /><p className="font-medium">No orders found</p></TableCell></TableRow>
|
|
) : (
|
|
filteredEvents.map((event) => {
|
|
const assignmentStatus = getAssignmentStatus(event);
|
|
const showAutoButton = assignmentStatus.status !== 'full' && event.status !== 'Canceled' && event.status !== 'Completed';
|
|
const hasConflicts = event.detected_conflicts && event.detected_conflicts.length > 0;
|
|
const eventTimes = getEventTimes(event);
|
|
const eventDate = safeParseDate(event.date);
|
|
const dayOfWeek = eventDate ? format(eventDate, 'EEEE') : '';
|
|
const invoiceReady = event.status === "Completed";
|
|
|
|
return (
|
|
<React.Fragment key={event.id}>
|
|
<TableRow className="hover:bg-slate-50 transition-colors border-b">
|
|
<TableCell className="py-3">
|
|
<p className="text-sm text-slate-700 font-medium">{event.business_name || "—"}</p>
|
|
</TableCell>
|
|
<TableCell className="py-3">
|
|
<div className="flex items-center gap-1.5 text-sm text-slate-500">
|
|
<MapPin className="w-3.5 h-3.5" />
|
|
{event.hub || event.event_location || "Main Hub"}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="py-3">
|
|
<p className="font-semibold text-slate-900 text-sm">{event.event_name}</p>
|
|
</TableCell>
|
|
<TableCell className="py-3">
|
|
<div className="space-y-0.5">
|
|
<p className="text-sm text-slate-900 font-semibold">{eventDate ? format(eventDate, 'MM.dd.yyyy') : '-'}</p>
|
|
<p className="text-xs text-slate-500">{dayOfWeek}</p>
|
|
<div className="flex items-center gap-1 text-xs text-slate-600 mt-1">
|
|
<Clock className="w-3 h-3" />
|
|
<span>{eventTimes.startTime} - {eventTimes.endTime}</span>
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="py-3">
|
|
{getStatusBadge(event, hasConflicts)}
|
|
</TableCell>
|
|
<TableCell className="text-center py-3">
|
|
<span className="font-semibold text-slate-700 text-sm">{event.requested || 0}</span>
|
|
</TableCell>
|
|
<TableCell className="text-center py-3">
|
|
<div className="flex flex-col items-center gap-1">
|
|
<Badge className={`${assignmentStatus.color} font-bold px-3 py-1 rounded-full text-xs`}>
|
|
{assignmentStatus.text}
|
|
</Badge>
|
|
<span className="text-[10px] text-slate-500 font-medium">{assignmentStatus.percent}</span>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="text-center py-3">
|
|
<div className="flex items-center justify-center">
|
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity ${invoiceReady ? 'bg-blue-100' : 'bg-slate-100'}`}>
|
|
<FileText className={`w-5 h-5 ${invoiceReady ? 'text-blue-600' : 'text-slate-400'}`} />
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="py-3">
|
|
<div className="flex items-center justify-center gap-1">
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => setAssignModal({ open: true, event: event })}
|
|
className="h-8 px-2 hover:bg-slate-100"
|
|
title="Smart Assign"
|
|
>
|
|
<UserCheck className="w-4 h-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => navigate(createPageUrl(`EventDetail?id=${event.id}`))}
|
|
className="hover:bg-slate-100 h-8 w-8"
|
|
title="View"
|
|
>
|
|
<Eye className="w-4 h-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => navigate(createPageUrl(`EditEvent?id=${event.id}`))}
|
|
className="hover:bg-slate-100 h-8 w-8"
|
|
title="Edit"
|
|
>
|
|
<Edit className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
{hasConflicts && activeTab === "conflicts" && (
|
|
<TableRow>
|
|
<TableCell colSpan={9} className="bg-orange-50/50 py-4">
|
|
<ConflictAlert conflicts={event.detected_conflicts} />
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</React.Fragment>
|
|
);
|
|
})
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
|
|
<SmartAssignModal
|
|
open={assignModal.open}
|
|
onClose={() => setAssignModal({ open: false, event: null })}
|
|
event={assignModal.event}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|