From cf40736f0e67b1fb0d7789db84619389d06fc129 Mon Sep 17 00:00:00 2001 From: domverse Date: Mon, 2 Mar 2026 21:38:02 +0100 Subject: [PATCH] feat: add date range option to create scan dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Rolling Window / Date Range toggle to the Parameters section. Rolling Window mode (default) keeps the existing window_months stepper. Date Range mode shows From/To date pickers and sends start_date/end_date to the API instead of window_months. Validation covers empty dates, past start dates, end ≤ start, and ranges exceeding 12 months. Co-Authored-By: Claude Sonnet 4.6 --- .../frontend/src/pages/Scans.tsx | 226 ++++++++++++++---- 1 file changed, 177 insertions(+), 49 deletions(-) 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 ( <>
@@ -331,54 +372,141 @@ export default function Scans() { Parameters

-
- {/* 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 */}