feat: sort flights by date and add sortable date/price columns
All checks were successful
Deploy / deploy (push) Successful in 36s

- Change API ORDER BY from price/date to date/price (chronological default)
- Add flightSortField/flightSortDir state to ScanDetails
- Make Date and Price sub-table headers clickable with sort icons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 15:27:21 +01:00
parent 3cad8a8447
commit 69c2ddae29
2 changed files with 48 additions and 7 deletions

View File

@@ -1827,7 +1827,7 @@ async def get_scan_flights(
departure_time, arrival_time, price, stops departure_time, arrival_time, price, stops
FROM flights FROM flights
WHERE scan_id = ? AND destination = ? WHERE scan_id = ? AND destination = ?
ORDER BY price ASC, date ASC ORDER BY date ASC, price ASC
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
""", (scan_id, destination.upper(), limit, offset)) """, (scan_id, destination.upper(), limit, offset))
else: else:
@@ -1836,7 +1836,7 @@ async def get_scan_flights(
departure_time, arrival_time, price, stops departure_time, arrival_time, price, stops
FROM flights FROM flights
WHERE scan_id = ? WHERE scan_id = ?
ORDER BY price ASC, date ASC ORDER BY date ASC, price ASC
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
""", (scan_id, limit, offset)) """, (scan_id, limit, offset))

View File

@@ -54,6 +54,8 @@ export default function ScanDetails() {
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
const [expandedRoute, setExpandedRoute] = useState<string | null>(null); const [expandedRoute, setExpandedRoute] = useState<string | null>(null);
const [flightsByDest, setFlightsByDest] = useState<Record<string, Flight[]>>({}); const [flightsByDest, setFlightsByDest] = useState<Record<string, Flight[]>>({});
const [flightSortField, setFlightSortField] = useState<'date' | 'price'>('date');
const [flightSortDir, setFlightSortDir] = useState<'asc' | 'desc'>('asc');
const [loadingFlights, setLoadingFlights] = useState<string | null>(null); const [loadingFlights, setLoadingFlights] = useState<string | null>(null);
const [rerunning, setRerunning] = useState(false); const [rerunning, setRerunning] = useState(false);
const [confirmDelete, setConfirmDelete] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false);
@@ -115,6 +117,24 @@ export default function ScanDetails() {
} }
}; };
const handleFlightSort = (field: 'date' | 'price') => {
if (flightSortField === field) {
setFlightSortDir(d => d === 'asc' ? 'desc' : 'asc');
} else {
setFlightSortField(field);
setFlightSortDir('asc');
}
};
const sortedFlights = (flights: Flight[]) =>
[...flights].sort((a, b) => {
const aVal = flightSortField === 'date' ? a.date : (a.price ?? Infinity);
const bVal = flightSortField === 'date' ? b.date : (b.price ?? Infinity);
if (aVal < bVal) return flightSortDir === 'asc' ? -1 : 1;
if (aVal > bVal) return flightSortDir === 'asc' ? 1 : -1;
return 0;
});
const toggleFlights = async (destination: string) => { const toggleFlights = async (destination: string) => {
if (expandedRoute === destination) { setExpandedRoute(null); return; } if (expandedRoute === destination) { setExpandedRoute(null); return; }
setExpandedRoute(destination); setExpandedRoute(destination);
@@ -217,6 +237,13 @@ export default function ScanDetails() {
: <ChevronDown size={14} className="text-primary" />; : <ChevronDown size={14} className="text-primary" />;
}; };
const FlightSortIcon = ({ field }: { field: 'date' | 'price' }) => {
if (flightSortField !== field) return <ChevronUp size={12} className="opacity-30" />;
return flightSortDir === 'asc'
? <ChevronUp size={12} className="text-secondary" />
: <ChevronDown size={12} className="text-secondary" />;
};
const thCls = (field?: typeof sortField) => cn( const thCls = (field?: typeof sortField) => cn(
'px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider select-none', 'px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider select-none',
field field
@@ -670,15 +697,29 @@ export default function ScanDetails() {
<table className="w-full"> <table className="w-full">
<thead className="bg-[#EEF7F0]"> <thead className="bg-[#EEF7F0]">
<tr> <tr>
<th className="pl-12 pr-4 py-2 text-left text-xs font-semibold uppercase tracking-wider text-on-surface-variant">Date</th> <th
className="pl-12 pr-4 py-2 text-left text-xs font-semibold uppercase tracking-wider select-none cursor-pointer hover:bg-[#D4EDDA] transition-colors"
onClick={() => handleFlightSort('date')}
>
<span className={cn('inline-flex items-center gap-1', flightSortField === 'date' ? 'text-secondary' : 'text-on-surface-variant')}>
Date <FlightSortIcon field="date" />
</span>
</th>
<th className="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wider text-on-surface-variant">Airline</th> <th className="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wider text-on-surface-variant">Airline</th>
<th className="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wider text-on-surface-variant">Departure</th> <th className="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wider text-on-surface-variant">Departure</th>
<th className="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wider text-on-surface-variant">Arrival</th> <th className="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wider text-on-surface-variant">Arrival</th>
<th className="px-4 py-2 text-right text-xs font-semibold uppercase tracking-wider text-on-surface-variant">Price</th> <th
className="px-4 py-2 text-right text-xs font-semibold uppercase tracking-wider select-none cursor-pointer hover:bg-[#D4EDDA] transition-colors"
onClick={() => handleFlightSort('price')}
>
<span className={cn('inline-flex items-center justify-end gap-1', flightSortField === 'price' ? 'text-secondary' : 'text-on-surface-variant')}>
Price <FlightSortIcon field="price" />
</span>
</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-[#D4EDDA]"> <tbody className="divide-y divide-[#D4EDDA]">
{(flightsByDest[route.destination] || []).map((f) => ( {sortedFlights(flightsByDest[route.destination] || []).map((f) => (
<tr key={f.id} className="hover:bg-[#EEF7F0] transition-colors"> <tr key={f.id} className="hover:bg-[#EEF7F0] transition-colors">
<td className="pl-12 pr-4 py-2.5 text-sm text-on-surface"> <td className="pl-12 pr-4 py-2.5 text-sm text-on-surface">
<span className="font-mono text-xs font-semibold text-on-surface-variant mr-2">{weekday(f.date)}</span> <span className="font-mono text-xs font-semibold text-on-surface-variant mr-2">{weekday(f.date)}</span>