Кладовщик у подъёмника выбирал размер из девятнадцати позиций глазами. Теперь
экран показывает только подходящее выбранной машине (по подтверждённой
применимости), с счётчиком и кнопкой-переключателем на полный список.
Если по модели применимость не отмечена, фильтровать нечем — показываются все
размеры и объясняется, почему: прятать правильную резину по незнанию хуже, чем
показать лишнюю.
Плашка на строке различает «подходит» (применимость) и «штатный» (справочник
размеров), и предвыбор позиции теперь учитывает оба признака.
Осенью через кладовщика проходит весь парк, поэтому экран построен вокруг
скорости: выбрал машину — приложение подсказало штатный размер модели, показало
остаток и контейнер, три кнопки состояния, выдал. Контекст приходит одним
запросом: телефон у подъёмника работает по слабой связи, и три round-trip'а на
каждую машину заметны на четырёхстах машинах.
Пока не сказано, что случилось с предыдущим комплектом (вернули / списали / не
найден), кнопка выдачи заблокирована. Сервер это тоже проверяет — здесь просто
не даём дойти до отказа.
Ключ идемпотентности живёт, пока открыта форма: ретрай после обрыва связи должен
попасть в ту же выдачу, а не создать вторую с четырьмя лишними покрышками.
Справочник размеров подсказывает те размеры, что реально лежат на складе:
набирать «225/65/17» с телефона долго и легко ошибиться. Модели отсортированы по
числу машин — самые ходовые заполняются первыми.
Разделы админские, как приёмка и инвентаризация.
Механик ошибся в количестве при пересчёте — раньше единственным выходом было
бросить инвентаризацию и начать заново. Теперь строку находки можно исправить
или удалить, пока пересчёт в черновике.
Правка идёт через тот же фасад склада, что и остальные складские действия
приложения, — отдельного пути в обход правил остатков не появилось.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Задача CRM 4386: «набрал номер авто, смотрю историю ремонтов по машине,
а пробеги не вижу сразу, нужно проваливаться в ремонт».
Пробег бэкенд отдавал в строке ленты и раньше — лента его просто не забирала.
Теперь он стоит справа на строке механика: так пробеги выстраиваются в колонку
и историю одной машины можно читать сверху вниз, не открывая каждый ремонт.
Показываем ровно тот пробег, что зафиксирован в самом ремонте. Он и есть
старлайновский: форма создания подставляет его из последнего снимка StarLine,
механик только подтверждает (если сервис недоступен — вводит руками).
Подставлять в историю ТЕКУЩИЙ пробег из StarLine нельзя: у ремонта полугодовой
давности он показал бы сегодняшнее число.
Когда пробега нет — не пишем ничего. У ремонтов из импортированного архива он
есть в 2% случаев, и подпись «пробег не записан» в каждой второй строке была бы
шумом. У ремонтов, заведённых в приложении, пробег заполнен в 72% случаев.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец: «чтобы по клику по инвентаризации я видел не просто информацию о том,
что я не могу её поменять, а итоги. Какие позиции найдены, какие списаны».
Клик по проведённой сессии открывал тот же экран обхода со списком,
отфильтрованным на «только непосчитанные». У проведённой посчитано всё, поэтому
список оказывался пуст, а сверху висел баннер «правки закрыты»: экран показывал
ровно то, чего не просили, и прятал всё остальное.
Теперь отдельный экран итогов: шапка (зона, кто и когда провёл, счётчики,
недостача и излишек в штуках и рублях) и список группами — не нашли, не хватает,
излишек, находки; «сошлось» и «не дошли» свёрнуты числом, чтобы не листать мимо
совпавших строк. Только просмотр.
Числа на этом экране решают, поэтому отдельно закрыто:
- «Всё сошлось — расхождений не было» показывалось при любом числе
НЕПОСЧИТАННЫХ позиций. Механик обошёл одну из ста, провёл — и сверху документа
зелёным «зона чистая». Теперь это отдельное предупреждение: расхождений среди
посчитанного нет, но обойдено не всё.
- Шапка считается по тем же строкам, из которых собираются секции, а не берётся
готовым агрегатом с бэкенда — сходимость по построению, а не по совпадению
определений. «Недостача» и «Излишек и находки» названы так, чтобы не совпадать
с именем секции ниже: одноимённость заставляла сверять и не сходиться в разы.
- Позиции без цены партии больше не выпадают из денег молча — помечены в строке,
и в шапке сказано, что реальная сумма больше.
- «Обошли X из Y» больше не включает находки: раньше они завышали долю
обойдённого и противоречили чипу «не дошли» в той же шапке.
Идущая инвентаризация не тронута: ввод количества, «Сходится», «Нет», находки и
проведение работают как раньше.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец: «не хочу ещё один раздел, хочу чтобы в списке ремонтов просто
появилось поле поиска». Поле встроено в шапку существующего экрана /repairs,
нового раздела и роута нет.
Поиск СЕРВЕРНЫЙ, и это принципиально. Лента грузится курсором по 20 строк:
фильтрация «по уже загруженному» на старте видела бы последние 20 ремонтов
парка и честно отвечала «ничего не найдено» на машину, которая была в ремонте
позавчера. Отсутствие поиска хотя бы не врёт. Вторая ловушка того же варианта:
маркер бесконечной прокрутки стоит сразу под списком, и схлопнутый фильтром
список подтянул бы его в кадр — лента начала бы молча выкачивать всю таблицу
по 20 строк за запрос.
- q уходит на КАЖДОЙ странице и входит в ключ кэша: смена запроса создаёт свой
список со своим курсором, поэтому курсор нефильтрованной ленты физически не
может подмешать чужие ремонты во вторую страницу найденного.
- Во время набора на экране остаётся прошлая выдача (приглушённой), а не
пульсирующие скелетоны на каждое нажатие клавиши.
- «Обновить» перезапрашивает одну страницу вместо всех загруженных: у
бесконечного запроса refetch тянет их последовательно, и на слабой сети это
десять round-trip'ов подряд с десятью шансами словить ошибку.
При этом во время обновления догрузка молчит — иначе схлопывание ленты
подтягивает маркер в кадр, fetchNextPage отменяет летящий запрос обновления,
и новые ремонты сверху не появляются, сколько ни жми.
- «Ничего не нашли по этому запросу» отличается от пустой ленты, а сбой сети —
от ответа сервера с ошибкой: совет «проверьте сеть» на 500-ку отправляет
механика перезагружать вайфай, когда чинить у него нечего.
Строка ремонта, дневные разделители, месячный итог и поведение ленты без
поиска не менялись.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Бэкенд раз в час закрывает осмотры, которые несколько часов никто не трогает,
— иначе снятые фото не попадают в отчёты. Редактирование в приложении завязано
на статус, поэтому механик, вернувшийся с перерыва, упирался бы в
заблокированную карточку и заводил осмотр заново.
На автозакрытом (meta.auto_completed) показываем плашку с объяснением и кнопку:
статус возвращается в in_progress, редактор открывается снова.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Поле количества приводилось к минимуму на КАЖДЫЙ onChange
(`Math.max(1, Number(v))`), поэтому стирание единицы мгновенно возвращало
её обратно: набрать «4 свечи» было нельзя — цифра дописывалась и выходило
«14». Ровно этот же баг однажды чинили в цене работы (3b7b2ae), там ""
уже разрешено как промежуточное состояние стейта.
- общий QtyStepper: "" остаётся в стейте, пока печатают; приведение — на
blur и ещё раз на отправке (пустое = 1 шт: строка есть, значит деталь
ставили);
- тап по полю выделяет число — набор поверх заменяет, а не дописывает;
- кнопки −/+: 4 свечи набираются одним пальцем в перчатках, мобильная
цифровая клавиатура для этого слишком долгая;
- то же в форме правки ремонта — там я повторил ту же ошибку при переводе
запчастей на складской каталог;
- регрессионные тесты на стирание/blur/степпер: баг повторился второй раз
на том же паттерне, пусть дальше ловится сам.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Строка в «Последних приходах» стала кликабельной, после проведения
накладной открывается сразу она — видно, что именно получилось.
Показываем снимки на момент приёмки (имя, артикул), а не текущие значения
каталога: накладная — исторический документ, переименование номенклатуры
не должно её переписывать. Единственное «сегодняшнее» поле — сколько от
партии ещё лежит на складе: лежит целиком / осталось N / разошлось.
Правки нет намеренно: проведённый приход уже стал партиями и движениями,
и «поправить» его значило бы разъехаться с журналом — сторно делается в
CRM, где видны последствия. В карточке это сказано текстом, чтобы не
выглядело как недоделка.
Заодно после проведения инвалидируются списки черновиков и приходов —
иначе при возврате в раздел они показывались из кэша устаревшими.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Приёмка. Главное действие — снять накладную/чек камерой; несколько листов
уходят одним документом. Кадр ужимается до 2200 px на клиенте: сервер всё
равно приводит к этому размеру, а на мобильном интернете в гараже разница
в секундах загрузки заметная. Распознанное проверяется рядом с фото
документа — строки правятся на месте, поставщик выбирается или заводится
прямо с накладной. «Провести» приходует товар в выбранную зону.
Инвентаризация. Запуск спрашивает зону: весь склад одной сессией — это
сотни строк, которые никто не обойдёт за раз. На обходе по умолчанию видны
только непосчитанные, посчитанная сразу уходит из списка. Пустое поле —
«ещё не считали», ноль вводится кнопкой «Нет» (это уже недостача). Находки
подбираются из складского каталога общим пикером. Внизу постоянно висят
недостача и излишек в штуках и рублях.
Доступ: плитки в меню и маршруты закрыты RequireAdmin (тот же гейт на
сервере — клиентская проверка нужна, чтобы прямая ссылка не открывала
экран с ошибкой вместо «вам сюда нельзя»).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Прод mechanic-pwa собирался с ветки feat/driver-pwa, а main отставал на 18
коммитов (24.05→10.06). Сборка от 29.07 ушла в прод с main — из приложения
пропали разделы. Вернул содержимое ветки и заново наложил работу 29.07.
Возвращено:
- лента: сумма за месяц, разделители по дням с итогом дня, цена в строке,
бейдж «с улицы»;
- карточка ремонта /repairs/:id (ссылка из ленты вела в никуда);
- правка ремонта /repairs/:id/edit (окно 1 час);
- ремонт «с улицы» /repairs/new-external;
- блок «Механики» — делёж оплаты 1/N;
- vitest-харнесс и вынесенные в lib/ тестируемые хелперы.
Поверх наложено (работа 29.07): несколько замен масла за заезд, запчасти из
складского каталога вместо свободного текста.
Дополнительно:
- пикер деталей вынесен в общий PartsPickerModal (создание + правка). Без
этого правка ремонта отправляла детали без part_id и сверка склада
возвращала всё списанное на остаток;
- форма правки знает про 1:N масла — раньше схлопнула бы вторую запись;
- карточка ремонта показывает артикул детали и все замены масла.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Блок «Запчасти» — модалка-пикер по складскому каталогу (searchParts):
поиск по названию/артикулу, остаток и пометка «подходит», тумблер
«Показать все» (снимает фильтр применимости). Выбор шлёт part_id —
бэкенд списывает деталь со склада. Свободный ввод убран.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Блок «Замена масла» теперь мульти: чекбоксы Двигатель / Коробка (можно оба),
общий одометр (одна замена — один пробег). Отправляем список oil_changes
(1:N), стикер прикрепляем к первой выбранной. Тип 'transmission' объединяет
АКПП/вариатор/робот — интервал по модели в отчёте СТО.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Для отчётов СТО по замене масла (задача 4078) нужно различать двигатель и
трансмиссию. В блоке «Замена масла» добавлен выбор типа oil_type
(engine|cvt|akpp) и он уходит в payload create. Для вариатора/АКПП поле
«следующая через (км)» скрыто — интервал задаётся по модели в отчёте СТО.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Активация раздела «Ремонт» в Premium Мехник PWA: лента + создание ремонта,
фоновая сверка прав (У35), различающие сообщения при отказе входа (У36).
Backend для всех новых API в taxi-dashboard (отдельный commit eccaebe).
Тулится на `ENABLE_STO_MODULE=true` в backend env.
PR-4: Login + useMeSync.
- src/api/me.ts: тип Me расширен (is_admin, can_login_pwa, departments,
support — для У35/У36).
- src/hooks/useMeSync.ts: фоновая сверка `/me` раз в 5 мин (У35). При
403 NO_PWA_ACCESS — сброс токена, revocation info в sessionStorage.
IndexedDB-черновики не трогаются.
- src/pages/Login.tsx: после успешного login делает getMe() и при 403
показывает баннер «У вас нет доступа» с кликабельным TG/телефонным
контактом (У36). Баннер также рендерится при revocation после
фоновой сверки.
- src/App.tsx: useMeSync() подключён в корне.
PR-5: лента ремонтов.
- src/pages/ActionMenu.tsx: снят `comingSoon` с тайла «Ремонт».
- src/pages/repairs/RepairsFeedPage.tsx: лента с infinite scroll (через
IntersectionObserver), плейсхолдер «В парке пока нет ремонтов» (У41),
ellipsis-имена работ + плюрализация «+N работ» (У40).
- src/hooks/useRepairsFeed.ts: useInfiniteQuery + stale-while-revalidate
+ visibility refresh (У38).
- src/lib/timeFormat.ts: формат «Сегодня / Вчера / 23 мая» (У39).
- src/api/repairsFeed.ts: GET /api/v1/mechanic/repairs клиент.
PR-7: создание ремонта (online-only).
- src/api/repairsCreate.ts: searchCars, getCarContext, searchWorks,
suggestParts, presignPhoto + uploadPhotoToTmp (PUT с правильным
Content-Type), createRepair (POST с Idempotency-Key).
- src/pages/repairs/CarSelectPage.tsx: поиск машины с debounce 200мс.
- src/pages/repairs/CreateRepairPage.tsx: карточка создания ремонта с 5
inline-блоками (Шапка / Пробег / Фото / Работы / Запчасти / Замена
масла). UUID v4 как Idempotency-Key. Карандашик-разблокировка цены
с обязательным комментарием (У19). Мягкая подписка пробега масла
на шапочный (У24). Дефолт next_in_km=10000 (У25). Полноэкранная
модалка поиска работ.
Routes: /repairs, /repairs/new, /repairs/new/:carId.
Co-Authored-By: claude-flow <ruv@ruv.net>
Previously slotsForType excluded all general slots for return-type
inspections and left only Домкрат, on the assumption that the car is
already familiar and приёмка should focus on damages/equipment. Feedback
from mechanics: при приёмке всё равно нужно фиксировать общее состояние
машины, иначе споры с водителем теряют доказательную базу. Now все типы
осмотров показывают одинаковый набор слотов.
Co-Authored-By: claude-flow <ruv@ruv.net>
Backend search now matches only on plate, so reflect that in the input
placeholder instead of misleading 'Поиск по номеру или VIN'.
Co-Authored-By: claude-flow <ruv@ruv.net>
Раньше textarea для заметок жил в самом низу страницы, после фото-сетки
и списка меток — на плановом осмотре приходилось много скроллить чтобы
добраться до основного поля ввода.
Перенёс Card 'Заметки механика' сразу под Card 'Схема'. На плановом осмотре
текстовое поле теперь второе на странице — не надо листать.
Раньше textarea для заметок жил только в InspectionReview (завершённый
осмотр). При создании нового планового осмотра (через '/inspections/<id>/edit')
поля ввода не было — приходилось завершить осмотр, потом открыть карточку
и только там вводить.
Теперь блок есть в обеих страницах:
- InspectionEditor (in_progress): textarea + кнопки Сохранить/Отменить
- InspectionEditor (completed): read-only вывод текста
- InspectionReview: оставлен прежний редактор как был
Бонус: при нажатии «Завершить осмотр» текущий draft заметки коммитится
тем же PATCH вместе со status=completed — не теряется если механик
ввёл текст и сразу нажал завершение, не сохранив отдельно.
VehicleSummary.status (cars_v2.element_status) рендерится как цветной
pill справа от гос.номера. Палитра по семантике:
emerald — На линии (норма)
amber — Ремонтируется / Ждет ремонта
red — ДТП
blue — Бронь / Выкуп / На продаже / Подготовка на продажу / Ждет
доставки до офиса (admin-операции)
muted — Простой и неизвестные статусы
Чип появляется в Home (списке машин) и в шапке VehicleCard. Backend
сторона — PremiumCRM 92d4317.
TtStateSummary.driver_id + driver_name добавлены в TS-тип. В строке
каждого Element-state'а под датой/пробегом теперь выводится '👤 ФИО'
если backend подтянул водителя. Логика идентична нашим осмотрам — общий
helper drivers_at_dates на backend (см. PremiumCRM aa55431).
Под датой/пробегом каждого осмотра добавлена строка «👤 ФИО» если backend
вернул driver_name (рассчитан как rental, активный на момент started_at —
см. PremiumCRM 4700314). Раньше механик/менеджер не мог быстро понять, у
кого мы принимали машину — приходилось открывать VDS-карточку.
InspectionSummary.driver_id + driver_name добавлены в TS-тип.
1. Сверху на /inspections/<id> добавлена кликабельная карточка машины:
гос.номер крупно + марка/модель/год + VIN. Использует новое поле
ins.vehicle, которое backend теперь возвращает в InspectionDetail
(см. PremiumCRM коммит a9c94d0). Клик ведёт на /vehicles/<id>.
2. Блок «Заметки механика» — textarea с локальным draft'ом и кнопкой
«Сохранить» (видна только если draft отличается от ins.notes). Под
капотом — PATCH /api/v1/mechanic/inspections/<id> с {notes}.
TG-импортированные осмотры из «Подготовка авто» уже приезжают с
заполненными notes (caption сообщения становится телом заметки), и
теперь механик может дополнить/исправить их прямо в карточке.
3. Бонус: добавлен показ пробега (mileage) в первой info-карте.
Counter «Фото (N)» по-прежнему учитывает TT carry-over (поэтому на 84-м
осмотре всё ещё 16), но карточки в гриде остаются только для наших MinIO-
фото + PWA-маркерных фото. TT-thumbnail рисуется один раз — inline под
своим маркером в списке.
1. Photo counter теперь учитывает уникальные tt_image_id из markers, не только
ins.photos + photo_id. На осмотрах, где почти все маркеры пришли из Element
carry-over, счётчик показывал 1 вместо 15+ — теперь корректно.
2. В галерее «Фото» добавлена секция карточек для TT-image carry-over (label
«из Element»). Картинки тянутся через ttImageUrl(hash), кликабельны.
3. Lightbox state поменялся с `number | null` на дискриминированный union
{ kind: 'photo', id } | { kind: 'tt', ttId }. Полноразмерное превью
выбирает между AuthImg (наши MinIO-фото) и обычным <img> (Element).
4. Превью маркеров в секции «Метки» теперь кликабельны (раньше только грид
с основными фото открывал lightbox). Работает и для photo_id, и для
tt_image_id маркеров.
Backend-зависимость: MarkerOut.tt_image_id (Pydantic) и
MechanicDamageMarker.tt_image_id (ORM) добавлены в PremiumCRM коммитом
d405e17 на crm.pptaxi.ru — без них фронт получает tt_image_id=undefined.
Single-screen grid layout showing all 5 PNG views + 4 bumper corner sun-zones + decorative
wheels at once. Tap any view for precise AddMarkerDialog; tap bumper corners for center-point
marker; global Tire Wear button opens new TireWearDialog (4-level radio cards).
Deletes MultiViewVehicleScheme. No backend changes. humanizeMarkerSide covers bumper-* and tires.
Co-Authored-By: claude-flow <ruv@ruv.net>
- Replace SVG sedan zone-click model with 5-view PNG scheme (top/front/back/left/right)
- New MultiViewVehicleScheme: tab selector, tap-to-exact-coord marker placement,
touchAction:manipulation prevents iOS double-tap zoom, marker dots at normalized x/y
- New AddMarkerDialog: bottom-sheet modal with damage_type (16 opts), severity (4 opts),
description, optional CameraCapture → uploadPhoto(side="free") → photo_id on submit
- InspectionEditor: removes addZoneMarker mutation and zone-count machinery; wires
pendingTap state → dialog open/close → query invalidation
- InspectionReview: adds read-only MultiViewVehicleScheme; both pages show photo_id
thumbnails via AuthImg in the marker list
- humanizeMarkerSide updated for new view names; keeps zone-* legacy mapping
- Deletes VehicleScheme.tsx and sedan-top.svg (no remaining imports)
- No backend changes, no new dependencies
Co-Authored-By: claude-flow <ruv@ruv.net>
Wire frontend to backend 2FA contract: two-step state machine in Login.tsx
(credentials → totp), new login2fa() call to POST /api/auth/2fa, credentials:include
on both fetches so the 30-day trusted_device cookie travels correctly.
Co-Authored-By: claude-flow <ruv@ruv.net>
- api/auth.ts: OAuth2 password flow via form-urlencoded POST /api/auth/login (not mechanic prefix)
- api/me.ts: GET /api/v1/mechanic/me with Me interface
- api/vehicles.ts: listVehicles (unwraps {items:[]}), getVehicle with VehicleDetail + InspectionSummary
- api/inspections.ts: createInspection returning full InspectionDetail; 9-value InspectionType union
- Login.tsx: full form with Логин/Пароль labels, useMutation, toast feedback, navigate on success
- Home.tsx: vehicle list with search, /me header with user name, logout, TanStack Query
- VehicleCard.tsx: vehicle info card, all 9 inspection type buttons (ru labels), history list
Co-Authored-By: claude-flow <ruv@ruv.net>