diff --git a/README.md b/README.md index bc7fe89..13cd2de 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Construction Delivery Control -React-приложение для управления заказами, доставкой, ролями сотрудников и публичным согласованием доставки с клиентом. +React-приложение для управления доставкой заказов. В текущем контуре есть три внутренние роли и публичная страница клиента: менеджер, логист, водитель и клиент. ## Запуск @@ -16,11 +16,12 @@ npm run dev ## Что уже есть - OTP-вход по email через Supabase Auth. -- Role-based dashboard для менеджера, логиста, водителя и администратора. -- Карточка заказа с историей, чатом и слотами доставки. -- Публичная страница `/delivery/:token` для выбора даты и половины дня доставки. +- Служебный вход `roles@local` для демонстрации ролей менеджера, логиста и водителя. +- Role-based dashboard для менеджера, логиста и водителя. +- Карточка заказа с составом, комментариями и историей. +- Публичная страница `/delivery/:token` для выбора даты, половины дня и просмотра состава заказа. - Supabase SQL-схема, таблицы приглашений и Edge Functions для invitation flow. -- Документация по архитектуре, сценариям и интеграциям. +- Документация по продукту, архитектуре и сценариям. ## Структура diff --git a/docs/product-overview.md b/docs/product-overview.md index d95e961..e63612c 100644 --- a/docs/product-overview.md +++ b/docs/product-overview.md @@ -1,76 +1,64 @@ # SuperSam: Обзор системы -`SuperSam` — это система управления доставкой заказов, которая объединяет внутренние рабочие кабинеты сотрудников и публичную страницу для клиента. Приложение помогает пройти путь от готовности заказа к отгрузке до согласования доставки, назначения исполнителей, фиксации результата и контроля исключений. +`SuperSam` — это система управления доставкой заказов. В текущем scope она состоит из трёх внутренних ролей и одной публичной страницы для клиента: менеджер, логист, водитель и клиент. ## Задачи приложения -- собрать в одном месте информацию по заказам, доставке, истории действий и коммуникациям; -- разделить рабочие зоны по ролям, чтобы каждый сотрудник видел только свой контур задач; -- дать логисту инструмент для запуска и контроля согласования доставки; -- дать клиенту простую ссылку, по которой он может выбрать дату и половину дня доставки; -- сохранить в Supabase историю изменений, статусы и интеграционные события. +- показать менеджеру единый реестр доставочных заказов с поиском и карточкой заказа; +- показать логисту список доставок на сегодня и ближайшие дни с половинами дня; +- показать водителю свои доставки, адрес, состав заказа и базовые статусы; +- дать клиенту публичную ссылку, по которой он выбирает дату и половину дня доставки; +- хранить состояние заказов, приглашений и истории изменений в Supabase. ## Роли ### Менеджер -Менеджер работает с заказами на ранних этапах: -- видит список заказов и карточки клиентов; -- следит за составом заказа и комментариями; -- передаёт заказ дальше по процессу после подтверждения. +- видит список заказов доставки; +- ищет по номеру заказа, клиенту и телефону; +- открывает карточку заказа и смотрит состав, комментарии и историю; +- не работает с созданием заказов и внутренними служебными экранами. ### Логист -Логист отвечает за доставку: -- видит готовые к запуску и проблемные заказы; -- контролирует статусы согласования доставки; -- назначает и корректирует слоты; -- переводит заказ в ручную обработку, если клиент не ответил; -- отслеживает историю и связанные сообщения. +- видит заказы, готовые к доставке; +- смотрит ближайшие даты: сегодня, завтра и послезавтра; +- смотрит половину дня и текущий статус доставки; +- открывает карточку заказа, чтобы свериться с деталями. ### Водитель -Водитель работает только со своими доставками: -- видит назначенные маршруты; -- открывает карточку точки доставки; -- фиксирует ход доставки и итоговый статус. - -### Администратор - -Администратор видит всю систему: -- пользователей и роли; -- общие списки заказов и событий; -- состояние интеграций и служебные данные. +- видит только свои доставки; +- открывает адрес, клиента, состав заказа и комментарии; +- меняет базовый статус доставки по маршруту. ### Клиент -Клиент не входит во внутренний кабинет. Он получает публичную ссылку вида `/delivery/:token` и по ней: -- видит номер заказа; -- выбирает удобную дату; -- выбирает половину дня: `До обеда` или `После обеда`; -- подтверждает выбор. +- получает публичную ссылку вида `/delivery/:token`; +- видит номер заказа и состав заказа; +- выбирает дату и половину дня: `До обеда` или `После обеда`; +- подтверждает выбор без входа во внутренний кабинет. ## Основные сценарии ### Внутренний сценарий -1. Заказ попадает в систему. -2. Менеджер и внутренние сотрудники ведут заказ по этапам. -3. Когда заказ готов к доставке, логист запускает приглашение клиенту. -4. Клиент выбирает слот по публичной ссылке. -5. Система переводит заказ в `Доставка согласована`. -6. Логист и водитель доводят доставку до результата. +1. Заказ появляется в Supabase. +2. Менеджер видит его в реестре и сверяет состав. +3. Логист отслеживает готовность и ближайшее окно доставки. +4. Водитель получает свою доставку и доводит её до результата. ### Сценарий клиента -Клиентская страница работает по token из таблицы `public.delivery_invitations`. Для рабочего показа используется заранее загруженный seed-набор данных. +Клиентская страница работает по token из таблицы `public.delivery_invitations`. После загрузки seed можно открыть ссылку: `/delivery/client-flow-1001` -Эта ссылка должна показывать: +Эта ссылка показывает: - заказ `CD-240031`; +- состав заказа; - четыре варианта слота; - две даты; - две половины дня: `До обеда` и `После обеда`. @@ -83,8 +71,6 @@ ## Что хранится в Supabase -### Основные таблицы - - `public.users` — пользователи и роли; - `public.orders` — заказы и текущие статусы; - `public.order_history` — история изменений; @@ -92,34 +78,24 @@ - `public.delivery_invitations` — публичные invitation token и состояние клиентского flow; - `public.integration_events` — технические и интеграционные события. -### Важные поля для клиентского flow - -- `delivery_invitations.token_hash` — хеш публичного токена; -- `delivery_invitations.state` — состояние приглашения; -- `delivery_invitations.available_slots` — список доступных вариантов для клиента; -- `delivery_invitations.delivery_date` и `delivery_invitations.delivery_time` — выбранный или основной слот; -- `orders.status` — текущий рабочий статус заказа; -- `orders.delivery_agreement_status` — статус согласования доставки. - ## Как подготовить систему к показу 1. Загрузить схему `supabase/schema.sql`. -2. Создать нужных пользователей в `auth.users`. -3. Выполнить `supabase/seed/stage-1-demo.sql`. -4. Убедиться, что Edge Functions развернуты: +2. Выполнить `supabase/seed/stage-1-demo.sql`. +3. Убедиться, что развернуты Edge Functions: - `get-delivery-invitation` - `confirm-delivery-choice` - `create-delivery-invitation` -5. Открыть внутренний кабинет. -6. Открыть клиентскую ссылку `/delivery/client-flow-1001`. +4. Открыть внутренний кабинет и пройти вход под ролью. +5. Открыть клиентскую ссылку `/delivery/client-flow-1001`. ## Что показывать на встрече -- вход во внутренний кабинет; -- список заказов для менеджера, логиста и водителя; -- карточку заказа и статусы; -- клиентскую ссылку с выбором даты и половины дня; -- изменение статуса заказа после подтверждения клиентом. +- вход менеджера, логиста и водителя; +- реестр заказов и карточку заказа; +- список доставок по датам для логиста; +- карточку доставки водителя; +- клиентскую ссылку с выбором даты и половины дня. ## Полезные документы diff --git a/docs/superpowers/plans/2026-04-16-role-focused-delivery-simplification.md b/docs/superpowers/plans/2026-04-16-role-focused-delivery-simplification.md new file mode 100644 index 0000000..4f0bed9 --- /dev/null +++ b/docs/superpowers/plans/2026-04-16-role-focused-delivery-simplification.md @@ -0,0 +1,299 @@ +# Role Focused Delivery Simplification Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Упростить продукт до рабочего delivery-контура из 4 ролей: менеджер, логист, водитель и клиент, убрав лишние разделы, действия и экраны, которые не входят в согласованный scope. + +**Architecture:** Текущий `DashboardPage` работает как широкий универсальный центр управления заказами. В рамках этого плана он сужается до трёх role-specific рабочих зон: менеджер видит только реестр и поиск, логист видит готовность и план доставки по датам/половине дня, водитель видит свои доставки и состав заказов. Клиентский route `/delivery/:token` дополняется составом заказа и остаётся отдельным публичным экраном. + +**Tech Stack:** React 18, Vite, existing UI kit, Supabase-backed orders/invitations, Vitest. + +--- + +## File Structure + +- Modify: `src/pages/DashboardPage.jsx` — убрать лишние секции (`production`, `admin`, `references`, избыточные tabs/orders views) и собрать role-focused рендеринг. +- Modify: `src/hooks/useOrders.js` — при необходимости сузить возвращаемые данные и убрать неиспользуемые callbacks. +- Modify: `src/components/dashboard/RoleWorkspacePanel.jsx` — переписать описания ролей под итоговый scope. +- Modify: `src/components/orders/OrderFilters.jsx` — оставить фильтры, которые реально нужны менеджеру/логисту. +- Modify: `src/components/orders/OrdersTable.jsx` — ориентировать таблицу на заказ доставки, номер, клиента, дату доставки и статус. +- Modify: `src/components/orders/OrderDetailPanel.jsx` — оставить просмотр деталей, состава заказа и базового статуса, без лишних командных функций. +- Delete or stop using: `src/components/orders/OrderEditorPanel.jsx` — создание/редактирование заказов должно уйти из продукта. +- Delete or stop using: `src/components/orders/OrdersKanbanBoard.jsx` +- Delete or stop using: `src/components/orders/OrdersCalendarView.jsx` +- Delete or stop using: `src/components/admin/AuditPanel.jsx` +- Delete or stop using: `src/components/admin/UserOnboardingPanel.jsx` +- Delete or stop using: `src/components/dashboard/ProductionQueuePanel.jsx` +- Modify or replace: `src/components/logistics/BotControlPanel.jsx` — вместо “управления ботами” сделать компактную панель логиста по доставке. +- Modify or replace: `src/components/logistics/DeliverySetDetailPanel.jsx` — если оставить, то только как простой delivery detail без производственного слоя; иначе убрать из сценария. +- Modify: `src/components/driver/DriverDeliveryPlanner.jsx` — упростить до списка доставок без kanban и reorder. +- Modify: `src/components/driver/DriverDeliveryDetail.jsx` — оставить адрес, состав заказа, слот доставки и минимальные действия водителя. +- Modify: `src/pages/ClientDeliveryPage.jsx` — показать клиенту состав заказа и информацию по заказу вместе с выбором даты/половины дня. +- Modify: `src/components/client/DeliveryChoiceFlow.jsx` — упростить copy и добавить summary по заказу. +- Modify: `src/components/client/DeliveryStateNotice.jsx` — оставить только states, которые нужны в реальном клиентском процессе. +- Modify: `src/services/supabase/orderRepository.js` — убедиться, что клиентская и role-specific UI берут состав заказа и доставочные поля из реальных данных. +- Modify: `docs/product-overview.md` — синхронизировать документ с новым узким scope. +- Modify: `README.md` — коротко обновить позиционирование и ссылку на новый scope. + +## Chunk 1: Scope Guard In Tests + +### Task 1: Зафиксировать тестами новый role-focused scope + +**Files:** +- Modify: `src/components/orders/OrderFilters.test.jsx` +- Modify or Create: `src/pages/DashboardPage.test.jsx` +- Modify or Create: role-specific component tests + +- [ ] **Step 1: Add failing tests for manager/logistician/driver visible sections** + +Покрыть: +- менеджер видит только список заказов, поиск и карточку заказа; +- логист видит только список готовых/запланированных доставок; +- водитель видит только свои доставки и состав заказа; +- лишние секции (`production`, `admin`, `references`, `kanban`, `calendar`, `archive`, `history`) не рендерятся. + +- [ ] **Step 2: Run targeted tests to verify current mismatch** + +Run: `npm test -- src/components/orders/OrderFilters.test.jsx src/pages/DashboardPage.test.jsx` +Expected: FAIL на текущем широком dashboard. + +### Task 2: Зафиксировать тестом клиентский заказный summary + +**Files:** +- Modify: `src/components/client/DeliveryChoiceFlow.test.jsx` +- Modify or Create: `src/pages/ClientDeliveryPage.test.js` + +- [ ] **Step 1: Add failing expectation for client order contents** + +Покрыть: +- номер заказа; +- состав заказа; +- выбор даты и половины дня. + +- [ ] **Step 2: Run targeted client tests and confirm failure** + +Run: `npm test -- src/components/client/DeliveryChoiceFlow.test.jsx src/pages/ClientDeliveryPage.test.js` +Expected: FAIL + +## Chunk 2: Simplify Dashboard By Role + +### Task 3: Сузить навигацию и секции dashboard + +**Files:** +- Modify: `src/pages/DashboardPage.jsx` +- Modify: `src/components/dashboard/RoleWorkspacePanel.jsx` + +- [ ] **Step 1: Remove non-scope nav sections** + +Убрать: +- `production` +- `admin` +- `references` + +И свести менеджера/логиста к простым рабочим секциям без обзорного “комбайна”. + +- [ ] **Step 2: Remove extra order tabs** + +Для менеджера оставить только основной список заказов. + +Убрать из пользовательского сценария: +- `calendar` +- `kanban` +- `history` +- `archive` + +- [ ] **Step 3: Re-run dashboard tests** + +Run: `npm test -- src/pages/DashboardPage.test.jsx` +Expected: PASS + +### Task 4: Упростить экран менеджера + +**Files:** +- Modify: `src/components/orders/OrderFilters.jsx` +- Modify: `src/components/orders/OrdersTable.jsx` +- Modify: `src/components/orders/OrderDetailPanel.jsx` + +- [ ] **Step 1: Keep only filters manager really needs** + +Оставить: +- поиск по номеру заказа; +- при необходимости телефон/клиент; +- статус доставки. + +Убрать лишние фильтры и продуктовую лексику, которая больше не используется. + +- [ ] **Step 2: Keep order details read-first** + +В карточке заказа оставить: +- номер; +- клиент; +- адрес; +- состав; +- дата доставки; +- половина дня; +- текущий статус. + +- [ ] **Step 3: Remove create/edit order entry points** + +Убрать из пользовательского flow любые кнопки и панели создания/редактирования заказов. + +- [ ] **Step 4: Re-run manager-facing tests** + +Run: `npm test -- src/components/orders/OrdersTable.test.jsx src/components/orders/OrderDetailPanel.test.jsx src/components/orders/OrderFilters.test.jsx` +Expected: PASS + +### Task 5: Упростить экран логиста + +**Files:** +- Modify: `src/pages/DashboardPage.jsx` +- Modify or Replace: `src/components/logistics/BotControlPanel.jsx` +- Modify or stop using: `src/components/logistics/DeliverySetDetailPanel.jsx` + +- [ ] **Step 1: Replace logistics board with practical delivery list** + +Логист должен видеть: +- заказы, готовые на сегодня; +- доставки, запланированные на завтра/послезавтра; +- половину дня; +- текущий статус. + +- [ ] **Step 2: Keep only logistics actions that are in scope** + +Оставить: +- просмотр заказа; +- фиксацию/изменение даты; +- фиксацию половины дня; +- перевод по нужному delivery status. + +Убрать: +- блок “Логика каналов”; +- широкую bot-control механику; +- лишние delivery-set детали производства. + +- [ ] **Step 3: Re-run logistics-focused tests** + +Run: `npm test -- src/services/orderService.test.js` +Expected: PASS + +### Task 6: Упростить экран водителя + +**Files:** +- Modify: `src/components/driver/DriverDeliveryPlanner.jsx` +- Modify: `src/components/driver/DriverDeliveryDetail.jsx` +- Modify: `src/pages/DashboardPage.jsx` + +- [ ] **Step 1: Remove planner-heavy behaviors** + +Убрать: +- `kanban` view; +- reorder drag-and-drop; +- лишние фильтры, которые не нужны для показа. + +- [ ] **Step 2: Keep delivery essentials only** + +Оставить: +- адрес; +- клиент; +- слот доставки; +- состав заказа; +- базовые статусы водителя. + +- [ ] **Step 3: Re-run driver-facing tests** + +Run: `npm test -- src/services/orderService.test.js` +Expected: PASS + +## Chunk 3: Client Page Completion + +### Task 7: Показать клиенту состав заказа и delivery summary + +**Files:** +- Modify: `src/pages/ClientDeliveryPage.jsx` +- Modify: `src/components/client/DeliveryChoiceFlow.jsx` +- Modify: `src/components/client/DeliveryStateNotice.jsx` +- Modify: `src/services/supabase/orderRepository.js` if extra fields need mapping + +- [ ] **Step 1: Add order items to invitation view model** + +Убедиться, что клиентская страница получает: +- номер заказа; +- имя клиента; +- состав заказа; +- доступные даты; +- половины дня. + +- [ ] **Step 2: Render order composition above slot choice** + +Показать клиенту: +- номер заказа; +- адрес/краткое описание; +- список позиций заказа. + +- [ ] **Step 3: Keep state notices minimal and product-focused** + +Оставить только сообщения для: +- активного выбора; +- уже согласованной доставки; +- передачи логисту; +- доставленного заказа. + +- [ ] **Step 4: Re-run client tests** + +Run: `npm test -- src/components/client/DeliveryChoiceFlow.test.jsx src/components/client/DeliverySlotsPicker.test.jsx src/pages/ClientDeliveryPage.test.js` +Expected: PASS + +## Chunk 4: Documentation Sync + +### Task 8: Синхронизировать документацию с новым scope + +**Files:** +- Modify: `docs/product-overview.md` +- Modify: `README.md` + +- [ ] **Step 1: Update product overview** + +Описать только: +- менеджера; +- логиста; +- водителя; +- клиента. + +- [ ] **Step 2: Remove unsupported features from docs** + +Убрать из описаний: +- производство; +- админку как отдельный пользовательский продукт; +- создание заказов; +- канбан/архив/историю как целевые экраны. + +- [ ] **Step 3: Re-read docs against current UI** + +Проверить, что документация описывает ровно тот интерфейс, который остался после упрощения. + +## Chunk 5: Final Verification + +### Task 9: Прогнать целевую регрессию после упрощения + +**Files:** +- Reference: `src/pages/DashboardPage.jsx` +- Reference: `src/pages/ClientDeliveryPage.jsx` +- Reference: `docs/product-overview.md` + +- [ ] **Step 1: Run focused UI suite** + +Run: `npm test -- src/pages/DashboardPage.test.jsx src/components/orders/OrdersTable.test.jsx src/components/orders/OrderDetailPanel.test.jsx src/components/orders/OrderFilters.test.jsx src/components/client/DeliveryChoiceFlow.test.jsx src/components/client/DeliverySlotsPicker.test.jsx src/pages/ClientDeliveryPage.test.js` +Expected: PASS + +- [ ] **Step 2: Run service regression tests** + +Run: `npm test -- src/services/orderService.test.js src/services/deliveryInvitationApi.test.js src/context/AuthContext.test.js src/components/auth/OtpLoginForm.test.jsx` +Expected: PASS + +- [ ] **Step 3: Manual acceptance check** + +Проверить вручную: +- менеджер видит только список заказов и поиск; +- логист видит готовые и запланированные доставки с датой/половиной дня; +- водитель видит свои доставки и состав заказов; +- клиент видит номер заказа, состав и выбор даты/половины дня. diff --git a/src/components/dashboard/RoleWorkspacePanel.jsx b/src/components/dashboard/RoleWorkspacePanel.jsx index a3f3d2c..4de6daf 100644 --- a/src/components/dashboard/RoleWorkspacePanel.jsx +++ b/src/components/dashboard/RoleWorkspacePanel.jsx @@ -6,29 +6,15 @@ import { Panel } from "../UI/Panel"; const ROLE_MODULES = { manager: [ - "Просмотр импортированных заказов", - "Поиск по клиенту, заказу и статусу", - "Комментарии и эскалации", - ], - production_lead: [ - "Очередь производства", - "Переключение статусов на производстве", - "Контроль готовности к отгрузке", - ], - logistician: [ - "Наборы доставки и слоты", - "Согласование с клиентом и назначение рейса", - "Разбор проблемных доставок и ручная работа", + "Поиск по заказу, клиенту и телефону", + "Просмотр состава и статуса заказа", + "Работа только с доставочным реестром", ], + logistician: ["Готовность заказов на сегодня", "Слоты завтра и послезавтра", "Половины дня и статус доставки"], driver: [ - "Назначенные доставки и маршрут", - "Загрузка, выезд и завершение рейса", - "Фиксация результата доставки", - ], - admin: [ - "Полный доступ к заказам и доставкам", - "Управление пользователями и ролями", - "Логи, ошибки и история действий", + "Назначенные доставки", + "Адрес, состав и слот доставки", + "Быстрые статусы по маршруту", ], }; @@ -44,8 +30,7 @@ export const RoleWorkspacePanel = ({ role, deliverySetBuckets }) => {

