308 lines
24 KiB
Markdown
308 lines
24 KiB
Markdown
# Сервисный центр — платформа: обзор проекта и руководство пользователя
|
||
|
||
> Этот файл — человеко-читаемое описание всей платформы и её основного модуля
|
||
> (`production`), для владельца бизнеса и для себя на будущее. Техническая
|
||
> справка по API/деплою этого репозитория — в [README.md](README.md), она не
|
||
> дублируется здесь.
|
||
|
||
## 1. Что это
|
||
|
||
Модульная CRM для сервисного центра (Севастополь): приём техники в ремонт,
|
||
заправка картриджей, продажа/выкуп техники, склад и закупки, касса,
|
||
аналитика. Заменяет разрозненные таблицы и мессенджеры одним рабочим местом
|
||
для владельца, менеджеров и мастеров.
|
||
|
||
Разработка началась 2026-08-08. Основная дорожная карта (фазы 0–10: MVP,
|
||
документы, картриджи, склад, AI-приёмка, касса, уведомления, аналитика,
|
||
AI-диагностика) закрыта к 2026-08-10, после чего добавлен второй слой фич
|
||
(лояльность, запись на приём, поставщики и закупки, гарантия/возвраты,
|
||
трейд-ин, кастомные поля, шаблоны документов, PWA, управление модулями).
|
||
|
||
## 2. Архитектура платформы
|
||
|
||
Платформа состоит из независимых сервисов — каждый со своей БД, своим
|
||
рантаймом, своим docker-compose. Общее — только staff-идентичность (JWT) и
|
||
реестр модулей, оба живут в `core`.
|
||
|
||
| Сервис | Репозиторий | Роль | Порт (loopback) |
|
||
|---|---|---|---|
|
||
| **core** | `~/service-center` | Auth (JWT owner/manager/master), реестр модулей, heartbeat/restart/maintenance | backend `18090` |
|
||
| **production** | `~/production` (этот репозиторий) | Приём в ремонт, картриджи, склад, касса, аналитика — основной объём функциональности | backend `18091`, web `18092` |
|
||
| **site** | `~/site` | Публичный лендинг + форма лида | backend `18093`, web `18094` |
|
||
| **online-store** | отдельный прод-деплой (Next.js) | Продажа техники, уже в проде, синкается вебхуком в `production.sale` | — |
|
||
|
||
Модуль регистрируется в `core` (`POST /api/modules`), получает `MODULE_TOKEN`
|
||
и шлёт heartbeat каждые 30с. `core` может дистанционно перезапустить модуль
|
||
или перевести его в режим обслуживания (страница `/modules`, owner-only).
|
||
`production` не имеет собственного логина — JWT выпускает `core`, оба сервиса
|
||
проверяют его одним и тем же `JWT_SECRET` (HS256).
|
||
|
||
Все сервисы стоят за системным Caddy на хосте (домен ещё не выбран), сеть
|
||
между контейнерами — общий docker-сеть `platform_net`.
|
||
|
||
## 3. Технологический стек
|
||
|
||
**Backend:** Go + Fiber, PostgreSQL (`pgx/v5`, собственные SQL-миграции —
|
||
не Supabase/ORM), MinIO (файлы: фото заявок, документы). AI — Gemini
|
||
(бесплатный тариф): structured output для AI-приёмки заявок и AI-диагностики
|
||
по фото, свободный текст для аналитической сводки. Docker + Caddy для
|
||
деплоя.
|
||
|
||
**Frontend (`web/`):** React 19 + Vite (без TypeScript, `.jsx`), React
|
||
Router 7, `@dnd-kit` для drag-n-drop Kanban-досок, `lucide-react` для икон,
|
||
`vite-plugin-pwa` для PWA-манифеста/service worker. Без Tailwind и без
|
||
UI-фреймворка — своя лёгкая дизайн-система (раздел 9).
|
||
|
||
**Аутентификация:** JWT, роли `owner` / `manager` / `master`, токен хранится
|
||
в `localStorage`, состояние — React Context (`AuthContext`).
|
||
|
||
## 4. Роли и права доступа
|
||
|
||
| Роль | Доступ |
|
||
|---|---|
|
||
| **Владелец (owner)** | Всё. Плюс единственный, кто видит: Сотрудники, Поля приёмки, Справочники, Шаблоны документов, Модули, Настройки |
|
||
| **Менеджер (manager)** | Всё операционное: канбан, картриджи, склад, поставщики, закупки, возвраты, гарантия, трейд-ин, клиенты. Плюс Касса и Аналитика |
|
||
| **Мастер (master)** | Операционные разделы, но видит только заявки/партии, назначенные на него (или ещё неназначенные — чтобы взять в работу). Кассу и Аналитику как отдельные страницы не видит, но кассовая история конкретного своего заказа доступна внутри карточки заказа |
|
||
|
||
Разграничение — не только на фронтенде (скрытие пункта меню), а на
|
||
уровне API (`internal/authz`): чужой заказ/партия картриджей отдают 403 на
|
||
чтение и запись для мастера, включая фото/комментарии/списание запчастей.
|
||
|
||
## 5. Руководство пользователя
|
||
|
||
Ниже — по каждому пункту левого меню (`web/src/layout/Sidebar.jsx`), в том
|
||
порядке, в котором он идёт в интерфейсе.
|
||
|
||
### Канбан (`/kanban`)
|
||
Главный экран. Доска приёма в ремонт с колонками по статусу: **Новая →
|
||
Диагностика → В ремонте → Ждём запчасти → Готово → Выдано** (плюс
|
||
**Отменено**). Карточки заказов перетаскиваются между колонками
|
||
(`@dnd-kit`) — перетаскивание пишет новую запись в таймлайн заказа. Клик по
|
||
карточке открывает модалку заказа: данные клиента и устройства, назначенный
|
||
мастер, оценка/итоговая цена, гарантия (срок + связь с исходным заказом при
|
||
повторном ремонте), таймлайн (смена статусов — всегда публична;
|
||
комментарии и фото — публичность настраивается отдельно для каждой записи),
|
||
кнопки генерации Счёта и Акта (PDF), кнопка AI-диагностики по фото, форма
|
||
AI-приёмки нового заказа (Gemini разбирает свободный текст на структурированные
|
||
поля). Обновляется поллингом раз в 20 секунд — realtime-канал (`internal/realtime`)
|
||
уже реализован для уведомлений о новых заявках, полноценный live-канбан без
|
||
поллинга — не сделано.
|
||
|
||
### Заявки на запись (`/bookings`)
|
||
Заявки, оставленные клиентом с публичной страницы записи (`/book`, без
|
||
логина) — желаемая дата/время, что за проблема. Владелец/менеджер/мастер
|
||
переводит заявку в реальный заказ или отклоняет.
|
||
|
||
### Картриджи (`/cartridges`)
|
||
Отдельная Kanban-доска для заправки картриджей — та же механика статусов,
|
||
что и у заказов в ремонт, но свой домен (`internal/cartridge`): партия
|
||
картриджей, а не устройство. Тоже поддерживает документы (Счёт/Акт), фото,
|
||
комментарии, назначенного мастера и тот же ACL по назначению.
|
||
|
||
### Склад (`/parts`)
|
||
Учёт запчастей и расходников: остатки, себестоимость, списание при ремонте
|
||
(привязка расхода к конкретному заказу — для реальной маржинальности по
|
||
заявке, не только по кассе в целом).
|
||
|
||
### Поставщики (`/suppliers`)
|
||
Справочник поставщиков запчастей — контакты, кто у кого что заказывает.
|
||
|
||
### Заказы поставщикам (`/purchase-orders`)
|
||
Цикл закупки: заказ поставщику → поступление (GRN) → приход на склад.
|
||
|
||
### Возвраты поставщикам (`/rma`)
|
||
Брак/возврат запчастей поставщику — отдельный от клиентской гарантии поток.
|
||
|
||
### Гарантийные случаи (`/warranty-claims`)
|
||
Повторные обращения по гарантии — связаны с исходным заказом
|
||
(`original_order_id`), чтобы видеть историю ремонта одного устройства
|
||
целиком.
|
||
|
||
### Выкуп техники (`/trade-ins`)
|
||
Приём техники от клиента на выкуп (trade-in) — отдельный поток от ремонта:
|
||
оценка состояния, цена выкупа, статус сделки.
|
||
|
||
### Касса (`/cash`) — только owner/manager
|
||
Ручная кассовая книга (`cash_transactions`) — приход/расход, привязка
|
||
операции к заказу или партии картриджей. Отдельная страница — сводный
|
||
журнал по всей кассе; по конкретному заказу кассовая история видна и
|
||
мастеру внутри карточки этого заказа (см. раздел 4).
|
||
|
||
### Аналитика (`/analytics`) — только owner/manager
|
||
Агрегированная отчётность: выручка, операционные метрики (сколько заказов в
|
||
каком статусе, средний срок ремонта и т.п.), остатки склада, плюс
|
||
AI-сводка на свободном тексте (Gemini интерпретирует цифры человеческим
|
||
языком).
|
||
|
||
### Клиенты (`/clients`)
|
||
Физ- и юрлица, поиск, карточка клиента с полной историей его заказов.
|
||
Программа лояльности (`internal/loyalty`) считается здесь же.
|
||
|
||
### Сотрудники (`/staff`) — только owner
|
||
Единственный способ управлять персоналом (у `core`, который реально владеет
|
||
staff-аккаунтами, своего фронтенда нет — эта страница ходит напрямую в
|
||
`core` API). Создание сотрудника, смена роли, отключение/включение
|
||
аккаунта, сброс пароля (владелец задаёт новый пароль лично — self-service
|
||
восстановления по почте нет, инфраструктуры email в проекте нет). Нельзя
|
||
разжаловать или отключить последнего активного owner'а — защита от
|
||
случайной блокировки.
|
||
|
||
### Поля приёмки (`/order-fields`) — только owner
|
||
Кастомные поля для формы приёма заказа (текст/число/выбор/чекбокс,
|
||
обязательное или нет, порядок отображения). Позволяет владельцу подстроить
|
||
форму приёма под свою специфику без изменения кода. Архивация вместо
|
||
удаления — старые значения в уже созданных заказах не теряются.
|
||
|
||
### Справочники (`/catalogs`) — только owner
|
||
Каталог устройств (`devicecatalog`) — бренды/модели для автозаполнения
|
||
формы приёма (плюс кнопка автозаполнения марки/модели телефона по IMEI).
|
||
|
||
### Шаблоны документов (`/document-templates`) — только owner
|
||
Блочный редактор вёрстки Счёта и Акта: реордер блоков, скрытие
|
||
необязательных блоков, свои текстовые вставки. Не freeform HTML/CSS —
|
||
осознанное ограничение, чтобы нельзя было случайно сломать документ.
|
||
Структура обязательных блоков и шрифты не редактируются.
|
||
|
||
### Модули (`/modules`) — только owner
|
||
Панель управления всей платформой на уровне `core`: список зарегистрированных
|
||
модулей (`production`, `site`, `online-store`) со статусом (`healthy` /
|
||
`unhealthy` / `maintenance`), кнопки реального перезапуска контейнера и
|
||
перевода в режим обслуживания (503 на все роуты, кроме `/health`).
|
||
|
||
### Настройки (`/settings`) — только owner
|
||
Реквизиты бизнеса для PDF-документов, ключ/модель Gemini, токен и chat_id
|
||
Telegram-бота, ключ IMEI-сервиса — всё редактируется владельцем без
|
||
редеплоя (фоллбэк на `.env`, если поле в БД пустое). Инфраструктурные
|
||
секреты (`JWT_SECRET`, пароли БД/MinIO, токены модулей) сюда сознательно не
|
||
вынесены — только `.env`.
|
||
|
||
## 6. Публичные страницы (без логина)
|
||
|
||
- **`/track/:token`** — трекинг заказа для клиента: статус, устройство,
|
||
гарантия, только публичные события таймлайна (фото на этой странице пока
|
||
не рендерятся, только факт "добавлено фото").
|
||
- **`/book`** — форма записи на приём, попадает в раздел «Заявки на запись».
|
||
|
||
## 7. Backend: домены (`backend/internal/`)
|
||
|
||
Каждый пакет — одна зона ответственности:
|
||
|
||
| Пакет | Отвечает за |
|
||
|---|---|
|
||
| `auth` | Верификация staff JWT, выпущенного `core` |
|
||
| `authz` | Единое правило ACL: свой/чужой заказ или партия по `assigned_master_id` |
|
||
| `order` / `cartridge` | Заказы в ремонт / партии картриджей — ядро домена |
|
||
| `client` | Физ/юр клиенты |
|
||
| `booking` | Публичные заявки на запись |
|
||
| `cash` | Ручная кассовая книга |
|
||
| `inventory` | Склад запчастей, списание, себестоимость |
|
||
| `supplier` / `purchaseorder` / `rma` | Поставщики → закупки → возвраты браком |
|
||
| `tradein` | Выкуп техники у клиента |
|
||
| `loyalty` | Программа лояльности клиентов |
|
||
| `customfields` | Owner-настраиваемые поля формы приёма |
|
||
| `devicecatalog` | Каталог брендов/моделей устройств |
|
||
| `imei` | Автозаполнение марки/модели по IMEI (HiCellTek TAC lookup) |
|
||
| `aiintake` | AI-приёмка заявки: свободный текст → структурированные поля (Gemini) |
|
||
| `diagnosis` | AI-диагностика по фото (Gemini vision + Google Search grounding) |
|
||
| `analytics` | Агрегированная отчётность + AI-сводка |
|
||
| `document` / `doctemplates` / `pdfgen` / `ordernum` | Генерация Счёта/Акта, блочные шаблоны, нумерация документов |
|
||
| `file` | Хранение файлов в MinIO, реальный MIME-снифф по байтам (не доверяет заголовку клиента) |
|
||
| `settings` | Owner-редактируемая конфигурация без редеплоя |
|
||
| `notify` / `clientnotify` / `tgbot` / `smsgw` | Уведомления персоналу и клиентам (Telegram/SMS) |
|
||
| `coreclient` / `modulecontrol` | Heartbeat в `core`, приём команд restart/maintenance от `core` |
|
||
| `realtime` | Push-сигналы подключённым клиентам о изменениях |
|
||
| `sale` | Записи о продажах, синкаемые из `online-store` |
|
||
| `scheduler` | Фоновые задачи по расписанию |
|
||
| `db` / `dbutil` | Подключение к БД и общие SQL-хелперы |
|
||
|
||
## 8. Документы и уведомления
|
||
|
||
PDF (Счёт на оплату / Акт выполненных работ) генерируются на лету
|
||
(`internal/pdfgen`, кириллица через встроенные шрифты DejaVu Sans),
|
||
структура настраивается блоками в «Шаблонах документов», реквизиты — в
|
||
«Настройках». Уведомления клиенту о смене статуса — Telegram/SMS
|
||
(`clientnotify`), уведомления персоналу о новых заявках — Telegram
|
||
(`notify`/`tgbot`).
|
||
|
||
## 9. Дизайн-система
|
||
|
||
Своя лёгкая дизайн-система, без Tailwind и без готового UI-кита — базовые
|
||
токены в `web/src/index.css`, специфика конкретного компонента — инлайн
|
||
`style={{}}` в самом компоненте (осознанный выбор для проекта такого
|
||
размера, не технический долг).
|
||
|
||
**Шрифты:** заголовки — **Montserrat** (600–800, `letter-spacing: -0.02em` —
|
||
плотный, «инженерный» вид), текст — **Roboto** (300–500). Оба через Google
|
||
Fonts.
|
||
|
||
**Палитра** (светлая тема, единственная — тёмная не реализована):
|
||
|
||
| Токен | Значение | Назначение |
|
||
|---|---|---|
|
||
| `--bg-light` | `#f8fafc` | Фон страницы |
|
||
| `--bg-lighter` | `#ffffff` | Фон карточек/панелей |
|
||
| `--accent-blue` | `#4361ee` | Основной акцент — активный пункт меню, кнопки, ссылки |
|
||
| `--accent-orange` | `#ea580c` | Вторичный акцент (предупреждения/ожидание) |
|
||
| `--accent-green` | `#22c55e` | Успех/готово |
|
||
| `--accent-red` | `#ef4444` | Ошибка/отмена |
|
||
| `--text-main` | `#1e1e2d` | Основной текст |
|
||
| `--text-muted` | `#64748b` | Второстепенный текст |
|
||
| `--border-light` | `#e9ecef` | Границы, разделители |
|
||
|
||
**Статусы заказа** (`orders/statuses.js`) имеют собственную, более широкую
|
||
палитру для наглядности канбан-доски: новая `#f59e0b` (янтарный),
|
||
диагностика `#0ea5e9` (голубой), в ремонте `#8b5cf6` (фиолетовый), ждём
|
||
запчасти `#f97316` (оранжевый), готово `#22c55e` (зелёный), выдано `#64748b`
|
||
(серый), отменено `#ef4444` (красный) — цвет статуса используется как
|
||
акцент на карточке и бейдже, не просто текстовая метка.
|
||
|
||
**Компоненты и приёмы:**
|
||
- `.glass-panel` — базовая карточка/панель: белый фон, тонкая граница
|
||
`border-light`, скругление 16px, мягкая тень (`0 8px 32px rgba(15,23,42,.08)`)
|
||
— лёгкий «стеклянный» эффект без размытия (blur не используется).
|
||
- `.btn-primary` (заливка `accent-blue`, белый текст, radius 8px) /
|
||
`.btn-outline` (белый фон, граница `border-light`, при hover обводка
|
||
синеет) — два уровня действий, primary/secondary.
|
||
- Sidebar фиксированный, 240px, белый, активный пункт — светло-синяя
|
||
подложка (`#eef2ff`) + синяя левая полоса 3px + синий текст/иконка. На
|
||
мобильном (`isMobile`) уезжает за экран и выезжает по `transform:
|
||
translateX`, с крестиком закрытия.
|
||
- Иконки — `lucide-react`, по одной на каждый пункт меню, семантически
|
||
подобранные (гаечный ключ для ремонта, капля для картриджей, коробки для
|
||
склада, кошелёк для кассы и т.д.).
|
||
- Формы/модалки переиспользуют общие стили (`orders/formStyles.js` и
|
||
аналоги) — не единый компонентный кит, а общие объекты стилей на домен.
|
||
|
||
**Характер дизайна:** утилитарный внутренний инструмент для персонала
|
||
(канбан-доска, таблицы, модалки), не публичный маркетинговый сайт —
|
||
приоритет на плотность информации и скорость сканирования глазами (статус
|
||
через цвет, а не только текст), а не на декоративность. Единственная
|
||
по-настоящему «клиентская» поверхность — `/track/:token` и `/book`.
|
||
|
||
## 10. Разработка и деплой
|
||
|
||
Кратко (полная версия — [README.md](README.md)):
|
||
|
||
```bash
|
||
docker network create platform_net # один раз, общая сеть с core/site
|
||
cp .env.example .env # JWT_SECRET должен совпадать с core/.env
|
||
docker compose up -d --build
|
||
curl http://127.0.0.1:18091/health
|
||
|
||
cd web && cp .env.example .env && npm install && npm run dev
|
||
```
|
||
|
||
## 11. Текущий статус и известные ограничения
|
||
|
||
- Полная дорожная карта (фазы 0–10) закрыта, плюс второй слой фич (см.
|
||
раздел 1) — реализован и живьём протестирован.
|
||
- Юрлицо бизнеса ещё не зарегистрировано → нет интеграции с Атол Онлайн
|
||
(фискализация, 54-ФЗ) и с Т-Банк эквайрингом для самого сервисного центра.
|
||
- Kanban обновляется поллингом (20с), не realtime (транспорт для realtime
|
||
уже есть в `internal/realtime`, но не подключён к канбан-доске).
|
||
- Публичный трекинг не показывает сами фото, только факт их добавления.
|
||
- Юнит-тестов почти нет (`core` — 0; `production` — часть пакетов без
|
||
тестов: `auth`, `client`, `coreclient`, `document`, `file`, `order`).
|
||
- Секреты в «Настройках» хранятся plaintext без истории изменений — нельзя
|
||
откатить или увидеть, кто менял банковские реквизиты.
|