Заказчик - грузоперевозочная компания в США: свои тягачи и прицепы, наёмные водители, диспетчер и владелец. Всю операционку вели вручную. Рейсы жили в таблицах, документы по рейсу (рейт-конфирмейшн, BOL, POD) водители присылали фотографиями в мессенджер, кто когда приехал на точку - выяснялось звонками. Раз в неделю владелец садился считать зарплату водителям в Excel: у одних процент от ставки рейса, у других оплата за милю по одометру. Прибыль по месяцу сводилась там же и расходилась с реальностью.
Задача: собрать одну систему на весь цикл рейса и на деньги вокруг него.
- Диспетчер создаёт рейс: брокер, ставка и корректировка, мили, точки погрузки и выгрузки с окнами подачи, назначение водителя, тягача и прицепа.
- Водитель работает с телефона в дороге: видит свой рейс, отмечает прибытие и убытие на каждой точке, прикладывает фотографии документов. Пока обязательные документы по точке не приложены, закрыть её нельзя.
- Рейс проходит понятный жизненный цикл до денег: доставлен, POD отправлен, ушёл в факторинг, брокер оплатил. Часть шагов ставится автоматически, часть - только владельцем.
- Владелец ведёт расходы по категориям, считает недельные выплаты водителям по тарифному плану и получает отчёт о прибыли за месяц: выручка по водителям, выплаты, расходы по категориям, итог.
- Деньги должны быть точными и неизменяемыми: после отметки «выплачено» расчёт замораживается и не пересчитывается задним числом, даже если рейс потом отредактировали.
- Три роли с разными правами: владелец видит всё, диспетчер - рейсы и брокеров, водитель - только свои рейсы и свою выплату, без чужих ставок.
- Система разворачивается на сервере заказчика и работает в реальной эксплуатации: правки приходят от живой работы и должны выкатываться быстро.
Отдельной задачей позже добавился публичный сайт компании: витрина для грузоотправителей и найма водителей со ссылкой на вход в систему.
Спроектировал и написал систему целиком: API, веб-интерфейс, мобильные экраны водителя, инфраструктуру и деплой.
. Бэкенд на NestJS и TypeScript, база PostgreSQL через Prisma. Около шестнадцати модулей: аутентификация и роли, рейсы и точки маршрута, водители, тягачи, прицепы, брокеры, документы и хранилище файлов, расходы и категории, тарифные планы, выплаты, отчёты, почта. Все данные привязаны к компании и фильтруются по ней на уровне общего CRUD-слоя, так что система готова к нескольким компаниям, но развёрнута на одну. Общий пакет типов и zod-схем используется и API, и вебом: одна схема валидирует запрос на сервере и типизирует форму на клиенте.
. Деньги. Ставка рейса считается как база плюс корректировка. Недельная выплата собирается сама: система берёт рейсы, доставленные внутри расчётной недели (понедельник-понедельник, по дате выгрузки в часовом поясе компании), считает базу по тарифному плану водителя - процент от ставки или мили по одометру - и добавляет ручные надбавки. Нажатие «выплачено» атомарно замораживает расчёт в снимок: строки рейсов, план, ставка, одометр, итог. Дальше выплата неизменяема, и отчёт за месяц берёт из неё факт. P&L собирает выручку по водителям по дате доставки, выплаты водителям одной строкой на человека и расходы, сгруппированные по уровням категорий с подытогами. Отчёты и выплаты выгружаются в PDF через pdfmake.
. Фронтенд на React и TypeScript: Vite, Redux Toolkit с RTK Query, Tailwind и Radix UI. Одно приложение обслуживает и офис, и водителя: по роли меняются навигация и экраны. Офис получает таблицу рейсов с вкладками по стадии оплаты, карточку рейса с вкладками, справочники, расходы, выплаты и отчёты. Водитель на телефоне - только свои рейсы, отметки прибытия и убытия по точкам и загрузка документов, с блокировкой закрытия точки без обязательных фото. Списки на обеих сторонах обновляются сами: офис видит отметки водителя без перезагрузки страницы. Тёмная и светлая темы.
. Документы. Загрузка с телефона с проверкой содержимого, а не только заголовка: PDF по сигнатуре файла, изображения нормализуются в JPEG через sharp, съёмка с айфона в HEIC конвертируется перед обработкой. Имена файлов формируются по рейсу и типу документа, доступ к файлу проверяется по роли и принадлежности рейса.
. Безопасность и эксплуатация. Аутентификация на JWT с коротким сроком жизни и ротируемыми refresh-токенами: сессия скользит неделю, при этом токен в базе хранится только хешем. Роли закрывают каждый маршрут, номер социального страхования водителя шифруется AES-256-GCM и расшифровывается только для владельца и самого водителя. Инфраструктура на Node.js: Docker Compose, Caddy с автоматическим HTTPS, GitHub Actions - пуш в ветку собирает образы и выкатывает их на сервер заказчика, схема применяется на старте контейнера. Публичный сайт компании сделан отдельно на Astro и Tailwind: статика, живое время трёх часовых поясов, форма заявки, ссылка на вход в систему.
Компания перешла с таблиц и переписки на одну рабочую систему. Диспетчер заводит рейс и назначает водителя, водитель в дороге закрывает точки с телефона и прикладывает документы, отметки времени тут же видны в офисе без перезагрузки. Рейс доходит до денег по понятной цепочке статусов, а не по памяти.
Зарплата и прибыль считаются системой, а не вручную в Excel. Недельная выплата собирается из доставленных рейсов по тарифному плану водителя, после отметки «выплачено» расчёт заморожен и не меняется задним числом. Месячный P&L показывает выручку по водителям, выплаты и расходы по категориям с подытогами и итоговую прибыль, отчёт и выплаты выгружаются в PDF.
Система работает в реальной эксплуатации на сервере заказчика, развитие идёт итерациями: заказчик присылает правки из живой работы, они выкатываются на прод в тот же день одним пушем через GitHub Actions. За время работы так вышли переработанный список рейсов со ставкой за милю и вкладками по стадии оплаты, живое обновление отметок водителя, исправления в расчёте месяца выплаты и публичный сайт компании.
Система закрытая, работает внутри компании под аутентификацией, поэтому публичной ссылки на неё нет: в портфолио показаны интерфейсы на демонстрационном автопарке с вымышленными водителями, брокерами и суммами.