218 lines
12 KiB
TypeScript
218 lines
12 KiB
TypeScript
import { useState, useMemo } from 'react';
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/Components/ui/dialog';
|
|
import { Input } from '@/Components/ui/input';
|
|
import { Badge } from '@/Components/ui/badge';
|
|
import { Search, Wrench, Check } from 'lucide-react';
|
|
|
|
interface EquipmentSpecification {
|
|
id: number;
|
|
ulid: string;
|
|
name: string;
|
|
}
|
|
|
|
interface Equipment {
|
|
id: number;
|
|
ulid: string;
|
|
name: string;
|
|
owner_name: string | null;
|
|
hourly_rate: string;
|
|
specifications: EquipmentSpecification[];
|
|
}
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
equipments: Equipment[];
|
|
onSelectEquipment: (equipment: Equipment) => void;
|
|
selectedEquipmentUlid?: string;
|
|
}
|
|
|
|
export default function EquipmentLookupModal({
|
|
open,
|
|
onOpenChange,
|
|
equipments,
|
|
onSelectEquipment,
|
|
selectedEquipmentUlid
|
|
}: Props) {
|
|
const [search, setSearch] = useState('');
|
|
const [ownerFilter, setOwnerFilter] = useState<string>('all');
|
|
|
|
const formatCurrency = (v: string | number) => {
|
|
return new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP' }).format(Number(v));
|
|
};
|
|
|
|
// Dynamically discover all unique owner names in the equipment list for filtering
|
|
const uniqueOwners = useMemo(() => {
|
|
const owners = new Set<string>();
|
|
equipments.forEach(e => {
|
|
if (e.owner_name) {
|
|
owners.add(e.owner_name);
|
|
}
|
|
});
|
|
return Array.from(owners).sort();
|
|
}, [equipments]);
|
|
|
|
const filteredEquipments = useMemo(() => {
|
|
return equipments.filter(eq => {
|
|
// Owner filter
|
|
if (ownerFilter !== 'all') {
|
|
if (ownerFilter === 'unspecified') {
|
|
if (eq.owner_name !== null && eq.owner_name !== '') return false;
|
|
} else {
|
|
if (eq.owner_name !== ownerFilter) return false;
|
|
}
|
|
}
|
|
|
|
// Search filter
|
|
if (!search) return true;
|
|
const query = search.toLowerCase();
|
|
const matchesName = eq.name.toLowerCase().includes(query);
|
|
const matchesOwner = eq.owner_name ? eq.owner_name.toLowerCase().includes(query) : false;
|
|
const matchesSpecs = eq.specifications && eq.specifications.some(spec =>
|
|
spec.name.toLowerCase().includes(query)
|
|
);
|
|
|
|
return matchesName || matchesOwner || matchesSpecs;
|
|
});
|
|
}, [equipments, search, ownerFilter]);
|
|
|
|
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 bg-white shadow-2xl overflow-hidden">
|
|
<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">
|
|
<Wrench className="h-5 w-5 text-emerald-600" />
|
|
Select Equipment / Tool
|
|
</DialogTitle>
|
|
<DialogDescription className="text-xs text-slate-500">
|
|
Search and select a piece of machinery or equipment to allocate to the project task.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="px-6 py-3 border-b border-slate-100 bg-slate-50 flex flex-col sm:flex-row gap-3 justify-between items-center">
|
|
{/* Owner Filter Badges */}
|
|
<div className="flex flex-wrap gap-1.5 w-full sm:w-auto">
|
|
<button
|
|
type="button"
|
|
onClick={() => setOwnerFilter('all')}
|
|
className={`px-3 py-1.5 text-xs font-semibold rounded-md border transition-all ${
|
|
ownerFilter === 'all'
|
|
? 'bg-slate-900 text-white border-slate-900 shadow-sm'
|
|
: 'bg-slate-100 text-slate-700 border-slate-300 hover:bg-slate-200'
|
|
}`}
|
|
>
|
|
All Owners
|
|
</button>
|
|
{uniqueOwners.map(owner => (
|
|
<button
|
|
key={owner}
|
|
type="button"
|
|
onClick={() => setOwnerFilter(owner)}
|
|
className={`px-3 py-1.5 text-xs font-semibold rounded-md border transition-all ${
|
|
ownerFilter === owner
|
|
? 'bg-slate-900 text-white border-slate-900 shadow-sm'
|
|
: 'bg-slate-100 text-slate-700 border-slate-300 hover:bg-slate-200'
|
|
}`}
|
|
>
|
|
{owner}
|
|
</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, specs, owner..."
|
|
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">
|
|
{filteredEquipments.length === 0 ? (
|
|
<div className="text-center py-12 text-slate-400 text-xs">
|
|
No equipment profiles 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">Equipment / Tool Name</th>
|
|
<th className="px-4 py-3 font-semibold w-36">Owner</th>
|
|
<th className="px-4 py-3 font-semibold">Specifications</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">
|
|
{filteredEquipments.map(eq => {
|
|
const isSelected = selectedEquipmentUlid === eq.ulid;
|
|
return (
|
|
<tr
|
|
key={eq.id}
|
|
className={`transition-all hover:bg-slate-50/50 cursor-pointer group ${
|
|
isSelected ? 'bg-emerald-50/40 hover:bg-emerald-50/60' : ''
|
|
}`}
|
|
onClick={() => onSelectEquipment(eq)}
|
|
>
|
|
<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">
|
|
{eq.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">
|
|
{eq.owner_name ? (
|
|
<Badge
|
|
variant="outline"
|
|
className="bg-slate-50 text-slate-700 border-slate-200 font-normal text-[10px]"
|
|
>
|
|
{eq.owner_name}
|
|
</Badge>
|
|
) : (
|
|
<span className="text-slate-400 italic text-[10px]">Unspecified</span>
|
|
)}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<div className="flex flex-wrap gap-1">
|
|
{eq.specifications && eq.specifications.length > 0 ? (
|
|
eq.specifications.map(spec => (
|
|
<Badge
|
|
key={spec.id}
|
|
variant="secondary"
|
|
className="bg-slate-100 text-slate-650 hover:bg-slate-200 text-[10px] font-normal"
|
|
>
|
|
{spec.name}
|
|
</Badge>
|
|
))
|
|
) : (
|
|
<span className="text-slate-400 italic">No specs listed</span>
|
|
)}
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3 text-right font-mono font-medium text-slate-700">
|
|
{formatCurrency(eq.hourly_rate)}/hr
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|