Merge pull request #987 from Tria-plc/alpha

Alpha
This commit is contained in:
Abubeker Yasin
2026-07-28 09:36:03 +03:00
committed by GitHub
3 changed files with 31 additions and 11 deletions

View File

@@ -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(

View File

@@ -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: () => {

View File

@@ -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 && (