mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Report figures correction updates
This commit is contained in:
@@ -13,8 +13,8 @@ export class DashboardService {
|
||||
async getBackofficeStats() {
|
||||
const [totalBookings, totalPackageBookings, totalTickets, totalPassengers, 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.$queryRaw<{ currency: string; total: bigint }[]>`
|
||||
|
||||
@@ -95,13 +95,18 @@ export class ReportsService {
|
||||
where: { departureAt: { gte: dateFrom, lte: dateTo } },
|
||||
include: {
|
||||
coachAssignments: { include: { coach: { include: { seats: true } } } },
|
||||
bookings: { include: { seats: { where: { scheduleId: schedule.id } } } },
|
||||
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.scheduleId === schedule.id).length, 0,
|
||||
);
|
||||
const occupancyRate = totalSeats > 0 ? (bookedSeats / totalSeats) * 100 : 0;
|
||||
return { scheduleId: schedule.id, departureAt: schedule.departureAt, totalSeats, bookedSeats, occupancyRate: +occupancyRate.toFixed(2) };
|
||||
});
|
||||
|
||||
@@ -29,6 +29,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)
|
||||
|
||||
@@ -598,6 +598,26 @@ export class SeatsService {
|
||||
await this.prisma.journey.deleteMany({ where: { bookingId } as any });
|
||||
}
|
||||
|
||||
async getBlockedSeats() {
|
||||
const blocks = await this.prisma.seatBlock.findMany({
|
||||
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 },
|
||||
|
||||
@@ -170,7 +170,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"
|
||||
@@ -183,18 +183,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">
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Download, Armchair, CheckCircle, Clock, AlertCircle } from 'lucide-react';
|
||||
import { bookingsApi } from '@/lib/api';
|
||||
import { Download, Armchair, CheckCircle, Clock, AlertCircle, Ban } from 'lucide-react';
|
||||
import { bookingsApi, seatsApi } from '@/lib/api';
|
||||
import Badge from '@/components/ui/Badge';
|
||||
import ActionButton from '@/components/ui/ActionButton';
|
||||
import { formatDateTime, formatCurrency } from '@/lib/utils';
|
||||
@@ -46,6 +46,11 @@ export default function SeatStatusReportPage() {
|
||||
const [statusFilter, setStatusFilter] = useState<'ALL' | 'PAID' | 'UNPAID'>('ALL');
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const { data: blockedSeats = [] } = useQuery({
|
||||
queryKey: ['blocked-seats'],
|
||||
queryFn: () => seatsApi.getBlocked().then((r: any) => Array.isArray(r) ? r : r?.data ?? []),
|
||||
});
|
||||
|
||||
const { data: bookingsData, isLoading } = useQuery({
|
||||
queryKey: ['seat-report-bookings'],
|
||||
queryFn: () => bookingsApi.getAll({ pageSize: 1000 }),
|
||||
@@ -150,7 +155,7 @@ export default function SeatStatusReportPage() {
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="card">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
@@ -189,6 +194,33 @@ export default function SeatStatusReportPage() {
|
||||
<AlertCircle className="h-8 w-8 text-red-500 opacity-30" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm font-medium">Blocked Seats</p>
|
||||
<p className="text-2xl font-bold mt-2 text-slate-600 dark:text-slate-400">
|
||||
{blockedSeats.length}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">Manually blocked</p>
|
||||
</div>
|
||||
<Ban className="h-8 w-8 text-slate-500 opacity-30" />
|
||||
</div>
|
||||
{blockedSeats.length > 0 && (
|
||||
<div className="mt-3 border-t border-border pt-3 flex flex-col gap-1 max-h-32 overflow-y-auto">
|
||||
{blockedSeats.map((b: any) => (
|
||||
<div key={b.id} className="flex items-center justify-between text-xs">
|
||||
<span className="font-medium text-foreground">
|
||||
Seat {b.seatNumber} · Coach {b.coachNumber}
|
||||
</span>
|
||||
<span className="text-muted-foreground truncate max-w-24" title={b.reason}>
|
||||
{b.reason}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user