# Destina Holidays Flights Results Redesign: Session Context

Companion to `hotels-activities-redesign-context.md`. Read that document first for repositories, design tokens (`src/brand.scss`), caching, payments and the general verification approach; this document only records what is specific to the flights results redesign.

## 1. Purpose and scope

The flights search results page (`/compare-flights`) in `homepage-ssr-app` was rebuilt to match the Trip.com / Viator style already applied to hotels and activities. Scope covered the desktop and mobile results templates, the shared results component logic, filters, the inline search-edit band, passenger and date editing, multi-city editing on mobile, and a set of behavioural fixes requested during the session. No backend (`process.php`) changes were made.

Git (`homepage-ssr-app`, branch as of the session):

| Commit | Summary |
| --- | --- |
| `adaa13c` refactored flights | Full template/SCSS rewrite for desktop and mobile, base component helpers, guided search chain |
| `2d25d1a` fixed flights on mobile | Filters inclusion model, mobile sheet fixes, pins, tooltips, swipe-to-close, multi-city on mobile, debug block, seats badge, "View itinerary" |
| `4c3945e` fixed flights | Pax modal auto-search, sort tiles single row, depart -> return picker chain with auto-refresh, staging flag fix |

Working tree was clean after `4c3945e`.

## 2. Architecture

### 2.1 Components (`src/app/flights`)

| Component | Role |
| --- | --- |
| `results/results.component.ts` | Route host. Picks desktop or mobile via `dataService.isMobileView()` (changed from the old UA-only check). |
| `flight-results/flight-results.component.ts` | Base class. All state, API calls, filtering, sorting, helper methods. Template is empty; never rendered directly. |
| `flight-results-desktop/*` | `FlightResultsDesktopComponent extends FlightResultsComponent`. Desktop template and SCSS only. |
| `flight-results-mobile/*` | `FlightResultsMobileComponent extends FlightResultsComponent`. Mobile template and SCSS only. |
| `flight-autosuggest` (shared, `src/app/flight-autosuggest`) | Airport autosuggest. Renders inline dropdown on desktop and a full-screen `.side-bar` panel on mobile. Mode is decided by `data.isMobileDevice()` (user agent), not viewport. |
| `mobile-calendar` (shared, `src/app/mobile-calendar`) | Full-screen date picker for mobile. Single date when `tripInput` is not `Round Trip`, range otherwise. `MobileCalendarModule` is now imported in `flights.module.ts`. |

Rule kept from the original code: logic lives in the base class, templates in the subclasses. When adding a helper used by both templates, add it to `flight-results.component.ts`.

### 2.2 Data flow

- Query params (`from`, `to`, `depart`, `adult`, `child`, `infant`, `cabin`, `username`, `track_id`, optional `debug`, `staging`, `forcendc`, `clearcache`) are parsed in the route subscription (~line 3100-3220). Multi-city is expressed as comma-separated `from`/`to`/`depart` lists; `flightmulti` is rebuilt from those on every navigation.
- `modifySearch()` (round trip / one way) and `flightSearchResultsMultiCity()` (multi-city) build params and call `router.navigate(['compare-flights'], { queryParams })`. The route subscription then re-runs the search. Both clear `searchOpen` and `showPaxOptions` first.
- Results come from Skybooker via `process.php`. Localhost gets `403 Domain not allowed`; see section 7 for how testing was done.
- `stagingEnvironment` is now `true` when the hostname is `localhost`/`127.0.0.1` or `?staging=true` (a real boolean; previously the raw string was passed through so `staging=false` was truthy). Guarded with `isPlatformBrowser`.

### 2.3 Key state on the base component

