# Day Tabs (B.3)

## Goal

Add a horizontal row of day tabs above the main flight details content, letting users navigate the same flight number across different dates. Matches Angular's behavior: paginated carousel on desktop (7 days per page), dropdown on mobile, tabs disabled for dates not operating, auto-scroll to the active tab on mount.

## Scope

Sub-feature **B.3** of the Flight Details parity work. Online Board only; Schedule day tabs are a separate concern.

## Current State

The React details page has no date navigation. `useFlightDetails` discards the `daysOfFlight` array from the API response. The app has no `useAppSettings` hook (the dev-server mocks the endpoint but nothing consumes it).

## Approach

Four additions:

1. **`useAppSettings` hook** — fetches `/api/appSettings`, parses `searchFrom`/`searchTo` strings (e.g., `"2d"` → `2`), caches across the app.
2. **Extend `useFlightDetails`** — also return `daysOfFlight: string[]` from the response.
3. **`DayTabs` component family** (container, `DayTabButton`, `DaySelect`) — generates tabs from `today - searchFrom` to `today + searchTo`, disables dates not in `daysOfFlight`, paginates 7 at a time, auto-scrolls to the page containing the selected date.
4. **Integrate into `OnlineBoardDetailsPage`** — render `DayTabs` as `stickyContent` in `PageLayout`. Click handler navigates to `/lang/onlineboard/{carrier}{flight}-{yyyymmdd}`.

## Architecture

### Hook: `useAppSettings`

New hook at `src/shared/hooks/useAppSettings.ts`.

```typescript
export interface UseAppSettingsResult {
  onlineboardSearchFrom: number;  // days before today, e.g. 2
  onlineboardSearchTo: number;    // days after today, e.g. 14
  scheduleSearchFrom: number;
  scheduleSearchTo: number;
  loading: boolean;
  error: ApiError | null;
}

export function useAppSettings(): UseAppSettingsResult;
```

Implementation fetches the `/api/appSettings` endpoint via the existing API client. Parses strings matching `/^(\d+)d$/` into numbers. Returns zeros (with `loading: false, error`) on parse failure or missing fields.

Fallback defaults when values are unavailable: `onlineboardSearchFrom: 2, onlineboardSearchTo: 14, scheduleSearchFrom: 30, scheduleSearchTo: 30`.

### Hook extension: `useFlightDetails`

Add `daysOfFlight: string[]` to the return. Same pattern as `allFlights` (stored from `response.data.daysOfFlight`).

```typescript
export interface UseFlightDetailsResult {
  flight: ISimpleFlight | null;
  allFlights: ISimpleFlight[];
  daysOfFlight: string[];   // NEW
  loading: boolean;
  error: ApiError | null;
}
```

### Component: `DayTabs` (container)

File: `src/features/online-board/components/DayTabs/DayTabs.tsx`.

```typescript
export interface DayTabsProps {
  selectedDate: string;       // yyyymmdd, matching URL
  availableDates: string[];   // yyyymmdd list from daysOfFlight
  daysBefore: number;
  daysAfter: number;
  onNavigate: (date: string) => void;
}
```

Logic:
- Compute `allDates`: array of yyyymmdd strings spanning `today - daysBefore` to `today + daysAfter` inclusive.
- `availableSet = new Set(availableDates)` for O(1) lookup.
- `pageSize = 7`.
- Initialize `currentPage` state to the page index containing `selectedDate` (or 0 if not found).
- Visible slice = `allDates.slice(currentPage * 7, (currentPage + 1) * 7)`.
- Prev/next arrows call `setCurrentPage(n - 1)` / `setCurrentPage(n + 1)` clamped to `[0, totalPages - 1]`.
- Renders `DayTabButton` for each visible date.
- Renders a single `DaySelect` alongside (hidden on desktop via CSS).

### Component: `DayTabButton`

File: `src/features/online-board/components/DayTabs/DayTabButton.tsx`.

```typescript
export interface DayTabButtonProps {
  date: string;          // yyyymmdd
  isActive: boolean;
  isDisabled: boolean;
  locale: string;        // for weekday + month labels
  onClick: (date: string) => void;
}
```

Renders a button showing:
- Weekday short (`Intl.DateTimeFormat(locale, { weekday: "short" })`)
- Day number (`Intl.DateTimeFormat(locale, { day: "numeric" })`)
- Month short (`Intl.DateTimeFormat(locale, { month: "short" })`)

Click calls `onClick(date)` unless `isDisabled`. Applies `.day-tab--active` / `.day-tab--disabled` modifiers. `data-testid="day-tab-{yyyymmdd}"`.

### Component: `DaySelect` (mobile)

File: `src/features/online-board/components/DayTabs/DaySelect.tsx`.

```typescript
export interface DaySelectProps {
  selectedDate: string;
  availableDates: string[];
  locale: string;
  onNavigate: (date: string) => void;
}
```

Native `<select>` with one `<option value="{yyyymmdd}">{label}</option>` per available date. Disabled dates (outside `availableDates`) are omitted entirely. `onChange` calls `onNavigate(e.target.value)`. `data-testid="day-select"`.

Label format: `"Mon, Apr 16"` or locale equivalent.

### Integration

In `OnlineBoardDetailsPage.tsx`:

