244 lines
13 KiB
TypeScript
244 lines
13 KiB
TypeScript
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
||
import { Head, Link, router, usePage } from '@inertiajs/react';
|
||
import { Badge } from '@/Components/ui/badge';
|
||
import { Button } from '@/Components/ui/button';
|
||
import { Card, CardContent } from '@/Components/ui/card';
|
||
import { DataTableToolbar } from '@/Components/DataTableToolbar';
|
||
import {
|
||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||
} from '@/Components/ui/select';
|
||
import {
|
||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||
} from '@/Components/ui/table';
|
||
import { PaginatedData, PageProps } from '@/types';
|
||
import { Plus, ArrowLeft, Eye, Pencil, Trash2, ClipboardList } from 'lucide-react';
|
||
import { useMemo, useState } from 'react';
|
||
|
||
interface WorkforceMetric {
|
||
active_workforce: number;
|
||
period_man_hours: string;
|
||
cumulative_man_hours: string;
|
||
logistics_km: string;
|
||
}
|
||
|
||
interface HseRecord {
|
||
toolbox_meetings: number;
|
||
safety_observations: number;
|
||
fatalities: number;
|
||
major_injuries: number;
|
||
first_aid_cases: number;
|
||
medical_cases: number;
|
||
near_misses: number;
|
||
environmental_damage: number;
|
||
property_damage: number;
|
||
fines: string;
|
||
}
|
||
|
||
interface Report {
|
||
id: number; ulid: string;
|
||
period_start: string;
|
||
period_end: string;
|
||
status: string;
|
||
submitted_by: number | null;
|
||
created_at: string;
|
||
submitter?: { id: number; ulid: string; name: string };
|
||
workforce_metric?: WorkforceMetric;
|
||
hse_record?: HseRecord;
|
||
}
|
||
|
||
interface StatusOption { value: string; label: string }
|
||
|
||
interface Props extends PageProps {
|
||
project: { id: number; ulid: string; name: string; code: string };
|
||
reports: PaginatedData<Report>;
|
||
filters: { status?: string };
|
||
statuses: StatusOption[];
|
||
}
|
||
|
||
const statusVariant = (status: string) => {
|
||
switch (status) {
|
||
case 'draft': return 'outline' as const;
|
||
case 'submitted': return 'secondary' as const;
|
||
case 'in_review': return 'default' as const;
|
||
case 'approved': return 'default' as const;
|
||
case 'rejected': return 'destructive' as const;
|
||
default: return 'outline' as const;
|
||
}
|
||
};
|
||
|
||
const statusLabel = (s: string) => s.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
||
const formatNumber = (v: string | number) => new Intl.NumberFormat('en-PH').format(Number(v));
|
||
|
||
function totalIncidents(hse?: HseRecord): number {
|
||
if (!hse) return 0;
|
||
return hse.fatalities + hse.major_injuries + hse.first_aid_cases
|
||
+ hse.medical_cases + hse.near_misses + hse.environmental_damage + hse.property_damage;
|
||
}
|
||
|
||
export default function Index({ project, reports, filters, statuses }: Props) {
|
||
const { flash } = usePage<PageProps>().props;
|
||
const [statusFilter, setStatusFilter] = useState(filters.status || 'all');
|
||
|
||
// Items array for Select label lookup
|
||
const statusFilterItems = useMemo(() => [{ value: 'all', label: 'All Statuses' }, ...statuses.map(s => ({ value: s.value, label: s.label }))], [statuses]);
|
||
|
||
const applyFilter = (value: string | null) => {
|
||
const v = value ?? 'all';
|
||
setStatusFilter(v);
|
||
router.get(route('projects.reports.index', project.ulid), {
|
||
status: v !== 'all' ? v : undefined,
|
||
}, { preserveState: true, replace: true });
|
||
};
|
||
|
||
const handleDelete = (report: Report) => {
|
||
if (confirm('Delete this draft report?')) {
|
||
router.delete(route('projects.reports.destroy', [project.ulid, report.ulid]));
|
||
}
|
||
};
|
||
|
||
return (
|
||
<AuthenticatedLayout
|
||
header={
|
||
<div className="flex items-center gap-4">
|
||
<Link href={route('projects.show', project.ulid)}>
|
||
<Button variant="ghost" size="icon-sm"><ArrowLeft className="h-4 w-4" /></Button>
|
||
</Link>
|
||
<div>
|
||
<h2 className="text-xl font-semibold leading-tight text-gray-800">
|
||
<ClipboardList className="inline mr-2 h-5 w-5" />
|
||
Status Reports
|
||
</h2>
|
||
<p className="text-sm text-gray-500">{project.name} ({project.code})</p>
|
||
</div>
|
||
</div>
|
||
}
|
||
>
|
||
<Head title={`Reports - ${project.name}`} />
|
||
|
||
<div className="py-6">
|
||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||
{flash?.success && <div className="mb-4 rounded-md bg-green-50 p-4 text-sm text-green-700">{flash.success}</div>}
|
||
{flash?.error && <div className="mb-4 rounded-md bg-red-50 p-4 text-sm text-red-700">{flash.error}</div>}
|
||
|
||
<Card>
|
||
<DataTableToolbar
|
||
filters={
|
||
<Select value={statusFilter} onValueChange={applyFilter} items={statusFilterItems}>
|
||
<SelectTrigger id="filter-report-status" className="w-[180px]">
|
||
<SelectValue placeholder="Filter by status" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="all">All Statuses</SelectItem>
|
||
{statuses.map((s) => (
|
||
<SelectItem key={s.value} value={s.value}>{s.label}</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
}
|
||
actions={
|
||
<Link href={route('projects.reports.create', project.ulid)}>
|
||
<Button size="sm"><Plus className="mr-2 h-4 w-4" /> New Report</Button>
|
||
</Link>
|
||
}
|
||
/>
|
||
<CardContent>
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>Period</TableHead>
|
||
<TableHead>Status</TableHead>
|
||
<TableHead className="text-right">Man-hours</TableHead>
|
||
<TableHead className="text-right">Workers</TableHead>
|
||
<TableHead className="text-right">Incidents</TableHead>
|
||
<TableHead>Submitted By</TableHead>
|
||
<TableHead className="text-right">Actions</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{reports.data.length === 0 ? (
|
||
<TableRow>
|
||
<TableCell colSpan={7} className="text-center text-gray-500 py-8">
|
||
No reports yet. Create your first status report.
|
||
</TableCell>
|
||
</TableRow>
|
||
) : (
|
||
reports.data.map((report) => (
|
||
<TableRow key={report.id}>
|
||
<TableCell className="font-medium">
|
||
{new Date(report.period_start).toLocaleDateString('en-PH', { month: 'short', day: 'numeric' })}
|
||
{' – '}
|
||
{new Date(report.period_end).toLocaleDateString('en-PH', { month: 'short', day: 'numeric', year: 'numeric' })}
|
||
</TableCell>
|
||
<TableCell>
|
||
<Badge variant={statusVariant(report.status)}>
|
||
{statusLabel(report.status)}
|
||
</Badge>
|
||
</TableCell>
|
||
<TableCell className="text-right tabular-nums">
|
||
{report.workforce_metric ? formatNumber(report.workforce_metric.period_man_hours) : '-'}
|
||
</TableCell>
|
||
<TableCell className="text-right tabular-nums">
|
||
{report.workforce_metric?.active_workforce ?? '-'}
|
||
</TableCell>
|
||
<TableCell className="text-right">
|
||
{totalIncidents(report.hse_record) === 0 ? (
|
||
<span className="text-green-600 font-medium">0 ✓</span>
|
||
) : (
|
||
<span className="text-red-600 font-medium">{totalIncidents(report.hse_record)}</span>
|
||
)}
|
||
</TableCell>
|
||
<TableCell className="text-gray-500">{report.submitter?.name || '-'}</TableCell>
|
||
<TableCell className="text-right">
|
||
<div className="flex items-center justify-end gap-1">
|
||
<Link href={route('projects.reports.show', [project.ulid, report.ulid])}>
|
||
<Button variant="ghost" size="icon-sm" title="View">
|
||
<Eye className="h-4 w-4" />
|
||
</Button>
|
||
</Link>
|
||
{(report.status === 'draft' || report.status === 'rejected') && (
|
||
<Link href={route('projects.reports.edit', [project.ulid, report.ulid])}>
|
||
<Button variant="ghost" size="icon-sm" title="Edit">
|
||
<Pencil className="h-4 w-4" />
|
||
</Button>
|
||
</Link>
|
||
)}
|
||
{report.status === 'draft' && (
|
||
<Button variant="ghost" size="icon-sm" title="Delete" onClick={() => handleDelete(report)}>
|
||
<Trash2 className="h-4 w-4 text-red-500" />
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
))
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
|
||
{reports.last_page > 1 && (
|
||
<div className="mt-4 flex items-center justify-between">
|
||
<p className="text-sm text-gray-600">
|
||
Showing {reports.from} to {reports.to} of {reports.total}
|
||
</p>
|
||
<div className="flex gap-1">
|
||
{reports.prev_page_url && (
|
||
<Link href={reports.prev_page_url}>
|
||
<Button variant="outline" size="sm">Previous</Button>
|
||
</Link>
|
||
)}
|
||
{reports.next_page_url && (
|
||
<Link href={reports.next_page_url}>
|
||
<Button variant="outline" size="sm">Next</Button>
|
||
</Link>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
</AuthenticatedLayout>
|
||
);
|
||
}
|