mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 17:38:12 +00:00
@@ -8,6 +8,10 @@ export class LogExcessBaggageDto {
|
|||||||
@IsOptional() @IsString() bookingReference?: string;
|
@IsOptional() @IsString() bookingReference?: string;
|
||||||
@ApiPropertyOptional({ example: 'agent-uuid', description: 'Injected from IAM token; optional override' })
|
@ApiPropertyOptional({ example: 'agent-uuid', description: 'Injected from IAM token; optional override' })
|
||||||
@IsOptional() @IsString() agentId?: string;
|
@IsOptional() @IsString() agentId?: string;
|
||||||
|
@ApiPropertyOptional({ example: '+251911223344', description: 'Override the phone the payment link SMS should go to. Defaults to the booking contact phone.' })
|
||||||
|
@IsOptional() @IsString() contactPhone?: string;
|
||||||
|
@ApiPropertyOptional({ example: 'passenger@example.com', description: 'Override the email the payment link should also be sent to. Defaults to the booking contact email.' })
|
||||||
|
@IsOptional() @IsString() contactEmail?: string;
|
||||||
@ApiProperty({ example: 7, description: 'Excess weight in kg above the free allowance' })
|
@ApiProperty({ example: 7, description: 'Excess weight in kg above the free allowance' })
|
||||||
@IsInt() @IsPositive() excessWeightKg: number;
|
@IsInt() @IsPositive() excessWeightKg: number;
|
||||||
@ApiPropertyOptional({ description: 'Collect cash now instead of sending a payment link' })
|
@ApiPropertyOptional({ description: 'Collect cash now instead of sending a payment link' })
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ export class ExcessBaggageService {
|
|||||||
|
|
||||||
const totalMinor = feePerKgMinor * dto.excessWeightKg;
|
const totalMinor = feePerKgMinor * dto.excessWeightKg;
|
||||||
const expiresAt = new Date(Date.now() + CHARGE_TTL_MS);
|
const expiresAt = new Date(Date.now() + CHARGE_TTL_MS);
|
||||||
const contactPhone = booking.contactPhone ?? booking.passenger?.user?.phone ?? null;
|
const contactPhone = dto.contactPhone?.trim() || (booking.contactPhone ?? booking.passenger?.user?.phone ?? null);
|
||||||
const contactEmail = booking.contactEmail ?? booking.passenger?.user?.email ?? null;
|
const contactEmail = dto.contactEmail?.trim() || (booking.contactEmail ?? booking.passenger?.user?.email ?? null);
|
||||||
|
|
||||||
const status = dto.collectCash ? 'CASH_COLLECTED' : 'PENDING';
|
const status = dto.collectCash ? 'CASH_COLLECTED' : 'PENDING';
|
||||||
const paidAt = dto.collectCash ? new Date() : null;
|
const paidAt = dto.collectCash ? new Date() : null;
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ class CreateSupplementaryChargeDto {
|
|||||||
@ApiProperty({ example: 'EDR-20240001', description: 'Booking reference number' }) @IsString() bookingRef: string;
|
@ApiProperty({ example: 'EDR-20240001', description: 'Booking reference number' }) @IsString() bookingRef: string;
|
||||||
@ApiProperty({ description: 'Amount owed in minor units (e.g. 5000 = 50 ETB)' }) @IsInt() @Min(1) amountMinor: number;
|
@ApiProperty({ description: 'Amount owed in minor units (e.g. 5000 = 50 ETB)' }) @IsInt() @Min(1) amountMinor: number;
|
||||||
@ApiProperty({ example: 'UNDERPAYMENT' }) @IsString() reason: string;
|
@ApiProperty({ example: 'UNDERPAYMENT' }) @IsString() reason: string;
|
||||||
|
@ApiPropertyOptional({ description: 'Override the phone the payment link SMS should go to. Falls back to the booking contact phone.', example: '+251911223344' }) @IsOptional() @IsString() contactPhone?: string;
|
||||||
|
@ApiPropertyOptional({ description: 'Override the email the payment link should also be sent to. Falls back to the booking contact email.', example: 'passenger@example.com' }) @IsOptional() @IsString() contactEmail?: string;
|
||||||
@ApiPropertyOptional() @IsOptional() @IsString() notes?: string;
|
@ApiPropertyOptional() @IsOptional() @IsString() notes?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export class SupplementaryChargesService {
|
|||||||
amountMinor: number;
|
amountMinor: number;
|
||||||
reason: string;
|
reason: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
contactPhone?: string;
|
||||||
|
contactEmail?: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
}) {
|
}) {
|
||||||
const booking = await this.prisma.booking.findUnique({
|
const booking = await this.prisma.booking.findUnique({
|
||||||
@@ -76,8 +78,8 @@ export class SupplementaryChargesService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const phone = booking.contactPhone ?? booking.passenger?.user?.phone ?? null;
|
const phone = dto.contactPhone?.trim() || (booking.contactPhone ?? booking.passenger?.user?.phone ?? null);
|
||||||
const email = booking.contactEmail ?? booking.passenger?.user?.email ?? null;
|
const email = dto.contactEmail?.trim() || (booking.contactEmail ?? booking.passenger?.user?.email ?? null);
|
||||||
await this.sendLink(charge, booking.bookingRef, phone, email);
|
await this.sendLink(charge, booking.bookingRef, phone, email);
|
||||||
|
|
||||||
await this.auditService.log({
|
await this.auditService.log({
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Plus, RefreshCw, Send, Trash2 } from 'lucide-react';
|
import { Plus, RefreshCw, Send, Trash2 } from 'lucide-react';
|
||||||
import DataTable from '@/components/ui/DataTable';
|
import DataTable from '@/components/ui/DataTable';
|
||||||
import Badge from '@/components/ui/Badge';
|
import Badge from '@/components/ui/Badge';
|
||||||
import ActionButton from '@/components/ui/ActionButton';
|
import ActionButton from '@/components/ui/ActionButton';
|
||||||
import Modal from '@/components/ui/Modal';
|
import Modal from '@/components/ui/Modal';
|
||||||
import { excessBaggageApi, apiClient } from '@/lib/api';
|
import { excessBaggageApi, apiClient, bookingsApi } from '@/lib/api';
|
||||||
import { formatDateTime, formatCurrency } from '@/lib/utils';
|
import { formatDateTime, formatCurrency } from '@/lib/utils';
|
||||||
import { useAuthStore } from '@/lib/auth-store';
|
import { useAuthStore } from '@/lib/auth-store';
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ export default function ExcessBaggagePage() {
|
|||||||
const [waiveReason, setWaiveReason] = useState('');
|
const [waiveReason, setWaiveReason] = useState('');
|
||||||
const [waiveError, setWaiveError] = useState<string | null>(null);
|
const [waiveError, setWaiveError] = useState<string | null>(null);
|
||||||
const [logModal, setLogModal] = useState(false);
|
const [logModal, setLogModal] = useState(false);
|
||||||
const [logForm, setLogForm] = useState({ bookingReference: '', excessWeightKg: '', collectCash: false });
|
const [logForm, setLogForm] = useState({ bookingReference: '', excessWeightKg: '', collectCash: false, paymentPhone: '' });
|
||||||
const [logError, setLogError] = useState<string | null>(null);
|
const [logError, setLogError] = useState<string | null>(null);
|
||||||
const [resendModal, setResendModal] = useState<any>(null);
|
const [resendModal, setResendModal] = useState<any>(null);
|
||||||
const [resendSuccess, setResendSuccess] = useState(false);
|
const [resendSuccess, setResendSuccess] = useState(false);
|
||||||
@@ -54,12 +54,36 @@ export default function ExcessBaggagePage() {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!logModal) return;
|
||||||
|
const bookingRef = logForm.bookingReference.trim();
|
||||||
|
if (!bookingRef) {
|
||||||
|
setLogForm((prev) => ({ ...prev, paymentPhone: '' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const response = await bookingsApi.getAll({ search: bookingRef, page: 1, pageSize: 5 });
|
||||||
|
const items = response?.items ?? [];
|
||||||
|
const match = items.find((booking: any) => booking.bookingRef?.toLowerCase() === bookingRef.toLowerCase()) ?? items[0];
|
||||||
|
if (!match) return;
|
||||||
|
const nextPhone = match.contactPhone ?? match.passenger?.user?.phone ?? '';
|
||||||
|
setLogForm((prev) => ({ ...prev, paymentPhone: prev.paymentPhone || nextPhone }));
|
||||||
|
} catch {
|
||||||
|
// Ignore lookup failures: the agent can still override the number manually.
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [logForm.bookingReference, logModal]);
|
||||||
|
|
||||||
const logMutation = useMutation({
|
const logMutation = useMutation({
|
||||||
mutationFn: (data: any) => excessBaggageApi.logCharge(data),
|
mutationFn: (data: any) => excessBaggageApi.logCharge(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['excess-baggage'] });
|
queryClient.invalidateQueries({ queryKey: ['excess-baggage'] });
|
||||||
setLogModal(false);
|
setLogModal(false);
|
||||||
setLogForm({ bookingReference: '', excessWeightKg: '', collectCash: false });
|
setLogForm({ bookingReference: '', excessWeightKg: '', collectCash: false, paymentPhone: '' });
|
||||||
setLogError(null);
|
setLogError(null);
|
||||||
},
|
},
|
||||||
onError: (e: any) => setLogError(e?.response?.data?.message || e?.message || 'Failed to log charge'),
|
onError: (e: any) => setLogError(e?.response?.data?.message || e?.message || 'Failed to log charge'),
|
||||||
@@ -178,7 +202,7 @@ export default function ExcessBaggagePage() {
|
|||||||
<h1 className="text-2xl font-bold text-foreground">Excess Lugagge</h1>
|
<h1 className="text-2xl font-bold text-foreground">Excess Lugagge</h1>
|
||||||
<p className="text-muted-foreground">Track and manage excess luggage charges at boarding</p>
|
<p className="text-muted-foreground">Track and manage excess luggage charges at boarding</p>
|
||||||
</div>
|
</div>
|
||||||
<ActionButton icon={Plus} onClick={() => { setLogModal(true); setLogError(null); setLogForm({ bookingReference: '', excessWeightKg: '', collectCash: false }); }}>
|
<ActionButton icon={Plus} onClick={() => { setLogModal(true); setLogError(null); setLogForm({ bookingReference: '', excessWeightKg: '', collectCash: false, paymentPhone: '' }); }}>
|
||||||
Log Excess Luggage
|
Log Excess Luggage
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -256,6 +280,15 @@ export default function ExcessBaggagePage() {
|
|||||||
onChange={(e) => setLogForm({ ...logForm, bookingReference: e.target.value })}
|
onChange={(e) => setLogForm({ ...logForm, bookingReference: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label">Payment SMS Phone</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
placeholder="e.g. +251911223344"
|
||||||
|
value={logForm.paymentPhone}
|
||||||
|
onChange={(e) => setLogForm({ ...logForm, paymentPhone: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="label">Excess Weight (kg)</label>
|
<label className="label">Excess Weight (kg)</label>
|
||||||
<input
|
<input
|
||||||
@@ -282,7 +315,7 @@ export default function ExcessBaggagePage() {
|
|||||||
</label>
|
</label>
|
||||||
{!logForm.collectCash && (
|
{!logForm.collectCash && (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
A payment link will be sent to the passenger's email and phone on file.
|
The payment link will be sent to the phone above and the booking's saved email when present.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -302,6 +335,7 @@ export default function ExcessBaggagePage() {
|
|||||||
bookingReference: logForm.bookingReference.trim(),
|
bookingReference: logForm.bookingReference.trim(),
|
||||||
excessWeightKg: parseInt(logForm.excessWeightKg),
|
excessWeightKg: parseInt(logForm.excessWeightKg),
|
||||||
collectCash: logForm.collectCash,
|
collectCash: logForm.collectCash,
|
||||||
|
contactPhone: logForm.paymentPhone.trim() || undefined,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { PlusCircle } from 'lucide-react';
|
import { PlusCircle } from 'lucide-react';
|
||||||
import Modal from '@/components/ui/Modal';
|
import Modal from '@/components/ui/Modal';
|
||||||
import ActionButton from '@/components/ui/ActionButton';
|
import ActionButton from '@/components/ui/ActionButton';
|
||||||
|
import { bookingsApi } from '@/lib/api';
|
||||||
import { useCreateSupplementaryCharge } from './useSupplementaryCharges';
|
import { useCreateSupplementaryCharge } from './useSupplementaryCharges';
|
||||||
|
|
||||||
const REASONS = ['UNDERPAYMENT', 'FARE_CORRECTION', 'CURRENCY_ADJUSTMENT', 'OTHER'];
|
const REASONS = ['UNDERPAYMENT', 'FARE_CORRECTION', 'CURRENCY_ADJUSTMENT', 'OTHER'];
|
||||||
@@ -14,13 +15,37 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SupplementaryChargesModal({ isOpen, onClose }: Props) {
|
export default function SupplementaryChargesModal({ isOpen, onClose }: Props) {
|
||||||
const [form, setForm] = useState({ bookingRef: '', amountEtb: '', reason: 'UNDERPAYMENT', notes: '' });
|
const [form, setForm] = useState({ bookingRef: '', amountEtb: '', reason: 'UNDERPAYMENT', notes: '', paymentPhone: '' });
|
||||||
const [formError, setFormError] = useState<string | null>(null);
|
const [formError, setFormError] = useState<string | null>(null);
|
||||||
const [createSuccess, setCreateSuccess] = useState<string | null>(null);
|
const [createSuccess, setCreateSuccess] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
const bookingRef = form.bookingRef.trim();
|
||||||
|
if (!bookingRef) {
|
||||||
|
setForm((prev) => ({ ...prev, paymentPhone: '' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const response = await bookingsApi.getAll({ search: bookingRef, page: 1, pageSize: 5 });
|
||||||
|
const items = response?.items ?? [];
|
||||||
|
const match = items.find((booking: any) => booking.bookingRef?.toLowerCase() === bookingRef.toLowerCase()) ?? items[0];
|
||||||
|
if (!match) return;
|
||||||
|
const nextPhone = match.contactPhone ?? match.passenger?.user?.phone ?? '';
|
||||||
|
setForm((prev) => ({ ...prev, paymentPhone: prev.paymentPhone || nextPhone }));
|
||||||
|
} catch {
|
||||||
|
// Ignore lookup failures here; the staff member can still type a phone override manually.
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [form.bookingRef, isOpen]);
|
||||||
|
|
||||||
const createMutation = useCreateSupplementaryCharge(() => {
|
const createMutation = useCreateSupplementaryCharge(() => {
|
||||||
setCreateSuccess('Charge created and payment link sent.');
|
setCreateSuccess('Charge created and payment link sent.');
|
||||||
setForm({ bookingRef: '', amountEtb: '', reason: 'UNDERPAYMENT', notes: '' });
|
setForm({ bookingRef: '', amountEtb: '', reason: 'UNDERPAYMENT', notes: '', paymentPhone: '' });
|
||||||
setFormError(null);
|
setFormError(null);
|
||||||
setTimeout(() => { setCreateSuccess(null); onClose(); }, 2000);
|
setTimeout(() => { setCreateSuccess(null); onClose(); }, 2000);
|
||||||
});
|
});
|
||||||
@@ -31,7 +56,13 @@ export default function SupplementaryChargesModal({ isOpen, onClose }: Props) {
|
|||||||
if (!form.bookingRef.trim()) return setFormError('Booking reference is required');
|
if (!form.bookingRef.trim()) return setFormError('Booking reference is required');
|
||||||
if (!form.amountEtb || isNaN(amountMinor) || amountMinor <= 0) return setFormError('Enter a valid amount');
|
if (!form.amountEtb || isNaN(amountMinor) || amountMinor <= 0) return setFormError('Enter a valid amount');
|
||||||
try {
|
try {
|
||||||
await createMutation.mutateAsync({ bookingRef: form.bookingRef.trim(), amountMinor, reason: form.reason, notes: form.notes || undefined });
|
await createMutation.mutateAsync({
|
||||||
|
bookingRef: form.bookingRef.trim(),
|
||||||
|
amountMinor,
|
||||||
|
reason: form.reason,
|
||||||
|
notes: form.notes || undefined,
|
||||||
|
contactPhone: form.paymentPhone.trim() || undefined,
|
||||||
|
});
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setFormError(e?.response?.data?.message ?? e?.message ?? 'Failed to create charge');
|
setFormError(e?.response?.data?.message ?? e?.message ?? 'Failed to create charge');
|
||||||
}
|
}
|
||||||
@@ -52,6 +83,10 @@ export default function SupplementaryChargesModal({ isOpen, onClose }: Props) {
|
|||||||
<label className="label">Booking Reference <span className="text-red-500">*</span></label>
|
<label className="label">Booking Reference <span className="text-red-500">*</span></label>
|
||||||
<input className="input" placeholder="e.g. EDR-20240001" value={form.bookingRef} onChange={(e) => setForm({ ...form, bookingRef: e.target.value })} />
|
<input className="input" placeholder="e.g. EDR-20240001" value={form.bookingRef} onChange={(e) => setForm({ ...form, bookingRef: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label">Payment SMS Phone</label>
|
||||||
|
<input className="input" placeholder="e.g. +251911223344" value={form.paymentPhone} onChange={(e) => setForm({ ...form, paymentPhone: e.target.value })} />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="label">Amount Owed (ETB) <span className="text-red-500">*</span></label>
|
<label className="label">Amount Owed (ETB) <span className="text-red-500">*</span></label>
|
||||||
<input className="input" type="number" min="0.01" step="0.01" placeholder="e.g. 50.00" value={form.amountEtb} onChange={(e) => setForm({ ...form, amountEtb: e.target.value })} />
|
<input className="input" type="number" min="0.01" step="0.01" placeholder="e.g. 50.00" value={form.amountEtb} onChange={(e) => setForm({ ...form, amountEtb: e.target.value })} />
|
||||||
@@ -69,7 +104,7 @@ export default function SupplementaryChargesModal({ isOpen, onClose }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
A payment link will be sent to the passenger's registered phone/email. The link expires in 72 hours.
|
The payment link sends to the phone above, falling back to the booking's saved contact details if left empty. The link expires in 72 hours.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-2 border-t border-muted">
|
<div className="flex justify-end gap-2 pt-2 border-t border-muted">
|
||||||
|
|||||||
@@ -13,8 +13,14 @@ export function useSupplementaryCharges(filters: { bookingRef?: string; status?:
|
|||||||
export function useCreateSupplementaryCharge(onSuccess: () => void) {
|
export function useCreateSupplementaryCharge(onSuccess: () => void) {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (data: { bookingRef: string; amountMinor: number; reason: string; notes?: string }) =>
|
mutationFn: (data: {
|
||||||
paymentsApi.supplementary.create(data),
|
bookingRef: string;
|
||||||
|
amountMinor: number;
|
||||||
|
reason: string;
|
||||||
|
notes?: string;
|
||||||
|
contactPhone?: string;
|
||||||
|
contactEmail?: string;
|
||||||
|
}) => paymentsApi.supplementary.create(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
qc.invalidateQueries({ queryKey: ['supplementary-charges'] });
|
qc.invalidateQueries({ queryKey: ['supplementary-charges'] });
|
||||||
onSuccess();
|
onSuccess();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { LogIn, ListCollapse, Trash2, Printer, Package } from 'lucide-react';
|
import { LogIn, ListCollapse, Trash2, Printer, Package } from 'lucide-react';
|
||||||
import { Download } from 'lucide-react';
|
import { Download } from 'lucide-react';
|
||||||
@@ -39,6 +39,7 @@ export default function TicketsPage() {
|
|||||||
const [excessTicket, setExcessTicket] = useState<any>(null);
|
const [excessTicket, setExcessTicket] = useState<any>(null);
|
||||||
const [excessKg, setExcessKg] = useState('');
|
const [excessKg, setExcessKg] = useState('');
|
||||||
const [excessCollectCash, setExcessCollectCash] = useState(false);
|
const [excessCollectCash, setExcessCollectCash] = useState(false);
|
||||||
|
const [excessPaymentPhone, setExcessPaymentPhone] = useState('');
|
||||||
const [excessError, setExcessError] = useState<string | null>(null);
|
const [excessError, setExcessError] = useState<string | null>(null);
|
||||||
const [excessResult, setExcessResult] = useState<any>(null);
|
const [excessResult, setExcessResult] = useState<any>(null);
|
||||||
|
|
||||||
@@ -163,10 +164,35 @@ export default function TicketsPage() {
|
|||||||
onError: (e: any) => setExcessError(e?.response?.data?.message || e?.message || 'Failed to log charge'),
|
onError: (e: any) => setExcessError(e?.response?.data?.message || e?.message || 'Failed to log charge'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!excessModalOpen || !excessTicket) return;
|
||||||
|
const bookingRef = excessTicket?.booking?.bookingRef ?? '';
|
||||||
|
if (!bookingRef) {
|
||||||
|
setExcessPaymentPhone('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const response = await bookingsApi.getAll({ search: bookingRef, page: 1, pageSize: 5 });
|
||||||
|
const items = response?.items ?? [];
|
||||||
|
const match = items.find((booking: any) => booking.bookingRef?.toLowerCase() === bookingRef.toLowerCase()) ?? items[0];
|
||||||
|
if (!match) return;
|
||||||
|
const nextPhone = match.contactPhone ?? match.passenger?.user?.phone ?? '';
|
||||||
|
setExcessPaymentPhone((prev) => prev || nextPhone);
|
||||||
|
} catch {
|
||||||
|
// Ignore lookup failures here; the staff member can still type a phone override manually.
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [excessModalOpen, excessTicket]);
|
||||||
|
|
||||||
const openExcessModal = (ticket: any) => {
|
const openExcessModal = (ticket: any) => {
|
||||||
setExcessTicket(ticket);
|
setExcessTicket(ticket);
|
||||||
setExcessKg('');
|
setExcessKg('');
|
||||||
setExcessCollectCash(false);
|
setExcessCollectCash(false);
|
||||||
|
setExcessPaymentPhone('');
|
||||||
setExcessError(null);
|
setExcessError(null);
|
||||||
setExcessResult(null);
|
setExcessResult(null);
|
||||||
setExcessModalOpen(true);
|
setExcessModalOpen(true);
|
||||||
@@ -179,6 +205,7 @@ export default function TicketsPage() {
|
|||||||
bookingId: excessTicket.booking?.id ?? excessTicket.bookingId,
|
bookingId: excessTicket.booking?.id ?? excessTicket.bookingId,
|
||||||
excessWeightKg: parseInt(excessKg),
|
excessWeightKg: parseInt(excessKg),
|
||||||
collectCash: excessCollectCash,
|
collectCash: excessCollectCash,
|
||||||
|
contactPhone: excessPaymentPhone.trim() || undefined,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -979,6 +1006,15 @@ export default function TicketsPage() {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label">Payment SMS Phone</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
placeholder="e.g. +251911223344"
|
||||||
|
value={excessPaymentPhone}
|
||||||
|
onChange={(e) => setExcessPaymentPhone(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<label className="flex items-center gap-3 cursor-pointer">
|
<label className="flex items-center gap-3 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
@@ -200,8 +200,14 @@ export const paymentsApi = {
|
|||||||
updateMethod: (id: string, data: any) => apiClient.patch(`/payments/methods/${id}`, data),
|
updateMethod: (id: string, data: any) => apiClient.patch(`/payments/methods/${id}`, data),
|
||||||
deleteMethod: (id: string) => apiClient.delete(`/payments/methods/${id}`),
|
deleteMethod: (id: string) => apiClient.delete(`/payments/methods/${id}`),
|
||||||
supplementary: {
|
supplementary: {
|
||||||
create: (data: { bookingRef: string; amountMinor: number; reason: string; notes?: string }) =>
|
create: (data: {
|
||||||
apiClient.post<any>('/payments/supplementary', data),
|
bookingRef: string;
|
||||||
|
amountMinor: number;
|
||||||
|
reason: string;
|
||||||
|
notes?: string;
|
||||||
|
contactPhone?: string;
|
||||||
|
contactEmail?: string;
|
||||||
|
}) => apiClient.post<any>('/payments/supplementary', data),
|
||||||
getAll: async (params?: any) => {
|
getAll: async (params?: any) => {
|
||||||
const cleanParams = Object.fromEntries(
|
const cleanParams = Object.fromEntries(
|
||||||
Object.entries(params || {}).filter(([, v]) => v !== '' && v !== undefined && v !== null)
|
Object.entries(params || {}).filter(([, v]) => v !== '' && v !== undefined && v !== null)
|
||||||
|
|||||||
Reference in New Issue
Block a user