mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 06:28:12 +00:00
Expand to show multiple passengers in payment discrepancy
This commit is contained in:
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
AlertTriangle, CheckCircle2, Download, Search,
|
||||
Loader2, PhoneCall, RefreshCw, X,
|
||||
Loader2, PhoneCall, RefreshCw, X, Users, ChevronDown, ChevronUp,
|
||||
} from 'lucide-react';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import DatePicker from '@/components/ui/DatePicker';
|
||||
@@ -18,6 +18,16 @@ interface Station {
|
||||
city: string;
|
||||
}
|
||||
|
||||
interface PassengerDetail {
|
||||
name: string;
|
||||
category: string;
|
||||
seatNumber: string | null;
|
||||
coachNumber: string | null;
|
||||
seatType: string | null;
|
||||
fareMinor: number | null;
|
||||
fareCurrency: string;
|
||||
}
|
||||
|
||||
interface DiscrepancyRow {
|
||||
pnr: string;
|
||||
passengerName: string;
|
||||
@@ -34,6 +44,8 @@ interface DiscrepancyRow {
|
||||
paidCurrency: string;
|
||||
balanceMinor: number;
|
||||
balanceCurrency: string;
|
||||
passengerCount: number;
|
||||
passengers: PassengerDetail[];
|
||||
bookingStatus?: string;
|
||||
paymentStatus?: string | null;
|
||||
}
|
||||
@@ -109,6 +121,7 @@ export default function PaymentDiscrepancyPage() {
|
||||
const [sortBy, setSortBy] = useState<'balance' | 'departure'>('balance');
|
||||
const [search, setSearch] = useState('');
|
||||
const [applied, setApplied] = useState<Applied | null>(null);
|
||||
const [expandedPnr, setExpandedPnr] = useState<string | null>(null);
|
||||
|
||||
const isSearchMode = !!(applied?.search);
|
||||
|
||||
@@ -305,7 +318,7 @@ export default function PaymentDiscrepancyPage() {
|
||||
<tr className="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/60">
|
||||
{[
|
||||
'Passenger', 'PNR', 'Booking Date', 'Route', 'Departure',
|
||||
'Seat Type', 'Actual Price', 'Paid', 'Balance', 'Phone',
|
||||
'Seat Type', 'Actual Price', 'Paid', 'Balance', 'Passengers', 'Phone',
|
||||
].map(h => (
|
||||
<th
|
||||
key={h}
|
||||
@@ -317,68 +330,133 @@ export default function PaymentDiscrepancyPage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
{rows.map((row, i) => (
|
||||
<tr
|
||||
key={row.pnr + i}
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors"
|
||||
>
|
||||
<td className="px-4 py-3 font-medium text-gray-900 dark:text-white whitespace-nowrap">
|
||||
{row.passengerName}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="font-mono text-xs bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 px-1.5 py-0.5 rounded">
|
||||
{row.pnr}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-600 dark:text-gray-400 whitespace-nowrap">
|
||||
{new Date(row.bookingDate).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap">
|
||||
<span className="text-gray-900 dark:text-white font-medium">
|
||||
{row.origin.city || row.origin.name}
|
||||
</span>
|
||||
<span className="text-gray-400 dark:text-gray-500 mx-1">→</span>
|
||||
<span className="text-gray-900 dark:text-white font-medium">
|
||||
{row.destination.city || row.destination.name}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-600 dark:text-gray-400 whitespace-nowrap">
|
||||
{new Date(row.departureAt).toLocaleString('en-US', {
|
||||
month: 'short', day: 'numeric',
|
||||
hour: 'numeric', minute: '2-digit', hour12: true,
|
||||
timeZone: 'Africa/Addis_Ababa',
|
||||
})}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="text-gray-900 dark:text-white">{row.seatType}</div>
|
||||
{row.coachNumber && (
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500">Coach {row.coachNumber}</div>
|
||||
{rows.map((row, i) => {
|
||||
const isExpanded = expandedPnr === row.pnr;
|
||||
const hasMultiple = row.passengerCount > 1;
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
key={row.pnr + i}
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors"
|
||||
>
|
||||
<td className="px-4 py-3 font-medium text-gray-900 dark:text-white whitespace-nowrap">
|
||||
{row.passengerName}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="font-mono text-xs bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 px-1.5 py-0.5 rounded">
|
||||
{row.pnr}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-600 dark:text-gray-400 whitespace-nowrap">
|
||||
{new Date(row.bookingDate).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap">
|
||||
<span className="text-gray-900 dark:text-white font-medium">
|
||||
{row.origin.city || row.origin.name}
|
||||
</span>
|
||||
<span className="text-gray-400 dark:text-gray-500 mx-1">→</span>
|
||||
<span className="text-gray-900 dark:text-white font-medium">
|
||||
{row.destination.city || row.destination.name}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-600 dark:text-gray-400 whitespace-nowrap">
|
||||
{new Date(row.departureAt).toLocaleString('en-US', {
|
||||
month: 'short', day: 'numeric',
|
||||
hour: 'numeric', minute: '2-digit', hour12: true,
|
||||
timeZone: 'Africa/Addis_Ababa',
|
||||
})}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="text-gray-900 dark:text-white">{row.seatType}</div>
|
||||
{row.coachNumber && (
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500">Coach {row.coachNumber}</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-700 dark:text-gray-300 whitespace-nowrap font-medium">
|
||||
{fmtMoney(row.actualMinor, row.actualCurrency)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-700 dark:text-gray-300 whitespace-nowrap">
|
||||
{fmtMoney(row.paidMinor, row.paidCurrency)}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap">
|
||||
<BalanceBadge row={row} />
|
||||
</td>
|
||||
{/* Passengers column */}
|
||||
<td className="px-4 py-3 whitespace-nowrap">
|
||||
{hasMultiple ? (
|
||||
<button
|
||||
onClick={() => setExpandedPnr(isExpanded ? null : row.pnr)}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-blue-50 dark:bg-blue-950/30 text-blue-700 dark:text-blue-300 border border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-950/50 transition-colors"
|
||||
>
|
||||
<Users className="w-3 h-3" />
|
||||
{row.passengerCount}
|
||||
{isExpanded ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500 flex items-center gap-1">
|
||||
<Users className="w-3 h-3" />1
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap">
|
||||
{row.phone && row.phone !== '—' ? (
|
||||
<a
|
||||
href={`tel:${row.phone}`}
|
||||
className="flex items-center gap-1.5 text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
<PhoneCall className="w-3.5 h-3.5 shrink-0" />
|
||||
{row.phone}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-gray-400 dark:text-gray-500">—</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* Expandable passenger breakdown */}
|
||||
{isExpanded && (
|
||||
<tr key={row.pnr + '-passengers'} className="bg-blue-50/50 dark:bg-blue-950/10">
|
||||
<td colSpan={11} className="px-6 py-3">
|
||||
<div className="text-xs font-semibold text-blue-700 dark:text-blue-400 uppercase tracking-wide mb-2 flex items-center gap-1.5">
|
||||
<Users className="w-3.5 h-3.5" />
|
||||
Passengers under {row.pnr}
|
||||
</div>
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-left text-gray-500 dark:text-gray-400">
|
||||
<th className="pb-1.5 pr-4 font-semibold uppercase tracking-wide">#</th>
|
||||
<th className="pb-1.5 pr-4 font-semibold uppercase tracking-wide">Name</th>
|
||||
<th className="pb-1.5 pr-4 font-semibold uppercase tracking-wide">Category</th>
|
||||
<th className="pb-1.5 pr-4 font-semibold uppercase tracking-wide">Coach</th>
|
||||
<th className="pb-1.5 pr-4 font-semibold uppercase tracking-wide">Seat</th>
|
||||
<th className="pb-1.5 pr-4 font-semibold uppercase tracking-wide">Class</th>
|
||||
<th className="pb-1.5 font-semibold uppercase tracking-wide">Fare</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-blue-100 dark:divide-blue-900/30">
|
||||
{row.passengers.map((p, pi) => (
|
||||
<tr key={pi} className="text-gray-700 dark:text-gray-300">
|
||||
<td className="py-1.5 pr-4 text-gray-400">{pi + 1}</td>
|
||||
<td className="py-1.5 pr-4 font-medium text-gray-900 dark:text-white whitespace-nowrap">{p.name}</td>
|
||||
<td className="py-1.5 pr-4 capitalize">{p.category.toLowerCase()}</td>
|
||||
<td className="py-1.5 pr-4">{p.coachNumber ?? '—'}</td>
|
||||
<td className="py-1.5 pr-4 font-mono">{p.seatNumber ?? '—'}</td>
|
||||
<td className="py-1.5 pr-4">{p.seatType ?? '—'}</td>
|
||||
<td className="py-1.5 font-medium">
|
||||
{p.fareMinor != null
|
||||
? fmtMoney(p.fareMinor, p.fareCurrency)
|
||||
: '—'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-700 dark:text-gray-300 whitespace-nowrap font-medium">
|
||||
{fmtMoney(row.actualMinor, row.actualCurrency)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-700 dark:text-gray-300 whitespace-nowrap">
|
||||
{fmtMoney(row.paidMinor, row.paidCurrency)}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap">
|
||||
<BalanceBadge row={row} />
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap">
|
||||
{row.phone && row.phone !== '—' ? (
|
||||
<a
|
||||
href={`tel:${row.phone}`}
|
||||
className="flex items-center gap-1.5 text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
<PhoneCall className="w-3.5 h-3.5 shrink-0" />
|
||||
{row.phone}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-gray-400 dark:text-gray-500">—</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user