| Field | Meaning |
| --- | --- |
| `itineraries`, `displayFlights`, `directFlights` | All results, currently visible results, direct-only subset |
| `tripType` | `'Round Trip' | 'One Way' | 'Multi City'` |
| `editOrigin`, `editDestination`, `editDates[0..1]`, `editAdult`, `editChild`, `editInfant`, `editCabin` | Values being edited in the band |
| `origin`, `destination`, `depart`, `return`, `adult`, `child`, `infant`, `cabin` | Values of the search currently shown (used by `paxChanged()` / `datesChanged()`) |
| `flightmulti[]` | `{ from, to, depart }` per leg; `from`/`to` may be an IATA string or an autosuggest object with `.iata` |
| `flightDates` | Array handed to `mobile-calendar` |
| `searchOpen` | Mobile: search-edit panel expanded |
| `showPaxOptions`, `guided`, `guidedNext` | Passenger dialog visible; guided-chain flags |
| `multiDateIndex`, `mobileMinDate` | Which multi-city leg the mobile calendar is editing and its lower bound |
| `departureFilter`, `returningFilter`, `airlineAlliances`, `airlinePrices`, `layoverAirports` | Filter option lists, each item has `checked` |
| `segmentFilterValue[]` | Pinned segment id per journey index |
| `sortLabels` | `['Cheapest','Shortest','Earliest departure','Earliest arrival','Latest departure','Latest arrival']` |
| `debug` | Shows the per-card debug block |

## 3. Feature summary and decisions

### 3.1 Search-edit band (desktop)

- Segment control `Round trip / One way / Multi-city`, travellers-and-cabin button, `Preferred airlines`, then the row From / swap / To / Depart / Return / Search.
- Guided chain when the user changes origin: origin -> destination autosuggest opens -> depart picker -> return picker -> passenger dialog -> search. Implemented with `guided`, `guidedNext`, `@ViewChild` refs `autosuggestTo`, `departPicker`, `returnPicker` and `onPickerHidden()`.
- Non-guided date editing (added in `4c3945e`): picking a depart date always opens the Return picker (`onDepartChange`), bumping return by 7 days only if it is before the new depart. When a picker closes with no next step pending, `onPickerHidden()` calls `modifySearch()` if `datesChanged()`.
- `< >` day arrows (`beforeDate`/`nextDate`) are debounced 900 ms via `queueDateSearch()`; depart cannot go before today, return cannot go before depart, moving depart past return drags return along.
- Multi-city block: per-leg From / To / Depart with `bsDatepicker` (`minDate` = previous leg), remove button, `Add flight` (max 5), Search.
- `autosuggestIata(code)` is memoised so `[defaultlocation]` bindings do not create new objects on each change detection.

### 3.2 Passenger and cabin dialog

- Steppers for adults (1-9), children (0-6), infants (0-5, never more than adults) and cabin pills.
- `applyPax()` and `closePax()` share one path: close, then if `guided` or `paxChanged()` run `modifySearch()` (or `flightSearchResultsMultiCity()` for multi-city). Closing without changes does nothing. `closePax()` is wired to the X button, the backdrop and the mobile swipe-down.

### 3.3 Sort tiles and results bar (desktop)

- Four tiles in one row at all desktop widths: Cheapest (price), Shortest (`shortestLabel()`), Earliest departure, More sorts (dropdown of `sortLabels`). Labels and values truncate with an ellipsis; padding/font shrink below 1200 px. The old `< 1200px -> 2x2 grid` breakpoint was removed.
- Results bar shows `N of M flights` and a spinner while `!pollingComplete`.

### 3.4 Flight card (desktop)

- Left: one row per journey with carrier logo, flight numbers, `Depart · Wed, 13th Jan` badge (`f-when`, tooltip via `segmentTip`), departure time/airport, duration (`f-dur`, tooltip via `durationTip`) with stops line, arrival time/airport, `+1` superscript (tooltip via `nextDayTip`), and a pin button.
- Right (`f-side`): `9 seats left` badge at top right (`f-seats`, only when the API reports it), per-traveller price, total, `View itinerary` button (renamed from `Select`; it opens the detail drawer, it does not go to checkout).
- Footer tags: cabin, baggage (`baggageLabel`).
- Debug block (`debug-info`, when `?debug`): `<dl class="dbg-grid">` with Fare (RBD, cabin, basis, rule), source, validating carrier, references as wrapped `<code>` blocks.
- Times: `.f-pt > span` (direct child only). The original `.f-pt span` also matched the `<span>` inside `<strong>` and shrank the arrival time.
- Carrier logos from the CDN; `MC.png` (multi-carrier) also from the CDN because it is not in `src/assets`.

### 3.5 Pins ("only show combinations with this leg")

