# Flight Details Accordion (B.1) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Build the per-leg details accordion on the React Online Board details page — six conditionally-rendered panels (Registration, Boarding, Deboarding, Aircraft, Meal, On-board Services) matching Angular's structure.

**Architecture:** Extend `src/features/online-board/types.ts` with Angular-matching shapes for `transition` and full `equipment`. Add seven new components in `src/features/online-board/components/details-panels/`: one container + six panel components. Wire the container into `OnlineBoardDetailsPage.tsx` per leg. Each panel is self-contained and independently testable.

**Tech Stack:** React 18, TypeScript, PrimeReact `Accordion` (already in `package.json`), Vitest + React Testing Library for unit tests. i18next for translation keys.

---

## File Structure

### New files (all under `src/features/online-board/components/details-panels/`)

- `shared.ts` — Visibility helpers, service icon mapping, meal link constants
- `FlightDetailsAccordion.tsx` — Container component orchestrating the 6 panels
- `FlightDetailsAccordion.scss` — Container styling
- `FlightDetailsAccordion.test.tsx`
- `RegistrationPanel.tsx` — Registration transition display
- `RegistrationPanel.test.tsx`
- `BoardingPanel.tsx` — Boarding transition display
- `BoardingPanel.test.tsx`
- `DeboardingPanel.tsx` — Deboarding transition + arrival gate/belt
- `DeboardingPanel.test.tsx`
- `AircraftPanel.tsx` — Aircraft type, configuration
- `AircraftPanel.test.tsx`
- `MealPanel.tsx` — Meal-type icons with aeroflot.ru links
- `MealPanel.test.tsx`
- `ServicesPanel.tsx` — On-board service icons
- `ServicesPanel.test.tsx`
- `panels.scss` — Shared panel styling (imported by each panel's SCSS)

### Assets to copy

From `ClientApp/src/assets/img/service-and-food-icons/` to `src/features/online-board/components/details-panels/icons/`:
- `shopping.svg`, `space.svg`, `taxi.svg`, `wifi.svg`, `gsm.svg`, `entertaintment.svg`, `seat_reservation.svg`, `comfort-plus.svg` (fallback)
- `econom.svg`, `comfort.svg`, `business.svg` (meal type icons)

### Modified files

- `src/features/online-board/types.ts` — Add transition, equipment, meal, service types
- `src/features/online-board/components/OnlineBoardDetailsPage.tsx` — Wire `<FlightDetailsAccordion />` per leg
- `src/i18n/locales/en/common.json` — Add DETAILS.* keys
- `src/i18n/locales/ru/common.json` — Add DETAILS.* keys

---

### Task 1: Extend Types for Transition and Full Equipment

**Files:**
- Modify: `src/features/online-board/types.ts:103-114` (IFlightLeg)

- [ ] **Step 1: Write the failing test**

Create `src/features/online-board/types.test.ts`:

```typescript
import { describe, it, expect } from "vitest";
import type {
  IFlightLeg,
  IFlightTransitions,
  IFlightTransitionItem,
  FlightTransitionStatus,
  IMealItem,
  MealType,
  IOnBoardService,
  IAircraftInfo,
  IEquipmentFull,
} from "./types.js";

describe("online-board types extension", () => {
  it("IFlightTransitionItem has start/end/status/isActual", () => {
    const item: IFlightTransitionItem = {
      start: { dayChange: { value: 0, title: "" }, local: "10:00", localTime: "10:00", tzOffset: "+03:00", utc: "07:00" },
      end: { dayChange: { value: 0, title: "" }, local: "10:30", localTime: "10:30", tzOffset: "+03:00", utc: "07:30" },
      status: "InProgress",
      isActual: true,
    };
    expect(item.status).toBe("InProgress");
    expect(item.isActual).toBe(true);
  });

  it("FlightTransitionStatus accepts all 5 values", () => {
    const statuses: FlightTransitionStatus[] = [
      "Finished", "Expected", "InProgress", "Specified", "Scheduled",
    ];
    expect(statuses).toHaveLength(5);
  });

  it("IFlightTransitions has optional registration/boarding/deboarding", () => {
    const t: IFlightTransitions = {};
    expect(t.registration).toBeUndefined();
    expect(t.boarding).toBeUndefined();
    expect(t.deboarding).toBeUndefined();
  });

  it("MealType accepts Economy/Comfort/Business/Special", () => {
    const meals: MealType[] = ["Economy", "Comfort", "Business", "Special"];
    expect(meals).toHaveLength(4);
  });

  it("IMealItem has type field", () => {
    const m: IMealItem = { type: "Economy" };
    expect(m.type).toBe("Economy");
  });

  it("IOnBoardService has id, optional title, optional url", () => {
    const svc: IOnBoardService = { id: 4 };
    expect(svc.id).toBe(4);
    expect(svc.title).toBeUndefined();
    expect(svc.url).toBeUndefined();
  });

  it("IAircraftInfo has optional title and onBoardServices", () => {
    const a: IAircraftInfo = { title: "Boeing 737", onBoardServices: [{ id: 1 }] };
    expect(a.title).toBe("Boeing 737");
    expect(a.onBoardServices).toHaveLength(1);
  });

  it("IEquipmentFull nests aircraft.scheduled/actual/configuration and meal[]", () => {
    const eq: IEquipmentFull = {
      aircraft: {
        scheduled: { title: "A320" },
        actual: { title: "A321", onBoardServices: [] },
        configuration: "C12Y138",
      },
      meal: [{ type: "Business" }],
    };
    expect(eq.aircraft?.scheduled?.title).toBe("A320");
    expect(eq.aircraft?.actual?.title).toBe("A321");
    expect(eq.aircraft?.configuration).toBe("C12Y138");
    expect(eq.meal?.[0]?.type).toBe("Business");
  });

  it("IFlightLeg includes transition and extended equipment", () => {
    const leg: Partial<IFlightLeg> = {
      transition: { registration: { start: {} as never, end: {} as never, status: "Finished", isActual: true } },
      equipment: { name: "A320", aircraft: { actual: { title: "A320" } } },
    };
    expect(leg.transition?.registration?.status).toBe("Finished");
    expect(leg.equipment?.aircraft?.actual?.title).toBe("A320");
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm vitest run src/features/online-board/types.test.ts`

Expected: FAIL — types `IFlightTransitions`, `IFlightTransitionItem`, `FlightTransitionStatus`, `MealType`, `IMealItem`, `IOnBoardService`, `IAircraftInfo`, `IEquipmentFull` not exported.

- [ ] **Step 3: Extend `src/features/online-board/types.ts`**

Add these exports after the existing `IFlightLegFlags` interface (after line 101). The file already imports `ITimesSet`:

```typescript
// ---------------------------------------------------------------------------
// Transition (registration/boarding/deboarding)
// ---------------------------------------------------------------------------

export type FlightTransitionStatus =
  | "Finished"
  | "Expected"
  | "InProgress"
  | "Specified"
  | "Scheduled";

export interface IFlightTransitionItem {
  end: ITimesSet;
  start: ITimesSet;
  status: FlightTransitionStatus;
  isActual: boolean;
}

export interface IFlightTransitions {
  registration?: IFlightTransitionItem;
  boarding?: IFlightTransitionItem;
  deboarding?: IFlightTransitionItem;
}

// ---------------------------------------------------------------------------
// Equipment (aircraft + meal + on-board services)
// ---------------------------------------------------------------------------

export type MealType = "Economy" | "Comfort" | "Business" | "Special";

export interface IMealItem {
  type: MealType;
}

export interface IOnBoardService {
  id: number;
  title?: string;
  url?: string;
}

export interface IAircraftInfo {
  title?: string;
  onBoardServices?: IOnBoardService[];
}

export interface IEquipmentFull {
  aircraft?: {
    scheduled?: IAircraftInfo;
    actual?: IAircraftInfo;
    configuration?: string;
  };
  meal?: IMealItem[];
}
```

Then replace the existing `IFlightLeg` interface (lines 103-114) with:

```typescript
export interface IFlightLeg {
  arrival: IFlightLegArrivalStation;
  dayChange: number;
  departure: IFlightLegDepartureStation;
  equipment: { name?: string; code?: string } & IEquipmentFull;
  flags: IFlightLegFlags;
  flyingTime: string;
  index: number;
  operatingBy: { carrier?: string; flightNumber?: string };
  status: FlightStatus;
  updated: string;
  transition?: IFlightTransitions;
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `pnpm vitest run src/features/online-board/types.test.ts`

Expected: PASS — all 8 type assertions pass.

- [ ] **Step 5: Verify no regressions**

Run: `pnpm typecheck`

Expected: No new type errors. Existing code that uses `equipment.name`/`equipment.code` still compiles (they're preserved in the intersection).

- [ ] **Step 6: Commit**

```bash
git add src/features/online-board/types.ts src/features/online-board/types.test.ts
git commit -m "Extend IFlightLeg with transition and full equipment types"
```

---

### Task 2: Shared Helpers + i18n Keys

**Files:**
- Create: `src/features/online-board/components/details-panels/shared.ts`
- Create: `src/features/online-board/components/details-panels/shared.test.ts`
- Modify: `src/i18n/locales/en/common.json`
- Modify: `src/i18n/locales/ru/common.json`

- [ ] **Step 1: Write failing tests**

Create `src/features/online-board/components/details-panels/shared.test.ts`:

```typescript
import { describe, it, expect } from "vitest";
import {
  shouldShowTransition,
  shouldShowAircraft,
  SERVICE_ICON_MAP,
  MEAL_LINKS,
} from "./shared.js";
import type { IFlightTransitionItem } from "../../types.js";

describe("shouldShowTransition", () => {
  const validItem: IFlightTransitionItem = {
    start: {} as never,
    end: {} as never,
    status: "InProgress",
    isActual: true,
  };

  it("returns false for Schedule viewType", () => {
    expect(shouldShowTransition(validItem, "Scheduled", "Schedule")).toBe(false);
  });

  it("returns false when leg is Cancelled", () => {
    expect(shouldShowTransition(validItem, "Cancelled", "Onlineboard")).toBe(false);
  });

  it("returns false when item is undefined", () => {
    expect(shouldShowTransition(undefined, "Scheduled", "Onlineboard")).toBe(false);
  });

  it("returns false when item.status is Scheduled", () => {
    const scheduled = { ...validItem, status: "Scheduled" as const };
    expect(shouldShowTransition(scheduled, "Scheduled", "Onlineboard")).toBe(false);
  });

  it("returns true for active transition on Onlineboard", () => {
    expect(shouldShowTransition(validItem, "Scheduled", "Onlineboard")).toBe(true);
  });
});

describe("shouldShowAircraft", () => {
  it("returns true when actual.title exists", () => {
    expect(shouldShowAircraft({ aircraft: { actual: { title: "A320" } } })).toBe(true);
  });

  it("returns true when scheduled.title exists", () => {
    expect(shouldShowAircraft({ aircraft: { scheduled: { title: "A320" } } })).toBe(true);
  });

  it("returns true when configuration exists", () => {
    expect(shouldShowAircraft({ aircraft: { configuration: "C12Y138" } })).toBe(true);
  });

  it("returns false when no aircraft info", () => {
    expect(shouldShowAircraft({})).toBe(false);
    expect(shouldShowAircraft({ aircraft: {} })).toBe(false);
  });

  it("returns false when titles are empty strings", () => {
    expect(shouldShowAircraft({ aircraft: { actual: { title: "" }, scheduled: { title: "" } } })).toBe(false);
  });
});

describe("SERVICE_ICON_MAP", () => {
  it("maps all 8 service IDs", () => {
    expect(SERVICE_ICON_MAP[1]).toBe("shopping");
    expect(SERVICE_ICON_MAP[2]).toBe("space");
    expect(SERVICE_ICON_MAP[3]).toBe("taxi");
    expect(SERVICE_ICON_MAP[4]).toBe("wifi");
    expect(SERVICE_ICON_MAP[5]).toBe("gsm");
    expect(SERVICE_ICON_MAP[6]).toBe("entertaintment");
    expect(SERVICE_ICON_MAP[7]).toBe("entertaintment");
    expect(SERVICE_ICON_MAP[8]).toBe("seat_reservation");
  });
});

describe("MEAL_LINKS", () => {
  it("has links for Economy, Comfort, Business", () => {
    expect(MEAL_LINKS.Economy).toContain("meal-type_0");
    expect(MEAL_LINKS.Comfort).toContain("meal-type_1");
    expect(MEAL_LINKS.Business).toContain("meal-type_2");
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm vitest run src/features/online-board/components/details-panels/shared.test.ts`

Expected: FAIL — module does not exist.

- [ ] **Step 3: Create the shared helpers file**

Create `src/features/online-board/components/details-panels/shared.ts`:

```typescript
import type {
  FlightStatus,
  IEquipmentFull,
  IFlightTransitionItem,
  MealType,
} from "../../types.js";

export type DetailsViewType = "Onlineboard" | "Schedule";

/**
 * Matches Angular's `showBoardProperty` in flight-details-wrapper.component.ts.
 * Transition panels are hidden for Schedule mode, Cancelled flights,
 * missing data, or when the transition hasn't started.
 */
export function shouldShowTransition(
  item: IFlightTransitionItem | undefined,
  legStatus: FlightStatus,
  viewType: DetailsViewType,
): boolean {
  if (viewType === "Schedule") return false;
  if (legStatus === "Cancelled") return false;
  if (!item) return false;
  if (item.status === "Scheduled") return false;
  return true;
}

/**
 * Matches Angular's `showAircraft` getter. Shows when actual or scheduled
 * title exists, or when configuration string is present.
 */
export function shouldShowAircraft(equipment: Pick<IEquipmentFull, "aircraft">): boolean {
  const aircraft = equipment.aircraft;
  if (!aircraft) return false;
  const title = aircraft.actual?.title || aircraft.scheduled?.title;
  if (title) return true;
  if (aircraft.configuration) return true;
  return false;
}

/**
 * Service ID → icon filename mapping.
 * From Angular flight-details-services.component.ts.
 * IDs 6 and 7 both use the entertaintment icon (typo in original is preserved
 * because that's how Angular ships the filename).
 */
export const SERVICE_ICON_MAP: Record<number, string> = {
  1: "shopping",
  2: "space",
  3: "taxi",
  4: "wifi",
  5: "gsm",
  6: "entertaintment",
  7: "entertaintment",
  8: "seat_reservation",
};

/** Fallback icon for unknown service IDs. */
export const SERVICE_ICON_FALLBACK = "comfort-plus";

/**
 * Meal type → aeroflot.ru info page link.
 * From Angular flight-details-meal.component.ts.
 */
export const MEAL_LINKS: Record<Exclude<MealType, "Special">, string> = {
  Economy: "https://www.aeroflot.ru/ru-ru/information/onboard/dining?0000#meal-type_0",
  Comfort: "https://www.aeroflot.ru/ru-ru/information/onboard/dining?0000#meal-type_1",
  Business: "https://www.aeroflot.ru/ru-ru/information/onboard/dining?0000#meal-type_2",
};
```

- [ ] **Step 4: Run test to verify it passes**

Run: `pnpm vitest run src/features/online-board/components/details-panels/shared.test.ts`

Expected: PASS — all helper tests pass.

- [ ] **Step 5: Add DETAILS i18n keys (en)**

Edit `src/i18n/locales/en/common.json`. Find the top-level `"BOARD"` object (around line 9). After the closing `}` of `BOARD` (and before `BOARDING-STATUSES`), add a new top-level `DETAILS` object:

```json
    "DETAILS": {
        "REGISTRATION": "Check-in",
        "BOARDING": "Boarding",
        "DEBOARDING": "Deboarding",
        "AIRCRAFT": "Aircraft",
        "MEAL": "Meal",
        "ON_BOARD_SERVICES": "On-board services",
        "SCHEDULED": "Scheduled",
        "ACTUAL": "Actual",
        "MEAL_ECONOMY": "Economy",
        "MEAL_COMFORT": "Comfort",
        "MEAL_BUSINESS": "Business",
        "STATUS_FINISHED": "Finished",
        "STATUS_EXPECTED": "Expected",
        "STATUS_IN_PROGRESS": "In progress",
        "STATUS_SPECIFIED": "Specified",
        "STATUS_SCHEDULED": "Scheduled",
        "TERMINAL": "Terminal",
        "GATE": "Gate",
        "BAG_BELT": "Baggage belt",
        "CONFIGURATION": "Configuration"
    },
```

- [ ] **Step 6: Add DETAILS i18n keys (ru)**

Edit `src/i18n/locales/ru/common.json`. Add the same structure with Russian translations:

```json
    "DETAILS": {
        "REGISTRATION": "Регистрация",
        "BOARDING": "Посадка",
        "DEBOARDING": "Высадка",
        "AIRCRAFT": "Воздушное судно",
        "MEAL": "Питание",
        "ON_BOARD_SERVICES": "Услуги на борту",
        "SCHEDULED": "По расписанию",
        "ACTUAL": "Фактически",
        "MEAL_ECONOMY": "Эконом",
        "MEAL_COMFORT": "Комфорт",
        "MEAL_BUSINESS": "Бизнес",
        "STATUS_FINISHED": "Завершена",
        "STATUS_EXPECTED": "Ожидается",
        "STATUS_IN_PROGRESS": "В процессе",
        "STATUS_SPECIFIED": "Назначена",
        "STATUS_SCHEDULED": "По расписанию",
        "TERMINAL": "Терминал",
        "GATE": "Выход",
        "BAG_BELT": "Лента выдачи багажа",
        "CONFIGURATION": "Компоновка"
    },
```

- [ ] **Step 7: Commit**

```bash
git add src/features/online-board/components/details-panels/shared.ts src/features/online-board/components/details-panels/shared.test.ts src/i18n/locales/en/common.json src/i18n/locales/ru/common.json
git commit -m "Add shared helpers and DETAILS i18n keys for flight details panels"
```

---

### Task 3: Copy Service Icon Assets

**Files:**
- Create: `src/features/online-board/components/details-panels/icons/` (directory with 11 SVGs)

- [ ] **Step 1: Create icons directory and copy SVGs**

Run:

```bash
mkdir -p src/features/online-board/components/details-panels/icons
cp ClientApp/src/assets/img/service-and-food-icons/shopping.svg src/features/online-board/components/details-panels/icons/
cp ClientApp/src/assets/img/service-and-food-icons/space.svg src/features/online-board/components/details-panels/icons/
cp ClientApp/src/assets/img/service-and-food-icons/taxi.svg src/features/online-board/components/details-panels/icons/
cp ClientApp/src/assets/img/service-and-food-icons/wifi.svg src/features/online-board/components/details-panels/icons/
cp ClientApp/src/assets/img/service-and-food-icons/gsm.svg src/features/online-board/components/details-panels/icons/
cp ClientApp/src/assets/img/service-and-food-icons/entertaintment.svg src/features/online-board/components/details-panels/icons/
cp ClientApp/src/assets/img/service-and-food-icons/seat_reservation.svg src/features/online-board/components/details-panels/icons/
cp ClientApp/src/assets/img/service-and-food-icons/comfort-plus.svg src/features/online-board/components/details-panels/icons/
cp ClientApp/src/assets/img/service-and-food-icons/econom.svg src/features/online-board/components/details-panels/icons/
cp ClientApp/src/assets/img/service-and-food-icons/comfort.svg src/features/online-board/components/details-panels/icons/
cp ClientApp/src/assets/img/service-and-food-icons/business.svg src/features/online-board/components/details-panels/icons/
```

Verify 11 files present:

```bash
ls src/features/online-board/components/details-panels/icons/ | wc -l
```

Expected: `11`

- [ ] **Step 2: Commit**

```bash
git add src/features/online-board/components/details-panels/icons/
git commit -m "Copy service and meal icon SVGs from Angular to React"
```

---

### Task 4: RegistrationPanel Component

**Files:**
- Create: `src/features/online-board/components/details-panels/RegistrationPanel.tsx`
- Create: `src/features/online-board/components/details-panels/RegistrationPanel.test.tsx`
- Create: `src/features/online-board/components/details-panels/panels.scss`

- [ ] **Step 1: Write failing tests**

Create `src/features/online-board/components/details-panels/RegistrationPanel.test.tsx`:

```tsx
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { RegistrationPanel } from "./RegistrationPanel.js";
import type { IFlightTransitionItem } from "../../types.js";

vi.mock("@/i18n/provider.js", () => ({
  useTranslation: () => ({ t: (k: string) => k }),
}));

const baseItem: IFlightTransitionItem = {
  start: {
    dayChange: { value: 0, title: "" },
    local: "10:00",
    localTime: "10:00",
    tzOffset: "+03:00",
    utc: "07:00",
  },
  end: {
    dayChange: { value: 0, title: "" },
    local: "10:30",
    localTime: "10:30",
    tzOffset: "+03:00",
    utc: "07:30",
  },
  status: "InProgress",
  isActual: true,
};

describe("RegistrationPanel", () => {
  it("renders start and end times", () => {
    render(<RegistrationPanel item={baseItem} />);
    expect(screen.getByText("10:00")).toBeTruthy();
    expect(screen.getByText("10:30")).toBeTruthy();
  });

  it("renders status label", () => {
    render(<RegistrationPanel item={baseItem} />);
    expect(screen.getByText("DETAILS.STATUS_IN_PROGRESS")).toBeTruthy();
  });

  it("renders only start time when end is missing", () => {
    const noEnd: IFlightTransitionItem = {
      ...baseItem,
      end: {
        dayChange: { value: 0, title: "" },
        local: "",
        localTime: "",
        tzOffset: "",
        utc: "",
      },
    };
    render(<RegistrationPanel item={noEnd} />);
    expect(screen.getByText("10:00")).toBeTruthy();
    expect(screen.queryByText("10:30")).toBeNull();
  });

  it("has data-testid for targeting in integration tests", () => {
    render(<RegistrationPanel item={baseItem} />);
    expect(screen.getByTestId("registration-panel")).toBeTruthy();
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm vitest run src/features/online-board/components/details-panels/RegistrationPanel.test.tsx`

Expected: FAIL — `RegistrationPanel` not defined.

- [ ] **Step 3: Create panels shared stylesheet**

Create `src/features/online-board/components/details-panels/panels.scss`:

```scss
.details-panel {
  padding: 12px 0;
  font-size: 14px;
  line-height: 1.5;
}

.details-panel__row {
  display: flex;
  justify-content: space-between;
  padding: 6px 0;
  border-bottom: 1px solid #eee;

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

.details-panel__label {
  color: #666;
}

.details-panel__value {
  color: #000;
  font-weight: 500;
}

.details-panel__status {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 4px;
  background: #f0f4f8;
  color: #2060c0;
  font-size: 12px;
}

.details-panel__icons {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
}

.details-panel__icon {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  text-decoration: none;
  color: inherit;

  img {
    width: 24px;
    height: 24px;
  }
}
```

- [ ] **Step 4: Create RegistrationPanel.tsx**

```tsx
import type { FC } from "react";
import { useTranslation } from "@/i18n/provider.js";
import type { IFlightTransitionItem, FlightTransitionStatus } from "../../types.js";
import "./panels.scss";

const STATUS_KEYS: Record<FlightTransitionStatus, string> = {
  Finished: "DETAILS.STATUS_FINISHED",
  Expected: "DETAILS.STATUS_EXPECTED",
  InProgress: "DETAILS.STATUS_IN_PROGRESS",
  Specified: "DETAILS.STATUS_SPECIFIED",
  Scheduled: "DETAILS.STATUS_SCHEDULED",
};

export interface RegistrationPanelProps {
  item: IFlightTransitionItem;
}

export const RegistrationPanel: FC<RegistrationPanelProps> = ({ item }) => {
  const { t } = useTranslation();
  const hasEnd = Boolean(item.end?.local);

  return (
    <div className="details-panel" data-testid="registration-panel">
      <div className="details-panel__row">
        <span className="details-panel__label">{t("DETAILS.STATUS")}</span>
        <span className="details-panel__status">{t(STATUS_KEYS[item.status])}</span>
      </div>
      <div className="details-panel__row">
        <span className="details-panel__label">{t("DETAILS.SCHEDULED")}</span>
        <span className="details-panel__value">{item.start.local}</span>
      </div>
      {hasEnd && (
        <div className="details-panel__row">
          <span className="details-panel__label">{t("DETAILS.ACTUAL")}</span>
          <span className="details-panel__value">{item.end.local}</span>
        </div>
      )}
    </div>
  );
};
```

- [ ] **Step 5: Run test to verify it passes**

Run: `pnpm vitest run src/features/online-board/components/details-panels/RegistrationPanel.test.tsx`

Expected: PASS — all 4 tests pass.

- [ ] **Step 6: Commit**

```bash
git add src/features/online-board/components/details-panels/RegistrationPanel.tsx src/features/online-board/components/details-panels/RegistrationPanel.test.tsx src/features/online-board/components/details-panels/panels.scss
git commit -m "Add RegistrationPanel component for flight details accordion"
```

---

### Task 5: BoardingPanel Component

**Files:**
- Create: `src/features/online-board/components/details-panels/BoardingPanel.tsx`
- Create: `src/features/online-board/components/details-panels/BoardingPanel.test.tsx`

- [ ] **Step 1: Write failing tests**

Create `src/features/online-board/components/details-panels/BoardingPanel.test.tsx`:

```tsx
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { BoardingPanel } from "./BoardingPanel.js";
import type { IFlightTransitionItem } from "../../types.js";

vi.mock("@/i18n/provider.js", () => ({
  useTranslation: () => ({ t: (k: string) => k }),
}));

const baseItem: IFlightTransitionItem = {
  start: {
    dayChange: { value: 0, title: "" },
    local: "11:00",
    localTime: "11:00",
    tzOffset: "+03:00",
    utc: "08:00",
  },
  end: {
    dayChange: { value: 0, title: "" },
    local: "11:30",
    localTime: "11:30",
    tzOffset: "+03:00",
    utc: "08:30",
  },
  status: "Finished",
  isActual: true,
};

describe("BoardingPanel", () => {
  it("renders start and end times", () => {
    render(<BoardingPanel item={baseItem} />);
    expect(screen.getByText("11:00")).toBeTruthy();
    expect(screen.getByText("11:30")).toBeTruthy();
  });

  it("renders status label", () => {
    render(<BoardingPanel item={baseItem} />);
    expect(screen.getByText("DETAILS.STATUS_FINISHED")).toBeTruthy();
  });

  it("has data-testid", () => {
    render(<BoardingPanel item={baseItem} />);
    expect(screen.getByTestId("boarding-panel")).toBeTruthy();
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm vitest run src/features/online-board/components/details-panels/BoardingPanel.test.tsx`

Expected: FAIL — module not found.

- [ ] **Step 3: Create BoardingPanel.tsx**

```tsx
import type { FC } from "react";
import { useTranslation } from "@/i18n/provider.js";
import type { IFlightTransitionItem, FlightTransitionStatus } from "../../types.js";
import "./panels.scss";

const STATUS_KEYS: Record<FlightTransitionStatus, string> = {
  Finished: "DETAILS.STATUS_FINISHED",
  Expected: "DETAILS.STATUS_EXPECTED",
  InProgress: "DETAILS.STATUS_IN_PROGRESS",
  Specified: "DETAILS.STATUS_SPECIFIED",
  Scheduled: "DETAILS.STATUS_SCHEDULED",
};

export interface BoardingPanelProps {
  item: IFlightTransitionItem;
}

export const BoardingPanel: FC<BoardingPanelProps> = ({ item }) => {
  const { t } = useTranslation();
  const hasEnd = Boolean(item.end?.local);

  return (
    <div className="details-panel" data-testid="boarding-panel">
      <div className="details-panel__row">
        <span className="details-panel__label">{t("DETAILS.STATUS")}</span>
        <span className="details-panel__status">{t(STATUS_KEYS[item.status])}</span>
      </div>
      <div className="details-panel__row">
        <span className="details-panel__label">{t("DETAILS.SCHEDULED")}</span>
        <span className="details-panel__value">{item.start.local}</span>
      </div>
      {hasEnd && (
        <div className="details-panel__row">
          <span className="details-panel__label">{t("DETAILS.ACTUAL")}</span>
          <span className="details-panel__value">{item.end.local}</span>
        </div>
      )}
    </div>
  );
};
```

- [ ] **Step 4: Run test to verify it passes**

Run: `pnpm vitest run src/features/online-board/components/details-panels/BoardingPanel.test.tsx`

Expected: PASS — 3 tests pass.

- [ ] **Step 5: Commit**

```bash
git add src/features/online-board/components/details-panels/BoardingPanel.tsx src/features/online-board/components/details-panels/BoardingPanel.test.tsx
git commit -m "Add BoardingPanel component for flight details accordion"
```

---

### Task 6: DeboardingPanel Component

**Files:**
- Create: `src/features/online-board/components/details-panels/DeboardingPanel.tsx`
- Create: `src/features/online-board/components/details-panels/DeboardingPanel.test.tsx`

- [ ] **Step 1: Write failing tests**

Create `src/features/online-board/components/details-panels/DeboardingPanel.test.tsx`:

```tsx
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { DeboardingPanel } from "./DeboardingPanel.js";
import type { IFlightTransitionItem, IFlightLegArrivalStation } from "../../types.js";

vi.mock("@/i18n/provider.js", () => ({
  useTranslation: () => ({ t: (k: string) => k }),
}));

const baseItem: IFlightTransitionItem = {
  start: {
    dayChange: { value: 0, title: "" },
    local: "13:00",
    localTime: "13:00",
    tzOffset: "+03:00",
    utc: "10:00",
  },
  end: {
    dayChange: { value: 0, title: "" },
    local: "13:30",
    localTime: "13:30",
    tzOffset: "+03:00",
    utc: "10:30",
  },
  status: "InProgress",
  isActual: true,
};

const arrival: IFlightLegArrivalStation = {
  scheduled: { airport: "SVO", airportCode: "SVO", city: "Moscow", cityCode: "MOW", countryCode: "RU" },
  latest: { airport: "SVO", airportCode: "SVO", city: "Moscow", cityCode: "MOW", countryCode: "RU" },
  dispatch: "Terminal D",
  gate: "B23",
  terminal: "D",
  bagBelt: "5",
  times: {
    scheduledArrival: { dayChange: { value: 0, title: "" }, local: "", localTime: "", tzOffset: "", utc: "" },
  },
};

describe("DeboardingPanel", () => {
  it("renders transition times and status", () => {
    render(<DeboardingPanel item={baseItem} arrival={arrival} />);
    expect(screen.getByText("13:00")).toBeTruthy();
    expect(screen.getByText("DETAILS.STATUS_IN_PROGRESS")).toBeTruthy();
  });

  it("renders terminal, gate, baggage belt from arrival", () => {
    render(<DeboardingPanel item={baseItem} arrival={arrival} />);
    expect(screen.getByText("D")).toBeTruthy();
    expect(screen.getByText("5")).toBeTruthy();
  });

  it("omits terminal row when arrival has no terminal", () => {
    const noTerminal = { ...arrival, terminal: undefined };
    render(<DeboardingPanel item={baseItem} arrival={noTerminal} />);
    expect(screen.queryByText("DETAILS.TERMINAL")).toBeNull();
  });

  it("omits bag belt row when arrival has no bagBelt", () => {
    const noBelt = { ...arrival, bagBelt: undefined };
    render(<DeboardingPanel item={baseItem} arrival={noBelt} />);
    expect(screen.queryByText("DETAILS.BAG_BELT")).toBeNull();
  });

  it("has data-testid", () => {
    render(<DeboardingPanel item={baseItem} arrival={arrival} />);
    expect(screen.getByTestId("deboarding-panel")).toBeTruthy();
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm vitest run src/features/online-board/components/details-panels/DeboardingPanel.test.tsx`

Expected: FAIL — module not found.

- [ ] **Step 3: Create DeboardingPanel.tsx**

```tsx
import type { FC } from "react";
import { useTranslation } from "@/i18n/provider.js";
import type {
  IFlightTransitionItem,
  IFlightLegArrivalStation,
  FlightTransitionStatus,
} from "../../types.js";
import "./panels.scss";

const STATUS_KEYS: Record<FlightTransitionStatus, string> = {
  Finished: "DETAILS.STATUS_FINISHED",
  Expected: "DETAILS.STATUS_EXPECTED",
  InProgress: "DETAILS.STATUS_IN_PROGRESS",
  Specified: "DETAILS.STATUS_SPECIFIED",
  Scheduled: "DETAILS.STATUS_SCHEDULED",
};

export interface DeboardingPanelProps {
  item: IFlightTransitionItem;
  arrival: IFlightLegArrivalStation;
}

export const DeboardingPanel: FC<DeboardingPanelProps> = ({ item, arrival }) => {
  const { t } = useTranslation();
  const hasEnd = Boolean(item.end?.local);

  return (
    <div className="details-panel" data-testid="deboarding-panel">
      <div className="details-panel__row">
        <span className="details-panel__label">{t("DETAILS.STATUS")}</span>
        <span className="details-panel__status">{t(STATUS_KEYS[item.status])}</span>
      </div>
      <div className="details-panel__row">
        <span className="details-panel__label">{t("DETAILS.SCHEDULED")}</span>
        <span className="details-panel__value">{item.start.local}</span>
      </div>
      {hasEnd && (
        <div className="details-panel__row">
          <span className="details-panel__label">{t("DETAILS.ACTUAL")}</span>
          <span className="details-panel__value">{item.end.local}</span>
        </div>
      )}
      {arrival.terminal && (
        <div className="details-panel__row">
          <span className="details-panel__label">{t("DETAILS.TERMINAL")}</span>
          <span className="details-panel__value">{arrival.terminal}</span>
        </div>
      )}
      {arrival.gate && (
        <div className="details-panel__row">
          <span className="details-panel__label">{t("DETAILS.GATE")}</span>
          <span className="details-panel__value">{arrival.gate}</span>
        </div>
      )}
      {arrival.bagBelt && (
        <div className="details-panel__row">
          <span className="details-panel__label">{t("DETAILS.BAG_BELT")}</span>
          <span className="details-panel__value">{arrival.bagBelt}</span>
        </div>
      )}
    </div>
  );
};
```

- [ ] **Step 4: Run test to verify it passes**

Run: `pnpm vitest run src/features/online-board/components/details-panels/DeboardingPanel.test.tsx`

Expected: PASS — 5 tests pass.

- [ ] **Step 5: Commit**

```bash
git add src/features/online-board/components/details-panels/DeboardingPanel.tsx src/features/online-board/components/details-panels/DeboardingPanel.test.tsx
git commit -m "Add DeboardingPanel component for flight details accordion"
```

---

### Task 7: AircraftPanel Component

**Files:**
- Create: `src/features/online-board/components/details-panels/AircraftPanel.tsx`
- Create: `src/features/online-board/components/details-panels/AircraftPanel.test.tsx`

- [ ] **Step 1: Write failing tests**

Create `src/features/online-board/components/details-panels/AircraftPanel.test.tsx`:

```tsx
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { AircraftPanel } from "./AircraftPanel.js";
import type { IEquipmentFull } from "../../types.js";

vi.mock("@/i18n/provider.js", () => ({
  useTranslation: () => ({ t: (k: string) => k }),
}));

describe("AircraftPanel", () => {
  it("renders actual title when present", () => {
    const eq: IEquipmentFull = { aircraft: { actual: { title: "Airbus A321" } } };
    render(<AircraftPanel equipment={eq} />);
    expect(screen.getByText("Airbus A321")).toBeTruthy();
  });

  it("falls back to scheduled when actual.title is empty", () => {
    const eq: IEquipmentFull = {
      aircraft: { actual: { title: "" }, scheduled: { title: "Boeing 737" } },
    };
    render(<AircraftPanel equipment={eq} />);
    expect(screen.getByText("Boeing 737")).toBeTruthy();
  });

  it("renders both actual and scheduled when they differ", () => {
    const eq: IEquipmentFull = {
      aircraft: {
        actual: { title: "Airbus A321" },
        scheduled: { title: "Airbus A320" },
      },
    };
    render(<AircraftPanel equipment={eq} />);
    expect(screen.getByText("Airbus A321")).toBeTruthy();
    expect(screen.getByText("Airbus A320")).toBeTruthy();
  });

  it("renders configuration when present", () => {
    const eq: IEquipmentFull = {
      aircraft: { actual: { title: "A320" }, configuration: "C12Y138" },
    };
    render(<AircraftPanel equipment={eq} />);
    expect(screen.getByText("C12Y138")).toBeTruthy();
  });

  it("has data-testid", () => {
    const eq: IEquipmentFull = { aircraft: { actual: { title: "A320" } } };
    render(<AircraftPanel equipment={eq} />);
    expect(screen.getByTestId("aircraft-panel")).toBeTruthy();
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm vitest run src/features/online-board/components/details-panels/AircraftPanel.test.tsx`

Expected: FAIL — module not found.

- [ ] **Step 3: Create AircraftPanel.tsx**

```tsx
import type { FC } from "react";
import { useTranslation } from "@/i18n/provider.js";
import type { IEquipmentFull } from "../../types.js";
import "./panels.scss";

export interface AircraftPanelProps {
  equipment: IEquipmentFull;
}

export const AircraftPanel: FC<AircraftPanelProps> = ({ equipment }) => {
  const { t } = useTranslation();
  const aircraft = equipment.aircraft;
  const actualTitle = aircraft?.actual?.title;
  const scheduledTitle = aircraft?.scheduled?.title;
  const configuration = aircraft?.configuration;

  // If actual has content and differs from scheduled, show both; otherwise
  // fall back to whichever is present.
  const showBoth = Boolean(actualTitle && scheduledTitle && actualTitle !== scheduledTitle);
  const primaryTitle = actualTitle || scheduledTitle;

  return (
    <div className="details-panel" data-testid="aircraft-panel">
      {showBoth ? (
        <>
          <div className="details-panel__row">
            <span className="details-panel__label">{t("DETAILS.ACTUAL")}</span>
            <span className="details-panel__value">{actualTitle}</span>
          </div>
          <div className="details-panel__row">
            <span className="details-panel__label">{t("DETAILS.SCHEDULED")}</span>
            <span className="details-panel__value">{scheduledTitle}</span>
          </div>
        </>
      ) : (
        primaryTitle && (
          <div className="details-panel__row">
            <span className="details-panel__label">{t("DETAILS.AIRCRAFT")}</span>
            <span className="details-panel__value">{primaryTitle}</span>
          </div>
        )
      )}
      {configuration && (
        <div className="details-panel__row">
          <span className="details-panel__label">{t("DETAILS.CONFIGURATION")}</span>
          <span className="details-panel__value">{configuration}</span>
        </div>
      )}
    </div>
  );
};
```

- [ ] **Step 4: Run test to verify it passes**

Run: `pnpm vitest run src/features/online-board/components/details-panels/AircraftPanel.test.tsx`

Expected: PASS — 5 tests pass.

- [ ] **Step 5: Commit**

```bash
git add src/features/online-board/components/details-panels/AircraftPanel.tsx src/features/online-board/components/details-panels/AircraftPanel.test.tsx
git commit -m "Add AircraftPanel component for flight details accordion"
```

---

### Task 8: MealPanel Component

**Files:**
- Create: `src/features/online-board/components/details-panels/MealPanel.tsx`
- Create: `src/features/online-board/components/details-panels/MealPanel.test.tsx`

- [ ] **Step 1: Write failing tests**

Create `src/features/online-board/components/details-panels/MealPanel.test.tsx`:

```tsx
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { MealPanel } from "./MealPanel.js";
import type { IMealItem } from "../../types.js";

vi.mock("@/i18n/provider.js", () => ({
  useTranslation: () => ({ t: (k: string) => k }),
}));

describe("MealPanel", () => {
  it("renders an icon for Economy meal", () => {
    const meals: IMealItem[] = [{ type: "Economy" }];
    render(<MealPanel meals={meals} />);
    expect(screen.getByTestId("meal-icon-Economy")).toBeTruthy();
  });

  it("renders icons for multiple meal types", () => {
    const meals: IMealItem[] = [
      { type: "Economy" }, { type: "Comfort" }, { type: "Business" },
    ];
    render(<MealPanel meals={meals} />);
    expect(screen.getByTestId("meal-icon-Economy")).toBeTruthy();
    expect(screen.getByTestId("meal-icon-Comfort")).toBeTruthy();
    expect(screen.getByTestId("meal-icon-Business")).toBeTruthy();
  });

  it("skips Special type (no link defined)", () => {
    const meals: IMealItem[] = [{ type: "Special" }, { type: "Economy" }];
    render(<MealPanel meals={meals} />);
    expect(screen.queryByTestId("meal-icon-Special")).toBeNull();
    expect(screen.getByTestId("meal-icon-Economy")).toBeTruthy();
  });

  it("meal icon is wrapped in a link to aeroflot.ru", () => {
    const meals: IMealItem[] = [{ type: "Economy" }];
    render(<MealPanel meals={meals} />);
    const link = screen.getByTestId("meal-icon-Economy").closest("a");
    expect(link?.getAttribute("href")).toContain("aeroflot.ru");
    expect(link?.getAttribute("href")).toContain("meal-type_0");
  });

  it("has panel data-testid", () => {
    const meals: IMealItem[] = [{ type: "Economy" }];
    render(<MealPanel meals={meals} />);
    expect(screen.getByTestId("meal-panel")).toBeTruthy();
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm vitest run src/features/online-board/components/details-panels/MealPanel.test.tsx`

Expected: FAIL — module not found.

- [ ] **Step 3: Create MealPanel.tsx**

```tsx
import type { FC } from "react";
import { useTranslation } from "@/i18n/provider.js";
import type { IMealItem, MealType } from "../../types.js";
import { MEAL_LINKS } from "./shared.js";
import "./panels.scss";

const MEAL_ICON_FILES: Record<Exclude<MealType, "Special">, string> = {
  Economy: "econom",
  Comfort: "comfort",
  Business: "business",
};

const MEAL_LABEL_KEYS: Record<Exclude<MealType, "Special">, string> = {
  Economy: "DETAILS.MEAL_ECONOMY",
  Comfort: "DETAILS.MEAL_COMFORT",
  Business: "DETAILS.MEAL_BUSINESS",
};

export interface MealPanelProps {
  meals: IMealItem[];
}

export const MealPanel: FC<MealPanelProps> = ({ meals }) => {
  const { t } = useTranslation();
  const types = new Set(meals.map((m) => m.type));

  return (
    <div className="details-panel" data-testid="meal-panel">
      <div className="details-panel__icons">
        {(["Economy", "Comfort", "Business"] as const).map((type) =>
          types.has(type) ? (
            <a
              key={type}
              href={MEAL_LINKS[type]}
              target="_blank"
              rel="noopener noreferrer"
              className="details-panel__icon"
            >
              <img
                src={`./icons/${MEAL_ICON_FILES[type]}.svg`}
                alt={t(MEAL_LABEL_KEYS[type])}
                data-testid={`meal-icon-${type}`}
              />
              <span>{t(MEAL_LABEL_KEYS[type])}</span>
            </a>
          ) : null,
        )}
      </div>
    </div>
  );
};
```

Note: SVG import via relative path assumes Modern.js/Rspack handles SVG as file assets. If it doesn't, the task will fail the next step — swap to `import econom from "./icons/econom.svg"` style imports and use the imported URLs.

- [ ] **Step 4: Run test to verify it passes**

Run: `pnpm vitest run src/features/online-board/components/details-panels/MealPanel.test.tsx`

Expected: PASS — 5 tests pass.

- [ ] **Step 5: Commit**

```bash
git add src/features/online-board/components/details-panels/MealPanel.tsx src/features/online-board/components/details-panels/MealPanel.test.tsx
git commit -m "Add MealPanel component with meal-type icons and aeroflot.ru links"
```

---

### Task 9: ServicesPanel Component

**Files:**
- Create: `src/features/online-board/components/details-panels/ServicesPanel.tsx`
- Create: `src/features/online-board/components/details-panels/ServicesPanel.test.tsx`

- [ ] **Step 1: Write failing tests**

Create `src/features/online-board/components/details-panels/ServicesPanel.test.tsx`:

```tsx
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { ServicesPanel } from "./ServicesPanel.js";
import type { IOnBoardService } from "../../types.js";

vi.mock("@/i18n/provider.js", () => ({
  useTranslation: () => ({ t: (k: string) => k }),
}));

describe("ServicesPanel", () => {
  it("renders an icon for each service", () => {
    const services: IOnBoardService[] = [{ id: 1 }, { id: 4 }, { id: 8 }];
    render(<ServicesPanel services={services} />);
    expect(screen.getByTestId("service-icon-1")).toBeTruthy();
    expect(screen.getByTestId("service-icon-4")).toBeTruthy();
    expect(screen.getByTestId("service-icon-8")).toBeTruthy();
  });

  it("maps service id 1 to shopping icon", () => {
    render(<ServicesPanel services={[{ id: 1 }]} />);
    const img = screen.getByTestId("service-icon-1") as HTMLImageElement;
    expect(img.src).toContain("shopping");
  });

  it("maps service id 4 to wifi icon", () => {
    render(<ServicesPanel services={[{ id: 4 }]} />);
    const img = screen.getByTestId("service-icon-4") as HTMLImageElement;
    expect(img.src).toContain("wifi");
  });

  it("uses fallback icon for unknown id", () => {
    render(<ServicesPanel services={[{ id: 99 }]} />);
    const img = screen.getByTestId("service-icon-99") as HTMLImageElement;
    expect(img.src).toContain("comfort-plus");
  });

  it("wraps icon in a link when service has url", () => {
    const services: IOnBoardService[] = [
      { id: 4, url: "https://wifi.example" },
    ];
    render(<ServicesPanel services={services} />);
    const link = screen.getByTestId("service-icon-4").closest("a");
    expect(link?.getAttribute("href")).toBe("https://wifi.example");
  });

  it("does not wrap icon in link when url is missing", () => {
    render(<ServicesPanel services={[{ id: 4 }]} />);
    const link = screen.getByTestId("service-icon-4").closest("a");
    expect(link).toBeNull();
  });

  it("has panel data-testid", () => {
    render(<ServicesPanel services={[{ id: 1 }]} />);
    expect(screen.getByTestId("services-panel")).toBeTruthy();
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm vitest run src/features/online-board/components/details-panels/ServicesPanel.test.tsx`

Expected: FAIL — module not found.

- [ ] **Step 3: Create ServicesPanel.tsx**

```tsx
import type { FC } from "react";
import { useTranslation } from "@/i18n/provider.js";
import type { IOnBoardService } from "../../types.js";
import { SERVICE_ICON_MAP, SERVICE_ICON_FALLBACK } from "./shared.js";
import "./panels.scss";

export interface ServicesPanelProps {
  services: IOnBoardService[];
}

export const ServicesPanel: FC<ServicesPanelProps> = ({ services }) => {
  const { t } = useTranslation();

  return (
    <div className="details-panel" data-testid="services-panel">
      <div className="details-panel__icons">
        {services.map((svc) => {
          const iconName = SERVICE_ICON_MAP[svc.id] ?? SERVICE_ICON_FALLBACK;
          const iconSrc = `./icons/${iconName}.svg`;
          const alt = svc.title ?? `service-${svc.id}`;
          const img = (
            <img
              src={iconSrc}
              alt={alt}
              data-testid={`service-icon-${svc.id}`}
            />
          );

          return svc.url ? (
            <a
              key={svc.id}
              href={svc.url}
              target="_blank"
              rel="noopener noreferrer"
              className="details-panel__icon"
            >
              {img}
              {svc.title && <span>{svc.title}</span>}
            </a>
          ) : (
            <span key={svc.id} className="details-panel__icon">
              {img}
              {svc.title && <span>{svc.title}</span>}
            </span>
          );
        })}
      </div>
    </div>
  );
};
```

- [ ] **Step 4: Run test to verify it passes**

Run: `pnpm vitest run src/features/online-board/components/details-panels/ServicesPanel.test.tsx`

Expected: PASS — 7 tests pass.

- [ ] **Step 5: Commit**

```bash
git add src/features/online-board/components/details-panels/ServicesPanel.tsx src/features/online-board/components/details-panels/ServicesPanel.test.tsx
git commit -m "Add ServicesPanel component for on-board service icons"
```

---

### Task 10: FlightDetailsAccordion Container

**Files:**
- Create: `src/features/online-board/components/details-panels/FlightDetailsAccordion.tsx`
- Create: `src/features/online-board/components/details-panels/FlightDetailsAccordion.scss`
- Create: `src/features/online-board/components/details-panels/FlightDetailsAccordion.test.tsx`

- [ ] **Step 1: Write failing tests**

Create `src/features/online-board/components/details-panels/FlightDetailsAccordion.test.tsx`:

```tsx
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { FlightDetailsAccordion } from "./FlightDetailsAccordion.js";
import type { IFlightLeg } from "../../types.js";

vi.mock("@/i18n/provider.js", () => ({
  useTranslation: () => ({ t: (k: string) => k }),
}));

function makeLeg(overrides: Partial<IFlightLeg> = {}): IFlightLeg {
  const base: IFlightLeg = {
    arrival: {
      scheduled: { airport: "SVO", airportCode: "SVO", city: "Moscow", cityCode: "MOW", countryCode: "RU" },
      latest: { airport: "SVO", airportCode: "SVO", city: "Moscow", cityCode: "MOW", countryCode: "RU" },
      dispatch: "",
      gate: "",
      terminal: "",
      times: { scheduledArrival: { dayChange: { value: 0, title: "" }, local: "", localTime: "", tzOffset: "", utc: "" } },
    },
    dayChange: 0,
    departure: {
      scheduled: { airport: "JFK", airportCode: "JFK", city: "New York", cityCode: "NYC", countryCode: "US" },
      latest: { airport: "JFK", airportCode: "JFK", city: "New York", cityCode: "NYC", countryCode: "US" },
      dispatch: "",
      gate: "",
      terminal: "",
      checkingStatus: "Scheduled",
      parkingStand: "",
      times: { scheduledDeparture: { dayChange: { value: 0, title: "" }, local: "", localTime: "", tzOffset: "", utc: "" } },
    },
    equipment: { name: "A320" },
    flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false },
    flyingTime: "1h 30m",
    index: 0,
    operatingBy: {},
    status: "Scheduled",
    updated: "",
  };
  return { ...base, ...overrides };
}

describe("FlightDetailsAccordion", () => {
  it("returns null when no panels should be visible", () => {
    const leg = makeLeg();  // no transition, no extended equipment
    const { container } = render(<FlightDetailsAccordion leg={leg} viewType="Onlineboard" />);
    expect(container.firstChild).toBeNull();
  });

  it("renders registration tab when transition.registration is active", () => {
    const leg = makeLeg({
      transition: {
        registration: {
          start: { dayChange: { value: 0, title: "" }, local: "10:00", localTime: "10:00", tzOffset: "", utc: "" },
          end: { dayChange: { value: 0, title: "" }, local: "10:30", localTime: "10:30", tzOffset: "", utc: "" },
          status: "InProgress",
          isActual: true,
        },
      },
    });
    render(<FlightDetailsAccordion leg={leg} viewType="Onlineboard" />);
    expect(screen.getByTestId("flight-details-accordion")).toBeTruthy();
    expect(screen.getByText("DETAILS.REGISTRATION")).toBeTruthy();
  });

  it("hides transition panels when viewType is Schedule", () => {
    const leg = makeLeg({
      transition: {
        registration: {
          start: { dayChange: { value: 0, title: "" }, local: "10:00", localTime: "", tzOffset: "", utc: "" },
          end: { dayChange: { value: 0, title: "" }, local: "", localTime: "", tzOffset: "", utc: "" },
          status: "InProgress",
          isActual: true,
        },
      },
    });
    const { container } = render(<FlightDetailsAccordion leg={leg} viewType="Schedule" />);
    // No panels visible -> container returns null
    expect(container.firstChild).toBeNull();
  });

  it("renders aircraft tab when equipment.aircraft has title", () => {
    const leg = makeLeg({
      equipment: { name: "A320", aircraft: { actual: { title: "Airbus A320" } } },
    });
    render(<FlightDetailsAccordion leg={leg} viewType="Onlineboard" />);
    expect(screen.getByText("DETAILS.AIRCRAFT")).toBeTruthy();
  });

  it("renders meal tab when equipment.meal has items", () => {
    const leg = makeLeg({
      equipment: { name: "A320", meal: [{ type: "Economy" }] },
    });
    render(<FlightDetailsAccordion leg={leg} viewType="Onlineboard" />);
    expect(screen.getByText("DETAILS.MEAL")).toBeTruthy();
  });

  it("renders services tab when aircraft.actual.onBoardServices has items", () => {
    const leg = makeLeg({
      equipment: { name: "A320", aircraft: { actual: { title: "A320", onBoardServices: [{ id: 1 }] } } },
    });
    render(<FlightDetailsAccordion leg={leg} viewType="Onlineboard" />);
    expect(screen.getByText("DETAILS.ON_BOARD_SERVICES")).toBeTruthy();
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm vitest run src/features/online-board/components/details-panels/FlightDetailsAccordion.test.tsx`

Expected: FAIL — module not found.

- [ ] **Step 3: Create FlightDetailsAccordion.scss**

```scss
.flight-details-accordion {
  margin-top: 16px;

  .p-accordion-tab {
    border: 1px solid #e0e0e0;
    border-radius: 4px;
    margin-bottom: 8px;
    overflow: hidden;
  }

  .p-accordion-header {
    padding: 12px 16px;
    background: #f8f9fa;
    cursor: pointer;
    font-weight: 500;
    display: flex;
    justify-content: space-between;
    align-items: center;

    &:hover {
      background: #eef1f4;
    }
  }

  .p-accordion-content {
    padding: 0 16px 12px;
    background: #fff;
  }
}
```

- [ ] **Step 4: Create FlightDetailsAccordion.tsx**

```tsx
import { type FC, useState } from "react";
import { useTranslation } from "@/i18n/provider.js";
import type { IFlightLeg } from "../../types.js";
import { shouldShowTransition, shouldShowAircraft, type DetailsViewType } from "./shared.js";
import { RegistrationPanel } from "./RegistrationPanel.js";
import { BoardingPanel } from "./BoardingPanel.js";
import { DeboardingPanel } from "./DeboardingPanel.js";
import { AircraftPanel } from "./AircraftPanel.js";
import { MealPanel } from "./MealPanel.js";
import { ServicesPanel } from "./ServicesPanel.js";
import "./FlightDetailsAccordion.scss";

export interface FlightDetailsAccordionProps {
  leg: IFlightLeg;
  viewType: DetailsViewType;
}

interface PanelDef {
  id: string;
  header: string;
  content: JSX.Element;
}

export const FlightDetailsAccordion: FC<FlightDetailsAccordionProps> = ({ leg, viewType }) => {
  const { t } = useTranslation();
  const [openIds, setOpenIds] = useState<Set<string>>(new Set());

  const panels: PanelDef[] = [];

  if (shouldShowTransition(leg.transition?.registration, leg.status, viewType)) {
    panels.push({
      id: "registration",
      header: t("DETAILS.REGISTRATION"),
      content: <RegistrationPanel item={leg.transition!.registration!} />,
    });
  }
  if (shouldShowTransition(leg.transition?.boarding, leg.status, viewType)) {
    panels.push({
      id: "boarding",
      header: t("DETAILS.BOARDING"),
      content: <BoardingPanel item={leg.transition!.boarding!} />,
    });
  }
  if (shouldShowTransition(leg.transition?.deboarding, leg.status, viewType)) {
    panels.push({
      id: "deboarding",
      header: t("DETAILS.DEBOARDING"),
      content: <DeboardingPanel item={leg.transition!.deboarding!} arrival={leg.arrival} />,
    });
  }
  if (shouldShowAircraft(leg.equipment)) {
    panels.push({
      id: "aircraft",
      header: t("DETAILS.AIRCRAFT"),
      content: <AircraftPanel equipment={leg.equipment} />,
    });
  }
  if ((leg.equipment.meal?.length ?? 0) > 0) {
    panels.push({
      id: "meal",
      header: t("DETAILS.MEAL"),
      content: <MealPanel meals={leg.equipment.meal!} />,
    });
  }
  if ((leg.equipment.aircraft?.actual?.onBoardServices?.length ?? 0) > 0) {
    panels.push({
      id: "services",
      header: t("DETAILS.ON_BOARD_SERVICES"),
      content: <ServicesPanel services={leg.equipment.aircraft!.actual!.onBoardServices!} />,
    });
  }

  if (panels.length === 0) return null;

  const toggle = (id: string) => {
    setOpenIds((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  };

  return (
    <div className="flight-details-accordion p-accordion" data-testid="flight-details-accordion">
      {panels.map((panel) => {
        const isOpen = openIds.has(panel.id);
        return (
          <div
            key={panel.id}
            className={`p-accordion-tab${isOpen ? " p-accordion-tab--active" : ""}`}
            data-testid={`accordion-tab-${panel.id}`}
          >
            <div
              className={`p-accordion-header${isOpen ? " p-highlight" : ""}`}
              role="button"
              tabIndex={0}
              onClick={() => toggle(panel.id)}
              onKeyDown={(e) => {
                if (e.key === "Enter" || e.key === " ") {
                  e.preventDefault();
                  toggle(panel.id);
                }
              }}
            >
              <span>{panel.header}</span>
              <span aria-hidden="true">{isOpen ? "▲" : "▼"}</span>
            </div>
            {isOpen && <div className="p-accordion-content">{panel.content}</div>}
          </div>
        );
      })}
    </div>
  );
};
```

- [ ] **Step 5: Run test to verify it passes**

Run: `pnpm vitest run src/features/online-board/components/details-panels/FlightDetailsAccordion.test.tsx`

Expected: PASS — 6 tests pass.

- [ ] **Step 6: Commit**

```bash
git add src/features/online-board/components/details-panels/FlightDetailsAccordion.tsx src/features/online-board/components/details-panels/FlightDetailsAccordion.scss src/features/online-board/components/details-panels/FlightDetailsAccordion.test.tsx
git commit -m "Add FlightDetailsAccordion container orchestrating 6 panel components"
```

---

### Task 11: Wire Accordion Into OnlineBoardDetailsPage

**Files:**
- Modify: `src/features/online-board/components/OnlineBoardDetailsPage.tsx` (inside `FlightLegs` component around lines 35-119)

- [ ] **Step 1: Update OnlineBoardDetailsPage.test.tsx to verify accordion integration**

Add this test to the existing `src/features/online-board/components/OnlineBoardDetailsPage.test.tsx` describe block (append at end):

```tsx
describe("accordion integration", () => {
  it("does not render accordion when leg has no transition or extended equipment", () => {
    render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />);
    expect(screen.queryByTestId("flight-details-accordion")).toBeNull();
  });

  it("renders accordion when leg has transition data", () => {
    vi.mocked(useFlightDetails).mockReturnValueOnce({
      flight: {
        ...mockFlight,
        leg: {
          ...mockFlight.leg,
          transition: {
            registration: {
              start: { dayChange: { value: 0, title: "" }, local: "10:00", localTime: "", tzOffset: "", utc: "" },
              end: { dayChange: { value: 0, title: "" }, local: "", localTime: "", tzOffset: "", utc: "" },
              status: "InProgress",
              isActual: true,
            },
          },
        },
      },
      loading: false,
      error: null,
    });
    render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />);
    expect(screen.getByTestId("flight-details-accordion")).toBeTruthy();
  });
});
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `pnpm vitest run src/features/online-board/components/OnlineBoardDetailsPage.test.tsx`

Expected: FAIL — the 2 new tests fail because the accordion isn't wired in yet.

- [ ] **Step 3: Wire the accordion into FlightLegs**

Edit `src/features/online-board/components/OnlineBoardDetailsPage.tsx`:

Add import near the top (after existing component imports, around line 18):

```tsx
import { FlightDetailsAccordion } from "./details-panels/FlightDetailsAccordion.js";
```

In the `FlightLegs` function, replace the final inline aircraft block (currently lines 109-114):

```tsx
          {leg.equipment.name && (
            <div className="flight-details__aircraft">
              Aircraft: {leg.equipment.name}
              {leg.equipment.code ? ` (${leg.equipment.code})` : ""}
            </div>
          )}
```

With the accordion call:

```tsx
          {leg.equipment.name && (
            <div className="flight-details__aircraft">
              Aircraft: {leg.equipment.name}
              {leg.equipment.code ? ` (${leg.equipment.code})` : ""}
            </div>
          )}
          <FlightDetailsAccordion leg={leg} viewType="Onlineboard" />
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `pnpm vitest run src/features/online-board/components/OnlineBoardDetailsPage.test.tsx`

Expected: PASS — all existing tests still pass plus the 2 new accordion integration tests.

- [ ] **Step 5: Run full test suite to check for regressions**

Run: `pnpm test`

Expected: No new test failures beyond any pre-existing ones.

- [ ] **Step 6: Commit**

```bash
git add src/features/online-board/components/OnlineBoardDetailsPage.tsx src/features/online-board/components/OnlineBoardDetailsPage.test.tsx
git commit -m "Wire FlightDetailsAccordion into OnlineBoardDetailsPage per leg"
```

---

### Task 12: Manual Browser Verification

**Files:** None — verification only.

- [ ] **Step 1: Start dev server with API proxy**

Run: `pnpm dev:full`

Wait for "Dev server: http://localhost:8080" message.

- [ ] **Step 2: Open a flight details page in the browser**

Navigate to: `http://localhost:8080/ru/onlineboard/SU0022-16042026`

Even if the real API returns empty `transition`/`equipment` fields (WAF-blocked dev environment), the page should render without errors and the accordion should either render nothing (no data) or render the Aircraft panel if `equipment.aircraft.actual.title` is populated.

Acceptance:
- Page loads without console errors
- If flight data has accordion-worthy fields, the accordion is visible with clickable panel headers
- Clicking a panel expands it to show the panel content
- Clicking again collapses it

- [ ] **Step 3: Run visual comparison pipeline against Angular**

Run: `pnpm compare:visual`

Check `comparison-report/visual/report.html` for `onlineboard-details` rows. The diff percentage for the details page should be lower than the previous run (baseline was 36-72% depending on viewport). Any reduction confirms the accordion is contributing content closer to Angular's layout.

If Angular's dev server isn't running on port 4200, start it: `cd ClientApp && NODE_OPTIONS=--openssl-legacy-provider npx ng serve --port 4200`.

- [ ] **Step 4: No commit — verification is observational**
