Appearance
Use Case #UC-027.1: Учёт велосипедных компонентов ⚙️ (Базовая инвентаризация и гараж)
1. Жизненная ситуация (Боль пользователя)
Представь: Увлеченный велосипедист имеет два велосипеда — шоссейник и гравийник. В процессе эксплуатации он меняет цепи, кассеты, покрышки, тормозные колодки, а иногда переставляет дорогой измеритель мощности или седло с одного байка на другой. Цепь изнашивается за 3000 км, тормозные колодки за 1500 км, а рама велосипеда может служить десятилетиями.
Как он мучается сейчас: Ведет учет в Excel, блокноте или держит в голове: "Так, эту цепь я поставил весной... пробег байка тогда был вроде 12 000 км... сейчас 14 500 км... значит, цепь прошла 2500 км... или я менял её позже?". В итоге цепь растягивается, съедает дорогую кассету и систему, а пользователь попадает на дорогостоящий ремонт из-за банальной забывчивости.
Мечта юзера: Зайти в приложение, увидеть интерактивный список всех деталей своего байка с точным остатком ресурса и индикатором "Пора менять", а также иметь "Graveyard" (кладбище) изношенных компонентов и склад ("Inventory") для быстрого нахождения запчастей.
2. Как это ДОЛЖНО работать у тебя (UX-Магия)
Весь интерфейс Гаража должен ощущаться как профессиональный гоночный паддок.
Сценарий (Что видит юзер):
- В гараже у каждого велосипеда появляется вкладка ⚙️ [ Компоненты ].
- Внутри находится аккуратный список компонентов, сгруппированных по 6 основным категориям (Трансмиссия, Тормоза, Колеса, Подвеска, Кокпит, Аксессуары).
- Правило чистоты: Если в категории нет компонентов, эта группа (аккордеон) автоматически скрывается, чтобы не захламлять экран пустыми блоками.
- У каждого компонента есть статус:
- Mounted (Установлен): Зеленая точка, привязан к конкретному байку, накапливает пробег.
- Inventory (На складе): Желтая точка, лежит на полке в гараже, готов к установке.
- Archived (В архиве): Серый ярлык, отслужившие детали (Кладбище компонентов).
- Форма добавления/редактирования:
- Позволяет ввести бренд, модель, цену и настроить двойные лимиты износа (максимальный километраж в км и максимальный налет в часах).
- Поддерживает мультиселект совместимых велосипедов, чтобы при замене предлагать только подходящие детали.
3. СЕКРЕТ ДЛЯ ПРОГРАММИСТА (Архитектурные правила)
Компоненты требуют сложного стейт-менеджмента на стыке реляционной базы данных и бизнес-логики:
Реляционная целостность (Drizzle ORM):
- Таблица
componentsхранит связьuserIdи опциональнуюbikeId(null для склада или архива). - Для хранения совместимости используется JSONB-массив
compatibleBikeIds: string[]. - Вся статистика износа разделена на 8 независимых счетчиков:
- Лимит жизненного цикла (
maxLifespanKm,maxLifespanHours) и текущий износ (currentLifespanKm,currentLifespanHours). - Межсервисный интервал (
serviceIntervalKm,serviceIntervalHours) и текущий сервисный набег (currentServiceKm,currentServiceHours).
- Лимит жизненного цикла (
- Таблица
API Эндпоинты (
apps/api):GET /api/components— получение списка с фильтрацией по статусу (?status=) или велосипеду (?bikeId=).POST /api/components— создание новой детали с валидацией через Zod.PATCH /api/components/:id— обновление параметров.POST /api/components/:id/archive— перевод в архив (при этом полеbikeIdжестко сбрасывается вnull).POST /api/components/:id/restore— восстановление из архива на склад (status = 'INVENTORY').
4. Защита от "дурака" (Бизнес-политики безопасности)
Ловушка 1: "Мутация на ходу"
Проблема: Если компонент установлен на велосипед (Mounted), пользователь не должен иметь возможности изменить его категорию или список совместимых байков в форме редактирования. Иначе можно "на ходу" превратить установленную цепь в амортизатор, сломав расчет физического износа. Решение:
- На бэкенде: В
PATCH /api/components/:idпри попытке обновитьcategoryилиcompatibleBikeIdsдля установленной детали сервер возвращает ошибку400 Bad Request. - На фронтенде: Форма редактирования автоматически блокирует (делает
disabled) поля Категории и Совместимости, если статус равенMOUNTED, сопровождая это подсказкой: "Нельзя менять категорию установленной детали".
Ловушка 2: "Физическое удаление активных данных"
Проблема: Случайное удаление установленной цепи сотрет историю износа и сломает транзакции Strava. Решение: Разрешить физическое удаление (DELETE /api/components/:id) только для архивных деталей (status === 'ARCHIVED'). Если деталь активна, сервер возвращает 400 Bad Request. Чтобы удалить деталь, пользователь должен сначала отправить её "на кладбище" (в архив).
Эпики и Задачи (Epics & Tasks)
Epic 1: База данных и Drizzle-схема
- Task 1.1: Описать таблицы
componentsвschema.ts, добавить индексы дляuserIdиbikeId. - Task 1.2: Сгенерировать и применить миграции Drizzle, проверить корректность работы внешних ключей.
Epic 2: REST API и Валидация (apps/api)
- Task 2.1: Реализовать роутер компонентов с безопасной проверкой
userIdна каждом шаге. - Task 2.2: Написать проверку целостности для
PATCH(запрет мутации категории установленной детали). - Task 2.3: Написать защитную логику для
DELETE(удаление только архивных компонентов).
Epic 3: Интерфейс инвентаризации в Веб-приложении (apps/web)
- Task 3.1: Сверстать динамический список с аккордеонами и фильтрацией пустых категорий.
- Task 3.2: Разработать форму добавления/редактирования с блокировкой полей для установленных компонентов.
- Task 3.3: Реализовать раздел "Кладбище компонентов" (Graveyard) с кнопками восстановления и окончательного удаления.
Epic 4: Автоматическое тестирование
- Task 4.1: Написать интеграционные тесты для API на проверку блокировок
PATCHиDELETE. - Task 4.2: Покрыть юнит-тестами поведение формы редактирования во фронтенд-слое.