- `isPinned(flight, s)` compares `segmentFilterValue[s]` with `flight.skybooker.segmentId[s]`.
- `togglePin(flight, s, e)` calls the existing `applyFilters('segmentFilter', ...)`; pin buttons exist on both desktop (`f-pin`) and mobile (`m-pin`) legs.

### 3.6 Filters (inclusion model)

- All checkboxes start unchecked and all results show. Checking options restricts results to those options; a group with nothing checked does not filter. This replaced the original exclusion model where everything was checked and unchecking hid results.
- Defaults changed to `checked: false` for `departureFilter`, `returningFilter`, `airlineAlliances`, `airlinePrices`; API-returned `layoverAirports` are normalised to `checked: false` on load.
- `processFilters` has a `"checks"` branch that recomputes `departFilterHide`, `returningFilterHide`, `airlineFilterHide`, `layoverAirportHide` from the currently checked items. Time parsing uses `HHmm` (was `HHMM`, which is month).
- Handlers: `toggleCheck(item)` for checkboxes, `onlyCheck(list, item)` for the `Only` buttons. `toggleTime` was removed.
- `activeFilterCount()` counts groups with at least one checked option; `resetFilters()`/`processResetFilters()` set everything back to unchecked and delete the hide flags (including `returningFilterHide`).
- Time buckets are ordered `0,500 / 500,1200 / 1200,1800 / 1800,2359` (the original array order was scrambled).

### 3.7 Mobile results

- Sticky band with a summary button (`originLabel() -> destinationLabel()`, dates, pax, cabin; multi-city shows the full chain via `multiRoute()` and first-last dates via `summaryDates()`) that calls `openSearch()`.
- Edit search is a bottom sheet (`.sheet-wrap.search > .sheet.tall.m-search`, `max-height: 94vh`), not an inline panel, so multi-city legs have room. Same anatomy as the filter sheet: backdrop, swipe grip, `Edit search` header with close, scrollable `.sheet-body.m-search-body` (segment control, From / swap / To, dates, travellers, errors), fixed `.sheet-foot` with a single `Search flights` button calling `modifySearch()` (which delegates to `flightSearchResultsMultiCity()` for multi-city).
- `openSearch()` sets `searchOpen`, clears `editError`/`flightMultiError` and locks body scroll; `closeSearch()` unlocks. It does not push history state (unlike filters) so it never interferes with the router navigation the Search button triggers. `modifySearch()` only closes the sheet on a valid search; validation errors keep it open so the message is visible.
- Child panels (`flight-autosuggest`, `mobile-calendar`) reset `body.overflow` to `auto` when they close. `relockBody()` re-applies `hidden` while the sheet is open and is called from `addFlightFrom`, `addFlightTo`, `processMobileDates` and the multi-city leg `(selected)` handlers. As a CSS backstop `.sheet-backdrop { touch-action: none }` and `.sheet-body { overscroll-behavior: contain }` block scroll-through behind any sheet.
- Stacking: sheets are `z-index: 1200`, pax dialog `1250`, and the autosuggest / calendar `.side-bar` panels are raised to `1300` via `::ng-deep` so they open above the edit sheet.
- Results count shows the page range: `Showing {{ resultRange() }} of N flights` (`resultRange()` clamps the page and returns `start–end`), on both mobile `.m-count` and the desktop results bar.
- Multi-city on mobile (no longer redirects to the homepage): per-leg card with From / To / Depart, remove, `Add another flight` (max 5), travellers, Search. `openMultiDate(i)` reuses the single `mobile-calendar` in single-date mode with heading `Flight N date` and `[disableupto]="mobileMinDate"` (previous leg date minus one day so same-day connections are allowed). `processMobileDates` writes back to `flightmulti[i].depart` and clears later legs that are now before it.
- Chips row: Sort, Filters (with count badge), Direct, Airlines, Times, Clear.
- Filter and sort sheets and the detail sheet are bottom sheets driven by `openFilters(type)` with `pathLocationStrategy.pushState` so the hardware back button closes them.
- Detail sheet (`.sheet.full`) is `position: absolute; inset: 0` with `padding-top: env(safe-area-inset-top)` on `.d-head`; the previous `height: 100vh` cropped the header on iOS Safari.
- Swipe-down to close: `.sheet-grip` wraps the handle and binds `touchstart/touchmove/touchend` to `sheetDragStart/Move/End`. Drag translates the sheet; past the threshold `sheetDragEnd` returns `true` and the template calls the close handler, otherwise it snaps back (`transition: transform 0.2s`).
- Tooltips on mobile use `triggers="click"`.
- Autosuggest overrides in `.m-field-dest ::ng-deep` are scoped to the inline trigger only (`.autosuggest > input.styled-input`, `.autosuggest > .options`, `#ismobile .autosuggest > .styled-input`). Earlier unscoped rules leaked into the full-screen `.side-bar` and cut the input and IATA codes off. The heading `<small>` is hidden with `small.fw-light:not(.text-muted)` so the `Select »` placeholder still shows; flag images with no country (`src$="undefined.png"`) are hidden.

