Files
GSB-Construction/Modules/ProjectManagement/resources/js/Pages/Projects/Show/TeamTab.tsx
Ajjj b98951f65f
Some checks failed
Tests / PHP 8.3 (push) Has been cancelled
Tests / PHP 8.4 (push) Has been cancelled
Tests / PHP 8.5 (push) Has been cancelled
feat: decompose project god components, add database performance indexes, and isolate contractor admin retention
2026-08-14 16:20:15 +08:00

119 lines
6.6 KiB
TypeScript

import React, { FormEvent } from 'react';
import { InertiaFormProps } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/Components/ui/card';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { Badge } from '@/Components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/Components/ui/dialog';
import { Users, Plus, Trash2, Mail } from 'lucide-react';
import { ProjectData, PersonnelItem } from '../../../types/project-show';
interface TeamTabProps {
project: ProjectData;
employees: { id: number; ulid: string; name: string }[];
memberForm: InertiaFormProps<{ user_id: string; role: string }>;
handleAddMember: (e: FormEvent) => void;
handleRemoveMember: (userUlid: string) => void;
statusLabel: (s: string) => string;
}
export default function TeamTab({
project,
employees,
memberForm,
handleAddMember,
handleRemoveMember,
statusLabel
}: TeamTabProps) {
const personnel = project.personnel || [];
const existingPersonnelUlids = new Set(personnel.map((p: PersonnelItem) => p.ulid));
const availableEmployees = employees.filter((e) => !existingPersonnelUlids.has(e.ulid));
return (
<div className="space-y-6">
{/* Project Personnel / Team Members */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2 text-base">
<Users className="h-5 w-5 text-blue-500" /> Assigned Personnel ({personnel.length})
</CardTitle>
<p className="text-xs text-gray-400 mt-1">Team members allocated to work on {project.name}</p>
</div>
<Dialog>
<DialogTrigger render={<Button size="sm" />}>
<Plus className="mr-2 h-4 w-4" /> Add Team Member
</DialogTrigger>
<DialogContent>
<DialogHeader><DialogTitle>Add Personnel to Project</DialogTitle></DialogHeader>
<form onSubmit={handleAddMember} className="space-y-4">
<div>
<label className="text-sm font-medium">Select Employee *</label>
<select
value={memberForm.data.user_id}
onChange={e => memberForm.setData('user_id', e.target.value)}
required
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
>
<option value="">Choose an employee...</option>
{availableEmployees.map((emp) => (
<option key={emp.ulid} value={emp.ulid}>{emp.name}</option>
))}
</select>
</div>
<div>
<label className="text-sm font-medium">Project Role</label>
<Input
value={memberForm.data.role}
onChange={e => memberForm.setData('role', e.target.value)}
placeholder="e.g. Lead Engineer, Safety Officer"
/>
</div>
<div className="flex justify-end">
<Button type="submit" disabled={memberForm.processing}>
<Plus className="mr-2 h-4 w-4" /> Add Member
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</CardHeader>
<CardContent>
{personnel.length === 0 ? (
<p className="text-sm text-gray-400 italic py-6 text-center">No additional personnel assigned. Add team members to collaborate on tasks.</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{personnel.map((member: PersonnelItem) => (
<div key={member.ulid} className="flex items-center justify-between p-3 rounded-lg border bg-white hover:bg-gray-50/50 transition-colors">
<div className="flex items-center gap-3 min-w-0">
<div className="h-9 w-9 rounded-full bg-slate-100 text-slate-700 font-semibold flex items-center justify-center text-xs shrink-0">
{member.name.charAt(0)}
</div>
<div className="min-w-0">
<p className="font-medium text-xs text-gray-900 truncate">{member.name}</p>
<p className="text-[11px] text-gray-400 truncate">{member.email}</p>
{member.pivot?.role && (
<Badge variant="outline" className="text-[9px] px-1 py-0 mt-1 font-normal text-slate-600">
{member.pivot.role}
</Badge>
)}
</div>
</div>
<Button
variant="ghost" size="icon-sm"
title="Remove member"
className="text-gray-400 hover:text-red-600"
onClick={() => handleRemoveMember(member.ulid)}
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
);
}