Expand to show multiple passengers in payment discrepancy

This commit is contained in:
Roba Boru
2026-07-20 16:10:28 +03:00
parent 6ea2216aeb
commit ef2fb2e05c
2 changed files with 191 additions and 81 deletions

View File

@@ -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>