mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 02:28:18 +00:00
@@ -352,12 +352,12 @@ export class BookingsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("reservations/:seatId/issue")
|
@Post("reservations/:seatId/issue")
|
||||||
@PassengerStaff([PASSENGER_PERMS.seats.manage, PASSENGER_PERMS.bookings.manage, PASSENGER_PERMS.admin])
|
@PassengerAdmin()
|
||||||
@ApiBearerAuth("IAM-auth")
|
@ApiBearerAuth("IAM-auth")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Issue a booking from a reserved (blocked) seat",
|
summary: "Issue a booking from a reserved (blocked) seat — admin only",
|
||||||
description:
|
description:
|
||||||
"Converts an admin-reserved seat into a real booking for one traveler. bookingKind STAFF waives the fee and issues the ticket immediately; bookingKind PASSENGER creates the booking as PENDING_PAYMENT and texts a payment link to the traveler's phone.",
|
"Converts an admin-reserved seat into a real booking for one traveler. bookingKind STAFF waives the fee and issues the ticket immediately; bookingKind PASSENGER creates the booking as PENDING_PAYMENT and texts a payment link to the traveler's phone. Restricted to admins (edr_passenger_app:admin) because STAFF issuance waives the fare.",
|
||||||
})
|
})
|
||||||
@ApiBody({ type: IssueReservationBookingDto })
|
@ApiBody({ type: IssueReservationBookingDto })
|
||||||
issueBookingFromReservation(
|
issueBookingFromReservation(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Plus, Loader2, Zap, Trash2, Edit, Search, X, GripVertical, Clock } from 'lucide-react';
|
import { Plus, Loader2, Zap, Trash2, Edit, Search, X, GripVertical, Clock, RefreshCw } from 'lucide-react';
|
||||||
import DataTable from '@/components/ui/DataTable';
|
import DataTable from '@/components/ui/DataTable';
|
||||||
import ActionButton from '@/components/ui/ActionButton';
|
import ActionButton from '@/components/ui/ActionButton';
|
||||||
import Modal from '@/components/ui/Modal';
|
import Modal from '@/components/ui/Modal';
|
||||||
@@ -211,6 +211,13 @@ export default function SchedulesPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const recalculateStopsMutation = useMutation({
|
||||||
|
mutationFn: (id: string) => apiClient.post(`/schedules/${id}/recalculate-stops`, {}),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['schedules'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const deleteScheduleMutation = useMutation({
|
const deleteScheduleMutation = useMutation({
|
||||||
mutationFn: ({ id, cascade }: { id: string; cascade?: boolean }) => apiClient.delete(`/schedules/${id}${cascade ? '?cascade=true' : ''}`),
|
mutationFn: ({ id, cascade }: { id: string; cascade?: boolean }) => apiClient.delete(`/schedules/${id}${cascade ? '?cascade=true' : ''}`),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { useState } from 'react';
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { seatsApi, schedulesApi, fleetApi, routeCoachTemplatesApi, bookingsApi } from '@/lib/api';
|
import { seatsApi, schedulesApi, fleetApi, routeCoachTemplatesApi, bookingsApi } from '@/lib/api';
|
||||||
import { routesApi } from '@/lib/api/routes';
|
import { routesApi } from '@/lib/api/routes';
|
||||||
|
import { usePermission } from '@/lib/use-permission';
|
||||||
|
import { PERMS } from '@/lib/permissions';
|
||||||
import Modal from '@/components/ui/Modal';
|
import Modal from '@/components/ui/Modal';
|
||||||
import ActionButton from '@/components/ui/ActionButton'
|
import ActionButton from '@/components/ui/ActionButton'
|
||||||
import { Armchair, Lock, Unlock, Bed, X, RotateCcw, ChevronDown, Train, Wrench, Ticket as TicketIcon } from 'lucide-react';
|
import { Armchair, Lock, Unlock, Bed, X, RotateCcw, ChevronDown, Train, Wrench, Ticket as TicketIcon } from 'lucide-react';
|
||||||
@@ -42,6 +44,10 @@ export default function SeatsPage() {
|
|||||||
const [issueBookingResult, setIssueBookingResult] = useState<{ payUrl?: string; bookingRef?: string } | null>(null);
|
const [issueBookingResult, setIssueBookingResult] = useState<{ payUrl?: string; bookingRef?: string } | null>(null);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
// Issuing a booking off a reserved seat is admin-only (POST /bookings/reservations/:seatId/issue
|
||||||
|
// is guarded by @PassengerAdmin) — hide the ticket action for everyone else.
|
||||||
|
const canIssueBooking = usePermission(PERMS.admin);
|
||||||
|
|
||||||
const { data: schedulesData } = useQuery({
|
const { data: schedulesData } = useQuery({
|
||||||
queryKey: ['schedules'],
|
queryKey: ['schedules'],
|
||||||
queryFn: () => schedulesApi.getAll(),
|
queryFn: () => schedulesApi.getAll(),
|
||||||
@@ -454,6 +460,7 @@ export default function SeatsPage() {
|
|||||||
handleSetMaintenance={handleSetMaintenance}
|
handleSetMaintenance={handleSetMaintenance}
|
||||||
handleClearMaintenance={handleClearMaintenance}
|
handleClearMaintenance={handleClearMaintenance}
|
||||||
handleIssueBooking={handleIssueBooking}
|
handleIssueBooking={handleIssueBooking}
|
||||||
|
canIssueBooking={canIssueBooking}
|
||||||
hideNumber={true}
|
hideNumber={true}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -551,6 +558,7 @@ export default function SeatsPage() {
|
|||||||
handleSetMaintenance={handleSetMaintenance}
|
handleSetMaintenance={handleSetMaintenance}
|
||||||
handleClearMaintenance={handleClearMaintenance}
|
handleClearMaintenance={handleClearMaintenance}
|
||||||
handleIssueBooking={handleIssueBooking}
|
handleIssueBooking={handleIssueBooking}
|
||||||
|
canIssueBooking={canIssueBooking}
|
||||||
hideNumber={true}
|
hideNumber={true}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -575,6 +583,7 @@ export default function SeatsPage() {
|
|||||||
handleSetMaintenance={handleSetMaintenance}
|
handleSetMaintenance={handleSetMaintenance}
|
||||||
handleClearMaintenance={handleClearMaintenance}
|
handleClearMaintenance={handleClearMaintenance}
|
||||||
handleIssueBooking={handleIssueBooking}
|
handleIssueBooking={handleIssueBooking}
|
||||||
|
canIssueBooking={canIssueBooking}
|
||||||
hideNumber={true}
|
hideNumber={true}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -1268,6 +1277,7 @@ interface SeatIconProps {
|
|||||||
handleSetMaintenance: (seat: any) => void;
|
handleSetMaintenance: (seat: any) => void;
|
||||||
handleClearMaintenance: (seat: any) => void;
|
handleClearMaintenance: (seat: any) => void;
|
||||||
handleIssueBooking: (seat: any, coach: any) => void;
|
handleIssueBooking: (seat: any, coach: any) => void;
|
||||||
|
canIssueBooking?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SeatIcon({
|
function SeatIcon({
|
||||||
@@ -1286,6 +1296,7 @@ function SeatIcon({
|
|||||||
handleSetMaintenance,
|
handleSetMaintenance,
|
||||||
handleClearMaintenance,
|
handleClearMaintenance,
|
||||||
handleIssueBooking,
|
handleIssueBooking,
|
||||||
|
canIssueBooking = false,
|
||||||
}: SeatIconProps) {
|
}: SeatIconProps) {
|
||||||
const isRemoved = seat.seatNumber && seat.seatNumber.startsWith('-');
|
const isRemoved = seat.seatNumber && seat.seatNumber.startsWith('-');
|
||||||
const seatClassStr = typeof coach?.seatClass === 'string' ? coach.seatClass : (coach?.seatClass?.name || coach?.coachClass || '');
|
const seatClassStr = typeof coach?.seatClass === 'string' ? coach.seatClass : (coach?.seatClass?.name || coach?.coachClass || '');
|
||||||
@@ -1399,13 +1410,15 @@ function SeatIcon({
|
|||||||
>
|
>
|
||||||
<Unlock className="h-3 w-3 text-gray-700" />
|
<Unlock className="h-3 w-3 text-gray-700" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
{canIssueBooking && (
|
||||||
onClick={() => handleIssueBooking(seat, coach)}
|
<button
|
||||||
className="p-1 bg-white rounded hover:bg-gray-100 pointer-events-auto"
|
onClick={() => handleIssueBooking(seat, coach)}
|
||||||
title="Issue booking"
|
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>
|
<TicketIcon className="h-3 w-3 text-gray-700" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{canMaintenance && (
|
{canMaintenance && (
|
||||||
|
|||||||
Reference in New Issue
Block a user