+
+
+
+ {staff.initial || getInitials(staff.employee_name)}
+
+
+
= 80 ? 'bg-emerald-500 animate-pulse' : 'bg-amber-500'}`} />
+
+
+
+
+ {staff.employee_name}
+
+
+
+ {staff.position || 'Professional Staff'}
+
+
+
+
+
navigate(`/staff/${staff.id}/edit`)}
+ className="rounded-full hover:bg-primary/10 hover:text-primary transition-premium border border-transparent hover:border-primary/20"
+ >
+
+
+
+
+ {/* Reliability & Rating */}
+
+
+ {renderStars(rating)}
+ {rating.toFixed(1)}
+
+
+
+ {reliability.icon}
+ {reliabilityScore}% Reliable
+
+
+
+ {/* Stats & Professional Details */}
+
+ {[
+ { label: 'Coverage', value: `${coveragePercentage}%`, icon:
, color: 'emerald' },
+ { label: 'Cancels', value: cancellationCount, icon:
, color: cancellationCount === 0 ? 'emerald' : 'rose' },
+ { label: 'No Shows', value: noShowCount, icon:
, color: noShowCount === 0 ? 'emerald' : 'rose' },
+ staff.profile_type && { label: 'Level', value: staff.profile_type, icon:
, color: 'primary' },
+ staff.english && { label: 'English', value: staff.english, icon:
, color: staff.english === 'Fluent' ? 'emerald' : 'blue' },
+ staff.invoiced && { label: 'Status', value: 'Verified', icon:
, color: 'emerald' },
+ ].filter(Boolean).map((item: any, i) => (
+
+
+
+ {item.icon}
+
+
{item.label}
+
+
+ {item.value}
+
+
+ ))}
+
+
+ {/* Contact Details with Glass effect */}
+
+ {[
+ { icon:
, text: staff.email, visible: !!staff.email },
+ { icon:
, text: staff.contact_number, visible: !!staff.contact_number },
+ { icon:
, text: staff.hub_location, visible: !!staff.hub_location },
+ { icon:
, text: `Updated ${formatDate(staff.check_in || '')}`, visible: !!staff.check_in },
+ ].filter(d => d.visible).map((detail, i) => (
+
+
+ {detail.icon}
+
+
{detail.text}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/apps/web/src/features/workforce/directory/components/FilterBar.tsx b/apps/web/src/features/workforce/directory/components/FilterBar.tsx
new file mode 100644
index 00000000..f5bebf61
--- /dev/null
+++ b/apps/web/src/features/workforce/directory/components/FilterBar.tsx
@@ -0,0 +1,75 @@
+import { Input } from "../../../../common/components/ui/input";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../common/components/ui/select";
+import { Search, MapPin, Briefcase } from "lucide-react";
+
+interface FilterBarProps {
+ searchTerm: string;
+ setSearchTerm: (value: string) => void;
+ departmentFilter: string;
+ setDepartmentFilter: (value: string) => void;
+ locationFilter: string;
+ setLocationFilter: (value: string) => void;
+ departments?: string[];
+ locations: string[];
+}
+
+export default function FilterBar({
+ searchTerm,
+ setSearchTerm,
+ departmentFilter,
+ setDepartmentFilter,
+ locationFilter,
+ setLocationFilter,
+ locations
+}: FilterBarProps) {
+ return (
+
+
setSearchTerm(e.target.value)}
+ leadingIcon={
}
+ className="flex-1"
+ />
+
+
+
+ }
+ >
+
+
+
+ All Departments
+ Operations
+ Sales
+ HR
+ Finance
+ IT
+ Marketing
+ Customer Service
+ Logistics
+
+
+
+
+ }
+ >
+
+
+
+ All Locations
+ {locations.map(location => (
+
+ {location}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/apps/web/src/features/workforce/directory/components/StaffForm.tsx b/apps/web/src/features/workforce/directory/components/StaffForm.tsx
new file mode 100644
index 00000000..cbe2cfb7
--- /dev/null
+++ b/apps/web/src/features/workforce/directory/components/StaffForm.tsx
@@ -0,0 +1,516 @@
+import React, { useState, useEffect } from "react";
+import { useForm, Controller } from "react-hook-form";
+import { Input } from "@/common/components/ui/input";
+import { Label } from "@/common/components/ui/label";
+import { Textarea } from "@/common/components/ui/textarea";
+import { Button } from "@/common/components/ui/button";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/common/components/ui/select";
+import { Checkbox } from "@/common/components/ui/checkbox";
+import {
+ Save, Loader2, User, Activity, MapPin,
+ Calendar, ChevronRight, FileText,
+ Shield, Star, Info
+} from "lucide-react";
+import { AnimatePresence } from "framer-motion";
+import type { Staff } from "../../type";
+import { TabContent } from "./TabContent";
+
+interface StaffFormProps {
+ staff?: Staff;
+ onSubmit: (data: Omit
) => Promise;
+ isSubmitting: boolean;
+}
+
+type TabType = "general" | "performance" | "location" | "additional";
+
+export default function StaffForm({ staff, onSubmit, isSubmitting }: StaffFormProps) {
+ const [activeTab, setActiveTab] = useState("general");
+
+ const { register, handleSubmit, control, reset } = useForm({
+ defaultValues: staff || {
+ employee_name: "",
+ manager: "",
+ contact_number: "",
+ phone: "",
+ email: "",
+ department: "",
+ hub_location: "",
+ event_location: "",
+ track: "",
+ address: "",
+ city: "",
+ position: "",
+ position_2: "",
+ initial: "",
+ profile_type: "",
+ employment_type: "",
+ english: "",
+ english_required: false,
+ check_in: "",
+ replaced_by: "",
+ ro: "",
+ mon: "",
+ schedule_days: "",
+ invoiced: false,
+ action: "",
+ notes: "",
+ accounting_comments: "",
+ rating: 0,
+ shift_coverage_percentage: 100,
+ cancellation_count: 0,
+ no_show_count: 0,
+ total_shifts: 0,
+ reliability_score: 100
+ }
+ });
+
+ useEffect(() => {
+ if (staff) {
+ reset(staff);
+ }
+ }, [staff, reset]);
+
+ const onFormSubmit = (data: Staff) => {
+ onSubmit(data);
+ };
+
+ const tabs: { id: TabType; label: string; icon: React.ReactNode }[] = [
+ { id: "general", label: "General Info", icon: },
+ { id: "performance", label: "Performance", icon: },
+ { id: "location", label: "Location", icon: },
+ { id: "additional", label: "Other", icon: },
+ ];
+
+ return (
+
+ );
+}
+
+const TrendingUp = ({ className }: { className?: string }) => (
+
+);
diff --git a/apps/web/src/features/workforce/directory/components/TabContent.tsx b/apps/web/src/features/workforce/directory/components/TabContent.tsx
new file mode 100644
index 00000000..19087bc4
--- /dev/null
+++ b/apps/web/src/features/workforce/directory/components/TabContent.tsx
@@ -0,0 +1,45 @@
+import React from "react";
+import { motion } from "framer-motion";
+import { Card, CardHeader, CardTitle, CardContent } from "@/common/components/ui/card";
+
+interface TabContentProps {
+ id: string;
+ title: string;
+ icon: React.ReactNode;
+ children: React.ReactNode;
+ className?: string;
+ footer?: React.ReactNode;
+}
+
+export const TabContent = ({
+ id,
+ title,
+ icon,
+ children,
+ className,
+ footer
+}: TabContentProps) => (
+
+
+
+
+
+ {icon}
+
+ {title}
+
+
+
+ {children}
+
+
+ {footer}
+
+);
diff --git a/apps/web/src/features/workforce/type.ts b/apps/web/src/features/workforce/type.ts
new file mode 100644
index 00000000..d57b4322
--- /dev/null
+++ b/apps/web/src/features/workforce/type.ts
@@ -0,0 +1,71 @@
+export interface Staff {
+ id?: string;
+ vendor_id?: string;
+ vendor_name?: string;
+ created_by?: string;
+ created_date?: string;
+
+ // Basic Info
+ employee_name: string;
+ initial?: string;
+ position?: string; // Primary Skill
+ position_2?: string; // Secondary Skill
+ profile_type?: string; // Skill Level
+ employment_type?: string;
+ manager?: string;
+ email?: string;
+ contact_number?: string;
+ phone?: string; // Additional Phone
+ photo?: string; // Photo URL
+ status?: string; // 'Active' | 'Pending' | 'Suspended'
+ skills?: string[]; // Array of skills
+
+ // Performance
+ averageRating?: number;
+ reliability_score?: number;
+ shift_coverage_percentage?: number;
+ cancellation_count?: number;
+ no_show_count?: number;
+ total_shifts?: number;
+ invoiced?: boolean;
+ last_active?: string; // ISO timestamp of last activity
+
+ // Location & Dept
+ department?: string;
+ city?: string;
+ hub_location?: string;
+ event_location?: string;
+ track?: string;
+ address?: string;
+
+ // Lang & Schedule
+ english?: string;
+ english_required?: boolean;
+ check_in?: string;
+ schedule_days?: string;
+
+ // Other
+ replaced_by?: string;
+ action?: string;
+ ro?: string;
+ mon?: string;
+ notes?: string;
+ accounting_comments?: string;
+}
+
+export interface User {
+ id: string;
+ email: string;
+ role: string; // 'admin' | 'client' | 'vendor' | 'workforce' | 'operator' | 'procurement' | 'sector'
+ user_role?: string; // MVP uses both sometimes
+ company_name?: string;
+ name: string;
+}
+
+export interface Event {
+ id: string;
+ client_email?: string;
+ business_name?: string;
+ created_by?: string;
+ assigned_staff?: { staff_id: string }[];
+}
diff --git a/apps/web/src/routes.tsx b/apps/web/src/routes.tsx
index c0950b3b..f1ceebee 100644
--- a/apps/web/src/routes.tsx
+++ b/apps/web/src/routes.tsx
@@ -9,6 +9,9 @@ import VendorDashboard from './features/dashboard/VendorDashboard';
import ProtectedRoute from './features/layouts/ProtectedRoute';
import { RoleDashboardRedirect } from './features/dashboard/RoleDashboardRedirect';
import PublicLayout from './features/layouts/PublicLayout';
+import StaffList from './features/workforce/directory/StaffList';
+import EditStaff from './features/workforce/directory/EditStaff';
+import AddStaff from './features/workforce/directory/AddStaff';
/**
* AppRoutes Component
@@ -74,6 +77,10 @@ const AppRoutes: React.FC = () => {
}
/>
+ {/* Workforce Routes */}
+ } />
+ } />
+ } />
} />
diff --git a/apps/web/src/services/workforceService.ts b/apps/web/src/services/workforceService.ts
new file mode 100644
index 00000000..bf36a8ea
--- /dev/null
+++ b/apps/web/src/services/workforceService.ts
@@ -0,0 +1,437 @@
+import type { Staff, User, Event } from "../features/workforce/type";
+
+/**
+ * Mock Workforce Service
+ * Provides placeholder data for UI development
+ * Replace with actual API calls when backend is ready
+ */
+
+const mockUsers: Record = {
+ admin: {
+ id: "admin-001",
+ email: "admin@krow.com",
+ role: "admin",
+ user_role: "admin",
+ name: "Admin User",
+ company_name: "Krow Workforce",
+ },
+ vendor: {
+ id: "vendor-001",
+ email: "vendor@staffagency.com",
+ role: "vendor",
+ user_role: "vendor",
+ name: "Vendor User",
+ company_name: "Staff Agency Pro",
+ },
+ client: {
+ id: "client-001",
+ email: "client@eventco.com",
+ role: "client",
+ user_role: "client",
+ name: "Client User",
+ company_name: "Event Co.",
+ },
+ operator: {
+ id: "operator-001",
+ email: "operator@krow.com",
+ role: "operator",
+ user_role: "operator",
+ name: "Operator User",
+ company_name: "Krow Workforce",
+ },
+};
+
+const mockStaff = [
+ {
+ id: "staff-001",
+ employee_name: "Sarah Johnson",
+ position: "Event Coordinator",
+ photo: "https://i.pravatar.cc/150?u=staff-001",
+ photo_url: "https://i.pravatar.cc/150?u=staff-001",
+ profile_type: "Senior",
+ email: "sarah.johnson@example.com",
+ contact_number: "+1 (555) 123-4567",
+ status: "Active",
+ skills: ["Event Management", "Customer Service", "Coordination"],
+ department: "Events",
+ hub_location: "New York",
+ averageRating: 4.8,
+ reliability_score: 95,
+ shift_coverage_percentage: 98,
+ vendor_id: "vendor-001",
+ vendor_name: "Staff Agency Pro",
+ created_by: "vendor@staffagency.com",
+ created_date: new Date().toISOString(),
+ last_active: new Date(Date.now() - 1000 * 60 * 60 * 1).toISOString(),
+ employment_type: "Contract",
+ manager: "John Smith",
+ cancellation_count: 0,
+ no_show_count: 0,
+ total_shifts: 145,
+ },
+ {
+ id: "staff-002",
+ employee_name: "Michael Chen",
+ position: "Logistics Manager",
+ photo: "https://i.pravatar.cc/150?u=staff-002",
+ photo_url: "https://i.pravatar.cc/150?u=staff-002",
+ profile_type: "Intermediate",
+ email: "michael.chen@example.com",
+ contact_number: "+1 (555) 234-5678",
+ status: "Active",
+ skills: ["Logistics", "Operations", "Planning"],
+ department: "Operations",
+ hub_location: "Los Angeles",
+ averageRating: 4.6,
+ reliability_score: 88,
+ shift_coverage_percentage: 85,
+ vendor_id: "vendor-001",
+ vendor_name: "Staff Agency Pro",
+ created_by: "vendor@staffagency.com",
+ created_date: new Date().toISOString(),
+ last_active: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3).toISOString(),
+ employment_type: "Full-time",
+ manager: "Jane Williams",
+ cancellation_count: 2,
+ no_show_count: 1,
+ total_shifts: 156,
+ },
+ {
+ id: "staff-003",
+ employee_name: "Emma Rodriguez",
+ position: "Customer Service Rep",
+ photo: "https://i.pravatar.cc/150?u=staff-003",
+ photo_url: "https://i.pravatar.cc/150?u=staff-003",
+ profile_type: "Junior",
+ email: "emma.rodriguez@example.com",
+ contact_number: "+1 (555) 345-6789",
+ status: "Pending",
+ skills: ["Customer Service", "Communication"],
+ department: "Support",
+ hub_location: "Chicago",
+ averageRating: 4.3,
+ reliability_score: 72,
+ shift_coverage_percentage: 65,
+ vendor_id: "vendor-001",
+ vendor_name: "Staff Agency Pro",
+ created_by: "vendor@staffagency.com",
+ created_date: new Date().toISOString(),
+ last_active: new Date(Date.now() - 1000 * 60 * 60 * 24 * 10).toISOString(),
+ employment_type: "Part-time",
+ manager: "Robert Brown",
+ cancellation_count: 5,
+ no_show_count: 3,
+ total_shifts: 89,
+ },
+ {
+ id: "staff-004",
+ employee_name: "James Wilson",
+ position: "Security Officer",
+ photo: "https://i.pravatar.cc/150?u=staff-004",
+ photo_url: "https://i.pravatar.cc/150?u=staff-004",
+ profile_type: "Senior",
+ email: "james.wilson@example.com",
+ contact_number: "+1 (555) 456-7890",
+ status: "Active",
+ skills: ["Security", "Safety"],
+ department: "Security",
+ hub_location: "Miami",
+ averageRating: 4.9,
+ reliability_score: 99,
+ shift_coverage_percentage: 100,
+ vendor_id: "vendor-001",
+ vendor_name: "Staff Agency Pro",
+ created_by: "vendor@staffagency.com",
+ created_date: new Date().toISOString(),
+ last_active: new Date(Date.now() - 1000 * 60 * 2).toISOString(),
+ employment_type: "Full-time",
+ manager: "Patricia Davis",
+ cancellation_count: 0,
+ no_show_count: 0,
+ total_shifts: 198,
+ },
+ {
+ id: "staff-005",
+ employee_name: "Lisa Anderson",
+ position: "HR Specialist",
+ photo: "https://i.pravatar.cc/150?u=staff-005",
+ photo_url: "https://i.pravatar.cc/150?u=staff-005",
+ profile_type: "Intermediate",
+ email: "lisa.anderson@example.com",
+ contact_number: "+1 (555) 567-8901",
+ status: "Suspended",
+ skills: ["HR", "Recruitment"],
+ department: "Human Resources",
+ hub_location: "New York",
+ averageRating: 4.5,
+ reliability_score: 91,
+ shift_coverage_percentage: 92,
+ vendor_id: "vendor-001",
+ vendor_name: "Staff Agency Pro",
+ created_by: "vendor@staffagency.com",
+ created_date: new Date().toISOString(),
+ last_active: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30).toISOString(),
+ employment_type: "Full-time",
+ manager: "John Smith",
+ cancellation_count: 1,
+ no_show_count: 0,
+ total_shifts: 167,
+ },
+ {
+ id: "staff-006",
+ employee_name: "David Martinez",
+ position: "Data Analyst",
+ photo: "https://i.pravatar.cc/150?u=staff-006",
+ photo_url: "https://i.pravatar.cc/150?u=staff-006",
+ profile_type: "Senior",
+ email: "david.martinez@example.com",
+ contact_number: "+1 (555) 678-9012",
+ status: "Active",
+ skills: ["Data Analysis", "Reporting", "SQL"],
+ department: "Analytics",
+ hub_location: "San Francisco",
+ averageRating: 4.7,
+ reliability_score: 93,
+ shift_coverage_percentage: 87,
+ vendor_id: "vendor-001",
+ vendor_name: "Staff Agency Pro",
+ created_by: "vendor@staffagency.com",
+ created_date: new Date().toISOString(),
+ last_active: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5).toISOString(),
+ employment_type: "Contract",
+ manager: "Michael Thompson",
+ cancellation_count: 1,
+ no_show_count: 1,
+ total_shifts: 134,
+ },
+ {
+ id: "staff-007",
+ employee_name: "Jessica Lee",
+ position: "Project Manager",
+ photo: "https://i.pravatar.cc/150?u=staff-007",
+ photo_url: "https://i.pravatar.cc/150?u=staff-007",
+ profile_type: "Senior",
+ email: "jessica.lee@example.com",
+ contact_number: "+1 (555) 789-0123",
+ status: "Active",
+ skills: ["Project Management", "Agile"],
+ department: "Projects",
+ hub_location: "Boston",
+ averageRating: 4.4,
+ reliability_score: 85,
+ shift_coverage_percentage: 79,
+ vendor_id: "vendor-001",
+ vendor_name: "Staff Agency Pro",
+ created_by: "vendor@staffagency.com",
+ created_date: new Date().toISOString(),
+ last_active: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString(),
+ employment_type: "Full-time",
+ manager: "Sarah Johnson",
+ cancellation_count: 3,
+ no_show_count: 1,
+ total_shifts: 142,
+ },
+ {
+ id: "staff-008",
+ employee_name: "Kevin Thompson",
+ position: "Business Analyst",
+ photo: "https://i.pravatar.cc/150?u=staff-008",
+ photo_url: "https://i.pravatar.cc/150?u=staff-008",
+ profile_type: "Intermediate",
+ email: "kevin.thompson@example.com",
+ contact_number: "+1 (555) 890-1234",
+ status: "Pending",
+ skills: ["Business Analysis", "Strategy"],
+ department: "Strategy",
+ hub_location: "Austin",
+ averageRating: 4.2,
+ reliability_score: 68,
+ shift_coverage_percentage: 72,
+ vendor_id: "vendor-001",
+ vendor_name: "Staff Agency Pro",
+ created_by: "vendor@staffagency.com",
+ created_date: new Date().toISOString(),
+ last_active: new Date(Date.now() - 1000 * 60 * 60 * 24 * 14).toISOString(),
+ employment_type: "Part-time",
+ manager: "Robert Brown",
+ cancellation_count: 6,
+ no_show_count: 2,
+ total_shifts: 95,
+ },
+ {
+ id: "staff-009",
+ employee_name: "Nicole White",
+ position: "Marketing Manager",
+ photo: "https://i.pravatar.cc/150?u=staff-009",
+ photo_url: "https://i.pravatar.cc/150?u=staff-009",
+ profile_type: "Senior",
+ email: "nicole.white@example.com",
+ contact_number: "+1 (555) 901-2345",
+ status: "Active",
+ skills: ["Marketing", "Branding"],
+ department: "Marketing",
+ hub_location: "Seattle",
+ averageRating: 4.6,
+ reliability_score: 89,
+ shift_coverage_percentage: 86,
+ vendor_id: "vendor-001",
+ vendor_name: "Staff Agency Pro",
+ created_by: "vendor@staffagency.com",
+ created_date: new Date().toISOString(),
+ last_active: new Date(Date.now() - 1000 * 60 * 60 * 24 * 4).toISOString(),
+ employment_type: "Full-time",
+ manager: "Patricia Davis",
+ cancellation_count: 2,
+ no_show_count: 0,
+ total_shifts: 178,
+ },
+] as unknown as Staff[];
+
+const mockEvents: Event[] = [
+ {
+ id: "event-001",
+ business_name: "Event Co.",
+ client_email: "client@eventco.com",
+ created_by: "client@eventco.com",
+ assigned_staff: [
+ { staff_id: "staff-001" },
+ { staff_id: "staff-004" },
+ ],
+ },
+ {
+ id: "event-002",
+ business_name: "Event Co.",
+ client_email: "client@eventco.com",
+ created_by: "client@eventco.com",
+ assigned_staff: [
+ { staff_id: "staff-002" },
+ { staff_id: "staff-009" },
+ ],
+ },
+];
+
+/**
+ * Simulates API delay for realistic behavior
+ */
+const delay = (ms: number = 500) =>
+ new Promise((resolve) => setTimeout(resolve, ms));
+
+/**
+ * Workforce Service - Mock implementation
+ */
+export const workforceService = {
+ auth: {
+ /**
+ * Get current user (mocked)
+ * In production, this would verify Firebase auth session
+ */
+ me: async (): Promise => {
+ await delay(800);
+ // Return a random user for demonstration
+ const users = Object.values(mockUsers);
+ return users[Math.floor(Math.random() * users.length)];
+ },
+
+ /**
+ * Sign out user (mocked)
+ */
+ logout: async (): Promise => {
+ await delay(300);
+ console.log("User logged out (mock)");
+ },
+ },
+
+ entities: {
+ Staff: {
+ /**
+ * List all staff members
+ * @param sortBy - Sort field (e.g., '-created_date' for descending)
+ */
+ list: async (sortBy?: string): Promise => {
+ await delay(1200);
+
+ const staffList = [...mockStaff];
+
+ // Simple sorting logic
+ if (sortBy === "-created_date") {
+ staffList.sort(
+ (a, b) =>
+ new Date(b.created_date || 0).getTime() -
+ new Date(a.created_date || 0).getTime()
+ );
+ } else if (sortBy === "created_date") {
+ staffList.sort(
+ (a, b) =>
+ new Date(a.created_date || 0).getTime() -
+ new Date(b.created_date || 0).getTime()
+ );
+ }
+
+ return staffList;
+ },
+
+ /**
+ * Get single staff member by ID
+ */
+ get: async (id: string): Promise => {
+ await delay(600);
+ return mockStaff.find((s) => s.id === id) || null;
+ },
+
+ /**
+ * Create new staff member
+ */
+ create: async (staff: Partial): Promise => {
+ await delay(1000);
+ const newStaff: Staff = {
+ ...staff,
+ id: `staff-${Date.now()}`,
+ created_date: new Date().toISOString(),
+ } as Staff;
+ mockStaff.push(newStaff);
+ return newStaff;
+ },
+
+ /**
+ * Update staff member
+ */
+ update: async (id: string, updates: Partial): Promise => {
+ await delay(800);
+ const index = mockStaff.findIndex((s) => s.id === id);
+ if (index === -1) throw new Error("Staff not found");
+ mockStaff[index] = { ...mockStaff[index], ...updates };
+ return mockStaff[index];
+ },
+
+ /**
+ * Delete staff member
+ */
+ delete: async (id: string): Promise => {
+ await delay(600);
+ const index = mockStaff.findIndex((s) => s.id === id);
+ if (index === -1) throw new Error("Staff not found");
+ mockStaff.splice(index, 1);
+ },
+ },
+
+ Event: {
+ /**
+ * List all events
+ */
+ list: async (): Promise => {
+ await delay(1000);
+ return [...mockEvents];
+ },
+
+ /**
+ * Get single event by ID
+ */
+ get: async (id: string): Promise => {
+ await delay(600);
+ return mockEvents.find((e) => e.id === id) || null;
+ },
+ },
+ },
+};
+
+export default workforceService;
diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts
new file mode 100644
index 00000000..da4786cc
--- /dev/null
+++ b/apps/web/tailwind.config.ts
@@ -0,0 +1,101 @@
+import type { Config } from 'tailwindcss';
+
+export default {
+ darkMode: ['class'],
+ content: [
+ './index.html',
+ './src/**/*.{js,ts,jsx,tsx}',
+ ],
+ theme: {
+ extend: {
+ fontFamily: {
+ body: ['Instrument Sans', 'sans-serif'],
+ headline: ['Instrument Sans', 'sans-serif'],
+ code: ['monospace'],
+ },
+ colors: {
+ background: 'hsl(var(--background))',
+ foreground: 'hsl(var(--foreground))',
+ card: {
+ DEFAULT: 'hsl(var(--card))',
+ foreground: 'hsl(var(--card-foreground))',
+ },
+ popover: {
+ DEFAULT: 'hsl(var(--popover))',
+ foreground: 'hsl(var(--popover-foreground))',
+ },
+ primary: {
+ DEFAULT: 'hsl(var(--primary))',
+ foreground: 'hsl(var(--primary-foreground))',
+ },
+ secondary: {
+ DEFAULT: 'hsl(var(--secondary))',
+ foreground: 'hsl(var(--secondary-foreground))',
+ },
+ muted: {
+ DEFAULT: 'hsl(var(--muted))',
+ foreground: 'hsl(var(--muted-foreground))',
+ },
+ accent: {
+ DEFAULT: 'hsl(var(--accent))',
+ foreground: 'hsl(var(--accent-foreground))',
+ },
+ destructive: {
+ DEFAULT: 'hsl(var(--destructive))',
+ foreground: 'hsl(var(--destructive-foreground))',
+ },
+ border: 'hsl(var(--border))',
+ input: 'hsl(var(--input))',
+ ring: 'hsl(var(--ring))',
+ chart: {
+ '1': 'hsl(var(--chart-1))',
+ '2': 'hsl(var(--chart-2))',
+ '3': 'hsl(var(--chart-3))',
+ '4': 'hsl(var(--chart-4))',
+ '5': 'hsl(var(--chart-5))',
+ },
+ sidebar: {
+ DEFAULT: 'hsl(var(--sidebar-background))',
+ foreground: 'hsl(var(--sidebar-foreground))',
+ primary: 'hsl(var(--sidebar-primary))',
+ 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
+ accent: 'hsl(var(--sidebar-accent))',
+ 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
+ border: 'hsl(var(--sidebar-border))',
+ ring: 'hsl(var(--sidebar-ring))',
+ },
+ 'primary-text': 'hsl(var(--text-primary))',
+ 'secondary-text': 'hsl(var(--text-secondary))',
+ 'muted-text': 'hsl(var(--text-muted))',
+ },
+ borderRadius: {
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
+ sm: 'calc(var(--radius) - 4px)',
+ },
+ keyframes: {
+ 'accordion-down': {
+ from: {
+ height: '0',
+ },
+ to: {
+ height: 'var(--radix-accordion-content-height)',
+ },
+ },
+ 'accordion-up': {
+ from: {
+ height: 'var(--radix-accordion-content-height)',
+ },
+ to: {
+ height: '0',
+ },
+ },
+ },
+ animation: {
+ 'accordion-down': 'accordion-down 0.2s ease-out',
+ 'accordion-up': 'accordion-up 0.2s ease-out',
+ },
+ },
+ },
+ plugins: [require('tailwindcss-animate')],
+} satisfies Config;
\ No newline at end of file
diff --git a/apps/web/tsconfig.app.json b/apps/web/tsconfig.app.json
index a9b5a59c..ea64ed3b 100644
--- a/apps/web/tsconfig.app.json
+++ b/apps/web/tsconfig.app.json
@@ -16,6 +16,12 @@
"noEmit": true,
"jsx": "react-jsx",
+ /* Path mapping */
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ },
+
/* Linting */
"strict": true,
"noUnusedLocals": true,
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index c4069b77..641af15a 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -1,8 +1,15 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
+import path from 'path'
+
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
})