{ROLE_LABELS[role]}: рабочая панель

- Интерфейс автоматически адаптируется под роль пользователя после входа по одноразовому - коду. + Интерфейс показывает только то, что нужно для повседневной работы в доставочном контуре.

@@ -82,4 +67,4 @@ export const RoleWorkspacePanel = ({ role, deliverySetBuckets }) => { ) : null} ); -}; \ No newline at end of file +}; diff --git a/src/components/driver/DriverDeliveryDetail.jsx b/src/components/driver/DriverDeliveryDetail.jsx index c8cfde5..cfd5907 100644 --- a/src/components/driver/DriverDeliveryDetail.jsx +++ b/src/components/driver/DriverDeliveryDetail.jsx @@ -1,19 +1,34 @@ import React from "react"; -import { - getAvailableTransitionsByRole, - getOrderStatusComment, - getStatusTone, -} from "../../constants/deliveryWorkflow"; -import { demoUsers } from "../../data/mockAppData"; +import { getAvailableTransitionsByRole, getOrderStatusComment, getStatusTone } from "../../constants/deliveryWorkflow"; import { getDeliveryCity, getDeliveryDay, getDeliveryHalfDay } from "../../services/driverDeliveries"; import { Badge } from "../UI/Badge"; import { Button } from "../UI/Button"; import { Panel } from "../UI/Panel"; -const getUsers = (users) => (Array.isArray(users) && users.length ? users : demoUsers); -const resolveUserName = (users, userId) => getUsers(users).find((user) => user.id === userId)?.name || "Не назначен"; +const splitItem = (item) => { + if (!item) { + return { name: "Позиция", quantity: "" }; + } -export const DriverDeliveryDetail = ({ order, onStatusChange, users }) => { + if (typeof item === "string") { + const [name, quantity] = item.split("|").map((part) => part.trim()); + return { + name: name || item, + quantity: quantity || "", + }; + } + + if (typeof item === "object") { + return { + name: item.name || item.label || "Позиция", + quantity: typeof item.quantity === "number" ? String(item.quantity) : item.quantity || "", + }; + } + + return { name: "Позиция", quantity: "" }; +}; + +export const DriverDeliveryDetail = ({ order, onStatusChange }) => { if (!order) { return null; } @@ -22,26 +37,20 @@ export const DriverDeliveryDetail = ({ order, onStatusChange, users }) => { status: order.status, role: "driver", }); - - const deliverySetKey = order.deliverySetKey; - const deliverySetName = order.deliverySetName; - const sourceOrderNumber = order.sourceOrderNumber; + const orderItems = Array.isArray(order.items) ? order.items.map(splitItem) : []; return (
-

- Доставка -

+

Доставка

{order.customer.address}

{order.orderNumber} · {order.customer.name}

- Точка {order.driverRouteOrder || "\u2014"} {order.status}
@@ -69,41 +78,26 @@ export const DriverDeliveryDetail = ({ order, onStatusChange, users }) => { {getDeliveryDay(order)} · {getDeliveryHalfDay(order)}

-
-

Логист

-

{resolveUserName(users, order.logisticianIds?.[0])}

-
- {sourceOrderNumber ? ( -
-

1С номер

-

{sourceOrderNumber}

-
- ) : null} - {deliverySetKey ? ( -
-

Набор доставки

-

{deliverySetName || deliverySetKey}

-
- ) : null}

Что везти

- {(order.items || []).map((item) => ( + {orderItems.map((item) => (
- {item} + {item.name} + {item.quantity ? {item.quantity} : null}
))}
-

Комментарии для водителя

+

Комментарии для доставки

{order.orderNotes?.[0]?.text || "Дополнительных комментариев нет."} @@ -116,20 +110,22 @@ export const DriverDeliveryDetail = ({ order, onStatusChange, users }) => {
- -

Быстрые действия

-
- {availableTransitions.map((status) => ( - - ))} -
-
+ {availableTransitions.length ? ( + +

Быстрые действия

+
+ {availableTransitions.map((status) => ( + + ))} +
+
+ ) : null}
); }; diff --git a/src/components/driver/DriverDeliveryDetail.test.jsx b/src/components/driver/DriverDeliveryDetail.test.jsx new file mode 100644 index 0000000..fe05e7e --- /dev/null +++ b/src/components/driver/DriverDeliveryDetail.test.jsx @@ -0,0 +1,37 @@ +import React from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; +import { DriverDeliveryDetail } from "./DriverDeliveryDetail"; + +describe("DriverDeliveryDetail", () => { + it("shows the delivery essentials and order contents without source-order noise", () => { + const markup = renderToStaticMarkup( + {}} + />, + ); + + expect(markup).toContain("Симферополь, ул. Ленина, 10"); + expect(markup).toContain("Мария Волкова"); + expect(markup).toContain("Кухонный гарнитур"); + expect(markup).toContain("1 комплект"); + expect(markup).toContain("Фурнитура Blum"); + expect(markup).toContain("12 шт"); + expect(markup).not.toContain("1С номер"); + expect(markup).not.toContain("Набор доставки"); + }); +}); diff --git a/src/components/driver/DriverDeliveryPlanner.jsx b/src/components/driver/DriverDeliveryPlanner.jsx index 1dad2fb..c3282e5 100644 --- a/src/components/driver/DriverDeliveryPlanner.jsx +++ b/src/components/driver/DriverDeliveryPlanner.jsx @@ -1,157 +1,39 @@ import React from "react"; import { getAvailableTransitionsByRole, getStatusTone } from "../../constants/deliveryWorkflow"; -import { - buildDriverKanbanColumns, - filterDriverDeliveries, - getDeliveryCity, - getDeliveryHalfDay, - getDriverCities, - groupDriverDeliveriesByDate, -} from "../../services/driverDeliveries"; +import { groupDriverDeliveriesByDate, getDeliveryCity, getDeliveryHalfDay } from "../../services/driverDeliveries"; import { Badge } from "../UI/Badge"; import { Button } from "../UI/Button"; import { Panel } from "../UI/Panel"; -import { SegmentedTabs } from "../UI/SegmentedTabs"; -import { Select } from "../UI/Select"; -const formatDayLabel = (date) => - new Date(`${date}T12:00:00`).toLocaleDateString("ru-RU", { - day: "numeric", - month: "long", - weekday: "long", - }); - -export const DriverDeliveryPlanner = ({ - orders, - filters, - setFilters, - onOpenOrder, - onStatusChange, - onReorder, -}) => { - const [viewTab, setViewTab] = React.useState("table"); - const [dragOrderId, setDragOrderId] = React.useState(null); - const [dropOrderId, setDropOrderId] = React.useState(null); - const [dropColumnKey, setDropColumnKey] = React.useState(null); - const filteredOrders = React.useMemo(() => filterDriverDeliveries(orders, filters), [filters, orders]); - const groupedOrders = React.useMemo(() => groupDriverDeliveriesByDate(filteredOrders), [filteredOrders]); - const kanbanColumns = React.useMemo(() => buildDriverKanbanColumns(filteredOrders), [filteredOrders]); - const cityOptions = React.useMemo(() => getDriverCities(orders), [orders]); - - const updateFilter = (key, value) => { - setFilters((current) => ({ - ...current, - [key]: value, - })); - }; - - const handleDrop = (group, targetOrderId) => { - if (!dragOrderId || dragOrderId === targetOrderId) { - setDragOrderId(null); - setDropOrderId(null); - return; - } - - const orderedIds = group.items.map((item) => item.id); - const nextIds = orderedIds.filter((id) => id !== dragOrderId); - const dropIndex = nextIds.indexOf(targetOrderId); - - nextIds.splice(dropIndex, 0, dragOrderId); - onReorder(nextIds); - setDragOrderId(null); - setDropOrderId(null); - }; - - const handleKanbanDrop = (column) => { - if (!dragOrderId) { - return; - } - - onStatusChange(dragOrderId, column.dropStatus); - setDragOrderId(null); - setDropColumnKey(null); - }; +export const DriverDeliveryPlanner = ({ orders, onOpenOrder, onStatusChange }) => { + const groupedOrders = React.useMemo(() => groupDriverDeliveriesByDate(orders), [orders]); return ( -
- +
+
-

План доставок

+

Мои доставки

- Отфильтруйте доставки, затем перетаскивайте карточки внутри дня, чтобы определить - последовательность маршрута. + Список доставок с адресом, клиентом, составом заказа и базовыми действиями по статусу.

- {filteredOrders.length} + {orders.length}
- -
- updateFilter("dateFrom", event.target.value)} - /> - updateFilter("dateTo", event.target.value)} - /> - - - -
- -
- -
- -
- {viewTab === "table" ? ( - groupedOrders.length ? ( + {groupedOrders.length ? ( groupedOrders.map((group) => (
-

{formatDayLabel(group.date)}

+

+ {new Date(`${group.date}T12:00:00`).toLocaleDateString("ru-RU", { + day: "numeric", + month: "long", + weekday: "long", + })} +

{group.items.length} {group.items.length === 1 ? "доставка" : "доставки"}

@@ -159,20 +41,7 @@ export const DriverDeliveryPlanner = ({ {group.date}
-
- - - - - - - - - - - - - +
{group.items.map((order) => { const availableTransitions = getAvailableTransitionsByRole({ status: order.status, @@ -180,62 +49,29 @@ export const DriverDeliveryPlanner = ({ }); return ( -
onOpenOrder(order.id)} - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - onOpenOrder(order.id); - } - }} - onDragOver={(event) => { - event.preventDefault(); - setDropOrderId(order.id); - }} - onDragLeave={() => - setDropOrderId((current) => (current === order.id ? null : current)) - } - onDrop={(event) => { - event.preventDefault(); - handleDrop(group, order.id); - }} - onDragStart={() => setDragOrderId(order.id)} - onDragEnd={() => { - setDragOrderId(null); - setDropOrderId(null); - }} - draggable - role="button" - tabIndex={0} + type="button" + className="rounded-[24px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-4 text-left transition hover:bg-[var(--color-accent-soft)]" + onClick={() => onOpenOrder?.(order.id)} > - - - - - - - - + + ); })} - -
ОчередьАдресКлиентОкноСтатусКомментарийДействия
- #{order.driverRouteOrder || "—"} - -
{order.customer.address}
-
{order.orderNumber}
-
-
{order.customer.name}
-
{order.customer.phone}
-
-
{getDeliveryCity(order)}
-
{getDeliveryHalfDay(order)}
-
+
+
+
{order.customer.address}
+
+ {order.orderNumber} · {order.customer.name} +
+
{order.status} -
- {order.orderNotes?.[0]?.text || order.comments?.[0] || "Комментариев нет"} - -
+
+ +
+
{getDeliveryCity(order)}
+
{getDeliveryHalfDay(order)}
+
{order.customer.phone}
+
+ +
{availableTransitions.map((status) => ( ))} -
-
)) - ) : ( - -

Доставки не найдены

-

- Попробуйте изменить дату, город или режим показа. -

-
- ) - ) : kanbanColumns.some((column) => column.items.length) ? ( -
- {kanbanColumns.map((column) => ( - -
-

{column.title}

- {column.items.length} -
-
{ - event.preventDefault(); - setDropColumnKey(column.key); - }} - onDragLeave={() => - setDropColumnKey((current) => (current === column.key ? null : current)) - } - onDrop={() => handleKanbanDrop(column)} - > - {column.items.map((order) => ( -
onOpenOrder(order.id)} - onDragStart={() => setDragOrderId(order.id)} - onDragEnd={() => { - setDragOrderId(null); - setDropColumnKey(null); - }} - draggable - role="button" - tabIndex={0} - > -
{order.customer.address}
-
- {order.orderNumber} · {order.customer.name} -
-
- {getDeliveryHalfDay(order)} · #{order.driverRouteOrder || "—"} -
-
- ))} -
-
- ))} -
) : (

Доставки не найдены

- Попробуйте изменить дату, город или режим показа. + Сейчас у вас нет назначенных доставок.

)} diff --git a/src/components/driver/DriverDeliveryPlanner.test.jsx b/src/components/driver/DriverDeliveryPlanner.test.jsx new file mode 100644 index 0000000..9ad0662 --- /dev/null +++ b/src/components/driver/DriverDeliveryPlanner.test.jsx @@ -0,0 +1,41 @@ +import React from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; +import { DriverDeliveryPlanner } from "./DriverDeliveryPlanner"; + +const orders = [ + { + id: "driver-order-1", + orderNumber: "CD-240031", + status: "К доставке", + scheduledDelivery: "2026-04-16T12:00:00Z", + customer: { + name: "Мария Волкова", + address: "Симферополь, ул. Ленина, 10", + phone: "+7 978 000-12-31", + }, + orderNotes: [{ text: "Подъезд узкий" }], + comments: ["Позвонить за час"], + driverRouteOrder: 1, + }, +]; + +describe("DriverDeliveryPlanner", () => { + it("renders a simple delivery list without kanban or route editing", () => { + const markup = renderToStaticMarkup( + {}} + onStatusChange={() => {}} + />, + ); + + expect(markup).toContain("Мои доставки"); + expect(markup).toContain("CD-240031"); + expect(markup).toContain("Мария Волкова"); + expect(markup).toContain("Симферополь, ул. Ленина, 10"); + expect(markup).not.toContain("Канбан"); + expect(markup).not.toContain("Перетащите"); + expect(markup).not.toContain("Календарь"); + }); +}); diff --git a/src/components/orders/OrderDetailPanel.jsx b/src/components/orders/OrderDetailPanel.jsx index 9bd416c..9198117 100644 --- a/src/components/orders/OrderDetailPanel.jsx +++ b/src/components/orders/OrderDetailPanel.jsx @@ -1,58 +1,37 @@ import React from "react"; -import { ROLE_PERMISSIONS } from "../../constants/roles"; -import { - getDeliveryAgreementComment, - getOrderStatusComment, - getStatusTone, -} from "../../constants/deliveryWorkflow"; +import { getDeliveryAgreementComment, getOrderStatusComment, getStatusTone } from "../../constants/deliveryWorkflow"; import { demoUsers } from "../../data/mockAppData"; -import { getAvailableTransitionsForOrder } from "../../services/orderService"; import { formatDateTime } from "../../utils/formatters"; import { Badge } from "../UI/Badge"; -import { Button } from "../UI/Button"; -import { Input } from "../UI/Input"; import { Panel } from "../UI/Panel"; -import { SegmentedTabs } from "../UI/SegmentedTabs"; -import { Select } from "../UI/Select"; -import { ChatTimeline } from "../chat/ChatTimeline"; const getUsers = (users) => (Array.isArray(users) && users.length ? users : demoUsers); const resolveUserName = (users, userId) => getUsers(users).find((user) => user.id === userId)?.name || "Не назначен"; + const splitItem = (item) => { - const [name, quantity] = item.split("|").map((part) => part.trim()); - return { - name: name || item, - quantity: quantity || "1", - }; + if (!item) { + return { name: "Позиция", quantity: "" }; + } + + if (typeof item === "string") { + const [name, quantity] = item.split("|").map((part) => part.trim()); + return { + name: name || item, + quantity: quantity || "", + }; + } + + if (typeof item === "object") { + return { + name: item.name || item.label || "Позиция", + quantity: typeof item.quantity === "number" ? String(item.quantity) : item.quantity || "", + }; + } + + return { name: "Позиция", quantity: "" }; }; -export const OrderDetailPanel = ({ - order, - currentUser, - onStatusChange, - onAssignDriver, - onClientMessage, - onInternalMessage, - onOrderNote, - users, -}) => { - const [nextStatus, setNextStatus] = React.useState(order?.status || "Новый"); - const [selectedDriverId, setSelectedDriverId] = React.useState(order?.assignedDriverId || ""); - const [clientReply, setClientReply] = React.useState("Подтверждаю доставку"); - const [chatQuery, setChatQuery] = React.useState(""); - const [activeTab, setActiveTab] = React.useState("overview"); - const [teamReply, setTeamReply] = React.useState("Новый комментарий для команды"); - const [noteReply, setNoteReply] = React.useState("Новая заметка по заказу"); - - React.useEffect(() => { - setNextStatus(order?.status || "Новый"); - setSelectedDriverId(order?.assignedDriverId || ""); - setChatQuery(""); - setActiveTab("overview"); - setTeamReply("Новый комментарий для команды"); - setNoteReply("Новая заметка по заказу"); - }, [order]); - +export const OrderDetailPanel = ({ order, users }) => { if (!order) { return ( @@ -61,33 +40,15 @@ export const OrderDetailPanel = ({ ); } - const filteredMessages = order.chatMessages.filter((message) => - [message.text, message.channel, message.sender] - .join(" ") - .toLowerCase() - .includes(chatQuery.toLowerCase()), - ); - const detailTabs = [ - { key: "overview", label: "Карточка" }, - { key: "history", label: "История" }, - { key: "chat", label: "Чат с клиентом" }, - { key: "team", label: "Команда" }, - ]; - const availableTransitions = getAvailableTransitionsForOrder({ - order, - role: currentUser.role, - }); - const canAssignDriver = currentUser.role === "logistician" || currentUser.role === "admin"; - const drivers = getUsers(users).filter((user) => user.role === "driver"); + const orderItems = Array.isArray(order.items) ? order.items.map(splitItem) : []; + const orderHistory = Array.isArray(order.history) ? order.history : []; return (
-

- Карточка заказа -

+

Карточка заказа

{order.orderNumber}

{order.customer.name} · {order.customer.address} @@ -96,6 +57,10 @@ export const OrderDetailPanel = ({ {order.status}

+

+ {getOrderStatusComment(order.status)} +

+

Менеджер

@@ -117,299 +82,114 @@ export const OrderDetailPanel = ({

План доставки

{formatDateTime(order.scheduledDelivery)}

+
+

Канал связи

+

{order.customer.messenger}

+
+
+

Согласование доставки

+

{order.deliveryAgreementStatus}

+
- - - - {activeTab === "overview" ? ( -
-
- -
- Данные клиента - {order.status} -
-

- {getOrderStatusComment(order.status)} -

-
-
-

Клиент

-

{order.customer.name}

-
-
-

Телефон

-

{order.customer.phone}

-
-
-

Адрес

-

{order.customer.address}

-
-
-

Канал связи

-

{order.customer.messenger}

-
-
-

Дата заказа

-

{formatDateTime(order.createdAt)}

-
-
-

План доставки

-

{formatDateTime(order.scheduledDelivery)}

-
-
-
- - - Состав заказа -
- {(order.items || []).map((item) => { - const parsedItem = splitItem(item); - return ( -
- {parsedItem.name} - {parsedItem.quantity} -
- ); - })} -
-
-
- -
- -
- Управление заказом -

- Можно изменить статус заказа и оставить пояснения по нему. -

-
- -
-
- Согласование доставки - {order.deliveryAgreementStatus} -
-

- {getDeliveryAgreementComment(order.deliveryAgreementStatus)} -

-
- -
- - -
- - {canAssignDriver ? ( -
-
-
Назначение водителя
-

- Выберите водителя для передачи заказа в этап доставки. -

-
- - -
- ) : null} - -
- Для вашей роли доступны типовые действия: -
-
- {ROLE_PERMISSIONS[currentUser.role].map((permission) => ( - {permission} - ))} -
-
- - - Комментарии и заметки -
- {(order.orderNotes || []).map((note) => ( -
-
- {note.authorName} - - {formatDateTime(note.createdAt)} - -
-

{note.text}

-
- ))} -
-