Files
edr-platform/apps/edr-passenger-web/backoffice/src/app/seats/page.tsx

1434 lines
58 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { seatsApi, schedulesApi, fleetApi, routeCoachTemplatesApi, bookingsApi } 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, Ticket as TicketIcon } 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 [showIssueBookingModal, setShowIssueBookingModal] = useState(false);
const [issueBookingCoach, setIssueBookingCoach] = useState<any>(null);
const [issueBookingForm, setIssueBookingForm] = useState({
bookingKind: 'STAFF' as 'STAFF' | 'PASSENGER',
// No seatClassId — the seat's class is already fixed by the reservation; the backend
// resolves it from the seat's own coach type + nationality tier.
passengerName: '',
dateOfBirth: '',
idDocumentType: 'PASSPORT' as 'NATIONAL_ID' | 'PASSPORT',
idDocumentNumber: '',
passportNumber: '',
nationality: '' as '' | 'Ethiopian' | 'Djiboutian' | 'Other',
phone: '',
email: '',
});
const [issueBookingResult, setIssueBookingResult] = useState<{ payUrl?: string; bookingRef?: string } | null>(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,
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, ...(activeTab === 'schedule' && selectedSchedule ? { scheduleId: selectedSchedule } : {}) }),
onSuccess: () => {
invalidateSeatData();
setShowBlockModal(false);
setSelectedSeat(null);
setBlockReason('');
},
});
const unblockMutation = useMutation({
mutationFn: (seatId: string) =>
seatsApi.unblock(seatId, activeTab === 'schedule' ? selectedSchedule : undefined),
onSuccess: () => {
invalidateSeatData();
},
});
const issueBookingMutation = useMutation({
mutationFn: ({ seatId, data }: { seatId: string; data: any }) =>
bookingsApi.issueFromReservation(seatId, data),
onSuccess: (result: any) => {
invalidateSeatData();
// Always show the reference — the PASSENGER path also needs the PNR alongside the
// pay link (staff need to know which booking a seat belongs to, whether it's
// awaiting payment or already ticketed), so no longer auto-closing for STAFF.
setIssueBookingResult({ payUrl: result?.payUrl, bookingRef: result?.booking?.bookingRef });
},
});
// Cancels a seat's still-unpaid reservation (payment link sent) and releases the seat —
// distinct from unblockMutation, which only handles a plain SeatBlock (no booking involved).
const cancelReservationMutation = useMutation({
mutationFn: (seatId: string) => bookingsApi.cancelReservation(seatId, selectedSchedule),
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);
const scheduleId = activeTab === 'schedule' ? selectedSchedule : undefined;
return Promise.all(seatIds.map((seatId: string) => seatsApi.block(seatId, { reason, ...(scheduleId ? { scheduleId } : {}) })));
},
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);
const scheduleId = activeTab === 'schedule' ? selectedSchedule : undefined;
return Promise.all(seatIds.map((seatId: string) => seatsApi.unblock(seatId, scheduleId)));
},
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('Release this reservation and make the seat available to the public?')) {
await unblockMutation.mutateAsync(seat.id);
}
};
// Distinct from handleUnblock — this seat has no SeatBlock (issuing the reservation already
// released it), it's HELD by the SeatHold behind an unpaid booking. Cancelling that booking
// invalidates its payment link immediately, so warn staff explicitly about that.
const handleCancelReservation = async (seat: any) => {
if (!selectedSchedule) return;
if (confirm(`Cancel the reservation for seat ${seat.seatNumber} (PNR ${seat.bookingRef})? The payment link already sent to the traveler will stop working.`)) {
await cancelReservationMutation.mutateAsync(seat.id);
}
};
const handleIssueBooking = (seat: any, coach: any) => {
if (activeTab !== 'schedule' || !selectedSchedule) {
alert('Select a specific schedule (Schedule tab) to issue a booking for a reserved seat.');
return;
}
setSelectedSeat(seat);
setIssueBookingCoach(coach);
setIssueBookingResult(null);
setIssueBookingForm({
bookingKind: 'STAFF',
passengerName: '',
dateOfBirth: '',
idDocumentType: 'PASSPORT',
idDocumentNumber: '',
passportNumber: '',
nationality: '',
phone: '',
email: '',
});
setShowIssueBookingModal(true);
};
const submitIssueBooking = async () => {
const schedule = schedules.find((s: any) => s.id === selectedSchedule);
if (!schedule?.originStation?.id || !schedule?.destinationStation?.id) {
alert('Could not resolve this schedule\'s origin/destination stations.');
return;
}
if (!issueBookingForm.passengerName.trim() || !issueBookingForm.dateOfBirth) {
alert('Traveler name and date of birth are required.');
return;
}
if (!issueBookingForm.nationality) {
alert('Select a nationality.');
return;
}
if (issueBookingForm.idDocumentType === 'PASSPORT' && !issueBookingForm.passportNumber.trim()) {
alert('Passport number is required.');
return;
}
if (issueBookingForm.bookingKind === 'PASSENGER' && !issueBookingForm.phone.trim()) {
alert('Phone number is required for a passenger booking (used to send the payment link).');
return;
}
await issueBookingMutation.mutateAsync({
seatId: selectedSeat.id,
data: {
scheduleId: selectedSchedule,
originStationId: schedule.originStation.id,
destinationStationId: schedule.destinationStation.id,
...issueBookingForm,
},
});
};
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 the reservation');
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}
handleCancelReservation={handleCancelReservation}
handleUndoRemove={handleUndoRemove}
handleSetMaintenance={handleSetMaintenance}
handleClearMaintenance={handleClearMaintenance}
handleIssueBooking={handleIssueBooking}
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}
handleCancelReservation={handleCancelReservation}
handleUndoRemove={handleUndoRemove}
handleSetMaintenance={handleSetMaintenance}
handleClearMaintenance={handleClearMaintenance}
handleIssueBooking={handleIssueBooking}
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}
handleCancelReservation={handleCancelReservation}
handleUndoRemove={handleUndoRemove}
handleSetMaintenance={handleSetMaintenance}
handleClearMaintenance={handleClearMaintenance}
handleIssueBooking={handleIssueBooking}
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 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="Reserve Seat"
size="md"
>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Reserve seat <strong>{selectedSeat?.seatNumber}</strong> in Coach <strong>{selectedSeat?.coach?.coachNumber}</strong>
</p>
<div>
<label className="label">Reason for Reservation *</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()}
>
Reserve Seat
</ActionButton>
</div>
</div>
</Modal>
<Modal
isOpen={showIssueBookingModal}
onClose={() => {
setShowIssueBookingModal(false);
setSelectedSeat(null);
setIssueBookingCoach(null);
setIssueBookingResult(null);
}}
title="Issue Booking"
size="md"
>
{issueBookingResult ? (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
{issueBookingResult.payUrl
? 'Booking created. A payment link has been sent via SMS to the traveler.'
: 'Booking confirmed and ticketed.'}
</p>
{issueBookingResult.bookingRef && (
<div>
<label className="label">Booking Reference (PNR)</label>
<div className="input font-mono font-semibold text-sm">{issueBookingResult.bookingRef}</div>
</div>
)}
{issueBookingResult.payUrl && (
<div>
<label className="label">Payment Link</label>
<div className="input break-all text-xs">{issueBookingResult.payUrl}</div>
</div>
)}
<div className="flex justify-end">
<ActionButton
onClick={() => {
setShowIssueBookingModal(false);
setSelectedSeat(null);
setIssueBookingCoach(null);
setIssueBookingResult(null);
}}
>
Done
</ActionButton>
</div>
</div>
) : (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Issue a booking for seat <strong>{selectedSeat?.seatNumber}</strong> in Coach{' '}
<strong>{issueBookingCoach?.coachNumber}</strong>
</p>
<div>
<label className="label">Booking Type *</label>
<div className="flex gap-2">
<button
type="button"
className={`flex-1 px-3 py-2 rounded border ${issueBookingForm.bookingKind === 'STAFF' ? 'bg-blue-600 text-white border-blue-600' : 'border-gray-300 dark:border-gray-600'}`}
onClick={() => setIssueBookingForm((f) => ({ ...f, bookingKind: 'STAFF' }))}
>
Staff (no fee)
</button>
<button
type="button"
className={`flex-1 px-3 py-2 rounded border ${issueBookingForm.bookingKind === 'PASSENGER' ? 'bg-blue-600 text-white border-blue-600' : 'border-gray-300 dark:border-gray-600'}`}
onClick={() => setIssueBookingForm((f) => ({ ...f, bookingKind: 'PASSENGER' }))}
>
Passenger (pay via link)
</button>
</div>
</div>
<div>
<label className="label">Traveler Name *</label>
<input
className="input"
value={issueBookingForm.passengerName}
onChange={(e) => setIssueBookingForm((f) => ({ ...f, passengerName: e.target.value }))}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="label">Date of Birth *</label>
<input
type="date"
className="input"
value={issueBookingForm.dateOfBirth}
onChange={(e) => setIssueBookingForm((f) => ({ ...f, dateOfBirth: e.target.value }))}
/>
</div>
<div>
<label className="label">Nationality *</label>
<select
className="input"
value={issueBookingForm.nationality}
onChange={(e) => {
const nationality = e.target.value as 'Ethiopian' | 'Djiboutian' | 'Other';
setIssueBookingForm((f) => ({
...f,
nationality,
// National ID is Ethiopian-only — switch back to Passport for anyone else.
idDocumentType: nationality === 'Ethiopian' ? f.idDocumentType : 'PASSPORT',
}));
}}
>
<option value="">Select nationality...</option>
<option value="Ethiopian">Ethiopian</option>
<option value="Djiboutian">Djiboutian</option>
<option value="Other">Other</option>
</select>
</div>
</div>
<div>
<label className="label">ID Document Type *</label>
<select
className="input"
value={issueBookingForm.idDocumentType}
onChange={(e) => setIssueBookingForm((f) => ({ ...f, idDocumentType: e.target.value as 'NATIONAL_ID' | 'PASSPORT' }))}
>
<option value="PASSPORT">Passport</option>
{issueBookingForm.nationality === 'Ethiopian' && (
<option value="NATIONAL_ID">National ID</option>
)}
</select>
</div>
{issueBookingForm.idDocumentType === 'NATIONAL_ID' ? (
<div>
<label className="label">National ID Number</label>
<input
className="input"
value={issueBookingForm.idDocumentNumber}
onChange={(e) => setIssueBookingForm((f) => ({ ...f, idDocumentNumber: e.target.value }))}
/>
</div>
) : (
<div>
<label className="label">Passport Number *</label>
<input
className="input"
value={issueBookingForm.passportNumber}
onChange={(e) => setIssueBookingForm((f) => ({ ...f, passportNumber: e.target.value }))}
/>
</div>
)}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="label">Phone{issueBookingForm.bookingKind === 'PASSENGER' ? ' * (payment link sent here)' : ''}</label>
<input
className="input"
value={issueBookingForm.phone}
onChange={(e) => setIssueBookingForm((f) => ({ ...f, phone: e.target.value }))}
placeholder="+251911234567"
/>
</div>
<div>
<label className="label">Email</label>
<input
className="input"
value={issueBookingForm.email}
onChange={(e) => setIssueBookingForm((f) => ({ ...f, email: e.target.value }))}
/>
</div>
</div>
<div className="flex justify-end gap-2">
<ActionButton
variant="secondary"
onClick={() => {
setShowIssueBookingModal(false);
setSelectedSeat(null);
setIssueBookingCoach(null);
}}
>
Cancel
</ActionButton>
<ActionButton onClick={submitIssueBooking} loading={issueBookingMutation.isPending}>
{issueBookingForm.bookingKind === 'STAFF' ? 'Issue Ticket' : 'Send Payment Link'}
</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;
handleCancelReservation: (seat: any) => void;
handleUndoRemove: (seat: any) => void;
handleSetMaintenance: (seat: any) => void;
handleClearMaintenance: (seat: any) => void;
handleIssueBooking: (seat: any, coach: any) => void;
}
function SeatIcon({
seat,
coach,
isBedCoach,
shouldFlipIcon = false,
hideNumber = false,
getSeatStatus,
getSeatColor,
handleBlock,
handleRemoveSeat,
handleUnblock,
handleCancelReservation,
handleUndoRemove,
handleSetMaintenance,
handleClearMaintenance,
handleIssueBooking,
}: 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';
// A HELD seat with a bookingRef + PENDING_PAYMENT is a backoffice reservation awaiting
// payment (see resolveActiveReservations) — issuing it already released the SeatBlock, so
// it's not reachable via canUnblock anymore; this is the seat's own release path.
const canCancelReservation = status === 'HELD' && !!seat.bookingRef && seat.reservationStatus === 'PENDING_PAYMENT';
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}${seat.bookingRef ? ` - PNR ${seat.bookingRef} (${seat.reservationStatus})` : ''}`}
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}${seat.bookingRef ? ` - PNR ${seat.bookingRef} (${seat.reservationStatus})` : ''}`}
style={seat.row % 2 === 0 ? { transform: 'scaleY(-1)' } : undefined}
>
<Armchair className="w-7 h-7 text-white" />
</div>
)}
{seat.bookingRef && (
<span
className="text-[9px] leading-3 font-semibold text-foreground/80 mt-0.5 max-w-[3.5rem] truncate"
title={`PNR ${seat.bookingRef}${seat.reservationStatus}${seat.reservationPassengerName ? `${seat.reservationPassengerName}` : ''}`}
>
{seat.bookingRef}
</span>
)}
{(canBlock || canUnblock || canCancelReservation || 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="Reserve 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>
</>
)}
{canCancelReservation && (
<button
onClick={() => handleCancelReservation(seat)}
className="p-1 bg-white rounded hover:bg-gray-100 pointer-events-auto"
title={`Cancel reservation (PNR ${seat.bookingRef}) — invalidates the payment link`}
>
<Unlock 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="Release reservation"
>
<Unlock className="h-3 w-3 text-gray-700" />
</button>
<button
onClick={() => handleIssueBooking(seat, coach)}
className="p-1 bg-white rounded hover:bg-gray-100 pointer-events-auto"
title="Issue booking"
>
<TicketIcon 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>
);
}