Add back button + flight schedule timeline (B.6) design spec
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
# Back Button + Flight Schedule Timeline (B.6)
|
||||
|
||||
## Goal
|
||||
|
||||
Add two Angular-parity elements to the React Online Board details page: a back-navigation button in the header (replacing `PageTabs`), and a flight-schedule timeline showing scheduled times, flight duration, and a days-of-operation indicator with the week's valid-through date range.
|
||||
|
||||
## Scope
|
||||
|
||||
Sub-feature **B.6** of the Flight Details parity work. Online Board details page only.
|
||||
|
||||
## Current State
|
||||
|
||||
- React details page uses `<PageTabs viewType="onlineboard" />` in `PageLayout.headerLeft`.
|
||||
- No flight-schedule timeline component exists.
|
||||
- `IFlightLeg` type has no `daysOfWeek` field.
|
||||
|
||||
## Approach
|
||||
|
||||
Two small independent component families:
|
||||
|
||||
1. **`DetailsBackButton`** — stateless `<Link>`-based button styled as the Angular back button. Replaces `PageTabs` in the details page's `headerLeft` slot.
|
||||
2. **`FlightSchedule`** — single-tab PrimeReact `Accordion` with the Schedule section (times + duration) inside, plus a persistent Days-of-week strip + date-range note below.
|
||||
|
||||
## Types Extension
|
||||
|
||||
`src/features/online-board/types.ts` gets:
|
||||
|
||||
```typescript
|
||||
export interface IDaysOfWeek {
|
||||
current: string; // 7-char bit string for today (e.g., "1000010")
|
||||
flight: string; // 7-char bit string for scheduled days (e.g., "1111111")
|
||||
}
|
||||
```
|
||||
|
||||
And `IFlightLeg` gains:
|
||||
|
||||
```typescript
|
||||
export interface IFlightLeg {
|
||||
// ... existing fields ...
|
||||
daysOfWeek?: IDaysOfWeek;
|
||||
}
|
||||
```
|
||||
|
||||
Position 0 = Monday, position 6 = Sunday. Character `"1"` means active; any other character means inactive.
|
||||
|
||||
## Architecture
|
||||
|
||||
### `DetailsBackButton`
|
||||
|
||||
File: `src/features/online-board/components/DetailsBackButton/DetailsBackButton.tsx`.
|
||||
|
||||
```typescript
|
||||
export interface DetailsBackButtonProps {
|
||||
locale: string;
|
||||
}
|
||||
```
|
||||
|
||||
Renders:
|
||||
|
||||
```tsx
|
||||
<Link
|
||||
to={`/${locale}/onlineboard`}
|
||||
className="details-back-button"
|
||||
data-testid="details-back-button"
|
||||
>
|
||||
<span className="details-back-button__arrow" aria-hidden="true">←</span>
|
||||
<span>{t("SHARED.BACK-BOARD")}</span>
|
||||
</Link>
|
||||
```
|
||||
|
||||
Always visible. Uses React Router `<Link>` (client-side nav). No conditional rendering.
|
||||
|
||||
### `FlightSchedule`
|
||||
|
||||
File: `src/features/online-board/components/FlightSchedule/FlightSchedule.tsx`.
|
||||
|
||||
```typescript
|
||||
export interface FlightScheduleProps {
|
||||
flight: ISimpleFlight;
|
||||
}
|
||||
```
|
||||
|
||||
Renders `null` when `firstLeg.daysOfWeek?.flight` is undefined or empty string.
|
||||
|
||||
Otherwise renders:
|
||||
|
||||
1. **PrimeReact `<Accordion multiple={false} activeIndex={0}>`** with a single `<AccordionTab header={t("SHARED.SCHEDULE-FLIGHT")}>` containing:
|
||||
- Row: label `SHARED.DEPARTURE-SCHEDULED` + value `firstLeg.departure.times.scheduledDeparture.local`
|
||||
- Row: label `SHARED.ARRIVAL-SCHEDULED` + value `lastLeg.arrival.times.scheduledArrival.local`
|
||||
- Row: label `SHARED.PATH-TIME` + value `flight.flyingTime`
|
||||
2. **Below the accordion** (always visible):
|
||||
- Section title: `SHARED.DAYS-EXECUTE-FLIGHT`
|
||||
- `<DaysOfWeekStrip flightBitString={firstLeg.daysOfWeek.flight} />`
|
||||
- Note: `t("SHARED.NOTE-TIME-SCHEDULE").replace("{START_DATE}", start).replace("{END_DATE}", end)`
|
||||
|
||||
Where `start` / `end` come from `getWeekDateRange(firstLeg.departure.times.scheduledDeparture.local)`.
|
||||
|
||||
Default `activeIndex={0}` opens the tab on mount (matches Angular's `selected = true`).
|
||||
|
||||
### `DaysOfWeekStrip`
|
||||
|
||||
File: `src/features/online-board/components/FlightSchedule/DaysOfWeekStrip.tsx`.
|
||||
|
||||
```typescript
|
||||
export interface DaysOfWeekStripProps {
|
||||
flightBitString: string;
|
||||
}
|
||||
```
|
||||
|
||||
Renders 7 spans in a flex row. For index `i = 0..6`:
|
||||
|
||||
- Active if `flightBitString[i] === "1"`
|
||||
- Label: `t("DAYS." + (i + 1))` (keys `DAYS.1` through `DAYS.7`)
|
||||
- Class: `day` (active) or `day day--inactive` (inactive)
|
||||
|
||||
### `weekDateRange` helper
|
||||
|
||||
File: `src/features/online-board/components/FlightSchedule/weekDateRange.ts`.
|
||||
|
||||
```typescript
|
||||
import { parseISO, startOfISOWeek, endOfISOWeek, format, isValid } from "date-fns";
|
||||
|
||||
export function getWeekDateRange(scheduledLocal: string | undefined): {
|
||||
start: string;
|
||||
end: string;
|
||||
} {
|
||||
if (!scheduledLocal) return { start: "", end: "" };
|
||||
const d = parseISO(scheduledLocal);
|
||||
if (!isValid(d)) return { start: "", end: "" };
|
||||
return {
|
||||
start: format(startOfISOWeek(d), "dd.MM.yyyy"),
|
||||
end: format(endOfISOWeek(d), "dd.MM.yyyy"),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Pure function — no React, no side effects. ISO week starts on Monday, ends on Sunday.
|
||||
|
||||
### Integration
|
||||
|
||||
In `src/features/online-board/components/OnlineBoardDetailsPage.tsx`:
|
||||
|
||||
1. Replace `headerLeft={<PageTabs viewType="onlineboard" />}` with `headerLeft={<DetailsBackButton locale={locale} />}` in the main happy-path return.
|
||||
- Also replace in the loading/error/not-found branches' `commonLayoutProps` for consistency.
|
||||
2. After `<FlightLegs legs={legs} />` (or after the flying-time div), add `<FlightSchedule flight={displayFlight} />`.
|
||||
|
||||
## Styling
|
||||
|
||||
### `DetailsBackButton.scss`
|
||||
|
||||
```scss
|
||||
.details-back-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: #e3f0ff;
|
||||
color: #1a3a5c;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover { background: #c7dff5; }
|
||||
|
||||
&__arrow {
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `FlightSchedule.scss`
|
||||
|
||||
```scss
|
||||
.flight-schedule {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px 24px;
|
||||
margin-top: 16px;
|
||||
|
||||
.p-accordion-content {
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
&__label { color: #666; }
|
||||
&__value { font-weight: 500; }
|
||||
|
||||
&__days-section {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__note {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.days-of-week-strip {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.day {
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
background: #e6f1fb;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1a3a5c;
|
||||
|
||||
&--inactive {
|
||||
background: #f6f6f6;
|
||||
color: rgba(102, 102, 102, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### `DetailsBackButton.test.tsx`
|
||||
- Renders link with `href="/ru/onlineboard"` for `locale="ru"`
|
||||
- Renders link label from `SHARED.BACK-BOARD` key
|
||||
- Has `data-testid="details-back-button"`
|
||||
- Renders arrow icon
|
||||
|
||||
### `DaysOfWeekStrip.test.tsx`
|
||||
- Renders 7 day boxes
|
||||
- First 3 active for `"1110000"`, last 4 have `--inactive` modifier
|
||||
- All active for `"1111111"`
|
||||
- All inactive for `"0000000"`
|
||||
- Renders DAYS.1 through DAYS.7 labels in order
|
||||
|
||||
### `weekDateRange.test.ts`
|
||||
- Returns Monday/Sunday formatted `dd.MM.yyyy` for a mid-week date
|
||||
- Handles a Sunday input (returns that week's Mon-Sun)
|
||||
- Handles a Monday input (returns same day as start)
|
||||
- Returns `{ start: "", end: "" }` for invalid input
|
||||
- Returns `{ start: "", end: "" }` for undefined input
|
||||
|
||||
### `FlightSchedule.test.tsx`
|
||||
- Returns null when firstLeg has no `daysOfWeek`
|
||||
- Returns null when `daysOfWeek.flight` is empty string
|
||||
- Renders scheduled times + duration when `daysOfWeek` is present
|
||||
- Renders DaysOfWeekStrip with `daysOfWeek.flight`
|
||||
- Substitutes `{START_DATE}` and `{END_DATE}` into the note
|
||||
|
||||
### `OnlineBoardDetailsPage.test.tsx` (update existing)
|
||||
- `headerLeft` renders `DetailsBackButton` (add `data-testid` assertion)
|
||||
- `FlightSchedule` rendered below legs when `daysOfWeek` is present
|
||||
- `FlightSchedule` not rendered when `daysOfWeek` is absent
|
||||
|
||||
## i18n
|
||||
|
||||
All required keys already present in React locale files. Confirmed via grep:
|
||||
|
||||
- `DAYS.1` through `DAYS.7`
|
||||
- `SHARED.SCHEDULE-FLIGHT`, `SHARED.DAYS-EXECUTE-FLIGHT`, `SHARED.NOTE-TIME-SCHEDULE`
|
||||
- `SHARED.PATH-TIME`, `SHARED.DEPARTURE-SCHEDULED`, `SHARED.ARRIVAL-SCHEDULED`
|
||||
- `SHARED.BACK-BOARD`
|
||||
|
||||
No new keys needed.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Schedule feature's back button (would navigate to `/schedule` — different viewType)
|
||||
- Schedule feature's flight-schedule (only needed on Online Board details per Angular's `!flight.isMultiLeg` gate in Schedule variant)
|
||||
- Custom accordion primitive extraction (we use PrimeReact directly)
|
||||
- Changes to existing breadcrumbs (back button is additional navigation, not replacing breadcrumbs)
|
||||
|
||||
## Files Touched
|
||||
|
||||
### New
|
||||
|
||||
- `src/features/online-board/components/DetailsBackButton/DetailsBackButton.tsx`
|
||||
- `src/features/online-board/components/DetailsBackButton/DetailsBackButton.scss`
|
||||
- `src/features/online-board/components/DetailsBackButton/DetailsBackButton.test.tsx`
|
||||
- `src/features/online-board/components/DetailsBackButton/index.ts`
|
||||
- `src/features/online-board/components/FlightSchedule/FlightSchedule.tsx`
|
||||
- `src/features/online-board/components/FlightSchedule/FlightSchedule.scss`
|
||||
- `src/features/online-board/components/FlightSchedule/FlightSchedule.test.tsx`
|
||||
- `src/features/online-board/components/FlightSchedule/DaysOfWeekStrip.tsx`
|
||||
- `src/features/online-board/components/FlightSchedule/DaysOfWeekStrip.test.tsx`
|
||||
- `src/features/online-board/components/FlightSchedule/weekDateRange.ts`
|
||||
- `src/features/online-board/components/FlightSchedule/weekDateRange.test.ts`
|
||||
- `src/features/online-board/components/FlightSchedule/index.ts`
|
||||
|
||||
### Modified
|
||||
|
||||
- `src/features/online-board/types.ts` — add `IDaysOfWeek`, extend `IFlightLeg` with optional `daysOfWeek`
|
||||
- `src/features/online-board/types.test.ts` — test new types
|
||||
- `src/features/online-board/components/OnlineBoardDetailsPage.tsx` — swap PageTabs for DetailsBackButton in 4 PageLayout call sites; add `<FlightSchedule>` below legs
|
||||
- `src/features/online-board/components/OnlineBoardDetailsPage.test.tsx` — update mocks and add integration tests
|
||||
Reference in New Issue
Block a user