### 3.8 Multi-city search validation

- `flightSearchResultsMultiCity()` resets `flightMultiError`, normalises `from`/`to` objects to IATA, marks the error if any leg is incomplete, and now returns before navigating when the error is set. Previously the `return` inside `forEach` only skipped the bad leg and the search ran with the remaining legs (desktop and mobile).
- It also calls `closeSearch()` on success.

### 3.9 Shared `mobile-calendar`: continue with current dates

- `src/app/mobile-calendar` is shared by flights, hotels, activities, the homepage and search boxes. When it opens with dates already selected (typical after only changing a location in the guided flow) the user previously had to re-tap both dates.
- Added a sticky footer `.keep-nav > .keep-btn` (`Continue with these dates` plus the dates it will keep). `canKeepDates()` is true when the panel is open, `firstClick` is still true (no half-finished range), the start date is valid and, for range mode (`isRange()`: round trip / hotel / activity), the end date is valid. `keepDates()` emits `selected` with the current dates and calls `hideMobileSidebar()`, so every host's existing `(selected)` handler runs as if the dates were re-tapped.
- The button hides after the first tap of a new range (the old end date would be stale) and reappears on the next open. The X still dismisses without emitting.
- On flights this continues the guided flow: `processMobileDates` -> pax sheet when `guided`, or a refresh only if `datesChanged()`.

### 3.10 Autosuggest swap and location refresh

- `FlightAutoSuggestComponent` implements `ngOnChanges` on `initialLocation` (`[defaultlocation]`): when the IATA changes it replaces `modifyLocation` and clears suggestions. Before this the input was read once, so `swapAirports()` and programmatic updates never showed in the fields.
- `originAirport` / `destinationAirport` are reset at the top of the route-param parsing so the band summary does not keep the previous city names after a new search.
- Mobile autosuggest side panel restyled with the hotel `sg-panel` structure (`sg-head`, `sg-ico`, `sg-text`, `sg-name`, `sg-sub`, `sg-meta`, `sg-child`) in `flight-autosuggest.component.html/scss`.

### 3.11 Scroll to top

- `scrollToTop()` uses `window.scrollTo({ behavior: 'instant' })` (global smooth scroll was being interrupted by re-render) and is called from `pageChanged`, `applyFilters`, `resetFilters`, `selectSort`, `switchResults`, resetting `page = 1` where appropriate.

### 3.12 Flight checkout review page (separate repo `ng-checkout-github`, Angular 11)

