Files
GSB-Construction/Modules/ProjectManagement/resources/js/Components/LaborLookupModal.tsx
Christopher Boyles 64d4b331a8
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
Additional Changes
2026-06-02 22:04:03 +08:00

212 lines
11 KiB
TypeScript

import { useState, useMemo } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/Components/ui/dialog';
import { Input } from '@/Components/ui/input';
import { Button } from '@/Components/ui/button';
import { Badge } from '@/Components/ui/badge';
import { Search, Users, Check } from 'lucide-react';
interface Skill {
id: number;
ulid: string;
name: string;
}
interface Labor {
id: number;
ulid: string;
name: string;
category: 'skilled' | 'unskilled';
hourly_rate: string;
skills: Skill[];
}
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
labors: Labor[];
onSelectLabor: (labor: Labor) => void;
selectedLaborUlid?: string;
}
export default function LaborLookupModal({
open,
onOpenChange,
labors,
onSelectLabor,
selectedLaborUlid
}: Props) {
const [search, setSearch] = useState('');
const [categoryFilter, setCategoryFilter] = useState<'all' | 'skilled' | 'unskilled'>('all');
const formatCurrency = (v: string | number) => {
return new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP' }).format(Number(v));
};
const filteredLabors = useMemo(() => {
return labors.filter(labor => {
// Category filter
if (categoryFilter !== 'all' && labor.category !== categoryFilter) {
return false;
}
// Search filter
if (!search) return true;
const query = search.toLowerCase();
const matchesName = labor.name.toLowerCase().includes(query);
const matchesCategory = labor.category.toLowerCase().includes(query);
const matchesSkills = labor.skills && labor.skills.some(skill =>
skill.name.toLowerCase().includes(query)
);
return matchesName || matchesCategory || matchesSkills;
});
}, [labors, search, categoryFilter]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[70vw] max-w-[95vw] max-h-[85vh] flex flex-col p-0 gap-0 border-slate-200">
<DialogHeader className="px-6 py-4 border-b border-slate-100">
<DialogTitle className="flex items-center gap-2 text-lg font-bold text-slate-800">
<Users className="h-5 w-5 text-emerald-600" />
Select Labor Record
</DialogTitle>
<DialogDescription className="text-xs text-slate-500">
Search and select a labor trade category to allocate to the project task.
</DialogDescription>
</DialogHeader>
<div className="px-6 py-3 border-b border-slate-100 bg-slate-50/50 flex flex-col sm:flex-row gap-3 justify-between items-center">
{/* Category Tabs */}
<div className="flex bg-slate-100 p-0.5 rounded-lg border border-slate-200/40 w-full sm:w-auto">
<button
type="button"
onClick={() => setCategoryFilter('all')}
className={`flex-1 sm:flex-initial px-3 py-1 text-xs font-semibold rounded-md transition-all ${
categoryFilter === 'all'
? 'bg-white text-slate-800 shadow-sm'
: 'text-slate-500 hover:text-slate-800'
}`}
>
All Categories
</button>
<button
type="button"
onClick={() => setCategoryFilter('skilled')}
className={`flex-1 sm:flex-initial px-3 py-1 text-xs font-semibold rounded-md transition-all ${
categoryFilter === 'skilled'
? 'bg-white text-slate-800 shadow-sm'
: 'text-slate-500 hover:text-slate-800'
}`}
>
Skilled
</button>
<button
type="button"
onClick={() => setCategoryFilter('unskilled')}
className={`flex-1 sm:flex-initial px-3 py-1 text-xs font-semibold rounded-md transition-all ${
categoryFilter === 'unskilled'
? 'bg-white text-slate-800 shadow-sm'
: 'text-slate-500 hover:text-slate-800'
}`}
>
Unskilled
</button>
</div>
{/* Search Input */}
<div className="relative w-full sm:w-72">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-slate-400" />
<Input
type="search"
placeholder="Search by name, skills..."
className="h-9 pl-9 bg-white text-xs border-slate-200 focus-visible:ring-emerald-500 focus-visible:border-emerald-500"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
<div className="flex-1 overflow-y-auto p-6 min-h-0">
{filteredLabors.length === 0 ? (
<div className="text-center py-12 text-slate-400 text-xs">
No labor records found matching "{search}".
</div>
) : (
<div className="border border-slate-200/80 rounded-xl overflow-hidden shadow-sm">
<table className="w-full text-xs text-left border-collapse">
<thead className="bg-slate-55 border-b border-slate-100 text-slate-600 font-semibold">
<tr>
<th className="px-4 py-3 font-semibold">Labor Record</th>
<th className="px-4 py-3 font-semibold w-28">Category</th>
<th className="px-4 py-3 font-semibold">Skills</th>
<th className="px-4 py-3 font-semibold text-right w-32">Hourly Rate</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 bg-white">
{filteredLabors.map(labor => {
const isSelected = selectedLaborUlid === labor.ulid;
return (
<tr
key={labor.id}
className={`transition-all hover:bg-slate-50/50 cursor-pointer group ${
isSelected ? 'bg-emerald-50/40 hover:bg-emerald-50/60' : ''
}`}
onClick={() => onSelectLabor(labor)}
>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<span className="font-semibold text-slate-800 group-hover:text-emerald-700 transition-colors">
{labor.name}
</span>
{isSelected && (
<Badge className="bg-emerald-100 text-emerald-800 border-none text-[10px] font-medium flex items-center gap-0.5">
<Check className="h-2.5 w-2.5" /> Selected
</Badge>
)}
</div>
</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={`capitalize font-normal text-[10px] ${
labor.category === 'skilled'
? 'bg-blue-50 text-blue-700 border-blue-100'
: 'bg-amber-50 text-amber-700 border-amber-100'
}`}
>
{labor.category}
</Badge>
</td>
<td className="px-4 py-3">
<div className="flex flex-wrap gap-1">
{labor.skills && labor.skills.length > 0 ? (
labor.skills.map(skill => (
<Badge
key={skill.id}
variant="secondary"
className="bg-slate-100 text-slate-650 hover:bg-slate-200 text-[10px] font-normal"
>
{skill.name}
</Badge>
))
) : (
<span className="text-slate-400 italic">No skills listed</span>
)}
</div>
</td>
<td className="px-4 py-3 text-right font-mono font-medium text-slate-700">
{formatCurrency(labor.hourly_rate)}/hr
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
}