Files
GSB-Construction/Modules/ProjectManagement/resources/js/Pages/Reports/Index.tsx

244 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}