Add P3 implementation plan: filter + validation + search history + search execution
This commit is contained in:
@@ -0,0 +1,683 @@
|
||||
# P3 — Filter + Validation + Search History + Search Execution + Cancel 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:** Bring §4.1.9 (filter attributes + validation + search history), §4.1.10 (Online-Board search execution + errors), §4.1.11 (Schedule search execution + errors), and §4.1.12 (search cancellation) of TZ РИ-07-2538С into compliance.
|
||||
|
||||
**Architecture:** Filter components (`OnlineBoardFilter`, `ScheduleFilter`, `CityAutocomplete`, `CityPickerPopup`) and search hooks already exist from phase-1. P3 is primarily an **audit + gap-fill pass** against TZ Tables 11–18: verify each attribute's placeholder/format/behavior rules, add missing validation, fix today/tomorrow/current-week display substitutions, clean up the clear-button (X) UX, and tighten the «Вы искали» search history to match TZ 4.1.9.5.
|
||||
|
||||
**Tech Stack:** TypeScript, React 18, PrimeReact (Calendar, Slider), Vitest, Playwright.
|
||||
|
||||
**Parent spec:** `docs/superpowers/specs/2026-04-21-online-board-schedule-tz-redesign-design.md`.
|
||||
|
||||
**Rule IDs covered:** §4.1.9-R1 through R-final (to be populated in Task 1), §4.1.10, §4.1.11, §4.1.12.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
### Files to create
|
||||
- `src/features/online-board/dateLabels.ts` — helpers `replaceWithTodayTomorrow(yyyymmdd)` + `replaceWithCurrentWeek(dateFrom, dateTo)`. Pure functions returning display strings.
|
||||
- `src/features/online-board/dateLabels.test.ts`.
|
||||
- `src/shared/hooks/useSearchHistoryNS.ts` — if current `useSearchHistory` doesn't scope items per section (Board vs Schedule, per TZ 4.1.9.5 + 4.1.8-R5), add a section-scoped wrapper. **Verify first before creating.**
|
||||
- `tests/e2e/p3-filter-search.spec.ts` — Playwright spec covering filter behaviors + search execution.
|
||||
|
||||
### Files to modify
|
||||
- `src/features/online-board/components/OnlineBoardFilter.tsx` — apply TZ-exact placeholders, today/tomorrow substitution on calendar display, clear-button (X) behavior, time-slider 1h minimum gap.
|
||||
- `src/features/schedule/components/ScheduleFilter.tsx` — same + current-week substitution on Schedule calendar.
|
||||
- `src/ui/city-autocomplete/CityAutocomplete.tsx` — audit manual-entry rules per §4.1.9.1 (what happens when user types vs selects).
|
||||
- `src/ui/city-autocomplete/CityPickerPopup.tsx` — audit dictionary picker per §4.1.9.2 (focus behavior, keyboard nav, close-on-outside-click).
|
||||
- `src/ui/layout/SearchHistory.tsx` + `src/shared/hooks/useSearchHistory.ts` — verify item limit, ordering, per-item icon, click-rehydrate behavior per §4.1.9.5.
|
||||
- `src/features/online-board/api.ts` / `src/features/schedule/api.ts` — verify error handling branches per §4.1.10.1 / §4.1.11.1.
|
||||
- `src/features/online-board/components/OnlineBoardSearchPage.tsx` — verify loader visibility + abort-on-navigate per §4.1.12.
|
||||
- `src/features/schedule/components/ScheduleSearchPage.tsx` — same.
|
||||
- `docs/superpowers/specs/2026-04-21-online-board-schedule-tz-redesign-design.md` — populate rules (Task 1), mark Done (Task 11).
|
||||
|
||||
### Files reviewed, not modified
|
||||
- `src/shared/state/crossSectionNavigation.ts` — filter state already handled per P1.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Populate rule enumeration for §4.1.9/10/11/12
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/superpowers/specs/2026-04-21-online-board-schedule-tz-redesign-design.md`
|
||||
|
||||
Expand skeleton rows in §4.1.9 (and its 5 subsections) + §4.1.10 / §4.1.11 / §4.1.12 with concrete rule rows.
|
||||
|
||||
- [ ] **Step 1.1: Read TZ bodies**
|
||||
|
||||
Line ranges in `/tmp/ri_tz_extract/content.txt`:
|
||||
- §4.1.9 (filter attributes): ~524-680
|
||||
- §4.1.9.1 (manual city entry): within 524-680
|
||||
- §4.1.9.2 (dictionary city picker): within 524-680
|
||||
- §4.1.9.3 (filter validation): within 524-680
|
||||
- §4.1.9.4 (schedule range+return validation): within 524-680
|
||||
- §4.1.9.5 (search history "Вы искали"): within 524-680
|
||||
- §4.1.10 (Online-Board search execution): ~681-723
|
||||
- §4.1.10.1 (OB errors): within 681-723
|
||||
- §4.1.11 (Schedule search execution): ~724-756
|
||||
- §4.1.11.1 (Schedule errors): within 724-756
|
||||
- §4.1.12 (cancel): ~757-766
|
||||
|
||||
Use `sed -n '524,766p' /tmp/ri_tz_extract/content.txt` to dump the entire §4.1.9-4.1.12 range.
|
||||
|
||||
- [ ] **Step 1.2: Enumerate rules**
|
||||
|
||||
For each subsection, enumerate every concrete assertion:
|
||||
- §4.1.9 Tables 11/12/13/14/15: placeholder + format + behavior per attribute per mode per geo-state.
|
||||
- §4.1.9.1: manual-entry behavior (filter strip diacritics, case-insensitive, highlight match, no-match state, backspace).
|
||||
- §4.1.9.2: picker behavior (opens on focus, keyboard arrows, Enter selects, Escape closes, click-outside closes, tab order).
|
||||
- §4.1.9.3: per-attribute validation rules, error-tooltip placement, submit-blocking vs inline.
|
||||
- §4.1.9.4: `Показать расписание на` range ≤ 7 days, `Дата обратного рейса` after outbound date.
|
||||
- §4.1.9.5: «Вы искали» — max items, dedup, ordering, icon per kind (Board/Schedule), click rehydrates filter + navigates, cleared-on-session-end.
|
||||
- §4.1.10 / §4.1.11: explicit-submit, loader visibility, endpoint routing per mode, URL-first navigation, no search on attribute change.
|
||||
- §4.1.10.1 / §4.1.11.1: network timeout, 4xx/5xx handling, empty result state, retry affordance.
|
||||
- §4.1.12: AbortController cancels in-flight, Escape cancels, navigate/tab-switch cancels.
|
||||
|
||||
Target rule count: §4.1.9 ≥60 rules, §4.1.10 ≥10, §4.1.11 ≥10, §4.1.12 ≥5. Total P3 additions ~85–100 rules.
|
||||
|
||||
- [ ] **Step 1.3: Update Coverage summary**
|
||||
|
||||
Bump `Total rules extracted` by the added count.
|
||||
|
||||
- [ ] **Step 1.4: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/superpowers/specs/2026-04-21-online-board-schedule-tz-redesign-design.md
|
||||
git commit -m "Populate rule rows for P3 subsections 4.1.9/10/11/12 in TZ audit spec"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Today / Tomorrow date-label substitution (Online-Board)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/features/online-board/dateLabels.ts`
|
||||
- Create: `src/features/online-board/dateLabels.test.ts`
|
||||
- Modify: `src/features/online-board/components/OnlineBoardFilter.tsx` (use the helper to display Calendar value)
|
||||
|
||||
Per TZ Table 11/12: when the selected date is today, the calendar input should display `"Сегодня"` (localized). When tomorrow, `"Завтра"`. Otherwise `DD.MM.YYYY`.
|
||||
|
||||
- [ ] **Step 2.1: Write failing test**
|
||||
|
||||
```ts
|
||||
// src/features/online-board/dateLabels.test.ts
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { formatDateWithTodayTomorrow } from "./dateLabels.js";
|
||||
|
||||
describe("4.1.9-R: formatDateWithTodayTomorrow (Board calendar display)", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(2026, 4, 15, 12, 0, 0));
|
||||
});
|
||||
afterEach(() => { vi.useRealTimers(); });
|
||||
|
||||
it("returns 'Сегодня' for today's date", () => {
|
||||
const t = (k: string) => (k === "SHARED.TODAY" ? "Сегодня" : k === "SHARED.TOMORROW" ? "Завтра" : k);
|
||||
expect(formatDateWithTodayTomorrow(new Date(2026, 4, 15), t)).toBe("Сегодня");
|
||||
});
|
||||
|
||||
it("returns 'Завтра' for tomorrow's date", () => {
|
||||
const t = (k: string) => (k === "SHARED.TODAY" ? "Сегодня" : k === "SHARED.TOMORROW" ? "Завтра" : k);
|
||||
expect(formatDateWithTodayTomorrow(new Date(2026, 4, 16), t)).toBe("Завтра");
|
||||
});
|
||||
|
||||
it("returns dd.MM.yyyy for other dates", () => {
|
||||
const t = (k: string) => k;
|
||||
expect(formatDateWithTodayTomorrow(new Date(2026, 4, 17), t)).toBe("17.05.2026");
|
||||
expect(formatDateWithTodayTomorrow(new Date(2026, 11, 31), t)).toBe("31.12.2026");
|
||||
});
|
||||
|
||||
it("returns dd.MM.yyyy for yesterday (not 'Вчера')", () => {
|
||||
// TZ only has Today/Tomorrow substitutions, not Yesterday.
|
||||
const t = (k: string) => k;
|
||||
expect(formatDateWithTodayTomorrow(new Date(2026, 4, 14), t)).toBe("14.05.2026");
|
||||
});
|
||||
|
||||
it("returns empty string for null/undefined", () => {
|
||||
expect(formatDateWithTodayTomorrow(null, () => "")).toBe("");
|
||||
expect(formatDateWithTodayTomorrow(undefined, () => "")).toBe("");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2.2: Run — FAIL**
|
||||
|
||||
Run: `pnpm vitest run src/features/online-board/dateLabels.test.ts`
|
||||
|
||||
- [ ] **Step 2.3: Implement**
|
||||
|
||||
```ts
|
||||
// src/features/online-board/dateLabels.ts
|
||||
/**
|
||||
* Board calendar date-label substitution per TZ §4.1.9 Tables 11 + 12.
|
||||
* Today → "Сегодня", tomorrow → "Завтра", otherwise dd.MM.yyyy.
|
||||
*
|
||||
* Only substitutes Today and Tomorrow per TZ — yesterday stays dd.MM.yyyy.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type TFunction = (key: string, opts?: any) => string;
|
||||
|
||||
export function formatDateWithTodayTomorrow(
|
||||
date: Date | null | undefined,
|
||||
t: TFunction,
|
||||
): string {
|
||||
if (!date) return "";
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const d = new Date(date);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
const deltaDays = Math.round((d.getTime() - today.getTime()) / 86_400_000);
|
||||
if (deltaDays === 0) return t("SHARED.TODAY");
|
||||
if (deltaDays === 1) return t("SHARED.TOMORROW");
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
const month = String(d.getMonth() + 1).padStart(2, "0");
|
||||
return `${day}.${month}.${d.getFullYear()}`;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2.4: Run — PASS**
|
||||
|
||||
Run: `pnpm vitest run src/features/online-board/dateLabels.test.ts`
|
||||
|
||||
- [ ] **Step 2.5: Wire into OnlineBoardFilter**
|
||||
|
||||
Find where the PrimeReact `<Calendar>` component is rendered in `OnlineBoardFilter.tsx` (flight-number mode + route mode). Find its `dateFormat` or `formatDateTime` / `value` prop. Add a `formatDateTime` callback that uses `formatDateWithTodayTomorrow`:
|
||||
|
||||
```tsx
|
||||
import { formatDateWithTodayTomorrow } from "../dateLabels.js";
|
||||
|
||||
<Calendar
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.value as Date)}
|
||||
// ...
|
||||
formatDateTime={(date) => formatDateWithTodayTomorrow(date, t)}
|
||||
/>
|
||||
```
|
||||
|
||||
(PrimeReact API: check if `formatDateTime` is the right prop name for the current version. If not, use `value={formatDateWithTodayTomorrow(date, t)}` with a custom Input approach. Adapt to the actual PrimeReact version in the project.)
|
||||
|
||||
- [ ] **Step 2.6: Add component test**
|
||||
|
||||
In `OnlineBoardFilter.test.tsx` (or create if missing), add a test asserting the input reads "Сегодня" when today's date is selected with a frozen clock.
|
||||
|
||||
- [ ] **Step 2.7: Run all Online-Board tests**
|
||||
|
||||
Run: `pnpm vitest run src/features/online-board/`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2.8: Commit**
|
||||
|
||||
```bash
|
||||
git add src/features/online-board/dateLabels.ts src/features/online-board/dateLabels.test.ts src/features/online-board/components/OnlineBoardFilter.tsx
|
||||
git commit -m "Add Today/Tomorrow label substitution to Online-Board date picker per TZ 4.1.9 Tables 11+12"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Current-Week label substitution (Schedule)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/features/schedule/dateLabels.ts`
|
||||
- Create: `src/features/schedule/dateLabels.test.ts`
|
||||
- Modify: `src/features/schedule/components/ScheduleFilter.tsx`
|
||||
|
||||
Per TZ Table 14 (Schedule `Показать расписание на`): if the selected date range corresponds to the current week (Mon-Sun), display `"Текущая неделя"` instead of `DD.MM.YYYY-DD.MM.YYYY`.
|
||||
|
||||
- [ ] **Step 3.1: Write failing test**
|
||||
|
||||
```ts
|
||||
// src/features/schedule/dateLabels.test.ts
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { formatScheduleDateRangeWithCurrentWeek } from "./dateLabels.js";
|
||||
|
||||
describe("4.1.9-R: formatScheduleDateRangeWithCurrentWeek", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(2026, 4, 15, 12, 0, 0)); // Fri 2026-05-15
|
||||
});
|
||||
afterEach(() => { vi.useRealTimers(); });
|
||||
|
||||
it("returns 'Текущая неделя' when range = Mon-Sun of current week", () => {
|
||||
const t = (k: string) => (k === "SCHEDULE.CURRENT-WEEK" ? "Текущая неделя" : k);
|
||||
expect(
|
||||
formatScheduleDateRangeWithCurrentWeek(
|
||||
new Date(2026, 4, 11), // Mon
|
||||
new Date(2026, 4, 17), // Sun
|
||||
t,
|
||||
),
|
||||
).toBe("Текущая неделя");
|
||||
});
|
||||
|
||||
it("returns dd.MM.yyyy-dd.MM.yyyy for other ranges", () => {
|
||||
const t = (k: string) => k;
|
||||
expect(
|
||||
formatScheduleDateRangeWithCurrentWeek(
|
||||
new Date(2026, 4, 18), // Mon next week
|
||||
new Date(2026, 4, 24), // Sun next week
|
||||
t,
|
||||
),
|
||||
).toBe("18.05.2026-24.05.2026");
|
||||
});
|
||||
|
||||
it("returns dd.MM.yyyy-dd.MM.yyyy for partial current week (not full Mon-Sun)", () => {
|
||||
const t = (k: string) => k;
|
||||
expect(
|
||||
formatScheduleDateRangeWithCurrentWeek(
|
||||
new Date(2026, 4, 13), // Wed
|
||||
new Date(2026, 4, 17), // Sun
|
||||
t,
|
||||
),
|
||||
).toBe("13.05.2026-17.05.2026");
|
||||
});
|
||||
|
||||
it("returns empty string for null inputs", () => {
|
||||
expect(formatScheduleDateRangeWithCurrentWeek(null, null, () => "")).toBe("");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 3.2: Run — FAIL**
|
||||
|
||||
- [ ] **Step 3.3: Implement**
|
||||
|
||||
```ts
|
||||
// src/features/schedule/dateLabels.ts
|
||||
/**
|
||||
* Schedule range-calendar label substitution per TZ §4.1.9 Table 14.
|
||||
* Current week Mon-Sun → "Текущая неделя", otherwise dd.MM.yyyy-dd.MM.yyyy.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type TFunction = (key: string, opts?: any) => string;
|
||||
|
||||
function toYmd(d: Date): string {
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
const month = String(d.getMonth() + 1).padStart(2, "0");
|
||||
return `${day}.${month}.${d.getFullYear()}`;
|
||||
}
|
||||
|
||||
function mondayOfWeek(base: Date): Date {
|
||||
const d = new Date(base);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
const offset = (d.getDay() + 6) % 7; // Mon = 0, Sun = 6
|
||||
d.setDate(d.getDate() - offset);
|
||||
return d;
|
||||
}
|
||||
|
||||
export function formatScheduleDateRangeWithCurrentWeek(
|
||||
dateFrom: Date | null | undefined,
|
||||
dateTo: Date | null | undefined,
|
||||
t: TFunction,
|
||||
): string {
|
||||
if (!dateFrom || !dateTo) return "";
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const thisMon = mondayOfWeek(today);
|
||||
const thisSun = new Date(thisMon);
|
||||
thisSun.setDate(thisSun.getDate() + 6);
|
||||
|
||||
const from = new Date(dateFrom);
|
||||
from.setHours(0, 0, 0, 0);
|
||||
const to = new Date(dateTo);
|
||||
to.setHours(0, 0, 0, 0);
|
||||
|
||||
if (from.getTime() === thisMon.getTime() && to.getTime() === thisSun.getTime()) {
|
||||
return t("SCHEDULE.CURRENT-WEEK");
|
||||
}
|
||||
return `${toYmd(from)}-${toYmd(to)}`;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3.4: Run — PASS**
|
||||
|
||||
- [ ] **Step 3.5: Wire into ScheduleFilter**
|
||||
|
||||
Find the calendar render in `ScheduleFilter.tsx` and apply the formatter to display the selected range.
|
||||
|
||||
- [ ] **Step 3.6: i18n key**
|
||||
|
||||
Ensure `SCHEDULE.CURRENT-WEEK` exists in `ru-ru` locale = `"Текущая неделя"` and in `en-us` = `"Current week"`. Other 7 locales: empty string per project pattern.
|
||||
|
||||
- [ ] **Step 3.7: Run tests**
|
||||
|
||||
Run: `pnpm vitest run src/features/schedule/`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3.8: Commit**
|
||||
|
||||
```bash
|
||||
git add src/features/schedule/dateLabels.ts src/features/schedule/dateLabels.test.ts src/features/schedule/components/ScheduleFilter.tsx src/i18n/locales/
|
||||
git commit -m "Add Current-Week label substitution to Schedule date-range picker per TZ 4.1.9 Table 14"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Clear-button (X) behavior on filter fields
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/features/online-board/components/OnlineBoardFilter.tsx`
|
||||
- Modify: `src/features/schedule/components/ScheduleFilter.tsx`
|
||||
- Modify: `src/ui/city-autocomplete/CityAutocomplete.tsx` (if the X is handled there)
|
||||
|
||||
Per TZ Tables 11/12/14 and Table 13: every filter attribute with a value must show a clear-button (X). Clicking X empties the attribute and displays its placeholder. When empty, X is hidden.
|
||||
|
||||
- [ ] **Step 4.1: Inspect current state**
|
||||
|
||||
Read the three filter files. Note which attributes currently have X buttons and which don't. Likely candidates for missing X:
|
||||
- Flight-number input
|
||||
- Calendar value (PrimeReact may have `showIcon` but not a clear X)
|
||||
- Time-range slider (probably doesn't have a clear button — acceptable, slider has no "empty" state per TZ)
|
||||
|
||||
Identify gaps.
|
||||
|
||||
- [ ] **Step 4.2: Write failing tests per field**
|
||||
|
||||
For each filter attribute (flight number, departure city, arrival city, date, schedule range, return date):
|
||||
|
||||
```tsx
|
||||
it("4.1.9-R: <field> shows X button when filled, hides when empty", async () => {
|
||||
render(<OnlineBoardFilter ... />);
|
||||
const input = screen.getByTestId("<field>");
|
||||
expect(screen.queryByTestId("<field>-clear")).not.toBeInTheDocument();
|
||||
userEvent.type(input, "SU1234");
|
||||
expect(screen.getByTestId("<field>-clear")).toBeInTheDocument();
|
||||
userEvent.click(screen.getByTestId("<field>-clear"));
|
||||
expect(input).toHaveValue("");
|
||||
expect(screen.queryByTestId("<field>-clear")).not.toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 4.3: Run — FAIL on fields missing X**
|
||||
|
||||
- [ ] **Step 4.4: Add X button to missing fields**
|
||||
|
||||
For each field missing X, add a clear button that:
|
||||
- Is rendered only when the field has a value (conditional render on `value && value.length > 0`).
|
||||
- Has `data-testid="<field>-clear"`.
|
||||
- Has `aria-label={t("SHARED.CLEAR")}` for a11y.
|
||||
- Click handler clears the field value + refocuses the input.
|
||||
|
||||
For PrimeReact `<Calendar>`: use `showButtonBar` + `showIcon` or wrap with a custom clear button overlaid per existing filter styling.
|
||||
|
||||
- [ ] **Step 4.5: Run — PASS**
|
||||
|
||||
Run: `pnpm vitest run src/features/online-board/ src/features/schedule/`
|
||||
|
||||
- [ ] **Step 4.6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/features/online-board/components/OnlineBoardFilter.tsx src/features/schedule/components/ScheduleFilter.tsx src/ui/city-autocomplete/CityAutocomplete.tsx src/i18n/locales/
|
||||
git commit -m "Add clear-button (X) to filter fields per TZ 4.1.9 Tables 11/12/14"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Time-slider 1-hour minimum gap
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/features/online-board/components/OnlineBoardFilter.tsx`
|
||||
- Modify: `src/features/schedule/components/ScheduleFilter.tsx`
|
||||
|
||||
Per TZ Table 12/14: `Время рейса` / `Время вылета` slider must enforce `(timeTo - timeFrom) ≥ 60 minutes`. Cannot set `timeTo` before `timeFrom`.
|
||||
|
||||
- [ ] **Step 5.1: Inspect current slider setup**
|
||||
|
||||
Read the `<Slider>` (PrimeReact) bindings in both filter components. Look for `min`, `max`, `step`, `range` props and the change handler.
|
||||
|
||||
- [ ] **Step 5.2: Add failing test**
|
||||
|
||||
```tsx
|
||||
it("4.1.9-R: time slider enforces 1h minimum gap", () => {
|
||||
render(<OnlineBoardFilter ... />);
|
||||
// Simulate setting timeTo equal to timeFrom (e.g. both at 12:00)
|
||||
// Expected: timeTo is auto-adjusted to timeFrom + 60 minutes
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 5.3: Implement clamp**
|
||||
|
||||
In the slider `onChange` handler, after getting `[from, to]` values, enforce:
|
||||
|
||||
```tsx
|
||||
const onSliderChange = (e: SliderChangeEvent) => {
|
||||
const [from, to] = e.value as [number, number];
|
||||
const MIN_GAP = 60; // minutes
|
||||
let nextTo = to;
|
||||
if (to - from < MIN_GAP) {
|
||||
// Push `to` forward or `from` back, clamped to bounds.
|
||||
nextTo = from + MIN_GAP;
|
||||
if (nextTo > 1440) {
|
||||
// can't extend — clamp `from` instead
|
||||
setRange([1440 - MIN_GAP, 1440]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setRange([from, nextTo]);
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 5.4: Run — PASS**
|
||||
|
||||
Run: `pnpm vitest run src/features/online-board/ src/features/schedule/`
|
||||
|
||||
- [ ] **Step 5.5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/features/online-board/components/OnlineBoardFilter.tsx src/features/schedule/components/ScheduleFilter.tsx
|
||||
git commit -m "Enforce 1h minimum gap on time-range slider per TZ 4.1.9 Tables 12/14"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: City autocomplete — manual-entry rules (§4.1.9.1)
|
||||
|
||||
**Files:**
|
||||
- Verify: `src/ui/city-autocomplete/CityAutocomplete.tsx`
|
||||
- Verify: `src/ui/city-autocomplete/searchCities.ts`
|
||||
|
||||
Per §4.1.9.1 (read the full text): when user types into the city field:
|
||||
- Case-insensitive match.
|
||||
- Accent-insensitive (e.g. "mosco" matches "Москва"? probably language-specific — check TZ).
|
||||
- Show suggestions in a dropdown.
|
||||
- No-match state: show "Нет результатов" or similar.
|
||||
- Typing does NOT submit.
|
||||
- Exact-match typed value can be auto-committed (already covered by existing commit `9efc76b` per memory).
|
||||
|
||||
- [ ] **Step 6.1: Read TZ §4.1.9.1 body** in full.
|
||||
|
||||
- [ ] **Step 6.2: Inspect current behavior** — read `CityAutocomplete.tsx` + `searchCities.ts` + tests.
|
||||
|
||||
- [ ] **Step 6.3: Enumerate gaps** against TZ.
|
||||
|
||||
- [ ] **Step 6.4: Add failing tests for each gap.**
|
||||
|
||||
- [ ] **Step 6.5: Fix.**
|
||||
|
||||
- [ ] **Step 6.6: Run tests + commit.**
|
||||
|
||||
```bash
|
||||
git add src/ui/city-autocomplete/
|
||||
git commit -m "Audit CityAutocomplete manual-entry behavior per TZ 4.1.9.1"
|
||||
```
|
||||
|
||||
(Task is inspection-heavy and depends on current state; cannot pre-write exact test code without reading the file first. Implementer subagent will enumerate gaps at kickoff.)
|
||||
|
||||
---
|
||||
|
||||
## Task 7: City picker popup — dictionary rules (§4.1.9.2)
|
||||
|
||||
**Files:**
|
||||
- Verify: `src/ui/city-autocomplete/CityPickerPopup.tsx`
|
||||
|
||||
Per §4.1.9.2:
|
||||
- Opens on focus / click.
|
||||
- Keyboard arrow-key navigation.
|
||||
- Enter commits; Escape closes without commit.
|
||||
- Click-outside closes.
|
||||
- List is scrollable; items grouped by country (check TZ Table 13 for exact grouping).
|
||||
- Selected item highlights.
|
||||
|
||||
- [ ] **Step 7.1: Read TZ §4.1.9.2** fully.
|
||||
|
||||
- [ ] **Step 7.2: Inspect `CityPickerPopup.tsx`** + tests.
|
||||
|
||||
- [ ] **Step 7.3: Gap-list, failing tests, fix, commit.**
|
||||
|
||||
```bash
|
||||
git add src/ui/city-autocomplete/
|
||||
git commit -m "Audit CityPickerPopup dictionary-picker behavior per TZ 4.1.9.2"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Filter validation (§4.1.9.3 + §4.1.9.4)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/features/online-board/components/OnlineBoardFilter.tsx` (validation messages + submit guards)
|
||||
- Modify: `src/features/schedule/components/ScheduleFilter.tsx`
|
||||
|
||||
§4.1.9.3 — per-attribute validation:
|
||||
- Flight number: format `SU####`, 4 digits, pad as needed.
|
||||
- Date: within `-1/+14` (Board) or `-1/+330` (Schedule) window.
|
||||
- City fields: must be selected from dictionary (not arbitrary text) — OR accept exact-match typed value per existing behavior.
|
||||
- Error tooltip placement: inline below the field or via toast — check TZ mockup.
|
||||
|
||||
§4.1.9.4 — Schedule-specific:
|
||||
- `Показать расписание на` range: max ≤ 7 days (one week).
|
||||
- `Дата обратного рейса` must be AFTER outbound `dateTo`.
|
||||
- Return date range: also max 7 days.
|
||||
|
||||
- [ ] **Step 8.1: Read TZ §4.1.9.3 + §4.1.9.4** fully.
|
||||
|
||||
- [ ] **Step 8.2: Inspect current validation** (`OnlineBoardFilter.tsx` + `ScheduleFilter.tsx`). Look for existing error-state handling.
|
||||
|
||||
- [ ] **Step 8.3: Enumerate gaps.**
|
||||
|
||||
- [ ] **Step 8.4: Write failing tests per rule.**
|
||||
|
||||
- [ ] **Step 8.5: Fix.**
|
||||
|
||||
- [ ] **Step 8.6: Commit.**
|
||||
|
||||
```bash
|
||||
git add src/features/online-board/ src/features/schedule/ src/i18n/
|
||||
git commit -m "Tighten filter validation per TZ 4.1.9.3 + 4.1.9.4"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Search history «Вы искали» (§4.1.9.5)
|
||||
|
||||
**Files:**
|
||||
- Verify: `src/shared/hooks/useSearchHistory.ts`
|
||||
- Verify: `src/ui/layout/SearchHistory.tsx`
|
||||
- Possibly create: `src/shared/hooks/useSearchHistoryNS.ts` (section-namespaced wrapper)
|
||||
|
||||
Per §4.1.9.5:
|
||||
- Recent searches displayed as a list with icons per kind (Board route / Board flight-number / Schedule / etc.).
|
||||
- Item limit: TZ specifies (check for exact number — typically 5 or 10).
|
||||
- Ordering: most-recent first.
|
||||
- Dedup: same search (same cities + date) doesn't appear twice; re-search pushes to top.
|
||||
- Session-scoped (cleared on page reload per P1 cross-section store semantics, OR persisted — check TZ).
|
||||
- Click rehydrates filter + navigates to results.
|
||||
- Per §4.1.8-R5: history is shared between Board and Schedule but NOT Flight-Map.
|
||||
|
||||
- [ ] **Step 9.1: Read TZ §4.1.9.5** fully.
|
||||
|
||||
- [ ] **Step 9.2: Inspect `useSearchHistory.ts` + `SearchHistory.tsx`** + tests.
|
||||
|
||||
- [ ] **Step 9.3: Gap-list.**
|
||||
|
||||
- [ ] **Step 9.4: Fix.**
|
||||
|
||||
- [ ] **Step 9.5: Commit.**
|
||||
|
||||
```bash
|
||||
git add src/shared/hooks/useSearchHistory.ts src/ui/layout/SearchHistory.tsx
|
||||
git commit -m "Audit «Вы искали» search history per TZ 4.1.9.5"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 10: Search execution + cancellation + error handling (§4.1.10, §4.1.11, §4.1.12)
|
||||
|
||||
**Files:**
|
||||
- Verify: `src/features/online-board/api.ts` + `hooks/useOnlineBoard.ts`
|
||||
- Verify: `src/features/schedule/api.ts` + related hooks
|
||||
- Verify: `src/ui/errors/*`
|
||||
|
||||
§4.1.10: OB search execution — explicit submit, loader visibility, URL-first navigation, endpoint routing per mode. §4.1.10.1: errors (timeout, 4xx, 5xx, empty) each with a specific message.
|
||||
|
||||
§4.1.11: same for Schedule.
|
||||
|
||||
§4.1.12: cancellation — AbortController on new search, Escape cancels, navigate/tab-switch cancels.
|
||||
|
||||
- [ ] **Step 10.1: Read TZ §4.1.10, 4.1.10.1, 4.1.11, 4.1.11.1, 4.1.12** fully.
|
||||
|
||||
- [ ] **Step 10.2: Inspect current API + hook files** for AbortController usage + error-state branches.
|
||||
|
||||
- [ ] **Step 10.3: Gap-list.**
|
||||
|
||||
- [ ] **Step 10.4: Add tests + fixes.**
|
||||
|
||||
- [ ] **Step 10.5: Commit.**
|
||||
|
||||
```bash
|
||||
git add src/features/online-board/ src/features/schedule/ src/ui/errors/
|
||||
git commit -m "Search execution + cancellation + error handling per TZ 4.1.10/11/12"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 11: Update spec with P3 completion
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/superpowers/specs/2026-04-21-online-board-schedule-tz-redesign-design.md`
|
||||
|
||||
- [ ] **Step 11.1**: Mark each P3 rule `Done` with the relevant commit SHA (from `git log --oneline main..HEAD`).
|
||||
|
||||
- [ ] **Step 11.2**: Add any new Conflicts encountered.
|
||||
|
||||
- [ ] **Step 11.3**: Append merge-log row.
|
||||
|
||||
- [ ] **Step 11.4**: Update coverage summary counts.
|
||||
|
||||
- [ ] **Step 11.5**: Commit.
|
||||
|
||||
```bash
|
||||
git add docs/superpowers/specs/2026-04-21-online-board-schedule-tz-redesign-design.md
|
||||
git commit -m "Mark P3 (filter + validation + history + search) rules Done in TZ audit spec"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 12: Verify + merge gate
|
||||
|
||||
- [ ] **Step 12.1**: `pnpm typecheck && pnpm lint && pnpm vitest run` — all pass.
|
||||
|
||||
- [ ] **Step 12.2**: `pnpm test:e2e` — if any P3-specific e2e spec was added.
|
||||
|
||||
- [ ] **Step 12.3**: Invoke `superpowers:finishing-a-development-branch`.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
**1. Spec coverage.** §4.1.9 (all 5 subsections) + §4.1.10 + §4.1.11 + §4.1.12 — Tasks 2–10 cover the rule clusters. Rule population in Task 1 ensures every rule has an ID.
|
||||
|
||||
**2. Placeholder scan.** Tasks 6/7/8/9/10 use "read full TZ body, enumerate gaps, fix" language rather than pre-written code because the existing filter components are large and the TZ-vs-impl gap is unknown without an inspection pass. Each of these tasks will generate focused subtasks during kickoff — this is acceptable for an audit-style plan where the exhaustive detail lives in the audit tables of the spec.
|
||||
|
||||
**3. Type consistency.**
|
||||
- `formatDateWithTodayTomorrow` (Task 2) and `formatScheduleDateRangeWithCurrentWeek` (Task 3) share the same `TFunction` signature.
|
||||
- Clear-button pattern (Task 4) uses consistent `data-testid="<field>-clear"` convention.
|
||||
- Time-range clamping (Task 5) uses minutes-since-midnight consistently.
|
||||
|
||||
---
|
||||
|
||||
## Execution Handoff
|
||||
|
||||
Plan complete. Execution options: Subagent-Driven (recommended) or Inline.
|
||||
Reference in New Issue
Block a user