# Board Details Header + Action Buttons (B.4)

## Goal

Add the flight status header to the React Online Board details page: airline logo, flight number, codesharing, action buttons (Buy Ticket, Register, Flight Status, Share, Print), flight event indicators (route change, reroute), and a last-update timestamp. Match Angular parity including time-based button visibility logic.

## Scope

Sub-feature **B.4** of the Flight Details parity work. Online Board details page only.

## Current State

The React details page has a minimal header:
```tsx
<div className="flight-details__header">
  <h1>{flightNumber}</h1>   // Now in PageLayout title from B.2
  <span>{displayFlight.status}</span>
</div>
```

Angular has a rich `board-details-header` component with 10+ sub-components, visibility logic, and airline-specific behavior.

## Approach

Full Angular parity port. New components live under `src/features/online-board/components/BoardDetailsHeader/`. Pure visibility logic lives in `visibility/*.ts` as side-effect-free functions, tested in isolation. Airline configuration (URLs, capabilities) goes in `airlines.ts` as a constant map. Time math uses `date-fns` (new dependency).

## New Dependency

- `date-fns` (~25kb tree-shakable) — `parseISO`, `subHours`, `isAfter`, `isBefore`, `isSameDay`

## Architecture

### File structure

```
src/features/online-board/components/BoardDetailsHeader/
├── BoardDetailsHeader.tsx          # Orchestrator
├── BoardDetailsHeader.scss
├── BoardDetailsHeader.test.tsx
├── DetailsHeaderBadge.tsx          # Flight number + codesharing + operator logo + small status button
├── DetailsHeaderBadge.test.tsx
├── OperatorLogo.tsx                # CSS-class-driven airline logo
├── OperatorLogo.scss               # Ported from Angular's _logos.scss
├── OperatorLogo.test.tsx
├── FlightActions.tsx               # Container for 5 action buttons
├── FlightActions.test.tsx
├── BuyTicketButton.tsx
├── BuyTicketButton.test.tsx
├── RegistrationButton.tsx
├── RegistrationButton.test.tsx
├── FlightStatusButton.tsx
├── FlightStatusButton.test.tsx
├── ShareButton.tsx                 # Button + panel together
├── SharePanel.tsx
├── ShareButton.test.tsx
├── SharePanel.test.tsx
├── PrintButton.tsx                 # Stub (empty URL, hidden)
├── FlightEvents.tsx                # Route change + reroute icons
├── FlightEvents.test.tsx
├── LastUpdate.tsx                  # Timestamp + mobile share
├── LastUpdate.test.tsx
├── airlines.ts                     # Airline URL/capability config
├── actions.scss                    # Shared button styles
├── icons/
│   ├── change.svg
│   ├── return.svg
│   ├── share.svg
│   └── print.svg
├── airlines-logo/                  # 35 airline logo folders copied from Angular
│   └── aeroflot/, rossiya/, aurora/, ... 
├── visibility/
│   ├── buyTicketVisibility.ts
│   ├── buyTicketVisibility.test.ts
│   ├── registrationVisibility.ts
│   ├── registrationVisibility.test.ts
│   ├── flightStatusVisibility.ts
│   └── flightStatusVisibility.test.ts
└── index.ts
```

### `useAppSettings` hook extension

```typescript
export interface UseAppSettingsResult {
  // Existing
  onlineboardSearchFrom: number;
  onlineboardSearchTo: number;
  scheduleSearchFrom: number;
  scheduleSearchTo: number;
  // New (B.4)
  flightStatusAvailableFromHours: number;  // default 24
  buyTicketMinHours: number;                // default 2
  buyTicketMaxHours: number;                // default 72
  loading: boolean;
  error: Error | null;
}
```

Parses `uiOptions.buttons.flightStatus.availableFrom` (e.g., `"24h"`) and `uiOptions.buttons.buyTicket.period.min/max`. Pattern: `/^(\d+)h$/`. Falls back to defaults if parsing fails.

### Visibility logic (pure functions in `visibility/`)

**`canBuyTicket(flight, now, minHours, maxHours)`**
- Returns false if status is "Cancelled" or "InFlight"
- Extracts first leg's scheduled departure UTC
- Returns true only when `now` is between `departure - maxHours` and `departure - minHours`

**`canRegister(flight, airlineConfig)`**
- Gets `flight.operatingBy.carrier`
- Returns false if carrier has no `registrationUrl`
- Returns true only when `firstLeg.transition.registration.status === "InProgress"`

