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
|
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))
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,10 @@ export default function ScanDetails() {
|
|||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const [sortField, setSortField] = useState<'min_price' | 'destination' | 'flight_count'>('min_price');
|
const [sortField, setSortField] = useState<'min_price' | 'destination' | 'flight_count'>('min_price');
|
||||||
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user