- `src/app/review/review.component.{html,scss,ts}` rewritten to the same design language; `src/brand.scss` copied from the homepage app and applied via `:host { @include brand-tokens }` so the palette is scoped to the component (the checkout otherwise uses per-domain `--primary`/`--alternate` from `dataService.setPalette`). `review-desktop` is not routed and was left untouched.
- Single responsive template (media queries at 1199 / 991 / 575 px) replaces the duplicated desktop and mobile trees. IDs used by the TS are kept: `bgheader` (upsell scroll target), `upsell-boxes` (arrow scrolling), `mobile-payment-details` (contact card; `gotoBookingPage()` validates billing name and email only when `getClientRects().length == 1`, i.e. when the card is visible, which is mobile only).
- Structure: gradient hero with 3-step indicator and trip pill; per-segment `f-card` (carrier, flight numbers, seats-left and Depart/Return badges, route with stops track, `+1` tooltip, fare/bag tags, airport-change warning, collapsible details with layovers, upgrade nudge); Bags card; horizontal upsell offers (`isSelectedOffer`, `upgradeCabinClass`); sticky side column with price summary, contact card (mobile), trust card; fixed `m-pricebar` on mobile; restyled fare-expired modal.
- Helpers added to the TS: `firstLeg`, `lastLeg`, `legLabel`, `tripTypeLabel`, `stopsLabel`, `segmentDays`, `flightNumbers`, `carrierLogo`, `fareName`, `cabinName`, `checkedBags`, `bagLabel`, `carryOnQty`, `paxLabel`, `money`, `isSelectedOffer`. `segment.showDetails` is collapsed by default on all widths.
- Local testing: `ng serve --port 4300` (log `/tmp/ngcheckout.log`); the fare API rejects localhost, so `/tmp/review-mock.js` builds a round-trip `fareData` with a connection, airport change, 3 upsell offers and 6 seats left, stubs `initComponent.cacheExpired`/`router`, and calls `setDataService.setFareData(...)` after navigating to `/TESTREF/review`. The component has no `cdr`; use `ng.applyChanges(c)` after mutating state from the console.

## 4. Design notes specific to flights

- Uses `@include brand-tokens` and the same navy/violet/accent palette as hotels. Desktop card grid: `minmax(0, 1fr) 190px` (below 1200 px) / wider side column above.
- Badges (`f-when`, `m-when`) carry an info icon and `cursor: help`; `+1` superscripts use a dotted underline.
- `::ng-deep .tooltip` restyled to the brand (dark navy, 12 px radius).
- Affirm: `affirm.ui.refresh()` after results render; plain class/attribute usage because the directive scoping did not work inside the results templates.

## 5. Notable bugs fixed and root causes

| Symptom | Root cause | Fix |
| --- | --- | --- |
| Arrival time smaller than departure time | `.f-pt span` matched nested `<span>` in `<strong>` | `.f-pt > span` (`.m-pt > span` on mobile) |
| Mobile detail sheet header/back button cropped on iOS | `height: 100vh` ignores the Safari toolbar | `position: absolute; inset: 0` + `env(safe-area-inset-top)` |
| Mobile "Flying from" panel input cut off, IATA codes off-screen | `::ng-deep` overrides for inline input/options applied inside `.side-bar` | Scoped selectors to direct children of `.autosuggest` |
| Empty multi-city leg showed nothing in From/To | `small.fw-light` rule also hid the `Select »` placeholder | `:not(.text-muted)` |
| Multi-city search ran with missing legs | `return` inside `forEach` | Early return after the loop when `flightMultiError` |
| Filters hid results when nothing was intended | Exclusion model with everything checked by default | Inclusion model (section 3.6) |
| Time filters matched wrong values | `moment().format("HHMM")` (month, not minutes) | `HHmm` |
| `staging=false` treated as staging | Raw string passed through | Boolean from hostname or `staging=true` |
| Sort tiles wrapped to 2x2 below 1200 px | Media query | Removed; ellipsis + tighter padding |
| Mobile trip-type switch to Multi-city left the page | Link to `/#flights` | In-place multi-city editor |
| Swap (`<->`) button did nothing visible | Autosuggest read `defaultlocation` once | `ngOnChanges` in `FlightAutoSuggestComponent` |
| Summary kept old city after a new search | `originAirport`/`destinationAirport` never reset | Cleared in route-param parsing |
| Next page / filters did not scroll to top | `#pagination-top` missing; smooth scroll interrupted | `scrollToTop()` with `behavior: 'instant'` |
| Inline edit panel too cramped for multi-city on mobile | Panel lived inside the sticky band | Bottom sheet with sticky Search footer |
| Page behind edit sheet scrollable after autosuggest/calendar closed | Child panels set `body.overflow = auto` | `relockBody()` + `touch-action`/`overscroll-behavior` |
| Had to re-tap unchanged dates after changing a location | Calendar only emitted on a second tap | `Continue with these dates` footer in `mobile-calendar` |

