Merge pull request #801 from Tria-plc/quick-fix

Quick fix
This commit is contained in:
Stephanos A.
2026-07-19 08:33:28 +03:00
committed by GitHub
7 changed files with 75 additions and 18 deletions

View File

@@ -13,8 +13,8 @@ export class DashboardService {
async getBackofficeStats() {
const [totalBookings, totalPackageBookings, totalTickets, totalPassengers, blockedSeatsCount, revenueRows, packageRevenueRows] =
await Promise.all([
this.prisma.booking.count(),
this.prisma.booking.count({ where: { packageId: { not: null } } }),
this.prisma.booking.count({ where: { status: { in: ['CONFIRMED', 'BOARDED'] } } }),
this.prisma.booking.count({ where: { status: { in: ['CONFIRMED', 'BOARDED'] }, packageId: { not: null } } }),
this.prisma.ticket.count(),
this.prisma.passenger.count(),
this.prisma.seat.count({ where: { status: 'BLOCKED' } }),

View File

@@ -95,13 +95,18 @@ export class ReportsService {
where: { departureAt: { gte: dateFrom, lte: dateTo } },
include: {
coachAssignments: { include: { coach: { include: { seats: true } } } },
bookings: { include: { seats: true } },
bookings: {
where: { status: { in: ['CONFIRMED', 'BOARDED'] } },
include: { seats: true },
},
},
});
const tripData = schedules.map(schedule => {
const totalSeats = schedule.coachAssignments.reduce((sum, a) => sum + a.coach.seats.length, 0);
const bookedSeats = schedule.bookings.reduce((sum, b) => sum + b.seats.length, 0);
const bookedSeats = schedule.bookings.reduce(
(sum, b) => sum + b.seats.filter((s: any) => s.leg === 1).length, 0,
);
const occupancyRate = totalSeats > 0 ? (bookedSeats / totalSeats) * 100 : 0;
return { scheduleId: schedule.id, departureAt: schedule.departureAt, totalSeats, bookedSeats, occupancyRate: +occupancyRate.toFixed(2) };
});
@@ -212,6 +217,7 @@ export class ReportsService {
where: { status: { in: ['CONFIRMED', 'BOARDED'] } },
include: {
seats: {
where: { leg: 1 },
include: {
seat: { include: { coach: { include: { coachType: true } } } },
},

View File

@@ -31,6 +31,16 @@ import { PASSENGER_PERMS } from "../../seed/passenger-permissions.registry";
export class SeatsController {
constructor(private service: SeatsService) {}
// ── Blocked Seats ─────────────────────────────────────────────────────────
@Get('blocks')
@PassengerStaff([PASSENGER_PERMS.seats.manage, PASSENGER_PERMS.admin])
@ApiBearerAuth('IAM-auth')
@ApiOperation({ summary: 'List all blocked seats with reason and coach info' })
@ApiResponse({ status: 200, description: 'Blocked seat records' })
getBlockedSeats() {
return this.service.getBlockedSeats();
}
// ── Coach Availability ────────────────────────────────────────────────────
@Get('coaches/:scheduleId')
@SetMetadata('isPublic', true)

View File

@@ -615,6 +615,29 @@ export class SeatsService {
await this.prisma.journey.deleteMany({ where: { bookingId } as any });
}
async getBlockedSeats() {
const blocks = await this.prisma.seatBlock.findMany({
where: {
NOT: { reason: { startsWith: 'MAINTENANCE:' } },
},
include: {
seat: { include: { coach: { select: { number: true } } } },
},
orderBy: { blockedAt: 'desc' },
});
return blocks.map(b => ({
id: b.id,
seatId: b.seatId,
seatNumber: b.seat.seatNumber,
coachNumber: b.seat.coach.number,
scheduleId: b.scheduleId,
reason: b.reason,
blockedBy: b.blockedBy,
blockedAt: b.blockedAt,
unblockAt: b.unblockAt,
}));
}
async getCoachesWithAvailability(scheduleId: string, originStationId?: string, destinationStationId?: string) {
const schedule = await this.prisma.trainSchedule.findUnique({
where: { id: scheduleId },

View File

@@ -172,7 +172,7 @@ function DashboardPageContent() {
)}
{/* Stat cards */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<StatCard
icon={<BookOpen className="h-4 w-4 text-blue-600 dark:text-blue-400" />}
iconBg="bg-blue-100 dark:bg-blue-900/30"
@@ -185,18 +185,7 @@ function DashboardPageContent() {
{ label: 'Package', value: stats?.totalPackageBookings ?? 0, href: '/package-bookings' },
]}
/>
<StatCard
icon={<Ticket className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />}
iconBg="bg-emerald-100 dark:bg-emerald-900/30"
label="Tickets"
total={stats?.totalTickets ?? 0}
loading={statsLoading}
href="/tickets"
rows={[
{ label: 'Regular', value: stats?.totalNormalTickets ?? 0, href: '/tickets' },
{ label: 'Package', value: stats?.totalPackageTickets ?? 0, href: '/tickets' },
]}
/>
{/* Tickets card hidden temporarily */}
{/* Revenue card */}
<div className="card flex flex-col gap-3">

View File

@@ -23,6 +23,17 @@ function StatCard({ label, value, sub, icon: Icon, color }: { label: string; val
);
}
interface PassengerRow {
bookingRef: string;
passengerName: string;
coachSeat: string;
origin: string;
destination: string;
departureAt: string | null;
}
type Tab = 'occupancy' | 'list';
export default function PassengersReportPage() {
const [scheduleId, setScheduleId] = useState('');
@@ -56,6 +67,22 @@ export default function PassengersReportPage() {
URL.revokeObjectURL(url);
};
const doExportOccupancy = () => {
if (!data) return;
const rows = data.byCoach.map(c => [c.coachNumber, c.coachType, String(c.totalSeats), String(c.booked), `${c.occupancyRate}%`]);
downloadCsv([['Coach', 'Type', 'Total Seats', 'Booked', 'Occupancy'].join(','), ...rows.map(r => r.join(','))].join('\n'), `occupancy-${scheduleId}.csv`);
};
const doExportList = () => {
if (!passengerList.length) return;
const headers = ['#', 'Name', 'Coach·Seat', 'Origin', 'Destination', 'Date', 'Booking Ref'];
const rows = passengerList.map((p, i) => [
String(i + 1), p.passengerName, p.coachSeat, p.origin, p.destination,
p.departureAt ? formatDateTime(p.departureAt) : '—', p.bookingRef,
].map(v => `"${String(v).replace(/"/g, '""')}"`));
downloadCsv([headers.join(','), ...rows.map(r => r.join(','))].join('\n'), `passengers-${scheduleId}.csv`);
};
return (
<div className="space-y-6">
<div>
@@ -259,6 +286,7 @@ export default function PassengersReportPage() {
<p className="text-sm text-muted-foreground">No boarding station data</p>
)}
</div>
)}
{/* By Destination */}
<div className="card">
@@ -296,7 +324,7 @@ export default function PassengersReportPage() {
<p className="text-sm text-muted-foreground">No alighting station data</p>
)}
</div>
</div>
)}
</>
)}

View File

@@ -151,6 +151,7 @@ export const seatsApi = {
return apiClient.get<any>(`/seats/seatmap/${scheduleId}${params}`);
},
getBySchedule: (scheduleId: string) => apiClient.get<any>(`/seats/schedule/${scheduleId}`),
getBlocked: () => apiClient.get<any[]>('/seats/blocks'),
hold: (data: any) => apiClient.post<any>('/seats/hold', data),
release: (holdId: string) => apiClient.delete(`/seats/hold/${holdId}`),
block: (seatId: string, data: any) => apiClient.post<any>(`/seats/${seatId}/block`, data),