757 lines
34 KiB
TypeScript
757 lines
34 KiB
TypeScript
import React from 'react';
|
|
import { Form } from "@remix-run/react";
|
|
|
|
interface ReportFormModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
isEditing: boolean;
|
|
isSubmitting: boolean;
|
|
editingReport: any;
|
|
actionData: any;
|
|
areas: any[];
|
|
dredgerLocations: any[];
|
|
reclamationLocations: any[];
|
|
foremen: any[];
|
|
equipment: any[];
|
|
timeSheetEntries: any[];
|
|
stoppageEntries: any[];
|
|
addTimeSheetEntry: () => void;
|
|
removeTimeSheetEntry: (id: string) => void;
|
|
updateTimeSheetEntry: (id: string, field: string, value: string) => void;
|
|
addStoppageEntry: () => void;
|
|
removeStoppageEntry: (id: string) => void;
|
|
updateStoppageEntry: (id: string, field: string, value: string) => void;
|
|
}
|
|
|
|
export default function ReportFormModal({
|
|
isOpen,
|
|
onClose,
|
|
isEditing,
|
|
isSubmitting,
|
|
editingReport,
|
|
actionData,
|
|
areas,
|
|
dredgerLocations,
|
|
reclamationLocations,
|
|
foremen,
|
|
equipment,
|
|
timeSheetEntries,
|
|
stoppageEntries,
|
|
addTimeSheetEntry,
|
|
removeTimeSheetEntry,
|
|
updateTimeSheetEntry,
|
|
addStoppageEntry,
|
|
removeStoppageEntry,
|
|
updateStoppageEntry
|
|
}: ReportFormModalProps) {
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 overflow-y-auto">
|
|
<div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onClick={onClose}></div>
|
|
|
|
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-6xl sm:w-full">
|
|
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h3 className="text-2xl font-bold text-gray-900">
|
|
{isEditing ? "Edit Report" : "Create New Report"}
|
|
</h3>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="bg-white rounded-md text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
>
|
|
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<Form method="post" id="modal-form" className="space-y-8 max-h-[80vh] overflow-y-auto pr-2">
|
|
<input type="hidden" name="intent" value={isEditing ? "update" : "create"} />
|
|
{isEditing && <input type="hidden" name="id" value={editingReport?.id} />}
|
|
|
|
{/* Hidden inputs for dynamic arrays */}
|
|
<input type="hidden" name="timeSheetData" value={JSON.stringify(timeSheetEntries)} />
|
|
<input type="hidden" name="stoppagesData" value={JSON.stringify(stoppageEntries)} />
|
|
|
|
<BasicInformation
|
|
editingReport={editingReport}
|
|
actionData={actionData}
|
|
areas={areas}
|
|
dredgerLocations={dredgerLocations}
|
|
reclamationLocations={reclamationLocations}
|
|
/>
|
|
|
|
<ReclamationHeight editingReport={editingReport} />
|
|
|
|
<PipelineLength editingReport={editingReport} />
|
|
|
|
<EquipmentStatistics
|
|
editingReport={editingReport}
|
|
foremen={foremen}
|
|
/>
|
|
|
|
<TimeSheetSection
|
|
timeSheetEntries={timeSheetEntries}
|
|
equipment={equipment}
|
|
addTimeSheetEntry={addTimeSheetEntry}
|
|
removeTimeSheetEntry={removeTimeSheetEntry}
|
|
updateTimeSheetEntry={updateTimeSheetEntry}
|
|
/>
|
|
|
|
<StoppagesSection
|
|
stoppageEntries={stoppageEntries}
|
|
addStoppageEntry={addStoppageEntry}
|
|
removeStoppageEntry={removeStoppageEntry}
|
|
updateStoppageEntry={updateStoppageEntry}
|
|
/>
|
|
|
|
<NotesSection editingReport={editingReport} />
|
|
</Form>
|
|
</div>
|
|
|
|
{/* Enhanced Modal Footer */}
|
|
<div className="bg-gray-50 px-6 py-4 sm:flex sm:flex-row-reverse border-t border-gray-200">
|
|
<button
|
|
type="submit"
|
|
form="modal-form"
|
|
disabled={isSubmitting}
|
|
className="w-full inline-flex justify-center rounded-lg border border-transparent shadow-sm px-6 py-3 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
|
|
>
|
|
{isSubmitting ? (
|
|
<>
|
|
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" 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>
|
|
{isEditing ? "Updating..." : "Creating..."}
|
|
</>
|
|
) : (
|
|
<>
|
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
{isEditing ? "Update Report" : "Create Report"}
|
|
</>
|
|
)}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={isSubmitting}
|
|
className="mt-3 w-full inline-flex justify-center rounded-lg border border-gray-300 shadow-sm px-6 py-3 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Basic Information Component
|
|
function BasicInformation({ editingReport, actionData, areas, dredgerLocations, reclamationLocations }: any) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-medium text-gray-900 border-b pb-2">Basic Information</h3>
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<label htmlFor="shift" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Shift
|
|
</label>
|
|
<select
|
|
name="shift"
|
|
id="shift"
|
|
required
|
|
defaultValue={editingReport?.shift || ""}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
>
|
|
<option value="">Select shift</option>
|
|
<option value="day">Day Shift</option>
|
|
<option value="night">Night Shift</option>
|
|
</select>
|
|
{actionData?.errors?.shift && (
|
|
<p className="mt-1 text-sm text-red-600">{actionData.errors.shift}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="areaId" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Area
|
|
</label>
|
|
<select
|
|
name="areaId"
|
|
id="areaId"
|
|
required
|
|
defaultValue={editingReport?.areaId || ""}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
>
|
|
<option value="">Select area</option>
|
|
{areas.map((area: any) => (
|
|
<option key={area.id} value={area.id}>
|
|
{area.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{actionData?.errors?.areaId && (
|
|
<p className="mt-1 text-sm text-red-600">{actionData.errors.areaId}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<label htmlFor="dredgerLocationId" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Dredger Location
|
|
</label>
|
|
<select
|
|
name="dredgerLocationId"
|
|
id="dredgerLocationId"
|
|
required
|
|
defaultValue={editingReport?.dredgerLocationId || ""}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
>
|
|
<option value="">Select dredger location</option>
|
|
{dredgerLocations.map((location: any) => (
|
|
<option key={location.id} value={location.id}>
|
|
{location.name} ({location.class.toUpperCase()})
|
|
</option>
|
|
))}
|
|
</select>
|
|
{actionData?.errors?.dredgerLocationId && (
|
|
<p className="mt-1 text-sm text-red-600">{actionData.errors.dredgerLocationId}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="reclamationLocationId" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Reclamation Location
|
|
</label>
|
|
<select
|
|
name="reclamationLocationId"
|
|
id="reclamationLocationId"
|
|
required
|
|
defaultValue={editingReport?.reclamationLocationId || ""}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
>
|
|
<option value="">Select reclamation location</option>
|
|
{reclamationLocations.map((location: any) => (
|
|
<option key={location.id} value={location.id}>
|
|
{location.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{actionData?.errors?.reclamationLocationId && (
|
|
<p className="mt-1 text-sm text-red-600">{actionData.errors.reclamationLocationId}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<label htmlFor="dredgerLineLength" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Dredger Line Length (m)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="dredgerLineLength"
|
|
id="dredgerLineLength"
|
|
min="0"
|
|
required
|
|
defaultValue={editingReport?.dredgerLineLength || ""}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
placeholder="Enter length in meters"
|
|
/>
|
|
{actionData?.errors?.dredgerLineLength && (
|
|
<p className="mt-1 text-sm text-red-600">{actionData.errors.dredgerLineLength}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="shoreConnection" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Shore Connection (m)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="shoreConnection"
|
|
id="shoreConnection"
|
|
min="0"
|
|
required
|
|
defaultValue={editingReport?.shoreConnection || ""}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
placeholder="Enter connection length"
|
|
/>
|
|
{actionData?.errors?.shoreConnection && (
|
|
<p className="mt-1 text-sm text-red-600">{actionData.errors.shoreConnection}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Reclamation Height Component
|
|
function ReclamationHeight({ editingReport }: any) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-medium text-gray-900 border-b pb-2">Reclamation Height</h3>
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<label htmlFor="reclamationHeightBase" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Base Height (m)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="reclamationHeightBase"
|
|
id="reclamationHeightBase"
|
|
min="0"
|
|
defaultValue={editingReport?.reclamationHeight?.base || ""}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="reclamationHeightExtra" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Extra Height (m)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="reclamationHeightExtra"
|
|
id="reclamationHeightExtra"
|
|
min="0"
|
|
defaultValue={editingReport?.reclamationHeight?.extra || ""}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Pipeline Length Component
|
|
function PipelineLength({ editingReport }: any) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-medium text-gray-900 border-b pb-2">Pipeline Length</h3>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label htmlFor="pipelineMain" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Main (m)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="pipelineMain"
|
|
id="pipelineMain"
|
|
min="0"
|
|
defaultValue={editingReport?.pipelineLength?.main || ""}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="pipelineExt1" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Ext1 (m)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="pipelineExt1"
|
|
id="pipelineExt1"
|
|
min="0"
|
|
defaultValue={editingReport?.pipelineLength?.ext1 || ""}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="pipelineReserve" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Reserve (m)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="pipelineReserve"
|
|
id="pipelineReserve"
|
|
min="0"
|
|
defaultValue={editingReport?.pipelineLength?.reserve || ""}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="pipelineExt2" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Ext2 (m)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="pipelineExt2"
|
|
id="pipelineExt2"
|
|
min="0"
|
|
defaultValue={editingReport?.pipelineLength?.ext2 || ""}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Equipment Statistics Component
|
|
function EquipmentStatistics({ editingReport, foremen }: any) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-medium text-gray-900 border-b pb-2">Equipment Statistics</h3>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label htmlFor="statsDozers" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Dozers
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="statsDozers"
|
|
id="statsDozers"
|
|
min="0"
|
|
defaultValue={editingReport?.stats?.Dozers || ""}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="statsExc" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Excavators
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="statsExc"
|
|
id="statsExc"
|
|
min="0"
|
|
defaultValue={editingReport?.stats?.Exc || ""}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="statsLoaders" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Loaders
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="statsLoaders"
|
|
id="statsLoaders"
|
|
min="0"
|
|
defaultValue={editingReport?.stats?.Loaders || ""}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="statsLaborer" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Laborers
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="statsLaborer"
|
|
id="statsLaborer"
|
|
min="0"
|
|
defaultValue={editingReport?.stats?.Laborer || ""}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="statsForeman" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Foreman
|
|
</label>
|
|
<select
|
|
name="statsForeman"
|
|
id="statsForeman"
|
|
defaultValue={editingReport?.stats?.Foreman || ""}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
>
|
|
<option value="">Select foreman</option>
|
|
{foremen.map((foreman: any) => (
|
|
<option key={foreman.id} value={foreman.name}>
|
|
{foreman.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// TimeSheet Section Component
|
|
function TimeSheetSection({ timeSheetEntries, equipment, addTimeSheetEntry, removeTimeSheetEntry, updateTimeSheetEntry }: any) {
|
|
return (
|
|
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 p-6 rounded-lg border border-blue-200">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h3 className="text-xl font-semibold text-gray-900 flex items-center">
|
|
<svg className="w-6 h-6 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
Equipments Time Sheet
|
|
</h3>
|
|
<p className="text-sm text-gray-600 mt-1">Track Equipment working hours and maintenance periods</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={addTimeSheetEntry}
|
|
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200 shadow-sm"
|
|
>
|
|
<svg className="w-5 h-5 mr-2" 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 Equipment Entry
|
|
</button>
|
|
</div>
|
|
|
|
{timeSheetEntries.length === 0 ? (
|
|
<div className="text-center py-8 text-gray-500">
|
|
<svg className="mx-auto h-12 w-12 text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<p className="text-lg font-medium">No Equipment entries yet</p>
|
|
<p className="text-sm">Click "Add Equipment Entry" to start tracking Equipment hours</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4 max-h-80 overflow-y-auto">
|
|
{timeSheetEntries.map((entry: any, index: number) => (
|
|
<div key={entry.id} className="bg-white p-4 rounded-lg shadow-sm border border-gray-200 hover:shadow-md transition-shadow duration-200">
|
|
<div className="flex justify-between items-center mb-3">
|
|
<span className="text-sm font-medium text-gray-500">Entry #{index + 1}</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => removeTimeSheetEntry(entry.id)}
|
|
className="inline-flex items-center justify-center w-8 h-8 border border-transparent text-sm font-medium rounded-full text-red-600 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors duration-200"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
|
<div className="lg:col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Equipment</label>
|
|
<select
|
|
value={entry.machine}
|
|
onChange={(e) => updateTimeSheetEntry(entry.id, 'machine', e.target.value)}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm"
|
|
>
|
|
<option value="">Select Equipment</option>
|
|
{equipment.filter((item: any) => {
|
|
const machineValue = `${item.model} (${item.number})`;
|
|
// Show if not selected by any other entry, or if it's the current entry's selection
|
|
return !timeSheetEntries.some((e: any) => e.id !== entry.id && e.machine === machineValue);
|
|
}).map((item: any) => (
|
|
<option key={item.id} value={`${item.model} (${item.number})`}>
|
|
{item.category} - {item.model} ({item.number})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="lg:col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Reason</label>
|
|
<input
|
|
type="text"
|
|
value={entry.reason}
|
|
onChange={(e) => updateTimeSheetEntry(entry.id, 'reason', e.target.value)}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm"
|
|
placeholder="e.g., Maintenance, Operation"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 items-end">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">From 1</label>
|
|
<input
|
|
type="time"
|
|
value={entry.from1}
|
|
onChange={(e) => updateTimeSheetEntry(entry.id, 'from1', e.target.value)}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">To 1</label>
|
|
<input
|
|
type="time"
|
|
value={entry.to1}
|
|
onChange={(e) => updateTimeSheetEntry(entry.id, 'to1', e.target.value)}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">From 2</label>
|
|
<input
|
|
type="time"
|
|
value={entry.from2}
|
|
onChange={(e) => updateTimeSheetEntry(entry.id, 'from2', e.target.value)}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">To 2</label>
|
|
<input
|
|
type="time"
|
|
value={entry.to2}
|
|
onChange={(e) => updateTimeSheetEntry(entry.id, 'to2', e.target.value)}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Total Hours</label>
|
|
<div className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-50 text-sm font-medium text-gray-900">
|
|
{entry.total}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Stoppages Section Component
|
|
function StoppagesSection({ stoppageEntries, addStoppageEntry, removeStoppageEntry, updateStoppageEntry }: any) {
|
|
return (
|
|
<div className="bg-gradient-to-r from-red-50 to-orange-50 p-6 rounded-lg border border-red-200">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h3 className="text-xl font-semibold text-gray-900 flex items-center">
|
|
<svg className="w-6 h-6 mr-2 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
|
</svg>
|
|
Operation Stoppages
|
|
</h3>
|
|
<p className="text-sm text-gray-600 mt-1">Record operational interruptions and downtime</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={addStoppageEntry}
|
|
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors duration-200 shadow-sm"
|
|
>
|
|
<svg className="w-5 h-5 mr-2" 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="text-center py-8 text-gray-500">
|
|
<svg className="mx-auto h-12 w-12 text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
|
</svg>
|
|
<p className="text-lg font-medium">No stoppages recorded</p>
|
|
<p className="text-sm">Click "Add Stoppage" to record operational interruptions</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4 max-h-80 overflow-y-auto">
|
|
{stoppageEntries.map((entry: any, index: number) => (
|
|
<div key={entry.id} className="bg-white p-4 rounded-lg shadow-sm border border-gray-200 hover:shadow-md transition-shadow duration-200">
|
|
<div className="flex justify-between items-center mb-3">
|
|
<span className="text-sm font-medium text-gray-500">Stoppage #{index + 1}</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => removeStoppageEntry(entry.id)}
|
|
className="inline-flex items-center justify-center w-8 h-8 border border-transparent text-sm font-medium rounded-full text-red-600 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors duration-200"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">From</label>
|
|
<input
|
|
type="time"
|
|
value={entry.from}
|
|
onChange={(e) => updateStoppageEntry(entry.id, 'from', e.target.value)}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">To</label>
|
|
<input
|
|
type="time"
|
|
value={entry.to}
|
|
onChange={(e) => updateStoppageEntry(entry.id, 'to', e.target.value)}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Total Duration</label>
|
|
<div className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-50 text-sm font-medium text-gray-900">
|
|
{entry.total}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Reason</label>
|
|
<input
|
|
type="text"
|
|
value={entry.reason}
|
|
onChange={(e) => updateStoppageEntry(entry.id, 'reason', e.target.value)}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 text-sm"
|
|
placeholder="e.g., Maintenance, Equipment failure"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Responsible</label>
|
|
<input
|
|
type="text"
|
|
value={entry.responsible}
|
|
onChange={(e) => updateStoppageEntry(entry.id, 'responsible', e.target.value)}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 text-sm"
|
|
placeholder="e.g., Maintenance team"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Additional Notes</label>
|
|
<textarea
|
|
value={entry.note}
|
|
onChange={(e) => updateStoppageEntry(entry.id, 'note', e.target.value)}
|
|
rows={2}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 text-sm"
|
|
placeholder="Additional details about the stoppage..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Notes Section Component
|
|
function NotesSection({ editingReport }: any) {
|
|
return (
|
|
<div className="bg-gradient-to-r from-gray-50 to-slate-50 p-6 rounded-lg border border-gray-200">
|
|
<h3 className="text-xl font-semibold text-gray-900 flex items-center mb-4">
|
|
<svg className="w-6 h-6 mr-2 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
</svg>
|
|
Additional Notes
|
|
</h3>
|
|
<div>
|
|
<label htmlFor="notes" className="block text-sm font-medium text-gray-700 mb-3">
|
|
Report Notes & Observations
|
|
</label>
|
|
<textarea
|
|
name="notes"
|
|
id="notes"
|
|
rows={5}
|
|
defaultValue={editingReport?.notes || ""}
|
|
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm resize-none"
|
|
placeholder="Enter any additional notes, observations, or important details about this shift..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |