Reports update

This commit is contained in:
Stephanos A
2026-07-18 12:52:28 +03:00
parent 8f0670ff9c
commit aac162ca3c
3 changed files with 74 additions and 23 deletions

View File

@@ -18,6 +18,12 @@ export class ReportsController {
return this.service.generateReport(dto);
}
@Get('schedules')
@ApiOperation({ summary: 'List schedules for the passengers report picker' })
listSchedulesForPicker() {
return this.service.listSchedulesForPicker();
}
@Get('passengers')
@ApiOperation({ summary: 'Passengers report for a specific schedule' })
getOccupancyReport(@Query('scheduleId') scheduleId: string) {

View File

@@ -286,6 +286,24 @@ export class ReportsService {
};
}
async listSchedulesForPicker() {
const schedules = await this.prisma.trainSchedule.findMany({
select: {
id: true,
departureAt: true,
train: { select: { number: true } },
originStation: { select: { name: true } },
destinationStation: { select: { name: true } },
},
orderBy: { departureAt: 'desc' },
take: 200,
});
return schedules.map(s => ({
id: s.id,
label: `${s.train.number} · ${s.originStation.name}${s.destinationStation.name} · ${new Date(s.departureAt).toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'short' })}`,
}));
}
async getReport(reportId: string) {
return this.prisma.operationalReport.findUnique({ where: { id: reportId } });
}

View File

@@ -7,15 +7,10 @@ import { apiClient } from '@/lib/api-client';
import ActionButton from '@/components/ui/ActionButton';
import { formatDateTime } from '@/lib/utils';
interface ScheduleOption { id: string; label: string; }
interface PassengersReport {
schedule: {
id: string;
trainName: string;
origin: string;
destination: string;
departureAt: string;
arrivalAt: string;
};
schedule: { id: string; trainName: string; origin: string; destination: string; departureAt: string; arrivalAt: string; };
summary: { totalSeats: number; totalPassengers: number; occupancyRate: number };
byCoach: { coachNumber: string; coachType: string; totalSeats: number; booked: number; occupancyRate: number }[];
byClass: { className: string; totalSeats: number; booked: number; occupancyRate: number }[];
@@ -25,8 +20,20 @@ interface PassengersReport {
export default function PassengersReportPage() {
const [scheduleId, setScheduleId] = useState('');
const [search, setSearch] = useState('');
const [submittedId, setSubmittedId] = useState('');
const { data: schedules = [], isLoading: loadingSchedules } = useQuery<ScheduleOption[]>({
queryKey: ['report-schedules'],
queryFn: () => apiClient.get('/reports/schedules'),
});
const filtered = search.trim()
? schedules.filter(s => s.label.toLowerCase().includes(search.toLowerCase()))
: schedules;
const selected = schedules.find(s => s.id === scheduleId);
const { data, isLoading, isError } = useQuery<PassengersReport>({
queryKey: ['passengers-report', submittedId],
queryFn: () => apiClient.get(`/reports/passengers?scheduleId=${submittedId}`),
@@ -54,20 +61,43 @@ export default function PassengersReportPage() {
<p className="text-muted-foreground mt-1">Occupancy and passenger breakdown for a schedule</p>
</div>
{/* Schedule ID input */}
{/* Schedule picker */}
<div className="card">
<div className="flex items-end gap-4 flex-wrap">
<div className="flex-1 min-w-64">
<label className="label">Schedule ID</label>
<input
type="text"
className="input"
placeholder="Enter schedule UUID…"
value={scheduleId}
onChange={(e) => setScheduleId(e.target.value)}
/>
<div className="flex-1 min-w-72">
<label className="label">Schedule</label>
<div className="relative">
<input
type="text"
className="input pr-8"
placeholder={loadingSchedules ? 'Loading schedules…' : 'Search by train, route or date…'}
value={search || selected?.label || ''}
onChange={(e) => { setSearch(e.target.value); setScheduleId(''); }}
onFocus={(e) => { setSearch(e.target.value); }}
/>
{(search || scheduleId) && (
<button
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground text-xs"
onClick={() => { setSearch(''); setScheduleId(''); }}
></button>
)}
</div>
{search && !scheduleId && (
<div className="border border-border rounded-md mt-1 max-h-56 overflow-y-auto bg-background shadow-md z-10 relative">
{filtered.length === 0
? <p className="text-xs text-muted-foreground px-3 py-2">No schedules found</p>
: filtered.map(s => (
<button
key={s.id}
className="w-full text-left px-3 py-2 text-sm hover:bg-muted truncate"
onClick={() => { setScheduleId(s.id); setSearch(''); }}
>{s.label}</button>
))
}
</div>
)}
</div>
<ActionButton onClick={() => setSubmittedId(scheduleId)} disabled={!scheduleId.trim() || isLoading}>
<ActionButton onClick={() => setSubmittedId(scheduleId)} disabled={!scheduleId || isLoading}>
Load Report
</ActionButton>
{data && (
@@ -77,7 +107,7 @@ export default function PassengersReportPage() {
)}
</div>
{isLoading && <p className="text-xs text-muted-foreground mt-2">Loading</p>}
{isError && <p className="text-xs text-red-500 mt-2">Failed to load report. Check the schedule ID.</p>}
{isError && <p className="text-xs text-red-500 mt-2">Failed to load report.</p>}
</div>
{data && (
@@ -158,7 +188,6 @@ export default function PassengersReportPage() {
{/* By Class + By Origin/Destination */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* By Class */}
<div className="card">
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4">By Class</h3>
<div className="space-y-3">
@@ -179,7 +208,6 @@ export default function PassengersReportPage() {
</div>
</div>
{/* By Origin */}
<div className="card">
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4">By Boarding Station</h3>
<div className="space-y-2">
@@ -193,7 +221,6 @@ export default function PassengersReportPage() {
</div>
</div>
{/* By Destination */}
<div className="card">
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4">By Alighting Station</h3>
<div className="space-y-2">