diff --git a/flight-comparator/frontend/src/pages/Scans.tsx b/flight-comparator/frontend/src/pages/Scans.tsx
index 5ec9d5b..ffe7c32 100644
--- a/flight-comparator/frontend/src/pages/Scans.tsx
+++ b/flight-comparator/frontend/src/pages/Scans.tsx
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
-import { Globe, PlaneTakeoff, Minus, Plus, ArrowRight, ArrowLeft } from 'lucide-react';
+import { Globe, PlaneTakeoff, Minus, Plus, ArrowRight, ArrowLeft, CalendarDays, CalendarRange } from 'lucide-react';
import { scanApi } from '../api';
import type { CreateScanRequest } from '../api';
import AirportSearch from '../components/AirportSearch';
@@ -16,6 +16,8 @@ interface FormErrors {
airports?: string;
window_months?: string;
adults?: string;
+ start_date?: string;
+ end_date?: string;
}
export default function Scans() {
@@ -34,6 +36,11 @@ export default function Scans() {
adults: 1,
});
+ // Window mode: rolling N months or specific date range
+ const [windowMode, setWindowMode] = useState<'window' | 'range'>('window');
+ const [startDate, setStartDate] = useState('');
+ const [endDate, setEndDate] = useState('');
+
// Shared state
const [selectedAirports, setSelectedAirports] = useState([]);
const [selectedOriginCountry, setSelectedOriginCountry] = useState('');
@@ -64,6 +71,26 @@ export default function Scans() {
}
}
+ if (windowMode === 'range') {
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ if (!startDate) {
+ next.start_date = 'Select a start date';
+ } else if (new Date(startDate) <= today) {
+ next.start_date = 'Start date must be in the future';
+ }
+ if (!endDate) {
+ next.end_date = 'Select an end date';
+ } else if (startDate && endDate <= startDate) {
+ next.end_date = 'End date must be after start date';
+ } else if (startDate && endDate) {
+ const s = new Date(startDate);
+ const e = new Date(endDate);
+ const months = (e.getFullYear() - s.getFullYear()) * 12 + (e.getMonth() - s.getMonth());
+ if (months > 12) next.end_date = 'Date range cannot exceed 12 months';
+ }
+ }
+
setErrors(next);
return Object.keys(next).length === 0;
};
@@ -76,22 +103,26 @@ export default function Scans() {
try {
let requestData: CreateScanRequest;
+ const windowParams = windowMode === 'range'
+ ? { start_date: startDate, end_date: endDate }
+ : { window_months: formData.window_months };
+
if (scanMode === 'reverse') {
requestData = {
scan_mode: 'reverse',
origin: selectedOriginCountry,
destinations: selectedAirports,
- window_months: formData.window_months,
seat_class: formData.seat_class,
adults: formData.adults,
+ ...windowParams,
};
} else {
requestData = {
scan_mode: 'forward',
origin: formData.origin,
- window_months: formData.window_months,
seat_class: formData.seat_class,
adults: formData.adults,
+ ...windowParams,
...(destinationMode === 'country'
? { country: formData.country }
: { destinations: selectedAirports }),
@@ -124,6 +155,16 @@ export default function Scans() {
? 'border-error focus:border-error focus:ring-2 focus:ring-error/20'
: 'border-outline focus:border-primary focus:ring-2 focus:ring-primary/20');
+ const tomorrowStr = (() => {
+ const d = new Date();
+ d.setDate(d.getDate() + 1);
+ return d.toISOString().split('T')[0];
+ })();
+
+ const minEndDate = startDate
+ ? (() => { const d = new Date(startDate); d.setDate(d.getDate() + 1); return d.toISOString().split('T')[0]; })()
+ : tomorrowStr;
+
return (
<>
-
- {/* Search Window */}
-
-
-
-
-
- {formData.window_months} {formData.window_months === 1 ? 'month' : 'months'}
-
-
-
-
Months to look ahead (1–12)
-
-
- {/* Seat Class */}
-
-
-
-
+ {/* Search Window toggle */}
+
+
+ {
+ setWindowMode(v as 'window' | 'range');
+ setStartDate('');
+ setEndDate('');
+ setErrors(prev => ({ ...prev, start_date: undefined, end_date: undefined }));
+ }}
+ />
+ {windowMode === 'window' ? (
+
+ {/* Stepper */}
+
+
+
+
+ {formData.window_months} {formData.window_months === 1 ? 'month' : 'months'}
+
+
+
+
Months to look ahead (1–12)
+
+
+ {/* Seat Class */}
+
+
+
+
+
+ ) : (
+ <>
+ {/* Date pickers */}
+
+
+
+
{
+ setStartDate(e.target.value);
+ if (errors.start_date) setErrors(prev => ({ ...prev, start_date: undefined }));
+ }}
+ className={inputCls(!!errors.start_date)}
+ />
+ {errors.start_date ? (
+
{errors.start_date}
+ ) : (
+
First date to scan
+ )}
+
+
+
+
{
+ setEndDate(e.target.value);
+ if (errors.end_date) setErrors(prev => ({ ...prev, end_date: undefined }));
+ }}
+ className={inputCls(!!errors.end_date)}
+ />
+ {errors.end_date ? (
+
{errors.end_date}
+ ) : (
+
Last date to scan
+ )}
+
+
+
+ Flights will be sampled on the 15th of each month within this range
+
+
+ {/* Seat Class */}
+
+
+
+
+
+
+ >
+ )}
+
{/* Adults — full width below */}