**`canViewFlightStatus(flight, now, availableFromHours, airlinesWithStatus)`**
- Returns false if carrier not in set `{"SU", "HZ", "FV", "DP"}`
- Returns false if `now` is not same-day as departure
- Returns true when `now > departure - availableFromHours`

### Airline config (`airlines.ts`)

```typescript
export interface AirlineConfig {
  name: string;
  registrationUrl?: string;
  hasNativeStatus: boolean;
  statusUrl?: string;  // only when hasNativeStatus is false
}

export const AIRLINES: Record<string, AirlineConfig> = {
  SU: { name: "Aeroflot", registrationUrl: "https://www.aeroflot.ru/sb/ckin/app/ru-ru", hasNativeStatus: true },
  FV: { name: "Rossiya", registrationUrl: "https://www.rossiya-airlines.com/flight-with-us/before_flight/the_ways_of_check-in/", hasNativeStatus: true },
  HZ: { name: "Aurora", registrationUrl: "https://www.flyaurora.ru", hasNativeStatus: false, statusUrl: "https://www.flyaurora.ru" },
  DP: { name: "Pobeda", registrationUrl: "https://www.pobeda.aero", hasNativeStatus: false, statusUrl: "https://www.pobeda.aero" },
  AF: { name: "AirFrance", hasNativeStatus: false },
};

export const AIRLINES_WITH_STATUS = new Set(["SU", "HZ", "FV", "DP"]);
```

### Component contracts

**`BoardDetailsHeader`**
```typescript
interface Props {
  flight: ISimpleFlight;
  locale: string;
}
```
Orchestrates `DetailsHeaderBadge`, `FlightActions`, `FlightEvents`, `LastUpdate` in a 2-column grid.

**`DetailsHeaderBadge`**
```typescript
interface Props {
  flight: ISimpleFlight;
  large?: boolean;
  round?: boolean;
  showStatus?: boolean;  // whether to render the small Flight Status button
}
```
Renders flight number, codesharing partners below (from `legs.map(l => l.operatingBy)`), `OperatorLogo`, and optionally a small `FlightStatusButton`.

**`OperatorLogo`**
```typescript
interface Props {
  flight: ISimpleFlight;
  locale: string;
  round?: boolean;
  large?: boolean;
  caption?: boolean;
}
```
Renders an empty `<div>` with classes `company-logo company-logo--{CARRIER} {large? "large" :""} {round? "round" :""} {locale === "ru" ? "ru" : ""}`. The SCSS applies background images.

**`FlightActions`**
```typescript
interface Props {
  flight: ISimpleFlight;
  locale: string;
  viewType: "Onlineboard" | "Schedule";
  showStatus?: boolean;    // default true
  showDetails?: boolean;   // default false
  showPrint?: boolean;     // default false (hidden on details page)
  showShare?: boolean;     // default true
  showRegister?: boolean;  // default true
  showBuy?: boolean;       // default true
}
```
Renders each action button conditional on visibility logic + show prop.

**`FlightEvents`**
```typescript
interface Props {
  changeRoute: boolean;
  reroute: boolean;
  direction?: "row" | "column-mobile";
  showDescription?: boolean;
}
```

**`LastUpdate`**
```typescript
interface Props {
  flight: ISimpleFlight;
  locale: string;
}
```
Renders timestamp (`HH:mm DD.MM.YYYY`) via `date-fns` `format`. Also renders a mobile-only `ShareButton` next to it.

### Button behaviors (click handlers)

**BuyTicket:** opens `https://www.aeroflot.ru/sb/app/{locale}-{locale}#/search?adults=1&cabin=economy&children=0&infants=0&routes={dep}.{yyyymmdd}.{arr}&autosearch=Y` in new tab.

**Registration:** opens `AIRLINES[carrier].registrationUrl` in new tab.

**FlightStatus:** if `hasNativeStatus`, opens the flight's own details URL in new tab; otherwise opens `statusUrl` in new tab.

**Share:** toggles `SharePanel`. Panel renders:
- Facebook: `https://www.facebook.com/sharer/sharer.php?u={url}`
- VK: `http://vk.com/share.php?url={url}`
- Twitter: `http://twitter.com/share?text={text}&url={url}`
- Weibo (zh only): `http://service.weibo.com/share/share.php?url={url}`
- Copy button: `navigator.clipboard.writeText(url)`

`url` is built from the current page's absolute URL.

**Print:** empty URL (matches Angular). The component is always rendered hidden on the details page via `showPrint={false}`.

