-
-
-
setFormData({ ...formData, first_name: e.target.value })}
- placeholder="John"
- className="mt-2"
- />
+
);
diff --git a/frontend-web/src/pages/Permissions.jsx b/frontend-web/src/pages/Permissions.jsx
index f6fb733e..ffafcb80 100644
--- a/frontend-web/src/pages/Permissions.jsx
+++ b/frontend-web/src/pages/Permissions.jsx
@@ -1,12 +1,18 @@
import React, { useState } from "react";
import { base44 } from "@/api/base44Client";
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+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 { Input } from "@/components/ui/input";
-import { Checkbox } from "@/components/ui/checkbox";
-import { Shield, Search, Save, Info, ChevronDown, ChevronRight, Users, Calendar, Package, DollarSign, FileText, Settings as SettingsIcon, BarChart3, MessageSquare, Briefcase, Building2 } from "lucide-react";
+import { Switch } from "@/components/ui/switch";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import {
+ Shield, Search, Save, Info, ChevronDown, ChevronRight, Users, Calendar,
+ Package, DollarSign, FileText, Settings as SettingsIcon, BarChart3,
+ MessageSquare, Briefcase, Building2, Layers, Lock, Unlock, Eye,
+ CheckCircle2, XCircle, AlertTriangle, Sparkles, UserCog, Plus, Trash2, Copy
+} from "lucide-react";
import PageHeader from "@/components/common/PageHeader";
import { useToast } from "@/components/ui/use-toast";
import {
@@ -14,57 +20,143 @@ import {
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogFooter,
+} from "@/components/ui/dialog";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
-// Role-specific permission sets
-const ROLE_PERMISSIONS = {
+// Layer hierarchy configuration
+const LAYER_HIERARCHY = [
+ {
+ id: "admin",
+ name: "KROW Admin",
+ icon: Shield,
+ color: "from-red-500 to-red-700",
+ bgColor: "bg-red-50",
+ borderColor: "border-red-200",
+ textColor: "text-red-700",
+ description: "Full platform control and oversight",
+ level: 1
+ },
+ {
+ id: "procurement",
+ name: "Procurement",
+ icon: Briefcase,
+ color: "from-[#0A39DF] to-[#1C323E]",
+ bgColor: "bg-blue-50",
+ borderColor: "border-blue-200",
+ textColor: "text-blue-700",
+ description: "Vendor management and rate control",
+ level: 2
+ },
+ {
+ id: "operator",
+ name: "Operator",
+ icon: Building2,
+ color: "from-emerald-500 to-emerald-700",
+ bgColor: "bg-emerald-50",
+ borderColor: "border-emerald-200",
+ textColor: "text-emerald-700",
+ description: "Enterprise-wide operations",
+ level: 3
+ },
+ {
+ id: "sector",
+ name: "Sector",
+ icon: Layers,
+ color: "from-purple-500 to-purple-700",
+ bgColor: "bg-purple-50",
+ borderColor: "border-purple-200",
+ textColor: "text-purple-700",
+ description: "Location-specific management",
+ level: 4
+ },
+ {
+ id: "client",
+ name: "Client",
+ icon: Users,
+ color: "from-green-500 to-green-700",
+ bgColor: "bg-green-50",
+ borderColor: "border-green-200",
+ textColor: "text-green-700",
+ description: "Service ordering and review",
+ level: 5
+ },
+ {
+ id: "vendor",
+ name: "Vendor",
+ icon: Package,
+ color: "from-amber-500 to-amber-700",
+ bgColor: "bg-amber-50",
+ borderColor: "border-amber-200",
+ textColor: "text-amber-700",
+ description: "Workforce supply and management",
+ level: 6
+ },
+ {
+ id: "workforce",
+ name: "Workforce",
+ icon: Users,
+ color: "from-slate-500 to-slate-700",
+ bgColor: "bg-slate-50",
+ borderColor: "border-slate-200",
+ textColor: "text-slate-700",
+ description: "Shift work and earnings",
+ level: 7
+ }
+];
+
+// Permission modules for each layer
+const PERMISSION_MODULES = {
admin: [
{
- id: "system",
- name: "System Administration",
+ id: "platform",
+ name: "Platform Administration",
icon: Shield,
- color: "text-red-600",
permissions: [
- { id: "admin.1", name: "Manage All Users", description: "Create, edit, and delete user accounts" },
- { id: "admin.2", name: "Configure System Settings", description: "Modify platform-wide settings" },
- { id: "admin.3", name: "Manage Roles & Permissions", description: "Define and assign user roles" },
- { id: "admin.4", name: "View All Activity Logs", description: "Access complete audit trail" },
- { id: "admin.5", name: "Manage Integrations", description: "Configure external integrations" },
- { id: "admin.6", name: "Access All Data", description: "Unrestricted access to all system data" },
+ { id: "platform.users", name: "Manage All Users", description: "Create, edit, delete users across all layers", critical: true },
+ { id: "platform.settings", name: "System Settings", description: "Configure platform-wide settings", critical: true },
+ { id: "platform.roles", name: "Role Management", description: "Define and modify role permissions", critical: true },
+ { id: "platform.audit", name: "Audit Logs", description: "Access complete system audit trail" },
+ { id: "platform.integrations", name: "Integrations", description: "Manage external system connections" },
]
},
{
- id: "enterprises",
- name: "Enterprise Management",
- icon: Building2,
- color: "text-purple-600",
+ id: "hierarchy",
+ name: "Hierarchy Control",
+ icon: Layers,
permissions: [
- { id: "admin.7", name: "Create Enterprises", description: "Onboard new enterprise clients" },
- { id: "admin.8", name: "Manage Sectors", description: "Create and configure sectors" },
- { id: "admin.9", name: "Manage Partners", description: "Onboard and manage partners" },
- { id: "admin.10", name: "Set Global Policies", description: "Define enterprise-wide policies" },
- ]
- },
- {
- id: "vendors",
- name: "Vendor Oversight",
- icon: Package,
- color: "text-amber-600",
- permissions: [
- { id: "admin.11", name: "Approve/Suspend Vendors", description: "Control vendor status" },
- { id: "admin.12", name: "View All Vendor Performance", description: "Access all vendor scorecards" },
- { id: "admin.13", name: "Manage Vendor Rates", description: "Override and approve rate cards" },
+ { id: "hierarchy.enterprises", name: "Enterprise Management", description: "Create and manage enterprises" },
+ { id: "hierarchy.sectors", name: "Sector Management", description: "Create and manage sectors" },
+ { id: "hierarchy.partners", name: "Partner Management", description: "Manage partner relationships" },
+ { id: "hierarchy.policies", name: "Global Policies", description: "Set enterprise-wide policies", critical: true },
]
},
{
id: "financial",
- name: "Financial Administration",
+ name: "Financial Control",
icon: DollarSign,
- color: "text-green-600",
permissions: [
- { id: "admin.14", name: "View All Financials", description: "Access all financial data" },
- { id: "admin.15", name: "Process All Payments", description: "Approve and process payments" },
- { id: "admin.16", name: "Manage Payroll", description: "Process workforce payroll" },
- { id: "admin.17", name: "Generate Financial Reports", description: "Create P&L and financial analytics" },
+ { id: "financial.all", name: "All Financials", description: "Access all financial data", critical: true },
+ { id: "financial.payments", name: "Process Payments", description: "Approve and process all payments" },
+ { id: "financial.payroll", name: "Payroll Management", description: "Process workforce payroll" },
+ { id: "financial.reports", name: "Financial Reports", description: "Generate P&L and analytics" },
]
}
],
@@ -74,49 +166,35 @@ const ROLE_PERMISSIONS = {
id: "vendors",
name: "Vendor Management",
icon: Package,
- color: "text-purple-600",
permissions: [
- { id: "proc.1", name: "View All Vendors", description: "Access vendor directory" },
- { id: "proc.2", name: "Onboard New Vendors", description: "Add vendors to the platform" },
- { id: "proc.3", name: "Edit Vendor Details", description: "Modify vendor information" },
- { id: "proc.4", name: "Review Vendor Compliance", description: "Check COI, W9, certifications" },
- { id: "proc.5", name: "Approve/Suspend Vendors", description: "Change vendor approval status" },
- { id: "proc.6", name: "View Vendor Performance", description: "Access scorecards and KPIs" },
+ { id: "vendors.view", name: "View Vendors", description: "Access vendor directory" },
+ { id: "vendors.onboard", name: "Onboard Vendors", description: "Add new vendors to platform" },
+ { id: "vendors.edit", name: "Edit Vendors", description: "Modify vendor information" },
+ { id: "vendors.compliance", name: "Compliance Review", description: "Review COI, W9, certifications" },
+ { id: "vendors.approve", name: "Approve/Suspend", description: "Change vendor approval status", critical: true },
+ { id: "vendors.performance", name: "Performance Data", description: "Access scorecards and KPIs" },
]
},
{
id: "rates",
- name: "Rate Card Management",
+ name: "Rate Management",
icon: DollarSign,
- color: "text-green-600",
permissions: [
- { id: "proc.7", name: "View All Rate Cards", description: "See vendor pricing" },
- { id: "proc.8", name: "Create Rate Cards", description: "Set up new rate cards" },
- { id: "proc.9", name: "Edit Rate Cards", description: "Modify existing rates" },
- { id: "proc.10", name: "Approve Rate Cards", description: "Approve vendor rates" },
- { id: "proc.11", name: "Set Markup Rules", description: "Define markup percentages" },
+ { id: "rates.view", name: "View Rates", description: "See all vendor pricing" },
+ { id: "rates.create", name: "Create Rate Cards", description: "Set up new rate cards" },
+ { id: "rates.edit", name: "Edit Rates", description: "Modify existing rates" },
+ { id: "rates.approve", name: "Approve Rates", description: "Approve vendor rate submissions", critical: true },
+ { id: "rates.markup", name: "Markup Rules", description: "Define markup percentages" },
]
},
{
id: "orders",
- name: "Order Management",
+ name: "Order Oversight",
icon: Calendar,
- color: "text-blue-600",
permissions: [
- { id: "proc.12", name: "View All Orders", description: "Access all orders across sectors" },
- { id: "proc.13", name: "Assign Vendors to Orders", description: "Match vendors with orders" },
- { id: "proc.14", name: "Monitor Order Fulfillment", description: "Track order completion" },
- ]
- },
- {
- id: "reports",
- name: "Analytics & Reports",
- icon: BarChart3,
- color: "text-indigo-600",
- permissions: [
- { id: "proc.15", name: "View Vendor Analytics", description: "Access vendor performance data" },
- { id: "proc.16", name: "Generate Procurement Reports", description: "Create spend and compliance reports" },
- { id: "proc.17", name: "Export Data", description: "Download reports as CSV/PDF" },
+ { id: "orders.viewAll", name: "View All Orders", description: "Access orders across sectors" },
+ { id: "orders.assign", name: "Assign Vendors", description: "Match vendors with orders" },
+ { id: "orders.monitor", name: "Monitor Fulfillment", description: "Track order completion" },
]
}
],
@@ -126,47 +204,34 @@ const ROLE_PERMISSIONS = {
id: "events",
name: "Event Management",
icon: Calendar,
- color: "text-blue-600",
permissions: [
- { id: "op.1", name: "View My Enterprise Events", description: "See events in my enterprise" },
- { id: "op.2", name: "Create Events", description: "Create new event orders" },
- { id: "op.3", name: "Edit Events", description: "Modify event details" },
- { id: "op.4", name: "Cancel Events", description: "Cancel event orders" },
- { id: "op.5", name: "Approve Events", description: "Approve event requests from sectors" },
- { id: "op.6", name: "View Event Financials", description: "See event costs and billing" },
+ { id: "events.view", name: "View Events", description: "See events in my enterprise" },
+ { id: "events.create", name: "Create Events", description: "Create new event orders" },
+ { id: "events.edit", name: "Edit Events", description: "Modify event details" },
+ { id: "events.cancel", name: "Cancel Events", description: "Cancel event orders" },
+ { id: "events.approve", name: "Approve Events", description: "Approve sector event requests" },
+ { id: "events.financials", name: "Event Financials", description: "View event costs and billing" },
]
},
{
id: "sectors",
- name: "Sector Management",
+ name: "Sector Oversight",
icon: Building2,
- color: "text-cyan-600",
permissions: [
- { id: "op.7", name: "View My Sectors", description: "See sectors under my enterprise" },
- { id: "op.8", name: "Manage Sector Settings", description: "Configure sector policies" },
- { id: "op.9", name: "Assign Vendors to Sectors", description: "Approve vendors for sectors" },
+ { id: "sectors.view", name: "View Sectors", description: "See sectors under my enterprise" },
+ { id: "sectors.settings", name: "Sector Settings", description: "Configure sector policies" },
+ { id: "sectors.vendors", name: "Assign Vendors", description: "Approve vendors for sectors" },
]
},
{
id: "workforce",
name: "Workforce Management",
icon: Users,
- color: "text-emerald-600",
permissions: [
- { id: "op.10", name: "View Workforce", description: "See staff across my enterprise" },
- { id: "op.11", name: "Assign Staff to Events", description: "Schedule staff for events" },
- { id: "op.12", name: "Approve Timesheets", description: "Review and approve hours" },
- { id: "op.13", name: "View Staff Performance", description: "Access ratings and reviews" },
- ]
- },
- {
- id: "reports",
- name: "Reports",
- icon: BarChart3,
- color: "text-indigo-600",
- permissions: [
- { id: "op.14", name: "View Enterprise Dashboards", description: "Access my enterprise analytics" },
- { id: "op.15", name: "Export Reports", description: "Download reports" },
+ { id: "workforce.view", name: "View Workforce", description: "See staff across enterprise" },
+ { id: "workforce.assign", name: "Assign Staff", description: "Schedule staff for events" },
+ { id: "workforce.timesheets", name: "Approve Timesheets", description: "Review and approve hours" },
+ { id: "workforce.performance", name: "Staff Performance", description: "Access ratings and reviews" },
]
}
],
@@ -174,37 +239,34 @@ const ROLE_PERMISSIONS = {
sector: [
{
id: "events",
- name: "Event Management",
+ name: "Location Events",
icon: Calendar,
- color: "text-blue-600",
permissions: [
- { id: "sec.1", name: "View My Sector Events", description: "See events at my location" },
- { id: "sec.2", name: "Create Event Requests", description: "Request new events" },
- { id: "sec.3", name: "Edit My Events", description: "Modify event details" },
- { id: "sec.4", name: "View Event Costs", description: "See event billing information" },
+ { id: "events.viewMy", name: "View My Events", description: "See events at my location" },
+ { id: "events.request", name: "Request Events", description: "Submit new event requests" },
+ { id: "events.editMy", name: "Edit My Events", description: "Modify my event details" },
+ { id: "events.costs", name: "View Costs", description: "See event billing information" },
]
},
{
- id: "workforce",
+ id: "staff",
name: "Staff Management",
icon: Users,
- color: "text-emerald-600",
permissions: [
- { id: "sec.5", name: "View My Location Staff", description: "See staff at my sector" },
- { id: "sec.6", name: "Schedule Staff", description: "Assign staff to shifts" },
- { id: "sec.7", name: "Approve Timesheets", description: "Review hours worked" },
- { id: "sec.8", name: "Rate Staff Performance", description: "Provide performance feedback" },
+ { id: "staff.view", name: "View Staff", description: "See staff at my sector" },
+ { id: "staff.schedule", name: "Schedule Staff", description: "Assign staff to shifts" },
+ { id: "staff.timesheets", name: "Approve Timesheets", description: "Review hours worked" },
+ { id: "staff.rate", name: "Rate Performance", description: "Provide staff feedback" },
]
},
{
id: "vendors",
- name: "Vendor Relations",
+ name: "Vendor Access",
icon: Package,
- color: "text-purple-600",
permissions: [
- { id: "sec.9", name: "View Approved Vendors", description: "See vendors available to my sector" },
- { id: "sec.10", name: "View Vendor Rates", description: "Access rate cards" },
- { id: "sec.11", name: "Request Vendor Services", description: "Submit staffing requests" },
+ { id: "vendors.viewApproved", name: "View Vendors", description: "See approved vendors" },
+ { id: "vendors.rates", name: "View Rates", description: "Access rate cards" },
+ { id: "vendors.request", name: "Request Services", description: "Submit staffing requests" },
]
}
],
@@ -212,48 +274,34 @@ const ROLE_PERMISSIONS = {
client: [
{
id: "orders",
- name: "Order Management",
+ name: "My Orders",
icon: Calendar,
- color: "text-blue-600",
permissions: [
- { id: "client.1", name: "View My Orders", description: "See my event orders" },
- { id: "client.2", name: "Create New Orders", description: "Request staffing for events" },
- { id: "client.3", name: "Edit My Orders", description: "Modify order details before confirmation" },
- { id: "client.4", name: "Cancel Orders", description: "Cancel pending or confirmed orders" },
- { id: "client.5", name: "View Order Status", description: "Track order fulfillment" },
+ { id: "orders.view", name: "View Orders", description: "See my event orders" },
+ { id: "orders.create", name: "Create Orders", description: "Request staffing for events" },
+ { id: "orders.edit", name: "Edit Orders", description: "Modify orders before confirmation" },
+ { id: "orders.cancel", name: "Cancel Orders", description: "Cancel pending orders" },
+ { id: "orders.status", name: "Track Status", description: "Monitor order fulfillment" },
]
},
{
id: "vendors",
name: "Vendor Selection",
icon: Package,
- color: "text-purple-600",
permissions: [
- { id: "client.6", name: "View Available Vendors", description: "See vendors I can work with" },
- { id: "client.7", name: "View Vendor Rates", description: "See pricing for services" },
- { id: "client.8", name: "Request Specific Vendors", description: "Prefer specific vendors for orders" },
- ]
- },
- {
- id: "workforce",
- name: "Staff Review",
- icon: Users,
- color: "text-emerald-600",
- permissions: [
- { id: "client.9", name: "View Assigned Staff", description: "See who's working my events" },
- { id: "client.10", name: "Rate Staff Performance", description: "Provide feedback on staff" },
- { id: "client.11", name: "Request Staff Changes", description: "Request staff replacements" },
+ { id: "vendors.browse", name: "Browse Vendors", description: "View available vendors" },
+ { id: "vendors.rates", name: "View Rates", description: "See service pricing" },
+ { id: "vendors.prefer", name: "Preferred Vendors", description: "Request specific vendors" },
]
},
{
id: "billing",
- name: "Billing & Invoices",
+ name: "Billing",
icon: DollarSign,
- color: "text-green-600",
permissions: [
- { id: "client.12", name: "View My Invoices", description: "Access invoices for my orders" },
- { id: "client.13", name: "Download Invoices", description: "Export invoice PDFs" },
- { id: "client.14", name: "View Spend Analytics", description: "See my spending trends" },
+ { id: "billing.invoices", name: "View Invoices", description: "Access my invoices" },
+ { id: "billing.download", name: "Download Invoices", description: "Export invoice PDFs" },
+ { id: "billing.analytics", name: "Spend Analytics", description: "View spending trends" },
]
}
],
@@ -263,59 +311,43 @@ const ROLE_PERMISSIONS = {
id: "orders",
name: "Order Fulfillment",
icon: Calendar,
- color: "text-blue-600",
permissions: [
- { id: "vendor.1", name: "View My Orders", description: "See orders assigned to me" },
- { id: "vendor.2", name: "Accept/Decline Orders", description: "Respond to order requests" },
- { id: "vendor.3", name: "Update Order Status", description: "Mark orders as in progress/completed" },
- { id: "vendor.4", name: "View Order Details", description: "Access order requirements" },
+ { id: "orders.viewAssigned", name: "View Orders", description: "See assigned orders" },
+ { id: "orders.respond", name: "Accept/Decline", description: "Respond to order requests" },
+ { id: "orders.update", name: "Update Status", description: "Mark order progress" },
+ { id: "orders.details", name: "Order Details", description: "Access requirements" },
]
},
{
id: "workforce",
name: "My Workforce",
icon: Users,
- color: "text-emerald-600",
permissions: [
- { id: "vendor.5", name: "View My Staff", description: "See my workforce members" },
- { id: "vendor.6", name: "Add New Staff", description: "Onboard new workers" },
- { id: "vendor.7", name: "Edit Staff Details", description: "Update staff information" },
- { id: "vendor.8", name: "Assign Staff to Orders", description: "Schedule staff for orders" },
- { id: "vendor.9", name: "Manage Staff Compliance", description: "Track certifications and background checks" },
- { id: "vendor.10", name: "View Staff Performance", description: "See ratings and feedback" },
+ { id: "workforce.view", name: "View Staff", description: "See my workforce" },
+ { id: "workforce.add", name: "Add Staff", description: "Onboard new workers" },
+ { id: "workforce.edit", name: "Edit Staff", description: "Update staff info" },
+ { id: "workforce.assign", name: "Assign Staff", description: "Schedule for orders" },
+ { id: "workforce.compliance", name: "Manage Compliance", description: "Track certifications" },
]
},
{
id: "rates",
- name: "Rate Management",
+ name: "My Rates",
icon: DollarSign,
- color: "text-green-600",
permissions: [
- { id: "vendor.11", name: "View My Rate Cards", description: "See my approved rates" },
- { id: "vendor.12", name: "Submit Rate Proposals", description: "Propose new rates" },
- { id: "vendor.13", name: "View Rate History", description: "Track rate changes" },
+ { id: "rates.viewMy", name: "View Rates", description: "See my approved rates" },
+ { id: "rates.propose", name: "Propose Rates", description: "Submit rate proposals" },
+ { id: "rates.history", name: "Rate History", description: "Track rate changes" },
]
},
{
- id: "performance",
- name: "Performance & Analytics",
- icon: BarChart3,
- color: "text-indigo-600",
- permissions: [
- { id: "vendor.14", name: "View My Scorecard", description: "See my performance metrics" },
- { id: "vendor.15", name: "View Fill Rate", description: "Track order fulfillment rate" },
- { id: "vendor.16", name: "View Revenue Analytics", description: "See my earnings trends" },
- ]
- },
- {
- id: "billing",
- name: "Invoices & Payments",
+ id: "invoices",
+ name: "Invoicing",
icon: FileText,
- color: "text-cyan-600",
permissions: [
- { id: "vendor.17", name: "View My Invoices", description: "Access invoices I've issued" },
- { id: "vendor.18", name: "Create Invoices", description: "Generate invoices for completed work" },
- { id: "vendor.19", name: "Track Payments", description: "Monitor payment status" },
+ { id: "invoices.view", name: "View Invoices", description: "Access my invoices" },
+ { id: "invoices.create", name: "Create Invoices", description: "Generate invoices" },
+ { id: "invoices.track", name: "Track Payments", description: "Monitor payment status" },
]
}
],
@@ -325,103 +357,57 @@ const ROLE_PERMISSIONS = {
id: "shifts",
name: "My Shifts",
icon: Calendar,
- color: "text-blue-600",
permissions: [
- { id: "work.1", name: "View My Schedule", description: "See my upcoming shifts" },
- { id: "work.2", name: "Clock In/Out", description: "Record shift start and end times" },
- { id: "work.3", name: "Request Time Off", description: "Submit time off requests" },
- { id: "work.4", name: "View Shift History", description: "See past shifts worked" },
+ { id: "shifts.view", name: "View Schedule", description: "See upcoming shifts" },
+ { id: "shifts.clock", name: "Clock In/Out", description: "Record shift times" },
+ { id: "shifts.timeoff", name: "Request Time Off", description: "Submit time off requests" },
+ { id: "shifts.history", name: "Shift History", description: "See past shifts" },
]
},
{
id: "profile",
name: "My Profile",
icon: Users,
- color: "text-emerald-600",
permissions: [
- { id: "work.5", name: "View My Profile", description: "See my worker profile" },
- { id: "work.6", name: "Edit Contact Info", description: "Update phone/email" },
- { id: "work.7", name: "Update Availability", description: "Set my available days/times" },
- { id: "work.8", name: "Upload Certifications", description: "Add certificates and licenses" },
+ { id: "profile.view", name: "View Profile", description: "See my worker profile" },
+ { id: "profile.edit", name: "Edit Contact", description: "Update phone/email" },
+ { id: "profile.availability", name: "Update Availability", description: "Set available times" },
+ { id: "profile.certs", name: "Upload Certs", description: "Add certifications" },
]
},
{
id: "earnings",
- name: "Earnings & Payments",
+ name: "Earnings",
icon: DollarSign,
- color: "text-green-600",
permissions: [
- { id: "work.9", name: "View My Earnings", description: "See my pay and hours" },
- { id: "work.10", name: "View Timesheets", description: "Access my timesheet records" },
- { id: "work.11", name: "View Payment History", description: "See past payments" },
- { id: "work.12", name: "Download Pay Stubs", description: "Export payment records" },
- ]
- },
- {
- id: "performance",
- name: "My Performance",
- icon: BarChart3,
- color: "text-indigo-600",
- permissions: [
- { id: "work.13", name: "View My Ratings", description: "See feedback from clients" },
- { id: "work.14", name: "View Performance Stats", description: "See my reliability metrics" },
- { id: "work.15", name: "View Badges/Achievements", description: "See earned badges" },
+ { id: "earnings.view", name: "View Earnings", description: "See pay and hours" },
+ { id: "earnings.timesheets", name: "View Timesheets", description: "Access timesheet records" },
+ { id: "earnings.history", name: "Payment History", description: "See past payments" },
+ { id: "earnings.download", name: "Download Stubs", description: "Export pay stubs" },
]
}
]
};
-const ROLE_TEMPLATES = {
- admin: {
- name: "Administrator",
- description: "Full system access",
- color: "bg-red-100 text-red-700",
- defaultPermissions: "all"
- },
- procurement: {
- name: "Procurement Manager",
- description: "Vendor and rate management",
- color: "bg-purple-100 text-purple-700",
- defaultPermissions: ["proc.1", "proc.2", "proc.3", "proc.4", "proc.5", "proc.6", "proc.7", "proc.8", "proc.9", "proc.10", "proc.12", "proc.13", "proc.15", "proc.16"]
- },
- operator: {
- name: "Operator",
- description: "Enterprise management",
- color: "bg-blue-100 text-blue-700",
- defaultPermissions: ["op.1", "op.2", "op.3", "op.5", "op.6", "op.7", "op.8", "op.10", "op.11", "op.12", "op.14"]
- },
- sector: {
- name: "Sector Manager",
- description: "Location-specific management",
- color: "bg-cyan-100 text-cyan-700",
- defaultPermissions: ["sec.1", "sec.2", "sec.3", "sec.4", "sec.5", "sec.6", "sec.7", "sec.9", "sec.10"]
- },
- client: {
- name: "Client",
- description: "Order creation and viewing",
- color: "bg-green-100 text-green-700",
- defaultPermissions: ["client.1", "client.2", "client.3", "client.5", "client.6", "client.7", "client.9", "client.12"]
- },
- vendor: {
- name: "Vendor Partner",
- description: "Vendor-specific access",
- color: "bg-amber-100 text-amber-700",
- defaultPermissions: ["vendor.1", "vendor.2", "vendor.3", "vendor.5", "vendor.6", "vendor.7", "vendor.8", "vendor.11", "vendor.14", "vendor.17", "vendor.18"]
- },
- workforce: {
- name: "Workforce Member",
- description: "Basic access for staff",
- color: "bg-slate-100 text-slate-700",
- defaultPermissions: ["work.1", "work.2", "work.4", "work.5", "work.6", "work.9", "work.10", "work.13"]
- }
-};
+// Default position templates
+const DEFAULT_POSITION_TEMPLATES = [
+ { id: "manager", position: "Manager", layer: "operator", description: "Full operational access", permissions: ["events.view", "events.create", "events.edit", "events.approve", "sectors.view", "workforce.view", "workforce.assign"] },
+ { id: "supervisor", position: "Supervisor", layer: "sector", description: "Location-level management", permissions: ["events.viewMy", "events.request", "staff.view", "staff.schedule", "staff.timesheets"] },
+ { id: "coordinator", position: "Coordinator", layer: "client", description: "Order coordination", permissions: ["orders.view", "orders.create", "orders.status", "vendors.browse"] },
+ { id: "team_lead", position: "Team Lead", layer: "vendor", description: "Workforce team management", permissions: ["orders.viewAssigned", "workforce.view", "workforce.assign", "rates.viewMy"] },
+ { id: "staff", position: "Staff Member", layer: "workforce", description: "Basic shift access", permissions: ["shifts.view", "shifts.clock", "profile.view", "earnings.view"] },
+];
export default function Permissions() {
- const [selectedRole, setSelectedRole] = useState("operator");
+ const [activeTab, setActiveTab] = useState("layers");
+ const [selectedLayer, setSelectedLayer] = useState("operator");
const [searchTerm, setSearchTerm] = useState("");
- const [expandedCategories, setExpandedCategories] = useState({});
+ const [expandedModules, setExpandedModules] = useState({});
const [permissions, setPermissions] = useState({});
- const [overrides, setOverrides] = useState({});
+ const [positionTemplates, setPositionTemplates] = useState(DEFAULT_POSITION_TEMPLATES);
+ const [showAddPosition, setShowAddPosition] = useState(false);
+ const [newPosition, setNewPosition] = useState({ position: "", description: "" });
+ const [editingPosition, setEditingPosition] = useState(null);
const { toast } = useToast();
const { data: user } = useQuery({
@@ -431,247 +417,561 @@ export default function Permissions() {
const userRole = user?.user_role || user?.role || "admin";
- // Get role-specific permissions
- const roleCategories = ROLE_PERMISSIONS[selectedRole] || [];
+ // Non-admin users can only see their own layer
+ const effectiveLayer = userRole === "admin" ? selectedLayer : userRole;
+ const selectedLayerConfig = LAYER_HIERARCHY.find(l => l.id === effectiveLayer);
+ const modules = PERMISSION_MODULES[effectiveLayer] || [];
- // Initialize permissions based on role template
+ // Filter layer hierarchy for non-admin users
+ const visibleLayers = userRole === "admin" ? LAYER_HIERARCHY : LAYER_HIERARCHY.filter(l => l.id === userRole);
+
+ // Initialize permissions with all enabled for demo
React.useEffect(() => {
- const template = ROLE_TEMPLATES[selectedRole];
- if (template) {
- const newPermissions = {};
- if (template.defaultPermissions === "all") {
- // Admin gets all permissions
- roleCategories.forEach(category => {
- category.permissions.forEach(perm => {
- newPermissions[perm.id] = true;
- });
- });
- } else {
- // Other roles get specific permissions
- template.defaultPermissions.forEach(permId => {
- newPermissions[permId] = true;
- });
- }
- setPermissions(newPermissions);
- setOverrides({});
- }
- }, [selectedRole]);
-
- const toggleCategory = (categoryId) => {
- setExpandedCategories(prev => ({
- ...prev,
- [categoryId]: !prev[categoryId]
- }));
- };
-
- const handlePermissionChange = (permId, value) => {
- setPermissions(prev => ({
- ...prev,
- [permId]: value
- }));
- setOverrides(prev => ({
- ...prev,
- [permId]: true
- }));
- };
-
- const handleInherit = (permId) => {
- const template = ROLE_TEMPLATES[selectedRole];
- const shouldBeEnabled = template.defaultPermissions === "all" || template.defaultPermissions.includes(permId);
- setPermissions(prev => ({
- ...prev,
- [permId]: shouldBeEnabled
- }));
- setOverrides(prev => {
- const newOverrides = { ...prev };
- delete newOverrides[permId];
- return newOverrides;
+ const newPermissions = {};
+ modules.forEach(module => {
+ module.permissions.forEach(perm => {
+ newPermissions[perm.id] = true;
+ });
});
+ setPermissions(newPermissions);
+ // Expand all modules by default
+ const expanded = {};
+ modules.forEach(m => expanded[m.id] = true);
+ setExpandedModules(expanded);
+ }, [selectedLayer]);
+
+ const toggleModule = (moduleId) => {
+ setExpandedModules(prev => ({
+ ...prev,
+ [moduleId]: !prev[moduleId]
+ }));
+ };
+
+ const togglePermission = (permId) => {
+ setPermissions(prev => ({
+ ...prev,
+ [permId]: !prev[permId]
+ }));
+ };
+
+ const toggleAllInModule = (module, value) => {
+ const newPerms = { ...permissions };
+ module.permissions.forEach(p => {
+ newPerms[p.id] = value;
+ });
+ setPermissions(newPerms);
};
const handleSave = () => {
toast({
title: "Permissions Saved",
- description: `Permissions for ${ROLE_TEMPLATES[selectedRole].name} role have been updated successfully.`,
+ description: `${selectedLayerConfig.name} layer permissions updated successfully.`,
});
};
- const filteredCategories = roleCategories.map(category => ({
- ...category,
- permissions: category.permissions.filter(perm =>
+ const handleAddPosition = () => {
+ if (!newPosition.position) return;
+ const id = newPosition.position.toLowerCase().replace(/\s+/g, '_');
+ setPositionTemplates([...positionTemplates, { ...newPosition, id, layer: effectiveLayer, permissions: [] }]);
+ setNewPosition({ position: "", description: "" });
+ setShowAddPosition(false);
+ toast({ title: "Position Added", description: `${newPosition.position} template created.` });
+ };
+
+ const handleDeletePosition = (id) => {
+ setPositionTemplates(positionTemplates.filter(p => p.id !== id));
+ toast({ title: "Position Deleted", description: "Position template removed." });
+ };
+
+ const handleEditPositionPermissions = (position) => {
+ setEditingPosition(position);
+ setSelectedLayer(position.layer);
+ // Load position's permissions
+ const newPerms = {};
+ position.permissions.forEach(p => newPerms[p] = true);
+ setPermissions(newPerms);
+ };
+
+ const handleSavePositionPermissions = () => {
+ if (!editingPosition) return;
+ const enabledPerms = Object.entries(permissions).filter(([_, v]) => v).map(([k]) => k);
+ setPositionTemplates(positionTemplates.map(p =>
+ p.id === editingPosition.id ? { ...p, permissions: enabledPerms } : p
+ ));
+ setEditingPosition(null);
+ toast({ title: "Position Updated", description: `${editingPosition.position} permissions saved.` });
+ };
+
+ const filteredModules = modules.map(module => ({
+ ...module,
+ permissions: module.permissions.filter(perm =>
perm.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
perm.description.toLowerCase().includes(searchTerm.toLowerCase())
)
- })).filter(category => category.permissions.length > 0);
+ })).filter(module => module.permissions.length > 0);
+
+ const enabledCount = Object.values(permissions).filter(Boolean).length;
+ const totalCount = modules.reduce((sum, m) => sum + m.permissions.length, 0);
+
- // Only admins can access this page
- if (userRole !== "admin") {
- return (
-
-
-
Access Denied
-
Only administrators can manage permissions.
-
- );
- }
return (
-
-
-
+
+
+
+
- {/* Role Selector */}
-
-
- Select Role to Configure
- Permissions shown below are contextual to the selected role
-
-
-
- {Object.entries(ROLE_TEMPLATES).map(([roleKey, role]) => (
-
- ))}
+ {/* Tab Navigation */}
+
+
+
+
+
+ {activeTab === "positions" && !editingPosition && (
+ <>
+ {/* Position Templates */}
+
+
+
+
+
+
+
+
+
Position Templates
+
Define default permissions for job positions
+
+
+
+
+
+
+
+ {positionTemplates.map((template) => {
+ const layerConfig = LAYER_HIERARCHY.find(l => l.id === template.layer);
+ const layerModules = PERMISSION_MODULES[template.layer] || [];
+
+ // Get permission names for display
+ const permissionNames = template.permissions.map(permId => {
+ for (const module of layerModules) {
+ const found = module.permissions.find(p => p.id === permId);
+ if (found) return found.name;
+ }
+ return permId;
+ });
+
+ return (
+
+
+
+
{template.position}
+
+ {layerConfig?.name || template.layer}
+
+
+
+
+
+
+
+
{template.description}
+
+ {layerModules.map((module) => {
+ const ModuleIcon = module.icon;
+ const moduleEnabledCount = module.permissions.filter(p => template.permissions.includes(p.id)).length;
+ const allModuleEnabled = moduleEnabledCount === module.permissions.length;
+
+ return (
+
+ {/* Module Header */}
+
+
+
+
+
+
+
{module.name}
+
{moduleEnabledCount} of {module.permissions.length} enabled
+
+
+
+
+
+ {/* Permissions List */}
+
+ {module.permissions.map((perm) => {
+ const isEnabled = template.permissions.includes(perm.id);
+ return (
+
{
+ const newPerms = isEnabled
+ ? template.permissions.filter(p => p !== perm.id)
+ : [...template.permissions, perm.id];
+ setPositionTemplates(positionTemplates.map(p =>
+ p.id === template.id ? { ...p, permissions: newPerms } : p
+ ));
+ toast({ title: isEnabled ? "Permission Disabled" : "Permission Enabled", description: `${perm.name} ${isEnabled ? 'removed from' : 'added to'} ${template.position}` });
+ }}
+ >
+
+
+
+
+
+
{perm.name}
+
{perm.description}
+
+
+
+
+ );
+ })}
+
+
+ );
+ })}
+
+
+ );
+ })}
+
+
+
+
+ {/* Add Position Dialog */}
+
+ >
+ )}
+
+ {editingPosition && (
+
+
+
+
+
Editing: {editingPosition.position}
+
Configure permissions for this position template
+
+
+
+
+
+
-
-
+ )}
- {/* Search */}
-
-
+ {(activeTab === "layers" || editingPosition) && (
+ <>
+ {/* Layer Hierarchy Visual */}
+
+
+
+
+
+
KROW Ecosystem Layers
+
Select a layer to configure its permissions
+
+
+
+
+ {/* Horizontal Layer Flow */}
+
+ {visibleLayers.map((layer, index) => {
+ const Icon = layer.icon;
+ const isSelected = effectiveLayer === layer.id;
+
+ return (
+
+
+ {index < visibleLayers.length - 1 && (
+
+ )}
+
+ );
+ })}
+
+
+ {/* Selected Layer Info */}
+ {selectedLayerConfig && (
+
+
+
+
+
+
+
+
+ {selectedLayerConfig.name} Layer
+
+
{selectedLayerConfig.description}
+
+
+
+
{enabledCount}/{totalCount}
+
enabled
+
+
+
+ )}
+
+
+
+ {/* Search */}
+
- {/* Permission Categories */}
-
- {filteredCategories.map((category) => {
- const Icon = category.icon;
- const isExpanded = expandedCategories[category.id] !== false; // Default to expanded
+ {/* Permission Modules */}
+
+ {filteredModules.map((module) => {
+ const Icon = module.icon;
+ const isExpanded = expandedModules[module.id] !== false;
+ const moduleEnabled = module.permissions.filter(p => permissions[p.id]).length;
+ const moduleTotal = module.permissions.length;
+ const allEnabled = moduleEnabled === moduleTotal;
- return (
-
- toggleCategory(category.id)}
- >
-
-
- {isExpanded ? (
-
- ) : (
-
- )}
-
-
{category.name}
-
- {category.permissions.filter(p => permissions[p.id]).length}/{category.permissions.length}
-
+ return (
+
+ toggleModule(module.id)}
+ >
+
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
{module.name}
+
+ {moduleEnabled} of {moduleTotal} enabled
+
+
+
+
e.stopPropagation()}>
+
+
+
+
+ Toggle all permissions in this module
+
+
-
-
+
- {isExpanded && (
-
-
- {category.permissions.map((perm) => {
- const isOverridden = overrides[perm.id];
+ {isExpanded && (
+
+ {module.permissions.map((perm) => {
const isEnabled = permissions[perm.id];
return (
-
{perm.id}
-
-
{perm.name}
-
-
-
-
-
- {perm.description}
-
-
+
+ {isEnabled ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {perm.name}
+
+ {perm.critical && (
+
+
+
+
+ Critical permission - use with caution
+
+ )}
+
+
{perm.description}
-
-
-
- handlePermissionChange(perm.id, checked)}
- className="border-slate-300 data-[state=checked]:bg-[#0A39DF] data-[state=checked]:border-[#0A39DF]"
- />
-
+
togglePermission(perm.id)}
+ className="data-[state=checked]:bg-green-600"
+ />
);
})}
-
-
- )}
-
- );
- })}
-
-
- {/* Save Button */}
-
-
- {Object.keys(overrides).length} permission{Object.keys(overrides).length !== 1 ? 's' : ''} overridden
+
+ )}
+
+ );
+ })}
-
+
+ {/* Save Footer */}
+ {!editingPosition && (
+
+
+
+
+
+
+
+
{selectedLayerConfig.name} Layer
+
+ {enabledCount} enabled • {totalCount - enabledCount} disabled
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ )}
-
+
);
}
\ No newline at end of file
diff --git a/frontend-web/src/pages/Schedule.jsx b/frontend-web/src/pages/Schedule.jsx
new file mode 100644
index 00000000..a1f65bf9
--- /dev/null
+++ b/frontend-web/src/pages/Schedule.jsx
@@ -0,0 +1,252 @@
+import React, { useState } from "react";
+import { base44 } from "@/api/base44Client";
+import { useQuery } from "@tanstack/react-query";
+import { useNavigate } from "react-router-dom";
+import { createPageUrl } from "@/utils";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { ChevronLeft, ChevronRight, Plus, Clock, DollarSign, Calendar as CalendarIcon } from "lucide-react";
+import { format, startOfWeek, addDays, isSameDay, addWeeks, subWeeks, isToday, parseISO } from "date-fns";
+
+const safeParseDate = (dateString) => {
+ if (!dateString) return null;
+ try {
+ if (typeof dateString === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
+ const [year, month, day] = dateString.split('-').map(Number);
+ return new Date(year, month - 1, day);
+ }
+ return parseISO(dateString);
+ } catch {
+ return null;
+ }
+};
+
+export default function Schedule() {
+ const navigate = useNavigate();
+ const [currentWeek, setCurrentWeek] = useState(startOfWeek(new Date(), { weekStartsOn: 0 }));
+
+ const { data: events = [] } = useQuery({
+ queryKey: ['events'],
+ queryFn: () => base44.entities.Event.list('-date'),
+ initialData: [],
+ });
+
+ const weekDays = Array.from({ length: 7 }, (_, i) => addDays(currentWeek, i));
+
+ const getEventsForDay = (date) => {
+ return events.filter(event => {
+ const eventDate = safeParseDate(event.date);
+ return eventDate && isSameDay(eventDate, date);
+ });
+ };
+
+ const calculateWeekMetrics = () => {
+ const weekEvents = events.filter(event => {
+ const eventDate = safeParseDate(event.date);
+ if (!eventDate) return false;
+ return weekDays.some(day => isSameDay(eventDate, day));
+ });
+
+ const totalHours = weekEvents.reduce((sum, event) => {
+ const hours = event.shifts?.reduce((shiftSum, shift) => {
+ return shiftSum + (shift.roles?.reduce((roleSum, role) => roleSum + (role.hours || 0), 0) || 0);
+ }, 0) || 0;
+ return sum + hours;
+ }, 0);
+
+ const totalCost = weekEvents.reduce((sum, event) => sum + (event.total || 0), 0);
+ const totalShifts = weekEvents.reduce((sum, event) => sum + (event.shifts?.length || 0), 0);
+
+ return { totalHours, totalCost, totalShifts };
+ };
+
+ const metrics = calculateWeekMetrics();
+
+ const goToPreviousWeek = () => setCurrentWeek(subWeeks(currentWeek, 1));
+ const goToNextWeek = () => setCurrentWeek(addWeeks(currentWeek, 1));
+ const goToToday = () => setCurrentWeek(startOfWeek(new Date(), { weekStartsOn: 0 }));
+
+ return (
+
+
+ {/* Header */}
+
+
+
Schedule
+
Plan and manage staff shifts
+
+
+
+
+ {/* Metrics Cards */}
+
+
+
+
+
+
Week Total Hours
+
{metrics.totalHours.toFixed(1)}
+
+
+
+
+
+
+
+
+
+
+
Week Labor Cost
+
${metrics.totalCost.toLocaleString()}
+
+
+
+
+
+
+
+
+
+
+
Total Shifts
+
{metrics.totalShifts}
+
+
+
+
+
+
+
+ {/* Week Navigation */}
+
+
+
+
+
+
Week of
+
{format(currentWeek, 'MMM d, yyyy')}
+
+
+
+
+
+
+
+ {/* Weekly Calendar */}
+
+ {weekDays.map((day, index) => {
+ const dayEvents = getEventsForDay(day);
+ const isTodayDay = isToday(day);
+
+ return (
+
+
+ {/* Day Header */}
+
+
+ {format(day, 'EEE')}
+
+
+ {format(day, 'd')}
+
+
+ {format(day, 'MMM')}
+
+
+
+ {/* Add Shift Button */}
+
+
+ {/* Events List */}
+
+ {dayEvents.length === 0 ? (
+
+ No shifts
+
+ ) : (
+ dayEvents.map((event) => {
+ const firstShift = event.shifts?.[0];
+ const firstRole = firstShift?.roles?.[0];
+ const firstStaff = event.assigned_staff?.[0];
+
+ return (
+
navigate(createPageUrl(`EventDetail?id=${event.id}`))}
+ className={`p-3 rounded cursor-pointer transition-all ${
+ isTodayDay
+ ? 'bg-white/20 hover:bg-white/30 border border-white/40'
+ : 'bg-white hover:bg-slate-50 border border-slate-200 shadow-sm'
+ }`}
+ >
+ {/* Status Badges */}
+
+ {firstRole?.role && (
+
+ {firstRole.role}
+
+ )}
+
+ {event.status || 'scheduled'}
+
+
+
+ {/* Staff Member */}
+ {firstStaff && (
+
+ 👤
+ {firstStaff.staff_name}
+
+ )}
+
+ {/* Time */}
+ {firstRole && (firstRole.start_time || firstRole.end_time) && (
+
+
+ {firstRole.start_time || '00:00'} - {firstRole.end_time || '00:00'}
+
+ )}
+
+ {/* Cost */}
+ {event.total > 0 && (
+
+ ${event.total.toFixed(2)}
+
+ )}
+
+ );
+ })
+ )}
+
+
+
+ );
+ })}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend-web/src/pages/StaffAvailability.jsx b/frontend-web/src/pages/StaffAvailability.jsx
new file mode 100644
index 00000000..d4e7da39
--- /dev/null
+++ b/frontend-web/src/pages/StaffAvailability.jsx
@@ -0,0 +1,469 @@
+import React, { useState, useMemo } from "react";
+import { base44 } from "@/api/base44Client";
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Badge } from "@/components/ui/badge";
+import { Avatar, AvatarFallback } from "@/components/ui/avatar";
+import { Users, Calendar, Clock, TrendingUp, TrendingDown, AlertCircle, CheckCircle, XCircle, Search, Filter, List, LayoutGrid, ChevronLeft, ChevronRight } from "lucide-react";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
+import { format } from "date-fns";
+
+export default function StaffAvailability() {
+ const [searchTerm, setSearchTerm] = useState("");
+ const [filterStatus, setFilterStatus] = useState("all");
+ const [filterUtilization, setFilterUtilization] = useState("all");
+ const [viewMode, setViewMode] = useState("cards");
+ const [currentPage, setCurrentPage] = useState(1);
+ const [itemsPerPage, setItemsPerPage] = useState(50);
+ const [sortBy, setSortBy] = useState("need_work_index");
+
+ const { data: allStaff = [] } = useQuery({
+ queryKey: ['staff-availability-all'],
+ queryFn: () => base44.entities.Staff.list(),
+ initialData: [],
+ });
+
+ const { data: availabilityData = [] } = useQuery({
+ queryKey: ['worker-availability'],
+ queryFn: () => base44.entities.WorkerAvailability.list(),
+ initialData: [],
+ });
+
+ const { data: events = [] } = useQuery({
+ queryKey: ['events-for-availability'],
+ queryFn: () => base44.entities.Event.list(),
+ initialData: [],
+ });
+
+ // Calculate metrics
+ const metrics = useMemo(() => {
+ const needsWork = availabilityData.filter(w => w.need_work_index >= 60).length;
+ const fullyBooked = availabilityData.filter(w => w.utilization_percentage >= 90).length;
+ const hasUtilization = availabilityData.filter(w => w.utilization_percentage > 0 && w.utilization_percentage < 90).length;
+ const onTimeOff = availabilityData.filter(w => w.availability_status === 'BLOCKED').length;
+
+ return { needsWork, fullyBooked, hasUtilization, onTimeOff };
+ }, [availabilityData]);
+
+ // Filter and search logic
+ const filteredAvailability = useMemo(() => {
+ let filtered = availabilityData;
+
+ // Search
+ if (searchTerm) {
+ filtered = filtered.filter(a =>
+ a.staff_name?.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+ }
+
+ // Status filter
+ if (filterStatus !== "all") {
+ filtered = filtered.filter(a => a.availability_status === filterStatus);
+ }
+
+ // Utilization filter
+ if (filterUtilization === "underutilized") {
+ filtered = filtered.filter(a => a.utilization_percentage < 50);
+ } else if (filterUtilization === "optimal") {
+ filtered = filtered.filter(a => a.utilization_percentage >= 50 && a.utilization_percentage < 100);
+ } else if (filterUtilization === "full") {
+ filtered = filtered.filter(a => a.utilization_percentage >= 100);
+ }
+
+ // Sort
+ if (sortBy === "need_work_index") {
+ filtered.sort((a, b) => (b.need_work_index || 0) - (a.need_work_index || 0));
+ } else if (sortBy === "utilization") {
+ filtered.sort((a, b) => (a.utilization_percentage || 0) - (b.utilization_percentage || 0));
+ } else if (sortBy === "name") {
+ filtered.sort((a, b) => (a.staff_name || "").localeCompare(b.staff_name || ""));
+ } else if (sortBy === "availability_score") {
+ filtered.sort((a, b) => (b.predicted_availability_score || 0) - (a.predicted_availability_score || 0));
+ }
+
+ return filtered;
+ }, [availabilityData, searchTerm, filterStatus, filterUtilization, sortBy]);
+
+ // Pagination
+ const totalPages = Math.ceil(filteredAvailability.length / itemsPerPage);
+ const paginatedData = filteredAvailability.slice(
+ (currentPage - 1) * itemsPerPage,
+ currentPage * itemsPerPage
+ );
+
+ React.useEffect(() => {
+ setCurrentPage(1);
+ }, [searchTerm, filterStatus, filterUtilization, sortBy]);
+
+ const getUtilizationColor = (percentage) => {
+ if (percentage === 0) return "text-slate-400";
+ if (percentage < 50) return "text-red-600";
+ if (percentage < 80) return "text-amber-600";
+ return "text-green-600";
+ };
+
+ const getStatusBadge = (worker) => {
+ const statusConfig = {
+ 'CONFIRMED_AVAILABLE': { bg: 'bg-green-100', text: 'text-green-800', label: 'Available' },
+ 'UNKNOWN': { bg: 'bg-gray-100', text: 'text-gray-800', label: 'Unknown' },
+ 'BLOCKED': { bg: 'bg-red-100', text: 'text-red-800', label: 'Unavailable' },
+ };
+ const config = statusConfig[worker.availability_status] || statusConfig['UNKNOWN'];
+ return
{config.label};
+ };
+
+ return (
+
+
+
+ {/* Header */}
+
+
+
Staff Availability
+
+ Showing {filteredAvailability.length} of {availabilityData.length} workers
+
+
+
+
+
+
+
+
+ {/* Metrics Cards */}
+
+
+
+
+
+
Needs Work
+
{metrics.needsWork}
+
Available workers
+
+
+
+
+
+
+
+
+
+
+
+
+
Fully Booked
+
{metrics.fullyBooked}
+
At capacity
+
+
+
+
+
+
+
+
+
+
+
+
+
Active
+
{metrics.hasUtilization}
+
Working now
+
+
+
+
+
+
+
+
+
+
+
+
+
On Time Off
+
{metrics.onTimeOff}
+
Unavailable
+
+
+
+
+
+
+
+
+
+ {/* Search and Filters */}
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10 h-10 border border-slate-300 focus:border-[#0A39DF]"
+ />
+
+
+
+
+
+
+
+
+ {/* Main Content - Table or Cards View */}
+ {viewMode === "table" ? (
+
+
+
+
+
+ Name
+ Status
+ Hours
+ Utilization
+ Hours Gap
+ Acceptance
+ Last Shift
+
+
+
+ {paginatedData.length === 0 ? (
+
+
+ No workers found
+
+
+ ) : (
+ paginatedData.map((worker) => (
+
+
+
+
+
+ {worker.staff_name?.charAt(0) || "?"}
+
+
+
{worker.staff_name}
+
+
+
+ {getStatusBadge(worker)}
+
+
+
+ {worker.scheduled_hours_this_period}h
+
+ / {worker.desired_hours_this_period}h
+
+
+
+
+ {Math.round(worker.utilization_percentage)}%
+
+
+
+
+
+ {worker.scheduled_hours_this_period < worker.desired_hours_this_period ? (
+
+ Needs {worker.desired_hours_this_period - worker.scheduled_hours_this_period}h
+
+ ) : (
+
+ Fully booked
+
+ )}
+
+
+ {worker.acceptance_rate || 0}%
+
+
+ {worker.last_shift_date ? format(new Date(worker.last_shift_date), 'MMM d') : '-'}
+
+
+ ))
+ )}
+
+
+
+
+ ) : (
+
+ {paginatedData.map((worker) => {
+ const staff = allStaff.find(s => s.id === worker.staff_id);
+
+ return (
+
+
+
+
+
+ {worker.staff_name?.charAt(0)?.toUpperCase()}
+
+
+
+
{worker.staff_name}
+
{staff?.position || 'Staff'}
+
+
+ {getStatusBadge(worker)}
+ {worker.scheduled_hours_this_period < worker.desired_hours_this_period && (
+
+ Needs {worker.desired_hours_this_period - worker.scheduled_hours_this_period}h
+
+ )}
+ {worker.scheduled_hours_this_period >= worker.desired_hours_this_period && (
+
+ Fully booked
+
+ )}
+
+
+
+
+
+ Weekly Hours
+
+ {worker.scheduled_hours_this_period}h / {worker.desired_hours_this_period}h
+
+
+
+
+
+ {worker.last_shift_date && (
+
+
+ Last shift: {format(new Date(worker.last_shift_date), 'MMM d')}
+
+ )}
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+ {/* Pagination */}
+
+
+
+
+
+ Showing {((currentPage - 1) * itemsPerPage) + 1} to {Math.min(currentPage * itemsPerPage, filteredAvailability.length)} of {filteredAvailability.length} workers
+
+
+
+
+
+
+ Page {currentPage} of {totalPages}
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend-web/src/pages/TeamDetails.jsx b/frontend-web/src/pages/TeamDetails.jsx
index 4b683349..ae77337a 100644
--- a/frontend-web/src/pages/TeamDetails.jsx
+++ b/frontend-web/src/pages/TeamDetails.jsx
@@ -647,7 +647,12 @@ export default function TeamDetails() {
-
{hub.hub_name}
+
+
{hub.hub_name}
+
+ {members.filter(m => m.hub === hub.hub_name).length} members
+
+
{hub.manager_name && (
Manager: {hub.manager_name}
)}
diff --git a/frontend-web/src/pages/Teams.jsx b/frontend-web/src/pages/Teams.jsx
index a7310228..acd931e7 100644
--- a/frontend-web/src/pages/Teams.jsx
+++ b/frontend-web/src/pages/Teams.jsx
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Users, Plus, Search, Building2, MapPin, UserCheck, Mail, Edit, Loader2, Trash2, UserX, LayoutGrid, List as ListIcon, RefreshCw, Send, Filter, Star, UserPlus } from "lucide-react";
import PageHeader from "@/components/common/PageHeader";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
@@ -232,8 +233,8 @@ export default function Teams() {
const uniqueDepartments = [...new Set([...teamDepartments, ...memberDepartments])];
// Separate active and deactivated members
- const activeMembers = teamMembers.filter(m => m.is_active);
- const deactivatedMembers = teamMembers.filter(m => !m.is_active);
+ const activeMembers = teamMembers.filter(m => m.is_active !== false);
+ const deactivatedMembers = teamMembers.filter(m => m.is_active === false);
const createTestInviteMutation = useMutation({
mutationFn: async () => {
@@ -289,15 +290,30 @@ export default function Teams() {
throw new Error("Unable to identify who is sending the invite. Please try logging out and back in.");
}
- // Create hub if it doesn't exist
- if (data.hub && !teamHubs.find(h => h.hub_name === data.hub)) {
+ // Create hub if it doesn't exist, or update hub with new department
+ const existingHub = teamHubs.find(h => h.hub_name === data.hub);
+
+ if (data.hub && !existingHub) {
+ // Create new hub with department
await base44.entities.TeamHub.create({
team_id: userTeam.id,
hub_name: data.hub,
address: "",
- is_active: true
+ is_active: true,
+ departments: data.department ? [{ department_name: data.department, cost_center: "" }] : []
});
queryClient.invalidateQueries({ queryKey: ['team-hubs-main', userTeam?.id] });
+ } else if (existingHub && data.department) {
+ // Add department to existing hub if it doesn't exist
+ const hubDepartments = existingHub.departments || [];
+ const departmentExists = hubDepartments.some(d => d.department_name === data.department);
+
+ if (!departmentExists) {
+ await base44.entities.TeamHub.update(existingHub.id, {
+ departments: [...hubDepartments, { department_name: data.department, cost_center: "" }]
+ });
+ queryClient.invalidateQueries({ queryKey: ['team-hubs-main', userTeam?.id] });
+ }
}
const inviteCode = `TEAM-${Math.floor(10000 + Math.random() * 90000)}`;
@@ -855,11 +871,19 @@ export default function Teams() {
{member.email}
)}
- {member.department && (
-
- {member.department}
-
- )}
+
+ {member.department && (
+
+ {member.department}
+
+ )}
+ {member.hub && (
+
+
+ {member.hub}
+
+ )}
+
@@ -1053,51 +1077,71 @@ export default function Teams() {
)}
{viewMode === "list" && filteredMembers(activeMembers).length > 0 && (
-
- {filteredMembers(activeMembers).map((member) => (
-
-
-
- {member.member_name?.split(' ').map(n => n[0]).join('') || '?'}
-
-
-
{member.member_name}
-
{member.role} {member.title && `• ${member.title}`}
-
- {member.phone && `${member.phone} • `}{member.email}
-
- {member.department && (
-
- {member.department}
-
- )}
-
-
-
-
-
-
-
- ))}
+
+
+
+
+ | # |
+ Name |
+ Title |
+ Role |
+ Department |
+ Hub |
+ Hub Address |
+ Actions |
+
+
+
+ {filteredMembers(activeMembers).map((member, index) => {
+ const memberHub = teamHubs.find(h => h.hub_name === member.hub);
+ return (
+
+ | {index + 1} |
+
+
+
+ {member.member_name?.split(' ').map(n => n[0]).join('') || '?'}
+
+
+ {member.member_name}
+ {member.email}
+
+
+ |
+ {member.title || '-'} |
+
+
+ {member.role}
+
+ |
+ {member.department || 'No Department'} |
+ {member.hub || 'No Hub'} |
+ {memberHub?.address || 'No Address'} |
+
+
+
+
+
+ |
+
+ );
+ })}
+
+
)}
@@ -1134,54 +1178,74 @@ export default function Teams() {
)}
{viewMode === "list" && filteredMembers(deactivatedMembers).length > 0 && (
-
- {filteredMembers(deactivatedMembers).map((member) => (
-
-
-
- {member.member_name?.split(' ').map(n => n[0]).join('') || '?'}
-
-
-
-
-
-
{member.member_name}
-
{member.role} {member.title && `• ${member.title}`}
-
- {member.phone && `${member.phone} • `}{member.email}
-
- {member.department && (
-
- {member.department}
-
- )}
-
-
-
-
-
-
-
- ))}
+
+
+
+
+ | # |
+ Name |
+ Title |
+ Role |
+ Department |
+ Hub |
+ Hub Address |
+ Actions |
+
+
+
+ {filteredMembers(deactivatedMembers).map((member, index) => {
+ const memberHub = teamHubs.find(h => h.hub_name === member.hub);
+ return (
+
+ | {index + 1} |
+
+
+
+ {member.member_name?.split(' ').map(n => n[0]).join('') || '?'}
+
+
+
+
+
+ {member.member_name}
+ {member.email}
+
+
+ |
+ {member.title || '-'} |
+
+
+ {member.role}
+
+ |
+ {member.department || 'No Department'} |
+ {member.hub || 'No Hub'} |
+ {memberHub?.address || 'No Address'} |
+
+
+
+
+
+ |
+
+ );
+ })}
+
+
)}
@@ -1258,124 +1322,271 @@ export default function Teams() {
{/* Hubs Tab */}
-
-
-
-
- Team Locations & Hubs
-
-
Manage your physical locations and departments
-
-
-
-
{teamHubs.length > 0 ? (
-
- {teamHubs.map((hub) => (
-
-
-
-
-
+ viewMode === "grid" ? (
+
+ {teamHubs.map((hub) => (
+
+ {/* Hub Header */}
+
+
+
+
-
-
{hub.hub_name}
+
+
{hub.hub_name}
+ {hub.address && (
+
+ )}
{hub.manager_name && (
-
-
-
{hub.manager_name}
+
)}
-
- {hub.departments?.length || 0} Depts
-
+
+
+
+
+
+ {/* Quick Stats */}
+
+
+
+
+
+
+
Team Members
+
{activeMembers.filter(m => m.hub === hub.hub_name).length}
+
+
+
+
+
+
+
+
Departments
+
{hub.departments?.length || 0}
+
+
-
-
- {hub.address && (
-
- )}
-
- {hub.manager_email && (
-
- )}
-
- {hub.departments && hub.departments.length > 0 ? (
-
-
Departments
-
- {hub.departments.map((dept, idx) => (
-
-
-
-
{dept.department_name}
- {dept.cost_center && (
-
CC: {dept.cost_center}
- )}
-
- {dept.manager_name && (
-
- {dept.manager_name}
-
- )}
-
-
- ))}
-
-
- ) : (
-
- )}
-
-
+
+ {/* Departments Section */}
+
+
+
+
+ Departments
+
-
-
-
- ))}
-
+
+ {hub.departments && hub.departments.length > 0 ? (
+
+ {hub.departments.map((dept, idx) => (
+
+
+
+
+
+
+
+
+
{dept.department_name}
+ {dept.cost_center && (
+
Cost Center: {dept.cost_center}
+ )}
+
+
+ {dept.manager_name && (
+
+ {dept.manager_name}
+
+ )}
+
+
+ {/* Team members in this department */}
+ {activeMembers.filter(m => m.hub === hub.hub_name && m.department === dept.department_name).length > 0 && (
+
+
+ Team ({activeMembers.filter(m => m.hub === hub.hub_name && m.department === dept.department_name).length})
+
+
+ {activeMembers.filter(m => m.hub === hub.hub_name && m.department === dept.department_name).map((member) => (
+
handleEditMember(member)}>
+
+ {member.member_name?.split(' ').map(n => n[0]).join('') || '?'}
+
+
+
{member.member_name}
+
{member.title || member.role}
+
+
+ ))}
+
+
+ )}
+
+
+ ))}
+
+ ) : (
+
+
+
No departments yet
+
+
+ )}
+
+
+ ))}
+
+ {/* Add New Hub Button */}
+
+
+ ) : (
+
+
+
+
+ Hub Name
+ Address
+ Manager
+ Departments
+ Members
+ Actions
+
+
+
+ {teamHubs.map((hub) => (
+
+
+
+
+
+
+
+ {hub.address || '—'}
+
+
+
+
+
{hub.manager_name || '—'}
+ {hub.manager_email && (
+
{hub.manager_email}
+ )}
+
+
+
+
+ {hub.departments?.length || 0} depts
+
+
+
+
+ {activeMembers.filter(m => m.hub === hub.hub_name).length} members
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+ )
) : (
@@ -1396,65 +1607,146 @@ export default function Teams() {
{/* Favorites Tab */}
-
-
-
-
+
+ {/* Header Section */}
+
+
+
+
+
+
+
+ Preferred Staff
+
+
Your go-to professionals for high-priority assignments
+
+
+
+
{userTeam?.favorite_staff_count || 0}
+
Favorites
+
+
+
+
+
+ {/* Search */}
+
+
setFavoriteSearch(e.target.value)}
- className="pl-10"
+ className="pl-12 h-12 bg-white border-2 border-amber-200 focus:border-amber-400 text-base"
/>
-
{userTeam?.favorite_staff && userTeam.favorite_staff.length > 0 ? (
-
- {userTeam.favorite_staff.filter(f =>
- !favoriteSearch ||
- f.staff_name?.toLowerCase().includes(favoriteSearch.toLowerCase()) ||
- f.position?.toLowerCase().includes(favoriteSearch.toLowerCase())
- ).map((fav) => (
-
-
-
-
-
-
- {fav.staff_name?.charAt(0)}
-
-
-
-
{fav.staff_name}
-
{fav.position}
+ viewMode === "grid" ? (
+
+ {userTeam.favorite_staff.filter(f =>
+ !favoriteSearch ||
+ f.staff_name?.toLowerCase().includes(favoriteSearch.toLowerCase()) ||
+ f.position?.toLowerCase().includes(favoriteSearch.toLowerCase())
+ ).map((fav) => (
+
+
+
+
+
+
+ {fav.staff_name?.charAt(0)}
+
+
+
+
+
+
+
{fav.staff_name}
+
{fav.position}
+
+ Added {new Date(fav.added_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
+
+
-
+
+
+
+
-
-
-
- ))}
-
+
+ ))}
+
+ ) : (
+
+
+
+ Staff Member
+ Position
+ Added Date
+ Actions
+
+
+
+ {userTeam.favorite_staff.filter(f =>
+ !favoriteSearch ||
+ f.staff_name?.toLowerCase().includes(favoriteSearch.toLowerCase()) ||
+ f.position?.toLowerCase().includes(favoriteSearch.toLowerCase())
+ ).map((fav) => (
+
+
+
+
+ {fav.staff_name?.charAt(0)}
+
+
{fav.staff_name}
+
+
+
+ {fav.position}
+
+
+ {new Date(fav.added_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
+
+
+
+
+
+
+ ))}
+
+
+ )
) : (
-
-
-
No Favorite Staff
-
Mark staff as favorites to see them here
-