mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 10:58:14 +00:00
734 lines
24 KiB
TypeScript
734 lines
24 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { Plus, Search, Grid3x3, Edit, Trash2, Bed, Armchair } from 'lucide-react';
|
|
import DataTable from '@/components/ui/DataTable';
|
|
import ActionButton from '@/components/ui/ActionButton';
|
|
import Modal from '@/components/ui/Modal';
|
|
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
|
import { fleetApi, apiClient } from '@/lib/api';
|
|
|
|
type Tab = 'types' | 'coaches';
|
|
|
|
const getBedLabel = (bedPosition: string | null): string => {
|
|
if (bedPosition === 'upper') return 'U';
|
|
if (bedPosition === 'middle') return 'M';
|
|
if (bedPosition === 'lower') return 'L';
|
|
return '';
|
|
};
|
|
|
|
const renderBedVisualization = (coach: any) => {
|
|
const seats = coach.seats || [];
|
|
const validSeats = seats.filter((s: any) => s.seatNumber && !s.seatNumber.startsWith('-'));
|
|
|
|
if (validSeats.length === 0) {
|
|
return <div className="text-xs text-muted-foreground">No seats</div>;
|
|
}
|
|
|
|
const hasBedPositionData = validSeats.some((s: any) => s.bedPosition);
|
|
const isBedCoach = coach.coachType?.name?.toLowerCase().includes('bed');
|
|
|
|
if (!isBedCoach || !hasBedPositionData) {
|
|
// Regular seat layout
|
|
const arrangement = coach.seatArrangement || coach.arrangement || '2+2';
|
|
const [left, right] = arrangement.split('+').map((p: string) => parseInt(p.trim()));
|
|
const cols = new Map<number, any[]>();
|
|
|
|
for (const seat of validSeats) {
|
|
if (!cols.has(seat.row)) cols.set(seat.row, []);
|
|
cols.get(seat.row)!.push(seat);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-1">
|
|
{Array.from(cols.entries()).map(([row, rowSeats]) => (
|
|
<div key={row} className="flex gap-3 justify-start">
|
|
<div className="flex gap-0.5">
|
|
{rowSeats.slice(0, left).map((s: any) => (
|
|
<div key={s.id} className="w-6 h-6 rounded bg-green-500 flex items-center justify-center">
|
|
<Armchair className="w-3 h-3 text-white" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="flex gap-0.5">
|
|
{rowSeats.slice(left).map((s: any) => (
|
|
<div key={s.id} className="w-6 h-6 rounded bg-green-500 flex items-center justify-center">
|
|
<Armchair className="w-3 h-3 text-white" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Bed layout with pairing
|
|
const seatsByRow = new Map<number, any[]>();
|
|
for (const seat of validSeats) {
|
|
if (!seatsByRow.has(seat.row)) seatsByRow.set(seat.row, []);
|
|
seatsByRow.get(seat.row)!.push(seat);
|
|
}
|
|
|
|
const beds = coach.coachType?.name?.toLowerCase().includes('vip') ? 'w-12' : 'w-10';
|
|
const rows = Array.from(seatsByRow.entries()).map(([r, s]) => s);
|
|
|
|
return (
|
|
<div className="space-y-1">
|
|
{rows.map((rowSeats: any[], idx: number) => {
|
|
const rowNumber = rowSeats[0]?.row || (idx + 1);
|
|
const isFirstInPair = (rowNumber - 1) % 2 === 0;
|
|
const isLastRow = idx === rows.length - 1;
|
|
const nextRowSeats = !isLastRow ? rows[idx + 1] : null;
|
|
|
|
return (
|
|
<div key={`row-${idx}`}>
|
|
{/* Row 1 of pair - label above */}
|
|
{isFirstInPair && (
|
|
<div className="flex gap-0.5 text-xs text-gray-500 mb-0.5">
|
|
{rowSeats.map((s: any) => (
|
|
<div key={`label-${s.id}`} className={`${beds} h-2 flex items-center justify-center text-xs font-bold leading-3`}>
|
|
{s.seatNumber}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
{/* Row 1 of pair - beds */}
|
|
<div className="flex gap-0.5">
|
|
{rowSeats.map((s: any) => (
|
|
<div
|
|
key={s.id}
|
|
className={`${beds} h-5 rounded flex items-center justify-center bg-green-500`}
|
|
style={isFirstInPair ? { transform: 'scaleY(-1)' } : undefined}
|
|
>
|
|
<Bed className="w-3 h-3 text-white" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
{/* Numbers between rows */}
|
|
{isFirstInPair && nextRowSeats && (
|
|
<div className="flex gap-0.5 text-xs text-gray-500 my-0.5">
|
|
{rowSeats.map((s: any, idx: number) => {
|
|
const nextSeat = nextRowSeats[idx];
|
|
return (
|
|
<div key={`between-${s.id}`} className={`${beds} h-2 flex items-center justify-center text-xs font-bold leading-3`}>
|
|
{nextSeat?.seatNumber}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
{/* Row 2 of pair - beds */}
|
|
{!isFirstInPair && (
|
|
<div className="flex gap-0.5">
|
|
{rowSeats.map((s: any) => (
|
|
<div
|
|
key={s.id}
|
|
className={`${beds} h-5 rounded flex items-center justify-center bg-green-500`}
|
|
>
|
|
<Bed className="w-3 h-3 text-white" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
{!isFirstInPair && <div className="h-1" />}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default function CoachesPage() {
|
|
const [activeTab, setActiveTab] = useState<Tab>('coaches');
|
|
const [search, setSearch] = useState('');
|
|
const [showModal, setShowModal] = useState(false);
|
|
const [editingItem, setEditingItem] = useState<any>(null);
|
|
const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; item: any | null }>({ isOpen: false, item: null });
|
|
const queryClient = useQueryClient();
|
|
|
|
// Coach Types Queries
|
|
const { data: coachTypesData, isLoading: typesLoading } = useQuery({
|
|
queryKey: ['coach-types'],
|
|
queryFn: () => apiClient.get('/fleet/coach-types'),
|
|
});
|
|
|
|
// Coaches Queries
|
|
const { data: coachesData, isLoading: coachesLoading } = useQuery({
|
|
queryKey: ['coaches'],
|
|
queryFn: () => fleetApi.getCoaches({}),
|
|
});
|
|
|
|
// Coach Type Mutations
|
|
const createCoachTypeMutation = useMutation({
|
|
mutationFn: (data: any) => apiClient.post('/fleet/coach-types', data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['coach-types'] });
|
|
setShowModal(false);
|
|
setEditingItem(null);
|
|
},
|
|
});
|
|
|
|
const updateCoachTypeMutation = useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: any }) => apiClient.patch(`/fleet/coach-types/${id}`, data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['coach-types'] });
|
|
setShowModal(false);
|
|
setEditingItem(null);
|
|
},
|
|
});
|
|
|
|
const deleteCoachTypeMutation = useMutation({
|
|
mutationFn: (id: string) => apiClient.delete(`/fleet/coach-types/${id}`),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['coach-types'] });
|
|
},
|
|
});
|
|
|
|
// Coach Mutations
|
|
const createCoachMutation = useMutation({
|
|
mutationFn: fleetApi.createCoach,
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['coaches'] });
|
|
setShowModal(false);
|
|
setEditingItem(null);
|
|
},
|
|
});
|
|
|
|
const updateCoachMutation = useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: any }) => fleetApi.updateCoach(id, data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['coaches'] });
|
|
setShowModal(false);
|
|
setEditingItem(null);
|
|
},
|
|
});
|
|
|
|
const deleteCoachMutation = useMutation({
|
|
mutationFn: fleetApi.deleteCoach,
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['coaches'] });
|
|
},
|
|
});
|
|
|
|
const handleCoachTypeSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.currentTarget);
|
|
const data = {
|
|
code: formData.get('code') as string,
|
|
name: formData.get('name') as string,
|
|
type: formData.get('type') as string,
|
|
};
|
|
|
|
if (editingItem?.isCoachType) {
|
|
await updateCoachTypeMutation.mutateAsync({ id: editingItem.id, data });
|
|
} else {
|
|
await createCoachTypeMutation.mutateAsync(data);
|
|
}
|
|
};
|
|
|
|
const handleCoachSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.currentTarget);
|
|
const data = {
|
|
number: formData.get('number') as string,
|
|
coachTypeId: formData.get('coachTypeId') as string,
|
|
arrangement: formData.get('arrangement') as string,
|
|
capacity: parseInt(formData.get('capacity') as string),
|
|
sequence: parseInt(formData.get('sequence') as string),
|
|
status: formData.get('status') as string,
|
|
};
|
|
|
|
if (editingItem?.isCoach) {
|
|
await updateCoachMutation.mutateAsync({ id: editingItem.id, data });
|
|
} else {
|
|
await createCoachMutation.mutateAsync(data);
|
|
}
|
|
};
|
|
|
|
const handleDelete = (item: any, isCoachType: boolean) => {
|
|
setDeleteConfirm({ isOpen: true, item: { ...item, isCoachType } });
|
|
};
|
|
|
|
const confirmDelete = async () => {
|
|
if (deleteConfirm.item?.isCoachType) {
|
|
await deleteCoachTypeMutation.mutateAsync(deleteConfirm.item.id);
|
|
} else {
|
|
await deleteCoachMutation.mutateAsync(deleteConfirm.item.id);
|
|
}
|
|
setDeleteConfirm({ isOpen: false, item: null });
|
|
};
|
|
|
|
const coachTypesArray = Array.isArray(coachTypesData) ? coachTypesData : (coachTypesData as any)?.items || (coachTypesData as any)?.data || [];
|
|
const coaches = coachesData?.items || coachesData?.data || [];
|
|
|
|
const filteredCoachTypes = coachTypesArray.filter((ct: any) => {
|
|
if (!search) return true;
|
|
const searchLower = search.toLowerCase();
|
|
return (
|
|
ct.code?.toLowerCase().includes(searchLower) ||
|
|
ct.name?.toLowerCase().includes(searchLower) ||
|
|
ct.type?.toLowerCase().includes(searchLower)
|
|
);
|
|
});
|
|
|
|
const filteredCoaches = coaches.filter((coach: any) => {
|
|
if (!search) return true;
|
|
const searchLower = search.toLowerCase();
|
|
return (
|
|
coach.number?.toLowerCase().includes(searchLower) ||
|
|
coach.coachNumber?.toLowerCase().includes(searchLower) ||
|
|
coach.coachType?.name?.toLowerCase().includes(searchLower) ||
|
|
coach.arrangement?.toLowerCase().includes(searchLower)
|
|
);
|
|
});
|
|
|
|
const typeColorMap: Record<string, string> = {
|
|
passenger: 'edr-badge-info',
|
|
sleeper: 'edr-badge-warning',
|
|
dining: 'edr-badge-success',
|
|
baggage: 'edr-badge-danger',
|
|
};
|
|
|
|
const statusMap: Record<string, string> = {
|
|
ACTIVE: 'edr-badge-success',
|
|
MAINTENANCE: 'edr-badge-warning',
|
|
INACTIVE: 'edr-badge-danger',
|
|
};
|
|
|
|
// Coach Types Columns
|
|
const coachTypeColumns = [
|
|
{
|
|
key: 'code',
|
|
label: 'Code',
|
|
sortable: true,
|
|
render: (ct: any) => (
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-[rgb(20,113,76)]">
|
|
<Grid3x3 className="h-4 w-4 text-white" />
|
|
</div>
|
|
<span className="font-mono font-medium">{ct.code}</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'name',
|
|
label: 'Name',
|
|
sortable: true,
|
|
render: (ct: any) => <span className="font-medium">{ct.name}</span>,
|
|
},
|
|
{
|
|
key: 'type',
|
|
label: 'Type',
|
|
render: (ct: any) => (
|
|
<span className={`edr-badge ${typeColorMap[ct.type] || 'edr-badge-info'}`}>
|
|
{ct.type}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'coaches',
|
|
label: 'Coaches',
|
|
render: (ct: any) => (
|
|
<span className="text-sm font-medium">{ct.coaches?.length || 0}</span>
|
|
),
|
|
},
|
|
];
|
|
|
|
// Coaches Columns
|
|
const coachColumns = [
|
|
{
|
|
key: 'sequence',
|
|
label: 'Sequence',
|
|
sortable: true,
|
|
render: (coach: any) => (
|
|
<span className="font-mono font-semibold text-sm">{coach.sequence}</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'number',
|
|
label: 'Number',
|
|
sortable: true,
|
|
render: (coach: any) => (
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-[rgb(20,113,76)]">
|
|
<Grid3x3 className="h-4 w-4 text-white" />
|
|
</div>
|
|
<span className="font-medium">{coach.number || coach.coachNumber}</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'coachType',
|
|
label: 'Coach Type',
|
|
render: (coach: any) => (
|
|
<span className="text-sm">{coach.coachType?.name || 'N/A'}</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'arrangement',
|
|
label: 'Arrangement',
|
|
render: (coach: any) => (
|
|
<span className="text-sm font-mono">{coach.arrangement || 'N/A'}</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'capacity',
|
|
label: 'Capacity',
|
|
render: (coach: any) => (
|
|
<span className="font-mono text-sm font-medium">{coach.capacity || 0}</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'status',
|
|
label: 'Status',
|
|
render: (coach: any) => {
|
|
const status = coach.status || 'ACTIVE';
|
|
return (
|
|
<span className={`edr-badge ${statusMap[status] || 'edr-badge-info'}`}>
|
|
{status}
|
|
</span>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
|
|
const coachTypeActions = [
|
|
{
|
|
label: 'Edit',
|
|
onClick: (item: any) => {
|
|
setEditingItem({ ...item, isCoachType: true });
|
|
setShowModal(true);
|
|
},
|
|
variant: 'secondary' as const,
|
|
icon: Edit,
|
|
},
|
|
{
|
|
label: 'Delete',
|
|
onClick: (item: any) => handleDelete(item, true),
|
|
variant: 'danger' as const,
|
|
icon: Trash2,
|
|
},
|
|
];
|
|
|
|
const coachActions = [
|
|
{
|
|
label: 'Edit',
|
|
onClick: (item: any) => {
|
|
setEditingItem({ ...item, isCoach: true });
|
|
setShowModal(true);
|
|
},
|
|
variant: 'secondary' as const,
|
|
icon: Edit,
|
|
},
|
|
{
|
|
label: 'Delete',
|
|
onClick: (item: any) => handleDelete(item, false),
|
|
variant: 'danger' as const,
|
|
icon: Trash2,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-foreground">Coach Management</h1>
|
|
<p className="text-muted-foreground mt-1">Manage coach types and train coaches</p>
|
|
</div>
|
|
<ActionButton
|
|
icon={Plus}
|
|
onClick={() => {
|
|
setEditingItem(null);
|
|
setSearch('');
|
|
setShowModal(true);
|
|
}}
|
|
>
|
|
{activeTab === 'types' ? 'Add Coach Type' : 'Add Coach'}
|
|
</ActionButton>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="card">
|
|
<div className="flex border-b border-border">
|
|
<button
|
|
onClick={() => {
|
|
setActiveTab('types');
|
|
setSearch('');
|
|
}}
|
|
className={`px-4 py-3 font-medium transition-colors ${
|
|
activeTab === 'types'
|
|
? 'border-b-2 border-primary text-primary'
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
}`}
|
|
>
|
|
Coach Types
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setActiveTab('coaches');
|
|
setSearch('');
|
|
}}
|
|
className={`px-4 py-3 font-medium transition-colors ${
|
|
activeTab === 'coaches'
|
|
? 'border-b-2 border-primary text-primary'
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
}`}
|
|
>
|
|
Coaches
|
|
</button>
|
|
</div>
|
|
|
|
{/* Coach Types Tab */}
|
|
{activeTab === 'types' && (
|
|
<div className="pt-6 space-y-4">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search by code, name, or type..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
className="input pl-10 w-full"
|
|
/>
|
|
</div>
|
|
<DataTable
|
|
columns={coachTypeColumns}
|
|
data={filteredCoachTypes}
|
|
actions={coachTypeActions}
|
|
loading={typesLoading}
|
|
emptyMessage={search ? "No coach types match your search" : "No coach types found. Create one to get started."}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Coaches Tab */}
|
|
{activeTab === 'coaches' && (
|
|
<div className="pt-6 space-y-4">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search by coach number, type, or arrangement..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
className="input pl-10 w-full"
|
|
/>
|
|
</div>
|
|
<DataTable
|
|
columns={coachColumns}
|
|
data={filteredCoaches}
|
|
actions={coachActions}
|
|
loading={coachesLoading}
|
|
emptyMessage={search ? "No coaches match your search" : "No coaches found. Create one to get started."}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Delete Confirmation */}
|
|
<ConfirmDialog
|
|
isOpen={deleteConfirm.isOpen}
|
|
onClose={() => setDeleteConfirm({ isOpen: false, item: null })}
|
|
onConfirm={confirmDelete}
|
|
title={`Delete ${deleteConfirm.item?.isCoachType ? 'Coach Type' : 'Coach'}`}
|
|
message={`Are you sure you want to delete ${deleteConfirm.item?.name || deleteConfirm.item?.number}?`}
|
|
confirmText="Delete"
|
|
isDanger={true}
|
|
warning={
|
|
deleteConfirm.item?.isCoachType
|
|
? 'This coach type may have coaches assigned. Deleting it may impact these systems.'
|
|
: 'This coach may be assigned to schedules. Deleting it may impact these systems.'
|
|
}
|
|
/>
|
|
|
|
{/* Add/Edit Modal */}
|
|
<Modal
|
|
isOpen={showModal}
|
|
onClose={() => {
|
|
setShowModal(false);
|
|
setEditingItem(null);
|
|
}}
|
|
title={
|
|
activeTab === 'types'
|
|
? `${editingItem?.isCoachType ? 'Edit' : 'Add'} Coach Type`
|
|
: `${editingItem?.isCoach ? 'Edit' : 'Add'} Coach`
|
|
}
|
|
size={activeTab === 'types' ? 'md' : 'lg'}
|
|
>
|
|
{/* Coach Type Form */}
|
|
{activeTab === 'types' && (
|
|
<form onSubmit={handleCoachTypeSubmit} className="space-y-4">
|
|
<div className="grid grid-cols-1 gap-4">
|
|
<div>
|
|
<label className="label">Code</label>
|
|
<input
|
|
type="text"
|
|
name="code"
|
|
className="input"
|
|
defaultValue={editingItem?.code || ''}
|
|
required
|
|
placeholder="e.g., HSC"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Name</label>
|
|
<input
|
|
type="text"
|
|
name="name"
|
|
className="input"
|
|
defaultValue={editingItem?.name || ''}
|
|
required
|
|
placeholder="e.g., Hard Seat Coach"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Type</label>
|
|
<input
|
|
type="text"
|
|
name="type"
|
|
className="input"
|
|
defaultValue={editingItem?.type || ''}
|
|
required
|
|
placeholder="e.g., Regular Seat"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 pt-4">
|
|
<ActionButton
|
|
type="button"
|
|
variant="secondary"
|
|
onClick={() => {
|
|
setShowModal(false);
|
|
setEditingItem(null);
|
|
}}
|
|
>
|
|
Cancel
|
|
</ActionButton>
|
|
<ActionButton
|
|
type="submit"
|
|
loading={createCoachTypeMutation.isPending || updateCoachTypeMutation.isPending}
|
|
>
|
|
{editingItem?.isCoachType ? 'Update' : 'Create'} Coach Type
|
|
</ActionButton>
|
|
</div>
|
|
</form>
|
|
)}
|
|
|
|
{/* Coach Form */}
|
|
{activeTab === 'coaches' && (
|
|
<form onSubmit={handleCoachSubmit} className="space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Coach Type *</label>
|
|
<select
|
|
name="coachTypeId"
|
|
className="input"
|
|
defaultValue={editingItem?.coachTypeId || ''}
|
|
required
|
|
>
|
|
<option value="">Select Coach Type</option>
|
|
{coachTypesArray.map((ct: any) => (
|
|
<option key={ct.id} value={ct.id}>
|
|
{ct.code} - {ct.name} - {ct.type}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Number *</label>
|
|
<input
|
|
type="text"
|
|
name="number"
|
|
className="input"
|
|
defaultValue={editingItem?.number || editingItem?.coachNumber || ''}
|
|
required
|
|
placeholder="e.g., HSC-0001"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Arrangement *</label>
|
|
<input
|
|
type="text"
|
|
name="arrangement"
|
|
className="input"
|
|
defaultValue={editingItem?.arrangement || editingItem?.seatArrangement }
|
|
required
|
|
placeholder="e.g., 3+2, 3+0, 2+0"
|
|
/>
|
|
<p className="text-xs text-muted-foreground mt-1">Format: separate columns with +</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Capacity *</label>
|
|
<input
|
|
type="number"
|
|
name="capacity"
|
|
className="input"
|
|
defaultValue={editingItem?.capacity || editingItem?.totalUnits || ''}
|
|
required
|
|
min="1"
|
|
placeholder="60"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Sequence Number *</label>
|
|
<input
|
|
type="number"
|
|
name="sequence"
|
|
className="input"
|
|
defaultValue={editingItem?.sequence || 0}
|
|
min="0"
|
|
required
|
|
placeholder="e.g., 1"
|
|
/>
|
|
<p className="text-xs text-muted-foreground mt-1">Used for ordering coaches in trains</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Status *</label>
|
|
<select
|
|
name="status"
|
|
className="input"
|
|
defaultValue={editingItem?.status || 'ACTIVE'}
|
|
required
|
|
>
|
|
<option value="ACTIVE">Active</option>
|
|
<option value="MAINTENANCE">Maintenance</option>
|
|
<option value="INACTIVE">Inactive</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 pt-4">
|
|
<ActionButton
|
|
type="button"
|
|
variant="secondary"
|
|
onClick={() => {
|
|
setShowModal(false);
|
|
setEditingItem(null);
|
|
}}
|
|
>
|
|
Cancel
|
|
</ActionButton>
|
|
<ActionButton
|
|
type="submit"
|
|
loading={createCoachMutation.isPending || updateCoachMutation.isPending}
|
|
>
|
|
{editingItem?.isCoach ? 'Update' : 'Create'} Coach
|
|
</ActionButton>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|