feat: Implement a form to add staff

This commit is contained in:
dhinesh-m24
2026-02-03 13:08:13 +05:30
parent 1ac38311ee
commit 8461a23adc

View File

@@ -1,38 +1,82 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Button } from "@/common/components/ui/button"; import { Button } from "@/common/components/ui/button";
import { ArrowLeft } from "lucide-react"; import { Input } from "@/common/components/ui/input";
import StaffForm from "./components/StaffForm"; import { Label } from "@/common/components/ui/label";
import { ArrowLeft, Loader2, Save, Mail, Phone, User, Award, ShieldAlert } from "lucide-react";
import DashboardLayout from "@/features/layouts/DashboardLayout"; import DashboardLayout from "@/features/layouts/DashboardLayout";
import { useCreateStaff } from "@/dataconnect-generated/react"; import { useCreateStaff, useGetUserById } from "@/dataconnect-generated/react";
import { dataConnect } from "@/features/auth/firebase"; import { dataConnect, auth } from "@/features/auth/firebase";
import type { Staff } from "../type"; import { useForm, Controller } from "react-hook-form";
import { Checkbox } from "@/common/components/ui/checkbox";
const COMMON_SKILLS = [
"Barista",
"Server",
"Cook",
"Dishwasher",
"Bartender",
"Manager"
];
interface AddStaffFormData {
firstName: string;
lastName: string;
email: string;
phone: string;
skills: string[];
}
export default function AddStaff() { export default function AddStaff() {
const navigate = useNavigate(); const navigate = useNavigate();
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
// Get current user and their role from DataConnect
const currentUser = auth.currentUser;
const { data: userData, isLoading: isUserLoading } = useGetUserById(
dataConnect,
{ id: currentUser?.uid || "" },
{ enabled: !!currentUser }
);
const { mutateAsync: createStaff } = useCreateStaff(dataConnect); const { mutateAsync: createStaff } = useCreateStaff(dataConnect);
const handleSubmit = async (staffData: Omit<Staff, 'id'>) => { const { register, handleSubmit, control, formState: { errors } } = useForm<AddStaffFormData>({
defaultValues: {
firstName: "",
lastName: "",
email: "",
phone: "",
skills: []
}
});
// Check for admin role
const isAdmin = userData?.user?.userRole?.toLowerCase() === 'admin';
useEffect(() => {
if (!isUserLoading && !isAdmin && currentUser) {
// Small delay to allow user to see why they are being redirected if needed,
// but usually immediate is better for security
const timer = setTimeout(() => {
navigate("/staff");
}, 2000);
return () => clearTimeout(timer);
}
}, [isAdmin, isUserLoading, navigate, currentUser]);
const onSubmit = async (data: AddStaffFormData) => {
if (!isAdmin) return;
setIsSubmitting(true); setIsSubmitting(true);
try { try {
await createStaff({ await createStaff({
userId: `user_${Math.random().toString(36).substr(2, 9)}`, // Placeholder UID userId: `user_${Math.random().toString(36).substring(2, 11)}`,
fullName: staffData.employee_name, fullName: `${data.firstName} ${data.lastName}`,
role: staffData.position, email: data.email,
level: staffData.profile_type, phone: data.phone,
email: staffData.email, skills: data.skills,
phone: staffData.contact_number, backgroundCheckStatus: "PENDING",
photoUrl: staffData.photo, initial: `${data.firstName.charAt(0)}${data.lastName.charAt(0)}`.toUpperCase(),
initial: staffData.initial,
bio: staffData.notes,
skills: staffData.skills,
averageRating: staffData.averageRating,
reliabilityScore: staffData.reliability_score,
onTimeRate: staffData.shift_coverage_percentage,
totalShifts: staffData.total_shifts,
city: staffData.city,
addres: staffData.address,
}); });
navigate("/staff"); navigate("/staff");
} catch (error) { } catch (error) {
@@ -42,10 +86,43 @@ export default function AddStaff() {
} }
}; };
if (isUserLoading) {
return (
<div className="flex flex-col items-center justify-center min-h-screen gap-4">
<Loader2 className="w-10 h-10 animate-spin text-primary" />
<p className="text-muted-foreground font-bold animate-pulse">VERIFYING PERMISSIONS...</p>
</div>
);
}
if (!isAdmin) {
return (
<div className="flex flex-col items-center justify-center min-h-screen gap-6 p-4 text-center">
<div className="w-20 h-20 bg-rose-500/10 rounded-3xl flex items-center justify-center border-2 border-rose-500/20 shadow-xl shadow-rose-500/5">
<ShieldAlert className="w-10 h-10 text-rose-500" />
</div>
<div className="space-y-2">
<h2 className="text-2xl font-black text-foreground">Access Denied</h2>
<p className="text-muted-foreground font-medium max-w-md">
Only administrators can manually onboard new staff members.
You are being redirected to the staff directory.
</p>
</div>
<Button
variant="outline"
onClick={() => navigate("/staff")}
className="rounded-xl font-bold px-8"
>
Return Now
</Button>
</div>
);
}
return ( return (
<DashboardLayout <DashboardLayout
title="Onboard New Staff" title="Add New Staff"
subtitle="Fill in the professional profile of your new team member" subtitle="Invite a new team member to join the platform"
backAction={ backAction={
<Button <Button
variant="ghost" variant="ghost"
@@ -57,10 +134,135 @@ export default function AddStaff() {
</Button> </Button>
} }
> >
<StaffForm <div className="max-w-2xl mx-auto">
onSubmit={handleSubmit} <form onSubmit={handleSubmit(onSubmit)} className="space-y-8 animate-in-slide-up">
isSubmitting={isSubmitting} <div className="bg-card/60 backdrop-blur-md border border-border p-8 rounded-3xl shadow-xl space-y-8">
/> {/* Identity Section */}
<div>
<h3 className="text-sm font-black text-muted-foreground/80 mb-6 flex items-center gap-2 uppercase tracking-wider">
<User className="w-4 h-4 text-primary" />
Staff Identity
</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">First Name <span className="text-rose-500">*</span></Label>
<Input
{...register("firstName", { required: "First name is required" })}
placeholder="John"
className="rounded-xl"
/>
{errors.firstName && <p className="text-xs text-rose-500 font-bold mt-1">{errors.firstName.message}</p>}
</div>
<div className="space-y-2">
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Last Name <span className="text-rose-500">*</span></Label>
<Input
{...register("lastName", { required: "Last name is required" })}
placeholder="Doe"
className="rounded-xl"
/>
{errors.lastName && <p className="text-xs text-rose-500 font-bold mt-1">{errors.lastName.message}</p>}
</div>
</div>
</div>
{/* Contact Section */}
<div>
<h3 className="text-sm font-black text-muted-foreground/80 mb-6 flex items-center gap-2 uppercase tracking-wider">
<Mail className="w-4 h-4 text-primary" />
Contact 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">Email Address <span className="text-rose-500">*</span></Label>
<Input
{...register("email", {
required: "Email is required",
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Invalid email address"
}
})}
type="email"
leadingIcon={<Mail className="w-4 h-4" />}
placeholder="john.doe@example.com"
className="rounded-xl"
/>
{errors.email && <p className="text-xs text-rose-500 font-bold mt-1">{errors.email.message}</p>}
</div>
<div className="space-y-2">
<Label className="text-xs font-black text-muted-foreground/80 pl-1">Phone Number <span className="text-rose-500">*</span></Label>
<Input
{...register("phone", { required: "Phone number is required" })}
type="tel"
leadingIcon={<Phone className="w-4 h-4" />}
placeholder="+1 (555) 000-0000"
className="rounded-xl"
/>
{errors.phone && <p className="text-xs text-rose-500 font-bold mt-1">{errors.phone.message}</p>}
</div>
</div>
</div>
{/* Skills Section */}
<div>
<h3 className="text-sm font-black text-muted-foreground/80 mb-6 flex items-center gap-2 uppercase tracking-wider">
<Award className="w-4 h-4 text-primary" />
Selection of Skills
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{COMMON_SKILLS.map((skill) => (
<Controller
key={skill}
name="skills"
control={control}
render={({ field }) => (
<div className="flex items-center space-x-3 p-3 rounded-xl hover:bg-primary/5 transition-premium border border-transparent hover:border-primary/10">
<Checkbox
id={skill}
checked={field.value?.includes(skill)}
onChange={(e) => {
const checked = e.target.checked;
const updatedSkills = checked
? [...(field.value || []), skill]
: field.value?.filter((s: string) => s !== skill);
field.onChange(updatedSkills);
}}
/>
<label
htmlFor={skill}
className="text-sm font-bold leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>
{skill}
</label>
</div>
)}
/>
))}
</div>
</div>
{/* Action Buttons */}
<div className="pt-6 flex gap-4">
<Button
type="button"
variant="outline"
onClick={() => navigate("/staff")}
className="flex-1 rounded-2xl font-bold py-6"
>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting}
className="flex-1 rounded-2xl font-bold py-6 shadow-lg shadow-primary/20"
leadingIcon={isSubmitting ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
>
{isSubmitting ? "Inviting..." : "Send Invitation"}
</Button>
</div>
</div>
</form>
</div>
</DashboardLayout> </DashboardLayout>
); );
} }