mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 06:28:12 +00:00
631 lines
23 KiB
TypeScript
631 lines
23 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { seatsApi, schedulesApi } from '@/lib/api';
|
|
import Modal from '@/components/ui/Modal';
|
|
import ActionButton from '@/components/ui/ActionButton'
|
|
import Badge from '@/components/ui/Badge';
|
|
import { Armchair, Lock, Unlock, Bed, X, RotateCcw } from 'lucide-react';
|
|
|
|
export default function SeatsPage() {
|
|
const [selectedSchedule, setSelectedSchedule] = useState('');
|
|
const [showBlockModal, setShowBlockModal] = useState(false);
|
|
const [showRemoveModal, setShowRemoveModal] = useState(false);
|
|
const [selectedSeat, setSelectedSeat] = useState<any>(null);
|
|
const [blockReason, setBlockReason] = 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,
|
|
});
|
|
|
|
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 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 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 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) {
|
|
// Render bed coach with flipping effect and bed position labels
|
|
const arrangement = parseSeatArrangement(coach.seatArrangement);
|
|
const seatsPerRow = arrangement[0] + (arrangement[1] || 0);
|
|
const allSeatsForLayout = [...validSeats, ...removedSeats];
|
|
const rows = [];
|
|
|
|
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 rowNumber = rowSeats[0]?.row || (idx + 1);
|
|
const shouldFlipIcon = rowNumber % 2 === 0;
|
|
const showSpacing = idx % 2 === 1;
|
|
|
|
return (
|
|
<div key={`bed-row-${idx}`}>
|
|
{shouldFlipIcon && (
|
|
<div className="flex gap-0.5 justify-start text-xs text-muted-foreground mb-1">
|
|
{rowSeats.map((seat: any) => (
|
|
<div key={`num-before-${seat.id}`} className="w-12 h-4 flex items-center justify-center">
|
|
{seat.seatNumber && !seat.seatNumber.startsWith('-') ? `${seat.seatNumber}${getBedLabel(seat.bedPosition)}` : ''}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
<div className="flex gap-0.5 justify-start">
|
|
{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>
|
|
{!shouldFlipIcon && (
|
|
<div className="flex gap-0.5 justify-start text-xs text-muted-foreground mb-1">
|
|
{rowSeats.map((seat: any) => (
|
|
<div key={`num-after-${seat.id}`} className="w-12 h-4 flex items-center justify-center">
|
|
{seat.seatNumber && !seat.seatNumber.startsWith('-') ? `${seat.seatNumber}${getBedLabel(seat.bedPosition)}` : ''}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
{showSpacing && <div className="h-2" />}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Regular armchair layout
|
|
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-start 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">
|
|
{seat.seatNumber && !seat.seatNumber.startsWith('-') ? seat.seatNumber : ''}
|
|
</div>
|
|
))}
|
|
</div>
|
|
{rightSeats.length > 0 && <div className="w-3" />}
|
|
{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">
|
|
{seat.seatNumber && !seat.seatNumber.startsWith('-') ? seat.seatNumber : ''}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
<div className="flex gap-0.5 justify-start">
|
|
<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-3" />}
|
|
{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-start 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">
|
|
{seat.seatNumber && !seat.seatNumber.startsWith('-') ? seat.seatNumber : ''}
|
|
</div>
|
|
))}
|
|
</div>
|
|
{rightSeats.length > 0 && <div className="w-3" />}
|
|
{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">
|
|
{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;
|
|
});
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-foreground">Seat Management</h1>
|
|
<p className="text-muted-foreground mt-1">View and manage seat availability by schedule</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card">
|
|
<div className="mb-6">
|
|
<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>
|
|
|
|
{!selectedSchedule ? (
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
<Armchair className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
|
<p>Select a schedule to view seat map</p>
|
|
</div>
|
|
) : isLoading ? (
|
|
<div className="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="text-center py-12 text-muted-foreground">
|
|
<p>No coaches with seats found for this schedule</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center gap-6 p-4 bg-muted/50 rounded-lg">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-4 h-4 rounded bg-green-500"></div>
|
|
<span className="text-sm">Available</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-4 h-4 rounded bg-red-500"></div>
|
|
<span className="text-sm">Booked</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-4 h-4 rounded bg-yellow-500"></div>
|
|
<span className="text-sm">Held</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-4 h-4 rounded bg-gray-500"></div>
|
|
<span className="text-sm">Blocked</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-4 h-4 rounded border-2 border-dashed border-gray-400"></div>
|
|
<span className="text-sm">Removed</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{coachesWithSeats.map((coach: any) => {
|
|
const isBedCoach = (coach.seatClass && coach.seatClass.toLowerCase().includes('bed')) ||
|
|
(coach.mode && coach.mode.toLowerCase().includes('bed'));
|
|
const seats = (coach.seats || []).filter((s: any) => s.seatNumber);
|
|
|
|
return (
|
|
<div key={coach.id} className="border rounded-lg p-3 bg-white dark:bg-card">
|
|
<div className="mb-3">
|
|
<h3 className="font-semibold text-sm">Coach {coach.coachNumber}</h3>
|
|
</div>
|
|
|
|
<div className="bg-gray-50 dark:bg-gray-900/30 py-2 rounded-lg">
|
|
{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>
|
|
</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('-');
|
|
|
|
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 h-3 leading-3 text-foreground">
|
|
{seat.seatNumber}
|
|
</span>
|
|
)}
|
|
|
|
{isBedCoach ? (
|
|
<div
|
|
className={`w-11 h-11 rounded flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity ${color}`}
|
|
title={`${seat.seatNumber} - ${seat.bedPosition} - ${status}`}
|
|
>
|
|
<Bed className="w-7 h-7 text-white" style={shouldFlipIcon ? { transform: 'scaleY(-1)' } : undefined} />
|
|
</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}`}
|
|
>
|
|
<Armchair className="w-7 h-7 text-white" style={shouldFlipIcon ? { transform: 'scaleY(-1)' } : undefined} />
|
|
</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>
|
|
);
|
|
}
|