feat: sort flights by date and add sortable date/price columns
All checks were successful
Deploy / deploy (push) Successful in 36s
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:
@@ -1827,7 +1827,7 @@ async def get_scan_flights(
|
||||
departure_time, arrival_time, price, stops
|
||||
FROM flights
|
||||
WHERE scan_id = ? AND destination = ?
|
||||
ORDER BY price ASC, date ASC
|
||||
ORDER BY date ASC, price ASC
|
||||
LIMIT ? OFFSET ?
|
||||
""", (scan_id, destination.upper(), limit, offset))
|
||||
else:
|
||||
@@ -1836,7 +1836,7 @@ async def get_scan_flights(
|
||||
departure_time, arrival_time, price, stops
|
||||
FROM flights
|
||||
WHERE scan_id = ?
|
||||
ORDER BY price ASC, date ASC
|
||||
ORDER BY date ASC, price ASC
|
||||
LIMIT ? OFFSET ?
|
||||
""", (scan_id, limit, offset))
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ export default function ScanDetails() {
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
const [expandedRoute, setExpandedRoute] = useState<string | null>(null);
|
||||
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 [rerunning, setRerunning] = 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) => {
|
||||
if (expandedRoute === destination) { setExpandedRoute(null); return; }
|
||||
setExpandedRoute(destination);
|
||||
@@ -217,6 +237,13 @@ export default function ScanDetails() {
|
||||
: <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(
|
||||
'px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider select-none',
|
||||
field
|
||||
@@ -670,15 +697,29 @@ export default function ScanDetails() {
|
||||
<table className="w-full">
|
||||
<thead className="bg-[#EEF7F0]">
|
||||
<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">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-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>
|
||||
</thead>
|
||||
<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">
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user