diff --git a/apps/web/src/features/dashboard/AdminDashboard.tsx b/apps/web/src/features/dashboard/AdminDashboard.tsx index 4f61639c..7f8263bf 100644 --- a/apps/web/src/features/dashboard/AdminDashboard.tsx +++ b/apps/web/src/features/dashboard/AdminDashboard.tsx @@ -1,8 +1,336 @@ +import { + useListShifts, + useListApplications, + useListInvoices, + useListStaff, + useListStaffDocumentsByStatus +} from '@/dataconnect-generated/react'; +import { useNavigate } from 'react-router-dom'; +import { useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/common/components/ui/card'; +import { Button } from '@/common/components/ui/button'; +import { Alert, AlertDescription, AlertTitle } from '@/common/components/ui/alert'; +import { + Clock, + FileText, + TrendingUp, + Users, + AlertTriangle, + PlusCircle, + UserPlus +} from 'lucide-react'; +import { format } from 'date-fns'; +import { DocumentStatus } from '@/dataconnect-generated'; +import CreateOrderDialog from '@/features/operations/orders/components/CreateOrderDialog'; +import { motion, type Variants } from 'framer-motion'; + +// Animation variants +const containerVariants: Variants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + delayChildren: 0.2 + } + } +}; + +const itemVariants: Variants = { + hidden: { y: 20, opacity: 0 }, + visible: { + y: 0, + opacity: 1, + transition: { + type: "spring", + stiffness: 100, + damping: 12 + } + } +}; + +const headerVariants: Variants = { + hidden: { y: -20, opacity: 0 }, + visible: { + y: 0, + opacity: 1, + transition: { + type: "spring", + stiffness: 120, + damping: 14 + } + } +}; + +const alertVariants: Variants = { + hidden: { scale: 0.95, opacity: 0 }, + visible: { + scale: 1, + opacity: 1, + transition: { + type: "spring", + stiffness: 100, + damping: 15 + } + } +}; const AdminDashboard = () => { - return ( -
Admin Dashboard
- ) -} + const navigate = useNavigate(); + const [isOrderDialogOpen, setIsOrderDialogOpen] = useState(false); -export default AdminDashboard \ No newline at end of file + // Data fetching + const { data: shiftsData } = useListShifts(); + const { data: applicationsData } = useListApplications(); + const { data: invoicesData } = useListInvoices(); + const { data: staffData } = useListStaff(); + const { data: complianceData } = useListStaffDocumentsByStatus({ status: DocumentStatus.EXPIRING }); + + // Metrics calculation + const today = new Date(); + const todayStart = new Date(today.setHours(0, 0, 0, 0)); + const todayEnd = new Date(today.setHours(23, 59, 59, 999)); + + const activeShiftsToday = shiftsData?.shifts.filter(shift => { + if (!shift.startTime) return false; + const shiftDate = new Date(shift.startTime); + return shiftDate >= todayStart && shiftDate <= todayEnd; + }).length || 0; + + const pendingApplications = applicationsData?.applications.filter(app => app.status === 'PENDING').length || 0; + // Assuming timesheets are also pending approvals if they exist in a similar way + const pendingApprovals = pendingApplications; // Extend if timesheet hook found + + const monthlyRevenue = invoicesData?.invoices + .filter(inv => { + if (!inv.issueDate) return false; + const invDate = new Date(inv.issueDate); + return invDate.getMonth() === new Date().getMonth() && + invDate.getFullYear() === new Date().getFullYear(); + }) + .reduce((sum, inv) => sum + (inv.amount || 0), 0) || 0; + + const totalStaff = staffData?.staffs.length || 0; + const staffUtilization = totalStaff > 0 ? + Math.round((activeShiftsToday / totalStaff) * 100) : 0; + + const unfilledPositions = shiftsData?.shifts.filter(shift => + shift.status === 'OPEN' || (shift.workersNeeded || 0) > (shift.filled || 0) + ).length || 0; + const complianceIssues = complianceData?.staffDocuments.length || 0; + + const metrics = [ + { + title: "Today's Active Shifts", + value: activeShiftsToday, + description: "Running shifts for today", + icon: Clock, + color: "text-blue-500" + }, + { + title: "Pending Approvals", + value: pendingApprovals, + description: "Applications & Timesheets", + icon: FileText, + color: "text-amber-500" + }, + { + title: "Monthly Revenue", + value: `$${monthlyRevenue.toLocaleString()}`, + description: `For ${format(today, 'MMMM yyyy')}`, + icon: TrendingUp, + color: "text-green-500" + }, + { + title: "Staff Utilization", + value: `${staffUtilization}%`, + description: null, + icon: Users, + color: "text-purple-500", + showProgress: true, + progress: staffUtilization + } + ]; + + return ( +
+ +

