feat: Implement Staff View

This commit is contained in:
dhinesh-m24
2026-02-02 16:58:48 +05:30
parent fa208f2838
commit 1ac38311ee
3 changed files with 142 additions and 496 deletions

View File

@@ -4,17 +4,36 @@ import { Button } from "@/common/components/ui/button";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import StaffForm from "./components/StaffForm"; import StaffForm from "./components/StaffForm";
import DashboardLayout from "@/features/layouts/DashboardLayout"; import DashboardLayout from "@/features/layouts/DashboardLayout";
import { workforceService } from "@/services/workforceService"; import { useCreateStaff } from "@/dataconnect-generated/react";
import { dataConnect } from "@/features/auth/firebase";
import type { Staff } from "../type"; import type { Staff } from "../type";
export default function AddStaff() { export default function AddStaff() {
const navigate = useNavigate(); const navigate = useNavigate();
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const { mutateAsync: createStaff } = useCreateStaff(dataConnect);
const handleSubmit = async (staffData: Omit<Staff, 'id'>) => { const handleSubmit = async (staffData: Omit<Staff, 'id'>) => {
setIsSubmitting(true); setIsSubmitting(true);
try { try {
await workforceService.entities.Staff.create(staffData); await createStaff({
userId: `user_${Math.random().toString(36).substr(2, 9)}`, // Placeholder UID
fullName: staffData.employee_name,
role: staffData.position,
level: staffData.profile_type,
email: staffData.email,
phone: staffData.contact_number,
photoUrl: staffData.photo,
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) {
console.error("Failed to create staff", error); console.error("Failed to create staff", error);

View File

@@ -1,48 +1,75 @@
import { useState, useEffect } from "react"; import { useState, useMemo } from "react";
import { useParams, useNavigate } from "react-router-dom"; import { useParams, useNavigate } from "react-router-dom";
import { Button } from "@/common/components/ui/button"; import { Button } from "@/common/components/ui/button";
import { ArrowLeft, Loader2 } from "lucide-react"; import { ArrowLeft, Loader2, Edit, Save, X, User, FileText, Briefcase, Shield } from "lucide-react";
import StaffForm from "./components/StaffForm"; import StaffForm from "./components/StaffForm";
import DashboardLayout from "@/features/layouts/DashboardLayout"; import DashboardLayout from "@/features/layouts/DashboardLayout";
import { workforceService } from "@/services/workforceService"; import { useGetStaffById, useUpdateStaff } from "@/dataconnect-generated/react";
import { dataConnect } from "@/features/auth/firebase";
import type { Staff } from "../type"; import type { Staff } from "../type";
import { Badge } from "@/common/components/ui/badge";
export default function EditStaff() { export default function EditStaff() {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const [staff, setStaff] = useState<Staff | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
useEffect(() => { const { data: staffData, isLoading, refetch } = useGetStaffById(dataConnect, { id: id || "" });
const fetchStaff = async () => { const { mutateAsync: updateStaff } = useUpdateStaff(dataConnect);
if (!id) return; const [isSubmitting, setIsSubmitting] = useState(false);
setIsLoading(true);
try { const staff = useMemo(() => {
const staffList = await workforceService.entities.Staff.list(); if (!staffData?.staff) return null;
const foundStaff = staffList.find(s => s.id === id); const s = staffData.staff;
if (foundStaff) { return {
setStaff(foundStaff); id: s.id,
} else { employee_name: s.fullName,
console.error("Staff member not found"); initial: s.initial || "",
navigate("/staff"); position: s.role || "",
} position_2: "",
} catch (error) { profile_type: s.level || "",
console.error("Failed to fetch staff", error); employment_type: s.employmentType || "",
} finally { manager: s.manager || "",
setIsLoading(false); email: s.email || "",
} contact_number: s.phone || "",
}; phone: s.phone || "",
fetchStaff(); photo: s.photoUrl || "",
}, [id, navigate]); status: "Active", // Default for now
skills: (s.skills as string[]) || [],
averageRating: s.averageRating || 0,
reliability_score: s.reliabilityScore || 0,
shift_coverage_percentage: s.onTimeRate || 0,
total_shifts: s.totalShifts || 0,
department: s.department || "",
city: s.city || "",
address: s.addres || "", // Using misspelled field from schema
notes: s.bio || "",
} as Staff;
}, [staffData]);
const handleSubmit = async (staffData: Staff) => { const handleSubmit = async (staffData: Staff) => {
if (!id) return; if (!id) return;
setIsSubmitting(true); setIsSubmitting(true);
try { try {
await workforceService.entities.Staff.update(id, staffData); await updateStaff({
setStaff(staffData); id: id,
fullName: staffData.employee_name,
role: staffData.position,
level: staffData.profile_type,
email: staffData.email,
phone: staffData.contact_number,
photoUrl: staffData.photo,
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,
});
await refetch();
setIsEditing(false); setIsEditing(false);
} catch (error) { } catch (error) {
console.error("Failed to update staff", error); console.error("Failed to update staff", error);
@@ -67,7 +94,7 @@ export default function EditStaff() {
return ( return (
<DashboardLayout <DashboardLayout
title={isEditing ? `Edit: ${staff?.employee_name || 'Staff Member'}` : 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`} subtitle={isEditing ? `Modifying ${staff?.employee_name}'s professional records` : `Management of ${staff?.employee_name}'s professional records`}
backAction={ backAction={
<Button <Button
variant="ghost" variant="ghost"
@@ -80,35 +107,72 @@ export default function EditStaff() {
} }
> >
{staff && ( {staff && (
<div> <div className="space-y-6">
{!isEditing && ( {/* Profile Header */}
<div className="mb-6 flex justify-end"> <div className="bg-card/60 backdrop-blur-md border border-border p-6 rounded-3xl flex flex-col md:flex-row items-center justify-between gap-6">
<Button onClick={() => setIsEditing(true)} variant="secondary"> <div className="flex flex-col md:flex-row items-center gap-6 text-center md:text-left">
Edit <div className="w-24 h-24 bg-primary/10 rounded-2xl flex items-center justify-center text-primary font-black text-3xl border-2 border-primary/20 shadow-xl overflow-hidden group">
</Button> {staff.photo ? (
<img src={staff.photo} alt={staff.employee_name} className="w-full h-full object-cover" />
) : (
staff.employee_name.charAt(0)
)}
</div>
<div>
<h2 className="text-3xl font-black text-foreground mb-1">{staff.employee_name}</h2>
<div className="flex flex-wrap items-center justify-center md:justify-start gap-2">
<Badge className="bg-emerald-500/10 text-emerald-600 border-emerald-500/20 font-black text-xs uppercase px-3 py-1">
{staff.status || 'Active'}
</Badge>
<span className="text-muted-foreground font-bold text-sm"></span>
<span className="text-muted-foreground font-bold text-sm">{staff.position || 'No Role Assigned'}</span>
{staff.profile_type && (
<>
<span className="text-muted-foreground font-bold text-sm"></span>
<span className="text-primary font-bold text-sm">{staff.profile_type}</span>
</>
)}
</div>
</div>
</div> </div>
)}
{isEditing && ( <div className="flex gap-3">
<div className="mb-6 flex justify-end gap-2"> {!isEditing ? (
<Button <Button
onClick={handleCancel} onClick={() => setIsEditing(true)}
variant="outline" variant="secondary"
disabled={isSubmitting} leadingIcon={<Edit className="w-4 h-4" />}
> className="rounded-xl px-6 font-bold"
Cancel >
</Button> Edit Profile
<Button </Button>
onClick={() => { ) : (
// Trigger form submission by dispatching event or calling form submit <>
const form = document.querySelector('form'); <Button
if (form) form.requestSubmit(); onClick={handleCancel}
}} variant="outline"
disabled={isSubmitting} disabled={isSubmitting}
> leadingIcon={<X className="w-4 h-4" />}
{isSubmitting ? 'Saving...' : 'Save'} className="rounded-xl font-bold"
</Button> >
Cancel
</Button>
<Button
onClick={() => {
const form = document.querySelector('form');
if (form) form.requestSubmit();
}}
disabled={isSubmitting}
leadingIcon={isSubmitting ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
className="rounded-xl px-6 font-bold"
>
{isSubmitting ? 'Saving...' : 'Save Changes'}
</Button>
</>
)}
</div> </div>
)} </div>
<StaffForm <StaffForm
staff={staff} staff={staff}
onSubmit={handleSubmit} onSubmit={handleSubmit}

View File

@@ -1,437 +0,0 @@
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<string, User> = {
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<User> => {
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<void> => {
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<Staff[]> => {
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<Staff | null> => {
await delay(600);
return mockStaff.find((s) => s.id === id) || null;
},
/**
* Create new staff member
*/
create: async (staff: Partial<Staff>): Promise<Staff> => {
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<Staff>): Promise<Staff> => {
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<void> => {
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<Event[]> => {
await delay(1000);
return [...mockEvents];
},
/**
* Get single event by ID
*/
get: async (id: string): Promise<Event | null> => {
await delay(600);
return mockEvents.find((e) => e.id === id) || null;
},
},
},
};
export default workforceService;