phosphat-report-app/app/routes/reports_.$id.edit.tsx
2025-12-04 09:05:38 +03:00

1198 lines
56 KiB
TypeScript

import type { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, useActionData, useLoaderData, useNavigation, Link } from "@remix-run/react";
import { requireAuthLevel } from "~/utils/auth.server";
import DashboardLayout from "~/components/DashboardLayout";
import { useState, useEffect } from "react";
import { prisma } from "~/utils/db.server";
export const meta: MetaFunction = () => [{ title: "Edit Report - Alhaffer Report System" }];
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const user = await requireAuthLevel(request, 1);
const reportId = params.id;
if (!reportId) {
throw new Response("Report ID is required", { status: 400 });
}
// Get the report to edit
const report = await prisma.report.findUnique({
where: { id: parseInt(reportId) },
include: {
employee: { select: { name: true } },
area: { select: { name: true } },
dredgerLocation: { select: { name: true, class: true } },
reclamationLocation: { select: { name: true } },
shiftWorkers: {
include: {
worker: { select: { id: true, name: true, status: true } }
}
}
}
});
if (!report) {
throw new Response("Report not found", { status: 404 });
}
// Check permissions
if (user.authLevel < 2 && report.employeeId !== user.id) {
throw new Response("You can only edit your own reports", { status: 403 });
}
// Get dropdown data for form
const [foremen, equipment, workers] = await Promise.all([
prisma.foreman.findMany({ orderBy: { name: 'asc' } }),
prisma.equipment.findMany({ orderBy: [{ category: 'asc' }, { model: 'asc' }, { number: 'asc' }] }),
prisma.worker.findMany({ where: { status: 'active' }, orderBy: { name: 'asc' } })
]);
return json({
user,
report,
foremen,
equipment,
workers
});
};
export const action = async ({ request, params }: ActionFunctionArgs) => {
const user = await requireAuthLevel(request, 1);
const reportId = params.id;
if (!reportId) {
return json({ errors: { form: "Report ID is required" } }, { status: 400 });
}
const existingReport = await prisma.report.findUnique({
where: { id: parseInt(reportId) },
select: {
employeeId: true,
createdDate: true,
shift: true,
areaId: true,
dredgerLocationId: true,
reclamationLocationId: true
}
});
if (!existingReport) {
return json({ errors: { form: "Report not found" } }, { status: 404 });
}
if (user.authLevel < 2) {
if (existingReport.employeeId !== user.id) {
return json({ errors: { form: "You can only edit your own reports" } }, { status: 403 });
}
const latestUserReport = await prisma.report.findFirst({
where: { employeeId: user.id },
orderBy: { createdDate: 'desc' },
select: { id: true }
});
if (!latestUserReport || latestUserReport.id !== parseInt(reportId)) {
return json({ errors: { form: "You can only edit your latest report" } }, { status: 403 });
}
}
const formData = await request.formData();
const dredgerLineLength = formData.get("dredgerLineLength");
const shoreConnection = formData.get("shoreConnection");
const notes = formData.get("notes");
const reclamationHeightBase = formData.get("reclamationHeightBase");
const reclamationHeightExtra = formData.get("reclamationHeightExtra");
const pipelineMain = formData.get("pipelineMain");
const pipelineExt1 = formData.get("pipelineExt1");
const pipelineReserve = formData.get("pipelineReserve");
const pipelineExt2 = formData.get("pipelineExt2");
const statsDozers = formData.get("statsDozers");
const statsExc = formData.get("statsExc");
const statsLoaders = formData.get("statsLoaders");
const statsForeman = formData.get("statsForeman");
const statsLaborer = formData.get("statsLaborer");
const workersListData = formData.get("workersList");
const timeSheetData = formData.get("timeSheetData");
const stoppagesData = formData.get("stoppagesData");
if (typeof dredgerLineLength !== "string" || isNaN(parseInt(dredgerLineLength))) {
return json({ errors: { dredgerLineLength: "Valid dredger line length is required" } }, { status: 400 });
}
if (typeof shoreConnection !== "string" || isNaN(parseInt(shoreConnection))) {
return json({ errors: { shoreConnection: "Valid shore connection is required" } }, { status: 400 });
}
try {
let timeSheet = [];
let stoppages = [];
let workersList = [];
if (timeSheetData && typeof timeSheetData === "string") {
try { timeSheet = JSON.parse(timeSheetData); } catch (e) { timeSheet = []; }
}
if (stoppagesData && typeof stoppagesData === "string") {
try { stoppages = JSON.parse(stoppagesData); } catch (e) { stoppages = []; }
}
if (workersListData && typeof workersListData === "string") {
try { workersList = JSON.parse(workersListData); } catch (e) { workersList = []; }
}
const ext1Value = parseInt(pipelineExt1 as string) || 0;
const ext2Value = parseInt(pipelineExt2 as string) || 0;
const shiftText = existingReport.shift === 'day' ? 'Day' : 'Night';
let automaticNotes = [];
if (ext1Value > 0) automaticNotes.push(`Main Extension ${ext1Value}m ${shiftText}`);
if (ext2Value > 0) automaticNotes.push(`Reserve Extension ${ext2Value}m ${shiftText}`);
let finalNotes = typeof notes === "string" ? notes : '';
if (automaticNotes.length > 0) {
const automaticNotesText = automaticNotes.join(', ');
finalNotes = finalNotes.trim() ? `${automaticNotesText}.\n${finalNotes}` : automaticNotesText;
}
await prisma.report.update({
where: { id: parseInt(reportId) },
data: {
dredgerLineLength: parseInt(dredgerLineLength),
shoreConnection: parseInt(shoreConnection),
reclamationHeight: {
base: parseInt(reclamationHeightBase as string) || 0,
extra: parseInt(reclamationHeightExtra as string) || 0
},
pipelineLength: {
main: parseInt(pipelineMain as string) || 0,
ext1: ext1Value,
reserve: parseInt(pipelineReserve as string) || 0,
ext2: ext2Value
},
stats: {
Dozers: parseInt(statsDozers as string) || 0,
Exc: parseInt(statsExc as string) || 0,
Loaders: parseInt(statsLoaders as string) || 0,
Foreman: statsForeman as string || "",
Laborer: parseInt(statsLaborer as string) || 0
},
timeSheet,
stoppages,
notes: finalNotes || null
}
});
// Update workers
await prisma.shiftWorker.deleteMany({ where: { reportId: parseInt(reportId) } });
if (workersList.length > 0) {
await prisma.shiftWorker.createMany({
data: workersList.map((workerId: number) => ({
reportId: parseInt(reportId),
workerId: workerId
}))
});
}
return redirect("/reports?success=Report updated successfully!");
} catch (error) {
return json({ errors: { form: "Failed to update report. Please try again." } }, { status: 400 });
}
};
export default function EditReport() {
const { user, report, foremen, equipment, workers } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const [formData, setFormData] = useState({
dredgerLineLength: report.dredgerLineLength.toString(),
shoreConnection: report.shoreConnection.toString(),
reclamationHeightBase: (report.reclamationHeight as any).base?.toString() || '0',
reclamationHeightExtra: (report.reclamationHeight as any).extra?.toString() || '0',
pipelineMain: (report.pipelineLength as any).main?.toString() || '0',
pipelineExt1: (report.pipelineLength as any).ext1?.toString() || '0',
pipelineReserve: (report.pipelineLength as any).reserve?.toString() || '0',
pipelineExt2: (report.pipelineLength as any).ext2?.toString() || '0',
statsDozers: (report.stats as any).Dozers?.toString() || '0',
statsExc: (report.stats as any).Exc?.toString() || '0',
statsLoaders: (report.stats as any).Loaders?.toString() || '0',
statsForeman: (report.stats as any).Foreman || '',
notes: report.notes || ''
});
const [selectedWorkers, setSelectedWorkers] = useState<number[]>(
report.shiftWorkers?.map((sw: any) => sw.worker.id) || []
);
const [workerSearchTerm, setWorkerSearchTerm] = useState('');
const [isLaborerManuallyEdited, setIsLaborerManuallyEdited] = useState(false);
const [timeSheetEntries, setTimeSheetEntries] = useState<Array<{
id: string,
machine: string,
from1: string,
to1: string,
from2: string,
to2: string,
total: string,
reason: string
}>>(Array.isArray(report.timeSheet) ? (report.timeSheet as any[]).map((entry: any, index: number) => ({
...entry,
id: entry.id || `existing-${index}`
})) : []);
const [stoppageEntries, setStoppageEntries] = useState<Array<{
id: string,
from: string,
to: string,
total: string,
reason: string,
responsible: string,
note: string
}>>(Array.isArray(report.stoppages) ? (report.stoppages as any[]).map((entry: any, index: number) => ({
...entry,
id: entry.id || `existing-${index}`
})) : []);
const [currentStep, setCurrentStep] = useState(1);
const totalSteps = 3;
const isSubmitting = navigation.state === "submitting";
const updateFormData = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
if (currentStep !== totalSteps) {
event.preventDefault();
event.stopPropagation();
return false;
}
const invalidStoppages = stoppageEntries.filter(entry =>
entry.responsible === 'reclamation' && !entry.note.trim()
);
if (invalidStoppages.length > 0) {
alert('Please add notes for all reclamation stoppages before submitting.');
event.preventDefault();
event.stopPropagation();
return false;
}
};
// Helper functions for time calculations
const calculateTimeDifference = (from1: string, to1: string, from2: string, to2: string) => {
if (!from1 || !to1) return "00:00";
const parseTime = (timeStr: string) => {
const [hours, minutes] = timeStr.split(':').map(Number);
return hours * 60 + minutes;
};
const formatTime = (minutes: number) => {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`;
};
let totalMinutes = 0;
if (from1 && to1) {
const start1 = parseTime(from1);
let end1 = parseTime(to1);
if (end1 < start1) end1 += 24 * 60;
totalMinutes += end1 - start1;
}
if (from2 && to2) {
const start2 = parseTime(from2);
let end2 = parseTime(to2);
if (end2 < start2) end2 += 24 * 60;
totalMinutes += end2 - start2;
}
return formatTime(Math.max(0, totalMinutes));
};
const calculateStoppageTime = (from: string, to: string) => {
if (!from || !to) return "00:00";
const parseTime = (timeStr: string) => {
const [hours, minutes] = timeStr.split(':').map(Number);
return hours * 60 + minutes;
};
const formatTime = (minutes: number) => {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`;
};
const startMinutes = parseTime(from);
let endMinutes = parseTime(to);
if (endMinutes < startMinutes) endMinutes += 24 * 60;
const totalMinutes = Math.max(0, endMinutes - startMinutes);
return formatTime(totalMinutes);
};
// Time Sheet management
const addTimeSheetEntry = () => {
const newEntry = {
id: Date.now().toString(),
machine: '',
from1: '',
to1: '',
from2: '',
to2: '',
total: '00:00',
reason: ''
};
setTimeSheetEntries([...timeSheetEntries, newEntry]);
};
const removeTimeSheetEntry = (id: string) => {
setTimeSheetEntries(timeSheetEntries.filter(entry => entry.id !== id));
};
const updateTimeSheetEntry = (id: string, field: string, value: string) => {
setTimeSheetEntries(timeSheetEntries.map(entry => {
if (entry.id === id) {
const updatedEntry = { ...entry, [field]: value };
if (['from1', 'to1', 'from2', 'to2'].includes(field)) {
updatedEntry.total = calculateTimeDifference(
updatedEntry.from1,
updatedEntry.to1,
updatedEntry.from2,
updatedEntry.to2
);
}
return updatedEntry;
}
return entry;
}));
};
// Auto-calculate equipment counts based on time sheet entries
useEffect(() => {
const counts = { dozers: 0, excavators: 0, loaders: 0 };
timeSheetEntries.forEach(entry => {
if (entry.machine) {
const equipmentItem = equipment.find(item =>
`${item.model} (${item.number})` === entry.machine
);
if (equipmentItem) {
const category = equipmentItem.category.toLowerCase();
if (category.includes('dozer')) {
counts.dozers++;
} else if (category.includes('excavator')) {
counts.excavators++;
} else if (category.includes('loader')) {
counts.loaders++;
}
}
}
});
setFormData(prev => ({
...prev,
statsDozers: counts.dozers.toString(),
statsExc: counts.excavators.toString(),
statsLoaders: counts.loaders.toString()
}));
}, [timeSheetEntries, equipment]);
// Stoppage management
const addStoppageEntry = () => {
const newEntry = {
id: Date.now().toString(),
from: '',
to: '',
total: '00:00',
reason: '',
responsible: 'reclamation',
note: ''
};
setStoppageEntries([...stoppageEntries, newEntry]);
};
const removeStoppageEntry = (id: string) => {
setStoppageEntries(stoppageEntries.filter(entry => entry.id !== id));
};
const updateStoppageEntry = (id: string, field: string, value: string) => {
setStoppageEntries(stoppageEntries.map(entry => {
if (entry.id === id) {
const updatedEntry = { ...entry, [field]: value };
if (['from', 'to'].includes(field)) {
updatedEntry.total = calculateStoppageTime(updatedEntry.from, updatedEntry.to);
}
if (field === 'responsible') {
if (value === 'reclamation') {
updatedEntry.reason = '';
}
}
return updatedEntry;
}
return entry;
}));
};
const nextStep = (event?: React.MouseEvent<HTMLButtonElement>) => {
if (event) {
event.preventDefault();
event.stopPropagation();
}
if (currentStep < totalSteps) {
setCurrentStep(currentStep + 1);
}
};
const prevStep = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
const getStepTitle = (step: number) => {
switch (step) {
case 1: return "Pipeline Details";
case 2: return "Equipment & Time Sheet";
case 3: return "Stoppages & Notes";
default: return "";
}
};
const isCurrentStepValid = () => {
return true; // All steps are optional for editing
};
// Auto-update laborer count when workers change (only if not manually edited)
useEffect(() => {
if (!isLaborerManuallyEdited) {
setFormData(prev => ({
...prev,
statsLaborer: selectedWorkers.length.toString()
}));
}
}, [selectedWorkers, isLaborerManuallyEdited]);
// Worker selection functions
const toggleWorker = (workerId: number) => {
setSelectedWorkers(prev =>
prev.includes(workerId)
? prev.filter(id => id !== workerId)
: [...prev, workerId]
);
};
const filteredWorkers = workers.filter(worker =>
worker.name.toLowerCase().includes(workerSearchTerm.toLowerCase()) &&
!selectedWorkers.includes(worker.id)
);
return (
<DashboardLayout user={user}>
<div className="max-w-full mx-auto">
{/* Header */}
<div className="mb-6 sm:mb-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between space-y-4 sm:space-y-0">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900">Edit Report</h1>
<p className="mt-2 text-sm sm:text-base text-gray-600">Update shift details</p>
</div>
<Link
to="/reports"
className="inline-flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to Reports
</Link>
</div>
</div>
{/* Progress Steps */}
<div className="mb-6 sm:mb-8">
<div className="flex items-center justify-between px-2 sm:px-0">
{[1, 2, 3].map((step) => (
<div key={step} className="flex items-center">
<div className={`flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-full border-2 ${step <= currentStep
? 'bg-indigo-600 border-indigo-600 text-white'
: 'border-gray-300 text-gray-500'
}`}>
{step < currentStep ? (
<svg className="w-4 h-4 sm:w-6 sm:h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : (
<span className="text-xs sm:text-sm font-medium">{step}</span>
)}
</div>
{step < totalSteps && (
<div className={`flex-1 h-1 mx-2 sm:mx-4 ${step < currentStep ? 'bg-indigo-600' : 'bg-gray-300'
}`} />
)}
</div>
))}
</div>
<div className="mt-4 text-center">
<h2 className="text-lg sm:text-xl font-semibold text-gray-900">{getStepTitle(currentStep)}</h2>
<p className="text-xs sm:text-sm text-gray-500">Step {currentStep} of {totalSteps}</p>
</div>
</div>
{/* Form */}
<Form method="post" onSubmit={handleSubmit} className="bg-white shadow-lg rounded-lg overflow-hidden">
<div className="p-4 sm:p-6">
{/* Step 1: Locked Fields Display + Pipeline Details */}
{currentStep === 1 && (
<div className="space-y-6">
{/* Locked Fields Display */}
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
<h3 className="text-sm font-medium text-gray-700 mb-3">Report Information (Cannot be changed)</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium text-gray-600">Date:</span>
<span className="ml-2 text-gray-900">{new Date(report.createdDate).toLocaleDateString('en-GB')}</span>
</div>
<div>
<span className="font-medium text-gray-600">Shift:</span>
<span className={`ml-2 px-2 py-1 rounded-full text-xs font-medium ${report.shift === 'day' ? 'bg-yellow-100 text-yellow-800' : 'bg-blue-100 text-blue-800'}`}>
{report.shift.charAt(0).toUpperCase() + report.shift.slice(1)}
</span>
</div>
<div>
<span className="font-medium text-gray-600">Area:</span>
<span className="ml-2 text-gray-900">{report.area.name}</span>
</div>
<div>
<span className="font-medium text-gray-600">Dredger Location:</span>
<span className="ml-2 text-gray-900">{report.dredgerLocation.name}</span>
</div>
<div className="col-span-2">
<span className="font-medium text-gray-600">Reclamation Location:</span>
<span className="ml-2 text-gray-900">{report.reclamationLocation.name}</span>
</div>
</div>
</div>
{/* Editable Dredger Line Length and Shore Connection */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<div>
<label htmlFor="dredgerLineLength" className="block text-sm font-medium text-gray-700 mb-2">
Dredger Line Length (m) <span className="text-red-500">*</span>
</label>
<input
type="number"
id="dredgerLineLength"
name="dredgerLineLength"
required
min="0"
value={formData.dredgerLineLength}
onChange={(e) => updateFormData('dredgerLineLength', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label htmlFor="shoreConnection" className="block text-sm font-medium text-gray-700 mb-2">
Shore Connection <span className="text-red-500">*</span>
</label>
<input
type="number"
id="shoreConnection"
name="shoreConnection"
required
min="0"
value={formData.shoreConnection}
onChange={(e) => updateFormData('shoreConnection', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
</div>
<div>
<h3 className="text-base sm:text-lg font-medium text-gray-900 mb-4">Reclamation Height</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<div>
<label htmlFor="reclamationHeightBase" className="block text-sm font-medium text-gray-700 mb-2">Base Height (m)</label>
<input
type="number"
id="reclamationHeightBase"
name="reclamationHeightBase"
min="0"
value={formData.reclamationHeightBase}
onChange={(e) => updateFormData('reclamationHeightBase', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label htmlFor="reclamationHeightExtra" className="block text-sm font-medium text-gray-700 mb-2">Extra Height (m)</label>
<input
type="number"
id="reclamationHeightExtra"
name="reclamationHeightExtra"
min="0"
value={formData.reclamationHeightExtra}
onChange={(e) => updateFormData('reclamationHeightExtra', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
</div>
</div>
<div>
<h3 className="text-base sm:text-lg font-medium text-gray-900 mb-4">Pipeline Length</h3>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4">
<div>
<label htmlFor="pipelineMain" className="block text-sm font-medium text-gray-700 mb-2">Main</label>
<input
type="number"
id="pipelineMain"
name="pipelineMain"
min="0"
value={formData.pipelineMain}
onChange={(e) => updateFormData('pipelineMain', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label htmlFor="pipelineExt1" className="block text-sm font-medium text-gray-700 mb-2">Extension 1</label>
<input
type="number"
id="pipelineExt1"
name="pipelineExt1"
min="0"
value={formData.pipelineExt1}
onChange={(e) => updateFormData('pipelineExt1', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label htmlFor="pipelineReserve" className="block text-sm font-medium text-gray-700 mb-2">Reserve</label>
<input
type="number"
id="pipelineReserve"
name="pipelineReserve"
min="0"
value={formData.pipelineReserve}
onChange={(e) => updateFormData('pipelineReserve', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label htmlFor="pipelineExt2" className="block text-sm font-medium text-gray-700 mb-2">Extension 2</label>
<input
type="number"
id="pipelineExt2"
name="pipelineExt2"
min="0"
value={formData.pipelineExt2}
onChange={(e) => updateFormData('pipelineExt2', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
</div>
</div>
</div>
)}
{/* Step 2: Equipment & Time Sheet */}
{currentStep === 2 && (
<div className="space-y-4 sm:space-y-6">
<div>
<h3 className="text-base sm:text-lg font-medium text-gray-900 mb-4">Equipment Statistics</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">
<div>
<label htmlFor="statsDozers" className="block text-sm font-medium text-gray-700 mb-2">
Dozers <span className="text-xs text-gray-500">(Auto-calculated)</span>
</label>
<input
type="number"
id="statsDozers"
name="statsDozers"
min="0"
value={formData.statsDozers}
readOnly
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-100 text-gray-700 cursor-not-allowed"
title="Auto-calculated based on time sheet entries"
/>
</div>
<div>
<label htmlFor="statsExc" className="block text-sm font-medium text-gray-700 mb-2">
Excavators <span className="text-xs text-gray-500">(Auto-calculated)</span>
</label>
<input
type="number"
id="statsExc"
name="statsExc"
min="0"
value={formData.statsExc}
readOnly
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-100 text-gray-700 cursor-not-allowed"
title="Auto-calculated based on time sheet entries"
/>
</div>
<div>
<label htmlFor="statsLoaders" className="block text-sm font-medium text-gray-700 mb-2">
Loaders <span className="text-xs text-gray-500">(Auto-calculated)</span>
</label>
<input
type="number"
id="statsLoaders"
name="statsLoaders"
min="0"
value={formData.statsLoaders}
readOnly
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-100 text-gray-700 cursor-not-allowed"
title="Auto-calculated based on time sheet entries"
/>
</div>
<div>
<label htmlFor="statsForeman" className="block text-sm font-medium text-gray-700 mb-2">Foreman</label>
<select
id="statsForeman"
name="statsForeman"
value={formData.statsForeman}
onChange={(e) => updateFormData('statsForeman', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
>
<option value="">Select foreman</option>
{foremen.map((foreman) => (
<option key={foreman.id} value={foreman.name}>{foreman.name}</option>
))}
</select>
</div>
<div>
<label htmlFor="statsLaborer" className="block text-sm font-medium text-gray-700 mb-2">
Laborers <span className="text-xs text-gray-500">({selectedWorkers.length} selected)</span>
</label>
<input
type="number"
id="statsLaborer"
name="statsLaborer"
min="0"
value={formData.statsLaborer}
onChange={(e) => {
updateFormData('statsLaborer', e.target.value);
setIsLaborerManuallyEdited(true);
}}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
title="Auto-calculated based on selected workers, but can be edited"
/>
</div>
</div>
</div>
<div>
<h3 className="text-base sm:text-lg font-medium text-gray-900 mb-4">Select Workers</h3>
<div className="space-y-3">
<div className="relative">
<input
type="text"
placeholder="Search workers..."
value={workerSearchTerm}
onChange={(e) => setWorkerSearchTerm(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
{workerSearchTerm && filteredWorkers.length > 0 && (
<div className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-auto">
{filteredWorkers.map((worker) => (
<button
key={worker.id}
type="button"
onClick={() => {
toggleWorker(worker.id);
setWorkerSearchTerm('');
}}
className="w-full text-left px-4 py-2 hover:bg-gray-100 focus:outline-none focus:bg-gray-100"
>
{worker.name}
</button>
))}
</div>
)}
</div>
{selectedWorkers.length > 0 && (
<div className="flex flex-wrap gap-2">
{selectedWorkers.map((workerId) => {
const worker = workers.find(w => w.id === workerId);
return worker ? (
<span
key={workerId}
className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-indigo-100 text-indigo-800"
>
{worker.name}
<button
type="button"
onClick={() => toggleWorker(workerId)}
className="ml-2 inline-flex items-center justify-center w-4 h-4 text-indigo-600 hover:text-indigo-800"
>
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</button>
</span>
) : null;
})}
</div>
)}
</div>
</div>
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900">Time Sheet</h3>
<button
type="button"
onClick={addTimeSheetEntry}
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Add Entry
</button>
</div>
{timeSheetEntries.length > 0 ? (
<div className="space-y-4">
{timeSheetEntries.map((entry) => (
<div key={entry.id} className="bg-gray-50 p-4 rounded-lg">
<div className="grid grid-cols-1 md:grid-cols-7 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Equipment</label>
<select
value={entry.machine}
onChange={(e) => updateTimeSheetEntry(entry.id, 'machine', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
>
<option value="">Select Equipment</option>
{equipment.filter((item) => {
const machineValue = `${item.model} (${item.number})`;
return !timeSheetEntries.some(e => e.id !== entry.id && e.machine === machineValue);
}).map((item) => (
<option key={item.id} value={`${item.model} (${item.number})`}>
{item.category} - {item.model} ({item.number})
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">From</label>
<input
type="time"
value={entry.from1}
onChange={(e) => updateTimeSheetEntry(entry.id, 'from1', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">To</label>
<input
type="time"
value={entry.to1}
onChange={(e) => updateTimeSheetEntry(entry.id, 'to1', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">From 2</label>
<input
type="time"
value={entry.from2}
onChange={(e) => updateTimeSheetEntry(entry.id, 'from2', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">To 2</label>
<input
type="time"
value={entry.to2}
onChange={(e) => updateTimeSheetEntry(entry.id, 'to2', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Total</label>
<input
type="text"
value={entry.total}
readOnly
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-100"
/>
</div>
<div className="flex items-end">
<button
type="button"
onClick={() => removeTimeSheetEntry(entry.id)}
className="w-full px-3 py-2 border border-red-300 text-red-700 rounded-md hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
Remove
</button>
</div>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Reason</label>
<input
type="text"
value={entry.reason}
onChange={(e) => updateTimeSheetEntry(entry.id, 'reason', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Reason for downtime (if any)"
/>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-gray-500">
<p className="mt-2">No time sheet entries yet. Click "Add Entry" to get started.</p>
</div>
)}
</div>
</div>
)}
{/* Step 3: Stoppages & Notes */}
{currentStep === 3 && (
<div className="space-y-6">
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900">Dredger Stoppages</h3>
<button
type="button"
onClick={addStoppageEntry}
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Add Stoppage
</button>
</div>
{stoppageEntries.length > 0 ? (
<div className="space-y-4">
{stoppageEntries.map((entry) => (
<div key={entry.id} className="bg-gray-50 p-4 rounded-lg">
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">From</label>
<input
type="time"
value={entry.from}
onChange={(e) => updateStoppageEntry(entry.id, 'from', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">To</label>
<input
type="time"
value={entry.to}
onChange={(e) => updateStoppageEntry(entry.id, 'to', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Total</label>
<input
type="text"
value={entry.total}
readOnly
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-100"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Reason</label>
<select
value={entry.reason}
onChange={(e) => updateStoppageEntry(entry.id, 'reason', e.target.value)}
disabled={entry.responsible === 'reclamation'}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 ${
entry.responsible === 'reclamation' ? 'bg-gray-100 text-gray-500 cursor-not-allowed border-gray-300' : 'border-gray-300'
}`}
>
<option value="">None</option>
<option value="maintenance">Maintenance</option>
<option value="shift">Shift</option>
<option value="lubrication">Lubrication</option>
<option value="check">Check</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Responsible</label>
<select
value={entry.responsible}
onChange={(e) => updateStoppageEntry(entry.id, 'responsible', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
>
<option value="reclamation">Reclamation</option>
<option value="dredger">Dredger</option>
</select>
</div>
<div className="flex items-end">
<button
type="button"
onClick={() => removeStoppageEntry(entry.id)}
className="w-full px-3 py-2 border border-red-300 text-red-700 rounded-md hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
Remove
</button>
</div>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
Notes {entry.responsible === 'reclamation' && <span className="text-red-500">*</span>}
</label>
<input
type="text"
value={entry.note}
onChange={(e) => updateStoppageEntry(entry.id, 'note', e.target.value)}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 ${
entry.responsible === 'reclamation' && !entry.note.trim() ? 'border-red-300 bg-red-50' : 'border-gray-300'
}`}
placeholder={entry.responsible === 'reclamation' ? 'Notes required for reclamation stoppages' : 'Additional notes'}
/>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-gray-500">
<p className="mt-2">No stoppages recorded. Click "Add Stoppage" if there were any.</p>
</div>
)}
</div>
<div>
<label htmlFor="notes" className="block text-sm font-medium text-gray-700 mb-2">
Additional Notes & Comments
</label>
<textarea
id="notes"
name="notes"
rows={4}
value={formData.notes}
onChange={(e) => updateFormData('notes', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Enter any additional notes or comments about the operation..."
/>
</div>
</div>
)}
{/* Error Message */}
{actionData?.errors && 'form' in actionData.errors && (
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-md">
<div className="flex">
<svg className="h-5 w-5 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-red-800">{actionData.errors.form}</p>
</div>
</div>
</div>
)}
</div>
{/* Navigation Buttons */}
<div className="px-4 sm:px-6 py-4 bg-gray-50 border-t border-gray-200 flex flex-col sm:flex-row sm:justify-between space-y-3 sm:space-y-0">
<button
type="button"
onClick={prevStep}
disabled={currentStep === 1}
className={`inline-flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium ${
currentStep === 1
? 'text-gray-400 bg-gray-100 cursor-not-allowed'
: 'text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500'
}`}
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Previous
</button>
<div className="flex space-x-3">
{currentStep < totalSteps ? (
<button
type="button"
onClick={(e) => nextStep(e)}
disabled={!isCurrentStepValid()}
className={`flex-1 sm:flex-none inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 ${
!isCurrentStepValid()
? 'text-gray-400 bg-gray-300 cursor-not-allowed'
: 'text-white bg-indigo-600 hover:bg-indigo-700 focus:ring-indigo-500'
}`}
>
Next
<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
</button>
) : (
<button
type="submit"
disabled={isSubmitting}
className="flex-1 sm:flex-none inline-flex items-center justify-center px-6 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<>
<svg className="animate-spin -ml-1 mr-3 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Updating Report...
</>
) : (
<>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
Update Report
</>
)}
</button>
)}
</div>
</div>
{/* Hidden inputs for dynamic data */}
<input type="hidden" name="timeSheetData" value={JSON.stringify(timeSheetEntries)} />
<input type="hidden" name="stoppagesData" value={JSON.stringify(stoppageEntries)} />
<input type="hidden" name="workersList" value={JSON.stringify(selectedWorkers)} />
{/* Hidden inputs for form data from all steps */}
{currentStep !== 1 && (
<>
<input type="hidden" name="dredgerLineLength" value={formData.dredgerLineLength} />
<input type="hidden" name="shoreConnection" value={formData.shoreConnection} />
<input type="hidden" name="reclamationHeightBase" value={formData.reclamationHeightBase} />
<input type="hidden" name="reclamationHeightExtra" value={formData.reclamationHeightExtra} />
<input type="hidden" name="pipelineMain" value={formData.pipelineMain} />
<input type="hidden" name="pipelineExt1" value={formData.pipelineExt1} />
<input type="hidden" name="pipelineReserve" value={formData.pipelineReserve} />
<input type="hidden" name="pipelineExt2" value={formData.pipelineExt2} />
</>
)}
{currentStep !== 2 && (
<>
<input type="hidden" name="statsDozers" value={formData.statsDozers} />
<input type="hidden" name="statsExc" value={formData.statsExc} />
<input type="hidden" name="statsLoaders" value={formData.statsLoaders} />
<input type="hidden" name="statsForeman" value={formData.statsForeman} />
<input type="hidden" name="statsLaborer" value={formData.statsLaborer} />
</>
)}
{currentStep !== 3 && (
<input type="hidden" name="notes" value={formData.notes} />
)}
</Form>
</div>
</DashboardLayout>
);
}