feat: Implement a form to add staff
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user