# Transfer Bar + Multi-Leg Full-Route Timeline (B.5)

## Goal

Add Angular-parity multi-leg visualization to the React Online Board details page: a carousel-style `FullRouteTimeline` at the top of multi-leg flights showing 2 consecutive legs at a time, and a `TransferBar` between every pair of legs showing layover duration, station change info, and arrival/departure times.

## Scope

Sub-feature **B.5** of the Flight Details parity work. Online Board details page only. Triggers for multi-leg flights (`routeType === "MultiLeg"`). Direct flights see no changes.

## Current State

The React details page has no multi-leg visualization. `FlightLegs` iterates legs without showing transfers or a top-level route timeline.

## Approach

Two new component families:

1. **`FullRouteTimeline`** — wrapper + inner `Timeline` component. Timeline shows 2 legs at a time with prev/next carousel buttons. Uses flexbox + CSS dividers (no SVG). Hidden on mobile (<768px).
2. **`TransferBar`** — rendered between consecutive legs. Displays a horizontal info bar with layover duration, arrival/departure times, and station-change indicator.

Supporting leaves: `Station`, `StationChange`, `TransferTime`. Pure helpers: `detectStationChange`, `computeTransferMinutes`, `formatMinutesAsDuration`.

## Types Extension

```typescript
// src/features/online-board/types.ts additions

export interface IDuration {
  days: number;
  hours: number;
  minutes: number;
  isNegative?: boolean;
}

export interface IFlightLeg {
  // ... existing fields ...
  estimatedDuration?: IDuration;
  scheduledDuration?: IDuration;
}
```

Both duration fields are optional. React continues to use `flyingTime: string` for primary display; the Duration fields power the "specifying" orange-color state (when `estimatedDuration.isNegative === true`) in the timeline.

## Architecture

### File structure

```
src/features/online-board/components/
├── FullRouteTimeline/
│   ├── FullRouteTimeline.tsx
│   ├── FullRouteTimeline.scss
│   ├── FullRouteTimeline.test.tsx
│   ├── Timeline.tsx
│   ├── Timeline.test.tsx
│   ├── Station.tsx
│   ├── Station.test.tsx
│   ├── StationChange.tsx
│   ├── StationChange.test.tsx
│   ├── detectStationChange.ts
│   ├── detectStationChange.test.ts
│   └── index.ts
└── TransferBar/
    ├── TransferBar.tsx
    ├── TransferBar.scss
    ├── TransferBar.test.tsx
    ├── TransferTime.tsx
    ├── TransferTime.test.tsx
    ├── computeTransferTime.ts
    ├── computeTransferTime.test.ts
    └── index.ts
```

### Component contracts

```typescript
// FullRouteTimeline — wrapper; renders Timeline when legs.length > 1
interface FullRouteTimelineProps {
  legs: IFlightLeg[];
  viewType: "Onlineboard" | "Schedule";
}

// Timeline — carousel state management, renders 2 legs at a time
interface TimelineProps {
  legs: IFlightLeg[];
  canChange: boolean;  // maps to viewType === "Onlineboard"
}

// TransferBar — between two consecutive legs
interface TransferBarProps {
  leg: IFlightLeg;         // the leg with arrival
  nextLeg: IFlightLeg;     // the leg with departure
  viewType: "Onlineboard" | "Schedule";
}

// Station — single station display
interface StationProps {
  station: IFlightLegStation;
  align?: "left" | "right" | "center";
  size?: "small" | "medium" | "large";
}

// StationChange — renders one of 4 visual branches
interface StationChangeProps {
  from: IFlightLegStation;
  to: IFlightLegStation;
}

// TransferTime — layover duration display
interface TransferTimeProps {
  arrivalUtc: string;
  departureUtc: string;
}
```

### Pure helpers

```typescript
// detectStationChange.ts
export type StationChange = "city" | "airport" | "terminal" | "noChange";
export function detectStationChange(
  from: IFlightLegStation,
  to: IFlightLegStation,
): StationChange;

// computeTransferTime.ts
/** UTC diff in minutes between departure and arrival. Returns null for invalid input. */
export function computeTransferMinutes(
  arrivalUtc: string | undefined,
  departureUtc: string | undefined,
): number | null;

/** Format minutes as "Nh Mm" (or "Mm" for <1h). */
export function formatMinutesAsDuration(minutes: number): string;
```

## Rendering Details

### Timeline (2 legs at a time)

Given an internal `index` state (0..legs.length-2):

- **Times row**: `depTime(legs[index])` — section separator — `arrTime(legs[index])` — middle transfer separator — `depTime(legs[index+1])` — section separator — `arrTime(legs[index+1])`
- **Stations row**: `<Station dep-of-legs[index]>` — `<StationChange from=arr-of-legs[index] to=dep-of-legs[index+1]>` — `<Station arr-of-legs[index+1]>`
- **Navigation**: prev button when `index > 0`, next button when `index < legs.length - 2`. Both are circular buttons with ‹ / › chevrons, disabled at boundaries (visually opacity 0.3).

### Time source logic (matches Angular `canChange` flag)

```typescript
function depTime(leg: IFlightLeg, canChange: boolean): string {
  const t = leg.departure.times;
  if (canChange) return t.actualBlockOff?.local ?? t.scheduledDeparture.local;
  return t.scheduledDeparture.local;
}

function arrTime(leg: IFlightLeg, canChange: boolean): string {
  const t = leg.arrival.times;
  if (canChange) return t.actualBlockOn?.local ?? t.scheduledArrival.local;
  return t.scheduledArrival.local;
}
```

