Files
edr-platform/apps/edr-passenger-web/backoffice/src/app/seats/page.tsx
2026-06-16 14:41:27 +03:00

862 lines
33 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { seatsApi, schedulesApi, fleetApi } from '@/lib/api';
import Modal from '@/components/ui/Modal';
import ActionButton from '@/components/ui/ActionButton'
import { Armchair, Lock, Unlock, Bed, X, RotateCcw, ChevronDown, Train } from 'lucide-react';
export default function SeatsPage() {
const [selectedSchedule, setSelectedSchedule] = 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 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,
});
const { data: coachTypesData } = useQuery({
queryKey: ['coachTypes'],
queryFn: () => fleetApi.getCoaches(),
});
const blockMutation = useMutation({
mutationFn: ({ seatId, reason }: any) => seatsApi.block(seatId, { reason }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['seatmap'] });
setShowBlockModal(false);
setSelectedSeat(null);
setBlockReason('');
},
});
const unblockMutation = useMutation({
mutationFn: (seatId: string) => seatsApi.unblock(seatId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['seatmap'] });
},
});
const removeSeatMutation = useMutation({
mutationFn: (seatId: string) => seatsApi.removeSeat(seatId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['seatmap'] });
setShowRemoveModal(false);
setSelectedSeat(null);
},
});
const undoRemoveMutation = useMutation({
mutationFn: (seatId: string) => seatsApi.undoRemove(seatId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['seatmap'] });
},
});
const schedules = schedulesData?.items || schedulesData?.data || [];
const coaches = seatMapData?.coaches || [];
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: () => {
queryClient.invalidateQueries({ queryKey: ['seatmap'] });
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: () => {
queryClient.invalidateQueries({ queryKey: ['seatmap'] });
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 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) => {
return coach.seats?.some((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 === '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';
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 && hasBedPositionData) {
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}
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}
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}
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) => (a.sequence || 0) - (b.sequence || 0));
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>
{!selectedSchedule ? (
<div className="card">
<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>
<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 schedule to view seat map</p>
</div>
</div>
) : isLoading ? (
<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="card text-center py-12 text-muted-foreground">
<p>No coaches with seats found for this schedule</p>
</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>
<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>
</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 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 || 'Coach';
const isBedCoach = coachTypeName.toLowerCase().includes('bed');
const seats = (coach.seats || []).filter((s: any) => s.seatNumber);
const isExpanded = expandedCoaches.has(coach.id);
const seatOrBedLabel = isBedCoach ? 'beds' : 'seats';
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">Coach {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"
>
{isCoachBlocked(coach) ? 'Unblock' : 'Block'}
</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>
</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;
}
function SeatIcon({
seat,
coach,
isBedCoach,
shouldFlipIcon = false,
hideNumber = false,
getSeatStatus,
getSeatColor,
handleBlock,
handleRemoveSeat,
handleUnblock,
handleUndoRemove,
}: 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';
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) && (
<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>
)}
</div>
)}
</div>
);
}