260 lines
15 KiB
TypeScript
260 lines
15 KiB
TypeScript
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
|
import { Head, Link, useForm } from '@inertiajs/react';
|
|
import { Button } from '@/Components/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/Components/ui/card';
|
|
import { Input } from '@/Components/ui/input';
|
|
import { Label } from '@/Components/ui/label';
|
|
import { Separator } from '@/Components/ui/separator';
|
|
import { ArrowLeft, Save, Send, HardHat, ShieldCheck, FileText } from 'lucide-react';
|
|
import { PageProps } from '@/types';
|
|
import { FormEvent } from 'react';
|
|
|
|
interface Props extends PageProps {
|
|
project: { id: number; ulid: string; name: string; code: string };
|
|
defaultPeriodStart: string;
|
|
defaultPeriodEnd: string;
|
|
}
|
|
|
|
export default function Create({ project, defaultPeriodStart, defaultPeriodEnd }: Props) {
|
|
const form = useForm({
|
|
period_start: defaultPeriodStart,
|
|
period_end: defaultPeriodEnd,
|
|
// Workforce
|
|
active_workforce: '',
|
|
period_man_hours: '',
|
|
logistics_km: '',
|
|
// HSE Proactive
|
|
toolbox_meetings: '',
|
|
safety_observations: '',
|
|
// HSE Reactive
|
|
fatalities: '0',
|
|
major_injuries: '0',
|
|
first_aid_cases: '0',
|
|
medical_cases: '0',
|
|
near_misses: '0',
|
|
environmental_damage: '0',
|
|
property_damage: '0',
|
|
fines: '0',
|
|
// Narrative
|
|
narrative_status: '',
|
|
narrative_weather: '',
|
|
narrative_compliance: '',
|
|
});
|
|
|
|
const handleSubmit = (e: FormEvent) => {
|
|
e.preventDefault();
|
|
form.post(route('projects.reports.store', project.ulid));
|
|
};
|
|
|
|
const fieldError = (field: string) => {
|
|
const err = (form.errors as Record<string, string>)[field];
|
|
return err ? <p className="mt-1 text-sm text-red-500">{err}</p> : null;
|
|
};
|
|
|
|
return (
|
|
<AuthenticatedLayout
|
|
header={
|
|
<div className="flex items-center gap-4">
|
|
<Link href={route('projects.reports.index', 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">New Status Report</h2>
|
|
<p className="text-sm text-gray-500">{project.name} ({project.code})</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
>
|
|
<Head title={`New Report - ${project.name}`} />
|
|
|
|
<div className="py-6">
|
|
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{/* Period */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Reporting Period</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<Label htmlFor="period_start">Period Start *</Label>
|
|
<Input id="period_start" type="date" value={form.data.period_start}
|
|
onChange={e => form.setData('period_start', e.target.value)} />
|
|
{fieldError('period_start')}
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="period_end">Period End *</Label>
|
|
<Input id="period_end" type="date" value={form.data.period_end}
|
|
onChange={e => form.setData('period_end', e.target.value)} />
|
|
{fieldError('period_end')}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Workforce */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<HardHat className="h-5 w-5 text-amber-600" />
|
|
Workforce & Productivity
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
|
<div>
|
|
<Label htmlFor="active_workforce">Active Workforce</Label>
|
|
<Input id="active_workforce" type="number" min="0" placeholder="e.g. 78"
|
|
value={form.data.active_workforce}
|
|
onChange={e => form.setData('active_workforce', e.target.value)} />
|
|
{fieldError('active_workforce')}
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="period_man_hours">Period Man-hours</Label>
|
|
<Input id="period_man_hours" type="number" min="0" step="0.01" placeholder="e.g. 4368"
|
|
value={form.data.period_man_hours}
|
|
onChange={e => form.setData('period_man_hours', e.target.value)} />
|
|
{fieldError('period_man_hours')}
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="logistics_km">Logistics (km driven)</Label>
|
|
<Input id="logistics_km" type="number" min="0" step="0.01" placeholder="e.g. 25200"
|
|
value={form.data.logistics_km}
|
|
onChange={e => form.setData('logistics_km', e.target.value)} />
|
|
{fieldError('logistics_km')}
|
|
</div>
|
|
</div>
|
|
<p className="mt-3 text-xs text-gray-400">
|
|
Cumulative man-hours will be calculated automatically from previous approved reports.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* HSE */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<ShieldCheck className="h-5 w-5 text-emerald-600" />
|
|
HSE Performance
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
{/* Proactive */}
|
|
<div>
|
|
<h4 className="text-sm font-medium text-gray-700 mb-3">Proactive Safety Measures</h4>
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<Label htmlFor="toolbox_meetings">Toolbox / Tailgate Meetings</Label>
|
|
<Input id="toolbox_meetings" type="number" min="0" placeholder="e.g. 6"
|
|
value={form.data.toolbox_meetings}
|
|
onChange={e => form.setData('toolbox_meetings', e.target.value)} />
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="safety_observations">Safety Observations</Label>
|
|
<Input id="safety_observations" type="number" min="0" placeholder="e.g. 7"
|
|
value={form.data.safety_observations}
|
|
onChange={e => form.setData('safety_observations', e.target.value)} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* Reactive */}
|
|
<div>
|
|
<h4 className="text-sm font-medium text-gray-700 mb-1">Reactive Safety Incidents</h4>
|
|
<p className="text-xs text-gray-400 mb-3">All fields should ideally remain at zero.</p>
|
|
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
|
{[
|
|
{ id: 'fatalities', label: 'Fatalities' },
|
|
{ id: 'major_injuries', label: 'Major Injuries' },
|
|
{ id: 'first_aid_cases', label: 'First Aid Cases' },
|
|
{ id: 'medical_cases', label: 'Medical Cases' },
|
|
{ id: 'near_misses', label: 'Near Misses' },
|
|
{ id: 'environmental_damage', label: 'Environmental' },
|
|
{ id: 'property_damage', label: 'Property Damage' },
|
|
{ id: 'fines', label: 'Fines / Costs (₱)' },
|
|
].map(field => (
|
|
<div key={field.id}>
|
|
<Label htmlFor={field.id}>{field.label}</Label>
|
|
<Input
|
|
id={field.id}
|
|
type="number"
|
|
min="0"
|
|
step={field.id === 'fines' ? '0.01' : '1'}
|
|
value={(form.data as Record<string, string>)[field.id]}
|
|
onChange={e => form.setData(field.id as keyof typeof form.data, e.target.value)}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Narrative */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<FileText className="h-5 w-5 text-blue-600" />
|
|
Weekly Narrative
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="narrative_status">Operations Status</Label>
|
|
<textarea
|
|
id="narrative_status"
|
|
rows={3}
|
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-gray-500 focus:ring-1 focus:ring-gray-500"
|
|
placeholder="e.g. Normal operations. All activities on track..."
|
|
value={form.data.narrative_status}
|
|
onChange={e => form.setData('narrative_status', e.target.value)}
|
|
/>
|
|
{fieldError('narrative_status')}
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="narrative_weather">Weather Impact</Label>
|
|
<textarea
|
|
id="narrative_weather"
|
|
rows={2}
|
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-gray-500 focus:ring-1 focus:ring-gray-500"
|
|
placeholder="e.g. Intermittent rainfall due to tropical weather..."
|
|
value={form.data.narrative_weather}
|
|
onChange={e => form.setData('narrative_weather', e.target.value)}
|
|
/>
|
|
{fieldError('narrative_weather')}
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="narrative_compliance">Compliance Notes</Label>
|
|
<textarea
|
|
id="narrative_compliance"
|
|
rows={2}
|
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-gray-500 focus:ring-1 focus:ring-gray-500"
|
|
placeholder="e.g. Toolbox meetings conducted consistently for worker alignment..."
|
|
value={form.data.narrative_compliance}
|
|
onChange={e => form.setData('narrative_compliance', e.target.value)}
|
|
/>
|
|
{fieldError('narrative_compliance')}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Actions */}
|
|
<div className="flex justify-end gap-3">
|
|
<Link href={route('projects.reports.index', project.ulid)}>
|
|
<Button type="button" variant="outline">Cancel</Button>
|
|
</Link>
|
|
<Button type="submit" disabled={form.processing}>
|
|
<Save className="mr-2 h-4 w-4" />
|
|
Save as Draft
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</AuthenticatedLayout>
|
|
);
|
|
}
|