Admin Dashboard

+
+ + + + + + +
+
+ + + + {/* Metrics Widgets */} + + {metrics.map((metric, index) => { + const Icon = metric.icon; + return ( + + + + + {metric.title} + + + + + + + {metric.value} + + {metric.description && ( +

+ {metric.description} +

+ )} + {metric.showProgress && ( +
+ +
+ )} +
+
+
+ ); + })} +
+ + {/* Alerts Section */} + {(unfilledPositions > 0 || complianceIssues > 0) && ( + + + + + + Critical Alerts + +
+ {unfilledPositions > 0 && ( + + + + Unfilled Positions + + There are {unfilledPositions} shifts that currently have no staff assigned. + + + + )} + {complianceIssues > 0 && ( + + + + Compliance Issues + + {complianceIssues} staff members have expired or missing documentation. + + + + )} +
+
+ )} +
+ ); +}; + +export default AdminDashboard; \ No newline at end of file diff --git a/apps/web/src/features/operations/orders/components/CreateOrderDialog.tsx b/apps/web/src/features/operations/orders/components/CreateOrderDialog.tsx new file mode 100644 index 00000000..718e6e82 --- /dev/null +++ b/apps/web/src/features/operations/orders/components/CreateOrderDialog.tsx @@ -0,0 +1,132 @@ +import React from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/common/components/ui/dialog"; +import EventFormWizard from "./EventFormWizard"; +import { useCreateOrder, useListBusinesses, useListHubs } from "@/dataconnect-generated/react"; +import { OrderType, OrderStatus } from "@/dataconnect-generated"; +import { dataConnect } from "@/features/auth/firebase"; +import { useToast } from "@/common/components/ui/use-toast"; +import { useQueryClient } from "@tanstack/react-query"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/common/components/ui/select"; +import { Label } from "@/common/components/ui/label"; + +interface CreateOrderDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export default function CreateOrderDialog({ open, onOpenChange }: CreateOrderDialogProps) { + const { toast } = useToast(); + const queryClient = useQueryClient(); + const [selectedBusinessId, setSelectedBusinessId] = React.useState(""); + const [selectedHubId, setSelectedHubId] = React.useState(""); + + const { data: businessesData } = useListBusinesses(dataConnect); + const { data: hubsData } = useListHubs(dataConnect); + + const createOrderMutation = useCreateOrder(dataConnect, { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["listOrders"] }); + toast({ + title: "✅ Order Created", + description: "Your new order has been created successfully.", + }); + onOpenChange(false); + }, + onError: (error) => { + toast({ + title: "❌ Creation Failed", + description: error.message || "Could not create the order. Please try again.", + variant: "destructive", + }); + }, + }); + + const handleSubmit = (eventData: Record) => { + if (!selectedBusinessId || !selectedHubId) { + toast({ + title: "Missing Information", + description: "Please select a business and a hub.", + variant: "destructive", + }); + return; + } + + // Recalculate requested count from current roles + const totalRequested = eventData.shifts.reduce((sum: number, shift: any) => { + const roles = Array.isArray(shift.roles) ? shift.roles : []; + return sum + roles.reduce((roleSum: number, role: any) => roleSum + (parseInt(role.count) || 0), 0); + }, 0); + + createOrderMutation.mutate({ + eventName: eventData.event_name, + businessId: selectedBusinessId, + teamHubId: selectedHubId, + orderType: OrderType.RAPID, // Defaulting to RAPID as per common use in this app + date: eventData.date, + startDate: eventData.startDate || eventData.date, + endDate: eventData.endDate, + notes: eventData.notes, + shifts: eventData.shifts, + requested: totalRequested, + total: eventData.total, + poReference: eventData.po_reference, + status: OrderStatus.POSTED + }); + }; + + return ( + + + + Create New Order + + +
+
+ + +
+
+ + +
+
+ + onOpenChange(false)} + /> +
+
+ ); +}