### Section label (number + duration badge)

```tsx
<div className="timeline-section">
  <div className="timeline-section__separator" />
  <span className="timeline-section__number">{index + 1}</span>
  <span className={`timeline-section__duration ${isSpecifying ? "timeline-section__duration--specifying" : ""}`}>
    {leg.flyingTime}
  </span>
  <div className="timeline-section__separator" />
</div>
```

`isSpecifying = leg.estimatedDuration?.isNegative === true` (only for canChange views).

### TransferBar

```tsx
<div className={`transfer-bar ${stationChange === "noChange" ? "" : "transfer-bar--separated"}`}>
  <div className="transfer-bar__icon"><svg>...</svg></div>
  <div className="transfer-bar__content">
    <span className="transfer-bar__type">{t("SHARED.INTERMEDIATE-LANDING")}</span>
    <div className="transfer-bar__duration">
      <TransferTime arrivalUtc={arrivalUtc} departureUtc={departureUtc} />
    </div>
    <div className="transfer-bar__times">
      <span>{arrivalLocal}</span><span> — </span><span>{departureLocal}</span>
    </div>
    {stationChange !== "noChange" && (
      <div className="transfer-bar__station-change">
        <StationChange from={leg.arrival} to={nextLeg.departure} />
      </div>
    )}
  </div>
</div>
```

Times come from `viewType === "Schedule"` → always scheduled; else latest-with-fallback-to-scheduled.

### Station

```tsx
<div className={`station station--${align} station--${size}`}>
  <span className="station__city">{station.scheduled.city}</span>
  <span className="station__code">{station.scheduled.airportCode}</span>
  {station.terminal && <span className="station__terminal">T{station.terminal}</span>}
</div>
```

### StationChange (4 branches)

- `noChange` → `<Station station={from} />` (single display)
- `city` → `<Station from> → <Station to>` (both cities with arrow)
- `airport` → `city | {from.airportCode}/{from.terminal} → {to.airportCode}/{to.terminal}` (same city, different airport)
- `terminal` → same as airport branch but only terminal differs

### Integration into `OnlineBoardDetailsPage`

```tsx
{displayFlight.routeType === "MultiLeg" && (
  <FullRouteTimeline
    legs={displayFlight.legs}
    viewType="Onlineboard"
  />
)}

{/* FlightLegs renders transfer bars between legs: */}
<FlightLegs legs={legs} viewType="Onlineboard" />
// Internally: legs.map((leg, i) => <>{<Leg>}{i < legs.length-1 && <TransferBar leg={leg} nextLeg={legs[i+1]} viewType="Onlineboard" />}</>)
```

## Styling

See the SCSS blocks presented in the design (Timeline flex layout, station-change arrow indicator, transfer-bar colored box). Mobile breakpoint at 768px hides the Timeline entirely. TransferBar stacks content vertically on mobile.

## Testing

### Helpers (pure)

- `detectStationChange.test.ts` — 4 cases: same station, city change, airport change, terminal change
- `computeTransferTime.test.ts` — positive diff, zero, negative returns null, invalid inputs return null, formatting "1h 30m" / "30m"

### Components

- `Station.test.tsx` — renders city, code, terminal; alignment & size modifiers applied
- `StationChange.test.tsx` — 4 branches render correctly (`noChange`, `city`, `airport`, `terminal`)
- `TransferTime.test.tsx` — renders formatted duration; returns null for negative/invalid
- `TransferBar.test.tsx` — renders icon + type label + duration + times + station change for city case; renders without station change for noChange
- `Timeline.test.tsx` — 2 legs visible, prev disabled on index=0, next disabled on last pair, prev/next change index, specifying class applied when `estimatedDuration.isNegative`
- `FullRouteTimeline.test.tsx` — returns null when `legs.length < 2`, renders Timeline otherwise

### Integration

- `OnlineBoardDetailsPage.test.tsx` — `FullRouteTimeline` renders for MultiLeg flights, not for Direct. `TransferBar` rendered between legs.

## i18n

Existing keys reused:
- `SHARED.INTERMEDIATE-LANDING`
- `SHARED.CONNECTING` (if needed for Connecting flights; multi-leg uses intermediate)
- `SHARED.TRANSFER`

No new keys needed.

## Out of Scope

- Schedule feature details variant — reuses existing `viewType="Schedule"` prop semantics but no schedule-specific wiring in this spec
- Connecting flights (separate flights booked together) — the `TransferBar` supports `viewType` switching but the Connecting route type is its own feature area, not wired into the React details page yet
- Timeline animation / transition effects between index changes
- Sprite-based icons — we use inline SVG for transfer icon

## Files Touched

### New
- All files under `src/features/online-board/components/FullRouteTimeline/` and `src/features/online-board/components/TransferBar/` (as listed in file structure)

### Modified
- `src/features/online-board/types.ts` — add `IDuration`, extend `IFlightLeg`
- `src/features/online-board/types.test.ts` — add test cases
- `src/features/online-board/components/OnlineBoardDetailsPage.tsx` — add `FullRouteTimeline`; update `FlightLegs` to interleave `TransferBar` between legs
- `src/features/online-board/components/OnlineBoardDetailsPage.test.tsx` — add integration tests
