1198 lines
56 KiB
TypeScript
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>
|
|
);
|
|
}
|