mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
Production checklists, issue fixes
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { Injectable, Inject, Optional } from '@nestjs/common';
|
||||
import { Injectable, Inject, Logger, Optional } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { PrismaService } from './prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuditService {
|
||||
private readonly logger = new Logger(AuditService.name);
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
@Optional() @Inject(REQUEST) private request?: any,
|
||||
@@ -34,7 +35,7 @@ export class AuditService {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to log audit event:', error);
|
||||
this.logger.error('Failed to log audit event:', error);
|
||||
// Don't throw - audit logging should not break main operations
|
||||
}
|
||||
}
|
||||
@@ -74,11 +75,20 @@ export class AuditService {
|
||||
where.entityType = filters.entityType;
|
||||
}
|
||||
|
||||
return this.prisma.auditLog.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 500,
|
||||
});
|
||||
const limit = Math.min(filters.limit ?? 50, 200);
|
||||
const offset = filters.offset ?? 0;
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
this.prisma.auditLog.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: limit,
|
||||
skip: offset,
|
||||
}),
|
||||
this.prisma.auditLog.count({ where }),
|
||||
]);
|
||||
|
||||
return { data, total, limit, offset };
|
||||
}
|
||||
|
||||
async getLog(id: string) {
|
||||
|
||||
@@ -41,9 +41,7 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
||||
this.logger.error(
|
||||
`${request.method} ${request.url} -> ${status}`,
|
||||
exception instanceof Error ? exception.stack : JSON.stringify(exception),
|
||||
);
|
||||
console.error('Full error details:', exception);
|
||||
} else {
|
||||
); } else {
|
||||
this.logger.warn(`${request.method} ${request.url} -> ${status} ${message}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
@@ -6,6 +6,7 @@ type TranslationMap = Record<string, any>;
|
||||
|
||||
@Injectable()
|
||||
export class I18nService {
|
||||
private readonly logger = new Logger(I18nService.name);
|
||||
private translations: Map<string, TranslationMap> = new Map();
|
||||
private readonly supportedLocales = ['en', 'am', 'fr', 'om'];
|
||||
private readonly defaultLocale = 'en';
|
||||
@@ -21,7 +22,7 @@ export class I18nService {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
this.translations.set(locale, JSON.parse(content));
|
||||
} catch (err) {
|
||||
console.warn(`Failed to load translation file for locale: ${locale}`);
|
||||
this.logger.warn(`Failed to load translation file for locale: ${locale}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,7 +548,7 @@ export class PassengerAuthService {
|
||||
|
||||
await this.dataSource.query(`DELETE FROM iam.users WHERE id = $1`, [iamUserId]);
|
||||
} catch (err) {
|
||||
console.error('[PassengerAuthService] IAM compensating cleanup failed for', email, (err as Error).message);
|
||||
this.logger.error(`[PassengerAuthService] IAM compensating cleanup failed for ${email}`, (err as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +317,10 @@ export class FleetService {
|
||||
}
|
||||
|
||||
getTrains() {
|
||||
return this.prisma.train.findMany({ include: { schedules: { take: 5, orderBy: { departureAt: 'desc' } } } });
|
||||
return this.prisma.train.findMany({
|
||||
where: { isActive: true },
|
||||
include: { schedules: { take: 5, orderBy: { departureAt: 'desc' } } },
|
||||
});
|
||||
}
|
||||
|
||||
createTrain(dto: CreateTrainDto) {
|
||||
@@ -462,6 +465,7 @@ export class FleetService {
|
||||
return this.prisma.coach.update({
|
||||
where: { id },
|
||||
data: {
|
||||
number: dto.number,
|
||||
arrangement: dto.arrangement,
|
||||
capacity: dto.capacity,
|
||||
status: dto.status,
|
||||
@@ -540,6 +544,7 @@ export class FleetService {
|
||||
]);
|
||||
if (!schedule) throw new NotFoundException('Schedule not found');
|
||||
if (!coach) throw new NotFoundException('Coach not found');
|
||||
if (coach.status !== 'ACTIVE') throw new BadRequestException('Coach is not active');
|
||||
return this.prisma.coachAssignment.create({ data: dto });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { Controller, Get, HttpStatus, Res } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { SkipThrottle } from '@nestjs/throttler';
|
||||
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
|
||||
import { PrismaService } from '../../common/prisma.service';
|
||||
import { Response } from 'express';
|
||||
|
||||
@ApiTags('Health')
|
||||
@Controller('health')
|
||||
@@ -20,17 +21,17 @@ export class HealthController {
|
||||
@Get('ready')
|
||||
@IsPublic()
|
||||
@ApiOperation({ summary: 'Readiness probe — checks database connectivity' })
|
||||
async readiness() {
|
||||
async readiness(@Res() res: Response) {
|
||||
const start = Date.now();
|
||||
try {
|
||||
await this.prisma.$queryRaw`SELECT 1`;
|
||||
return {
|
||||
return res.status(HttpStatus.OK).json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
checks: { database: { status: 'ok', latencyMs: Date.now() - start } },
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
return {
|
||||
return res.status(HttpStatus.SERVICE_UNAVAILABLE).json({
|
||||
status: 'error',
|
||||
timestamp: new Date().toISOString(),
|
||||
checks: {
|
||||
@@ -40,7 +41,7 @@ export class HealthController {
|
||||
error: err instanceof Error ? err.message : 'Unknown error',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { PrismaService } from '../../common/prisma.service';
|
||||
@@ -25,6 +25,7 @@ type IamUserRow = {
|
||||
|
||||
@Injectable()
|
||||
export class PassengersService {
|
||||
private readonly logger = new Logger(PassengersService.name);
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
@InjectDataSource() private readonly dataSource: DataSource,
|
||||
@@ -373,7 +374,7 @@ export class PassengersService {
|
||||
verifiedData = verification.passengerData;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Fayda verification failed, using manual data:', error);
|
||||
this.logger.warn('Fayda verification failed, using manual data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { PrismaService } from '../../common/prisma.service';
|
||||
@@ -6,6 +6,7 @@ import { GenerateReportDto, ReportType } from './reports.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ReportsService {
|
||||
private readonly logger = new Logger(ReportsService.name);
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
@InjectDataSource() private dataSource: DataSource,
|
||||
@@ -60,8 +61,6 @@ export class ReportsService {
|
||||
include: { paymentIntent: true }
|
||||
});
|
||||
|
||||
console.log(`[Reports] Revenue Report: Found ${bookings.length} bookings between ${dateFrom} and ${dateTo}`);
|
||||
|
||||
const totalRevenue = bookings.reduce((sum, b) => sum + b.totalMinor, 0);
|
||||
const byPaymentMethod = bookings.reduce((acc, b) => {
|
||||
const method = b.paymentIntent?.method ?? 'UNKNOWN';
|
||||
|
||||
@@ -100,10 +100,15 @@ export class SchedulesService {
|
||||
const arr = parseEthiopianTime(dto.arrivalAt);
|
||||
if (arr <= dep) throw new BadRequestException('arrivalAt must be after departureAt');
|
||||
|
||||
const route = await this.prisma.route.findUnique({
|
||||
where: { id: dto.routeId },
|
||||
include: { stops: { orderBy: { sequence: 'asc' } } },
|
||||
});
|
||||
const [train, route] = await Promise.all([
|
||||
this.prisma.train.findUnique({ where: { id: dto.trainId } }),
|
||||
this.prisma.route.findUnique({
|
||||
where: { id: dto.routeId },
|
||||
include: { stops: { orderBy: { sequence: 'asc' } } },
|
||||
}),
|
||||
]);
|
||||
if (!train) throw new NotFoundException('Train not found');
|
||||
if (!train.isActive) throw new BadRequestException('Train is not active');
|
||||
if (!route) throw new NotFoundException('Route not found');
|
||||
if (!route.active) throw new BadRequestException('Route is not active');
|
||||
if (route.stops.length < 2) throw new BadRequestException('Route must have at least 2 stops');
|
||||
@@ -247,10 +252,15 @@ export class SchedulesService {
|
||||
const arr = parseEthiopianTime(dto.arrivalAt);
|
||||
if (arr <= dep) throw new BadRequestException('arrivalAt must be after departureAt');
|
||||
|
||||
const route = await this.prisma.route.findUnique({
|
||||
where: { id: dto.routeId },
|
||||
include: { stops: { orderBy: { sequence: 'asc' } } },
|
||||
});
|
||||
const [train, route] = await Promise.all([
|
||||
this.prisma.train.findUnique({ where: { id: dto.trainId } }),
|
||||
this.prisma.route.findUnique({
|
||||
where: { id: dto.routeId },
|
||||
include: { stops: { orderBy: { sequence: 'asc' } } },
|
||||
}),
|
||||
]);
|
||||
if (!train) throw new NotFoundException('Train not found');
|
||||
if (!train.isActive) throw new BadRequestException('Train is not active');
|
||||
if (!route) throw new NotFoundException('Route not found');
|
||||
if (!route.active) throw new BadRequestException('Route is not active');
|
||||
if (route.stops.length < 2) throw new BadRequestException('Route must have at least 2 stops');
|
||||
@@ -540,6 +550,8 @@ export class SchedulesService {
|
||||
const coachIds = coaches.map(c => c.coachId);
|
||||
const existingCoaches = await this.prisma.coach.findMany({ where: { id: { in: coachIds } } });
|
||||
if (existingCoaches.length !== coachIds.length) throw new NotFoundException('One or more coaches not found');
|
||||
const inactiveCoach = existingCoaches.find(c => c.status !== 'ACTIVE');
|
||||
if (inactiveCoach) throw new BadRequestException(`Coach ${inactiveCoach.number} is not active`);
|
||||
|
||||
await this.prisma.coachAssignment.deleteMany({ where: { scheduleId } });
|
||||
|
||||
|
||||
@@ -6,12 +6,16 @@ export class SeatClassesService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
listSeatClasses() {
|
||||
return this.prisma.seatClass.findMany({ orderBy: { createdAt: 'asc' } });
|
||||
return this.prisma.seatClass.findMany({
|
||||
where: { isActive: true },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
async getSeatClass(id: string) {
|
||||
const sc = await this.prisma.seatClass.findUnique({ where: { id } });
|
||||
if (!sc) throw new NotFoundException('SeatClass not found');
|
||||
if (!sc.isActive) throw new NotFoundException('SeatClass is not active');
|
||||
return sc;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,8 +33,11 @@ export class StationsService {
|
||||
where.countryCode = filters.country;
|
||||
}
|
||||
|
||||
// Default to operational stations only; allow explicit override (e.g. back-office)
|
||||
if (filters.operational !== undefined && filters.operational !== '') {
|
||||
where.isOperational = filters.operational === 'true';
|
||||
} else {
|
||||
where.isOperational = true;
|
||||
}
|
||||
|
||||
return this.prisma.station.findMany({
|
||||
@@ -46,6 +49,7 @@ export class StationsService {
|
||||
async findOne(id: string) {
|
||||
const s = await this.prisma.station.findUnique({ where: { id } });
|
||||
if (!s) throw new NotFoundException('Station not found');
|
||||
if (!s.isOperational) throw new NotFoundException('Station is not operational');
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
@@ -99,18 +99,11 @@ export class TicketsService {
|
||||
let guestEmail = null;
|
||||
const matchingProfile = t.booking?.passenger?.travelerProfiles?.find((tp: any) => tp.fullName === t.passengerName);
|
||||
|
||||
// DEBUG: Log to see what we're getting
|
||||
this.logger.debug(`Ticket ${t.id}: passengerName=${t.passengerName}, profiles count=${t.booking?.passenger?.travelerProfiles?.length || 0}, matchingProfile=${!!matchingProfile}`);
|
||||
if (matchingProfile) {
|
||||
this.logger.debug(`Matching profile notes: ${matchingProfile.notes}`);
|
||||
}
|
||||
|
||||
if (matchingProfile?.notes) {
|
||||
try {
|
||||
const notesData = JSON.parse(matchingProfile.notes);
|
||||
guestPhone = notesData.phone || null;
|
||||
guestEmail = notesData.email || null;
|
||||
this.logger.debug(`Extracted from notes: phone=${guestPhone}, email=${guestEmail}`);
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to parse notes JSON: ${err}`);
|
||||
}
|
||||
@@ -120,13 +113,11 @@ export class TicketsService {
|
||||
if (!guestPhone) guestPhone = t.booking?.contactPhone;
|
||||
if (!guestEmail) guestEmail = t.booking?.contactEmail;
|
||||
|
||||
this.logger.debug(`Final values: phone=${guestPhone}, email=${guestEmail}`);
|
||||
|
||||
const passengerInfo = iam
|
||||
? { fullName: iam.name?.en ?? iam.name?.am ?? null, email: iam.email, phone: iam.phone_number }
|
||||
: { fullName: 'Guest', email: guestEmail, phone: guestPhone };
|
||||
|
||||
this.logger.debug(`Final passenger info: ${JSON.stringify(passengerInfo)}`);
|
||||
|
||||
return {
|
||||
id: t.id,
|
||||
@@ -596,8 +587,7 @@ export class TicketsService {
|
||||
}
|
||||
|
||||
private async fireBoardingPassNotification(booking: any, ticket: any, leg: string | null) {
|
||||
// TODO: Implement notification logic
|
||||
console.log(`Boarding pass notification for booking ${booking.bookingRef}, leg: ${leg}`);
|
||||
this.logger.log(`Boarding pass notification for booking ${booking.bookingRef}, leg: ${leg}`);
|
||||
}
|
||||
|
||||
async getValidationLogs(ticketId: string) {
|
||||
@@ -687,4 +677,4 @@ export class TicketsService {
|
||||
if (!ticket) throw new NotFoundException('Ticket not found');
|
||||
return this.prisma.ticket.update({ where: { id }, data: { status: 'ACTIVE' } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,11 +144,10 @@ export default function CoachesPage() {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('coaches');
|
||||
const [search, setSearch] = useState('');
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [showPreviewModal, setShowPreviewModal] = useState(false);
|
||||
const [seatMapPreview, setSeatMapPreview] = useState<any>(null);
|
||||
const [editingItem, setEditingItem] = useState<any>(null);
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; item: any | null; error?: string }>({ isOpen: false, item: null });
|
||||
const [selectedCoachTypeId, setSelectedCoachTypeId] = useState<string>('');
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Coach Types Queries
|
||||
@@ -221,14 +220,6 @@ export default function CoachesPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const generateSeatMapMutation = useMutation({
|
||||
mutationFn: fleetApi.generateSeatMap,
|
||||
onSuccess: (data) => {
|
||||
setSeatMapPreview(data);
|
||||
setShowPreviewModal(true);
|
||||
},
|
||||
});
|
||||
|
||||
const handleCoachTypeSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
@@ -275,27 +266,6 @@ export default function CoachesPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreviewSeatMap = async () => {
|
||||
const form = document.querySelector('form') as HTMLFormElement;
|
||||
const formData = new FormData(form);
|
||||
const bedCategory = formData.get('bedCategory') as string;
|
||||
const capacity = parseInt(formData.get('capacity') as string);
|
||||
|
||||
if (!bedCategory || !capacity) {
|
||||
alert('Please select a bed category and enter capacity to preview seat map');
|
||||
return;
|
||||
}
|
||||
|
||||
const bedsPerRoom = bedCategory === 'VIP_BED' ? 4 : 6;
|
||||
const roomsPerCoach = Math.ceil(capacity / bedsPerRoom);
|
||||
|
||||
await generateSeatMapMutation.mutateAsync({
|
||||
coachCount: 1,
|
||||
roomsPerCoach,
|
||||
roomType: bedCategory,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (item: any, isCoachType: boolean) => {
|
||||
setDeleteConfirm({ isOpen: true, item: { ...item, isCoachType } });
|
||||
};
|
||||
@@ -779,23 +749,25 @@ export default function CoachesPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Conditionally show bed fields only for Economy and Regular coach types */}
|
||||
{(() => {
|
||||
const selectedCoachType = coachTypesArray.find((ct: any) => ct.id === (selectedCoachTypeId || editingItem?.coachTypeId));
|
||||
const isEconomyOrRegular = selectedCoachType &&
|
||||
(selectedCoachType.name?.toLowerCase().includes('economy') ||
|
||||
selectedCoachType.name?.toLowerCase().includes('regular') ||
|
||||
selectedCoachType.type?.toLowerCase().includes('economy') ||
|
||||
selectedCoachType.type?.toLowerCase().includes('regular'));
|
||||
|
||||
return isEconomyOrRegular ? (
|
||||
const isBedType = selectedCoachType &&
|
||||
(selectedCoachType.name?.toLowerCase().includes('bed') ||
|
||||
selectedCoachType.name?.toLowerCase().includes('sleeper') ||
|
||||
selectedCoachType.type?.toLowerCase().includes('sleeper'));
|
||||
|
||||
const derivedBedCategory = editingItem?.isCoach && selectedCoachType
|
||||
? (selectedCoachType.name?.toLowerCase().includes('vip') ? 'VIP_BED' : 'ECONOMY_BED')
|
||||
: (editingItem?.bedCategory || '');
|
||||
|
||||
return isBedType ? (
|
||||
<>
|
||||
<div>
|
||||
<label className="label">Bed Category</label>
|
||||
<select
|
||||
name="bedCategory"
|
||||
className="input"
|
||||
defaultValue={editingItem?.bedCategory || ''}
|
||||
defaultValue={derivedBedCategory}
|
||||
>
|
||||
<option value="">Select bed category</option>
|
||||
<option value="ECONOMY_BED">Economy Bed</option>
|
||||
@@ -832,9 +804,9 @@ export default function CoachesPage() {
|
||||
type="text"
|
||||
name="arrangement"
|
||||
className="input"
|
||||
defaultValue={editingItem?.arrangement || editingItem?.seatArrangement || '2+2'}
|
||||
defaultValue={editingItem?.arrangement || editingItem?.seatArrangement }
|
||||
required
|
||||
placeholder="e.g., 2+2, 3+2"
|
||||
placeholder="e.g., 3+2"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
For regular seats: columns separated by +
|
||||
@@ -850,7 +822,7 @@ export default function CoachesPage() {
|
||||
defaultValue={editingItem?.capacity || editingItem?.totalUnits || ''}
|
||||
required
|
||||
min="1"
|
||||
placeholder="60"
|
||||
placeholder="e.g., 60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -860,10 +832,10 @@ export default function CoachesPage() {
|
||||
type="number"
|
||||
name="sequence"
|
||||
className="input"
|
||||
defaultValue={editingItem?.sequence || 1}
|
||||
defaultValue={editingItem?.sequence }
|
||||
min="1"
|
||||
required
|
||||
placeholder="1"
|
||||
placeholder="e.g., 1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Position in train consist
|
||||
@@ -885,14 +857,6 @@ export default function CoachesPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<ActionButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handlePreviewSeatMap}
|
||||
loading={generateSeatMapMutation.isPending}
|
||||
>
|
||||
Preview Bed Layout
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
@@ -915,57 +879,7 @@ export default function CoachesPage() {
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* Seat Map Preview Modal */}
|
||||
<Modal
|
||||
isOpen={showPreviewModal}
|
||||
onClose={() => {
|
||||
setShowPreviewModal(false);
|
||||
setSeatMapPreview(null);
|
||||
}}
|
||||
title="Bed Layout Preview"
|
||||
size="lg"
|
||||
>
|
||||
{seatMapPreview && (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-muted/50 p-4 rounded-lg">
|
||||
<h4 className="font-semibold mb-2">Configuration</h4>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>Room Type: <span className="font-medium">{seatMapPreview.roomType}</span></div>
|
||||
<div>Rooms per Coach: <span className="font-medium">{seatMapPreview.roomsPerCoach}</span></div>
|
||||
<div>Beds per Room: <span className="font-medium">{seatMapPreview.bedsPerRoom}</span></div>
|
||||
<div>Total Beds: <span className="font-medium">{seatMapPreview.totalBeds}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold">Bed Layout Sample (First Few Rooms)</h4>
|
||||
<div className="bg-gray-50 p-4 rounded border max-h-64 overflow-y-auto">
|
||||
{seatMapPreview.seats?.slice(0, 24).map((seat: any, idx: number) => (
|
||||
<div key={idx} className="text-xs mb-1 font-mono">
|
||||
{seat.seat_id} - Room: {seat.room_id} - {seat.position} {seat.bed_type}
|
||||
</div>
|
||||
))}
|
||||
{seatMapPreview.seats?.length > 24 && (
|
||||
<div className="text-xs text-muted-foreground mt-2">
|
||||
... and {seatMapPreview.seats.length - 24} more beds
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<ActionButton
|
||||
onClick={() => {
|
||||
setShowPreviewModal(false);
|
||||
setSeatMapPreview(null);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,12 +55,6 @@ function DashboardPageContent() {
|
||||
retry: 1,
|
||||
});
|
||||
|
||||
const { data: topAgents, isLoading: agentsLoading } = useQuery({
|
||||
queryKey: ['top-agents'],
|
||||
queryFn: () => dashboardApi.getTopAgents(5),
|
||||
retry: 1,
|
||||
});
|
||||
|
||||
const { data: upcomingTrips, isLoading: tripsLoading } = useQuery({
|
||||
queryKey: ['upcoming-trips'],
|
||||
queryFn: () => dashboardApi.getUpcomingTrips(5),
|
||||
@@ -109,13 +103,6 @@ function DashboardPageContent() {
|
||||
{ key: 'createdAt', label: 'Created', render: (item: any) => formatDateTime(item.createdAt) },
|
||||
];
|
||||
|
||||
const agentColumns = [
|
||||
{ key: 'name', label: 'Agent Name', render: (item: any) => item.name || item.fullName },
|
||||
{ key: 'bookings', label: 'Bookings', render: (item: any) => item.bookingsCount || item.bookings || 0 },
|
||||
{ key: 'revenue', label: 'Revenue', render: (item: any) => formatCurrency(item.totalRevenue || item.revenue || 0, 'ETB') },
|
||||
{ key: 'commission', label: 'Commission', render: (item: any) => formatCurrency(item.commission || 0, 'ETB') },
|
||||
];
|
||||
|
||||
const tripColumns = [
|
||||
{ key: 'trainName', label: 'Train', render: (item: any) => item.trainName || item.train?.name },
|
||||
{ key: 'route', label: 'Route', render: (item: any) => `${item.originStation?.name || item.origin?.name} → ${item.destinationStation?.name || item.destination?.name}` },
|
||||
@@ -231,19 +218,6 @@ function DashboardPageContent() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Top Agents */}
|
||||
<div className="card">
|
||||
<h2 className="mb-4 text-lg font-semibold text-foreground flex items-center gap-2">
|
||||
<Users className="h-5 w-5" />
|
||||
Top Performing Agents
|
||||
</h2>
|
||||
<DataTable
|
||||
data={topAgents || []}
|
||||
columns={agentColumns}
|
||||
loading={agentsLoading}
|
||||
emptyMessage="No agent performance data available"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ export default function OperationalReportsPage() {
|
||||
refetch();
|
||||
setShowGenerateModal(false);
|
||||
} catch (error) {
|
||||
console.error('Error generating report:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -64,7 +64,6 @@ export default function PaymentMethodsPage() {
|
||||
setTimeout(() => setSuccessMessage(''), 3000);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Update failed:', error);
|
||||
setSuccessMessage('Failed to update payment method');
|
||||
setTimeout(() => setSuccessMessage(''), 3000);
|
||||
},
|
||||
@@ -131,8 +130,6 @@ export default function PaymentMethodsPage() {
|
||||
processingTime: formData.processingTime,
|
||||
};
|
||||
|
||||
console.log('Submitting data:', submitData);
|
||||
|
||||
if (selectedMethod) {
|
||||
updateMutation.mutate({ id: selectedMethod.id, ...submitData });
|
||||
} else {
|
||||
|
||||
@@ -49,8 +49,7 @@ export default function ActionButton({
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await onClick();
|
||||
} catch (error) {
|
||||
console.error('Action failed:', error);
|
||||
} catch (error) {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -13,10 +13,14 @@ export const dashboardApi = {
|
||||
const bookingsTotal = bookingsRes?.meta?.total || 0;
|
||||
const passengersTotal = passengersRes?.meta?.total || 0;
|
||||
|
||||
// Calculate revenue from bookings
|
||||
const allBookingsRes = await apiClient.get<any>('/bookings?pageSize=100');
|
||||
const allBookings = Array.isArray(allBookingsRes) ? allBookingsRes : allBookingsRes?.items || [];
|
||||
const totalRevenue = allBookings.reduce((sum: number, b: any) => sum + (b.totalMinor || 0), 0);
|
||||
// Calculate revenue from confirmed bookings only
|
||||
const confirmedRes = await apiClient.get<any>('/bookings?pageSize=1000&status=CONFIRMED');
|
||||
const boardedRes = await apiClient.get<any>('/bookings?pageSize=1000&status=BOARDED');
|
||||
const paidBookings = [
|
||||
...(Array.isArray(confirmedRes) ? confirmedRes : confirmedRes?.items || []),
|
||||
...(Array.isArray(boardedRes) ? boardedRes : boardedRes?.items || []),
|
||||
];
|
||||
const totalRevenue = paidBookings.reduce((sum: number, b: any) => sum + (b.totalMinor || 0), 0);
|
||||
|
||||
// Calculate average occupancy (placeholder - would need dedicated endpoint)
|
||||
const occupancyRate = Math.floor(Math.random() * 100); // Replace with actual data
|
||||
@@ -29,10 +33,9 @@ export const dashboardApi = {
|
||||
totalTripsToday: 0,
|
||||
activeTrips: 0,
|
||||
cancelledBookings: 0,
|
||||
averageTicketPrice: allBookings.length > 0 ? totalRevenue / allBookings.length : 0,
|
||||
averageTicketPrice: paidBookings.length > 0 ? Math.round(totalRevenue / paidBookings.length) : 0,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch dashboard stats:', error);
|
||||
return {
|
||||
totalBookings: 0,
|
||||
totalRevenue: 0,
|
||||
@@ -51,7 +54,6 @@ export const dashboardApi = {
|
||||
const response = await apiClient.get<RevenueData[]>(`/dashboard/revenue?days=${days}`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch revenue chart:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
@@ -82,7 +84,6 @@ export const dashboardApi = {
|
||||
paymentIntent: booking.paymentIntent,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch recent bookings:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
@@ -92,7 +93,6 @@ export const dashboardApi = {
|
||||
const response = await apiClient.get<any[]>(`/agents/top?limit=${limit}`);
|
||||
return response || [];
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch top agents:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
@@ -102,7 +102,6 @@ export const dashboardApi = {
|
||||
const response = await apiClient.get<any[]>(`/dashboard/occupancy?days=${days}`);
|
||||
return response || [];
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch occupancy trend:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
@@ -112,7 +111,6 @@ export const dashboardApi = {
|
||||
const response = await apiClient.get<any[]>(`/schedules/upcoming?limit=${limit}`);
|
||||
return response || [];
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch upcoming trips:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
@@ -122,7 +120,6 @@ export const dashboardApi = {
|
||||
const response = await apiClient.get<any[]>('/dashboard/payment-methods');
|
||||
return response || [];
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch payment methods:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
@@ -137,7 +134,6 @@ export const dashboardApi = {
|
||||
loyaltyPoints: 0,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch passenger stats:', error);
|
||||
return {
|
||||
totalPassengers: 0,
|
||||
newPassengersToday: 0,
|
||||
@@ -157,7 +153,6 @@ export const dashboardApi = {
|
||||
totalAmount: 0,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch transaction summary:', error);
|
||||
return {
|
||||
totalTransactions: 0,
|
||||
successfulTransactions: 0,
|
||||
@@ -176,7 +171,6 @@ export const dashboardApi = {
|
||||
activePayments: 0,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch live metrics:', error);
|
||||
return {
|
||||
onlineUsers: 0,
|
||||
activeBookings: 0,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { PaginatedResponse } from '@edr/types';
|
||||
export const routesApi = {
|
||||
getAll: async () => {
|
||||
const response = await apiClient.get<any>('/routes');
|
||||
console.log('Routes API response:', response);
|
||||
// Handle both direct array and wrapped response
|
||||
if (Array.isArray(response)) {
|
||||
return { items: response };
|
||||
@@ -24,7 +23,6 @@ export const routesApi = {
|
||||
},
|
||||
|
||||
create: (data: any) => {
|
||||
console.log('Creating route with data:', JSON.stringify(data, null, 2));
|
||||
return apiClient.post<Route>('/routes', data);
|
||||
},
|
||||
|
||||
|
||||
@@ -681,7 +681,7 @@ export default function PassengersPage() {
|
||||
|
||||
try {
|
||||
const response: any = await apiClient.post('/fayda/verification/start', {
|
||||
purpose: 'PURCHASE',
|
||||
purpose: 'VERIFY',
|
||||
platform: 'WEB',
|
||||
saveToAccount: index === 0 && isAuthenticated,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user