Skip to content

Use Case #UC-027.1: Учёт велосипедных компонентов ⚙️ (Базовая инвентаризация и гараж)

1. Жизненная ситуация (Боль пользователя)

Представь: Увлеченный велосипедист имеет два велосипеда — шоссейник и гравийник. В процессе эксплуатации он меняет цепи, кассеты, покрышки, тормозные колодки, а иногда переставляет дорогой измеритель мощности или седло с одного байка на другой. Цепь изнашивается за 3000 км, тормозные колодки за 1500 км, а рама велосипеда может служить десятилетиями.

Как он мучается сейчас: Ведет учет в Excel, блокноте или держит в голове: "Так, эту цепь я поставил весной... пробег байка тогда был вроде 12 000 км... сейчас 14 500 км... значит, цепь прошла 2500 км... или я менял её позже?". В итоге цепь растягивается, съедает дорогую кассету и систему, а пользователь попадает на дорогостоящий ремонт из-за банальной забывчивости.

Мечта юзера: Зайти в приложение, увидеть интерактивный список всех деталей своего байка с точным остатком ресурса и индикатором "Пора менять", а также иметь "Graveyard" (кладбище) изношенных компонентов и склад ("Inventory") для быстрого нахождения запчастей.


2. Как это ДОЛЖНО работать у тебя (UX-Магия)

Весь интерфейс Гаража должен ощущаться как профессиональный гоночный паддок.

Сценарий (Что видит юзер):

  1. В гараже у каждого велосипеда появляется вкладка ⚙️ [ Компоненты ].
  2. Внутри находится аккуратный список компонентов, сгруппированных по 6 основным категориям (Трансмиссия, Тормоза, Колеса, Подвеска, Кокпит, Аксессуары).
  3. Правило чистоты: Если в категории нет компонентов, эта группа (аккордеон) автоматически скрывается, чтобы не захламлять экран пустыми блоками.
  4. У каждого компонента есть статус:
    • Mounted (Установлен): Зеленая точка, привязан к конкретному байку, накапливает пробег.
    • Inventory (На складе): Желтая точка, лежит на полке в гараже, готов к установке.
    • Archived (В архиве): Серый ярлык, отслужившие детали (Кладбище компонентов).
  5. Форма добавления/редактирования:
    • Позволяет ввести бренд, модель, цену и настроить двойные лимиты износа (максимальный километраж в км и максимальный налет в часах).
    • Поддерживает мультиселект совместимых велосипедов, чтобы при замене предлагать только подходящие детали.

3. СЕКРЕТ ДЛЯ ПРОГРАММИСТА (Архитектурные правила)

Компоненты требуют сложного стейт-менеджмента на стыке реляционной базы данных и бизнес-логики:

  1. Реляционная целостность (Drizzle ORM):

    • Таблица components хранит связь userId и опциональную bikeId (null для склада или архива).
    • Для хранения совместимости используется JSONB-массив compatibleBikeIds: string[].
    • Вся статистика износа разделена на 8 независимых счетчиков:
      • Лимит жизненного цикла (maxLifespanKm, maxLifespanHours) и текущий износ (currentLifespanKm, currentLifespanHours).
      • Межсервисный интервал (serviceIntervalKm, serviceIntervalHours) и текущий сервисный набег (currentServiceKm, currentServiceHours).
  2. 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: Покрыть юнит-тестами поведение формы редактирования во фронтенд-слое.