mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 09:42:53 +00:00
Merge branch 'alpha' into passenger/feat/iam
This commit is contained in:
@@ -107,7 +107,7 @@ CREATE TABLE "system_features" (
|
||||
"is_enabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"config" JSONB,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "system_features_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
@@ -115,25 +115,35 @@ export class PassengersService {
|
||||
const guestBooking = (passenger as any)?.bookings?.[0] ?? null;
|
||||
const guestSeat = guestBooking?.seats?.[0] ?? null;
|
||||
|
||||
// Parse notes JSON to extract phone and other data
|
||||
let notesData: any = null;
|
||||
if (profile.notes) {
|
||||
try {
|
||||
notesData = typeof profile.notes === 'string' ? JSON.parse(profile.notes) : profile.notes;
|
||||
} catch {
|
||||
notesData = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: profile.id,
|
||||
fullName: profile.fullName,
|
||||
email: localUser?.email ?? iam?.email ?? guestBooking?.contactEmail ?? null,
|
||||
phone: localUser?.phone ?? iam?.phone_number ?? guestBooking?.contactPhone ?? null,
|
||||
email: localUser?.email ?? iam?.email ?? notesData?.email ?? guestBooking?.contactEmail ?? null,
|
||||
phone: localUser?.phone ?? iam?.phone_number ?? notesData?.phone ?? guestBooking?.contactPhone ?? null,
|
||||
gender: profile.gender ?? localUser?.gender ?? iam?.metadata?.gender ?? null,
|
||||
dateOfBirth: profile.dateOfBirth
|
||||
? new Date(profile.dateOfBirth).toISOString().split('T')[0]
|
||||
: (localUser?.dateOfBirth
|
||||
? (localUser.dateOfBirth instanceof Date ? localUser.dateOfBirth.toISOString().split('T')[0] : localUser.dateOfBirth)
|
||||
: iam?.metadata?.dateOfBirth ?? null),
|
||||
nationality: localUser?.nationality ?? iam?.metadata?.nationality ?? (guestSeat?.passportCountry ? (guestSeat.passportCountry === 'Ethiopia' ? 'Ethiopian' : guestSeat.passportCountry) : null),
|
||||
nationality: localUser?.nationality ?? iam?.metadata?.nationality ?? notesData?.nationality ?? (guestSeat?.passportCountry ? (guestSeat.passportCountry === 'Ethiopia' ? 'Ethiopian' : guestSeat.passportCountry) : null),
|
||||
nationalityCode: localUser?.nationalityCode ?? iam?.metadata?.nationalityCode ?? null,
|
||||
faydaVerified,
|
||||
faydaVerifiedAt: localUser?.faydaVerifiedAt ?? iam?.metadata?.faydaVerifiedAt ?? null,
|
||||
passportNumber: localUser?.passportNumber ?? iam?.metadata?.passportNumber ?? guestSeat?.passportNumber ?? null,
|
||||
passportCountry: localUser?.passportCountry ?? iam?.metadata?.passportCountry ?? guestSeat?.passportCountry ?? null,
|
||||
passportNumber: localUser?.passportNumber ?? iam?.metadata?.passportNumber ?? notesData?.passportNumber ?? guestSeat?.passportNumber ?? null,
|
||||
passportCountry: localUser?.passportCountry ?? iam?.metadata?.passportCountry ?? notesData?.passportCountry ?? guestSeat?.passportCountry ?? null,
|
||||
passportExpiryDate: localUser?.passportExpiryDate ?? iam?.metadata?.passportExpiryDate ?? null,
|
||||
idDocumentType: profile.nationalId ? 'NATIONAL_ID' : null,
|
||||
idDocumentType: profile.nationalId ? 'NATIONAL_ID' : (notesData?.idDocumentType ?? null),
|
||||
verified: faydaVerified,
|
||||
lastLoginAt: localUser?.lastLoginAt ?? null,
|
||||
role: localUser?.role ?? null,
|
||||
|
||||
@@ -54,11 +54,11 @@ export class TicketsService {
|
||||
...(filters.dateTo ? { lte: new Date(new Date(filters.dateTo).setHours(23, 59, 59, 999)) } : {}),
|
||||
};
|
||||
}
|
||||
// if (filters.coachId) {
|
||||
// where.seat = {
|
||||
// coachId: filters.coachId
|
||||
// };
|
||||
// }
|
||||
if (filters.coachId) {
|
||||
where.seat = {
|
||||
coachId: filters.coachId
|
||||
};
|
||||
}
|
||||
|
||||
const [tickets, total] = await Promise.all([
|
||||
this.prisma.ticket.findMany({
|
||||
@@ -67,8 +67,8 @@ export class TicketsService {
|
||||
booking: {
|
||||
include: {
|
||||
schedule: { include: { originStation: true, destinationStation: true, train: true } },
|
||||
returnSchedule: { select: { departureAt: true, arrivalAt: true, originStation: true, destinationStation: true } },
|
||||
passenger: { select: { id: true, iamUserId: true } },
|
||||
returnSchedule: { include: { originStation: true, destinationStation: true } },
|
||||
passenger: { include: { travelerProfiles: true } },
|
||||
seats: { include: { seat: { include: { coach: true } } } },
|
||||
},
|
||||
},
|
||||
@@ -93,9 +93,41 @@ export class TicketsService {
|
||||
return {
|
||||
items: tickets.map((t: any) => {
|
||||
const iam = t.booking?.passenger?.iamUserId ? iamMap.get(t.booking.passenger.iamUserId) : undefined;
|
||||
|
||||
// Extract phone from TravelerProfile notes JSON
|
||||
let guestPhone = null;
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to booking contact info if no match in TravelerProfile
|
||||
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: t.booking?.contactEmail, phone: null };
|
||||
: { fullName: 'Guest', email: guestEmail, phone: guestPhone };
|
||||
|
||||
this.logger.debug(`Final passenger info: ${JSON.stringify(passengerInfo)}`);
|
||||
|
||||
return {
|
||||
id: t.id,
|
||||
ticketNumber: t.barcodePayload,
|
||||
|
||||
@@ -11,7 +11,7 @@ export type PassengerSeedRole = {
|
||||
};
|
||||
|
||||
export const EDR_PASSENGER_APPLICATION = {
|
||||
id: 'd2000001-0001-4000-8000-000000000001',
|
||||
id: '921cd1a4-98a7-4601-bfb1-6fe19518be52',
|
||||
key: 'edr_passenger_app',
|
||||
name: {
|
||||
am: 'EDR Passenger App',
|
||||
|
||||
@@ -15,26 +15,26 @@ const perm = (id: string, key: string, en: string): PassengerPermissionSeed => (
|
||||
});
|
||||
|
||||
export const PASSENGER_PERMISSIONS: PassengerPermissionSeed[] = [
|
||||
perm('c1000001-0001-4000-8000-000000000001', 'edr_passenger_app:bookings:view', 'View bookings'),
|
||||
perm('c1000001-0001-4000-8000-000000000002', 'edr_passenger_app:bookings:manage', 'Manage bookings'),
|
||||
perm('c1000001-0001-4000-8000-000000000003', 'edr_passenger_app:bookings:cancel', 'Cancel bookings'),
|
||||
perm('c1000001-0001-4000-8000-000000000004', 'edr_passenger_app:passengers:view', 'View passengers'),
|
||||
perm('c1000001-0001-4000-8000-000000000005', 'edr_passenger_app:passengers:manage', 'Manage passengers'),
|
||||
perm('c1000001-0001-4000-8000-000000000006', 'edr_passenger_app:tickets:view', 'View tickets'),
|
||||
perm('c1000001-0001-4000-8000-000000000007', 'edr_passenger_app:tickets:manage', 'Manage tickets'),
|
||||
perm('c1000001-0001-4000-8000-000000000008', 'edr_passenger_app:payments:view_all', 'View all payments'),
|
||||
perm('c1000001-0001-4000-8000-000000000009', 'edr_passenger_app:payments:refund', 'Refund payments'),
|
||||
perm('c1000001-0001-4000-8000-00000000000a', 'edr_passenger_app:payments:manage_methods', 'Manage payment methods'),
|
||||
perm('c1000001-0001-4000-8000-00000000000b', 'edr_passenger_app:reports:view', 'View reports'),
|
||||
perm('c1000001-0001-4000-8000-00000000000c', 'edr_passenger_app:fraud:view', 'View fraud alerts'),
|
||||
perm('c1000001-0001-4000-8000-00000000000d', 'edr_passenger_app:fraud:manage', 'Manage fraud rules'),
|
||||
perm('c1000001-0001-4000-8000-00000000000e', 'edr_passenger_app:audit:view', 'View audit logs'),
|
||||
perm('c1000001-0001-4000-8000-00000000000f', 'edr_passenger_app:agents:view', 'View agents'),
|
||||
perm('c1000001-0001-4000-8000-000000000010', 'edr_passenger_app:agents:manage', 'Manage agents'),
|
||||
perm('c1000001-0001-4000-8000-000000000011', 'edr_passenger_app:currencies:manage', 'Manage currencies'),
|
||||
perm('c1000001-0001-4000-8000-000000000012', 'edr_passenger_app:notifications:send', 'Send notifications'),
|
||||
perm('c1000001-0001-4000-8000-000000000013', 'edr_passenger_app:dashboard:view', 'View dashboard'),
|
||||
perm('c1000001-0001-4000-8000-000000000014', 'edr_passenger_app:admin', 'Full admin access'),
|
||||
perm('40f1b49c-c33d-4563-a6bb-9373eabbde9b', 'edr_passenger_app:bookings:view', 'View bookings'),
|
||||
perm('62810ae5-315e-4ae5-8ed1-33cead51b95a', 'edr_passenger_app:bookings:manage', 'Manage bookings'),
|
||||
perm('b593adf3-2060-48b0-b35d-ff9ff5d72bc4', 'edr_passenger_app:bookings:cancel', 'Cancel bookings'),
|
||||
perm('566c968f-71f1-462d-9824-4b7cd33cecbb', 'edr_passenger_app:passengers:view', 'View passengers'),
|
||||
perm('ff5d33a0-0fe7-427f-a065-46dd14ac1da0', 'edr_passenger_app:passengers:manage', 'Manage passengers'),
|
||||
perm('326ec767-1da8-4c7e-b557-d4d2f9dd6d2c', 'edr_passenger_app:tickets:view', 'View tickets'),
|
||||
perm('8ec5697f-d2d4-40a2-a365-ad624991a2ab', 'edr_passenger_app:tickets:manage', 'Manage tickets'),
|
||||
perm('736aca18-6660-4865-9773-81a636f51fa0', 'edr_passenger_app:payments:view_all', 'View all payments'),
|
||||
perm('44065042-b4af-4af2-b213-34a823f78be1', 'edr_passenger_app:payments:refund', 'Refund payments'),
|
||||
perm('558f0172-ab9f-4d13-9477-4ca247d94f3c', 'edr_passenger_app:payments:manage_methods', 'Manage payment methods'),
|
||||
perm('bf6cbd48-7a4a-46ec-91f3-0a23245da0a4', 'edr_passenger_app:reports:view', 'View reports'),
|
||||
perm('53b49280-a272-4688-8345-e14fbedce50e', 'edr_passenger_app:fraud:view', 'View fraud alerts'),
|
||||
perm('834f576c-afe2-41f4-9e6e-5f87b155fbf4', 'edr_passenger_app:fraud:manage', 'Manage fraud rules'),
|
||||
perm('988b680d-df5a-4e79-9c01-c9441753ce1a', 'edr_passenger_app:audit:view', 'View audit logs'),
|
||||
perm('38736897-545f-47dc-9531-7fc381478a1f', 'edr_passenger_app:agents:view', 'View agents'),
|
||||
perm('30ada94a-cd15-4588-97bd-cd2f9d33ad7d', 'edr_passenger_app:agents:manage', 'Manage agents'),
|
||||
perm('75b5ff62-a8e4-4331-b6e6-d53e1456d10e', 'edr_passenger_app:currencies:manage', 'Manage currencies'),
|
||||
perm('4a47da9b-cf6e-4240-aff8-aadf01641c54', 'edr_passenger_app:notifications:send', 'Send notifications'),
|
||||
perm('bfe3428f-8b85-4a36-87c6-33063b084bf3', 'edr_passenger_app:dashboard:view', 'View dashboard'),
|
||||
perm('49fd28cd-5b58-4403-8e53-1df4b93cbbd2', 'edr_passenger_app:admin', 'Full admin access'),
|
||||
];
|
||||
|
||||
export const PASSENGER_PERMISSION_KEYS = PASSENGER_PERMISSIONS.map((p) => p.key);
|
||||
|
||||
@@ -185,7 +185,7 @@ function BookingsPageContent() {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'contact', label: 'Contact',
|
||||
key: 'contact', label: 'Primary contact',
|
||||
render: (booking: any) => (
|
||||
<div>
|
||||
<div className="font-medium">{booking.contactPhone || booking.passenger?.phone}</div>
|
||||
|
||||
@@ -348,14 +348,8 @@ export default function TicketsPage() {
|
||||
key: 'contact',
|
||||
label: 'Contact',
|
||||
render: (ticket: any) => {
|
||||
// Find the booking seat that matches this ticket's passenger
|
||||
const matchingSeat = ticket.booking?.seats?.find((s: any) =>
|
||||
s.passengerName === ticket.passengerName && s.leg === ticket.leg
|
||||
);
|
||||
|
||||
// Try to get phone from BookingSeat first, then fall back to booking contact
|
||||
const phone = matchingSeat?.phone || ticket.booking?.contactPhone || ticket.booking?.passenger?.phone || 'N/A';
|
||||
const email = matchingSeat?.email || ticket.booking?.contactEmail || ticket.booking?.passenger?.email || 'N/A';
|
||||
const phone = ticket.booking?.passenger?.phone || 'N/A';
|
||||
const email = ticket.booking?.passenger?.email || 'N/A';
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -372,16 +366,21 @@ export default function TicketsPage() {
|
||||
label: 'Trip',
|
||||
render: (ticket: any) => {
|
||||
const isRoundTrip = ticket.booking?.bookingType === 'ROUND_TRIP' || ticket.booking?.bookingType === 'ROUND_TRIP_TRANSIT';
|
||||
const returnArrivalAt = ticket.booking?.returnSchedule?.arrivalAt;
|
||||
const returnDeparture = ticket.booking?.returnSchedule?.departureAt;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="font-medium">
|
||||
{ticket.schedule?.originStation?.name || 'N/A'} → {ticket.schedule?.destinationStation?.name || 'N/A'}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{ticket.schedule?.departureAt ? formatDateTimeShort(ticket.schedule.departureAt) : 'N/A'}
|
||||
{isRoundTrip && (
|
||||
<span> → {returnArrivalAt ? formatDateTimeShort(returnArrivalAt) : 'N/A'}</span>
|
||||
{!isRoundTrip ? (
|
||||
<span>{ticket.schedule?.departureAt ? formatDateTimeShort(ticket.schedule.departureAt) : 'N/A'}</span>
|
||||
) : (
|
||||
<span>
|
||||
{ticket.schedule?.departureAt ? formatDateTimeShort(ticket.schedule.departureAt) : 'N/A'} ·
|
||||
{returnDeparture ? formatDateTimeShort(returnDeparture) : 'N/A'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -425,9 +424,26 @@ export default function TicketsPage() {
|
||||
{
|
||||
key: 'arrivalDate',
|
||||
label: 'Arrival Date',
|
||||
render: (ticket: any) => (
|
||||
<span className="text-sm">{ticket.schedule?.arrivalAt ? new Date(ticket.schedule.arrivalAt).toLocaleDateString() : '—'}</span>
|
||||
),
|
||||
render: (ticket: any) => {
|
||||
const isRoundTrip = ticket.booking?.bookingType === 'ROUND_TRIP' || ticket.booking?.bookingType === 'ROUND_TRIP_TRANSIT';
|
||||
const outboundArrival = ticket.schedule?.arrivalAt;
|
||||
const returnArrival = ticket.booking?.returnSchedule?.arrivalAt;
|
||||
|
||||
if (!isRoundTrip) {
|
||||
return (
|
||||
<span className="text-sm">
|
||||
{outboundArrival ? new Date(outboundArrival).toLocaleDateString() : '—'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5 text-sm">
|
||||
<span>➡ {outboundArrival ? new Date(outboundArrival).toLocaleDateString() : '—'}</span>
|
||||
<span>⬅ {returnArrival ? new Date(returnArrival).toLocaleDateString() : '—'}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'boardingTimes',
|
||||
|
||||
Reference in New Issue
Block a user