Files
edr-platform/apps/edr-passenger-web/backoffice/src/app/seats/page.tsx
2026-07-15 21:11:23 +03:00

1106 lines
44 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { seatsApi, schedulesApi, fleetApi, routeCoachTemplatesApi } from '@/lib/api';
import { routesApi } from '@/lib/api/routes';
import Modal from '@/components/ui/Modal';
import ActionButton from '@/components/ui/ActionButton'
import { Armchair, Lock, Unlock, Bed, X, RotateCcw, ChevronDown, Train, Wrench } from 'lucide-react';
export default function SeatsPage() {
const [activeTab, setActiveTab] = useState<'route' | 'schedule'>('route');
const [selectedSchedule, setSelectedSchedule] = useState('');
const [selectedRoute, setSelectedRoute] = useState('');
const [expandedCoaches, setExpandedCoaches] = useState<Set<string>>(new Set());
const [showBlockModal, setShowBlockModal] = useState(false);
const [showRemoveModal, setShowRemoveModal] = useState(false);
const [selectedSeat, setSelectedSeat] = useState<any>(null);
const [blockReason, setBlockReason] = useState('');
const [showBlockCoachModal, setShowBlockCoachModal] = useState(false);
const [selectedCoach, setSelectedCoach] = useState<any>(null);
const [blockCoachReason, setBlockCoachReason] = useState('');
const [showUnblockCoachModal, setShowUnblockCoachModal] = useState(false);
const [coachToUnblock, setCoachToUnblock] = useState<any>(null);
const [showMaintenanceModal, setShowMaintenanceModal] = useState(false);
const [maintenanceReason, setMaintenanceReason] = useState('');
const queryClient = useQueryClient();
const { data: schedulesData } = useQuery({
queryKey: ['schedules'],
queryFn: () => schedulesApi.getAll(),
});
const { data: seatMapData, isLoading } = useQuery({
queryKey: ['seatmap', selectedSchedule],
queryFn: () => selectedSchedule ? seatsApi.getSeatMap(selectedSchedule) : Promise.resolve(null),
enabled: !!selectedSchedule,
staleTime: 0,
});
const { data: coachTypesData } = useQuery({
queryKey: ['coachTypes'],
queryFn: () => fleetApi.getCoaches(),
});
const { data: routesData } = useQuery({
queryKey: ['routes'],
queryFn: () => routesApi.getAll(),
});
const { data: routeCoachesData, isLoading: routeCoachesLoading } = useQuery({
queryKey: ['routeCoaches', selectedRoute],
staleTime: 0,
queryFn: async () => {
if (!selectedRoute) return null;
const template: any[] = await routeCoachTemplatesApi.get(selectedRoute);
if (template?.length) {
const fullCoaches = await Promise.all(
template.map((entry: any) => fleetApi.getCoach(entry.coachId ?? entry.coach?.id))
);
return fullCoaches.map((coach: any, i: number) => ({
...coach,
coachNumber: coach.number,
positionNumber: template[i].positionNumber,
seatArrangement: coach.arrangement,
}));
}
// No template — fetch coaches from the most recent schedule for this route
const schedules: any = await schedulesApi.getAll({ routeId: selectedRoute });
const scheduleList: any[] = schedules?.items || schedules?.data || (Array.isArray(schedules) ? schedules : []);
if (!scheduleList.length) return [];
const latestSchedule = scheduleList[scheduleList.length - 1];
const seatMap: any = await seatsApi.getSeatMap(latestSchedule.id);
return (seatMap?.coaches || []).map((coach: any, i: number) => ({
...coach,
coachNumber: coach.number ?? coach.coachNumber,
positionNumber: coach.positionNumber ?? i + 1,
seatArrangement: coach.arrangement ?? coach.seatArrangement,
}));
},
enabled: !!selectedRoute,
});
const invalidateSeatData = () => {
queryClient.refetchQueries({ queryKey: ['seatmap', selectedSchedule] });
queryClient.refetchQueries({ queryKey: ['routeCoaches', selectedRoute] });
};
const blockMutation = useMutation({
mutationFn: ({ seatId, reason }: any) => seatsApi.block(seatId, { reason }),
onSuccess: () => {
invalidateSeatData();
setShowBlockModal(false);
setSelectedSeat(null);
setBlockReason('');
},
});
const unblockMutation = useMutation({
mutationFn: (seatId: string) => seatsApi.unblock(seatId),
onSuccess: () => {
invalidateSeatData();
},
});
const removeSeatMutation = useMutation({
mutationFn: (seatId: string) => seatsApi.removeSeat(seatId),
onSuccess: () => {
invalidateSeatData();
setShowRemoveModal(false);
setSelectedSeat(null);
},
});
const undoRemoveMutation = useMutation({
mutationFn: (seatId: string) => seatsApi.undoRemove(seatId),
onSuccess: () => {
invalidateSeatData();
},
});
const maintenanceMutation = useMutation({
mutationFn: ({ seatId, reason }: { seatId: string; reason: string }) =>
seatsApi.setMaintenance(seatId, reason),
onSuccess: () => {
invalidateSeatData();
setShowMaintenanceModal(false);
setSelectedSeat(null);
setMaintenanceReason('');
},
});
const clearMaintenanceMutation = useMutation({
mutationFn: (seatId: string) => seatsApi.clearMaintenance(seatId),
onSuccess: () => invalidateSeatData(),
});
const schedules = schedulesData?.items || schedulesData?.data || [];
const routes = routesData?.items || routesData?.data || [];
const coaches = activeTab === 'schedule' ? (seatMapData?.coaches || []) : (Array.isArray(routeCoachesData) ? routeCoachesData : []);
const blockCoachMutation = useMutation({
mutationFn: async ({ coachId, reason }: any) => {
const coachSeats = coaches.find((c: any) => c.id === coachId)?.seats || [];
const seatIds = coachSeats.map((s: any) => s.id).filter((id: any) => id);
return Promise.all(seatIds.map((seatId: string) => seatsApi.block(seatId, { reason })));
},
onSuccess: () => {
invalidateSeatData();
setShowBlockCoachModal(false);
setSelectedCoach(null);
setBlockCoachReason('');
},
});
const unblockCoachMutation = useMutation({
mutationFn: async ({ coachId }: any) => {
const coachSeats = coaches.find((c: any) => c.id === coachId)?.seats || [];
const seatIds = coachSeats.map((s: any) => s.id).filter((id: any) => id);
return Promise.all(seatIds.map((seatId: string) => seatsApi.unblock(seatId)));
},
onSuccess: () => {
invalidateSeatData();
setShowUnblockCoachModal(false);
setCoachToUnblock(null);
},
});
const toggleCoach = (coachId: string) => {
const newExpanded = new Set(expandedCoaches);
if (newExpanded.has(coachId)) {
newExpanded.delete(coachId);
} else {
newExpanded.add(coachId);
}
setExpandedCoaches(newExpanded);
};
const handleBlock = (seat: any) => {
setSelectedSeat(seat);
setShowBlockModal(true);
};
const handleUnblock = async (seat: any) => {
if (confirm('Are you sure you want to unblock this seat?')) {
await unblockMutation.mutateAsync(seat.id);
}
};
const handleRemoveSeat = (seat: any) => {
setSelectedSeat(seat);
setShowRemoveModal(true);
};
const handleUndoRemove = async (seat: any) => {
if (confirm('Restore this removed seat?')) {
await undoRemoveMutation.mutateAsync(seat.id);
}
};
const handleSetMaintenance = (seat: any) => {
setSelectedSeat(seat);
setShowMaintenanceModal(true);
};
const handleClearMaintenance = async (seat: any) => {
if (confirm('Clear maintenance status for this seat?')) {
await clearMaintenanceMutation.mutateAsync(seat.id);
}
};
const handleBlockCoach = (coach: any) => {
setSelectedCoach(coach);
setShowBlockCoachModal(true);
};
const handleUnblockCoach = (coach: any) => {
const isBlocked = coach.seats?.some((s: any) => s.status === 'BLOCKED' || s.isBlocked);
if (isBlocked) {
setCoachToUnblock(coach);
setShowUnblockCoachModal(true);
}
};
const confirmUnblockCoach = async () => {
if (coachToUnblock) {
await unblockCoachMutation.mutateAsync({ coachId: coachToUnblock.id });
}
};
const isCoachBlocked = (coach: any) => {
const seats = (coach.seats || []).filter((s: any) => s.seatNumber && !s.seatNumber.startsWith('-'));
return seats.length > 0 && seats.every((s: any) => s.status === 'BLOCKED' || s.isBlocked);
};
const isCoachUnblocked = (coach: any) => {
const seats = (coach.seats || []).filter((s: any) => s.seatNumber && !s.seatNumber.startsWith('-'));
return seats.length > 0 && seats.every((s: any) => s.status !== 'BLOCKED' && !s.isBlocked);
};
const submitBlockCoach = async () => {
if (!blockCoachReason.trim()) {
alert('Please provide a reason for blocking');
return;
}
await blockCoachMutation.mutateAsync({ coachId: selectedCoach.id, reason: blockCoachReason });
};
const submitBlock = async () => {
if (!blockReason.trim()) {
alert('Please provide a reason for blocking');
return;
}
await blockMutation.mutateAsync({ seatId: selectedSeat.id, reason: blockReason });
};
const submitRemoveSeat = async () => {
await removeSeatMutation.mutateAsync(selectedSeat.id);
};
const getSeatStatus = (seat: any) => {
if (seat.status === 'UNDER_MAINTENANCE') return 'UNDER_MAINTENANCE';
if (seat.status === 'BLOCKED' || seat.isBlocked) return 'BLOCKED';
if (seat.status === 'BOOKED' || seat.isBooked) return 'BOOKED';
if (seat.status === 'HELD') return 'HELD';
return 'AVAILABLE';
};
const getSeatColor = (status: string) => {
switch (status) {
case 'AVAILABLE': return 'bg-green-500';
case 'BOOKED': return 'bg-red-500';
case 'HELD': return 'bg-yellow-500';
case 'BLOCKED': return 'bg-gray-500';
case 'UNDER_MAINTENANCE': return 'bg-orange-500';
default: return 'bg-gray-300';
}
};
const parseSeatArrangement = (arrangement: string | null): number[] => {
if (!arrangement) return [2, 2];
const parts = arrangement.split('+').map(p => parseInt(p.trim()));
return parts.length === 2 ? parts : [2, 2];
};
const getBedLabel = (bedPosition: string | null): string => {
if (bedPosition === 'upper') return 'U';
if (bedPosition === 'middle') return 'M';
if (bedPosition === 'lower') return 'L';
return '';
};
const formatBedSeatNumber = (seat: any): string => {
if (!seat.seatNumber || !seat.bedPosition) return seat.seatNumber || '';
const label = getBedLabel(seat.bedPosition);
return `${seat.seatNumber}${label}`;
};
const renderCoachSeats = (coach: any, isBedCoach: boolean) => {
const allSeats = coach.seats || [];
const validSeats = allSeats.filter((s: any) => s.seatNumber && !s.seatNumber.startsWith('-'));
const removedSeats = allSeats.filter((s: any) => s.seatNumber && s.seatNumber.startsWith('-'));
if (validSeats.length === 0 && removedSeats.length === 0) {
return <div className="text-xs text-muted-foreground">No seats</div>;
}
const hasBedPositionData = validSeats.some((s: any) => s.bedPosition);
if (isBedCoach) {
const arrangement = parseSeatArrangement(coach.seatArrangement);
const seatsPerRow = arrangement[0] + (arrangement[1] || 0);
const allSeatsForLayout = [...validSeats, ...removedSeats];
const rows: any[][] = [];
const seatClassStr = typeof coach?.seatClass === 'string' ? coach.seatClass : (coach?.seatClass?.name || '');
const isVipBed = seatClassStr.toLowerCase().includes('vip');
const bedWidth = isVipBed ? 'w-20' : 'w-16';
for (let i = 0; i < allSeatsForLayout.length; i += seatsPerRow) {
rows.push(allSeatsForLayout.slice(i, i + seatsPerRow));
}
return (
<div className="space-y-0">
{rows.map((rowSeats: any[], idx: number) => {
const isFirstInPair = idx % 2 === 0;
const shouldFlipIcon = !isFirstInPair;
const isLastRow = idx === rows.length - 1;
const nextRowSeats = !isLastRow ? rows[idx + 1] : null;
return (
<div key={`bed-row-${idx}`}>
<div className="flex gap-0.5 justify-center">
{rowSeats.map((seat: any) => (
<SeatIcon
key={seat.id}
seat={seat}
coach={coach}
isBedCoach={true}
shouldFlipIcon={shouldFlipIcon}
getSeatStatus={getSeatStatus}
getSeatColor={getSeatColor}
handleBlock={handleBlock}
handleRemoveSeat={handleRemoveSeat}
handleUnblock={handleUnblock}
handleUndoRemove={handleUndoRemove}
handleSetMaintenance={handleSetMaintenance}
handleClearMaintenance={handleClearMaintenance}
hideNumber={true}
/>
))}
</div>
{isFirstInPair && nextRowSeats && (
<div className="flex gap-0.5 justify-center text-xs my-1">
{rowSeats.map((seat: any, seatIdx: number) => {
const currentSeat = rowSeats[seatIdx];
const nextSeat = nextRowSeats[seatIdx];
const currentFormatted = currentSeat ? formatBedSeatNumber(currentSeat) : '';
const nextFormatted = nextSeat ? formatBedSeatNumber(nextSeat) : '';
return (
<div key={`num-between-${seat.id}`} className={`${bedWidth} flex flex-col items-center justify-center text-xs font-bold mb-1 leading-3 text-foreground`}>
<div className="mb-1">{currentFormatted}</div>
<div className="mt-1">{nextFormatted}</div>
</div>
);
})}
</div>
)}
{!isFirstInPair && <div className="h-2" />}
</div>
);
})}
</div>
);
}
const arrangement = parseSeatArrangement(coach.seatArrangement);
const leftCount = arrangement[0];
const rightCount = arrangement[1] || 0;
const rows = [];
const processedRows = new Set();
const allSeatsForLayout = [...validSeats, ...removedSeats];
for (const seat of allSeatsForLayout) {
if (!processedRows.has(seat.row)) {
rows.push(allSeatsForLayout.filter((s: any) => s.row === seat.row).sort((a: any, b: any) => {
const colA = a.col.charCodeAt(0);
const colB = b.col.charCodeAt(0);
return colA - colB;
}));
processedRows.add(seat.row);
}
}
return (
<div className="space-y-0">
{rows.map((rowSeats: any[], rowIdx: number) => {
const leftSeats = rowSeats.slice(0, leftCount);
const rightSeats = rowSeats.slice(leftCount);
const rowNumber = rowSeats[0]?.row || 1;
const shouldFlipArmchair = rowNumber % 2 === 0;
const showSpacing = rowIdx % 2 === 1;
return (
<div key={`row-${rowSeats[0]?.id}`}>
{shouldFlipArmchair && (
<div className="flex gap-0.5 justify-center text-xs text-muted-foreground mb-1">
<div className="flex gap-0.5">
{leftSeats.map((seat: any) => (
<div key={`num-before-left-${seat.id}`} className="w-11 h-4 flex items-center justify-center text-xs font-bold mb-0.5 leading-3 text-foreground">
{seat.seatNumber && !seat.seatNumber.startsWith('-') ? seat.seatNumber : ''}
</div>
))}
</div>
{rightSeats.length > 0 && <div className="w-8" />}
{rightSeats.length > 0 && (
<div className="flex gap-0.5">
{rightSeats.map((seat: any) => (
<div key={`num-before-right-${seat.id}`} className="w-11 h-4 flex items-center justify-center text-xs font-bold mb-0.5 leading-3 text-foreground">
{seat.seatNumber && !seat.seatNumber.startsWith('-') ? seat.seatNumber : ''}
</div>
))}
</div>
)}
</div>
)}
<div className="flex gap-0.5 justify-center">
<div className="flex gap-0.5">
{leftSeats.map((seat: any) => (
<SeatIcon
key={seat.id}
seat={seat}
coach={coach}
isBedCoach={false}
shouldFlipIcon={shouldFlipArmchair}
getSeatStatus={getSeatStatus}
getSeatColor={getSeatColor}
handleBlock={handleBlock}
handleRemoveSeat={handleRemoveSeat}
handleUnblock={handleUnblock}
handleUndoRemove={handleUndoRemove}
handleSetMaintenance={handleSetMaintenance}
handleClearMaintenance={handleClearMaintenance}
hideNumber={true}
/>
))}
</div>
{rightSeats.length > 0 && <div className="w-8" />}
{rightSeats.length > 0 && (
<div className="flex gap-0.5">
{rightSeats.map((seat: any) => (
<SeatIcon
key={seat.id}
seat={seat}
coach={coach}
isBedCoach={false}
shouldFlipIcon={shouldFlipArmchair}
getSeatStatus={getSeatStatus}
getSeatColor={getSeatColor}
handleBlock={handleBlock}
handleRemoveSeat={handleRemoveSeat}
handleUnblock={handleUnblock}
handleUndoRemove={handleUndoRemove}
handleSetMaintenance={handleSetMaintenance}
handleClearMaintenance={handleClearMaintenance}
hideNumber={true}
/>
))}
</div>
)}
</div>
{!shouldFlipArmchair && (
<div className="flex gap-0.5 justify-center text-xs text-muted-foreground mb-1">
<div className="flex gap-0.5">
{leftSeats.map((seat: any) => (
<div key={`num-left-${seat.id}`} className="w-11 h-4 flex items-center justify-center text-xs font-bold mb-0.5 leading-3 text-foreground">
{seat.seatNumber && !seat.seatNumber.startsWith('-') ? seat.seatNumber : ''}
</div>
))}
</div>
{rightSeats.length > 0 && <div className="w-8" />}
{rightSeats.length > 0 && (
<div className="flex gap-0.5">
{rightSeats.map((seat: any) => (
<div key={`num-right-${seat.id}`} className="w-11 h-4 flex items-center justify-center text-xs font-bold mb-0.5 leading-3 text-foreground">
{seat.seatNumber && !seat.seatNumber.startsWith('-') ? seat.seatNumber : ''}
</div>
))}
</div>
)}
</div>
)}
{showSpacing && <div className="h-2" />}
</div>
);
})}
</div>
);
};
const coachesWithSeats = coaches
.filter((coach: any) => {
const seats = (coach.seats || []).filter((s: any) => s.seatNumber);
return seats.length > 0;
})
.sort((a: any, b: any) => {
// Try multiple sequence field possibilities
const seqA = a.positionNumber ?? a.sequence ?? a.coach?.sequence ?? 999;
const seqB = b.positionNumber ?? b.sequence ?? b.coach?.sequence ?? 999;
return seqA - seqB;
});
const activeSelection = activeTab === 'schedule' ? selectedSchedule : selectedRoute;
const isLoadingData = activeTab === 'schedule' ? isLoading : routeCoachesLoading;
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-foreground">Seat Management</h1>
<p className="text-muted-foreground mt-1">View and manage seats by coach</p>
</div>
{/* Tab switcher */}
<div className="flex gap-1 p-1 bg-muted rounded-lg w-fit">
<button
onClick={() => setActiveTab('route')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
activeTab === 'route'
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
By Route
</button>
<button
onClick={() => setActiveTab('schedule')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
activeTab === 'schedule'
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
By Schedule
</button>
</div>
{!activeSelection ? (
<div className="card">
{activeTab === 'schedule' ? (
<>
<label className="label">Select Schedule</label>
<select value={selectedSchedule} onChange={(e) => setSelectedSchedule(e.target.value)} className="input">
<option value="">Select a schedule...</option>
{schedules.map((schedule: any) => {
const routeName = schedule.route?.name || 'N/A';
const date = schedule.departureAt ? new Date(schedule.departureAt).toLocaleDateString() : 'N/A';
return <option key={schedule.id} value={schedule.id}>{date} - {routeName}</option>;
})}
</select>
</>
) : (
<>
<label className="label">Select Route</label>
<select value={selectedRoute} onChange={(e) => setSelectedRoute(e.target.value)} className="input">
<option value="">Select a route...</option>
{routes.map((route: any) => (
<option key={route.id} value={route.id}>{route.name}</option>
))}
</select>
</>
)}
<div className="text-center py-12 text-muted-foreground mt-8">
<Armchair className="h-12 w-12 mx-auto mb-3 opacity-50" />
<p>Select a {activeTab === 'schedule' ? 'schedule' : 'route'} to view seat map</p>
</div>
</div>
) : isLoadingData ? (
<div className="card text-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
<p className="text-muted-foreground mt-3">Loading seats...</p>
</div>
) : coachesWithSeats.length === 0 ? (
<div className="space-y-6">
<div className="card">
{activeTab === 'schedule' ? (
<>
<label className="label">Select Schedule</label>
<select value={selectedSchedule} onChange={(e) => setSelectedSchedule(e.target.value)} className="input">
<option value="">Select a schedule...</option>
{schedules.map((schedule: any) => {
const trainNumber = schedule.train?.trainNumber || schedule.train?.name || 'N/A';
const routeName = schedule.route?.name || 'N/A';
const date = schedule.departureAt ? new Date(schedule.departureAt).toLocaleDateString() : 'N/A';
return <option key={schedule.id} value={schedule.id}>{trainNumber} - {routeName} - {date}</option>;
})}
</select>
</>
) : (
<>
<label className="label">Select Route</label>
<select value={selectedRoute} onChange={(e) => setSelectedRoute(e.target.value)} className="input">
<option value="">Select a route...</option>
{routes.map((route: any) => (
<option key={route.id} value={route.id}>{route.name}</option>
))}
</select>
</>
)}
</div>
<div className="card text-center py-12 text-muted-foreground">
<p>No coaches with seats found for this {activeTab === 'schedule' ? 'schedule' : 'route'}</p>
</div>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="card h-fit sticky top-6 space-y-6">
<div>
{activeTab === 'schedule' ? (
<>
<label className="label">Select Schedule</label>
<select value={selectedSchedule} onChange={(e) => setSelectedSchedule(e.target.value)} className="input">
<option value="">Select a schedule...</option>
{schedules.map((schedule: any) => {
const trainNumber = schedule.train?.trainNumber || schedule.train?.name || 'N/A';
const routeName = schedule.route?.name || 'N/A';
const date = schedule.departureAt ? new Date(schedule.departureAt).toLocaleDateString() : 'N/A';
return <option key={schedule.id} value={schedule.id}>{trainNumber} - {routeName} - {date}</option>;
})}
</select>
</>
) : (
<>
<label className="label">Select Route</label>
<select value={selectedRoute} onChange={(e) => setSelectedRoute(e.target.value)} className="input">
<option value="">Select a route...</option>
{routes.map((route: any) => (
<option key={route.id} value={route.id}>{route.name}</option>
))}
</select>
</>
)}
</div>
<div className="space-y-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<h3 className="font-semibold text-sm text-foreground">Seat Status</h3>
<div className="space-y-2">
<div className="flex items-center gap-3">
<div className="w-5 h-5 rounded bg-green-500"></div>
<span className="text-sm text-muted-foreground">Available</span>
</div>
<div className="flex items-center gap-3">
<div className="w-5 h-5 rounded bg-red-500"></div>
<span className="text-sm text-muted-foreground">Booked</span>
</div>
<div className="flex items-center gap-3">
<div className="w-5 h-5 rounded bg-yellow-500"></div>
<span className="text-sm text-muted-foreground">Held</span>
</div>
<div className="flex items-center gap-3">
<div className="w-5 h-5 rounded bg-gray-500"></div>
<span className="text-sm text-muted-foreground">Blocked</span>
</div>
<div className="flex items-center gap-3">
<div className="w-5 h-5 rounded bg-orange-500"></div>
<span className="text-sm text-muted-foreground">Under Maintenance</span>
</div>
<div className="flex items-center gap-3">
<div className="w-5 h-5 rounded border-2 border-dashed border-gray-400"></div>
<span className="text-sm text-muted-foreground">Removed</span>
</div>
</div>
</div>
</div>
<div className="space-y-4 w-80">
<div className="bg-gradient-to-r from-[rgb(20,113,76)] to-[rgb(15,85,57)] rounded-lg border-2 border-[rgb(20,113,76)] flex items-center justify-center shadow-lg p-6 h-24">
<Train className="w-14 h-14 text-white" />
</div>
{coachesWithSeats.map((coach: any, index: number) => {
const coachData = coachTypesData?.items?.find((c: any) => c.id === coach.id) || coach;
const coachTypeName = coachData?.coachType?.type || coachData?.coachType?.name || 'Coach';
const seatClassName = coachData?.seatClass?.name || coach?.seatClass?.name || coach?.coachClass || '';
const isBedCoach = seatClassName.toLowerCase().includes('bed') ||
coachTypeName.toLowerCase().includes('bed') ||
(coach.seats || []).some((s: any) => s.bedPosition);
const seats = (coach.seats || []).filter((s: any) => s.seatNumber);
const isExpanded = expandedCoaches.has(coach.id);
const seatOrBedLabel = isBedCoach ? 'beds' : 'seats';
const sequence = coach.positionNumber ?? coach.sequence ?? coachData?.sequence ?? index + 1;
return (
<div key={coach.id} className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-white dark:bg-gray-800/50 shadow-md hover:shadow-lg transition-shadow">
<div className="px-4 py-3 bg-gradient-to-r from-[rgb(20,113,76)]/10 to-[rgb(20,113,76)]/5 dark:from-[rgb(20,113,76)]/20 dark:to-[rgb(20,113,76)]/10 border-b border-[rgb(20,113,76)]/20 dark:border-[rgb(20,113,76)]/30 flex items-center justify-between">
<button
onClick={() => toggleCoach(coach.id)}
className="flex-1 flex items-center gap-3 hover:opacity-75 transition-opacity"
>
<div className={`transform transition-transform ${isExpanded ? 'rotate-180' : ''}`}>
<ChevronDown className="w-5 h-5 text-[rgb(20,113,76)]" />
</div>
<div className="text-left">
<p className="font-semibold text-foreground">{sequence} - {coach.coachNumber}</p>
<p className="text-xs text-muted-foreground">{coachTypeName} {seats.length} {seatOrBedLabel}</p>
</div>
</button>
<ActionButton
variant={isCoachBlocked(coach) ? 'danger' : 'secondary'}
size="sm"
onClick={() => isCoachBlocked(coach) ? handleUnblockCoach(coach) : handleBlockCoach(coach)}
className="ml-2"
disabled={!isCoachBlocked(coach) && !isCoachUnblocked(coach)}
>
{isCoachBlocked(coach) ? (
<>
<Unlock className="w-4 h-4" /> Unblock
</>
) : isCoachUnblocked(coach) ? (
<>
<Lock className="w-4 h-4" /> Block
</>
) : (
'Mixed Status'
)}
</ActionButton>
</div>
{isExpanded && (
<div className="px-4 py-4 bg-white dark:bg-gray-900/50 border-t border-gray-200 dark:border-gray-700">
<div className="bg-gray-50 dark:bg-gray-900/30 rounded-lg p-3 inline-block">
{renderCoachSeats(coach, isBedCoach)}
</div>
</div>
)}
</div>
);
})}
</div>
</div>
)}
<Modal
isOpen={showBlockModal}
onClose={() => {
setShowBlockModal(false);
setSelectedSeat(null);
setBlockReason('');
}}
title="Block Seat"
size="md"
>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Block seat <strong>{selectedSeat?.seatNumber}</strong> in Coach <strong>{selectedSeat?.coach?.coachNumber}</strong>
</p>
<div>
<label className="label">Reason for Blocking *</label>
<textarea
className="input"
rows={3}
value={blockReason}
onChange={(e) => setBlockReason(e.target.value)}
placeholder="e.g., Maintenance required, Damaged seat, Reserved for staff"
/>
</div>
<div className="flex justify-end gap-2">
<ActionButton
variant="secondary"
onClick={() => {
setShowBlockModal(false);
setSelectedSeat(null);
setBlockReason('');
}}
>
Cancel
</ActionButton>
<ActionButton
onClick={submitBlock}
loading={blockMutation.isPending}
disabled={!blockReason.trim()}
>
Block Seat
</ActionButton>
</div>
</div>
</Modal>
<Modal
isOpen={showRemoveModal}
onClose={() => {
setShowRemoveModal(false);
setSelectedSeat(null);
}}
title="Remove Seat"
size="md"
>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Remove seat <strong>{selectedSeat?.seatNumber}</strong> from Coach <strong>{selectedSeat?.coach?.coachNumber}</strong>
</p>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<p className="text-sm text-yellow-800">
This will mark the seat as removed. The seat will show as an empty space on the seat map.
You can undo this action anytime by clicking the undo button on the removed seat.
</p>
</div>
<div className="flex justify-end gap-2">
<ActionButton
variant="secondary"
onClick={() => {
setShowRemoveModal(false);
setSelectedSeat(null);
}}
>
Cancel
</ActionButton>
<ActionButton
variant="danger"
onClick={submitRemoveSeat}
loading={removeSeatMutation.isPending}
>
Remove Seat
</ActionButton>
</div>
</div>
</Modal>
<Modal
isOpen={showBlockCoachModal}
onClose={() => {
setShowBlockCoachModal(false);
setSelectedCoach(null);
setBlockCoachReason('');
}}
title="Block Coach"
size="md"
>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Block all seats in Coach <strong>{selectedCoach?.coachNumber}</strong>
</p>
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<p className="text-sm text-red-800">
This will block all {selectedCoach?.seats?.length || 0} seats in this coach.
</p>
</div>
<div>
<label className="label">Reason for Blocking *</label>
<textarea
className="input"
rows={3}
value={blockCoachReason}
onChange={(e) => setBlockCoachReason(e.target.value)}
placeholder="e.g., Major maintenance, Safety inspection, Temporary withdrawal"
/>
</div>
<div className="flex justify-end gap-2">
<ActionButton
variant="secondary"
onClick={() => {
setShowBlockCoachModal(false);
setSelectedCoach(null);
setBlockCoachReason('');
}}
>
Cancel
</ActionButton>
<ActionButton
variant="danger"
onClick={submitBlockCoach}
loading={blockCoachMutation.isPending}
disabled={!blockCoachReason.trim()}
>
Block Coach
</ActionButton>
</div>
</div>
</Modal>
<Modal
isOpen={showUnblockCoachModal}
onClose={() => {
setShowUnblockCoachModal(false);
setCoachToUnblock(null);
}}
title="Unblock Coach"
size="md"
>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Unblock all seats in Coach <strong>{coachToUnblock?.coachNumber}</strong>
</p>
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
<p className="text-sm text-green-800">
This will unblock all {coachToUnblock?.seats?.filter((s: any) => s.status === 'BLOCKED' || s.isBlocked).length || 0} blocked seats in this coach.
</p>
</div>
<div className="flex justify-end gap-2">
<ActionButton
variant="secondary"
onClick={() => {
setShowUnblockCoachModal(false);
setCoachToUnblock(null);
}}
>
Cancel
</ActionButton>
<ActionButton
onClick={confirmUnblockCoach}
loading={unblockCoachMutation.isPending}
>
Unblock Coach
</ActionButton>
</div>
</div>
</Modal>
<Modal
isOpen={showMaintenanceModal}
onClose={() => { setShowMaintenanceModal(false); setSelectedSeat(null); setMaintenanceReason(''); }}
title="Set Seat Under Maintenance"
size="md"
>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Set seat <strong>{selectedSeat?.seatNumber}</strong> to Under Maintenance
</p>
<div>
<label className="label">Reason *</label>
<textarea
className="input"
rows={3}
value={maintenanceReason}
onChange={(e) => setMaintenanceReason(e.target.value)}
placeholder="e.g., Seat mechanism broken, Upholstery replacement"
/>
</div>
<div className="flex justify-end gap-2">
<ActionButton
variant="secondary"
onClick={() => { setShowMaintenanceModal(false); setSelectedSeat(null); setMaintenanceReason(''); }}
>
Cancel
</ActionButton>
<ActionButton
onClick={() => maintenanceMutation.mutate({ seatId: selectedSeat.id, reason: maintenanceReason })}
loading={maintenanceMutation.isPending}
disabled={!maintenanceReason.trim()}
>
Set Maintenance
</ActionButton>
</div>
</div>
</Modal>
</div>
);
}
interface SeatIconProps {
seat: any;
coach: any;
isBedCoach: boolean;
shouldFlipIcon?: boolean;
hideNumber?: boolean;
getSeatStatus: (seat: any) => string;
getSeatColor: (status: string) => string;
handleBlock: (seat: any) => void;
handleRemoveSeat: (seat: any) => void;
handleUnblock: (seat: any) => void;
handleUndoRemove: (seat: any) => void;
handleSetMaintenance: (seat: any) => void;
handleClearMaintenance: (seat: any) => void;
}
function SeatIcon({
seat,
coach,
isBedCoach,
shouldFlipIcon = false,
hideNumber = false,
getSeatStatus,
getSeatColor,
handleBlock,
handleRemoveSeat,
handleUnblock,
handleUndoRemove,
handleSetMaintenance,
handleClearMaintenance,
}: SeatIconProps) {
const isRemoved = seat.seatNumber && seat.seatNumber.startsWith('-');
const seatClassStr = typeof coach?.seatClass === 'string' ? coach.seatClass : (coach?.seatClass?.name || coach?.coachClass || '');
const isVipBed = isBedCoach && seatClassStr.toLowerCase().includes('vip');
const bedWidth = isVipBed ? 'w-24' : 'w-16';
const width = isBedCoach ? bedWidth : 'w-10';
if (!seat.seatNumber) {
return <div className="w-7 h-7" />;
}
if (isRemoved) {
return (
<div className="relative group flex flex-col items-center">
<div className="w-11 h-11 rounded border-2 border-dashed border-gray-400 flex items-center justify-center hover:opacity-80 transition-opacity" title="Removed seat">
</div>
<div className="absolute top-full mt-1 bg-black/80 rounded shadow-lg flex items-center gap-1 p-1 z-20 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none group-hover:pointer-events-auto">
<button
onClick={() => handleUndoRemove(seat)}
className="p-1 bg-white rounded hover:bg-gray-100 pointer-events-auto"
title="Undo remove"
>
<RotateCcw className="h-3 w-3 text-gray-700" />
</button>
</div>
</div>
);
}
const status = getSeatStatus(seat);
const color = getSeatColor(status);
const canBlock = status === 'AVAILABLE';
const canUnblock = status === 'BLOCKED';
const canMaintenance = false;
const canClearMaintenance = status === 'UNDER_MAINTENANCE';
return (
<div className="relative group flex flex-col items-center">
{!hideNumber && (
<span className="text-xs font-bold mb-0.5 leading-3 text-foreground">
{seat.seatNumber}
</span>
)}
{isBedCoach ? (
<div
className={`${width} h-11 rounded flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity ${color}`}
title={`${seat.seatNumber} - ${seat.bedPosition} - ${status}`}
style={!shouldFlipIcon ? { transform: 'scaleY(-1)' } : undefined}
>
<Bed className="w-7 h-7 text-white" />
</div>
) : (
<div
className={`w-11 h-11 rounded flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity ${color}`}
title={`${seat.seatNumber} - ${status}`}
style={seat.row % 2 === 0 ? { transform: 'scaleY(-1)' } : undefined}
>
<Armchair className="w-7 h-7 text-white" />
</div>
)}
{(canBlock || canUnblock || canMaintenance || canClearMaintenance) && (
<div className="absolute top-full mt-1 bg-black/80 rounded shadow-lg flex items-center gap-1 p-1 z-20 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none group-hover:pointer-events-auto">
{canBlock && (
<>
<button
onClick={() => handleBlock(seat)}
className="p-1 bg-white rounded hover:bg-gray-100 pointer-events-auto"
title="Block seat"
>
<Lock className="h-3 w-3 text-gray-700" />
</button>
<button
onClick={() => handleRemoveSeat(seat)}
className="p-1 bg-white rounded hover:bg-gray-100 pointer-events-auto"
title="Remove seat"
>
<X className="h-3 w-3 text-gray-700" />
</button>
</>
)}
{canUnblock && (
<button
onClick={() => handleUnblock(seat)}
className="p-1 bg-white rounded hover:bg-gray-100 pointer-events-auto"
title="Unblock seat"
>
<Unlock className="h-3 w-3 text-gray-700" />
</button>
)}
{canMaintenance && (
<button
onClick={() => handleSetMaintenance(seat)}
className="p-1 bg-white rounded hover:bg-gray-100 pointer-events-auto"
title="Set under maintenance"
>
<Wrench className="h-3 w-3 text-orange-600" />
</button>
)}
{canClearMaintenance && (
<button
onClick={() => handleClearMaintenance(seat)}
className="p-1 bg-white rounded hover:bg-gray-100 pointer-events-auto"
title="Clear maintenance"
>
<Unlock className="h-3 w-3 text-orange-600" />
</button>
)}
</div>
)}
</div>
);
}