feat: Implement Staff Detail View
This commit is contained in:
@@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import AppRoutes from './routes';
|
||||
import { store } from './store/store';
|
||||
import { initializeAuthPersistence } from './services/authService';
|
||||
import AuthInitializer from './features/auth/AuthInitializer';
|
||||
|
||||
// Initialize the QueryClient
|
||||
const queryClient = new QueryClient();
|
||||
@@ -13,13 +14,16 @@ initializeAuthPersistence();
|
||||
|
||||
/**
|
||||
* Root Application Component.
|
||||
* Wraps the app with Redux Provider and React Query Provider.
|
||||
* Wraps the app with Redux Provider, React Query Provider, and AuthInitializer.
|
||||
* AuthInitializer ensures auth state is restored from persistence before routes are rendered.
|
||||
*/
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthInitializer>
|
||||
<AppRoutes />
|
||||
</AuthInitializer>
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
44
apps/web/src/features/auth/AuthInitializer.tsx
Normal file
44
apps/web/src/features/auth/AuthInitializer.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { checkAuthStatus } from './authSlice';
|
||||
import type { RootState, AppDispatch } from '../../store/store';
|
||||
|
||||
interface AuthInitializerProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* AuthInitializer Component
|
||||
* Initializes authentication state from Firebase persistence on app load
|
||||
* Shows a loading screen until the initial auth check is complete
|
||||
* This prevents premature redirect to login before the persisted session is restored
|
||||
*/
|
||||
const AuthInitializer: React.FC<AuthInitializerProps> = ({ children }) => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const { isInitialized } = useSelector((state: RootState) => state.auth);
|
||||
|
||||
useEffect(() => {
|
||||
// Perform initial auth check when component mounts
|
||||
// This restores the persisted session from Firebase
|
||||
dispatch(checkAuthStatus());
|
||||
}, [dispatch]);
|
||||
|
||||
// Show loading state while initializing auth
|
||||
if (!isInitialized) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen bg-slate-50">
|
||||
<div className="text-center">
|
||||
<div className="inline-block">
|
||||
<div className="w-8 h-8 border-4 border-slate-200 border-t-primary rounded-full animate-spin"></div>
|
||||
</div>
|
||||
<p className="mt-4 text-slate-600">Loading application...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default AuthInitializer;
|
||||
@@ -17,6 +17,7 @@ interface AuthState {
|
||||
isAuthenticated: boolean;
|
||||
status: "idle" | "loading" | "succeeded" | "failed";
|
||||
error: string | null;
|
||||
isInitialized: boolean; // Track whether initial auth check has completed
|
||||
}
|
||||
|
||||
const initialState: AuthState = {
|
||||
@@ -24,6 +25,7 @@ const initialState: AuthState = {
|
||||
isAuthenticated: false,
|
||||
status: "idle",
|
||||
error: null,
|
||||
isInitialized: false, // Start as false until initial auth check completes
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -154,6 +156,9 @@ const authSlice = createSlice({
|
||||
|
||||
// Check auth status thunk
|
||||
builder
|
||||
.addCase(checkAuthStatus.pending, (state) => {
|
||||
state.status = "loading";
|
||||
})
|
||||
.addCase(checkAuthStatus.fulfilled, (state, action) => {
|
||||
if (action.payload) {
|
||||
state.user = action.payload;
|
||||
@@ -163,6 +168,11 @@ const authSlice = createSlice({
|
||||
state.isAuthenticated = false;
|
||||
}
|
||||
state.status = "idle";
|
||||
state.isInitialized = true; // Mark initialization as complete
|
||||
})
|
||||
.addCase(checkAuthStatus.rejected, (state) => {
|
||||
state.isInitialized = true; // Mark initialization as complete even on error
|
||||
state.isAuthenticated = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ export default function EditStaff() {
|
||||
const [staff, setStaff] = useState<Staff | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStaff = async () => {
|
||||
@@ -41,7 +42,8 @@ export default function EditStaff() {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await workforceService.entities.Staff.update(id, staffData);
|
||||
navigate("/staff");
|
||||
setStaff(staffData);
|
||||
setIsEditing(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to update staff", error);
|
||||
} finally {
|
||||
@@ -49,6 +51,10 @@ export default function EditStaff() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[60vh] gap-4">
|
||||
@@ -60,7 +66,7 @@ export default function EditStaff() {
|
||||
|
||||
return (
|
||||
<DashboardLayout
|
||||
title={`Edit: ${staff?.employee_name || 'Staff Member'}`}
|
||||
title={isEditing ? `Edit: ${staff?.employee_name || 'Staff Member'}` : staff?.employee_name || 'Staff Member'}
|
||||
subtitle={`Management of ${staff?.employee_name}'s professional records`}
|
||||
backAction={
|
||||
<Button
|
||||
@@ -74,11 +80,42 @@ export default function EditStaff() {
|
||||
}
|
||||
>
|
||||
{staff && (
|
||||
<div>
|
||||
{!isEditing && (
|
||||
<div className="mb-6 flex justify-end">
|
||||
<Button onClick={() => setIsEditing(true)} variant="secondary">
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isEditing && (
|
||||
<div className="mb-6 flex justify-end gap-2">
|
||||
<Button
|
||||
onClick={handleCancel}
|
||||
variant="outline"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
// Trigger form submission by dispatching event or calling form submit
|
||||
const form = document.querySelector('form');
|
||||
if (form) form.requestSubmit();
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<StaffForm
|
||||
staff={staff}
|
||||
onSubmit={handleSubmit}
|
||||
isSubmitting={isSubmitting}
|
||||
disabled={!isEditing}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</DashboardLayout>
|
||||
);
|
||||
|
||||
@@ -7,9 +7,9 @@ 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
|
||||
Save, Loader2, User, FileText, Briefcase,
|
||||
Calendar, ChevronRight, Shield, Star, Info,
|
||||
Mail, Phone, MapPin, Award, Clock, AlertCircle
|
||||
} from "lucide-react";
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
import type { Staff } from "../../type";
|
||||
@@ -19,12 +19,13 @@ interface StaffFormProps {
|
||||
staff?: Staff;
|
||||
onSubmit: (data: Omit<Staff, 'id'>) => Promise<void>;
|
||||
isSubmitting: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
type TabType = "general" | "performance" | "location" | "additional";
|
||||
type TabType = "overview" | "documents" | "work_history" | "compliance";
|
||||
|
||||
export default function StaffForm({ staff, onSubmit, isSubmitting }: StaffFormProps) {
|
||||
const [activeTab, setActiveTab] = useState<TabType>("general");
|
||||
export default function StaffForm({ staff, onSubmit, isSubmitting, disabled = false }: StaffFormProps) {
|
||||
const [activeTab, setActiveTab] = useState<TabType>("overview");
|
||||
|
||||
const { register, handleSubmit, control, reset } = useForm<Staff>({
|
||||
defaultValues: staff || {
|
||||
@@ -55,7 +56,7 @@ export default function StaffForm({ staff, onSubmit, isSubmitting }: StaffFormPr
|
||||
action: "",
|
||||
notes: "",
|
||||
accounting_comments: "",
|
||||
rating: 0,
|
||||
averageRating: 0,
|
||||
shift_coverage_percentage: 100,
|
||||
cancellation_count: 0,
|
||||
no_show_count: 0,
|
||||
@@ -75,10 +76,10 @@ export default function StaffForm({ staff, onSubmit, isSubmitting }: StaffFormPr
|
||||
};
|
||||
|
||||
const tabs: { id: TabType; label: string; icon: React.ReactNode }[] = [
|
||||
{ id: "general", label: "General Info", icon: <User className="w-4 h-4" /> },
|
||||
{ id: "performance", label: "Performance", icon: <Activity className="w-4 h-4" /> },
|
||||
{ id: "location", label: "Location", icon: <MapPin className="w-4 h-4" /> },
|
||||
{ id: "additional", label: "Other", icon: <Info className="w-4 h-4" /> },
|
||||
{ id: "overview", label: "Overview", icon: <User className="w-4 h-4" /> },
|
||||
{ id: "documents", label: "Documents", icon: <FileText className="w-4 h-4" /> },
|
||||
{ id: "work_history", label: "Work History", icon: <Briefcase className="w-4 h-4" /> },
|
||||
{ id: "compliance", label: "Compliance", icon: <Shield className="w-4 h-4" /> },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -125,22 +126,22 @@ export default function StaffForm({ staff, onSubmit, isSubmitting }: StaffFormPr
|
||||
{/* Content Area */}
|
||||
<div className="flex-1 space-y-6">
|
||||
<AnimatePresence mode="wait">
|
||||
{activeTab === "general" && (
|
||||
{activeTab === "overview" && (
|
||||
<TabContent
|
||||
id="general"
|
||||
title="Basic Information"
|
||||
id="overview"
|
||||
title="Staff Overview"
|
||||
icon={<User className="w-5 h-5" />}
|
||||
className="p-8 grid grid-cols-1 md:grid-cols-2 gap-8"
|
||||
className="p-8 space-y-8"
|
||||
footer={
|
||||
<div className="bg-primary/5 p-6 rounded-3xl border border-primary/10 flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-bold text-foreground">Next Step: Performance</h4>
|
||||
<p className="text-sm text-muted-foreground">Define reliability and coverage metrics.</p>
|
||||
<h4 className="font-bold text-foreground">Next Step: Documents</h4>
|
||||
<p className="text-sm text-muted-foreground">Upload and manage staff documentation.</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setActiveTab("performance")}
|
||||
onClick={() => setActiveTab("documents")}
|
||||
className="hover:bg-primary/10 hover:text-primary rounded-xl font-bold transition-premium"
|
||||
>
|
||||
Continue
|
||||
@@ -149,10 +150,18 @@ export default function StaffForm({ staff, onSubmit, isSubmitting }: StaffFormPr
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{/* Basic Information */}
|
||||
<div>
|
||||
<h3 className="text-sm font-black text-muted-foreground/80 mb-4 flex items-center gap-2">
|
||||
<User className="w-4 h-4" />
|
||||
BASIC INFORMATION
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Name <span className="text-rose-500">*</span></Label>
|
||||
<Input
|
||||
{...register("employee_name", { required: true })}
|
||||
disabled={disabled}
|
||||
className="font-medium"
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
@@ -162,20 +171,53 @@ export default function StaffForm({ staff, onSubmit, isSubmitting }: StaffFormPr
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Initials</Label>
|
||||
<Input
|
||||
{...register("initial")}
|
||||
disabled={disabled}
|
||||
maxLength={3}
|
||||
className="font-medium "
|
||||
className="font-medium"
|
||||
placeholder="JD"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Email</Label>
|
||||
<Input
|
||||
{...register("email")}
|
||||
disabled={disabled}
|
||||
type="email"
|
||||
leadingIcon={<Mail />}
|
||||
className="font-medium"
|
||||
placeholder="j.doe@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Contact Number</Label>
|
||||
<Input
|
||||
{...register("contact_number")}
|
||||
disabled={disabled}
|
||||
leadingIcon={<Phone />}
|
||||
className="font-medium"
|
||||
placeholder="+1 (555) 000-0000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skills & Position */}
|
||||
<div>
|
||||
<h3 className="text-sm font-black text-muted-foreground/80 mb-4 flex items-center gap-2">
|
||||
<Award className="w-4 h-4" />
|
||||
SKILLS & POSITION
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Primary Skill</Label>
|
||||
<Controller
|
||||
name="position"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="font-medium">
|
||||
<Select onValueChange={disabled ? undefined : field.onChange} value={field.value} disabled={disabled}>
|
||||
<SelectTrigger className="font-medium" disabled={disabled}>
|
||||
<SelectValue placeholder="Select primary skill" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl glass">
|
||||
@@ -191,14 +233,24 @@ export default function StaffForm({ staff, onSubmit, isSubmitting }: StaffFormPr
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Secondary Skill</Label>
|
||||
<Input
|
||||
{...register("position_2")}
|
||||
disabled={disabled}
|
||||
className="font-medium"
|
||||
placeholder="Additional skills"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Skill Level</Label>
|
||||
<Controller
|
||||
name="profile_type"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="font-medium">
|
||||
<Select onValueChange={disabled ? undefined : field.onChange} value={field.value} disabled={disabled}>
|
||||
<SelectTrigger className="font-medium" disabled={disabled}>
|
||||
<SelectValue placeholder="Select level" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl glass">
|
||||
@@ -211,34 +263,14 @@ export default function StaffForm({ staff, onSubmit, isSubmitting }: StaffFormPr
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Email</Label>
|
||||
<Input
|
||||
{...register("email")}
|
||||
type="email"
|
||||
leadingIcon={<FileText />}
|
||||
className="font-medium"
|
||||
placeholder="j.doe@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Contact Number</Label>
|
||||
<Input
|
||||
{...register("contact_number")}
|
||||
className="font-medium"
|
||||
placeholder="+1 (555) 000-0000"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Employment Type</Label>
|
||||
<Controller
|
||||
name="employment_type"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="font-medium">
|
||||
<Select onValueChange={disabled ? undefined : field.onChange} value={field.value} disabled={disabled}>
|
||||
<SelectTrigger className="font-medium" disabled={disabled}>
|
||||
<SelectValue placeholder="Select type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl glass">
|
||||
@@ -251,138 +283,79 @@ export default function StaffForm({ staff, onSubmit, isSubmitting }: StaffFormPr
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Reporting Manager</Label>
|
||||
<Controller
|
||||
name="manager"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="font-medium">
|
||||
<SelectValue placeholder="Select manager" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl glass">
|
||||
<SelectItem value="Fernando">Fernando</SelectItem>
|
||||
<SelectItem value="Maria">Maria</SelectItem>
|
||||
<SelectItem value="Paola">Paola</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</TabContent>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{activeTab === "performance" && (
|
||||
<TabContent
|
||||
id="performance"
|
||||
title="Performance Metrics"
|
||||
icon={<Activity className="w-5 h-5" />}
|
||||
className="p-8 space-y-8"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Rating & Performance Overview */}
|
||||
<div>
|
||||
<h3 className="text-sm font-black text-muted-foreground/80 mb-4 flex items-center gap-2">
|
||||
<Star className="w-4 h-4" />
|
||||
RATING & PERFORMANCE
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="p-6 bg-amber-50 dark:bg-amber-950/20 rounded-2xl border border-amber-200 dark:border-amber-900">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Star className="w-4 h-4 text-amber-500" />
|
||||
<Label className="text-xs font-black text-muted-foreground/80">Rating (0-5)</Label>
|
||||
<Label className="text-xs font-black text-amber-700 dark:text-amber-400">Average Rating</Label>
|
||||
</div>
|
||||
<Input
|
||||
{...register("rating", { valueAsNumber: true })}
|
||||
{...register("averageRating", { valueAsNumber: true })}
|
||||
disabled={disabled}
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="5"
|
||||
className="h-14 rounded-2xl text-2xl font-black text-center"
|
||||
className="h-12 rounded-xl text-xl font-black text-center bg-white dark:bg-amber-950/40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-6 bg-emerald-50 dark:bg-emerald-950/20 rounded-2xl border border-emerald-200 dark:border-emerald-900">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Shield className="w-4 h-4 text-emerald-500" />
|
||||
<Label className="text-xs font-black text-muted-foreground/80">Reliability %</Label>
|
||||
<Label className="text-xs font-black text-emerald-700 dark:text-emerald-400">Reliability Score</Label>
|
||||
</div>
|
||||
<Input
|
||||
{...register("reliability_score", { valueAsNumber: true })}
|
||||
disabled={disabled}
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
className="h-14 rounded-2xl text-2xl font-black text-center"
|
||||
className="h-12 rounded-xl text-xl font-black text-center bg-white dark:bg-emerald-950/40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4 text-primary" />
|
||||
<Label className="text-xs font-black text-muted-foreground/80">Coverage %</Label>
|
||||
<div className="p-6 bg-blue-50 dark:bg-blue-950/20 rounded-2xl border border-blue-200 dark:border-blue-900">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<TrendingUp className="w-4 h-4 text-blue-500" />
|
||||
<Label className="text-xs font-black text-blue-700 dark:text-blue-400">Coverage %</Label>
|
||||
</div>
|
||||
<Input
|
||||
{...register("shift_coverage_percentage", { valueAsNumber: true })}
|
||||
disabled={disabled}
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
className="h-14 rounded-2xl text-2xl font-black text-center"
|
||||
className="h-12 rounded-xl text-xl font-black text-center bg-white dark:bg-blue-950/40"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Cancellations</Label>
|
||||
<Input
|
||||
{...register("cancellation_count", { valueAsNumber: true })}
|
||||
type="number"
|
||||
className="font-medium"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">No Shows</Label>
|
||||
<Input
|
||||
{...register("no_show_count", { valueAsNumber: true })}
|
||||
type="number"
|
||||
className="font-medium"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-secondary/20 rounded-2xl border border-border/40 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Controller
|
||||
name="invoiced"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
id="invoiced"
|
||||
checked={field.value}
|
||||
onChange={(e) => field.onChange(e.target.checked)}
|
||||
className="w-6 h-6 rounded-lg border-2 border-primary/20 data-[state=checked]:bg-primary data-[state=checked]:border-primary"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{/* Location & Department */}
|
||||
<div>
|
||||
<Label htmlFor="invoiced" className="font-black text-foreground cursor-pointer">Verified Invoicing</Label>
|
||||
<p className="text-xs text-muted-foreground font-medium">Mark this member as verified for automatic invoicing.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabContent>
|
||||
)}
|
||||
|
||||
{activeTab === "location" && (
|
||||
<TabContent
|
||||
id="location"
|
||||
title="Deployment & Location"
|
||||
icon={<MapPin className="w-5 h-5" />}
|
||||
className="p-8 grid grid-cols-1 md:grid-cols-2 gap-8"
|
||||
>
|
||||
<h3 className="text-sm font-black text-muted-foreground/80 mb-4 flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4" />
|
||||
LOCATION & DEPARTMENT
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80">Department</Label>
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Department</Label>
|
||||
<Controller
|
||||
name="department"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger>
|
||||
<Select onValueChange={disabled ? undefined : field.onChange} value={field.value} disabled={disabled}>
|
||||
<SelectTrigger disabled={disabled}>
|
||||
<SelectValue placeholder="Select department" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl glass">
|
||||
@@ -397,39 +370,338 @@ export default function StaffForm({ staff, onSubmit, isSubmitting }: StaffFormPr
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80">Primary City</Label>
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Primary City</Label>
|
||||
<Input
|
||||
{...register("city")}
|
||||
disabled={disabled}
|
||||
className="font-medium"
|
||||
placeholder="e.g., San Francisco"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80">Hub Site</Label>
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Hub Location</Label>
|
||||
<Input
|
||||
{...register("hub_location")}
|
||||
disabled={disabled}
|
||||
className="font-medium"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80">Detailed Address</Label>
|
||||
<Textarea
|
||||
{...register("address")}
|
||||
className="font-medium"
|
||||
placeholder="Street address, building, etc."
|
||||
placeholder="Primary hub site"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80">English Fluency</Label>
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Reporting Manager</Label>
|
||||
<Controller
|
||||
name="manager"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select onValueChange={disabled ? undefined : field.onChange} value={field.value} disabled={disabled}>
|
||||
<SelectTrigger className="font-medium" disabled={disabled}>
|
||||
<SelectValue placeholder="Select manager" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl glass">
|
||||
<SelectItem value="Fernando">Fernando</SelectItem>
|
||||
<SelectItem value="Maria">Maria</SelectItem>
|
||||
<SelectItem value="Paola">Paola</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabContent>
|
||||
)}
|
||||
|
||||
{activeTab === "documents" && (
|
||||
<TabContent
|
||||
id="documents"
|
||||
title="Staff Documents"
|
||||
icon={<FileText className="w-5 h-5" />}
|
||||
className="p-8 space-y-8"
|
||||
>
|
||||
{/* Document Upload Section */}
|
||||
<div>
|
||||
<h3 className="text-sm font-black text-muted-foreground/80 mb-4 flex items-center gap-2">
|
||||
<FileText className="w-4 h-4" />
|
||||
REQUIRED DOCUMENTS
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="p-6 bg-secondary/20 rounded-2xl border border-border/40">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label className="font-bold text-foreground">I-9 Form</Label>
|
||||
<span className="text-xs px-2 py-1 bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 rounded-lg font-bold">
|
||||
Pending
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Employment eligibility verification</p>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-secondary/20 rounded-2xl border border-border/40">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label className="font-bold text-foreground">W-4 Form</Label>
|
||||
<span className="text-xs px-2 py-1 bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 rounded-lg font-bold">
|
||||
Pending
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Tax withholding information</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Certifications */}
|
||||
<div>
|
||||
<h3 className="text-sm font-black text-muted-foreground/80 mb-4 flex items-center gap-2">
|
||||
<Award className="w-4 h-4" />
|
||||
CERTIFICATIONS
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="p-6 bg-secondary/20 rounded-2xl border border-border/40">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label className="font-bold text-foreground">Food Handler Certificate</Label>
|
||||
<span className="text-xs px-2 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 rounded-lg font-bold">
|
||||
Valid
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Expires: 12/31/2026</p>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-secondary/20 rounded-2xl border border-border/40">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label className="font-bold text-foreground">Additional Certifications</Label>
|
||||
<Button type="button" variant="outline" size="sm" disabled={disabled}>
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Add any additional certifications or licenses</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80">Document Notes</Label>
|
||||
<Textarea
|
||||
{...register("notes")}
|
||||
disabled={disabled}
|
||||
placeholder="Notes about uploaded documents, missing items, or follow-ups needed..."
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
</div>
|
||||
</TabContent>
|
||||
)}
|
||||
|
||||
{activeTab === "work_history" && (
|
||||
<TabContent
|
||||
id="work_history"
|
||||
title="Work History"
|
||||
icon={<Briefcase className="w-5 h-5" />}
|
||||
className="p-8 space-y-8"
|
||||
>
|
||||
{/* Shift Statistics */}
|
||||
<div>
|
||||
<h3 className="text-sm font-black text-muted-foreground/80 mb-4 flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
SHIFT STATISTICS
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="p-6 bg-secondary/20 rounded-2xl border border-border/40">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 mb-2 block">Total Shifts</Label>
|
||||
<Input
|
||||
{...register("total_shifts", { valueAsNumber: true })}
|
||||
disabled={disabled}
|
||||
type="number"
|
||||
className="h-12 rounded-xl text-xl font-black text-center"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-secondary/20 rounded-2xl border border-border/40">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 mb-2 block">Cancellations</Label>
|
||||
<Input
|
||||
{...register("cancellation_count", { valueAsNumber: true })}
|
||||
disabled={disabled}
|
||||
type="number"
|
||||
className="h-12 rounded-xl text-xl font-black text-center"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-secondary/20 rounded-2xl border border-border/40">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 mb-2 block">No Shows</Label>
|
||||
<Input
|
||||
{...register("no_show_count", { valueAsNumber: true })}
|
||||
disabled={disabled}
|
||||
type="number"
|
||||
className="h-12 rounded-xl text-xl font-black text-center"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Schedule Information */}
|
||||
<div>
|
||||
<h3 className="text-sm font-black text-muted-foreground/80 mb-4 flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
SCHEDULE INFORMATION
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Last Check-in</Label>
|
||||
<Input
|
||||
{...register("check_in")}
|
||||
disabled={disabled}
|
||||
type="date"
|
||||
leadingIcon={<Calendar />}
|
||||
className="font-medium"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Schedule Days</Label>
|
||||
<Input
|
||||
{...register("schedule_days")}
|
||||
disabled={disabled}
|
||||
className="font-medium"
|
||||
placeholder="e.g., M/W/F Only"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Event Location</Label>
|
||||
<Input
|
||||
{...register("event_location")}
|
||||
disabled={disabled}
|
||||
className="font-medium"
|
||||
placeholder="Primary event location"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Track</Label>
|
||||
<Input
|
||||
{...register("track")}
|
||||
disabled={disabled}
|
||||
className="font-medium"
|
||||
placeholder="Assignment track"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Assignment Details */}
|
||||
<div>
|
||||
<h3 className="text-sm font-black text-muted-foreground/80 mb-4 flex items-center gap-2">
|
||||
<Briefcase className="w-4 h-4" />
|
||||
ASSIGNMENT DETAILS
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Replaced By</Label>
|
||||
<Input
|
||||
{...register("replaced_by")}
|
||||
disabled={disabled}
|
||||
className="font-medium"
|
||||
placeholder="Staff member name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Action Status</Label>
|
||||
<Input
|
||||
{...register("action")}
|
||||
disabled={disabled}
|
||||
className="font-medium"
|
||||
placeholder="Current action status"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Work History Notes */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80">Work History Notes</Label>
|
||||
<Textarea
|
||||
{...register("accounting_comments")}
|
||||
disabled={disabled}
|
||||
placeholder="Notes about work history, patterns, or observations..."
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
</div>
|
||||
</TabContent>
|
||||
)}
|
||||
|
||||
{activeTab === "compliance" && (
|
||||
<TabContent
|
||||
id="compliance"
|
||||
title="Compliance & Verification"
|
||||
icon={<Shield className="w-5 h-5" />}
|
||||
className="p-8 space-y-8"
|
||||
>
|
||||
{/* Verification Status */}
|
||||
<div>
|
||||
<h3 className="text-sm font-black text-muted-foreground/80 mb-4 flex items-center gap-2">
|
||||
<Shield className="w-4 h-4" />
|
||||
VERIFICATION STATUS
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="p-6 bg-secondary/20 rounded-2xl border border-border/40 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Controller
|
||||
name="invoiced"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
id="invoiced"
|
||||
disabled={disabled}
|
||||
checked={field.value}
|
||||
onChange={(e) => field.onChange(e.target.checked)}
|
||||
className="w-6 h-6 rounded-lg border-2 border-primary/20 data-[state=checked]:bg-primary data-[state=checked]:border-primary"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<Label htmlFor="invoiced" className="font-black text-foreground cursor-pointer">Verified for Invoicing</Label>
|
||||
<p className="text-xs text-muted-foreground font-medium">Staff member approved for automatic invoicing</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-green-50 dark:bg-green-950/20 rounded-2xl border border-green-200 dark:border-green-900">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center">
|
||||
<Shield className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<Label className="font-bold text-green-900 dark:text-green-100">Background Check</Label>
|
||||
</div>
|
||||
<p className="text-xs text-green-700 dark:text-green-400">Completed & Approved</p>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-amber-50 dark:bg-amber-950/20 rounded-2xl border border-amber-200 dark:border-amber-900">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-8 h-8 rounded-full bg-amber-500 flex items-center justify-center">
|
||||
<AlertCircle className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<Label className="font-bold text-amber-900 dark:text-amber-100">Drug Screening</Label>
|
||||
</div>
|
||||
<p className="text-xs text-amber-700 dark:text-amber-400">Pending Review</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Language Requirements */}
|
||||
<div>
|
||||
<h3 className="text-sm font-black text-muted-foreground/80 mb-4 flex items-center gap-2">
|
||||
<Info className="w-4 h-4" />
|
||||
LANGUAGE REQUIREMENTS
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">English Proficiency</Label>
|
||||
<Controller
|
||||
name="english"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger>
|
||||
<Select onValueChange={disabled ? undefined : field.onChange} value={field.value} disabled={disabled}>
|
||||
<SelectTrigger disabled={disabled}>
|
||||
<SelectValue placeholder="Select level" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl glass">
|
||||
@@ -443,65 +715,88 @@ export default function StaffForm({ staff, onSubmit, isSubmitting }: StaffFormPr
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 pt-6">
|
||||
<div className="flex items-center gap-3 pt-8">
|
||||
<Controller
|
||||
name="english_required"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
id="english_required"
|
||||
disabled={disabled}
|
||||
checked={field.value}
|
||||
onChange={(e) => field.onChange(e.target.checked)}
|
||||
className="w-5 h-5"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Label htmlFor="english_required" className="text-sm font-bold text-foreground cursor-pointer">Required for role</Label>
|
||||
<Label htmlFor="english_required" className="text-sm font-bold text-foreground cursor-pointer">
|
||||
English Required for Role
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabContent>
|
||||
)}
|
||||
|
||||
{activeTab === "additional" && (
|
||||
<TabContent
|
||||
id="additional"
|
||||
title="Notes & Comments"
|
||||
icon={<Info className="w-5 h-5" />}
|
||||
className="p-8 space-y-6"
|
||||
>
|
||||
{/* Additional Compliance Fields */}
|
||||
<div>
|
||||
<h3 className="text-sm font-black text-muted-foreground/80 mb-4 flex items-center gap-2">
|
||||
<FileText className="w-4 h-4" />
|
||||
ADDITIONAL INFORMATION
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80">Staff Notes</Label>
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">RO Number</Label>
|
||||
<Input
|
||||
{...register("ro")}
|
||||
disabled={disabled}
|
||||
className="font-medium"
|
||||
placeholder="RO reference number"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">MON Reference</Label>
|
||||
<Input
|
||||
{...register("mon")}
|
||||
disabled={disabled}
|
||||
className="font-medium"
|
||||
placeholder="Monitoring reference"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Phone (Alt)</Label>
|
||||
<Input
|
||||
{...register("phone")}
|
||||
disabled={disabled}
|
||||
leadingIcon={<Phone />}
|
||||
className="font-medium"
|
||||
placeholder="Alternative contact"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Address</Label>
|
||||
<Input
|
||||
{...register("address")}
|
||||
disabled={disabled}
|
||||
leadingIcon={<MapPin />}
|
||||
className="font-medium"
|
||||
placeholder="Street address"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Compliance Notes */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80">Compliance Notes</Label>
|
||||
<Textarea
|
||||
{...register("notes")}
|
||||
placeholder="Internal notes about performance, behavior, etc."
|
||||
disabled={disabled}
|
||||
placeholder="Compliance-related notes, verification details, or follow-up items..."
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80">Accounting Comments</Label>
|
||||
<Textarea
|
||||
{...register("accounting_comments")}
|
||||
placeholder="Pay rates, bonus structures, or audit notes."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80">Last Check-in</Label>
|
||||
<Input
|
||||
{...register("check_in")}
|
||||
type="date"
|
||||
leadingIcon={<Calendar />}
|
||||
className="font-medium"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-black text-muted-foreground/80">Schedule Override</Label>
|
||||
<Input
|
||||
{...register("schedule_days")}
|
||||
className="font-medium"
|
||||
placeholder="e.g., M/W/F Only"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabContent>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
Reference in New Issue
Block a user