From 5d569a9127ce80cf61f0daf5ab299c2ed57be68d Mon Sep 17 00:00:00 2001 From: aiserrock Date: Wed, 15 Jul 2026 10:12:30 +0300 Subject: [PATCH] =?UTF-8?q?chore:=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D1=81=D0=BB=D1=83=D1=87=D0=B0=D0=B9=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20design-specs=20(search-and-sort)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Спека и план поиска/сортировки попали в develop через #54 (git add -A захватил untracked docs). Убираю — они не должны были коммититься. --- docs/design-specs/search-and-sort-chips.md | 180 ------------------ .../search-and-sort-chips.plan.md | 131 ------------- 2 files changed, 311 deletions(-) delete mode 100644 docs/design-specs/search-and-sort-chips.md delete mode 100644 docs/design-specs/search-and-sort-chips.plan.md diff --git a/docs/design-specs/search-and-sort-chips.md b/docs/design-specs/search-and-sort-chips.md deleted file mode 100644 index 7b7ede0..0000000 --- a/docs/design-specs/search-and-sort-chips.md +++ /dev/null @@ -1,180 +0,0 @@ -# Design Spec: Поиск + чипсы сортировки (вкладки «Сегодня» и «Животные») - -## Problem - -Поиск на вкладках «Сегодня» (`MainScreen`) и «Животные» (`AnimalsScreen`) не -работает вообще. Причина одна на обеих вкладках — `TextField` в шапке не связан -с бизнес-логикой: - -- **«Животные»** (`animals_screen.dart` → `_buildTitle`): `TextField` без - `controller`, без `onChanged`/listener. Ввод уходит в никуда. `AnimalsCubit` - не имеет метода поиска, `loadAnimalList` дёргает `fetchAnimalList` без - `searchRequest`. -- **«Сегодня»** (`main_screen.dart` → `_buildTitle`): `controller` есть, но нет - `addListener`/`onChanged`. `MainCubit` не умеет искать; `fetchTodayPrescriptionList` - не пробрасывает `search`. - -При этом бэкенд-контракт **уже готов**: `apiV1AnimalsGet` и -`apiV1PrescriptionsExecutionsGet` принимают `search` и `ordering`; -`AnimalService.fetchAnimalList(searchRequest:)` уже прокидывает поиск. Чинить -надо только проводку UI → cubit → service, бэкенд не трогаем. - -Вторая часть — чипсы над списком (со скрина Figma): это **сортировка** -(серверный `ordering`), а не фильтры-подмножества. Пользователи путают их с -фильтрами — надо визуально обозначить как сортировку. - -Успех: набрал текст → список фильтруется; тапнул чипс → список пересортировался; -поиск и сортировка комбинируются в одном запросе; выравнивание поля ввода -аккуратное. - -## Solution Overview - -- Починить поиск на обеих вкладках: подключить `TextEditingController` + - listener → cubit-метод → service с `searchRequest`, с debounce и защитой от - устаревших ответов (generation counter, как уже сделано в `AnimalsCubit`). -- Выровнять поле поиска в шапке (вертикальное центрирование текста, паддинги, - префикс-иконка/крестик) — единообразно на обеих вкладках. -- Добавить горизонтальную ленту чипсов сортировки под шапкой. Пресеты - взаимоисключающие (radio, один активный). Каждый чипс = готовая фраза + один - серверный `ordering`. -- Явно обозначить, что это сортировка: перед лентой лейбл `⇅ Сортировка:` - (`Icons.sort` + подпись), чтобы не путали с фильтрами. -- Дефолтный активный пресет — «Сначала новые» (`-date_joined`) для «Животных», - «По времени» (`execute_at`) для «Сегодня». Активный чипс автоскроллится в - видимую зону. -- Поиск и сортировка **независимы и комбинируются**: оба уходят в один запрос - (`search=… & ordering=…`). Смена одного сохраняет второе. -- Пресеты вынесены в конфиг-список — легко править, безопасный дефолт при - игнорировании `ordering` бэкендом. - -## Scope - -- In scope: - - Починка поиска на `MainScreen` и `AnimalsScreen`. - - Выравнивание/вёрстка поля ввода поиска. - - Лента чипсов сортировки + лейбл «Сортировка» на обеих вкладках. - - Проброс `search` в `fetchTodayPrescriptionList` (сейчас его нет). - - Проброс `ordering` в `AnimalService.fetchAnimalList` и в today-запрос. - - Комбинирование search + ordering в одном запросе. - -- Out of scope: - - Модальный `Search` picker (`search_screen/`), `SearchScreen` (выбор вида) - — это отдельные экраны выбора, у них поиск уже работает. - - Реальные фильтры-подмножества (по статусу как отбор, а не сортировка) — - другой API-контракт, отдельная задача. - - Изменение бэкенда / OpenAPI-схемы. - - Пагинация — остаётся как есть, но должна переиспользовать текущие - search + ordering при догрузке страниц. - -## Architecture Sketch - -``` -AnimalsScreen / MainScreen (StatefulWidget) - ├─ AppBar.title: TextField ──(listener, debounce 300ms)──► Cubit.onSearchChanged(q) - ├─ под AppBar: SortChipsBar - │ ├─ лейбл ⇅ Сортировка: - │ └─ ListView.horizontal[ ChoiceChip(preset) ] ──► Cubit.onSortChanged(preset) - └─ body: список (существующий) - -Cubit (AnimalsCubit / MainCubit) - ├─ state: + searchRequest:String, + activeSort:SortPreset - ├─ onSearchChanged(q) → сохранить q, reload(offset=0) - ├─ onSortChanged(p) → сохранить p, reload(offset=0) - └─ load*(...) → service.fetch(search: searchRequest, ordering: activeSort.value, ...) - -Service - ├─ AnimalService.fetchAnimalList(searchRequest, ordering, limit, offset) // + ordering - └─ PrescriptionService.fetchTodayPrescriptionList(search, ordering) // + оба параметра -``` - -Общий виджет `SortChipsBar` (переиспользуемый обеими вкладками) принимает -`List`, `activeId`, `onSelected`. Debounce/generation-guard — -на уровне cubit (в `AnimalsCubit` generation counter уже есть, для `MainCubit` -добавить аналогичный). - -## Data Model - -- `SortPreset` — value object: `id` (стабильный ключ), `labelKey` (l10n), - `ordering` (строка для API, напр. `-date_joined`). -- `AnimalSortPresets` — список из 8 пресетов (см. таблицу ниже). -- `TodaySortPresets` — список из 5 пресетов. -- `AnimalsState` — добавить `searchRequest:String`, `activeSort:SortPreset`. -- `MainCubit` state — сейчас голый `DataState<...>`; расширить до объекта - состояния с полями `data`, `searchRequest`, `activeSort` (либо ввести - `MainState`, аналогично `AnimalsState`). - -### Пресеты — «Животные» (модель `AnimalRead`) - -| id | label | ordering | -|----|-------|----------| -| newest *(default)* | Сначала новые | `-date_joined` | -| oldest | Сначала старые | `date_joined` | -| name_asc | По имени А–Я | `name` | -| name_desc | По имени Я–А | `-name` | -| spec | По породе | `spec` | -| status | По статусу | `status` | -| young | Моложе → старше | `-birth_date` | -| old | Старше → моложе | `birth_date` | - -### Пресеты — «Сегодня» (модель `PrescriptionExecutionToday`) - -| id | label | ordering | -|----|-------|----------| -| time *(default)* | По времени | `execute_at` | -| time_desc | Сначала поздние | `-execute_at` | -| animal_name_asc | По кличке А–Я | `prescription__animal__name` | -| animal_name_desc | По кличке Я–А | `-prescription__animal__name` | -| animal_spec | По виду | `prescription__animal__spec_name` | - -> **Риск:** OpenAPI-схема (`doc/api/openapi.json`) объявляет `ordering` как голый -> `string` и не перечисляет разрешённые поля; mockoon игнорирует `ordering` -> вовсе. Вложенные (`prescription__animal__*`), а также `spec`/`status` могут -> быть не внесены в `ordering_fields` бэкенда. Стратегия: дефолт всегда -> безопасный; если бэкенд игнорирует `ordering` — вернётся дефолтный порядок, -> приложение не падает. Финальный список пресетов проверить прогоном по **живому** -> API и выкинуть неработающие. - -## Screens / Flows (UI tasks) - -- **AnimalsScreen (вкладка «Животные»)** - - Purpose: список животных с поиском и сортировкой. - - Key states: loading / content / empty («ничего не найдено» при активном - поиске) / error; пагинация внизу. - - Поле поиска: выровнено (текст по центру по вертикали, префикс-иконка поиска, - суффикс-крестик очистки), очистка не выходит из режима поиска, а очищает - строку. - - Лента сортировки: видима всегда (и в обычном режиме, и при поиске), под - шапкой, лейбл «Сортировка». -- **MainScreen (вкладка «Сегодня»)** - - Purpose: исполнения назначений на сегодня с поиском и сортировкой. - - Идентичная механика (лента + поиск), но пресеты `TodaySortPresets`. - - Key states: те же. - -Entry/exit: тап на иконку поиска в `actions` → режим поиска (поле в шапке). -Крестик очищает строку; выход из режима поиска — существующей кнопкой. - -## Alternatives Considered - -1. **B — две группы чипсов (поле × направление)** — отвергнут: две группы хуже - считываются как «сортировка», «направление» для даты неоднозначно - («А–Я» для даты непонятно), не отвечает требованию «людям должно быть - понятно». -2. **C — реальные фильтры-подмножества (по статусу как отбор)** — отвергнут: - другой API-параметр, которого нет в клиенте; требует доработки контракта. - Вынесен из scope. -3. **Wrap-лента в 2 строки** — отвергнута: съедает вертикаль над списком; на - скрине Figma лента горизонтальная (чипсы обрезаны справа). -4. **Клиентская сортировка/фильтрация** — отвергнута: список пагинируется, - сортировать/искать надо на сервере, иначе результат неполный. - -## Open Questions - -- Ни одного блокирующего. На этапе имплементации: проверить на живом бэкенде, - какие `ordering` реально поддерживаются (особенно `prescription__animal__*`, - `spec`, `status`), и подрезать список пресетов по факту. - -## Next Step - -Hand this spec to `writing-plans` skill to produce a task-level plan. -Стек — Flutter (`.dart`, `lib/ui/screen/...`), имплементацию будет вести -Flutter-профиль планировщика. diff --git a/docs/design-specs/search-and-sort-chips.plan.md b/docs/design-specs/search-and-sort-chips.plan.md deleted file mode 100644 index cc42be0..0000000 --- a/docs/design-specs/search-and-sort-chips.plan.md +++ /dev/null @@ -1,131 +0,0 @@ -# Implementation Plan: Поиск + чипсы сортировки - -Спека: `docs/design-specs/search-and-sort-chips.md` -Стек: Flutter 3.44.0 (fvm). Кодоген: `fvm dart run build_runner build --delete-conflicting-outputs`. -Формат: `fvm dart format -l 120 .`. Анализ: `fvm dart analyze .`. - -## Соглашения проекта (обнаружено) -- State-management: `bloc`/`cubit` + `flutter_bloc`, `Equatable`, `safeEmit` (`util/bloc_ext.dart`). -- Загрузка: `DataState` (`util/data_state.dart`) + `DataStateBuilder`. -- l10n: easy_localization, ключи в `assets/translations/{ru,en}.json`, кодоген в - `lib/gen/l10n/locale_keys.g.dart` (класс `LocaleKeys`). Уже есть `commonSort`, - `commonSearch`. -- API: `apiV1AnimalsGet(search:, ordering:)`, `apiV1PrescriptionsExecutionsGet(search:, ordering:)`. -- Guard от устаревших ответов: generation counter (образец — `AnimalsCubit._requestGen`). - ---- - -## Task 1 — Модель пресетов сортировки -**Файл (new):** `lib/ui/screen/common/sort/sort_preset.dart` -- `class SortPreset extends Equatable { final String id, labelKey, ordering; }` -- `const AnimalSortPresets` — список из 8 (newest дефолт … birth_date). Значения - `ordering` строго из спеки. -- `const TodaySortPresets` — список из 5 (time дефолт … animal_spec). -- Экспорт дефолта-геттера у каждого списка (`first` как дефолт — newest / time). - -**Проверка:** компилируется, `AnimalSortPresets.first.id == 'newest'`. - -## Task 2 — l10n ключи чипсов -**Файлы:** `assets/translations/ru.json`, `assets/translations/en.json` -- Добавить ключи под каждый `labelKey`: `sortNewest, sortOldest, sortNameAsc, - sortNameDesc, sortSpec, sortStatus, sortYoung, sortOld` (Животные) и - `sortTime, sortTimeDesc, sortAnimalNameAsc, sortAnimalNameDesc, sortAnimalSpec` - (Сегодня). Переиспользовать существующий `commonSort` как лейбл ленты. -- Прогнать кодоген l10n (build_runner) → обновится `LocaleKeys`. - -**Проверка:** `LocaleKeys.sortNewest` существует после кодогена. - -## Task 3 — Виджет ленты чипсов `SortChipsBar` -**Файл (new):** `lib/ui/screen/common/sort/sort_chips_bar.dart` -- `StatelessWidget`, параметры: `List presets`, `String activeId`, - `ValueChanged onSelected`. -- Вёрстка: `Row[ Icon(Icons.sort) + Text(commonSort) : ]` слева, затем - горизонтальный `ListView`/`SingleChildScrollView` из `ChoiceChip` (radio: - `selected == p.id == activeId`, `onSelected` → колбэк). Один активный. -- Активный чипс автоскроллится в видимую зону при первом построении - (`ScrollController` + `Scrollable.ensureVisible` через ключи, либо расчётный - offset). Держать простым — при желании отложить автоскролл в отдельный подтаск. -- Тема: цвета из `Theme.of(context).colorScheme` (проект на ColorScheme, тёмная - тема поддержана — не хардкодить цвета). - -**Проверка:** виджет показывает N чипсов, тап меняет `activeId` через колбэк. - -## Task 4 — Сервисы: проброс ordering -**Файл:** `lib/service/animal/animal_service.dart` -- `fetchAnimalList` уже есть `searchRequest`; **добавить `String? ordering`** и - прокинуть в `apiV1AnimalsGet(ordering: ordering)`. - -**Файл:** `lib/service/prescription/prescription_service.dart` -- `fetchTodayPrescriptionList` — **добавить `String? search, String? ordering`** и - прокинуть в `apiV1PrescriptionsExecutionsGet(search:, ordering:)`. - -**Проверка:** сигнатуры принимают параметры, existing-вызовы не сломаны -(параметры опциональные). - -## Task 5 — AnimalsCubit: поиск + сортировка -**Файлы:** `lib/ui/screen/animals/cubit/animals_state.dart`, `animals_cubit.dart` -- State: добавить `String searchRequest` (default `''`), `SortPreset activeSort` - (default `AnimalSortPresets.first`). Обновить `copyWith`/`props`. -- Cubit: - - `onSearchChanged(String q)` — debounce 300ms (Timer в cubit или - `stream_transform`; проще Timer-debounce), сохранить `q`, `loadAnimalList(reset)`. - - `onSortChanged(SortPreset p)` — сохранить, `loadAnimalList(reset)`. - - `loadAnimalList` — пробросить `searchRequest: state.searchRequest`, - `ordering: state.activeSort.ordering` в `fetchAnimalList`. Сохранить существующий - generation-guard. - - Дефолт при старте — `activeSort = newest`, значит первый запрос уже с - `ordering=-date_joined`. - -**Проверка:** смена сортировки/поиска ре-грузит список offset=0 с обоими параметрами. - -## Task 6 — AnimalsScreen: подключить UI -**Файл:** `lib/ui/screen/animals/animals_screen.dart` -- `TextField` в `_buildTitle`: добавить `controller` (новый - `TextEditingController` в state) + listener → `cubit.onSearchChanged`. - Выровнять: `textAlignVertical: center`, префикс-иконка поиска, суффикс-крестик - (крестик очищает текст, не выходит из режима). Единообразно с `search_spec_screen`. -- Под `AppBar` (в body, над списком): вставить `SortChipsBar(presets: - AnimalSortPresets, activeId: state.activeSort.id, onSelected: cubit.onSortChanged)`. - Видима и в обычном режиме, и при поиске. -- Empty-state при активном поиске: текст «ничего не найдено» (существующий - `_buildEmptyState`/`commonNotFound`). - -**Проверка:** ввод текста фильтрует, тап чипса сортирует, крестик чистит. - -## Task 7 — MainCubit + MainScreen (вкладка «Сегодня») -**Файлы:** `lib/ui/screen/main/cubit/main_cubit.dart` (+ new `main_state.dart`), -`lib/ui/screen/main/main_screen.dart` -- Ввести `MainState` (Equatable): `DataState<...> data`, `String searchRequest`, - `SortPreset activeSort` (default `TodaySortPresets.first`). Переключить - `MainCubit extends Cubit`. -- `onSearchChanged` (debounce 300ms) + `onSortChanged`, generation-guard - (добавить, по образцу AnimalsCubit). -- `loadExecutions` — пробросить `search`, `ordering` в - `fetchTodayPrescriptionList`. -- MainScreen: убрать локальный `_isSearchActive`/`_searchController`-без-listener - → поиск через cubit (listener на controller). Вставить `SortChipsBar(presets: - TodaySortPresets, ...)` под шапкой. Empty-state при поиске. -- Обновить `_buildBody`/`BlocBuilder` тип на `MainState`. - -**Проверка:** «Сегодня» ищет и сортирует так же, как «Животные». - -## Task 8 — Прогон и правки -- `fvm dart run build_runner build --delete-conflicting-outputs` (l10n + любые - генераторы). -- `fvm dart format -l 120 .` -- `fvm dart analyze .` — 0 ошибок. -- Ручная проверка: обе вкладки, светлая/тёмная тема, пустой поиск, длинная лента - чипсов (горизонтальный скролл + автоскролл к активному). - ---- - -## Порядок исполнения -1 → 2 → 4 (независимы, можно параллельно) → 3 → 5 → 6 (Животные, вертикальный -срез готов, проверяемо) → 7 (Сегодня) → 8. - -## Риски / примечания -- Вложенные `ordering` (`prescription__animal__*`), `spec`, `status` могут быть не - в `ordering_fields` бэкенда → проверить на живом API, лишние пресеты выкинуть из - Task 1 списков. mockoon это не покажет. -- Debounce лучше держать в cubit, чтобы UI оставался тонким и тестируемым. -- Не хардкодить цвета — проект перешёл на ColorScheme (тёмная тема, коммит 20af93d1).