export base44 - Nov 18
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
271
frontend-web/src/pages/NotificationSettings.jsx
Normal file
271
frontend-web/src/pages/NotificationSettings.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
505
frontend-web/src/pages/RapidOrder.jsx
Normal file
505
frontend-web/src/pages/RapidOrder.jsx
Normal 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
36
frontend-web/src/pages/SmartScheduler.jsx
Normal file
36
frontend-web/src/pages/SmartScheduler.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
197
frontend-web/src/pages/StaffOnboarding.jsx
Normal file
197
frontend-web/src/pages/StaffOnboarding.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
466
frontend-web/src/pages/TaskBoard.jsx
Normal file
466
frontend-web/src/pages/TaskBoard.jsx
Normal 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
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user