Files
GSB-Construction/Modules/Labors/resources/js/Pages/Index.tsx
Ajjj 778f27e0a5
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: implement labor and skills management pages with CRUD functionality and analytics dashboard
2026-08-24 18:01:33 +08:00

923 lines
58 KiB
TypeScript

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, useForm, Link } from '@inertiajs/react';
import { Button } from '@/Components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/Components/ui/card';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
import { Badge } from '@/Components/ui/badge';
import { Textarea } from '@/Components/ui/textarea';
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from '@/Components/ui/table';
import {
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
} from '@/Components/ui/dialog';
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/Components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/Components/ui/tabs';
import {
Users, Plus, Pencil, Trash2, ShieldAlert, Sparkles, BookOpen, Check,
Layers, HardHat, Wrench, Search, DollarSign, Activity, Tag, PackageCheck
} from 'lucide-react';
import { ConfirmModal } from '@/Components/ConfirmModal';
import { useState, useMemo } from 'react';
interface Skill {
id: number;
ulid: string;
name: string;
description: string | null;
}
interface Labor {
id: number;
ulid: string;
name: string;
category: 'skilled' | 'unskilled';
hourly_rate: string;
status: string;
skills: Skill[];
}
interface Props {
labors: Labor[];
skills: Skill[];
}
export default function Index({ labors = [], skills = [] }: Props) {
// Tab State
const [activeTab, setActiveTab] = useState<'labor' | 'skill'>('labor');
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all');
const [categoryFilter, setCategoryFilter] = useState<'all' | 'skilled' | 'unskilled'>('all');
// Dialog state for Labor
const [laborModalOpen, setLaborModalOpen] = useState(false);
const [editingLabor, setEditingLabor] = useState<Labor | null>(null);
// Dialog state for Skill
const [skillModalOpen, setSkillModalOpen] = useState(false);
const [editingSkill, setEditingSkill] = useState<Skill | null>(null);
// Inertia forms
const laborForm = useForm({
name: '',
category: 'skilled' as 'skilled' | 'unskilled',
hourly_rate: '',
status: 'active',
skills: [] as number[],
});
const skillForm = useForm({
name: '',
description: '',
});
const formatCurrency = (v: string | number) => {
return new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP' }).format(Number(v));
};
// Calculate Summary Metrics
const stats = useMemo(() => {
const activeLabors = labors.filter(l => l.status === 'active');
const avgLaborRate = activeLabors.length > 0
? activeLabors.reduce((acc, l) => acc + parseFloat(l.hourly_rate || '0'), 0) / activeLabors.length
: 0;
const skilledCount = labors.filter(l => l.category === 'skilled').length;
const unskilledCount = labors.filter(l => l.category === 'unskilled').length;
return {
totalLabors: labors.length,
activeLaborsCount: activeLabors.length,
avgLaborRate,
skilledCount,
unskilledCount,
totalSkills: skills.length,
};
}, [labors, skills]);
// Filtered Single Labor Profiles
const filteredLabors = useMemo(() => {
return labors.filter((labor) => {
if (statusFilter !== 'all' && labor.status !== statusFilter) return false;
if (categoryFilter !== 'all' && labor.category !== categoryFilter) return false;
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase();
const nameMatch = labor.name.toLowerCase().includes(q);
const rateMatch = labor.hourly_rate.toString().includes(q);
const skillMatch = labor.skills.some(s => s.name.toLowerCase().includes(q));
return nameMatch || rateMatch || skillMatch;
}
return true;
});
}, [labors, statusFilter, categoryFilter, searchQuery]);
// Filtered Skills
const filteredSkills = useMemo(() => {
return skills.filter((skill) => {
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase();
const nameMatch = skill.name.toLowerCase().includes(q);
const descMatch = (skill.description || '').toLowerCase().includes(q);
return nameMatch || descMatch;
}
return true;
});
}, [skills, searchQuery]);
// Labor actions
const openAddLabor = () => {
setEditingLabor(null);
laborForm.setData({
name: '',
category: 'skilled',
hourly_rate: '',
status: 'active',
skills: [],
});
laborForm.clearErrors();
setLaborModalOpen(true);
};
const openEditLabor = (labor: Labor) => {
setEditingLabor(labor);
laborForm.setData({
name: labor.name,
category: labor.category,
hourly_rate: labor.hourly_rate,
status: labor.status,
skills: labor.skills.map(s => s.id),
});
laborForm.clearErrors();
setLaborModalOpen(true);
};
const handleLaborSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (editingLabor) {
laborForm.put(route('labors.update', editingLabor.ulid || editingLabor.id), {
onSuccess: () => {
setLaborModalOpen(false);
laborForm.reset();
},
});
} else {
laborForm.post(route('labors.store'), {
onSuccess: () => {
setLaborModalOpen(false);
laborForm.reset();
},
});
}
};
const [deleteLaborModalOpen, setDeleteLaborModalOpen] = useState(false);
const [laborToDelete, setLaborToDelete] = useState<Labor | null>(null);
const promptDeleteLabor = (labor: Labor) => {
setLaborToDelete(labor);
setDeleteLaborModalOpen(true);
};
const handleConfirmLaborDelete = () => {
if (laborToDelete) {
laborForm.delete(route('labors.destroy', laborToDelete.ulid || laborToDelete.id), {
onSuccess: () => {
setDeleteLaborModalOpen(false);
setLaborToDelete(null);
},
});
}
};
// Skill actions
const openAddSkill = () => {
setEditingSkill(null);
skillForm.setData({
name: '',
description: '',
});
skillForm.clearErrors();
setSkillModalOpen(true);
};
const openEditSkill = (skill: Skill) => {
setEditingSkill(skill);
skillForm.setData({
name: skill.name,
description: skill.description || '',
});
skillForm.clearErrors();
setSkillModalOpen(true);
};
const handleSkillSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (editingSkill) {
skillForm.put(route('skills.update', editingSkill.ulid || editingSkill.id), {
onSuccess: () => {
setSkillModalOpen(false);
skillForm.reset();
},
});
} else {
skillForm.post(route('skills.store'), {
onSuccess: () => {
setSkillModalOpen(false);
skillForm.reset();
},
});
}
};
const [deleteSkillModalOpen, setDeleteSkillModalOpen] = useState(false);
const [skillToDelete, setSkillToDelete] = useState<Skill | null>(null);
const promptDeleteSkill = (skill: Skill) => {
setSkillToDelete(skill);
setDeleteSkillModalOpen(true);
};
const handleConfirmSkillDelete = () => {
if (skillToDelete) {
skillForm.delete(route('skills.destroy', skillToDelete.ulid || skillToDelete.id), {
onSuccess: () => {
setDeleteSkillModalOpen(false);
setSkillToDelete(null);
},
});
}
};
const toggleSkillSelection = (skillId: number) => {
const current = [...laborForm.data.skills];
const index = current.indexOf(skillId);
if (index > -1) {
current.splice(index, 1);
} else {
current.push(skillId);
}
laborForm.setData('skills', current);
};
return (
<AuthenticatedLayout
header={
<div>
<h2 className="text-xl font-bold leading-tight text-gray-900 dark:text-white flex items-center gap-2.5">
<div className="p-2 rounded-lg bg-emerald-50 dark:bg-emerald-950/50 text-emerald-600 dark:text-emerald-400 border border-emerald-100 dark:border-emerald-900/50">
<Users className="h-5 w-5" />
</div>
Single Labor Profiles & Skill Dictionary
</h2>
<p className="mt-1 text-xs text-gray-500 font-medium">
Standardized labor trades, classification grades, standard hourly rates, and trade competency dictionary
</p>
</div>
}
>
<Head title="Labor Catalog & Trades" />
<div className="py-6 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
{/* Sub-Navigation Tabs */}
<div className="flex items-center justify-between border-b border-border/80 pb-3">
<div className="flex items-center gap-1.5 p-1 bg-muted/60 dark:bg-muted/30 rounded-xl border border-border/50">
<Link href={route('labors.index')}>
<button
type="button"
className="flex items-center gap-2 px-3.5 py-1.5 text-xs font-bold rounded-lg bg-background text-foreground shadow-2xs border border-border/60 transition-all"
>
<Users className="w-3.5 h-3.5 text-emerald-500" />
<span>Single Labor Profiles & Skills</span>
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-4 bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 font-semibold border-none">
{stats.totalLabors}
</Badge>
</button>
</Link>
<Link href={route('labors.bundles.index')}>
<button
type="button"
className="flex items-center gap-2 px-3.5 py-1.5 text-xs font-semibold rounded-lg text-muted-foreground hover:text-foreground hover:bg-background/50 transition-all cursor-pointer"
>
<PackageCheck className="w-3.5 h-3.5 text-indigo-500" />
<span>Job Bundles & Crew Packages</span>
</button>
</Link>
</div>
</div>
{/* 1. Summary KPI Cards */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{/* Labor Workforce KPI */}
<Card className="bg-gradient-to-br from-white to-emerald-50/30 dark:from-gray-900 dark:to-emerald-950/20 border border-gray-100 dark:border-gray-800 shadow-2xs">
<CardHeader className="pb-2 flex flex-row items-center justify-between">
<CardTitle className="text-xs font-bold tracking-wider uppercase text-gray-500 dark:text-gray-400">
Single Labor Workforce
</CardTitle>
<div className="p-2 rounded-lg bg-emerald-50 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400">
<Users className="w-4 h-4" />
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="text-2xl font-black text-gray-900 dark:text-white tracking-tight">
{stats.totalLabors} <span className="text-sm font-semibold text-gray-400">Trades</span>
</div>
<p className="text-xs text-gray-500 mt-1 flex items-center gap-1.5">
<span className="font-bold text-emerald-600 dark:text-emerald-400">{stats.activeLaborsCount} active</span>
<span></span>
<span>Avg: <strong className="font-mono text-gray-700 dark:text-gray-300">{formatCurrency(stats.avgLaborRate)}/hr</strong></span>
</p>
</CardContent>
</Card>
{/* Skill Breakdown KPI */}
<Card className="bg-gradient-to-br from-white to-blue-50/30 dark:from-gray-900 dark:to-blue-950/20 border border-gray-100 dark:border-gray-800 shadow-2xs">
<CardHeader className="pb-2 flex flex-row items-center justify-between">
<CardTitle className="text-xs font-bold tracking-wider uppercase text-gray-500 dark:text-gray-400">
Classification Breakdown
</CardTitle>
<div className="p-2 rounded-lg bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400">
<HardHat className="w-4 h-4" />
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="text-2xl font-black text-gray-900 dark:text-white tracking-tight">
{stats.skilledCount} <span className="text-sm font-semibold text-gray-400">Skilled</span>
<span className="text-gray-300 dark:text-gray-700 mx-2">/</span>
<span className="text-lg font-bold text-amber-600 dark:text-amber-400">{stats.unskilledCount} Unskilled</span>
</div>
<p className="text-xs text-gray-500 mt-1">
Master craftsman vs apprentice manpower ratios
</p>
</CardContent>
</Card>
{/* Master Skills Dictionary KPI */}
<Card className="bg-gradient-to-br from-white to-indigo-50/30 dark:from-gray-900 dark:to-indigo-950/20 border border-gray-100 dark:border-gray-800 shadow-2xs">
<CardHeader className="pb-2 flex flex-row items-center justify-between">
<CardTitle className="text-xs font-bold tracking-wider uppercase text-gray-500 dark:text-gray-400">
Skill Dictionary
</CardTitle>
<div className="p-2 rounded-lg bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400">
<BookOpen className="w-4 h-4" />
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="text-2xl font-black text-gray-900 dark:text-white tracking-tight">
{stats.totalSkills} <span className="text-sm font-semibold text-gray-400">Skills</span>
</div>
<p className="text-xs text-gray-500 mt-1">
Standard trade competencies & certifications
</p>
</CardContent>
</Card>
</div>
{/* 2. Main Tabs & Table View */}
<Tabs value={activeTab} onValueChange={(val) => setActiveTab(val as 'labor' | 'skill')} className="space-y-4">
{/* Navigation Header Toolbar */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3 bg-white dark:bg-gray-900 p-3 rounded-xl border border-gray-100 dark:border-gray-800 shadow-2xs">
<TabsList className="bg-gray-100 dark:bg-gray-800 p-1 rounded-lg">
<TabsTrigger value="labor" className="flex items-center gap-2 px-3.5 py-1.5 text-xs font-semibold rounded-md data-[state=active]:bg-white data-[state=active]:text-gray-900 dark:data-[state=active]:bg-gray-900 dark:data-[state=active]:text-white data-[state=active]:shadow-2xs cursor-pointer">
<Users className="h-3.5 w-3.5 text-emerald-500" />
Single Labor Profiles
<span className="ml-1 text-[10px] px-1.5 py-0.2 rounded-full bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300 font-bold">
{labors.length}
</span>
</TabsTrigger>
<TabsTrigger value="skill" className="flex items-center gap-2 px-3.5 py-1.5 text-xs font-semibold rounded-md data-[state=active]:bg-white data-[state=active]:text-gray-900 dark:data-[state=active]:bg-gray-900 dark:data-[state=active]:text-white data-[state=active]:shadow-2xs cursor-pointer">
<BookOpen className="h-3.5 w-3.5 text-indigo-500" />
Skill Dictionary
<span className="ml-1 text-[10px] px-1.5 py-0.2 rounded-full bg-indigo-100 text-indigo-700 dark:bg-indigo-950 dark:text-indigo-300 font-bold">
{skills.length}
</span>
</TabsTrigger>
</TabsList>
{/* Search & Category/Status Filters */}
<div className="flex items-center gap-2.5">
{/* Category Filter Buttons (for Labor Tab) */}
{activeTab === 'labor' && (
<div className="flex items-center bg-gray-100 dark:bg-gray-800 p-0.5 rounded-lg">
{(['all', 'skilled', 'unskilled'] as const).map((cat) => (
<button
key={cat}
type="button"
onClick={() => setCategoryFilter(cat)}
className={`text-xs px-2.5 py-1 rounded-md font-semibold capitalize transition-all cursor-pointer ${
categoryFilter === cat
? 'bg-white text-gray-900 dark:bg-gray-900 dark:text-white shadow-2xs'
: 'text-gray-500 hover:text-gray-900 dark:hover:text-white'
}`}
>
{cat}
</button>
))}
</div>
)}
{/* Search Input */}
<div className="relative min-w-[220px]">
<Search className="w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={
activeTab === 'labor' ? 'Search trades, skills...' : 'Search master skills...'
}
className="w-full text-xs pl-8 pr-3 py-1.5 rounded-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-hidden focus:ring-1 focus:ring-emerald-500 focus:border-emerald-500 transition-all"
/>
</div>
</div>
</div>
{/* 1. Single Labor Profiles Tab Content */}
<TabsContent value="labor" className="mt-0">
<Card className="shadow-xs bg-white dark:bg-gray-900 border border-gray-100 dark:border-gray-800 overflow-hidden">
<CardHeader className="border-b border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/50 py-3.5 px-5">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<CardTitle className="text-sm font-bold text-gray-900 dark:text-white flex items-center gap-2">
<Users className="w-4 h-4 text-emerald-500" /> Single Labor Profiles
</CardTitle>
<CardDescription className="text-xs text-gray-500 mt-0.5">
Individual labor records, classification grades, standard hourly rates, and skills.
</CardDescription>
</div>
<div className="flex items-center gap-3">
<span className="text-xs text-gray-400 font-mono hidden sm:inline">
Showing {filteredLabors.length} of {labors.length}
</span>
<Button
onClick={openAddLabor}
className="bg-emerald-600 hover:bg-emerald-700 text-white font-semibold text-xs shadow-2xs flex items-center gap-1.5 h-8 px-3 cursor-pointer shrink-0"
>
<Plus className="h-3.5 w-3.5" /> Add Labor Profile
</Button>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full text-left text-xs">
<thead>
<tr className="border-b border-gray-100 dark:border-gray-800 bg-gray-50/40 dark:bg-gray-800/30 text-gray-400 uppercase tracking-wider text-[10px] font-bold">
<th className="py-3 px-5">Labor Trade Name</th>
<th className="py-3 px-4">Category</th>
<th className="py-3 px-4">Standard Hourly Rate</th>
<th className="py-3 px-4">Grouped Skillsets</th>
<th className="py-3 px-4">Status</th>
<th className="py-3 px-5 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
{filteredLabors.length === 0 ? (
<tr>
<td colSpan={6} className="text-center py-12 text-gray-400">
<div className="flex flex-col items-center justify-center gap-2">
<div className="p-3 rounded-full bg-gray-50 dark:bg-gray-800 text-gray-400 border border-gray-100 dark:border-gray-700">
<Users className="w-6 h-6" />
</div>
<p className="font-semibold text-gray-700 dark:text-gray-300 text-sm">
{searchQuery || categoryFilter !== 'all' ? 'No matching labor profiles found.' : 'No labor profiles registered yet.'}
</p>
<p className="text-xs text-gray-400">
{searchQuery || categoryFilter !== 'all' ? 'Try adjusting your search query or category filter.' : 'Click "Add Labor Profile" above to register your first trade.'}
</p>
</div>
</td>
</tr>
) : (
filteredLabors.map((labor) => (
<tr key={labor.id} className="group hover:bg-emerald-50/20 dark:hover:bg-emerald-950/10 transition-colors">
<td className="py-3.5 px-5 align-middle">
<div className="flex items-center gap-2.5">
<div className="p-1.5 rounded-md bg-emerald-50 dark:bg-emerald-950/50 text-emerald-600 dark:text-emerald-400 border border-emerald-100 dark:border-emerald-900/50">
<HardHat className="w-3.5 h-3.5" />
</div>
<span className="font-bold text-gray-900 dark:text-white text-xs group-hover:text-emerald-600 dark:group-hover:text-emerald-400 transition-colors">
{labor.name}
</span>
</div>
</td>
<td className="py-3.5 px-4 align-middle">
<Badge className={
labor.category === 'skilled'
? 'bg-blue-50 text-blue-700 dark:bg-blue-950/60 dark:text-blue-300 border-blue-200 dark:border-blue-800 font-bold uppercase text-[10px]'
: 'bg-amber-50 text-amber-700 dark:bg-amber-950/60 dark:text-amber-300 border-amber-200 dark:border-amber-800 font-bold uppercase text-[10px]'
}>
{labor.category}
</Badge>
</td>
<td className="py-3.5 px-4 align-middle">
<span className="font-mono font-black text-xs text-emerald-700 dark:text-emerald-400">
{formatCurrency(labor.hourly_rate)}
</span>
<span className="text-gray-400 text-[11px] ml-1">/ hr</span>
</td>
<td className="py-3.5 px-4 align-middle max-w-[280px]">
<div className="flex flex-wrap gap-1">
{labor.skills.length === 0 ? (
<span className="text-[11px] text-gray-400 italic">No skills grouped</span>
) : (
labor.skills.map(s => (
<span
key={s.id}
className="inline-flex items-center gap-1 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-[10px] font-medium py-0.5 px-2 rounded-md border border-gray-200 dark:border-gray-700"
>
<Wrench className="w-2.5 h-2.5 text-gray-500" />
{s.name}
</span>
))
)}
</div>
</td>
<td className="py-3.5 px-4 align-middle">
<Badge className={
labor.status === 'active'
? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-950/60 dark:text-emerald-300 border-emerald-200 dark:border-emerald-800 font-semibold'
: 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400 border-gray-200 dark:border-gray-700 font-medium'
}>
<span className={`inline-block w-1.5 h-1.5 rounded-full mr-1.5 ${labor.status === 'active' ? 'bg-emerald-500 animate-pulse' : 'bg-gray-400'}`} />
{labor.status}
</Badge>
</td>
<td className="py-3.5 px-5 align-middle text-right">
<div className="flex justify-end gap-1">
<Button
variant="ghost"
size="icon"
onClick={() => openEditLabor(labor)}
className="h-7 w-7 text-gray-500 hover:text-emerald-600 hover:bg-emerald-50 dark:hover:bg-emerald-950/50 cursor-pointer"
title="Edit Labor Profile"
>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => promptDeleteLabor(labor)}
className="h-7 w-7 text-rose-500 hover:text-rose-700 hover:bg-rose-50 dark:hover:bg-rose-950/50 cursor-pointer"
title="Delete Labor Profile"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
<div className="p-3 bg-gray-50/50 dark:bg-gray-800/40 border-t border-gray-100 dark:border-gray-800 flex items-center justify-between text-xs text-gray-500">
<span>Total Registered Profiles: <strong>{labors.length} trades</strong></span>
<span>Skilled vs Unskilled: <strong>{stats.skilledCount} Skilled / {stats.unskilledCount} Unskilled</strong></span>
</div>
</CardContent>
</Card>
</TabsContent>
{/* 2. Master Skill Dictionary Tab Content */}
<TabsContent value="skill" className="mt-0">
<Card className="shadow-xs bg-white dark:bg-gray-900 border border-gray-100 dark:border-gray-800 overflow-hidden">
<CardHeader className="border-b border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/50 py-3.5 px-5">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<CardTitle className="text-sm font-bold text-gray-900 dark:text-white flex items-center gap-2">
<BookOpen className="w-4 h-4 text-indigo-500" /> Master Skill Dictionary
</CardTitle>
<CardDescription className="text-xs text-gray-500 mt-0.5">
Dictionary of construction trades, certifications, and specialized capabilities.
</CardDescription>
</div>
<div className="flex items-center gap-3">
<span className="text-xs text-gray-400 font-mono hidden sm:inline">
Showing {filteredSkills.length} of {skills.length}
</span>
<Button
onClick={openAddSkill}
className="bg-emerald-600 hover:bg-emerald-700 text-white font-semibold text-xs shadow-2xs flex items-center gap-1.5 h-8 px-3 cursor-pointer shrink-0"
>
<Plus className="h-3.5 w-3.5" /> Add Master Skill
</Button>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full text-left text-xs">
<thead>
<tr className="border-b border-gray-100 dark:border-gray-800 bg-gray-50/40 dark:bg-gray-800/30 text-gray-400 uppercase tracking-wider text-[10px] font-bold">
<th className="py-3 px-5">Skill Name</th>
<th className="py-3 px-4">Description & Scope</th>
<th className="py-3 px-4">Associated Trades</th>
<th className="py-3 px-5 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
{filteredSkills.length === 0 ? (
<tr>
<td colSpan={4} className="text-center py-12 text-gray-400">
<div className="flex flex-col items-center justify-center gap-2">
<div className="p-3 rounded-full bg-gray-50 dark:bg-gray-800 text-gray-400 border border-gray-100 dark:border-gray-700">
<BookOpen className="w-6 h-6" />
</div>
<p className="font-semibold text-gray-700 dark:text-gray-300 text-sm">
{searchQuery ? 'No matching skills found.' : 'No master skills registered yet.'}
</p>
<p className="text-xs text-gray-400">
{searchQuery ? 'Try adjusting your search query.' : 'Click "Add Master Skill" above to register trade capabilities.'}
</p>
</div>
</td>
</tr>
) : (
filteredSkills.map((skill) => {
const assignedCount = labors.filter(l => l.skills.some(s => s.id === skill.id)).length;
return (
<tr key={skill.id} className="group hover:bg-indigo-50/20 dark:hover:bg-indigo-950/10 transition-colors">
<td className="py-3.5 px-5 align-middle">
<div className="flex items-center gap-2">
<div className="p-1 rounded-md bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 border border-indigo-100 dark:border-indigo-900/50">
<Tag className="w-3 h-3" />
</div>
<span className="font-bold text-gray-900 dark:text-white text-xs group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors">
{skill.name}
</span>
</div>
</td>
<td className="py-3.5 px-4 align-middle text-gray-500 dark:text-gray-400 text-xs max-w-md">
{skill.description || <span className="italic text-gray-400">No description provided</span>}
</td>
<td className="py-3.5 px-4 align-middle">
{assignedCount > 0 ? (
<span className="inline-flex items-center gap-1 bg-emerald-50 dark:bg-emerald-950/50 text-emerald-700 dark:text-emerald-300 text-[11px] font-semibold py-0.5 px-2 rounded-md border border-emerald-200 dark:border-emerald-800">
<Users className="w-3 h-3 text-emerald-500" />
{assignedCount} {assignedCount === 1 ? 'Trade' : 'Trades'}
</span>
) : (
<span className="text-gray-400 italic text-[11px]">Unassigned</span>
)}
</td>
<td className="py-3.5 px-5 align-middle text-right">
<div className="flex justify-end gap-1">
<Button
variant="ghost"
size="icon"
onClick={() => openEditSkill(skill)}
className="h-7 w-7 text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-950/50 cursor-pointer"
title="Edit Skill"
>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => promptDeleteSkill(skill)}
className="h-7 w-7 text-rose-500 hover:text-rose-700 hover:bg-rose-50 dark:hover:bg-rose-950/50 cursor-pointer"
title="Delete Skill"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
<div className="p-3 bg-gray-50/50 dark:bg-gray-800/40 border-t border-gray-100 dark:border-gray-800 flex items-center justify-between text-xs text-gray-500">
<span>Registered Master Skills: <strong>{skills.length} skills</strong></span>
<span>Active Filtered: <strong>{filteredSkills.length}</strong></span>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
{/* Labor Modal */}
<Dialog open={laborModalOpen} onOpenChange={setLaborModalOpen}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-base font-bold text-gray-900 dark:text-white">
<Users className="h-5 w-5 text-emerald-600" />
{editingLabor ? 'Edit Labor Profile' : 'Add Labor Profile'}
</DialogTitle>
<DialogDescription className="text-xs text-gray-500">
Define labor record details, classification category, and select capability skillsets.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleLaborSubmit} className="space-y-4 pt-2">
<div className="space-y-1">
<Label htmlFor="name" className="text-xs font-semibold text-gray-700 dark:text-gray-300">Labor Trade Name *</Label>
<Input
id="name"
value={laborForm.data.name}
onChange={e => laborForm.setData('name', e.target.value)}
placeholder="e.g. Master Welder, Mason - Class A"
required
className="text-xs"
/>
{laborForm.errors.name && (
<p className="text-xs text-rose-500 mt-1 flex items-center gap-1">
<ShieldAlert className="h-3 w-3" /> {laborForm.errors.name}
</p>
)}
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label htmlFor="category" className="text-xs font-semibold text-gray-700 dark:text-gray-300">Classification *</Label>
<Select
value={laborForm.data.category}
onValueChange={(value) => laborForm.setData('category', (value as 'skilled' | 'unskilled') || 'skilled')}
>
<SelectTrigger id="category" className="text-xs">
<SelectValue placeholder="Select Category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="skilled">Skilled Labor</SelectItem>
<SelectItem value="unskilled">Unskilled / Helper</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label htmlFor="hourly_rate" className="text-xs font-semibold text-gray-700 dark:text-gray-300">Hourly Rate () *</Label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 font-bold text-sm"></span>
<Input
id="hourly_rate"
type="number"
step="0.01"
min="0"
value={laborForm.data.hourly_rate}
onChange={e => laborForm.setData('hourly_rate', e.target.value)}
placeholder="120.00"
required
className="pl-8 font-mono text-xs"
/>
</div>
</div>
</div>
<div className="space-y-1">
<Label htmlFor="labor_status" className="text-xs font-semibold text-gray-700 dark:text-gray-300">Status *</Label>
<Select
value={laborForm.data.status}
onValueChange={value => laborForm.setData('status', value || 'active')}
>
<SelectTrigger id="labor_status" className="text-xs">
<SelectValue placeholder="Select Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>
</div>
{/* Skill Selector */}
<div className="space-y-1.5">
<Label className="text-xs font-semibold text-gray-700 dark:text-gray-300 flex justify-between">
<span>Assign Skillsets ({laborForm.data.skills.length} selected)</span>
<span className="text-[10px] text-gray-400 font-normal">Click to toggle</span>
</Label>
{skills.length === 0 ? (
<div className="p-3 border border-dashed border-gray-200 dark:border-gray-700 rounded-lg text-center bg-gray-50/50 dark:bg-gray-800/50">
<p className="text-[11px] text-gray-500">No master skills found. Create them in the dictionary tab first.</p>
</div>
) : (
<div className="border border-gray-200 dark:border-gray-700 rounded-lg max-h-[140px] overflow-y-auto p-2 bg-gray-50/40 dark:bg-gray-800/40 space-y-1">
{skills.map((skill) => {
const isSelected = laborForm.data.skills.includes(skill.id);
return (
<div
key={skill.id}
onClick={() => toggleSkillSelection(skill.id)}
className={`flex items-center justify-between p-2 rounded-md cursor-pointer transition-colors text-xs select-none ${
isSelected
? 'bg-emerald-50 dark:bg-emerald-950/60 text-emerald-900 dark:text-emerald-200 font-semibold'
: 'hover:bg-white dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300'
}`}
>
<span className="flex items-center gap-1.5">
<Wrench className={`w-3 h-3 ${isSelected ? 'text-emerald-600 dark:text-emerald-400' : 'text-gray-400'}`} />
{skill.name}
</span>
{isSelected && <Check className="h-3.5 w-3.5 text-emerald-600 dark:text-emerald-400 stroke-[3px]" />}
</div>
);
})}
</div>
)}
</div>
<DialogFooter className="pt-3 border-t border-gray-100 dark:border-gray-800">
<Button type="button" variant="outline" onClick={() => setLaborModalOpen(false)}>
Cancel
</Button>
<Button type="submit" disabled={laborForm.processing} className="bg-emerald-600 hover:bg-emerald-700 text-white font-semibold">
{laborForm.processing ? 'Saving...' : 'Save Labor Profile'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
{/* Skill Modal */}
<Dialog open={skillModalOpen} onOpenChange={setSkillModalOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-base font-bold text-gray-900 dark:text-white">
<BookOpen className="h-5 w-5 text-indigo-600" />
{editingSkill ? 'Edit Master Skill' : 'Add Master Skill'}
</DialogTitle>
<DialogDescription className="text-xs text-gray-500">
Define new construction trade competency, certification, or capability category.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSkillSubmit} className="space-y-4 pt-2">
<div className="space-y-1">
<Label htmlFor="skill_name" className="text-xs font-semibold text-gray-700 dark:text-gray-300">Skill Name *</Label>
<Input
id="skill_name"
value={skillForm.data.name}
onChange={e => skillForm.setData('name', e.target.value)}
placeholder="e.g. Rebar Tying, 6G Pipe Welding, Concrete Finishing"
required
className="text-xs"
/>
{skillForm.errors.name && (
<p className="text-xs text-rose-500 mt-1 flex items-center gap-1">
<ShieldAlert className="h-3 w-3" /> {skillForm.errors.name}
</p>
)}
</div>
<div className="space-y-1">
<Label htmlFor="skill_desc" className="text-xs font-semibold text-gray-700 dark:text-gray-300">Description</Label>
<Textarea
id="skill_desc"
value={skillForm.data.description}
onChange={e => skillForm.setData('description', e.target.value)}
placeholder="Brief description of the skill competency or certification requirements."
className="text-xs"
/>
</div>
<DialogFooter className="pt-3 border-t border-gray-100 dark:border-gray-800">
<Button type="button" variant="outline" onClick={() => setSkillModalOpen(false)}>
Cancel
</Button>
<Button type="submit" disabled={skillForm.processing} className="bg-emerald-600 hover:bg-emerald-700 text-white font-semibold">
{skillForm.processing ? 'Saving...' : 'Save Skill'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
{/* Confirm Delete Labor Modal */}
<ConfirmModal
open={deleteLaborModalOpen}
onOpenChange={setDeleteLaborModalOpen}
title="Delete Labor Profile"
message={`Are you sure you want to delete "${laborToDelete?.name}"? This will remove this trade profile and its associated task allocations.`}
confirmText="Delete Labor"
cancelText="Cancel"
variant="destructive"
loading={laborForm.processing}
onConfirm={handleConfirmLaborDelete}
/>
{/* Confirm Delete Skill Modal */}
<ConfirmModal
open={deleteSkillModalOpen}
onOpenChange={setDeleteSkillModalOpen}
title="Delete Master Skill"
message={`Are you sure you want to delete skill "${skillToDelete?.name}"? It will be unlinked from all assigned labor profiles.`}
confirmText="Delete Skill"
cancelText="Cancel"
variant="destructive"
loading={skillForm.processing}
onConfirm={handleConfirmSkillDelete}
/>
</AuthenticatedLayout>
);
}