Files
edr-platform/apps/edr-passenger-web/backoffice/src/app/coaches/page.tsx
2026-07-01 09:03:09 +03:00

972 lines
34 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' | 'utilization';
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 [showPreviewModal, setShowPreviewModal] = useState(false);
const [seatMapPreview, setSeatMapPreview] = useState<any>(null);
const [editingItem, setEditingItem] = useState<any>(null);
const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; item: any | null; error?: string }>({ isOpen: false, item: null });
const [selectedCoachTypeId, setSelectedCoachTypeId] = useState<string>('');
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({}),
});
const { data: utilizationData, isLoading: utilizationLoading } = useQuery({
queryKey: ['coach-utilization'],
queryFn: () => apiClient.get<any[]>('/fleet/coaches/utilization'),
enabled: activeTab === 'utilization',
});
// 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 generateSeatMapMutation = useMutation({
mutationFn: fleetApi.generateSeatMap,
onSuccess: (data) => {
setSeatMapPreview(data);
setShowPreviewModal(true);
},
});
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: any = {
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,
};
// Add bed-specific fields if bed coach is selected
const bedCategory = formData.get('bedCategory') as string;
if (bedCategory) {
data.bedCategory = bedCategory as 'ECONOMY_BED' | 'VIP_BED';
const bedsPerRoom = formData.get('bedsPerRoom') as string;
if (bedsPerRoom) {
data.bedsPerRoom = parseInt(bedsPerRoom);
}
}
if (editingItem?.isCoach) {
await updateCoachMutation.mutateAsync({ id: editingItem.id, data });
} else {
await createCoachMutation.mutateAsync(data);
}
};
const handlePreviewSeatMap = async () => {
const form = document.querySelector('form') as HTMLFormElement;
const formData = new FormData(form);
const bedCategory = formData.get('bedCategory') as string;
const capacity = parseInt(formData.get('capacity') as string);
if (!bedCategory || !capacity) {
alert('Please select a bed category and enter capacity to preview seat map');
return;
}
const bedsPerRoom = bedCategory === 'VIP_BED' ? 4 : 6;
const roomsPerCoach = Math.ceil(capacity / bedsPerRoom);
await generateSeatMapMutation.mutateAsync({
coachCount: 1,
roomsPerCoach,
roomType: bedCategory,
});
};
const handleDelete = (item: any, isCoachType: boolean) => {
setDeleteConfirm({ isOpen: true, item: { ...item, isCoachType } });
};
const confirmDelete = async () => {
try {
if (deleteConfirm.item?.isCoachType) {
await deleteCoachTypeMutation.mutateAsync(deleteConfirm.item.id);
} else {
await deleteCoachMutation.mutateAsync(deleteConfirm.item.id);
}
setDeleteConfirm({ isOpen: false, item: null });
} catch (err: any) {
const msg = err?.response?.data?.message || err?.message || 'Delete failed';
setDeleteConfirm((prev) => ({ ...prev, error: msg }));
}
};
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',
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: 'Type/Arrangement',
render: (coach: any) => {
// Check if this is a bed coach based on coach type name containing 'bed'
const coachTypeName = coach.coachType?.name?.toLowerCase() || '';
const isBedCoach = coachTypeName.includes('bed') || coachTypeName.includes('sleeper') || coachTypeName.includes('berth');
if (isBedCoach) {
// Determine if it's VIP or Economy based on coach type name
const isVIP = coachTypeName.includes('vip');
return (
<div className="flex items-center gap-2">
<Bed className="h-4 w-4 text-blue-600" />
<span className="text-sm font-mono">{coach.arrangement || 'N/A'}</span>
</div>
);
}
return (
<div className="flex items-center gap-2">
<Armchair className="h-4 w-4 text-green-600" />
<span className="text-sm font-mono">{coach.arrangement || 'N/A'}</span>
</div>
);
},
},
{
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 });
setSelectedCoachTypeId(item.coachTypeId || '');
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);
setSelectedCoachTypeId('');
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>
<button
onClick={() => { setActiveTab('utilization'); setSearch(''); }}
className={`px-4 py-3 font-medium transition-colors ${
activeTab === 'utilization'
? 'border-b-2 border-primary text-primary'
: 'text-muted-foreground hover:text-foreground'
}`}
>
Utilization Report
</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>
)}
{/* Utilization Tab */}
{activeTab === 'utilization' && (() => {
const rows = Array.isArray(utilizationData) ? utilizationData : (utilizationData as any)?.data || [];
return (
<div className="pt-6 space-y-4">
<DataTable
columns={[
{ key: 'sequence', label: 'Seq', render: (r: any) => <span className="font-mono">{r.sequence}</span> },
{ key: 'number', label: 'Coach', render: (r: any) => <span className="font-medium">{r.number}</span> },
{ key: 'coachType', label: 'Type', render: (r: any) => <span className="text-sm">{r.coachType || 'N/A'}</span> },
{ key: 'totalSeats', label: 'Total Seats', render: (r: any) => <span className="font-mono">{r.totalSeats}</span> },
{ key: 'availableSeats', label: 'Available', render: (r: any) => <span className="font-mono text-green-600">{r.availableSeats}</span> },
{ key: 'bookedSeats', label: 'Booked', render: (r: any) => <span className="font-mono text-red-600">{r.bookedSeats}</span> },
{ key: 'blockedSeats', label: 'Blocked', render: (r: any) => <span className="font-mono text-gray-500">{r.blockedSeats}</span> },
{ key: 'maintenanceSeats', label: 'Maintenance', render: (r: any) => <span className="font-mono text-orange-500">{r.maintenanceSeats}</span> },
{
key: 'utilizationRate', label: 'Utilization',
render: (r: any) => (
<div className="flex items-center gap-2">
<div className="w-20 h-2 bg-muted rounded-full overflow-hidden">
<div className="h-full bg-primary rounded-full" style={{ width: `${r.utilizationRate}%` }} />
</div>
<span className="font-mono text-sm">{r.utilizationRate}%</span>
</div>
),
},
{ key: 'totalAssignments', label: 'Assignments', render: (r: any) => <span className="font-mono">{r.totalAssignments}</span> },
{ key: 'totalBookings', label: 'Total Bookings', render: (r: any) => <span className="font-mono font-semibold">{r.totalBookings}</span> },
]}
data={rows}
actions={[]}
loading={utilizationLoading}
emptyMessage="No coach utilization data available"
/>
</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}
isLoading={deleteCoachTypeMutation.isPending || deleteCoachMutation.isPending}
error={deleteConfirm.error}
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);
setSelectedCoachTypeId('');
}}
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 || ''}
onChange={(e) => setSelectedCoachTypeId(e.target.value)}
required
>
<option value="">Select Coach Type</option>
{coachTypesArray.map((ct: any) => (
<option key={ct.id} value={ct.id}>
{ct.code} - {ct.name}
</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>
{/* Conditionally show bed fields only for Economy and Regular coach types */}
{(() => {
const selectedCoachType = coachTypesArray.find((ct: any) => ct.id === (selectedCoachTypeId || editingItem?.coachTypeId));
const isEconomyOrRegular = selectedCoachType &&
(selectedCoachType.name?.toLowerCase().includes('economy') ||
selectedCoachType.name?.toLowerCase().includes('regular') ||
selectedCoachType.type?.toLowerCase().includes('economy') ||
selectedCoachType.type?.toLowerCase().includes('regular'));
return isEconomyOrRegular ? (
<>
<div>
<label className="label">Bed Category</label>
<select
name="bedCategory"
className="input"
defaultValue={editingItem?.bedCategory || ''}
>
<option value="">Select bed category</option>
<option value="ECONOMY_BED">Economy Bed</option>
<option value="VIP_BED">VIP Bed</option>
</select>
<p className="text-xs text-muted-foreground mt-1">
Select if this is a bed coach
</p>
</div>
<div>
<label className="label">Beds Per Room</label>
<select
name="bedsPerRoom"
className="input"
defaultValue={editingItem?.bedsPerRoom || ''}
>
<option value="">Auto (VIP: 4, Economy: 6)</option>
<option value="2">2 beds per room</option>
<option value="4">4 beds per room</option>
<option value="6">6 beds per room</option>
</select>
<p className="text-xs text-muted-foreground mt-1">
Only applies to bed coaches
</p>
</div>
</>
) : null;
})()}
<div>
<label className="label">Arrangement *</label>
<input
type="text"
name="arrangement"
className="input"
defaultValue={editingItem?.arrangement || editingItem?.seatArrangement || '2+2'}
required
placeholder="e.g., 2+2, 3+2"
/>
<p className="text-xs text-muted-foreground mt-1">
For regular seats: columns separated by +
</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 || 1}
min="1"
required
placeholder="1"
/>
<p className="text-xs text-muted-foreground mt-1">
Position in train consist
</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="INACTIVE">Inactive</option>
</select>
</div>
</div>
<div className="flex justify-end gap-2 pt-4">
<ActionButton
type="button"
variant="secondary"
onClick={handlePreviewSeatMap}
loading={generateSeatMapMutation.isPending}
>
Preview Bed Layout
</ActionButton>
<ActionButton
type="button"
variant="secondary"
onClick={() => {
setShowModal(false);
setEditingItem(null);
setSelectedCoachTypeId('');
}}
>
Cancel
</ActionButton>
<ActionButton
type="submit"
loading={createCoachMutation.isPending || updateCoachMutation.isPending}
>
{editingItem?.isCoach ? 'Update' : 'Create'} Coach
</ActionButton>
</div>
</form>
)}
</Modal>
{/* Seat Map Preview Modal */}
<Modal
isOpen={showPreviewModal}
onClose={() => {
setShowPreviewModal(false);
setSeatMapPreview(null);
}}
title="Bed Layout Preview"
size="lg"
>
{seatMapPreview && (
<div className="space-y-4">
<div className="bg-muted/50 p-4 rounded-lg">
<h4 className="font-semibold mb-2">Configuration</h4>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>Room Type: <span className="font-medium">{seatMapPreview.roomType}</span></div>
<div>Rooms per Coach: <span className="font-medium">{seatMapPreview.roomsPerCoach}</span></div>
<div>Beds per Room: <span className="font-medium">{seatMapPreview.bedsPerRoom}</span></div>
<div>Total Beds: <span className="font-medium">{seatMapPreview.totalBeds}</span></div>
</div>
</div>
<div className="space-y-2">
<h4 className="font-semibold">Bed Layout Sample (First Few Rooms)</h4>
<div className="bg-gray-50 p-4 rounded border max-h-64 overflow-y-auto">
{seatMapPreview.seats?.slice(0, 24).map((seat: any, idx: number) => (
<div key={idx} className="text-xs mb-1 font-mono">
{seat.seat_id} - Room: {seat.room_id} - {seat.position} {seat.bed_type}
</div>
))}
{seatMapPreview.seats?.length > 24 && (
<div className="text-xs text-muted-foreground mt-2">
... and {seatMapPreview.seats.length - 24} more beds
</div>
)}
</div>
</div>
<div className="flex justify-end">
<ActionButton
onClick={() => {
setShowPreviewModal(false);
setSeatMapPreview(null);
}}
>
Close
</ActionButton>
</div>
</div>
)}
</Modal>
</div>
);
}