feat: Initial commit of KROW Workforce Web client (Base44 export)

This commit is contained in:
bwnyasse
2025-11-11 06:08:01 -05:00
commit e571193362
173 changed files with 50898 additions and 0 deletions

561
src/pages/Events.jsx Normal file
View File

@@ -0,0 +1,561 @@
import React, { useState } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery } from "@tanstack/react-query";
import { Link, useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Plus, Search, Calendar as CalendarIcon, Eye, Edit, Copy, X, RefreshCw } from "lucide-react";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import StatusCard from "../components/events/StatusCard";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { format, isSameDay, parseISO, isWithinInterval, startOfDay, endOfDay, isValid } from "date-fns";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import EventHoverCard from "../components/events/EventHoverCard";
import QuickAssignPopover from "../components/events/QuickAssignPopover";
import { Calendar } from "@/components/ui/calendar";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { useToast } from "@/components/ui/use-toast";
import PageHeader from "../components/common/PageHeader";
const statusColors = {
Draft: "bg-gray-100 text-gray-800",
Active: "bg-green-100 text-green-800",
Pending: "bg-purple-100 text-purple-800",
Confirmed: "bg-blue-100 text-blue-800",
Assigned: "bg-yellow-100 text-yellow-800",
Completed: "bg-slate-100 text-slate-800",
Canceled: "bg-red-100 text-red-800",
Cancelled: "bg-red-100 text-red-800"
};
// Helper function to safely parse dates
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;
}
};
// Helper function to safely format dates
const safeFormatDate = (dateString, formatStr) => {
const date = safeParseDate(dateString);
if (!date) return "-";
try {
return format(date, formatStr);
} catch {
return "-";
}
};
export default function Events() {
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState("all");
const [searchTerm, setSearchTerm] = useState("");
const [selectedDates, setSelectedDates] = useState([]);
const [dateRange, setDateRange] = useState(null);
const [selectionMode, setSelectionMode] = useState("multiple");
const [calendarOpen, setCalendarOpen] = useState(false);
const [showAlert, setShowAlert] = useState(true);
const { toast } = useToast();
const { data: events, isLoading } = useQuery({
queryKey: ['events'],
queryFn: () => base44.entities.Event.list('-date'),
initialData: [],
});
const getStatusCounts = () => {
const total = events.length;
const active = events.filter(e => e.status === "Active").length;
const pending = events.filter(e => e.status === "Pending" || e.status === "Assigned").length;
const confirmed = events.filter(e => e.status === "Confirmed").length;
const completed = events.filter(e => e.status === "Completed").length;
return {
active: { count: active, percentage: total ? Math.round((active / total) * 100) : 0 },
pending: { count: pending, percentage: total ? Math.round((pending / total) * 100) : 0 },
confirmed: { count: confirmed, percentage: total ? Math.round((confirmed / total) * 100) : 0 },
completed: { count: completed, percentage: total ? Math.round((completed / total) * 100) : 0 },
};
};
const getFilteredEvents = () => {
let filtered = events;
if (selectionMode === "range" && dateRange?.from) {
filtered = filtered.filter(e => {
const eventDate = safeParseDate(e.date);
if (!eventDate) return false;
if (dateRange.to) {
try {
return isWithinInterval(eventDate, {
start: startOfDay(dateRange.from),
end: endOfDay(dateRange.to)
});
} catch {
return false;
}
} else {
try {
return isSameDay(eventDate, dateRange.from);
} catch {
return false;
}
}
});
} else if (selectionMode === "multiple" && selectedDates.length > 0) {
filtered = filtered.filter(e => {
const eventDate = safeParseDate(e.date);
if (!eventDate) return false;
return selectedDates.some(selectedDate => {
try {
return isSameDay(eventDate, selectedDate);
} catch {
return false;
}
});
});
}
if (activeTab === "last_minute") {
filtered = filtered.filter(e => e.event_type === "Last Minute Request");
} else 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 === "canceled") {
filtered = filtered.filter(e => e.status === "Canceled" || e.status === "Cancelled");
} else if (activeTab === "past") {
filtered = filtered.filter(e => e.status === "Completed");
}
if (searchTerm) {
filtered = filtered.filter(e =>
e.event_name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
e.hub?.toLowerCase().includes(searchTerm.toLowerCase()) ||
e.business_name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
e.id?.toLowerCase().includes(searchTerm.toLowerCase())
);
}
return filtered;
};
const statusCounts = getStatusCounts();
const filteredEvents = getFilteredEvents();
const getTabCount = (tab) => {
if (tab === "all") return events.length;
if (tab === "last_minute") return events.filter(e => e.event_type === "Last Minute Request").length;
if (tab === "upcoming") {
return events.filter(e => {
const eventDate = safeParseDate(e.date);
return eventDate && eventDate > new Date();
}).length;
}
if (tab === "active") return events.filter(e => e.status === "Active").length;
if (tab === "canceled") return events.filter(e => e.status === "Canceled" || e.status === "Cancelled").length;
if (tab === "past") return events.filter(e => e.status === "Completed").length;
return 0;
};
const handleDateSelect = (date) => {
if (selectionMode === "multiple") {
setSelectedDates(prev => {
const exists = prev.some(d => {
try {
return isSameDay(d, date);
} catch {
return false;
}
});
if (exists) {
return prev.filter(d => {
try {
return !isSameDay(d, date);
} catch {
return true;
}
});
} else {
return [...prev, date];
}
});
}
};
const handleRangeSelect = (range) => {
setDateRange(range);
};
const clearDates = () => {
setSelectedDates([]);
setDateRange(null);
setShowAlert(true);
};
const getDateSelectionText = () => {
try {
if (selectionMode === "range" && dateRange?.from) {
if (dateRange.to) {
return `${format(dateRange.from, 'MMM d')} - ${format(dateRange.to, 'MMM d, yyyy')}`;
}
return format(dateRange.from, 'MMM d, yyyy');
} else if (selectionMode === "multiple" && selectedDates.length > 0) {
if (selectedDates.length === 1) {
return format(selectedDates[0], 'MMM d, yyyy');
}
return `${selectedDates.length} dates selected`;
}
} catch {
return "Select dates";
}
return "Select dates";
};
const getEventCountForDate = (date) => {
return events.filter(e => {
const eventDate = safeParseDate(e.date);
if (!eventDate) return false;
try {
return isSameDay(eventDate, date);
} catch {
return false;
}
}).length;
};
React.useEffect(() => {
if (showAlert && (filteredEvents.length > 0 && (selectedDates.length > 0 || dateRange?.from))) {
const timer = setTimeout(() => {
setShowAlert(false);
}, 5000);
return () => clearTimeout(timer);
}
}, [showAlert, filteredEvents.length, selectedDates.length, dateRange]);
const handleReorder = (event) => {
// Create a clean copy of the event for reordering
const reorderData = {
event_name: event.event_name,
business_id: event.business_id,
business_name: event.business_name,
hub: event.hub,
event_location: event.event_location,
event_type: event.event_type,
requested: event.requested,
client_name: event.client_name,
client_email: event.client_email,
client_phone: event.client_phone,
client_address: event.client_address,
notes: event.notes,
};
sessionStorage.setItem('reorderData', JSON.stringify(reorderData));
toast({
title: "Reordering Event",
description: `Creating new order based on "${event.event_name}"`,
});
navigate(createPageUrl("CreateEvent") + "?reorder=true");
};
return (
<div className="p-4 md:p-8 bg-slate-50 min-h-screen">
<div className="max-w-7xl mx-auto">
<PageHeader
title="Events Management"
subtitle={`Managing ${events.length} ${events.length === 1 ? 'event' : 'events'}${statusCounts.active.count} active`}
showUnpublished={true}
actions={
<Link to={createPageUrl("CreateEvent")}>
<Button className="bg-gradient-to-r from-[#0A39DF] to-[#1C323E] hover:from-[#0A39DF]/90 hover:to-[#1C323E]/90 text-white shadow-lg">
<Plus className="w-5 h-5 mr-2" />
Create Event
</Button>
</Link>
}
/>
{/* Enhanced Date Selection Section */}
<div className="bg-white rounded-xl p-6 mb-6 border border-slate-200 shadow-sm">
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<div className="flex items-center gap-4">
<h3 className="font-semibold text-[#1C323E] text-lg">Select Event Dates</h3>
<div className="flex items-center gap-2 bg-slate-50 p-1 rounded-lg border border-slate-200">
<Button
size="sm"
variant={selectionMode === "multiple" ? "default" : "ghost"}
onClick={() => {
setSelectionMode("multiple");
setDateRange(null);
}}
className={selectionMode === "multiple" ? "bg-[#0A39DF] hover:bg-[#0A39DF]/90 text-white" : "text-slate-600 hover:text-slate-900 hover:bg-white"}
>
Multiple
</Button>
<Button
size="sm"
variant={selectionMode === "range" ? "default" : "ghost"}
onClick={() => {
setSelectionMode("range");
setSelectedDates([]);
}}
className={selectionMode === "range" ? "bg-[#0A39DF] hover:bg-[#0A39DF]/90 text-white" : "text-slate-600 hover:text-slate-900 hover:bg-white"}
>
Range
</Button>
<Button
size="sm"
variant="ghost"
onClick={clearDates}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
>
Clear All
</Button>
</div>
</div>
<Popover open={calendarOpen} onOpenChange={setCalendarOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
className="border-[#0A39DF] text-[#0A39DF] hover:bg-[#0A39DF]/5 hover:border-[#0A39DF] font-medium min-w-[200px]"
>
<CalendarIcon className="w-4 h-4 mr-2" />
{getDateSelectionText()}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="end">
<div className="bg-white rounded-lg shadow-xl border border-slate-200">
<div className="p-4 border-b border-slate-200 bg-slate-50">
<p className="font-semibold text-slate-900">
{selectionMode === "range" ? "Select Date Range" : "Select Multiple Dates"}
</p>
<p className="text-xs text-slate-500 mt-1">
{selectionMode === "range"
? "Click start date, then end date"
: "Click dates to select/deselect"}
</p>
</div>
<Calendar
mode={selectionMode === "range" ? "range" : "multiple"}
selected={selectionMode === "range" ? dateRange : selectedDates}
onSelect={selectionMode === "range" ? handleRangeSelect : handleDateSelect}
numberOfMonths={2}
modifiers={{
hasEvents: (date) => getEventCountForDate(date) > 0
}}
modifiersStyles={{
hasEvents: {
fontWeight: 'bold',
textDecoration: 'underline',
color: '#0A39DF'
}
}}
className="rounded-md border-0 p-4"
/>
<div className="p-4 border-t border-slate-200 bg-slate-50 flex items-center justify-between">
<p className="text-xs text-slate-500">
<span className="font-bold text-[#0A39DF]">Bold underlined dates</span> have events
</p>
<Button
size="sm"
onClick={() => setCalendarOpen(false)}
className="bg-[#0A39DF] hover:bg-[#0A39DF]/90"
>
Done
</Button>
</div>
</div>
</PopoverContent>
</Popover>
</div>
{(selectedDates.length > 0 || dateRange?.from) && showAlert && filteredEvents.length > 0 && (
<Alert className="bg-[#0A39DF]/5 border-[#0A39DF]/20 relative mt-4">
<Button
variant="ghost"
size="icon"
className="absolute top-2 right-2 h-6 w-6 text-[#0A39DF] hover:bg-[#0A39DF]/10"
onClick={() => setShowAlert(false)}
>
<X className="w-4 h-4" />
</Button>
<CalendarIcon className="h-4 w-4 text-[#0A39DF]" />
<AlertDescription className="text-[#0A39DF] font-medium pr-8">
{filteredEvents.length} event{filteredEvents.length !== 1 ? 's' : ''} found for selected date{selectionMode === "multiple" && selectedDates.length > 1 ? 's' : ''}
</AlertDescription>
</Alert>
)}
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="mb-6">
<TabsList className="bg-white border border-slate-200 h-auto p-1 shadow-sm">
<TabsTrigger value="all" className="data-[state=active]:bg-[#0A39DF] data-[state=active]:text-white">
Total Events <span className="ml-2 px-2 py-0.5 rounded-full bg-slate-100 data-[state=active]:bg-white/20 text-slate-700 data-[state=active]:text-white text-xs font-medium">{getTabCount("all")}</span>
</TabsTrigger>
<TabsTrigger value="last_minute" className="data-[state=active]:bg-[#0A39DF] data-[state=active]:text-white">
Last Minute <span className="ml-2 px-2 py-0.5 rounded-full bg-slate-100 data-[state=active]:bg-white/20 text-slate-700 data-[state=active]:text-white text-xs font-medium">{getTabCount("last_minute")}</span>
</TabsTrigger>
<TabsTrigger value="upcoming" className="data-[state=active]:bg-[#0A39DF] data-[state=active]:text-white">
Upcoming <span className="ml-2 px-2 py-0.5 rounded-full bg-slate-100 data-[state=active]:bg-white/20 text-slate-700 data-[state=active]:text-white text-xs font-medium">{getTabCount("upcoming")}</span>
</TabsTrigger>
<TabsTrigger value="active" className="data-[state=active]:bg-[#0A39DF] data-[state=active]:text-white">
Active <span className="ml-2 px-2 py-0.5 rounded-full bg-slate-100 data-[state=active]:bg-white/20 text-slate-700 data-[state=active]:text-white text-xs font-medium">{getTabCount("active")}</span>
</TabsTrigger>
<TabsTrigger value="canceled" className="data-[state=active]:bg-[#0A39DF] data-[state=active]:text-white">
Canceled <span className="ml-2 px-2 py-0.5 rounded-full bg-slate-100 data-[state=active]:bg-white/20 text-slate-700 data-[state=active]:text-white text-xs font-medium">{getTabCount("canceled")}</span>
</TabsTrigger>
<TabsTrigger value="past" className="data-[state=active]:bg-[#0A39DF] data-[state=active]:text-white">
Past <span className="ml-2 px-2 py-0.5 rounded-full bg-slate-100 data-[state=active]:bg-white/20 text-slate-700 data-[state=active]:text-white text-xs font-medium">{getTabCount("past")}</span>
</TabsTrigger>
</TabsList>
</Tabs>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<StatusCard status="Active" count={statusCounts.active.count} percentage={statusCounts.active.percentage} color="blue" />
<StatusCard status="Pending/Assigned" count={statusCounts.pending.count} percentage={statusCounts.pending.percentage} color="yellow" />
<StatusCard status="Confirmed" count={statusCounts.confirmed.count} percentage={statusCounts.confirmed.percentage} color="green" />
<StatusCard status="Completed" count={statusCounts.completed.count} percentage={statusCounts.completed.percentage} color="gray" />
</div>
<div className="bg-white rounded-xl p-4 mb-6 flex items-center gap-4 border border-slate-200 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" />
<Input
placeholder="Search by ID, company, event name..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 border-slate-300"
/>
</div>
<Avatar className="w-10 h-10 bg-slate-200">
<AvatarFallback className="bg-slate-200 text-slate-700 font-bold">M</AvatarFallback>
</Avatar>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-slate-100 hover:bg-slate-100">
<TableHead className="font-semibold text-slate-700">ID</TableHead>
<TableHead className="font-semibold text-slate-700">Company Name</TableHead>
<TableHead className="font-semibold text-slate-700">Hub</TableHead>
<TableHead className="font-semibold text-slate-700">Status</TableHead>
<TableHead className="font-semibold text-slate-700">Event Date</TableHead>
<TableHead className="font-semibold text-slate-700">Event Name</TableHead>
<TableHead className="font-semibold text-slate-700">PO</TableHead>
<TableHead className="font-semibold text-slate-700 text-center">Requested</TableHead>
<TableHead className="font-semibold text-slate-700 text-center">Assigned</TableHead>
<TableHead className="font-semibold text-slate-700 text-center" style={{width: "200px"}}>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredEvents.length === 0 ? (
<TableRow>
<TableCell colSpan={10} 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 events found</p>
<p className="text-sm mt-1">Try selecting different dates or adjusting your filters</p>
</TableCell>
</TableRow>
) : (
filteredEvents.map((event) => {
const assignedCount = event.assigned_staff?.length || 0;
const confirmedCount = event.assigned_staff?.filter(s => s.confirmed).length || 0;
return (
<EventHoverCard key={event.id} event={event}>
<TableRow className="hover:bg-slate-50 cursor-pointer transition-colors">
<TableCell className="font-medium text-slate-700">{event.id?.slice(-4).toUpperCase()}</TableCell>
<TableCell className="font-medium">{event.business_name || "Company Name"}</TableCell>
<TableCell>{event.hub || "-"}</TableCell>
<TableCell>
<Badge className={`${statusColors[event.status]} font-medium px-3 py-1`}>
{event.status}
</Badge>
{event.status === "Assigned" && confirmedCount > 0 && (
<Badge variant="outline" className="ml-1 text-xs border-green-500 text-green-700">
{confirmedCount}/{assignedCount}
</Badge>
)}
</TableCell>
<TableCell className="font-bold text-slate-700 text-base">
{safeFormatDate(event.date, "MM/dd/yyyy")}
</TableCell>
<TableCell className="font-medium">{event.event_name}</TableCell>
<TableCell>{event.po || event.po_number || "-"}</TableCell>
<TableCell className="text-center font-semibold">{event.requested || 0}</TableCell>
<TableCell className="text-center">
<QuickAssignPopover event={event}>
<button className={`hover:text-[#0A39DF] font-semibold ${
assignedCount >= event.requested && event.requested > 0 ? 'text-green-600' : 'text-orange-600'
}`}>
{assignedCount}
</button>
</QuickAssignPopover>
</TableCell>
<TableCell>
<div className="flex items-center justify-center gap-1">
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
navigate(createPageUrl(`EventDetail?id=${event.id}`));
}}
className="hover:text-[#0A39DF] hover:bg-[#0A39DF]/10"
title="View Details"
>
<Eye className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
navigate(createPageUrl(`EditEvent?id=${event.id}`));
}}
className="hover:text-[#0A39DF] hover:bg-[#0A39DF]/10"
title="Edit Event"
>
<Edit className="w-4 h-4" />
</Button>
<Button
size="sm"
onClick={(e) => {
e.stopPropagation();
handleReorder(event);
}}
className="bg-green-600 hover:bg-green-700 text-white px-3 py-1 h-8 text-xs font-semibold"
>
<RefreshCw className="w-3 h-3 mr-1" />
Reorder
</Button>
</div>
</TableCell>
</TableRow>
</EventHoverCard>
);
})
)}
</TableBody>
</Table>
</div>
</div>
</div>
);
}