### Integration into `OnlineBoardDetailsPage`

Replace:
```tsx
<div className="flight-details__header">
  <span className="flight-details__overall-status">{displayFlight.status}</span>
</div>
```

with:
```tsx
<BoardDetailsHeader flight={displayFlight} locale={locale} />
```

Move the overall-status indication into the header's badge. Remove the stale `flight-details__header` div from the main content — the header is the entire top section now.

## Styling

### Copied from Angular `_logos.scss`

All 35 `.company-logo--{CODE}` rules with CSS `background-image` URLs. Paths rewritten from `~src/assets/...` to relative (`./airlines-logo/...`). Rspack handles the `.svg`/`.png` imports as file assets.

### Action buttons

Shared `actions.scss`:
```scss
.flight-action-btn {
  padding: 8px 16px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
  border: none;
  font-family: inherit;

  &--orange { background: #ff9000; color: #fff; }
  &--blue-light { background: #e3f0ff; color: #1a3a5c; }
  &--transparent { background: transparent; padding: 8px; }
}
```

### Header layout

```scss
.board-details-header {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 16px;
  padding: 24px;
  background: #fff;
  border-radius: 8px;

  &__badge { grid-column: 1; }
  &__actions { grid-column: 2; display: flex; gap: 8px; justify-content: flex-end; }
  &__events { grid-column: 3; }
  &__last-update { grid-column: 1 / -1; }

  @media (max-width: 768px) {
    grid-template-columns: 1fr;
    padding: 16px;
    .share-button--desktop { display: none; }
  }
}
```

## Testing

### Visibility logic (unit, pure functions)

- `buyTicketVisibility.test.ts` — window boundary tests (just before showFrom, middle, just after showUntil), status exclusions
- `registrationVisibility.test.ts` — airline support matrix, status "InProgress" check
- `flightStatusVisibility.test.ts` — same-day boundary, airline filtering, time window

### Components (unit)

- `OperatorLogo.test.tsx` — applies correct CSS class per carrier code
- `DetailsHeaderBadge.test.tsx` — flight number, codesharing list, small status button visibility
- `BuyTicketButton.test.tsx` / `RegistrationButton.test.tsx` / `FlightStatusButton.test.tsx` — render, click opens expected URL (mock `window.open`)
- `ShareButton.test.tsx` — toggles panel visibility
- `SharePanel.test.tsx` — renders 4 social links, Weibo conditional, copy calls `navigator.clipboard`
- `FlightEvents.test.tsx` — conditional rendering of change/reroute icons
- `LastUpdate.test.tsx` — timestamp format, mobile share visibility
- `FlightActions.test.tsx` — renders only enabled visible buttons
- `BoardDetailsHeader.test.tsx` — integration smoke test

### Hook extension

- `useAppSettings.test.ts` — new tests for `buttons` parsing

### Integration

- `OnlineBoardDetailsPage.test.tsx` — `<BoardDetailsHeader>` replaces the inline header

## i18n

No new keys needed. Reuses:
- `SHARED.BUY-TICKET`, `SHARED.ONLINE-REGISTRATION`
- `SHARED.DETAILS`, `SHARED.DETAILS-TOOLTIP`, `SHARED.FLIGHT-INFO`
- `SHARED.LAST-UPDATE`, `SHARED.ROUTE-CHANGE`, `SHARED.RETURN`
- `SHARED.AVIACOMPANY`
- `BOARD.SHARE`

(All confirmed present in `src/i18n/locales/en/common.json`.)

## Assets

### Airline logos
Copy all 35 directories from `ClientApp/src/assets/img/airlines-logo/` to `src/features/online-board/components/BoardDetailsHeader/airlines-logo/`.

### Event/action icons
Copy `change.svg`, `return.svg`, `share.svg`, `print.svg` from Angular's icon directory to `./icons/`.

## Files Touched

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

### Modified files
- `src/shared/hooks/useAppSettings.ts` — extend with buttons config
- `src/shared/hooks/useAppSettings.test.ts` — add tests for buttons parsing
- `src/features/online-board/components/OnlineBoardDetailsPage.tsx` — replace inline header
- `src/features/online-board/components/OnlineBoardDetailsPage.test.tsx` — update mock state
- `package.json` — add `date-fns` dependency

## Out of Scope

- Schedule feature equivalent (`schedule-details-header`)
- Print page implementation (URL stays empty per Angular)
- Registration state re-fetch on button click
- Feature flags for China-specific share filtering beyond locale check
- Analytics wiring on button clicks