## 6. Known issues and follow-ups not done

- `flight-autosuggest` chooses mobile/desktop by user agent while `results.component.ts` uses viewport width; a narrow desktop window gets the mobile results template with the desktop (inline) autosuggest. Works, but the two checks could be unified.
- The mobile summary for multi-city while a leg is incomplete falls back to the first leg date only (by design, `summaryDates()` needs valid first and last dates).
- Trip-type changes alone (e.g. Round trip -> One way) do not auto-search; the user still presses Search. Only pax and date changes auto-refresh.
- `flightSearchResultsMultiCity()` still scrolls to `.modify-trip` on error; that class no longer exists in the new templates (harmless).
- Old `.spec.ts` files were not updated for the new templates.
- The mobile edit sheet is not closed by the hardware back button (no `pushState`, by design); filters and the detail sheet are.
- If the autosuggest or calendar is dismissed with its own X (no selection) while the edit sheet is open, body scroll is only protected by the CSS backstop, not re-locked.

## 7. Verification approach

- Dev server: `ng serve` on `http://127.0.0.1:4200` (use the IPv4 address; `localhost` resolved to another project's IPv6 server on this machine). Build log tailed from `/tmp/ngserve.log`.
- API from localhost returns `403 Domain not allowed`. Workaround used during the session: fetch a real payload with `curl` spoofing `Referer`/`Origin`, save it to `/tmp/sb.json`, temporarily copy it to `src/assets/__sb_mock.json`, then in the browser console:

  ```js
  fetch('/assets/__sb_mock.json').then(r => r.json()).then(d => {
    const c = ng.getComponent(document.querySelector('flight-results-desktop') || document.querySelector('app-flight-results-mobile'));
    c.skybookerError = false; c.skyscannerError = false;
    c.processSkybookerResponse(d);
    c.skybookerLoaded = true; c.skyscannerLoaded = true; c.partnersLoaded = 1; c.filteringResults = false;
    c.searchComplete && c.searchComplete();
    c.cdr.detectChanges();
  });
  ```

  Delete `src/assets/__sb_mock.json` afterwards; it must not be committed.
- Mobile checks: CDP `Emulation.setDeviceMetricsOverride` (390x844, mobile) plus `Emulation.setUserAgentOverride` with an iPhone UA so `flight-autosuggest` and `mobile-calendar` render their mobile variants. Reset both when done.
- Behaviour checks were driven through `ng.getComponent(...)` (e.g. `setTripType('Multi City')`, `openMultiDate(i)`, `stepPax('editAdult', 1)`, `closePax()`, clicking `bs-datepicker-container td span`) and confirmed via `location.search` after navigation.

## 8. Chronological list of user requests (flights)

1. Redesign flights results like hotels/activities.
2. Filters: all unchecked by default, show all; checking shows only those.
3. Detail drawer close button cropped (desktop), then mobile sheet header cropped.
4. Pin ("only this leg") missing on mobile.
5. Format debug info; show `9 seats left` top-right of the card.
6. Rename `Select` to something like `View itinerary`.
7. Swipe down on the grey handle closes mobile sheets.
8. Arrival time rendered smaller than departure time.
9. Tooltips and info icons for `+1`, depart/arrival badges and duration.
10. Mobile autosuggest panel broken in edit; multi-city in edit redirected to homepage.
11. Passenger modal should auto-search after changes on close.
12. Four sort boxes must fit one row with ellipsis.
13. After changing depart date open the return picker; refresh if either changed.
14. `stagingEnvironment` should be true on localhost or `staging=true`.
15. Update md docs (this file).
16. Airlines panel showed more carriers than the 3 visible results (explained: filters are inclusion-based, list is from the full result set).
17. Mobile "Flying from / to" autosuggest panel formatted like hotels/activities.
18. Summary still showed the previous origin after modifying locations.
19. Next page / applied filters scroll to top on mobile and desktop.
20. `Showing 1–30 of 166 flights` range on top of results.
21. Swap `<->` button did nothing.
22. Mobile edit view shown as a sheet like filters, because multi-city needs the space.
23. Calendar: continue with the same dates without re-tapping (flights, hotels, activities).
24. Update md docs (this file).
