diff --git a/frontend-web/package-lock.json b/frontend-web/package-lock.json index 03a306e4..ededb861 100644 --- a/frontend-web/package-lock.json +++ b/frontend-web/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@base44/sdk": "^0.1.2", "@dataconnect/generated": "file:src/dataconnect-generated", + "@hello-pangea/dnd": "^18.0.1", "@hookform/resolvers": "^4.1.2", "@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-alert-dialog": "^1.1.6", @@ -1740,6 +1741,23 @@ "node": ">=6" } }, + "node_modules/@hello-pangea/dnd": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz", + "integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.26.7", + "css-box-model": "^1.2.1", + "raf-schd": "^4.0.3", + "react-redux": "^9.2.0", + "redux": "^5.0.1" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/@hookform/resolvers": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz", @@ -4491,6 +4509,12 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -5274,6 +5298,15 @@ "node": ">= 8" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "license": "MIT", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -8375,6 +8408,12 @@ ], "license": "MIT" }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", + "license": "MIT" + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -8436,6 +8475,29 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -8653,6 +8715,12 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", diff --git a/frontend-web/package.json b/frontend-web/package.json index 9b5067ef..e6f98a78 100644 --- a/frontend-web/package.json +++ b/frontend-web/package.json @@ -12,6 +12,7 @@ "dependencies": { "@base44/sdk": "^0.1.2", "@dataconnect/generated": "file:src/dataconnect-generated", + "@hello-pangea/dnd": "^18.0.1", "@hookform/resolvers": "^4.1.2", "@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-alert-dialog": "^1.1.6", diff --git a/frontend-web/src/api/entities.js b/frontend-web/src/api/entities.js index 797a7e85..26cbda33 100644 --- a/frontend-web/src/api/entities.js +++ b/frontend-web/src/api/entities.js @@ -63,6 +63,10 @@ export const TeamMemberInvite = base44.entities.TeamMemberInvite; export const VendorDocumentReview = base44.entities.VendorDocumentReview; +export const Task = base44.entities.Task; + +export const TaskComment = base44.entities.TaskComment; + // auth sdk: diff --git a/frontend-web/src/components/dashboard/DashboardCustomizer.jsx b/frontend-web/src/components/dashboard/DashboardCustomizer.jsx new file mode 100644 index 00000000..fc0cc12c --- /dev/null +++ b/frontend-web/src/components/dashboard/DashboardCustomizer.jsx @@ -0,0 +1,396 @@ +import React, { useState, useEffect } from "react"; +import { base44 } from "@/api/base44Client"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Settings, + GripVertical, + X, + Plus, + Eye, + EyeOff, + Info, + Save, + RotateCcw, + Sparkles +} from "lucide-react"; +import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"; +import { useToast } from "@/components/ui/use-toast"; +import { motion, AnimatePresence } from "framer-motion"; + +export default function DashboardCustomizer({ + user, + availableWidgets = [], + currentLayout = [], + onLayoutChange, + dashboardType = "default" // admin, client, vendor, operator, etc +}) { + const [isOpen, setIsOpen] = useState(false); + const [showHowItWorks, setShowHowItWorks] = useState(false); + const [visibleWidgets, setVisibleWidgets] = useState([]); + const [hiddenWidgets, setHiddenWidgets] = useState([]); + const [hasChanges, setHasChanges] = useState(false); + const { toast } = useToast(); + const queryClient = useQueryClient(); + + // Initialize widgets from user's saved layout or defaults + useEffect(() => { + const layoutKey = `dashboard_layout_${dashboardType}`; + const savedLayout = user?.[layoutKey]; + + if (savedLayout?.widgets && savedLayout.widgets.length > 0) { + const savedVisible = savedLayout.widgets + .map(id => availableWidgets.find(w => w.id === id)) + .filter(Boolean); + setVisibleWidgets(savedVisible); + + const savedHidden = savedLayout.hidden_widgets || []; + const hiddenWidgetsList = availableWidgets.filter(w => + savedHidden.includes(w.id) + ); + setHiddenWidgets(hiddenWidgetsList); + } else { + // Default: all widgets visible in provided order + setVisibleWidgets(availableWidgets); + setHiddenWidgets([]); + } + }, [user, availableWidgets, isOpen, dashboardType]); + + // Save layout mutation + const saveLayoutMutation = useMutation({ + mutationFn: async (layoutData) => { + const layoutKey = `dashboard_layout_${dashboardType}`; + await base44.auth.updateMe({ + [layoutKey]: layoutData + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['current-user'] }); + queryClient.invalidateQueries({ queryKey: ['current-user-layout'] }); + queryClient.invalidateQueries({ queryKey: ['current-user-client'] }); + queryClient.invalidateQueries({ queryKey: ['current-user-vendor'] }); + queryClient.invalidateQueries({ queryKey: ['current-user-operator'] }); + toast({ + title: "✅ Layout Saved", + description: "Your dashboard layout has been updated", + }); + setHasChanges(false); + if (onLayoutChange) { + onLayoutChange(visibleWidgets); + } + setTimeout(() => { + setIsOpen(false); + }, 500); + }, + onError: () => { + toast({ + title: "❌ Save Failed", + description: "Could not save your layout. Please try again.", + variant: "destructive", + }); + } + }); + + const handleDragEnd = (result) => { + if (!result.destination) return; + + const { source, destination } = result; + + if (source.droppableId === "visible" && destination.droppableId === "visible") { + const items = Array.from(visibleWidgets); + const [reorderedItem] = items.splice(source.index, 1); + items.splice(destination.index, 0, reorderedItem); + setVisibleWidgets(items); + setHasChanges(true); + } + }; + + const handleHideWidget = (widget) => { + setVisibleWidgets(visibleWidgets.filter(w => w.id !== widget.id)); + setHiddenWidgets([...hiddenWidgets, widget]); + setHasChanges(true); + }; + + const handleShowWidget = (widget) => { + setHiddenWidgets(hiddenWidgets.filter(w => w.id !== widget.id)); + setVisibleWidgets([...visibleWidgets, widget]); + setHasChanges(true); + }; + + const handleSave = () => { + const layoutData = { + widgets: visibleWidgets.map(w => w.id), + hidden_widgets: hiddenWidgets.map(w => w.id), + layout_version: "2.0" + }; + saveLayoutMutation.mutate(layoutData); + }; + + const handleReset = () => { + setVisibleWidgets(availableWidgets); + setHiddenWidgets([]); + setHasChanges(true); + }; + + const handleOpenCustomizer = () => { + setIsOpen(true); + setShowHowItWorks(true); + setHasChanges(false); + }; + + const handleClose = () => { + if (hasChanges) { + if (window.confirm("You have unsaved changes. Are you sure you want to close?")) { + setIsOpen(false); + setHasChanges(false); + } + } else { + setIsOpen(false); + } + }; + + return ( + <> + {/* Customize Button */} + + + {/* Customizer Dialog */} + + > + ); +} \ No newline at end of file diff --git a/frontend-web/src/components/events/AssignedStaffManager.jsx b/frontend-web/src/components/events/AssignedStaffManager.jsx new file mode 100644 index 00000000..384e3bfa --- /dev/null +++ b/frontend-web/src/components/events/AssignedStaffManager.jsx @@ -0,0 +1,235 @@ +import React, { useState } from "react"; +import { base44 } from "@/api/base44Client"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { useToast } from "@/components/ui/use-toast"; +import { Edit2, Trash2, ArrowLeftRight, Clock, MapPin, Check, X } from "lucide-react"; +import { format } from "date-fns"; + +export default function AssignedStaffManager({ event, shift, role }) { + const { toast } = useToast(); + const queryClient = useQueryClient(); + const [editTarget, setEditTarget] = useState(null); + const [swapTarget, setSwapTarget] = useState(null); + const [editTimes, setEditTimes] = useState({ start: "", end: "" }); + + // Get assigned staff for this role + const assignedStaff = (event.assigned_staff || []).filter( + s => s.role === role?.role + ); + + // Remove staff mutation + const removeMutation = useMutation({ + mutationFn: async (staffId) => { + const updatedAssignedStaff = (event.assigned_staff || []).filter( + s => s.staff_id !== staffId || s.role !== role.role + ); + + const updatedShifts = (event.shifts || []).map(s => { + if (s.shift_name === shift.shift_name) { + const updatedRoles = (s.roles || []).map(r => { + if (r.role === role.role) { + return { + ...r, + assigned: Math.max((r.assigned || 0) - 1, 0), + }; + } + return r; + }); + return { ...s, roles: updatedRoles }; + } + return s; + }); + + await base44.entities.Event.update(event.id, { + assigned_staff: updatedAssignedStaff, + shifts: updatedShifts, + requested: Math.max((event.requested || 0) - 1, 0), + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['events'] }); + toast({ + title: "✅ Staff Removed", + description: "Staff member has been removed from this assignment", + }); + }, + }); + + // Edit times mutation + const editMutation = useMutation({ + mutationFn: async () => { + const updatedShifts = (event.shifts || []).map(s => { + if (s.shift_name === shift.shift_name) { + const updatedRoles = (s.roles || []).map(r => { + if (r.role === role.role) { + return { + ...r, + start_time: editTimes.start, + end_time: editTimes.end, + }; + } + return r; + }); + return { ...s, roles: updatedRoles }; + } + return s; + }); + + await base44.entities.Event.update(event.id, { + shifts: updatedShifts, + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['events'] }); + toast({ + title: "✅ Times Updated", + description: "Assignment times have been updated", + }); + setEditTarget(null); + }, + }); + + const handleEdit = (staff) => { + setEditTarget(staff); + setEditTimes({ + start: role.start_time || "09:00", + end: role.end_time || "17:00", + }); + }; + + const handleSaveEdit = () => { + editMutation.mutate(); + }; + + const handleRemove = (staffId) => { + if (confirm("Are you sure you want to remove this staff member?")) { + removeMutation.mutate(staffId); + } + }; + + if (!assignedStaff.length) { + return ( +
{staff.staff_name}
++ ✓ Rates will be automatically applied from {formData.vendor_name} +
+ )} +Managers:
-{staff.staff_name}
-{staff.position || "john@email.com"}
-Location:
-{shift.location || "848 East Glen Road New York CA, USA"}
++ Assigned Staff +
+Uniform
+{role.uniform}
+Rate
+${role.cost_per_hour}/hr
++ {isRapid ? 'RAPID ORDER ROUTING' : 'Order Routing'} +
++ {routingMode === 'multi' + ? `Sending to ${selectedVendors.length} vendors` + : 'Default vendor routing'} +
++ No vendor selected. Please choose a vendor. +
++ {vendor.doing_business_as || vendor.legal_name} +
+ {isPreferred && ( ++ Multi-Vendor Mode: Order sent to all selected vendors. + First to confirm gets the job. +
++ RAPID Priority: This order will be marked urgent with priority notification. +
+Review your information before completing onboarding
+Name
+{profile.full_name}
+{profile.email}
+Position
+{profile.position}
+Location
+{profile.city}
+No documents uploaded
+ )} ++ {training.completed.length} training modules completed +
+ {training.acknowledged && ( +Upload required documents for compliance
+{doc.description}
+ + {uploadedDoc && ( +Complete required training modules to ensure readiness
+{module.duration} · {module.description}
+Emergency staffing in minutes
+Just describe what you need, I'll handle the rest
++ {msg.content} +
+ + {msg.showConfirm && detectedOrder && ( +Staff Needed
+{detectedOrder.count} {detectedOrder.role}{detectedOrder.count > 1 ? 's' : ''}
+Location
+{detectedOrder.location}
+Time
+{detectedOrder.start_time} → {detectedOrder.end_time}
+{assignment.role}
+Date
++ {event.date ? format(new Date(event.date), "MMM d, yyyy") : "TBD"} +
+Time
++ {assignment.scheduled_start ? format(new Date(assignment.scheduled_start), "h:mm a") : "ASAP"} +
+Location
+{event.event_location || event.hub}
+Attire: {event.shifts[0].uniform_type}
+ )} + {event.addons?.meal_provided && ( +Meal: Provided
+ )} + {event.notes && ( +Notes: {event.notes}
+ )} +Track client engagement and satisfaction metrics
+Total Clients
+{totalClients}
+Avg Satisfaction
+{avgSatisfaction}/5
+Repeat Rate
+{repeatRate}%
+{client.name}
+{client.bookings} bookings
+Create custom reports with selected fields and filters
+{reportConfig.name}
+Track process improvements and automation effectiveness
+Automation Rate
+{automationRate}%
+Avg Time to Fill
+{avgTimeToFill}h
+Response Time
+{avgResponseTime}h
+Completed
+{events.filter(e => e.status === 'Completed').length}
+Manual Work Reduction
+85%
+First-Time Fill Rate
+92%
+Staff Utilization
+88%
+Conflict Detection
+97%
+Reliability, fill rates, and performance tracking
+Avg Reliability
+{avgReliability.toFixed(1)}%
+Avg Fill Rate
+{avgFillRate.toFixed(1)}%
+Total Cancellations
+{totalCancellations}
+Track spending and budget compliance
+Total Spent
+${totalSpent.toLocaleString(undefined, { maximumFractionDigits: 0 })}
+Budget
+${totalBudget.toLocaleString(undefined, { maximumFractionDigits: 0 })}
+Budget Adherence
+{adherence}%
++ Buffer required: {conflict.buffer_required} +
+ )} +{localStaff.length} unassigned
+{s.employee_name}
+{s.position}
+All staff assigned
+ )} +{s.staff_name}
+{s.role}
++ Drag staff here to assign +
+ )} +{staff.position}
+ {staff.position_2 && ( +Also: {staff.position_2}
+ )} +Rating
+{staff.rating || 4.5} ★
+Reliability
++ {staff.reliability_score || 90}% +
+Certifications
+On-Time
++ {staff.shift_coverage_percentage || 95}% +
+No-Shows
++ {staff.no_show_count || 0} +
+Cancels
++ {staff.cancellation_count || 0} +
+Background check pending
++ Pick your go-to vendor for faster ordering and consistent service +
+ +Preferred Vendor
++ {preferredVendor.doing_business_as || preferredVendor.legal_name} +
+All orders route here by default
++ {preferredVendor.workforce_count || 0} +
+Staff Available
+4.9
+Rating
+98%
+Fill Rate
++ Priority Support: Faster response times and dedicated account management +
+Your staffing operations at a glance
+Create New
+Order Now
+ORDER TYPE
+ {rapidOrders.length > 0 ? ( +RAPID
+Click to order
- {format(new Date(), 'EEEE, MMMM d, yyyy')} -
-Today's Orders
+{todayOrders.length}
+In Progress
+{inProgressOrders.length}
+Needs Attention
+{needsAttention.length}
+This month breakdown
-| Position | -Headcount | -Hours | -Total Cost | -Avg/Hour | -
|---|---|---|---|---|
|
-
-
- {item.position}
-
- |
-
- |
- - - {Math.round(item.hours)}h - - | -- - ${Math.round(item.cost).toLocaleString()} - - | -- - ${Math.round(item.cost / item.hours)}/hr - - | -
| - No labor data for this month - | -||||
| TOTAL | -{totalHeadcount} | -{Math.round(totalHours)}h | -- ${Math.round(totalLaborCost).toLocaleString()} - | -- ${Math.round(avgCostPerHour)}/hr - | -
This Month
-- ${Math.round(totalLaborCost / 1000)}k -
-Avg Cost per Hour
-- ${Math.round(avgCostPerHour)} -
-- Across {totalHours.toFixed(0)} hours -
-Total Staff Hired
-- {totalHeadcount} -
-- This month -
-+ {format(new Date(), 'EEEE, MMMM d, yyyy')} +
| BUSINESS | +HUB | +EVENT NAME | +STATUS | +DATE | +TIME | +REQUESTED | +ASSIGNED | +INVOICE | +ACTIONS | +
|---|---|---|---|---|---|---|---|---|---|
| + + {order.business_name || "Primary Location"} + + | ++ + {order.hub || order.event_location || "Main Hub"} + + | ++ + {order.event_name} + + | ++ {getOrderStatusBadge(order)} + | ++ + {order.date ? format(new Date(order.date), 'MM/dd/yy') : "—"} + + | +
+
+
+ |
+ + + {requestedCount} + + | ++ + {assignedCount} + + | ++ — + | +
+
+
+
+
+
+ |
+
One tap to reorder
+- {event.hub || 'No location'} +
+ Stay ahead of your operations — create a new order or schedule one for today to keep your workforce running smoothly +
+ + {/* Action Buttons */} +Smart Insights
+ ++ {pendingInvoices.length} invoice{pendingInvoices.length !== 1 ? 's' : ''} requiring attention
No previous orders
-Your favorites will appear here
-Next 7 days
+ )} + + {/* Order Health Report */} ++ Your fulfillment performance this month +
+No upcoming orders
-Find Vendors
-Browse marketplace
-Messages
-Chat with vendors
-Detailed breakdown for November 2025
Total Amount
++ ${totalLaborCost.toFixed(2)} +
+Total Hours
++ {totalHours.toFixed(2)} hours +
+| Job Title | +Reg Hrs | +OT Hrs | +DT Hrs | +Reg Amt | +OT Amt | +DT Amt | +Total Pay | +
|---|---|---|---|---|---|---|---|
| {item.position} | +{item.regHours.toFixed(1)} | +{item.otHours.toFixed(1)} | +{item.dtHours.toFixed(1)} | +${item.regAmt.toFixed(0)} | +${item.otAmt.toFixed(0)} | +${item.dtAmt.toFixed(0)} | +${item.totalPay.toFixed(0)} | +
| + No labor data for this month + | +|||||||
Find Vendors
+Browse marketplace
+View and manage your event orders
+View and manage all your orders
+TOTAL
+{clientEvents.length}
+ACTIVE
+{activeOrders}
+COMPLETED
+{completedOrders}
+TOTAL SPENT
+${Math.round(totalSpent / 1000)}k
+Total Orders
-{stats.total}
-- {useAI ? "Use AI to create your order naturally" : "Fill out the form to create your order"} + Fill out the details for your planned event
- The form has been pre-filled with information from your conversation. Review and edit as needed. -
++ 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. +
+Orders scheduled for today only
+No orders scheduled for today
+Event not found
+ + + +