export base44 - Nov 18

This commit is contained in:
bwnyasse
2025-11-18 21:32:16 -05:00
parent f7c2027065
commit d26bcaeed2
67 changed files with 13716 additions and 8102 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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>
);
}
}

View File

@@ -1,31 +1,38 @@
import React, { useState } from "react";
import React from "react";
import { base44 } from "@/api/base44Client";
import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import EventFormWizard from "@/components/events/EventFormWizard";
import AIOrderAssistant from "@/components/events/AIOrderAssistant";
import { useToast } from "@/components/ui/use-toast";
import { Button } from "@/components/ui/button";
import { Sparkles, FileText, X } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { X, AlertTriangle } from "lucide-react";
import { detectAllConflicts, ConflictAlert } from "@/components/scheduling/ConflictDetection";
import { Card, CardContent } from "@/components/ui/card";
export default function CreateEvent() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { toast } = useToast();
const [useAI, setUseAI] = useState(false);
const [aiExtractedData, setAiExtractedData] = useState(null);
const [pendingEvent, setPendingEvent] = React.useState(null);
const [showConflictWarning, setShowConflictWarning] = React.useState(false);
const { data: currentUser } = useQuery({
queryKey: ['current-user-create-event'],
queryFn: () => base44.auth.me(),
});
const { data: allEvents = [] } = useQuery({
queryKey: ['events-for-conflict-check'],
queryFn: () => base44.entities.Event.list(),
initialData: [],
});
const createEventMutation = useMutation({
mutationFn: (eventData) => base44.entities.Event.create(eventData),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['events'] });
queryClient.invalidateQueries({ queryKey: ['client-events'] });
toast({
title: "✅ Event Created",
description: "Your event has been created successfully.",
@@ -42,107 +49,98 @@ export default function CreateEvent() {
});
const handleSubmit = (eventData) => {
createEventMutation.mutate(eventData);
// Detect conflicts before creating
const conflicts = detectAllConflicts(eventData, allEvents);
if (conflicts.length > 0) {
setPendingEvent({ ...eventData, detected_conflicts: conflicts });
setShowConflictWarning(true);
} else {
createEventMutation.mutate(eventData);
}
};
const handleAIDataExtracted = (extractedData) => {
setAiExtractedData(extractedData);
setUseAI(false);
const handleConfirmWithConflicts = () => {
if (pendingEvent) {
createEventMutation.mutate(pendingEvent);
setShowConflictWarning(false);
setPendingEvent(null);
}
};
const handleCancelConflicts = () => {
setShowConflictWarning(false);
setPendingEvent(null);
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
<div className="max-w-7xl mx-auto p-4 md:p-8">
{/* Header with AI Toggle */}
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold text-[#1C323E]">Create New Order</h1>
<h1 className="text-3xl font-bold text-[#1C323E]">Create Standard Order</h1>
<p className="text-slate-600 mt-1">
{useAI ? "Use AI to create your order naturally" : "Fill out the form to create your order"}
Fill out the details for your planned event
</p>
</div>
<div className="flex gap-2">
<Button
variant={useAI ? "default" : "outline"}
onClick={() => setUseAI(true)}
className={useAI ? "bg-gradient-to-r from-[#0A39DF] to-purple-600" : ""}
>
<Sparkles className="w-4 h-4 mr-2" />
AI Assistant
</Button>
<Button
variant={!useAI ? "default" : "outline"}
onClick={() => setUseAI(false)}
className={!useAI ? "bg-[#1C323E]" : ""}
>
<FileText className="w-4 h-4 mr-2" />
Form
</Button>
<Button
variant="ghost"
onClick={() => navigate(createPageUrl("Events"))}
>
<X className="w-4 h-4" />
</Button>
</div>
<Button
variant="ghost"
onClick={() => navigate(createPageUrl("ClientDashboard"))}
>
<X className="w-4 h-4" />
</Button>
</div>
{/* AI Assistant Interface */}
<AnimatePresence>
{useAI && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
>
<AIOrderAssistant
onOrderDataExtracted={handleAIDataExtracted}
onClose={() => setUseAI(false)}
/>
</motion.div>
)}
</AnimatePresence>
{/* Wizard Form */}
{!useAI && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
{aiExtractedData && (
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-xl">
<div className="flex items-center gap-2 mb-2">
<Sparkles className="w-5 h-5 text-green-600" />
<span className="font-semibold text-green-900">AI Pre-filled Data</span>
{/* Conflict Warning Modal */}
{showConflictWarning && pendingEvent && (
<Card className="mb-6 border-2 border-orange-500">
<CardContent className="p-6">
<div className="flex items-start gap-4 mb-4">
<div className="w-12 h-12 bg-orange-100 rounded-full flex items-center justify-center flex-shrink-0">
<AlertTriangle className="w-6 h-6 text-orange-600" />
</div>
<p className="text-sm text-green-700 mb-3">
The form has been pre-filled with information from your conversation. Review and edit as needed.
</p>
<div>
<h3 className="font-bold text-lg text-slate-900 mb-1">
Scheduling Conflicts Detected
</h3>
<p className="text-sm text-slate-600">
This event has {pendingEvent.detected_conflicts.length} potential conflict{pendingEvent.detected_conflicts.length !== 1 ? 's' : ''}
with existing bookings. Review the conflicts below and decide how to proceed.
</p>
</div>
</div>
<div className="mb-6">
<ConflictAlert conflicts={pendingEvent.detected_conflicts} />
</div>
<div className="flex gap-3 justify-end">
<Button
variant="outline"
size="sm"
onClick={() => {
setAiExtractedData(null);
setUseAI(true);
}}
className="border-green-300 text-green-700 hover:bg-green-100"
onClick={handleCancelConflicts}
>
<Sparkles className="w-4 h-4 mr-2" />
Chat with AI Again
Go Back & Edit
</Button>
<Button
onClick={handleConfirmWithConflicts}
className="bg-orange-600 hover:bg-orange-700"
>
Create Anyway
</Button>
</div>
)}
<EventFormWizard
event={aiExtractedData}
onSubmit={handleSubmit}
isSubmitting={createEventMutation.isPending}
currentUser={currentUser}
onCancel={() => navigate(createPageUrl("Events"))}
/>
</motion.div>
</CardContent>
</Card>
)}
<EventFormWizard
event={null}
onSubmit={handleSubmit}
isSubmitting={createEventMutation.isPending}
currentUser={currentUser}
onCancel={() => navigate(createPageUrl("ClientDashboard"))}
/>
</div>
</div>
);

View File

@@ -6,11 +6,104 @@ import { Link, useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Users, Building2, UserPlus, TrendingUp, MapPin, Calendar, DollarSign, Award, Target, BarChart3, Shield, Leaf } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Users, Building2, UserPlus, TrendingUp, MapPin, Calendar, DollarSign, Award, Target, BarChart3, Shield, Leaf, Eye, Edit, Sparkles, Zap, Clock, AlertTriangle, CheckCircle, FileText, X } from "lucide-react";
import StatsCard from "@/components/staff/StatsCard";
import EcosystemWheel from "@/components/dashboard/EcosystemWheel";
import QuickMetrics from "@/components/dashboard/QuickMetrics";
import PageHeader from "@/components/common/PageHeader";
import { format, parseISO, isValid, isSameDay, startOfDay } 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, 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) => {
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>
);
};
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) : "-"
};
};
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-200 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-slate-200 text-slate-600', text: assigned, percent: `${fillPercent}%`, status: 'partial' };
return { color: 'bg-slate-200 text-slate-600', text: assigned, percent: '0%', status: 'partial' };
};
export default function Dashboard() {
const navigate = useNavigate();
@@ -28,6 +121,13 @@ export default function Dashboard() {
initialData: [],
});
// Filter events for today only
const today = startOfDay(new Date());
const todaysEvents = events.filter(event => {
const eventDate = safeParseDate(event.date);
return eventDate && isSameDay(eventDate, today);
});
const recentStaff = staff.slice(0, 6);
const uniqueDepartments = [...new Set(staff.map(s => s.department).filter(Boolean))];
const uniqueLocations = [...new Set(staff.map(s => s.hub_location).filter(Boolean))];
@@ -105,7 +205,7 @@ export default function Dashboard() {
<Link to={createPageUrl("Events")}>
<Button className="bg-gradient-to-r from-[#0A39DF] to-[#1C323E] hover:from-[#0A39DF]/90 hover:to-[#1C323E]/90 text-white shadow-lg">
<Calendar className="w-5 h-5 mr-2" />
View All Events
View All Orders
</Button>
</Link>
</>
@@ -143,6 +243,133 @@ export default function Dashboard() {
/>
</div>
{/* Today's Orders Section */}
<Card className="mb-8 border-slate-200 shadow-lg">
<CardHeader className="bg-gradient-to-br from-slate-50 to-white border-b border-slate-100">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-[#1C323E] flex items-center gap-2">
<Calendar className="w-6 h-6 text-[#0A39DF]" />
Today's Orders - {format(today, 'EEEE, MMMM d, yyyy')}
</CardTitle>
<p className="text-sm text-slate-500 mt-1">Orders scheduled for today only</p>
</div>
<Link to={createPageUrl("Events")}>
<Button variant="outline" className="border-slate-300">
View All Orders
</Button>
</Link>
</div>
</CardHeader>
<CardContent className="p-0">
{todaysEvents.length === 0 ? (
<div className="text-center py-12 text-slate-500">
<Calendar className="w-12 h-12 mx-auto mb-3 text-slate-300" />
<p className="font-medium">No orders scheduled for today</p>
</div>
) : (
<div className="overflow-x-auto">
<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">ACTIONS</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{todaysEvents.map((event) => {
const assignmentStatus = getAssignmentStatus(event);
const eventTimes = getEventTimes(event);
const eventDate = safeParseDate(event.date);
const dayOfWeek = eventDate ? format(eventDate, 'EEEE') : '';
return (
<TableRow key={event.id} 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)}
</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">
<div className={`w-10 h-10 rounded-full ${assignmentStatus.color} flex items-center justify-center font-bold text-sm`}>
{assignmentStatus.text}
</div>
<span className="text-[10px] text-slate-500 font-medium">{assignmentStatus.percent}</span>
</div>
</TableCell>
<TableCell className="py-3">
<div className="flex items-center justify-center gap-1">
<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>
{event.invoice_id && (
<Button
variant="ghost"
size="icon"
onClick={() => navigate(createPageUrl(`Invoices?id=${event.invoice_id}`))}
className="hover:bg-slate-100 h-8 w-8"
title="View Invoice"
>
<FileText className="w-4 h-4 text-blue-600" />
</Button>
)}
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
{/* Ecosystem Puzzle */}
<Card className="mb-8 border-slate-200 shadow-xl overflow-hidden">
<CardHeader className="bg-gradient-to-br from-slate-50 to-white border-b border-slate-100">

View File

@@ -1,52 +1,48 @@
import React, { useState } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ArrowLeft, Bell, RefreshCw } from "lucide-react";
import { format } from "date-fns";
import ShiftCard from "@/components/events/ShiftCard";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { ArrowLeft, Calendar, MapPin, Users, DollarSign, Send, Edit3, X, AlertTriangle } from "lucide-react";
import ShiftCard from "@/components/events/ShiftCard";
import OrderStatusBadge from "@/components/orders/OrderStatusBadge";
import { useToast } from "@/components/ui/use-toast";
import { format } from "date-fns";
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",
Completed: "bg-slate-100 text-slate-800",
Canceled: "bg-red-100 text-red-800" // Added Canceled status for completeness
};
// Safe date formatter
const safeFormatDate = (dateString, formatStr) => {
if (!dateString) return "-";
const safeFormatDate = (dateString) => {
if (!dateString) return "—";
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return "-";
return format(date, formatStr);
return format(new Date(dateString), "MMMM d, yyyy");
} catch {
return "-";
return "";
}
};
export default function EventDetail() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [showNotifyDialog, setShowNotifyDialog] = useState(false);
const urlParams = new URLSearchParams(window.location.search);
const eventId = urlParams.get('id');
const { toast } = useToast();
const [notifyDialog, setNotifyDialog] = useState(false);
const [cancelDialog, setCancelDialog] = useState(false);
const urlParams = new URLSearchParams(window.location.search);
const eventId = urlParams.get("id");
const { data: user } = useQuery({
queryKey: ['current-user-event-detail'],
queryFn: () => base44.auth.me(),
});
const { data: allEvents, isLoading } = useQuery({
queryKey: ['events'],
@@ -54,208 +50,314 @@ export default function EventDetail() {
initialData: [],
});
const { data: shifts } = useQuery({
queryKey: ['shifts', eventId],
queryFn: () => base44.entities.Shift.filter({ event_id: eventId }),
initialData: [],
enabled: !!eventId
});
const event = allEvents.find(e => e.id === eventId);
const handleReorder = () => {
if (!event) return; // Should not happen if event is loaded, but for safety
// Cancel order mutation
const cancelOrderMutation = useMutation({
mutationFn: () => base44.entities.Event.update(eventId, { status: "Canceled" }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['events'] });
queryClient.invalidateQueries({ queryKey: ['all-events-client'] });
toast({
title: "✅ Order Canceled",
description: "Your order has been canceled successfully",
});
setCancelDialog(false);
navigate(createPageUrl("ClientOrders"));
},
onError: () => {
toast({
title: "❌ Failed to Cancel",
description: "Could not cancel order. Please try again.",
variant: "destructive",
});
},
});
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));
const handleNotifyStaff = async () => {
const assignedStaff = event?.assigned_staff || [];
toast({
title: "Reordering Event",
description: `Creating new order based on "${event.event_name}"`,
});
for (const staff of assignedStaff) {
try {
await base44.integrations.Core.SendEmail({
to: staff.email || `${staff.staff_name}@example.com`,
subject: `Shift Update: ${event.event_name}`,
body: `You have an update for: ${event.event_name}\nDate: ${event.date}\nLocation: ${event.event_location || event.hub}\n\nPlease check the platform for details.`
});
} catch (error) {
console.error("Failed to send email:", error);
}
}
navigate(createPageUrl("CreateEvent") + "?reorder=true");
toast({
title: "✅ Notifications Sent",
description: `Notified ${assignedStaff.length} staff members`,
});
setNotifyDialog(false);
};
if (isLoading || !event) {
const isClient = user?.user_role === 'client' ||
event?.created_by === user?.email ||
event?.client_email === user?.email;
const canEditOrder = () => {
if (!event) return false;
const eventDate = new Date(event.date);
const now = new Date();
return isClient &&
event.status !== "Completed" &&
event.status !== "Canceled" &&
eventDate > now;
};
const canCancelOrder = () => {
if (!event) return false;
return isClient &&
event.status !== "Completed" &&
event.status !== "Canceled";
};
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full" />
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600" />
</div>
);
}
if (!event) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<p className="text-xl font-semibold text-slate-900 mb-4">Event not found</p>
<Link to={createPageUrl("Events")}>
<Button variant="outline">Back to Events</Button>
</Link>
</div>
);
}
// Get shifts from event.shifts array (primary source)
const eventShifts = event.shifts || [];
return (
<div className="p-4 md:p-8 bg-slate-50 min-h-screen">
<div className="max-w-[1600px] mx-auto">
<div className="flex items-center gap-4 mb-6">
<Button variant="ghost" size="icon" onClick={() => navigate(createPageUrl("Events"))}>
<ArrowLeft className="w-5 h-5" />
</Button>
<h1 className="text-2xl font-bold">{event.event_name}</h1>
<div className="flex items-center gap-2 ml-auto">
{(event.status === "Completed" || event.status === "Canceled") && (
<Button
onClick={handleReorder}
className="bg-green-600 hover:bg-green-700 text-white"
<div className="p-4 md:p-8">
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="icon"
onClick={() => navigate(-1)}
>
<ArrowLeft className="w-5 h-5" />
</Button>
<div>
<h1 className="text-3xl font-bold text-slate-900">{event.event_name}</h1>
<p className="text-slate-600 mt-1">Order Details & Information</p>
</div>
</div>
<div className="flex items-center gap-3">
<OrderStatusBadge order={event} />
{canEditOrder() && (
<button
onClick={() => navigate(createPageUrl(`EditEvent?id=${event.id}`))}
className="flex items-center gap-2 px-5 py-2.5 bg-white hover:bg-blue-50 border-2 border-blue-200 rounded-full text-blue-600 font-semibold text-base transition-all shadow-md hover:shadow-lg"
>
<RefreshCw className="w-4 h-4 mr-2" />
Reorder
<Edit3 className="w-5 h-5" />
Edit
</button>
)}
{canCancelOrder() && (
<button
onClick={() => setCancelDialog(true)}
className="flex items-center gap-2 px-5 py-2.5 bg-white hover:bg-red-50 border-2 border-red-200 rounded-full text-red-600 font-semibold text-base transition-all shadow-md hover:shadow-lg"
>
<X className="w-5 h-5" />
cancel
</button>
)}
{!isClient && event.assigned_staff?.length > 0 && (
<Button
onClick={() => setNotifyDialog(true)}
className="bg-blue-600 hover:bg-blue-700"
>
<Send className="w-4 h-4 mr-2" />
Notify Staff
</Button>
)}
<Bell className="w-5 h-5" />
<div className="w-10 h-10 bg-blue-600 rounded-full flex items-center justify-center text-white font-bold">
M
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<Card className="border-slate-200">
<CardHeader className="bg-gradient-to-br from-blue-50 to-white border-b border-slate-100">
<CardTitle className="text-base">Order Details</CardTitle>
</CardHeader>
<CardContent className="p-6 space-y-4">
<div>
<p className="text-xs text-slate-500">PO number</p>
<p className="font-medium">{event.po_number || event.po || "#RC-36559419"}</p>
</div>
<div>
<p className="text-xs text-slate-500">Data</p>
<p className="font-medium">{safeFormatDate(event.date, "dd.MM.yyyy")}</p>
</div>
<div>
<p className="text-xs text-slate-500">Status</p>
<Badge className={`${statusColors[event.status]} font-medium mt-1`}>
{event.status}
</Badge>
</div>
<div className="flex gap-2 pt-4">
<Button variant="outline" className="flex-1 text-sm">
Edit Order
</Button>
<Button variant="outline" className="flex-1 text-sm text-red-600 hover:text-red-700">
Cancel Order
</Button>
</div>
</CardContent>
</Card>
<Card className="border-slate-200 lg:col-span-2">
<CardHeader className="bg-gradient-to-br from-blue-50 to-white border-b border-slate-100">
<CardTitle className="text-base">Client info</CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="grid grid-cols-2 gap-6">
<div>
<p className="text-xs text-slate-500 mb-1">Client name</p>
<p className="font-medium">{event.client_name || "Legendary"}</p>
</div>
<div>
<p className="text-xs text-slate-500 mb-1">Number</p>
<p className="font-medium">{event.client_phone || "(408) 815-9180"}</p>
</div>
<div className="col-span-2">
<p className="text-xs text-slate-500 mb-1">Address</p>
<p className="font-medium">{event.client_address || event.event_location || "848 E Dash Rd, Ste 264 E San Jose, CA 95122"}</p>
</div>
<div className="col-span-2">
<p className="text-xs text-slate-500 mb-1">Email</p>
<p className="font-medium">{event.client_email || "order@legendarysweetssf.com"}</p>
</div>
</div>
</CardContent>
</Card>
</div>
<Card className="border-slate-200 mb-6">
<CardHeader className="bg-gradient-to-br from-blue-50 to-white border-b border-slate-100">
<CardTitle className="text-base">Event: {event.event_name}</CardTitle>
{/* Order Details Card */}
<Card className="bg-white border border-slate-200 shadow-md">
<CardHeader className="border-b border-slate-100">
<CardTitle className="text-lg font-bold text-slate-900">Order Information</CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="grid grid-cols-2 gap-6 text-sm">
<div>
<p className="text-slate-500">Hub</p>
<p className="font-medium">{event.hub || "Hub Name"}</p>
<div className="grid grid-cols-4 gap-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-50 rounded-lg flex items-center justify-center">
<Calendar className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-xs text-slate-500">Event Date</p>
<p className="font-bold text-slate-900">{safeFormatDate(event.date)}</p>
</div>
</div>
<div>
<p className="text-slate-500">Name of Department</p>
<p className="font-medium">Department name</p>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-purple-50 rounded-lg flex items-center justify-center">
<MapPin className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-xs text-slate-500">Location</p>
<p className="font-bold text-slate-900">{event.hub || event.event_location || "—"}</p>
</div>
</div>
<div className="col-span-2">
<p className="text-slate-500 mb-2">Order Addons</p>
<div className="flex gap-2">
<Badge variant="outline" className="text-xs">Title</Badge>
<Badge variant="outline" className="text-xs">Travel Time</Badge>
<Badge variant="outline" className="text-xs">Meal Provided</Badge>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-50 rounded-lg flex items-center justify-center">
<Users className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-xs text-slate-500">Staff Assigned</p>
<p className="font-bold text-slate-900">
{event.assigned_staff?.length || 0} / {event.requested || 0}
</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-amber-50 rounded-lg flex items-center justify-center">
<DollarSign className="w-5 h-5 text-amber-600" />
</div>
<div>
<p className="text-xs text-slate-500">Total Cost</p>
<p className="font-bold text-slate-900">${(event.total || 0).toLocaleString()}</p>
</div>
</div>
</div>
</CardContent>
</Card>
<div className="space-y-6">
{shifts.length > 0 ? (
shifts.map((shift, idx) => (
<ShiftCard
key={shift.id}
shift={shift}
onNotifyStaff={() => setShowNotifyDialog(true)}
/>
{/* Client Information (if not client viewing) */}
{!isClient && (
<Card className="bg-white border border-slate-200 shadow-md">
<CardHeader className="border-b border-slate-100">
<CardTitle className="text-lg font-bold text-slate-900">Client Information</CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="grid grid-cols-3 gap-6">
<div>
<p className="text-xs text-slate-500 mb-1">Business Name</p>
<p className="font-bold text-slate-900">{event.business_name || "—"}</p>
</div>
<div>
<p className="text-xs text-slate-500 mb-1">Contact Name</p>
<p className="font-bold text-slate-900">{event.client_name || "—"}</p>
</div>
<div>
<p className="text-xs text-slate-500 mb-1">Contact Email</p>
<p className="font-bold text-slate-900">{event.client_email || "—"}</p>
</div>
</div>
</CardContent>
</Card>
)}
{/* Shifts - Using event.shifts array */}
<div className="space-y-4">
<h2 className="text-xl font-bold text-slate-900">Event Shifts & Staff Assignment</h2>
{eventShifts.length > 0 ? (
eventShifts.map((shift, idx) => (
<ShiftCard key={idx} shift={shift} event={event} />
))
) : (
<ShiftCard
shift={{
shift_name: "Shift 1",
assigned_staff: event.assigned_staff || [],
location: event.event_location,
unpaid_break: 0,
price: 23,
amount: 120
}}
onNotifyStaff={() => setShowNotifyDialog(true)}
/>
<Card className="bg-white border border-slate-200">
<CardContent className="p-12 text-center">
<Users className="w-12 h-12 mx-auto mb-4 text-slate-400" />
<p className="text-slate-600 font-medium mb-2">No shifts defined for this event</p>
<p className="text-slate-500 text-sm">Add roles and staff requirements to get started</p>
</CardContent>
</Card>
)}
</div>
<Dialog open={showNotifyDialog} onOpenChange={setShowNotifyDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<div className="flex items-center justify-center mb-4">
<div className="w-12 h-12 bg-pink-500 rounded-full flex items-center justify-center text-white font-bold text-xl">
L
</div>
</div>
<DialogTitle className="text-center">Notification Name</DialogTitle>
<p className="text-center text-sm text-slate-600">
Order #5 Admin (cancelled/replace) Want to proceed?
</p>
</DialogHeader>
<DialogFooter className="flex gap-3 sm:justify-center">
<Button variant="outline" onClick={() => setShowNotifyDialog(false)} className="flex-1">
Cancel
</Button>
<Button onClick={() => setShowNotifyDialog(false)} className="flex-1 bg-blue-600 hover:bg-blue-700">
Proceed
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Notes */}
{event.notes && (
<Card className="bg-white border border-slate-200 shadow-md">
<CardHeader className="border-b border-slate-100">
<CardTitle className="text-lg font-bold text-slate-900">Additional Notes</CardTitle>
</CardHeader>
<CardContent className="p-6">
<p className="text-slate-700 whitespace-pre-wrap">{event.notes}</p>
</CardContent>
</Card>
)}
</div>
{/* Notify Staff Dialog */}
<Dialog open={notifyDialog} onOpenChange={setNotifyDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Notify Assigned Staff</DialogTitle>
<DialogDescription>
Send notification to all {event.assigned_staff?.length || 0} assigned staff members about this event.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setNotifyDialog(false)}>
Cancel
</Button>
<Button onClick={handleNotifyStaff} className="bg-blue-600 hover:bg-blue-700">
<Send className="w-4 h-4 mr-2" />
Send Notifications
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Cancel Order Dialog */}
<Dialog open={cancelDialog} onOpenChange={setCancelDialog}>
<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 and the vendor will be notified immediately.
</DialogDescription>
</DialogHeader>
<div className="bg-slate-50 rounded-lg p-4 space-y-2">
<p className="font-bold text-slate-900">{event.event_name}</p>
<div className="flex items-center gap-2 text-sm text-slate-600">
<Calendar className="w-4 h-4" />
{safeFormatDate(event.date)}
</div>
<div className="flex items-center gap-2 text-sm text-slate-600">
<MapPin className="w-4 h-4" />
{event.hub || event.event_location}
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setCancelDialog(false)}
>
Keep Order
</Button>
<Button
variant="destructive"
onClick={() => cancelOrderMutation.mutate()}
disabled={cancelOrderMutation.isPending}
>
{cancelOrderMutation.isPending ? "Canceling..." : "Yes, Cancel Order"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -29,7 +29,7 @@ const statusColors = {
'Overdue': 'bg-red-500 text-white',
'Resolved': 'bg-blue-500 text-white',
'Paid': 'bg-green-500 text-white',
'Reconciled': 'bg-yellow-600 text-white',
'Reconciled': 'bg-amber-600 text-white', // Changed from bg-yellow-600
'Disputed': 'bg-gray-500 text-white',
'Verified': 'bg-teal-500 text-white',
'Pending': 'bg-amber-500 text-white',
@@ -161,7 +161,7 @@ export default function Invoices() {
<Button
onClick={() => setShowPaymentDialog(true)}
variant="outline"
className="bg-yellow-400 hover:bg-yellow-500 text-slate-900 border-0 font-semibold"
className="bg-amber-500 hover:bg-amber-600 text-white border-0 font-semibold" // Changed className
>
Record Payment
</Button>

View File

@@ -1,5 +1,4 @@
import React from "react";
import { Link, useLocation } from "react-router-dom";
import { createPageUrl } from "@/utils";
@@ -10,7 +9,7 @@ import {
DollarSign, Award, HelpCircle, BarChart3, Activity, Menu, MessageSquare,
Package, TrendingUp, Clipboard, LogOut, Shield, MapPin, Bell, CloudOff,
RefreshCw, User, Search, ShoppingCart, Home, Settings as SettingsIcon, MoreVertical,
Building2, Sparkles, CheckSquare, UserCheck, Store
Building2, Sparkles, CheckSquare, UserCheck, Store, GraduationCap
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
@@ -32,6 +31,7 @@ import { Badge } from "@/components/ui/badge";
import ChatBubble from "@/components/chat/ChatBubble";
import RoleSwitcher from "@/components/dev/RoleSwitcher";
import NotificationPanel from "@/components/notifications/NotificationPanel";
import { NotificationEngine } from "@/components/notifications/NotificationEngine";
import { Toaster } from "@/components/ui/toaster";
// Navigation items for each role
@@ -44,7 +44,9 @@ const roleNavigationMap = {
{ title: "Partners", url: createPageUrl("PartnerManagement"), icon: Briefcase },
{ title: "Vendors", url: createPageUrl("VendorManagement"), icon: Package },
{ title: "Workforce", url: createPageUrl("StaffDirectory"), icon: Users },
{ title: "Onboarding", url: createPageUrl("StaffOnboarding"), icon: GraduationCap },
{ title: "Teams", url: createPageUrl("Teams"), icon: UserCheck },
{ title: "Task Board", url: createPageUrl("TaskBoard"), icon: CheckSquare },
{ title: "Messages", url: createPageUrl("Messages"), icon: MessageSquare },
{ title: "Invoices", url: createPageUrl("Invoices"), icon: FileText },
{ title: "Payroll", url: createPageUrl("Payroll"), icon: DollarSign },
@@ -57,13 +59,14 @@ const roleNavigationMap = {
],
procurement: [
{ title: "Dashboard", url: createPageUrl("ProcurementDashboard"), icon: LayoutDashboard },
{ title: "Orders", url: createPageUrl("Events"), icon: Calendar },
{ title: "Enterprises", url: createPageUrl("EnterpriseManagement"), icon: Building2 },
{ title: "Sectors", url: createPageUrl("SectorManagement"), icon: MapPin },
{ title: "Partners", url: createPageUrl("PartnerManagement"), icon: Briefcase },
{ title: "Vendors", url: createPageUrl("VendorManagement"), icon: Package },
{ title: "Teams", url: createPageUrl("Teams"), icon: UserCheck },
{ title: "Task Board", url: createPageUrl("TaskBoard"), icon: CheckSquare },
{ title: "Compliance", url: createPageUrl("WorkforceCompliance"), icon: Shield },
{ title: "Orders", url: createPageUrl("Events"), icon: Clipboard },
{ title: "Rate Matrix", url: createPageUrl("VendorRateCard"), icon: DollarSign },
{ title: "Messages", url: createPageUrl("Messages"), icon: MessageSquare },
{ title: "Invoices", url: createPageUrl("Invoices"), icon: FileText },
@@ -71,25 +74,27 @@ const roleNavigationMap = {
],
operator: [
{ title: "Dashboard", url: createPageUrl("OperatorDashboard"), icon: LayoutDashboard },
{ title: "Orders", url: createPageUrl("Events"), icon: Calendar },
{ title: "My Sectors", url: createPageUrl("SectorManagement"), icon: MapPin },
{ title: "Partners", url: createPageUrl("PartnerManagement"), icon: Briefcase },
{ title: "Vendors", url: createPageUrl("VendorManagement"), icon: Package },
{ title: "Orders", url: createPageUrl("Events"), icon: Calendar },
{ title: "Clients", url: createPageUrl("Business"), icon: Users },
{ title: "Workforce", url: createPageUrl("StaffDirectory"), icon: Users },
{ title: "Teams", url: createPageUrl("Teams"), icon: UserCheck },
{ title: "Task Board", url: createPageUrl("TaskBoard"), icon: CheckSquare },
{ title: "Messages", url: createPageUrl("Messages"), icon: MessageSquare },
{ title: "Invoices", url: createPageUrl("Invoices"), icon: FileText },
{ title: "Reports", url: createPageUrl("Reports"), icon: BarChart3 },
],
sector: [
{ title: "Dashboard", url: createPageUrl("OperatorDashboard"), icon: LayoutDashboard },
{ title: "Orders", url: createPageUrl("Events"), icon: Calendar },
{ title: "Partners", url: createPageUrl("PartnerManagement"), icon: Briefcase },
{ title: "Vendors", url: createPageUrl("VendorManagement"), icon: Package },
{ title: "Orders", url: createPageUrl("Events"), icon: Calendar },
{ title: "Clients", url: createPageUrl("Business"), icon: Users },
{ title: "Workforce", url: createPageUrl("StaffDirectory"), icon: Users },
{ title: "Teams", url: createPageUrl("Teams"), icon: UserCheck },
{ title: "Task Board", url: createPageUrl("TaskBoard"), icon: CheckSquare },
{ title: "Messages", url: createPageUrl("Messages"), icon: MessageSquare },
{ title: "Invoices", url: createPageUrl("Invoices"), icon: FileText },
{ title: "Reports", url: createPageUrl("Reports"), icon: BarChart3 },
@@ -101,6 +106,7 @@ const roleNavigationMap = {
{ title: "Vendor Marketplace", url: createPageUrl("VendorMarketplace"), icon: Store },
{ title: "Compare Rates", url: createPageUrl("VendorRateCard"), icon: DollarSign },
{ title: "Teams", url: createPageUrl("Teams"), icon: UserCheck },
{ title: "Task Board", url: createPageUrl("TaskBoard"), icon: CheckSquare },
{ title: "Messages", url: createPageUrl("Messages"), icon: MessageSquare },
{ title: "Invoices", url: createPageUrl("Invoices"), icon: FileText },
{ title: "Reports", url: createPageUrl("Reports"), icon: BarChart3 },
@@ -108,16 +114,17 @@ const roleNavigationMap = {
],
vendor: [
{ title: "Dashboard", url: createPageUrl("VendorDashboard"), icon: LayoutDashboard },
{ title: "Service Rates", url: createPageUrl("VendorRates"), icon: DollarSign },
{ title: "Orders", url: createPageUrl("VendorOrders"), icon: FileText },
{ title: "Service Rates", url: createPageUrl("VendorRates"), icon: DollarSign },
{ title: "Invoices", url: createPageUrl("Invoices"), icon: Clipboard },
{ title: "Schedule", url: createPageUrl("WorkforceShifts"), icon: Calendar },
{ title: "Workforce", url: createPageUrl("StaffDirectory"), icon: Users },
{ title: "Onboard Staff", url: createPageUrl("StaffOnboarding"), icon: GraduationCap },
{ title: "Team", url: createPageUrl("Teams"), icon: UserCheck },
{ title: "Task Board", url: createPageUrl("TaskBoard"), icon: CheckSquare },
{ title: "Compliance", url: createPageUrl("VendorCompliance"), icon: Shield },
{ title: "Communications", url: createPageUrl("Messages"), icon: MessageSquare },
{ title: "Leads", url: createPageUrl("Business"), icon: UserCheck },
{ title: "Tasks", url: createPageUrl("ActivityLog"), icon: CheckSquare },
{ title: "Business", url: createPageUrl("Business"), icon: Briefcase },
{ title: "Reports", url: createPageUrl("Reports"), icon: BarChart3 },
{ title: "Audit Trail", url: createPageUrl("ActivityLog"), icon: Activity },
@@ -125,8 +132,10 @@ const roleNavigationMap = {
],
workforce: [
{ title: "Dashboard", url: createPageUrl("WorkforceDashboard"), icon: LayoutDashboard },
{ title: "Onboard Staff", url: createPageUrl("StaffOnboarding"), icon: GraduationCap },
{ title: "My Shifts", url: createPageUrl("WorkforceShifts"), icon: Calendar },
{ title: "Teams", url: createPageUrl("Teams"), icon: UserCheck },
{ title: "Task Board", url: createPageUrl("TaskBoard"), icon: CheckSquare },
{ title: "Messages", url: createPageUrl("Messages"), icon: MessageSquare },
{ title: "Certifications", url: createPageUrl("Certification"), icon: Award },
{ title: "Earnings", url: createPageUrl("WorkforceEarnings"), icon: DollarSign },
@@ -281,200 +290,34 @@ export default function Layout({ children }) {
--muted: 241 245 249;
}
/* Calendar styling kept as is */
.rdp * {
border-color: transparent !important;
}
.rdp-day {
font-size: 0.875rem !important;
min-width: 36px !important;
height: 36px !important;
border-radius: 50% !important;
transition: all 0.2s ease !important;
font-weight: 500 !important;
position: relative !important;
}
.rdp-day button {
width: 100% !important;
height: 100% !important;
border-radius: 50% !important;
background-color: transparent !important;
}
.rdp-day_range_start,
.rdp-day_range_start > button {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: white !important;
font-weight: 700 !important;
border-radius: 50% !important;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important;
}
.rdp-day_range_end,
.rdp-day_range_end > button {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: white !important;
font-weight: 700 !important;
border-radius: 50% !important;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important;
}
.rdp-day_selected:not(.rdp-day_range_start):not(.rdp-day_range_end),
.rdp-day_selected:not(.rdp-day_range_start):not(.rdp-day_range_end) > button {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: white !important;
font-weight: 700 !important;
border-radius: 50% !important;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important;
}
.rdp-day_selected,
.rdp-day_selected > button {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: white !important;
font-weight: 700 !important;
border-radius: 50% !important;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important;
}
.rdp-day_range_middle,
.rdp-day_range_middle > button {
background-color: #dbeafe !important;
background: #dbeafe !important;
color: #2563eb !important;
font-weight: 600 !important;
border-radius: 0 !important;
box-shadow: none !important;
}
.rdp-day_range_start.rdp-day_range_end,
.rdp-day_range_start.rdp-day_range_end > button {
border-radius: 50% !important;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
}
.rdp-day:hover:not(.rdp-day_selected):not(.rdp-day_disabled):not(.rdp-day_range_start):not(.rdp-day_range_end):not(.rdp-day_range_middle) > button {
background-color: #eff6ff !important;
background: #eff6ff !important;
color: #2563eb !important;
border-radius: 50% !important;
}
.rdp-day_today:not(.rdp-day_selected):not(.rdp-day_range_start):not(.rdp-day_range_end)::after {
content: '' !important;
position: absolute !important;
bottom: 4px !important;
left: 50% !important;
transform: translateX(-50%) !important;
width: 4px !important;
height: 4px !important;
background-color: #ec4899 !important;
border-radius: 50% !important;
z-index: 10 !important;
}
.rdp-day_today.rdp-day_selected,
.rdp-day_today.rdp-day_range_start,
.rdp-day_today.rdp-day_range_end {
color: white !important;
}
.rdp-day_today.rdp-day_selected > button,
.rdp-day_today.rdp-day_range_start > button,
.rdp-day_today.rdp-day_range_end > button {
color: white !important;
}
.rdp-day_outside,
.rdp-day_outside > button {
color: #cbd5e1 !important;
opacity: 0.5 !important;
}
.rdp-day_disabled,
.rdp-day_disabled > button {
opacity: 0.3 !important;
cursor: not-allowed !important;
}
.rdp-day_selected,
.rdp-day_range_start,
.rdp-day_range_end,
.rdp-day_range_middle {
opacity: 1 !important;
visibility: visible !important;
z-index: 5 !important;
}
.rdp-head_cell {
color: #64748b !important;
font-weight: 600 !important;
font-size: 0.75rem !important;
text-transform: uppercase !important;
padding: 8px 0 !important;
}
.rdp-caption_label {
font-size: 1rem !important;
font-weight: 700 !important;
color: #0f172a !important;
}
.rdp-nav_button {
width: 32px !important;
height: 32px !important;
border-radius: 6px !important;
transition: all 0.2s ease !important;
}
.rdp-nav_button:hover {
background-color: #eff6ff !important;
color: #2563eb !important;
}
.rdp-day.has-events:not(.rdp-day_selected):not(.rdp-day_range_start):not(.rdp-day_range_end)::before {
content: '' !important;
position: absolute !important;
top: 4px !important;
right: 4px !important;
width: 4px !important;
height: 4px !important;
background-color: #2563eb !important;
border-radius: 50% !important;
}
.rdp-day_selected.has-events::before,
.rdp-day_range_start.has-events::before,
.rdp-day_range_end.has-events::before {
background-color: white !important;
}
.rdp-day_range_middle.has-events::before {
background-color: #2563eb !important;
}
.rdp-months {
gap: 2rem !important;
}
.rdp-month {
padding: 0.75rem !important;
}
.rdp-table {
border-spacing: 0 !important;
margin-top: 1rem !important;
}
.rdp-cell {
padding: 2px !important;
}
.rdp-day[style*="background"] {
background: transparent !important;
}
.rdp * { border-color: transparent !important; }
.rdp-day { font-size: 0.875rem !important; min-width: 36px !important; height: 36px !important; border-radius: 50% !important; transition: all 0.2s ease !important; font-weight: 500 !important; position: relative !important; }
.rdp-day button { width: 100% !important; height: 100% !important; border-radius: 50% !important; background-color: transparent !important; }
.rdp-day_range_start, .rdp-day_range_start > button { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important; color: white !important; font-weight: 700 !important; border-radius: 50% !important; box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important; }
.rdp-day_range_end, .rdp-day_range_end > button { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important; color: white !important; font-weight: 700 !important; border-radius: 50% !important; box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important; }
.rdp-day_selected:not(.rdp-day_range_start):not(.rdp-day_range_end), .rdp-day_selected:not(.rdp-day_range_start):not(.rdp-day_range_end) > button { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important; color: white !important; font-weight: 700 !important; border-radius: 50% !important; box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important; }
.rdp-day_selected, .rdp-day_selected > button { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important; color: white !important; font-weight: 700 !important; border-radius: 50% !important; box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important; }
.rdp-day_range_middle, .rdp-day_range_middle > button { background-color: #dbeafe !important; background: #dbeafe !important; color: #2563eb !important; font-weight: 600 !important; border-radius: 0 !important; box-shadow: none !important; }
.rdp-day_range_start.rdp-day_range_end, .rdp-day_range_start.rdp-day_range_end > button { border-radius: 50% !important; background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important; }
.rdp-day:hover:not(.rdp-day_selected):not(.rdp-day_disabled):not(.rdp-day_range_start):not(.rdp-day_range_end):not(.rdp-day_range_middle) > button { background-color: #eff6ff !important; background: #eff6ff !important; color: #2563eb !important; border-radius: 50% !important; }
.rdp-day_today:not(.rdp-day_selected):not(.rdp-day_range_start):not(.rdp-day_range_end)::after { content: '' !important; position: absolute !important; bottom: 4px !important; left: 50% !important; transform: translateX(-50%) !important; width: 4px !important; height: 4px !important; background-color: #ec4899 !important; border-radius: 50% !important; z-index: 10 !important; }
.rdp-day_today.rdp-day_selected, .rdp-day_today.rdp-day_range_start, .rdp-day_today.rdp-day_range_end { color: white !important; }
.rdp-day_today.rdp-day_selected > button, .rdp-day_today.rdp-day_range_start > button, .rdp-day_today.rdp-day_range_end > button { color: white !important; }
.rdp-day_outside, .rdp-day_outside > button { color: #cbd5e1 !important; opacity: 0.5 !important; }
.rdp-day_disabled, .rdp-day_disabled > button { opacity: 0.3 !important; cursor: not-allowed !important; }
.rdp-day_selected, .rdp-day_range_start, .rdp-day_range_end, .rdp-day_range_middle { opacity: 1 !important; visibility: visible !important; z-index: 5 !important; }
.rdp-day.has-events:not(.rdp-day_selected):not(.rdp-day_range_start):not(.rdp-day_range_end)::before { content: '' !important; position: absolute !important; top: 4px !important; right: 4px !important; width: 4px !important; height: 4px !important; background-color: #2563eb !important; border-radius: 50% !important; }
.rdp-day_selected.has-events::before, .rdp-day_range_start.has-events::before, .rdp-day_range_end.has-events::before { background-color: white !important; }
.rdp-day_range_middle.has-events::before { background-color: #2563eb !important; }
.rdp-head_cell { color: #64748b !important; font-weight: 600 !important; font-size: 0.75rem !important; text-transform: uppercase !important; padding: 8px 0 !important; }
.rdp-caption_label { font-size: 1rem !important; font-weight: 700 !important; color: #0f172a !important; }
.rdp-nav_button { width: 32px !important; height: 32px !important; border-radius: 6px !important; transition: all 0.2s ease !important; }
.rdp-nav_button:hover { background-color: #eff6ff !important; color: #2563eb !important; }
.rdp-months { gap: 2rem !important; }
.rdp-month { padding: 0.75rem !important; }
.rdp-table { border-spacing: 0 !important; margin-top: 1rem !important; }
.rdp-cell { padding: 2px !important; }
.rdp-day[style*="background"] { background: transparent !important; }
`}</style>
<header className="bg-white border-b border-slate-200 shadow-sm sticky top-0 z-30">
@@ -490,20 +333,14 @@ export default function Layout({ children }) {
<div className="border-b border-slate-200 p-6">
<Link to={getDashboardUrl(userRole)} className="flex items-center gap-3 mb-4" onClick={() => setMobileMenuOpen(false)}>
<div className="w-8 h-8 flex items-center justify-center">
<img
src="https://qtrypzzcjebvfcihiynt.supabase.co/storage/v1/object/public/base44-prod/public/68fc6cf01386035c266e7a5d/3ba390829_KROWlogo.png"
alt="KROW Logo"
className="w-full h-full object-contain"
/>
<img src="https://qtrypzzcjebvfcihiynt.supabase.co/storage/v1/object/public/base44-prod/public/68fc6cf01386035c266e7a5d/3ba390829_KROWlogo.png" alt="KROW Logo" className="w-full h-full object-contain" />
</div>
<h2 className="font-bold text-[#1C323E]">KROW</h2>
</Link>
<div className="flex items-center gap-3 bg-slate-50 p-3 rounded-lg">
<Avatar className="w-10 h-10">
<AvatarImage src={userAvatar} alt={userName} />
<AvatarFallback className="bg-[#0A39DF] text-white font-bold">
{userInitial}
</AvatarFallback>
<AvatarFallback className="bg-[#0A39DF] text-white font-bold">{userInitial}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="font-semibold text-[#1C323E] text-sm truncate">{userName}</p>
@@ -515,13 +352,8 @@ export default function Layout({ children }) {
<NavigationMenu location={location} userRole={userRole} closeSheet={() => setMobileMenuOpen(false)} />
</div>
<div className="p-3 border-t border-slate-200">
<Button
variant="ghost"
className="w-full justify-start text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={() => {handleLogout(); setMobileMenuOpen(false);}}
>
<LogOut className="w-4 h-4 mr-2" />
Logout
<Button variant="ghost" className="w-full justify-start text-red-600 hover:text-red-700 hover:bg-red-50" onClick={() => {handleLogout(); setMobileMenuOpen(false);}}>
<LogOut className="w-4 h-4 mr-2" />Logout
</Button>
</div>
</SheetContent>
@@ -529,11 +361,7 @@ export default function Layout({ children }) {
<Link to={getDashboardUrl(userRole)} className="flex items-center gap-2 hover:opacity-80 transition-opacity">
<div className="w-8 h-8 flex items-center justify-center">
<img
src="https://qtrypzzcjebvfcihiynt.supabase.co/storage/v1/object/public/base44-prod/public/68fc6cf01386035c266e7a5d/3ba390829_KROWlogo.png"
alt="KROW Logo"
className="w-full h-full object-contain"
/>
<img src="https://qtrypzzcjebvfcihiynt.supabase.co/storage/v1/object/public/base44-prod/public/68fc6cf01386035c266e7a5d/3ba390829_KROWlogo.png" alt="KROW Logo" className="w-full h-full object-contain" />
</div>
<div className="hidden sm:block">
<h1 className="text-base font-bold text-[#1C323E]">KROW Workforce Control Tower</h1>
@@ -543,39 +371,22 @@ export default function Layout({ children }) {
<div className="hidden md:flex flex-1 max-w-xl">
<div className="relative w-full">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Find employees, menu items, settings, and more..."
className="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#0A39DF] focus:border-transparent"
/>
<input type="text" placeholder="Find employees, menu items, settings, and more..." className="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#0A39DF] focus:border-transparent" />
</div>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleRefresh}
className="flex items-center gap-2 px-3 py-2 text-slate-500 hover:text-[#0A39DF] hover:bg-blue-50 rounded-lg transition-all group"
title="Unpublished changes - Click to refresh"
>
<button onClick={handleRefresh} className="flex items-center gap-2 px-3 py-2 text-slate-500 hover:text-[#0A39DF] hover:bg-blue-50 rounded-lg transition-all group" title="Unpublished changes - Click to refresh">
<CloudOff className="w-5 h-5 group-hover:animate-pulse" />
<span className="hidden lg:inline text-sm font-medium">Unpublished changes</span>
</button>
<Button
variant="ghost"
size="icon"
className="md:hidden hover:bg-slate-100"
title="Search"
>
<Button variant="ghost" size="icon" className="md:hidden hover:bg-slate-100" title="Search">
<Search className="w-5 h-5 text-slate-600" />
</Button>
<button
onClick={() => setShowNotifications(true)}
className="relative p-2 hover:bg-slate-100 rounded-lg transition-colors"
title="Notifications"
>
<button onClick={() => setShowNotifications(true)} className="relative p-2 hover:bg-slate-100 rounded-lg transition-colors" title="Notifications">
<Bell className="w-5 h-5 text-slate-600" />
{unreadCount > 0 && (
<span className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white text-[10px] font-bold rounded-full flex items-center justify-center">
@@ -609,22 +420,21 @@ export default function Layout({ children }) {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem onClick={() => window.location.href = createPageUrl("NotificationSettings")}>
<Bell className="w-4 h-4 mr-2" />Notification Settings
</DropdownMenuItem>
<DropdownMenuItem onClick={() => window.location.href = createPageUrl("Settings")}>
<SettingsIcon className="w-4 h-4 mr-2" />
Settings
<SettingsIcon className="w-4 h-4 mr-2" />Settings
</DropdownMenuItem>
<DropdownMenuItem onClick={() => window.location.href = createPageUrl("Reports")}>
<FileText className="w-4 h-4 mr-2" />
Reports
<FileText className="w-4 h-4 mr-2" />Reports
</DropdownMenuItem>
<DropdownMenuItem onClick={() => window.location.href = createPageUrl("ActivityLog")}>
<Activity className="w-4 h-4 mr-2" />
Activity Log
<Activity className="w-4 h-4 mr-2" />Activity Log
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout} className="text-red-600">
<LogOut className="w-4 h-4 mr-2" />
Logout
<LogOut className="w-4 h-4 mr-2" />Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -634,9 +444,7 @@ export default function Layout({ children }) {
<button className="flex items-center gap-2 hover:bg-slate-100 rounded-lg p-1.5 transition-colors" title={`${userName} - ${getRoleName(userRole)}`}>
<Avatar className="w-8 h-8">
<AvatarImage src={userAvatar} alt={userName} />
<AvatarFallback className="bg-gradient-to-br from-[#0A39DF] to-[#1C323E] text-white font-bold text-sm">
{userInitial}
</AvatarFallback>
<AvatarFallback className="bg-gradient-to-br from-[#0A39DF] to-[#1C323E] text-white font-bold text-sm">{userInitial}</AvatarFallback>
</Avatar>
<span className="hidden lg:block text-sm font-medium text-slate-700">{userName.split(' ')[0]}</span>
</button>
@@ -651,12 +459,10 @@ export default function Layout({ children }) {
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => window.location.href = getDashboardUrl(userRole)}>
<Home className="w-4 h-4 mr-2" />
Dashboard
<Home className="w-4 h-4 mr-2" />Dashboard
</DropdownMenuItem>
<DropdownMenuItem onClick={() => window.location.href = createPageUrl("WorkforceProfile")}>
<User className="w-4 h-4 mr-2" />
My Profile
<User className="w-4 h-4 mr-2" />My Profile
</DropdownMenuItem>
<DropdownMenuSeparator />
</DropdownMenuContent>
@@ -686,15 +492,11 @@ export default function Layout({ children }) {
</div>
</div>
<NotificationPanel
isOpen={showNotifications}
onClose={() => setShowNotifications(false)}
/>
<NotificationPanel isOpen={showNotifications} onClose={() => setShowNotifications(false)} />
<NotificationEngine />
<ChatBubble />
<RoleSwitcher />
<Toaster />
</div>
);
}

View File

@@ -0,0 +1,271 @@
import React, { useState } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { Bell, Mail, Calendar, Briefcase, AlertCircle, CheckCircle } from "lucide-react";
import { useToast } from "@/components/ui/use-toast";
import { Badge } from "@/components/ui/badge";
export default function NotificationSettings() {
const { toast } = useToast();
const queryClient = useQueryClient();
const { data: currentUser } = useQuery({
queryKey: ['current-user-notification-settings'],
queryFn: () => base44.auth.me(),
});
const [preferences, setPreferences] = useState(
currentUser?.notification_preferences || {
email_notifications: true,
in_app_notifications: true,
shift_assignments: true,
shift_reminders: true,
shift_changes: true,
upcoming_events: true,
new_leads: true,
invoice_updates: true,
system_alerts: true,
}
);
const updatePreferencesMutation = useMutation({
mutationFn: (prefs) => base44.auth.updateMe({ notification_preferences: prefs }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['current-user-notification-settings'] });
toast({
title: "✅ Settings Updated",
description: "Your notification preferences have been saved",
});
},
onError: (error) => {
toast({
title: "❌ Update Failed",
description: error.message,
variant: "destructive",
});
},
});
const handleToggle = (key) => {
setPreferences(prev => ({ ...prev, [key]: !prev[key] }));
};
const handleSave = () => {
updatePreferencesMutation.mutate(preferences);
};
const userRole = currentUser?.role || currentUser?.user_role || 'admin';
return (
<div className="p-4 md:p-8 bg-slate-50 min-h-screen">
<div className="max-w-4xl mx-auto">
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-900">Notification Settings</h1>
<p className="text-sm text-slate-500 mt-1">
Configure how and when you receive notifications
</p>
</div>
<div className="space-y-4">
{/* Global Settings */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Bell className="w-5 h-5" />
Global Notification Settings
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div className="flex items-center gap-3">
<Bell className="w-5 h-5 text-blue-600" />
<div>
<Label className="font-semibold">In-App Notifications</Label>
<p className="text-sm text-slate-500">Receive notifications in the app</p>
</div>
</div>
<Switch
checked={preferences.in_app_notifications}
onCheckedChange={() => handleToggle('in_app_notifications')}
/>
</div>
<div className="flex items-center justify-between p-3 bg-purple-50 rounded-lg">
<div className="flex items-center gap-3">
<Mail className="w-5 h-5 text-purple-600" />
<div>
<Label className="font-semibold">Email Notifications</Label>
<p className="text-sm text-slate-500">Receive notifications via email</p>
</div>
</div>
<Switch
checked={preferences.email_notifications}
onCheckedChange={() => handleToggle('email_notifications')}
/>
</div>
</CardContent>
</Card>
{/* Staff/Workforce Notifications */}
{(userRole === 'workforce' || userRole === 'admin' || userRole === 'vendor') && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Calendar className="w-5 h-5" />
Shift Notifications
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center justify-between p-3 hover:bg-slate-50 rounded-lg">
<div>
<Label className="font-semibold">Shift Assignments</Label>
<p className="text-sm text-slate-500">When you're assigned to a new shift</p>
</div>
<Switch
checked={preferences.shift_assignments}
onCheckedChange={() => handleToggle('shift_assignments')}
/>
</div>
<div className="flex items-center justify-between p-3 hover:bg-slate-50 rounded-lg">
<div>
<Label className="font-semibold">Shift Reminders</Label>
<p className="text-sm text-slate-500">24 hours before your shift starts</p>
</div>
<Switch
checked={preferences.shift_reminders}
onCheckedChange={() => handleToggle('shift_reminders')}
/>
</div>
<div className="flex items-center justify-between p-3 hover:bg-slate-50 rounded-lg">
<div>
<Label className="font-semibold">Shift Changes</Label>
<p className="text-sm text-slate-500">When shift details are modified</p>
</div>
<Switch
checked={preferences.shift_changes}
onCheckedChange={() => handleToggle('shift_changes')}
/>
</div>
</CardContent>
</Card>
)}
{/* Client Notifications */}
{(userRole === 'client' || userRole === 'admin') && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Briefcase className="w-5 h-5" />
Event Notifications
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center justify-between p-3 hover:bg-slate-50 rounded-lg">
<div>
<Label className="font-semibold">Upcoming Events</Label>
<p className="text-sm text-slate-500">Reminders 3 days before your event</p>
</div>
<Switch
checked={preferences.upcoming_events}
onCheckedChange={() => handleToggle('upcoming_events')}
/>
</div>
<div className="flex items-center justify-between p-3 hover:bg-slate-50 rounded-lg">
<div>
<Label className="font-semibold">Staff Updates</Label>
<p className="text-sm text-slate-500">When staff are assigned or changed</p>
</div>
<Switch
checked={preferences.shift_changes}
onCheckedChange={() => handleToggle('shift_changes')}
/>
</div>
</CardContent>
</Card>
)}
{/* Vendor Notifications */}
{(userRole === 'vendor' || userRole === 'admin') && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Briefcase className="w-5 h-5" />
Business Notifications
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center justify-between p-3 hover:bg-slate-50 rounded-lg">
<div>
<Label className="font-semibold">New Leads</Label>
<p className="text-sm text-slate-500">When new staffing opportunities are available</p>
</div>
<Switch
checked={preferences.new_leads}
onCheckedChange={() => handleToggle('new_leads')}
/>
</div>
<div className="flex items-center justify-between p-3 hover:bg-slate-50 rounded-lg">
<div>
<Label className="font-semibold">Invoice Updates</Label>
<p className="text-sm text-slate-500">Invoice status changes and payments</p>
</div>
<Switch
checked={preferences.invoice_updates}
onCheckedChange={() => handleToggle('invoice_updates')}
/>
</div>
</CardContent>
</Card>
)}
{/* System Notifications */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<AlertCircle className="w-5 h-5" />
System Notifications
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between p-3 hover:bg-slate-50 rounded-lg">
<div>
<Label className="font-semibold">System Alerts</Label>
<p className="text-sm text-slate-500">Important platform updates and announcements</p>
</div>
<Switch
checked={preferences.system_alerts}
onCheckedChange={() => handleToggle('system_alerts')}
/>
</div>
</CardContent>
</Card>
{/* Save Button */}
<div className="flex justify-end gap-3">
<Button
variant="outline"
onClick={() => setPreferences(currentUser?.notification_preferences || {})}
>
Reset
</Button>
<Button
onClick={handleSave}
disabled={updatePreferencesMutation.isPending}
className="bg-[#0A39DF]"
>
{updatePreferencesMutation.isPending ? "Saving..." : "Save Preferences"}
</Button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,505 @@
import React, { useState, useEffect } 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, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
import { Zap, Send, Check, Edit3, MapPin, Clock, Users, AlertCircle, Sparkles, Mic, X, Calendar as CalendarIcon, ArrowLeft } from "lucide-react";
import { useToast } from "@/components/ui/use-toast";
import { motion, AnimatePresence } from "framer-motion";
import { format } from "date-fns";
// Helper function to convert 24-hour time to 12-hour format
const convertTo12Hour = (time24) => {
if (!time24 || time24 === "—") return time24;
try {
const parts = time24.split(':');
if (!parts || parts.length < 2) return time24;
const hours = parseInt(parts[0], 10);
const minutes = parseInt(parts[1], 10);
if (isNaN(hours) || isNaN(minutes)) return time24;
const period = hours >= 12 ? 'PM' : 'AM';
const hours12 = hours % 12 || 12;
const minutesStr = minutes.toString().padStart(2, '0');
return `${hours12}:${minutesStr} ${period}`;
} catch (error) {
console.error('Error converting time:', error);
return time24;
}
};
export default function RapidOrder() {
const navigate = useNavigate();
const { toast } = useToast();
const queryClient = useQueryClient();
const [message, setMessage] = useState("");
const [conversation, setConversation] = useState([]);
const [detectedOrder, setDetectedOrder] = useState(null);
const [isProcessing, setIsProcessing] = useState(false);
const [isListening, setIsListening] = useState(false);
const [submissionTime, setSubmissionTime] = useState(null);
const { data: user } = useQuery({
queryKey: ['current-user-rapid'],
queryFn: () => base44.auth.me(),
});
const { data: businesses } = useQuery({
queryKey: ['user-businesses'],
queryFn: () => base44.entities.Business.filter({ contact_name: user?.full_name }),
enabled: !!user,
initialData: [],
});
const createRapidOrderMutation = useMutation({
mutationFn: (orderData) => base44.entities.Event.create(orderData),
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['events'] });
queryClient.invalidateQueries({ queryKey: ['client-events'] });
const now = new Date();
setSubmissionTime(now);
toast({
title: "✅ RAPID Order Created",
description: "Order sent to preferred vendor with priority notification",
});
// Show success message in chat
setConversation(prev => [...prev, {
role: 'assistant',
content: `🚀 **Order Submitted Successfully!**\n\nOrder Number: **${data.id?.slice(-8) || 'RAPID-001'}**\nSubmitted: **${format(now, 'h:mm:ss a')}**\n\nYour preferred vendor has been notified and will assign staff shortly.`,
isSuccess: true
}]);
// Reset after delay
setTimeout(() => {
navigate(createPageUrl("ClientDashboard"));
}, 3000);
},
});
const analyzeMessage = async (msg) => {
setIsProcessing(true);
setConversation(prev => [...prev, { role: 'user', content: msg }]);
try {
const response = await base44.integrations.Core.InvokeLLM({
prompt: `You are an order assistant. Analyze this message and extract order details:
Message: "${msg}"
Current user: ${user?.full_name}
User's locations: ${businesses.map(b => b.business_name).join(', ')}
Extract:
1. Urgency keywords (ASAP, today, emergency, call out, urgent, rapid, now)
2. Role/position needed (cook, bartender, server, dishwasher, etc.)
3. Number of staff (if mentioned, parse the number correctly - e.g., "5 cooks" = 5, "need 3 servers" = 3)
4. End time (if mentioned, extract the time - e.g., "until 5am" = "05:00", "until 11pm" = "23:00", "until midnight" = "00:00")
5. Location (if mentioned, otherwise use first available location)
IMPORTANT:
- Make sure to correctly extract the number of staff from phrases like "need 5 cooks" or "I need 3 bartenders"
- If end time is mentioned (e.g., "until 5am", "till 11pm"), extract it in 24-hour format (e.g., "05:00", "23:00")
- If no end time is mentioned, leave it as null
Return a concise summary.`,
response_json_schema: {
type: "object",
properties: {
is_urgent: { type: "boolean" },
role: { type: "string" },
count: { type: "number" },
location: { type: "string" },
end_time: { type: "string" }
}
}
});
const parsed = response;
const primaryLocation = businesses[0]?.business_name || "Primary Location";
// Ensure count is properly set - default to 1 if not detected
const staffCount = parsed.count && parsed.count > 0 ? parsed.count : 1;
// Get current time for start_time (when ASAP)
const now = new Date();
const currentTime = format(now, 'HH:mm');
// Handle end_time - use parsed end time or current time as confirmation time
const endTime = parsed.end_time || currentTime;
const order = {
is_rapid: parsed.is_urgent || true,
role: parsed.role || "Staff Member",
count: staffCount,
location: parsed.location || primaryLocation,
start_time: currentTime, // Always use current time for ASAP orders (24-hour format for storage)
end_time: endTime, // Use parsed end time or current time (24-hour format for storage)
start_time_display: convertTo12Hour(currentTime), // For display
end_time_display: convertTo12Hour(endTime), // For display
business_name: primaryLocation,
hub: businesses[0]?.hub_building || "Main Hub",
submission_time: now // Store the actual submission time
};
setDetectedOrder(order);
const aiMessage = `Is this a RAPID ORDER for **${order.count} ${order.role}${order.count > 1 ? 's' : ''}** at **${order.location}**?\n\nStart Time: ${order.start_time_display}\nEnd Time: ${order.end_time_display}`;
setConversation(prev => [...prev, {
role: 'assistant',
content: aiMessage,
showConfirm: true
}]);
} catch (error) {
setConversation(prev => [...prev, {
role: 'assistant',
content: "I couldn't process that. Please provide more details like: role needed, how many, and when."
}]);
} finally {
setIsProcessing(false);
}
};
const handleSendMessage = () => {
if (!message.trim()) return;
analyzeMessage(message);
setMessage("");
};
const handleVoiceInput = () => {
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
toast({
title: "Voice not supported",
description: "Your browser doesn't support voice input",
variant: "destructive",
});
return;
}
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();
recognition.onstart = () => setIsListening(true);
recognition.onend = () => setIsListening(false);
recognition.onresult = (event) => {
const transcript = event.results[0][0].transcript;
setMessage(transcript);
analyzeMessage(transcript);
};
recognition.onerror = () => {
setIsListening(false);
toast({
title: "Voice input failed",
description: "Please try typing instead",
variant: "destructive",
});
};
recognition.start();
};
const handleConfirmOrder = () => {
if (!detectedOrder) return;
const now = new Date();
const confirmTime = format(now, 'HH:mm');
const confirmTime12Hour = convertTo12Hour(confirmTime);
// Create comprehensive order data with proper requested field and actual times
const orderData = {
event_name: `RAPID: ${detectedOrder.count} ${detectedOrder.role}${detectedOrder.count > 1 ? 's' : ''}`,
is_rapid: true,
status: "Pending",
business_name: detectedOrder.business_name,
hub: detectedOrder.hub,
event_location: detectedOrder.location,
date: now.toISOString().split('T')[0],
requested: Number(detectedOrder.count), // Ensure it's a number
client_name: user?.full_name,
client_email: user?.email,
notes: `RAPID ORDER - Submitted at ${detectedOrder.start_time_display} - Confirmed at ${confirmTime12Hour}\nStart: ${detectedOrder.start_time_display} | End: ${detectedOrder.end_time_display}`,
shifts: [{
shift_name: "Emergency Shift",
location: detectedOrder.location,
roles: [{
role: detectedOrder.role,
count: Number(detectedOrder.count), // Ensure it's a number
start_time: detectedOrder.start_time, // Store in 24-hour format
end_time: detectedOrder.end_time // Store in 24-hour format
}]
}]
};
console.log('Creating RAPID order with data:', orderData); // Debug log
createRapidOrderMutation.mutate(orderData);
};
const handleEditOrder = () => {
setConversation(prev => [...prev, {
role: 'assistant',
content: "Please describe what you'd like to change."
}]);
setDetectedOrder(null);
};
return (
<div className="min-h-screen bg-gradient-to-br from-red-50 via-orange-50 to-yellow-50 p-6">
<div className="max-w-5xl mx-auto space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="icon"
onClick={() => navigate(createPageUrl("ClientDashboard"))}
className="hover:bg-white/50"
>
<ArrowLeft className="w-5 h-5" />
</Button>
<div>
<div className="flex items-center gap-3 mb-2">
<div className="w-12 h-12 bg-gradient-to-br from-red-500 to-orange-500 rounded-xl flex items-center justify-center shadow-lg">
<Zap className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold text-red-700 flex items-center gap-2">
<Sparkles className="w-6 h-6" />
RAPID Order
</h1>
<p className="text-sm text-red-600 mt-1">Emergency staffing in minutes</p>
</div>
</div>
</div>
</div>
<div className="text-right">
<div className="flex items-center gap-2 text-sm text-slate-600 mb-1">
<CalendarIcon className="w-4 h-4" />
<span>{format(new Date(), 'EEEE, MMMM d, yyyy')}</span>
</div>
<div className="flex items-center gap-2 text-sm text-slate-600">
<Clock className="w-4 h-4" />
<span>{format(new Date(), 'h:mm a')}</span>
</div>
</div>
</div>
<Card className="bg-white border-2 border-red-300 shadow-2xl">
<CardHeader className="border-b border-red-200 bg-gradient-to-r from-red-50 to-orange-50">
<div className="flex items-center justify-between">
<CardTitle className="text-lg font-bold text-red-700">
Tell us what you need
</CardTitle>
<Badge className="bg-red-600 text-white font-bold text-sm px-4 py-2 shadow-md animate-pulse">
URGENT
</Badge>
</div>
</CardHeader>
<CardContent className="p-6">
{/* Chat Messages */}
<div className="space-y-4 mb-6 max-h-[500px] overflow-y-auto">
{conversation.length === 0 && (
<div className="text-center py-12">
<div className="w-20 h-20 mx-auto mb-6 bg-gradient-to-br from-red-500 to-orange-500 rounded-2xl flex items-center justify-center shadow-2xl">
<Zap className="w-10 h-10 text-white" />
</div>
<h3 className="font-bold text-2xl text-slate-900 mb-3">Need staff urgently?</h3>
<p className="text-base text-slate-600 mb-6">Type or speak what you need, I'll handle the rest</p>
<div className="text-left max-w-lg mx-auto space-y-3">
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 p-4 rounded-xl border-2 border-blue-200 text-sm">
<strong className="text-blue-900">Example:</strong> <span className="text-slate-700">"We had a call out. Need 2 cooks ASAP"</span>
</div>
<div className="bg-gradient-to-r from-purple-50 to-pink-50 p-4 rounded-xl border-2 border-purple-200 text-sm">
<strong className="text-purple-900">Example:</strong> <span className="text-slate-700">"Need 5 bartenders ASAP until 5am"</span>
</div>
<div className="bg-gradient-to-r from-green-50 to-emerald-50 p-4 rounded-xl border-2 border-green-200 text-sm">
<strong className="text-green-900">Example:</strong> <span className="text-slate-700">"Emergency! Need 3 servers right now till midnight"</span>
</div>
</div>
</div>
)}
<AnimatePresence>
{conversation.map((msg, idx) => (
<motion.div
key={idx}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div className={`max-w-[85%] ${
msg.role === 'user'
? 'bg-gradient-to-br from-blue-600 to-blue-700 text-white'
: msg.isSuccess
? 'bg-gradient-to-br from-green-50 to-emerald-50 border-2 border-green-300'
: 'bg-white border-2 border-red-200'
} rounded-2xl p-5 shadow-lg`}>
{msg.role === 'assistant' && !msg.isSuccess && (
<div className="flex items-center gap-2 mb-3">
<div className="w-7 h-7 bg-gradient-to-br from-red-500 to-orange-500 rounded-full flex items-center justify-center">
<Sparkles className="w-4 h-4 text-white" />
</div>
<span className="text-xs font-bold text-red-600">AI Assistant</span>
</div>
)}
<p className={`text-base whitespace-pre-line ${
msg.role === 'user' ? 'text-white' :
msg.isSuccess ? 'text-green-900' :
'text-slate-900'
}`}>
{msg.content}
</p>
{msg.showConfirm && detectedOrder && (
<div className="mt-5 space-y-4">
<div className="grid grid-cols-2 gap-4 p-4 bg-gradient-to-br from-slate-50 to-blue-50 rounded-xl border-2 border-blue-300">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center">
<Users className="w-5 h-5 text-white" />
</div>
<div>
<p className="text-xs text-slate-500 font-semibold">Staff Needed</p>
<p className="font-bold text-base text-slate-900">{detectedOrder.count} {detectedOrder.role}{detectedOrder.count > 1 ? 's' : ''}</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center">
<MapPin className="w-5 h-5 text-white" />
</div>
<div>
<p className="text-xs text-slate-500 font-semibold">Location</p>
<p className="font-bold text-base text-slate-900">{detectedOrder.location}</p>
</div>
</div>
<div className="flex items-center gap-3 col-span-2">
<div className="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center">
<Clock className="w-5 h-5 text-white" />
</div>
<div>
<p className="text-xs text-slate-500 font-semibold">Time</p>
<p className="font-bold text-base text-slate-900">
Start: {detectedOrder.start_time_display} | End: {detectedOrder.end_time_display}
</p>
</div>
</div>
</div>
<div className="flex gap-3">
<Button
onClick={handleConfirmOrder}
disabled={createRapidOrderMutation.isPending}
className="flex-1 bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-700 hover:to-orange-700 text-white font-bold shadow-xl text-base py-6"
>
<Check className="w-5 h-5 mr-2" />
{createRapidOrderMutation.isPending ? "Creating..." : "CONFIRM & SEND"}
</Button>
<Button
onClick={handleEditOrder}
variant="outline"
className="border-2 border-red-300 hover:bg-red-50 text-base py-6"
>
<Edit3 className="w-5 h-5 mr-2" />
EDIT
</Button>
</div>
</div>
)}
</div>
</motion.div>
))}
</AnimatePresence>
{isProcessing && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex justify-start"
>
<div className="bg-white border-2 border-red-200 rounded-2xl p-5 shadow-lg">
<div className="flex items-center gap-3">
<div className="w-7 h-7 bg-gradient-to-br from-red-500 to-orange-500 rounded-full flex items-center justify-center animate-pulse">
<Sparkles className="w-4 h-4 text-white" />
</div>
<span className="text-base text-slate-600">Processing your request...</span>
</div>
</div>
</motion.div>
)}
</div>
{/* Input */}
<div className="space-y-4">
<div className="flex gap-2">
<Textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
}}
placeholder="Type or speak... (e.g., 'Need 5 cooks ASAP until 5am')"
className="flex-1 border-2 border-red-300 focus:border-red-500 text-base resize-none"
rows={3}
disabled={isProcessing}
/>
</div>
<div className="flex gap-2">
<Button
onClick={handleVoiceInput}
disabled={isProcessing || isListening}
variant="outline"
className={`border-2 ${isListening ? 'border-red-500 bg-red-50' : 'border-red-300'} hover:bg-red-50 text-base py-6 px-6`}
>
<Mic className={`w-5 h-5 mr-2 ${isListening ? 'animate-pulse text-red-600' : ''}`} />
{isListening ? 'Listening...' : 'Speak'}
</Button>
<Button
onClick={handleSendMessage}
disabled={!message.trim() || isProcessing}
className="flex-1 bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-700 hover:to-orange-700 text-white font-bold shadow-xl text-base py-6"
>
<Send className="w-5 h-5 mr-2" />
Send Message
</Button>
</div>
</div>
{/* Helper Text */}
<div className="mt-4 p-4 bg-blue-50 border-2 border-blue-200 rounded-xl">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" />
<div className="text-sm text-blue-800">
<strong>Tip:</strong> Include role, quantity, and urgency for fastest processing.
Optionally add end time like "until 5am" or "till midnight".
AI will auto-detect your location and send to your preferred vendor with priority notification.
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
import React from "react";
import { useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { ArrowRight, Sparkles } from "lucide-react";
export default function SmartScheduler() {
const navigate = useNavigate();
return (
<div className="p-4 md:p-8 bg-slate-50 min-h-screen flex items-center justify-center">
<Card className="max-w-2xl w-full">
<CardContent className="p-12 text-center">
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center mx-auto mb-6">
<Sparkles className="w-8 h-8 text-white" />
</div>
<h1 className="text-3xl font-bold text-slate-900 mb-4">
Smart Scheduling is Now Part of Orders
</h1>
<p className="text-lg text-slate-600 mb-8">
All smart assignment, automation, and scheduling features have been unified into the main Order Management view for a consistent experience.
</p>
<Button
size="lg"
onClick={() => navigate(createPageUrl("Events"))}
className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
>
Go to Order Management
<ArrowRight className="w-5 h-5 ml-2" />
</Button>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,197 @@
import React, { useState } from "react";
import { base44 } from "@/api/base44Client";
import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { CheckCircle, Circle } from "lucide-react";
import { useToast } from "@/components/ui/use-toast";
import ProfileSetupStep from "@/components/onboarding/ProfileSetupStep";
import DocumentUploadStep from "@/components/onboarding/DocumentUploadStep";
import TrainingStep from "@/components/onboarding/TrainingStep";
import CompletionStep from "@/components/onboarding/CompletionStep";
const steps = [
{ id: 1, name: "Profile Setup", description: "Basic information" },
{ id: 2, name: "Documents", description: "Upload required documents" },
{ id: 3, name: "Training", description: "Complete compliance training" },
{ id: 4, name: "Complete", description: "Finish onboarding" },
];
export default function StaffOnboarding() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { toast } = useToast();
const [currentStep, setCurrentStep] = useState(1);
const [onboardingData, setOnboardingData] = useState({
profile: {},
documents: [],
training: { completed: [] },
});
const { data: currentUser } = useQuery({
queryKey: ['current-user-onboarding'],
queryFn: () => base44.auth.me(),
});
const createStaffMutation = useMutation({
mutationFn: (staffData) => base44.entities.Staff.create(staffData),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['staff'] });
toast({
title: "✅ Onboarding Complete",
description: "Welcome to KROW! Your profile is now active.",
});
navigate(createPageUrl("WorkforceDashboard"));
},
onError: (error) => {
toast({
title: "❌ Onboarding Failed",
description: error.message,
variant: "destructive",
});
},
});
const handleNext = (stepData) => {
setOnboardingData(prev => ({
...prev,
[stepData.type]: stepData.data,
}));
if (currentStep < steps.length) {
setCurrentStep(currentStep + 1);
}
};
const handleBack = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
const handleComplete = () => {
const staffData = {
employee_name: onboardingData.profile.full_name,
email: onboardingData.profile.email || currentUser?.email,
phone: onboardingData.profile.phone,
address: onboardingData.profile.address,
city: onboardingData.profile.city,
position: onboardingData.profile.position,
department: onboardingData.profile.department,
hub_location: onboardingData.profile.hub_location,
employment_type: onboardingData.profile.employment_type,
english: onboardingData.profile.english_level,
certifications: onboardingData.documents.filter(d => d.type === 'certification').map(d => ({
name: d.name,
issued_date: d.issued_date,
expiry_date: d.expiry_date,
document_url: d.url,
})),
background_check_status: onboardingData.documents.some(d => d.type === 'background_check') ? 'pending' : 'not_required',
notes: `Onboarding completed: ${new Date().toISOString()}. Training modules completed: ${onboardingData.training.completed.length}`,
};
createStaffMutation.mutate(staffData);
};
const renderStep = () => {
switch (currentStep) {
case 1:
return (
<ProfileSetupStep
data={onboardingData.profile}
onNext={handleNext}
currentUser={currentUser}
/>
);
case 2:
return (
<DocumentUploadStep
data={onboardingData.documents}
onNext={handleNext}
onBack={handleBack}
/>
);
case 3:
return (
<TrainingStep
data={onboardingData.training}
onNext={handleNext}
onBack={handleBack}
/>
);
case 4:
return (
<CompletionStep
data={onboardingData}
onComplete={handleComplete}
onBack={handleBack}
isSubmitting={createStaffMutation.isPending}
/>
);
default:
return null;
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50 p-4 md:p-8">
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-slate-900 mb-2">
Welcome to KROW! 👋
</h1>
<p className="text-slate-600">
Let's get you set up in just a few steps
</p>
</div>
{/* Progress Steps */}
<div className="mb-8">
<div className="flex items-center justify-between">
{steps.map((step, idx) => (
<React.Fragment key={step.id}>
<div className="flex flex-col items-center flex-1">
<div className={`w-12 h-12 rounded-full flex items-center justify-center ${
currentStep > step.id
? "bg-green-500 text-white"
: currentStep === step.id
? "bg-[#0A39DF] text-white"
: "bg-slate-200 text-slate-400"
}`}>
{currentStep > step.id ? (
<CheckCircle className="w-6 h-6" />
) : (
<span className="font-bold">{step.id}</span>
)}
</div>
<p className={`text-sm font-medium mt-2 ${
currentStep >= step.id ? "text-slate-900" : "text-slate-400"
}`}>
{step.name}
</p>
<p className="text-xs text-slate-500">{step.description}</p>
</div>
{idx < steps.length - 1 && (
<div className={`flex-1 h-1 ${
currentStep > step.id ? "bg-green-500" : "bg-slate-200"
}`} />
)}
</React.Fragment>
))}
</div>
</div>
{/* Step Content */}
<Card>
<CardContent className="p-6 md:p-8">
{renderStep()}
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,466 @@
import React, { useState, useMemo } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { DragDropContext, Draggable } from "@hello-pangea/dnd";
import { Link2, Plus, Users } from "lucide-react";
import TaskCard from "@/components/tasks/TaskCard";
import TaskColumn from "@/components/tasks/TaskColumn";
import TaskDetailModal from "@/components/tasks/TaskDetailModal";
import { useToast } from "@/components/ui/use-toast";
export default function TaskBoard() {
const { toast } = useToast();
const queryClient = useQueryClient();
const [createDialog, setCreateDialog] = useState(false);
const [selectedTask, setSelectedTask] = useState(null);
const [selectedStatus, setSelectedStatus] = useState("pending");
const [newTask, setNewTask] = useState({
task_name: "",
description: "",
priority: "normal",
due_date: "",
progress: 0,
assigned_members: []
});
const [selectedMembers, setSelectedMembers] = useState([]);
const { data: user } = useQuery({
queryKey: ['current-user-taskboard'],
queryFn: () => base44.auth.me(),
});
const { data: teams = [] } = useQuery({
queryKey: ['teams'],
queryFn: () => base44.entities.Team.list(),
initialData: [],
});
const { data: teamMembers = [] } = useQuery({
queryKey: ['team-members'],
queryFn: () => base44.entities.TeamMember.list(),
initialData: [],
});
const { data: tasks = [] } = useQuery({
queryKey: ['tasks'],
queryFn: () => base44.entities.Task.list(),
initialData: [],
});
const userTeam = teams.find(t => t.owner_id === user?.id) || teams[0];
const teamTasks = tasks.filter(t => t.team_id === userTeam?.id);
const currentTeamMembers = teamMembers.filter(m => m.team_id === userTeam?.id);
const leadMembers = currentTeamMembers.filter(m => m.role === 'admin' || m.role === 'manager');
const regularMembers = currentTeamMembers.filter(m => m.role === 'member');
// Get unique departments from team members
const departments = [...new Set(currentTeamMembers.map(m => m.department).filter(Boolean))];
const tasksByStatus = useMemo(() => ({
pending: teamTasks.filter(t => t.status === 'pending').sort((a, b) => (a.order_index || 0) - (b.order_index || 0)),
in_progress: teamTasks.filter(t => t.status === 'in_progress').sort((a, b) => (a.order_index || 0) - (b.order_index || 0)),
on_hold: teamTasks.filter(t => t.status === 'on_hold').sort((a, b) => (a.order_index || 0) - (b.order_index || 0)),
completed: teamTasks.filter(t => t.status === 'completed').sort((a, b) => (a.order_index || 0) - (b.order_index || 0)),
}), [teamTasks]);
const overallProgress = useMemo(() => {
if (teamTasks.length === 0) return 0;
const totalProgress = teamTasks.reduce((sum, t) => sum + (t.progress || 0), 0);
return Math.round(totalProgress / teamTasks.length);
}, [teamTasks]);
const createTaskMutation = useMutation({
mutationFn: (taskData) => base44.entities.Task.create(taskData),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
setCreateDialog(false);
setNewTask({
task_name: "",
description: "",
priority: "normal",
due_date: "",
progress: 0,
assigned_members: []
});
setSelectedMembers([]);
toast({
title: "✅ Task Created",
description: "New task added to the board",
});
},
});
const updateTaskMutation = useMutation({
mutationFn: ({ id, data }) => base44.entities.Task.update(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
},
});
const handleDragEnd = (result) => {
if (!result.destination) return;
const { source, destination, draggableId } = result;
if (source.droppableId === destination.droppableId && source.index === destination.index) {
return;
}
const task = teamTasks.find(t => t.id === draggableId);
if (!task) return;
const newStatus = destination.droppableId;
updateTaskMutation.mutate({
id: task.id,
data: {
...task,
status: newStatus,
order_index: destination.index
}
});
};
const handleCreateTask = () => {
if (!newTask.task_name.trim()) {
toast({
title: "Task name required",
variant: "destructive",
});
return;
}
createTaskMutation.mutate({
...newTask,
team_id: userTeam?.id,
status: selectedStatus,
order_index: tasksByStatus[selectedStatus]?.length || 0,
assigned_members: selectedMembers.map(m => ({
member_id: m.id,
member_name: m.member_name,
avatar_url: m.avatar_url
})),
assigned_department: selectedMembers.length > 0 && selectedMembers[0].department ? selectedMembers[0].department : null
});
};
return (
<div className="p-6 bg-slate-50 min-h-screen">
<div className="max-w-[1800px] mx-auto">
{/* Header */}
<div className="bg-white rounded-xl p-6 mb-6 shadow-sm border border-slate-200">
<div className="flex items-center justify-between mb-5">
<div>
<h1 className="text-2xl font-bold text-slate-900 mb-2">Task Board</h1>
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-slate-600">Lead</span>
<div className="flex -space-x-2">
{leadMembers.slice(0, 3).map((member, idx) => (
<Avatar key={idx} className="w-8 h-8 border-2 border-white">
<img
src={member.avatar_url || `https://i.pravatar.cc/150?u=${encodeURIComponent(member.member_name)}`}
alt={member.member_name}
className="w-full h-full object-cover"
/>
</Avatar>
))}
{leadMembers.length > 3 && (
<div className="w-8 h-8 rounded-full bg-slate-200 border-2 border-white flex items-center justify-center text-xs font-semibold text-slate-600">
+{leadMembers.length - 3}
</div>
)}
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-slate-600">Team</span>
<div className="flex -space-x-2">
{regularMembers.slice(0, 3).map((member, idx) => (
<Avatar key={idx} className="w-8 h-8 border-2 border-white">
<img
src={member.avatar_url || `https://i.pravatar.cc/150?u=${encodeURIComponent(member.member_name)}`}
alt={member.member_name}
className="w-full h-full object-cover"
/>
</Avatar>
))}
{regularMembers.length > 3 && (
<div className="w-8 h-8 rounded-full bg-slate-200 border-2 border-white flex items-center justify-center text-xs font-semibold text-slate-600">
+{regularMembers.length - 3}
</div>
)}
</div>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<Button variant="outline" size="sm" className="border-slate-300">
<Link2 className="w-4 h-4 mr-2" />
Share
</Button>
<Button
onClick={() => {
setSelectedStatus("pending");
setCreateDialog(true);
}}
className="bg-[#0A39DF] hover:bg-blue-700"
>
<Plus className="w-4 h-4 mr-2" />
Create List
</Button>
</div>
</div>
{/* Overall Progress */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex-1 h-3 bg-slate-100 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-[#0A39DF] to-blue-600 transition-all"
style={{ width: `${overallProgress}%` }}
/>
</div>
<span className="text-sm font-bold text-slate-900 ml-4">{overallProgress}%</span>
</div>
</div>
</div>
{/* Kanban Board */}
<DragDropContext onDragEnd={handleDragEnd}>
<div className="flex gap-4 overflow-x-auto pb-4">
{['pending', 'in_progress', 'on_hold', 'completed'].map((status) => (
<TaskColumn
key={status}
status={status}
tasks={tasksByStatus[status]}
onAddTask={(status) => {
setSelectedStatus(status);
setCreateDialog(true);
}}
>
{tasksByStatus[status].map((task, index) => (
<Draggable key={task.id} draggableId={task.id} index={index}>
{(provided) => (
<TaskCard
task={task}
provided={provided}
onClick={() => setSelectedTask(task)}
/>
)}
</Draggable>
))}
</TaskColumn>
))}
</div>
</DragDropContext>
{teamTasks.length === 0 && (
<div className="text-center py-16 bg-white rounded-xl border-2 border-dashed border-slate-300">
<div className="w-16 h-16 mx-auto mb-4 bg-slate-100 rounded-xl flex items-center justify-center">
<Plus className="w-8 h-8 text-slate-400" />
</div>
<h3 className="font-bold text-xl text-slate-900 mb-2">No tasks yet</h3>
<p className="text-slate-600 mb-5">Create your first task to get started</p>
<Button onClick={() => setCreateDialog(true)} className="bg-[#0A39DF]">
<Plus className="w-4 h-4 mr-2" />
Create Task
</Button>
</div>
)}
</div>
{/* Create Task Dialog */}
<Dialog open={createDialog} onOpenChange={setCreateDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Create New Task</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div>
<Label>Task Name *</Label>
<Input
value={newTask.task_name}
onChange={(e) => setNewTask({ ...newTask, task_name: e.target.value })}
placeholder="e.g., Website Design"
className="mt-1"
/>
</div>
<div>
<Label>Description</Label>
<Textarea
value={newTask.description}
onChange={(e) => setNewTask({ ...newTask, description: e.target.value })}
placeholder="Task details..."
rows={3}
className="mt-1"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Priority</Label>
<Select value={newTask.priority} onValueChange={(val) => setNewTask({ ...newTask, priority: val })}>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="normal">Normal</SelectItem>
<SelectItem value="high">High</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Due Date</Label>
<Input
type="date"
value={newTask.due_date}
onChange={(e) => setNewTask({ ...newTask, due_date: e.target.value })}
className="mt-1"
/>
</div>
</div>
<div>
<Label>Initial Progress (%)</Label>
<Input
type="number"
min="0"
max="100"
value={newTask.progress}
onChange={(e) => setNewTask({ ...newTask, progress: parseInt(e.target.value) || 0 })}
className="mt-1"
/>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<Label>Assign Team Members</Label>
{departments.length > 0 && (
<Select onValueChange={(dept) => {
const deptMembers = currentTeamMembers.filter(m => m.department === dept);
setSelectedMembers(deptMembers);
}}>
<SelectTrigger className="w-56">
<SelectValue placeholder="Assign entire department" />
</SelectTrigger>
<SelectContent>
{departments.map((dept) => {
const count = currentTeamMembers.filter(m => m.department === dept).length;
return (
<SelectItem key={dept} value={dept}>
<div className="flex items-center gap-2">
<Users className="w-4 h-4" />
{dept} ({count} members)
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
)}
</div>
<div className="mt-2 space-y-2">
{currentTeamMembers.length === 0 ? (
<p className="text-sm text-slate-500">No team members available</p>
) : (
<div className="max-h-48 overflow-y-auto border border-slate-200 rounded-lg p-2 space-y-1">
{currentTeamMembers.map((member) => {
const isSelected = selectedMembers.some(m => m.id === member.id);
return (
<div
key={member.id}
onClick={() => {
if (isSelected) {
setSelectedMembers(selectedMembers.filter(m => m.id !== member.id));
} else {
setSelectedMembers([...selectedMembers, member]);
}
}}
className={`flex items-center gap-3 p-2 rounded-lg cursor-pointer transition-all ${
isSelected ? 'bg-blue-50 border-2 border-[#0A39DF]' : 'hover:bg-slate-50 border-2 border-transparent'
}`}
>
<input
type="checkbox"
checked={isSelected}
onChange={() => {}}
className="w-4 h-4 rounded text-[#0A39DF] focus:ring-[#0A39DF]"
/>
<Avatar className="w-8 h-8">
<img
src={member.avatar_url || `https://i.pravatar.cc/150?u=${encodeURIComponent(member.member_name)}`}
alt={member.member_name}
className="w-full h-full object-cover"
/>
</Avatar>
<div className="flex-1">
<p className="text-sm font-medium text-slate-900">{member.member_name}</p>
<p className="text-xs text-slate-500">
{member.department ? `${member.department}` : ''}{member.role || 'Member'}
</p>
</div>
</div>
);
})}
</div>
)}
{selectedMembers.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2 p-2 bg-slate-50 rounded-lg">
{selectedMembers.map((member) => (
<Badge key={member.id} className="bg-[#0A39DF] text-white flex items-center gap-1">
{member.member_name}
<button
onClick={(e) => {
e.stopPropagation();
setSelectedMembers(selectedMembers.filter(m => m.id !== member.id));
}}
className="ml-1 hover:bg-white/20 rounded-full p-0.5"
>
×
</button>
</Badge>
))}
</div>
)}
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setCreateDialog(false)}>
Cancel
</Button>
<Button
onClick={handleCreateTask}
disabled={createTaskMutation.isPending}
className="bg-[#0A39DF]"
>
Create Task
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Task Detail Modal with Comments */}
<TaskDetailModal
task={selectedTask}
open={!!selectedTask}
onClose={() => setSelectedTask(null)}
/>
</div>
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -120,6 +120,16 @@ import VendorDocumentReview from "./VendorDocumentReview";
import VendorMarketplace from "./VendorMarketplace";
import RapidOrder from "./RapidOrder";
import SmartScheduler from "./SmartScheduler";
import StaffOnboarding from "./StaffOnboarding";
import NotificationSettings from "./NotificationSettings";
import TaskBoard from "./TaskBoard";
import { BrowserRouter as Router, Route, Routes, useLocation } from 'react-router-dom';
const PAGES = {
@@ -244,6 +254,16 @@ const PAGES = {
VendorMarketplace: VendorMarketplace,
RapidOrder: RapidOrder,
SmartScheduler: SmartScheduler,
StaffOnboarding: StaffOnboarding,
NotificationSettings: NotificationSettings,
TaskBoard: TaskBoard,
}
function _getCurrentPage(url) {
@@ -391,6 +411,16 @@ function PagesContent() {
<Route path="/VendorMarketplace" element={<VendorMarketplace />} />
<Route path="/RapidOrder" element={<RapidOrder />} />
<Route path="/SmartScheduler" element={<SmartScheduler />} />
<Route path="/StaffOnboarding" element={<StaffOnboarding />} />
<Route path="/NotificationSettings" element={<NotificationSettings />} />
<Route path="/TaskBoard" element={<TaskBoard />} />
</Routes>
</Layout>
);