```tsx
const { flight, allFlights, daysOfFlight, loading, error } = useFlightDetails(detailsParams);
const { onlineboardSearchFrom, onlineboardSearchTo } = useAppSettings();
const navigate = useNavigate();

const handleNavigateDate = useCallback((newDate: string) => {
  const url = buildOnlineBoardUrl({
    type: "details",
    carrier: flightId.carrier,
    flightNumber: flightId.flightNumber,
    ...(flightId.suffix ? { suffix: flightId.suffix } : {}),
    date: newDate,
  });
  navigate(`/${locale}/${url}`);
}, [flightId, locale, navigate]);

// In the PageLayout:
<PageLayout
  ...
  stickyContent={
    <DayTabs
      selectedDate={flightId.date}
      availableDates={daysOfFlight}
      daysBefore={onlineboardSearchFrom}
      daysAfter={onlineboardSearchTo}
      onNavigate={handleNavigateDate}
    />
  }
>
```

`PageLayout` already has a `stickyContent` slot.

## Styling

`DayTabs.scss`:

```scss
.day-tabs {
  display: flex;
  align-items: stretch;
  background: #e8f0f7;
  border-radius: 8px 8px 0 0;

  &__arrow {
    width: 48px;
    flex-shrink: 0;
    background: transparent;
    border: none;
    cursor: pointer;
    color: #2060c0;

    &:disabled {
      opacity: 0.3;
      cursor: not-allowed;
    }
  }

  &__list {
    flex: 1;
    display: grid;
    grid-template-columns: repeat(7, 1fr);
  }

  @media (max-width: 768px) {
    display: none;
  }
}

.day-tab {
  padding: 12px 8px;
  text-align: center;
  cursor: pointer;
  background: #e8f0f7;
  color: #2060c0;
  border: none;
  border-right: 1px solid #d0dae5;

  &:last-child { border-right: none; }

  &--active {
    background: #fff;
    color: #1a3a5c;
    font-weight: 600;
  }

  &--disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }

  &__weekday { font-size: 12px; display: block; }
  &__day { font-size: 20px; font-weight: 500; display: block; }
  &__month { font-size: 12px; display: block; }
}

.day-select {
  display: none;

  @media (max-width: 768px) {
    display: block;
    width: 100%;
    padding: 12px;
    border-radius: 8px;
    background: #fff;
    border: 1px solid #d0dae5;
  }
}
```

## Testing

### `useAppSettings.test.ts`
- Parses `"2d"` → `2`, `"14d"` → `14`
- Returns `loading: true` before response
- Returns `error` when fetch fails
- Returns defaults (2/14/30/30) when `uiOptions.filter.*` fields are missing
- Handles non-matching patterns (`"blah"`) as default

### `DayTabButton.test.tsx`
- Renders weekday short, day number, month short
- Click fires `onClick(date)` when enabled
- Click does nothing when disabled
- Applies `--active` / `--disabled` modifiers correctly
- Has `data-testid="day-tab-{yyyymmdd}"`

### `DayTabs.test.tsx`
- Renders 7 tabs on first page
- Dates outside `availableDates` are disabled
- Prev arrow disabled on page 0
- Next arrow disabled on last page
- Prev/next arrows change page
- Initializes `currentPage` to the page containing `selectedDate`
- Click on a day tab fires `onNavigate(date)`
- Has `data-testid="day-tabs"`

### `DaySelect.test.tsx`
- Renders one `<option>` per date in `availableDates`
- `<select>` value is `selectedDate`
- onChange fires `onNavigate(new_date)`
- Has `data-testid="day-select"`

### `useFlightDetails.test.ts` (extend)
- Response with `daysOfFlight: ["20260415", "20260416"]` → hook returns that array
- Empty response → `daysOfFlight: []`

### `OnlineBoardDetailsPage.test.tsx` (update)
- `stickyContent` includes `DayTabs` (look for `data-testid="day-tabs"`)
- Existing accordion + mini-list tests still pass

## i18n Keys

No new keys. `DayTabButton` and `DaySelect` generate labels entirely from `Intl.DateTimeFormat(locale, ...)`. Locale comes from the existing `locale` prop.

## Out of Scope

- Schedule day tabs (different data source).
- Fetching `daysOfFlight` from a separate endpoint.
- Keyboard navigation between tabs (left/right arrow keys).
- Custom month/weekday labels beyond `Intl` defaults.

## Files Touched

### New
- `src/shared/hooks/useAppSettings.ts`
- `src/shared/hooks/useAppSettings.test.ts`
- `src/features/online-board/components/DayTabs/DayTabs.tsx`
- `src/features/online-board/components/DayTabs/DayTabs.scss`
- `src/features/online-board/components/DayTabs/DayTabs.test.tsx`
- `src/features/online-board/components/DayTabs/DayTabButton.tsx`
- `src/features/online-board/components/DayTabs/DayTabButton.test.tsx`
- `src/features/online-board/components/DayTabs/DaySelect.tsx`
- `src/features/online-board/components/DayTabs/DaySelect.test.tsx`
- `src/features/online-board/components/DayTabs/index.ts`

### Modified
- `src/features/online-board/hooks/useFlightDetails.ts` — add `daysOfFlight` to return
- `src/features/online-board/hooks/useFlightDetails.test.ts` — add test case
- `src/features/online-board/components/OnlineBoardDetailsPage.tsx` — wire DayTabs into PageLayout.stickyContent
- `src/features/online-board/components/OnlineBoardDetailsPage.test.tsx` — update existing tests for new structure
