From 5dcfa80940eb453476b612605c00428cf4a77fcc Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 27 Apr 2026 20:37:21 +0300 Subject: [PATCH] Polish demo UI and delivery filters --- README.md | 3 +- docs/customer-demo-readiness-audit-plan.md | 299 ++++++++++++++++++ docs/scenarios.md | 16 +- eslint.config.js | 2 +- public/manifest.webmanifest | 4 +- src/components/admin/UserDirectoryPanel.jsx | 5 +- src/components/admin/UserOnboardingPanel.jsx | 39 +-- src/components/auth/OtpLoginForm.jsx | 68 +--- src/components/auth/OtpLoginForm.test.jsx | 23 +- src/components/chat/ChatTimeline.jsx | 3 +- .../dashboard/ProductGuidePanel.jsx | 87 +++++ .../dashboard/ProductGuidePanel.test.jsx | 17 + .../dashboard/RoleWorkspacePanel.jsx | 70 ---- src/components/logistics/BotControlPanel.jsx | 10 +- .../logistics/DeliverySetDetailPanel.jsx | 8 +- .../logistics/LogisticsReadinessBoard.jsx | 90 +++--- .../orders/OrderDetailPanel.test.jsx | 2 +- src/components/orders/OrderEditorPanel.jsx | 7 +- src/components/orders/OrderFilters.jsx | 156 +++++++-- src/components/orders/OrderFilters.test.jsx | 36 ++- src/components/orders/OrdersTable.jsx | 23 +- src/components/orders/OrdersTable.test.jsx | 12 +- src/constants/orderStatuses.js | 12 + src/context/AuthContext.jsx | 22 +- src/context/AuthContext.test.js | 25 +- src/data/mockAppData.js | 48 +-- src/layouts/AppShell.jsx | 23 +- src/layouts/AppShell.test.jsx | 40 +++ src/pages/DashboardPage.jsx | 205 +++++------- src/pages/DashboardPage.test.jsx | 46 ++- src/pages/LoginPage.jsx | 49 +-- src/services/deliveryInvitationApi.js | 6 +- src/services/orderService.test.js | 2 +- src/services/supabase/orderRepository.js | 11 +- src/services/supabase/orderRepository.test.js | 2 +- src/styles/designSystem.css | 2 + 36 files changed, 917 insertions(+), 556 deletions(-) create mode 100644 docs/customer-demo-readiness-audit-plan.md create mode 100644 src/components/dashboard/ProductGuidePanel.jsx create mode 100644 src/components/dashboard/ProductGuidePanel.test.jsx delete mode 100644 src/components/dashboard/RoleWorkspacePanel.jsx diff --git a/README.md b/README.md index 13cd2de..6e3245c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,6 @@ npm run dev ## Что уже есть - OTP-вход по email через Supabase Auth. -- Служебный вход `roles@local` для демонстрации ролей менеджера, логиста и водителя. - Role-based dashboard для менеджера, логиста и водителя. - Карточка заказа с составом, комментариями и историей. - Публичная страница `/delivery/:token` для выбора даты, половины дня и просмотра состава заказа. @@ -28,7 +27,7 @@ npm run dev - `src/` — интерфейс и клиентская логика. - `supabase/schema.sql` — структура БД, роли, индексы, RLS, триггеры. - `supabase/functions/` — Edge Functions для приглашений, статусов и чат-коммуникаций. -- `supabase/seed/stage-1-demo.sql` — рабочий набор seed-данных для показа. +- `supabase/seed/stage-1-demo.sql` — набор seed-данных для показа заказчику. - `docs/architecture.md` — архитектура фронтенда и модулей. - `docs/product-overview.md` — общий обзор продукта, ролей и сценариев. - `docs/scenarios.md` — сценарии жизненного цикла заказа. diff --git a/docs/customer-demo-readiness-audit-plan.md b/docs/customer-demo-readiness-audit-plan.md new file mode 100644 index 0000000..4d6f883 --- /dev/null +++ b/docs/customer-demo-readiness-audit-plan.md @@ -0,0 +1,299 @@ +# План подготовки приложения к демонстрации заказчику + +Дата проверки: 2026-04-27 +Цель: привести приложение к виду полноценного рабочего продукта перед показом заказчику, без упоминаний тестового, демо, локального, боевого или служебного режима в пользовательском интерфейсе. + +## Краткий вывод + +Приложение частично соответствует последнему продуктовому заданию: есть OTP-вход, роль-ориентированный кабинет, публичная клиентская страница выбора доставки, модель delivery sets, Supabase schema и Edge Functions. Тесты проходят, production build собирается. + +Главная проблема перед показом: в UI и публичных метаданных остались служебные и демонстрационные следы. Также логистический кабинет сейчас не использует готовую доску `LogisticsReadinessBoard`, хотя она описана в ТЗ и уже реализована как отдельный компонент. Из-за этого сценарий логиста в приложении не совпадает с документированным demo/customer flow. + +## Что проверено + +- `docs/product-overview.md` +- `docs/scenarios.md` +- `docs/superpowers/specs/2026-04-13-1c-delivery-ui-design.md` +- `docs/superpowers/plans/2026-04-13-1c-delivery-frontend-supabase.md` +- `src/pages/LoginPage.jsx` +- `src/components/auth/OtpLoginForm.jsx` +- `src/context/AuthContext.jsx` +- `src/pages/DashboardPage.jsx` +- `src/hooks/useOrders.js` +- `src/components/dashboard/RoleWorkspacePanel.jsx` +- `src/components/logistics/LogisticsReadinessBoard.jsx` +- `src/components/logistics/DeliverySetDetailPanel.jsx` +- `src/pages/ClientDeliveryPage.jsx` +- `src/components/client/DeliveryChoiceFlow.jsx` +- `src/components/client/DeliverySlotsPicker.jsx` +- `src/components/driver/DriverDeliveryPlanner.jsx` +- `src/components/driver/DriverDeliveryDetail.jsx` +- `public/manifest.webmanifest` +- `README.md` + +## Проверка команд + +```bash +npm test +``` + +Результат: проходит, 71 test file, 305 tests. + +```bash +npm run build +``` + +Результат: проходит, Vite production build собран. + +```bash +npm run lint +``` + +Результат: падает. Основные проблемы: + +- `src/pages/DashboardPage.jsx`: `React.useMemo` вызывается после условного `return`, нарушение `react-hooks/rules-of-hooks`. +- `src/services/deliveryInvitationApi.js`: неиспользуемые переменные `error` в `catch`. +- `.worktrees/role-focus-dashboard-slice/...`: ESLint также проверяет рабочие worktree-копии и находит там ошибки. Нужно либо исключить `.worktrees` из lint, либо привести эти worktree в порядок/удалить их, если они не нужны. + +## Соответствие ТЗ + +| Блок | Ожидание по ТЗ | Текущее состояние | Статус | +| --- | --- | --- | --- | +| Вход оператора | Email + одноразовый код, без служебных подсказок | Есть OTP, но видны служебный/локальный вход, выбор роли, `roles@local`, `000000`, "Рабочий режим" | Не соответствует для показа | +| Роли | Менеджер, логист, водитель, админ в рабочем контуре | Роли есть, но сохраняются legacy-следы `production_lead` и производственный контур | Частично | +| Логист | `LogisticsReadinessBoard` с наборами доставки | Компонент есть, но `DashboardPage` показывает старые колонки "Сегодня/Завтра/Послезавтра" по отдельным заказам | Не соответствует | +| Delivery sets | Набор доставки как основная единица | Хелперы и компоненты есть, но основной логистический экран их не использует | Частично | +| Клиентская ссылка | `/delivery/:token`, выбор даты и половины дня | Реализовано, flow выглядит близко к ТЗ | В целом соответствует | +| Водитель | Назначенные доставки, адрес, состав, быстрые статусы | Реализовано | В целом соответствует | +| Supabase | Live data, invitations, slots, history | Схема и API есть, но UI показывает "Данные загружены из Supabase, живой контур активен" | Технически есть, copy требует полировки | +| Публичные тексты | Не должно быть "демо", "тест", "локальный", "боевой", "рабочий режим" | Такие тексты найдены в login UI, manifest, README/scenarios | Не соответствует | + +## Найденные замечания + +### P0. Убрать служебный вход и режимные надписи из экрана входа + +Файлы: + +- `src/components/auth/OtpLoginForm.jsx` +- `src/pages/LoginPage.jsx` +- `src/context/AuthContext.jsx` +- `src/components/auth/OtpLoginForm.test.jsx` +- `src/context/AuthContext.test.js` + +Что сейчас видно пользователю: + +- "Для быстрого доступа к рабочим кабинетам можно использовать служебный адрес и выбрать роль вручную." +- "Служебный вход" +- "Локальный вход" +- "Роль для быстрого входа" +- "Войти без кода" +- "Локальный вход использует единый адрес и код 000000." +- "Локальный режим позволяет открыть интерфейс..." +- "Рабочий режим: код отправляется на email..." + +Что нужно сделать: + +- Оставить только обычный сценарий: email -> отправка кода -> ввод кода -> вход. +- Убрать из UI выбор роли на входе. +- Убрать публичное поведение `roles@local`. +- Убрать публичный текст про `000000`. +- Сохранить локальный fallback только как dev-only механизм, если он ещё нужен разработке, но не показывать его в интерфейсе. +- Переписать intro copy: "Введите email, мы отправим одноразовый код для входа." +- После отправки кода оставить нейтральное сообщение: "Код отправлен на указанную почту. Проверьте входящие и папку Спам." +- Обновить тесты, чтобы они проверяли отсутствие служебного/локального/рабочего режима. + +Критерий готовности: + +- На `/login` нет слов: `служебный`, `локальный`, `рабочий режим`, `демо`, `test`, `roles@local`, `local@local`, `000000`, `без кода`. + +### P0. Подключить реальный логистический экран delivery sets + +Файлы: + +- `src/pages/DashboardPage.jsx` +- `src/hooks/useOrders.js` +- `src/components/dashboard/RoleWorkspacePanel.jsx` +- `src/components/logistics/LogisticsReadinessBoard.jsx` +- `src/components/logistics/DeliverySetDetailPanel.jsx` +- `src/pages/DashboardPage.test.jsx` + +Что сейчас не совпадает: + +- ТЗ и `docs/scenarios.md` обещают логисту `LogisticsReadinessBoard`. +- В `DashboardPage.jsx` компонент импортирован не используется. +- Логист видит старый экран с колонками "Сегодня", "Завтра", "Послезавтра" по отдельным заказам. + +Что нужно сделать: + +- Передать `deliverySetBuckets` из `useOrders()` в `RoleWorkspacePanel`. +- Заменить старый `renderLogisticsWorkspace` на `LogisticsReadinessBoard`. +- При клике по набору открывать `DeliverySetDetailPanel`. +- Сохранить понятную карточку заказа там, где она нужна менеджеру. +- Добавить/обновить тест на то, что логист видит "Наборы доставки", "На подходе", "Готово к запуску", "Ожидает клиента", "Нужна ручная работа", "Согласовано", "Завершено". + +Критерий готовности: + +- Логистический сценарий в приложении совпадает с `docs/scenarios.md`. +- Логист работает с наборами доставки, а не с отдельными заказами как главной единицей. + +### P1. Убрать технические баннеры Supabase/live contour из UI + +Файлы: + +- `src/pages/DashboardPage.jsx` +- `src/hooks/useOrders.js` + +Что сейчас видно: + +- "Загружаем данные из Supabase..." +- "Данные загружены из Supabase, живой контур активен." +- Ошибки могут показывать "Supabase" как техническую деталь. + +Что нужно сделать: + +- Заменить на пользовательские формулировки: + - "Загружаем данные..." + - Баннер успешной загрузки убрать полностью. + - Ошибки показывать как "Не удалось загрузить данные. Обратитесь к администратору." +- Технические детали Supabase оставлять только в логах. + +Критерий готовности: + +- В рабочем UI нет слова `Supabase`, кроме внутренних логов/документации для команды. + +### P1. Очистить публичные метаданные и demo wording + +Файлы: + +- `public/manifest.webmanifest` +- `README.md` +- `docs/scenarios.md` +- `docs/product-overview.md` + +Что сейчас найдено: + +- `public/manifest.webmanifest`: "PWA-демо панели заказов и доставки..." +- `README.md`: "Служебный вход `roles@local` для демонстрации ролей..." +- `docs/scenarios.md`: "Demo-скрипт для первого платного milestone" + +Что нужно сделать: + +- В manifest заменить описание на рабочее: "Панель управления доставкой заказов с доступом к кабинетам логиста, водителя и менеджера." +- В README убрать служебный вход из публичного списка "Что уже есть". +- В `docs/scenarios.md` переименовать demo-скрипт в "Сценарий показа заказчику". +- Убрать из пользовательской/презентационной документации `roles@local`, `demo`, `тестовый`, `локальный`, если это не инструкция строго для разработчика. + +Критерий готовности: + +- Поиск по публичным файлам не находит демонстрационные маркеры в пользовательском контексте. + +### P1. Привести lint в зелёное состояние + +Файлы: + +- `src/pages/DashboardPage.jsx` +- `src/services/deliveryInvitationApi.js` +- `eslint.config.js` или `.eslintignore`/аналогичная настройка +- `.worktrees/`, если они остаются в репозитории/рабочей папке + +Что нужно сделать: + +- Перенести `useMemo` в `DashboardPage.jsx` выше раннего `return`, чтобы hooks всегда вызывались в одном порядке. +- В `deliveryInvitationApi.js` заменить `catch (error)` на `catch` там, где переменная не используется. +- Исключить `.worktrees/**` из lint, если эти директории не являются частью основного приложения. +- Повторно запустить `npm run lint`. + +Критерий готовности: + +- `npm run lint` проходит без ошибок. + +### P2. Убрать устаревшие производственные экраны из маршрута показа + +Файлы: + +- `src/constants/roles.js` +- `src/constants/deliveryWorkflow.js` +- `src/components/dashboard/ProductionQueuePanel.jsx` +- `src/components/orders/OrderEditorPanel.jsx` +- `src/services/orderService.js` +- `src/data/mockAppData.js` + +Что сейчас может сбивать: + +- `production_lead`, "Начальник производства", "Очередь производства". +- `OrderEditorPanel` всё ещё выглядит как форма ручного создания/редактирования заказа. +- `createOrder` и уведомление "Новый заказ" остаются в hook, хотя ТЗ говорит, что заказы приходят из 1С. + +Что нужно сделать: + +- Проверить, доступны ли эти экраны из текущего UI. Если недоступны, оставить как legacy code с задачей на последующую чистку. +- Если доступны, убрать их из демонстрационного маршрута. +- Для менеджера оставить только просмотр реестра, поиск и карточку заказа. +- Не показывать создание заказа как пользовательскую возможность. + +Критерий готовности: + +- На показе нет сценария ручного создания заказа в web app. +- Заказ в интерфейсе воспринимается как импортированный из 1С. + +### P2. Финальная визуальная проверка всех ролей + +Файлы: + +- UI-компоненты по факту найденных визуальных замечаний. + +Что нужно сделать: + +- Открыть `/login`. +- Проверить вход оператора. +- Проверить кабинет менеджера. +- Проверить кабинет логиста. +- Проверить кабинет водителя. +- Проверить `/delivery/client-flow-1001`. +- Проверить мобильную ширину для `/login`, `/dashboard`, `/delivery/client-flow-1001`. + +Чеклист: + +- Нет служебных/локальных/demo/test/боевых/рабочих режимов. +- Нет технических баннеров про Supabase/live contour. +- Тексты звучат как рабочий продукт, а не как стенд. +- Логист видит delivery sets. +- Клиентская страница понятна без объяснений команды. +- Водительский экран показывает только назначенные доставки и действия маршрута. + +## Рекомендуемый порядок работ + +1. Исправить экран входа и auth-copy. +2. Подключить `LogisticsReadinessBoard` в `DashboardPage`. +3. Убрать технические баннеры и режимные тексты из dashboard. +4. Очистить manifest/README/scenarios от demo wording. +5. Починить lint. +6. Пройти ручной smoke test по ролям. +7. Подготовить короткий сценарий показа заказчику. + +## Команды финальной проверки + +```bash +npm test +npm run lint +npm run build +rg -n "(demo|демо|test|тест|боев|рабочий режим|локальный|служебный|roles@local|local@local|000000|без кода|живой контур|Supabase)" src public README.md docs/product-overview.md docs/scenarios.md -S +``` + +Ожидание: + +- `npm test` проходит. +- `npm run lint` проходит. +- `npm run build` проходит. +- `rg` не находит запрещённые слова в пользовательском UI и публичных материалах. Допустимы только внутренние тесты, dev-only комментарии и техническая документация, не используемая в демонстрации. + +## Definition of Done + +- Экран входа выглядит как production-ready вход по email-коду. +- Все режимные и служебные подсказки убраны из UI. +- Логистический кабинет работает по delivery sets. +- Клиентская ссылка работает и не содержит технических пояснений. +- Водительский кабинет показывает назначенные доставки и статусы маршрута. +- Публичные метаданные приложения не называют продукт демо. +- Тесты, lint и build зелёные. +- Есть отдельный сценарий показа заказчику без внутренних технических деталей. diff --git a/docs/scenarios.md b/docs/scenarios.md index b0e8b73..e6d5160 100644 --- a/docs/scenarios.md +++ b/docs/scenarios.md @@ -10,14 +10,14 @@ ## 2. Логист открывает рабочее пространство доставки 1. Логист видит **LogisticsReadinessBoard** — доску с наборами доставки, сгруппированными по статусам: - - **На подходе**: не все заказы набора приняты ОТК. + - **На подходе**: не все заказы набора прошли контроль качества. - **Готово к запуску**: все заказы приняты, можно запускать доставку. - **Ожидает клиента**: отправлено приглашение, ждём ответа. - **Нужна ручная работа**: передано логисту, платное хранение, проблема. - **Согласовано**: клиент подтвердил слот. - **Завершено**: все заказы доставлены. 2. Клик по набору открывает **DeliverySetDetailPanel** с: - - Перечнем заказов набора, их 1С-номерами и шагами производства (раскрой, склейка, криволинейные, приёмка ОТК, отгрузка). + - Перечнем заказов набора, их 1С-номерами и шагами производства (раскрой, склейка, криволинейные, контроль качества, отгрузка). - Телефоном и email клиента, городом, связанными счетами. - Текущим статусом слота. 3. Логист может запустить приглашение, назначить водителя или перейти к ручной обработке. @@ -55,16 +55,16 @@ 2. После закрытия всех заказов набора он переходит в «Завершено». 3. В истории появляется финальная запись, а чат закрывается для активных действий. -## Demo-скрипт для первого платного milestone +## Сценарий показа заказчику -1. Зайти под логистом (email: `mk7029953@yandex.ru`). -2. На дашборде увидеть LogisticsReadinessBoard с наборами: +1. Зайти под логистом. +2. На дашборде увидеть `LogisticsReadinessBoard` с наборами: - Волкова М.А. — «На подходе» (кухня готова, столешница ещё в производстве). - - Савин А.П. — «Готово к запуску» (все заказы приняты ОТК). + - Савин А.П. — «Готово к запуску» (все заказы прошли контроль качества). - Тарасова Е.И. — «Ожидает клиента» (приглашение отправлено). - Фролова И.Д. — «Нужна ручная работа» (платное хранение). - Орлова Н.С. — «Завершено». 3. Кликнуть по набору Савина — увидеть source-поля, production-шаги, готовность к запуску. -4. Перейти на публичную страницу приглашения — увидеть DeliverySlotsPicker с выбором даты и половины дня. +4. Перейти на публичную страницу приглашения — увидеть `DeliverySlotsPicker` с выбором даты и половины дня. 5. Зайти под водителем — увидеть назначенные доставки с адресами и быстрыми действиями. -6. Зайти под несуществующим email — увидеть «Email не найден в системе. Обратитесь к администратору.» \ No newline at end of file +6. Зайти под несуществующим email — увидеть «Email не найден в системе. Обратитесь к администратору.» diff --git a/eslint.config.js b/eslint.config.js index dd2f86d..054a7d9 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,7 +5,7 @@ import reactHooks from "eslint-plugin-react-hooks"; export default [ { - ignores: ["dist/**", "node_modules/**"], + ignores: ["dist/**", "node_modules/**", ".worktrees/**"], }, js.configs.recommended, { diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest index b8b7166..107202f 100644 --- a/public/manifest.webmanifest +++ b/public/manifest.webmanifest @@ -1,7 +1,7 @@ { - "name": "Школьное питание Demo", + "name": "Школьное питание", "short_name": "Школьное питание", - "description": "PWA-демо панели заказов и доставки с офлайн-доступом после первого запуска.", + "description": "Панель управления доставкой заказов с доступом к кабинетам логиста, водителя и менеджера.", "start_url": "/dashboard", "scope": "/", "display": "standalone", diff --git a/src/components/admin/UserDirectoryPanel.jsx b/src/components/admin/UserDirectoryPanel.jsx index 0a3ee24..7661027 100644 --- a/src/components/admin/UserDirectoryPanel.jsx +++ b/src/components/admin/UserDirectoryPanel.jsx @@ -47,9 +47,8 @@ export const UserDirectoryPanel = ({ currentUser, users }) => {
- Каналы: Телеграм {user.botBindings?.telegram || "не привязан"} · ВКонтакте{" "} - {user.botBindings?.vk || "не привязан"} · Макс{" "} - {user.botBindings?.messengerMax || "не привязан"} + Контакты для уведомлений: {user.phone || "телефон не указан"} ·{" "} + {user.email || "email не указан"}
))} diff --git a/src/components/admin/UserOnboardingPanel.jsx b/src/components/admin/UserOnboardingPanel.jsx index dda5adf..9c84679 100644 --- a/src/components/admin/UserOnboardingPanel.jsx +++ b/src/components/admin/UserOnboardingPanel.jsx @@ -10,10 +10,7 @@ const defaultState = { email: "", phone: "", role: "manager", - botLinkMode: "phone", - telegram: "", - vk: "", - messengerMax: "", + notifyBy: "phone", }; export const UserOnboardingPanel = () => { @@ -45,12 +42,10 @@ export const UserOnboardingPanel = () => { return (
-

Добавление пользователя и привязка к ботам

+

Добавление пользователя

- Практичный сценарий: сотрудник добавляется по электронной почте и телефону, получает роль, после - чего к нему привязываются идентификаторы каналов. Для ботов лучше иметь два варианта - привязки: по номеру телефона и по имени пользователя или идентификатору в конкретном - мессенджере. + Сотрудник добавляется по электронной почте и телефону, получает роль и может получать + рабочие уведомления выбранным способом.

@@ -78,27 +73,12 @@ export const UserOnboardingPanel = () => { ))} - updateField("telegram", event.target.value)} - /> - updateField("vk", event.target.value)} - /> - updateField("messengerMax", event.target.value)} - /> @@ -114,8 +94,7 @@ export const UserOnboardingPanel = () => { {draft.email} · {draft.phone} · {ROLE_LABELS[draft.role]}
- Способ привязки:{" "} - {draft.botLinkMode === "phone" ? "по телефону" : "по аккаунту или идентификатору"} + Уведомления: {draft.notifyBy === "phone" ? "SMS" : "email"}
))} diff --git a/src/components/auth/OtpLoginForm.jsx b/src/components/auth/OtpLoginForm.jsx index 8b105cb..767e342 100644 --- a/src/components/auth/OtpLoginForm.jsx +++ b/src/components/auth/OtpLoginForm.jsx @@ -1,30 +1,19 @@ import React from "react"; -import { ROLE_LABELS } from "../../constants/roles"; import { Button } from "../UI/Button"; import { Input } from "../UI/Input"; import { Panel } from "../UI/Panel"; -import { Select } from "../UI/Select"; export const OtpLoginForm = ({ email, setEmail, - roleHint, - setRoleHint, otp, setOtp, isOtpSent, isLoading, - isDemoMode, - isRoleSwitchMode, onRequestOtp, onVerifyOtp, error, }) => { - const isServiceAccessMode = - Boolean(isRoleSwitchMode) || String(email || "").trim().toLowerCase() === "roles@local"; - const showsLocalRolePicker = !isOtpSent && (isServiceAccessMode || isDemoMode); - const submitLabel = isServiceAccessMode ? "Войти без кода" : "Отправить код"; - return (
@@ -34,10 +23,11 @@ export const OtpLoginForm = ({

Вход по email и коду

-

- Введите email, и код придет на почту. Для быстрого доступа к рабочим кабинетам можно - использовать служебный адрес и выбрать роль вручную. -

+ {!isOtpSent ? ( +

+ Введите email, и мы отправим одноразовый код для входа. +

+ ) : null}
@@ -49,41 +39,11 @@ export const OtpLoginForm = ({ id="email" value={email} onChange={(event) => setEmail(event.target.value)} - placeholder="Введите email" + placeholder="name@company.ru" type="email" - disabled={isDemoMode} />
- {showsLocalRolePicker ? ( -
-

- {isServiceAccessMode ? "Служебный вход" : "Локальный вход"} -

-
- - -
-

- {isServiceAccessMode - ? "Выберите кабинет и войдите сразу без подтверждения кода." - : "Локальный вход использует единый адрес и код 000000."} -

-
- ) : null} - {isOtpSent && (
-
- {isServiceAccessMode - ? "Служебный вход открывает кабинеты менеджера, логиста и водителя без ожидания кода." - : isDemoMode - ? "Локальный режим позволяет открыть интерфейс и проверить структуру кабинетов." - : "Рабочий режим: код отправляется на email, а доступ определяется учетной записью в системе."} -
); }; diff --git a/src/components/auth/OtpLoginForm.test.jsx b/src/components/auth/OtpLoginForm.test.jsx index 19ea6f5..ab1c22f 100644 --- a/src/components/auth/OtpLoginForm.test.jsx +++ b/src/components/auth/OtpLoginForm.test.jsx @@ -6,13 +6,10 @@ import { OtpLoginForm } from "./OtpLoginForm"; const baseProps = { email: "skylanguage@yandex.ru", setEmail: () => {}, - roleHint: "manager", - setRoleHint: () => {}, otp: "", setOtp: () => {}, isOtpSent: false, isLoading: false, - isDemoMode: false, onRequestOtp: () => {}, onVerifyOtp: () => {}, error: "", @@ -23,20 +20,10 @@ describe("OtpLoginForm", () => { const markup = renderToStaticMarkup().toLowerCase(); expect(markup).toContain("введите email"); - expect(markup).toContain("доступ определяется учетной записью"); - expect(markup).not.toContain("роль для быстрого входа"); - expect(markup).not.toContain("демо-режим"); - }); - - it("shows role selection for the special access email without demo wording", () => { - const markup = renderToStaticMarkup( - , - ).toLowerCase(); - - expect(markup).toContain("служебный вход"); - expect(markup).toContain("роль для быстрого входа"); - expect(markup).toContain("войти без кода"); - expect(markup).not.toContain("демо-режим"); + expect(markup).toContain("одноразовый код"); + expect(markup).not.toContain("служебный вход"); + expect(markup).not.toContain("локальный вход"); + expect(markup).not.toContain("рабочий режим"); }); it("tells operators to check inbox and spam after OTP is sent", () => { @@ -46,6 +33,8 @@ describe("OtpLoginForm", () => { expect(markup).toContain("входящие"); expect(markup).toContain("спам"); + expect(markup).not.toContain("мы отправим одноразовый код"); + expect(markup).not.toContain("рабочие кабинеты"); }); it("shows unknown-email admin-help message when error matches", () => { diff --git a/src/components/chat/ChatTimeline.jsx b/src/components/chat/ChatTimeline.jsx index 86bfe51..0c24f43 100644 --- a/src/components/chat/ChatTimeline.jsx +++ b/src/components/chat/ChatTimeline.jsx @@ -6,8 +6,7 @@ export const ChatTimeline = ({ messages }) => { if (!messages.length) { return (
- Пока нет сообщений. Здесь появится история переписки с клиентом из ВКонтакте, Телеграма - и Макса. + Пока нет сообщений. Здесь появится история переписки с клиентом.
); } diff --git a/src/components/dashboard/ProductGuidePanel.jsx b/src/components/dashboard/ProductGuidePanel.jsx new file mode 100644 index 0000000..2935cef --- /dev/null +++ b/src/components/dashboard/ProductGuidePanel.jsx @@ -0,0 +1,87 @@ +import React from "react"; +import { Panel } from "../UI/Panel"; + +const workflowItems = [ + { + title: "1С", + text: "Заказы создаются и проходят производство во внешней системе. В приложение попадает уже доставочная часть работы.", + }, + { + title: "Логист", + text: "Проверяет готовность набора доставки, запускает согласование с клиентом и разбирает исключения.", + }, + { + title: "Клиент", + text: "Открывает публичную ссылку и выбирает дату и половину дня доставки без входа в кабинет.", + }, + { + title: "Водитель", + text: "Видит назначенные доставки, адреса, состав заказа и отмечает движение по маршруту.", + }, +]; + +const roleItems = [ + ["Менеджер", "Ищет заказ, открывает карточку, смотрит состав, клиента и текущий статус."], + ["Логист", "Работает с наборами доставки: готовность, согласование, проблемные случаи."], + ["Водитель", "Работает только со своими доставками и маршрутными статусами."], + ["Клиент", "Выбирает удобное окно доставки по ссылке."], +]; + +const faqItems = [ + ["Что такое набор доставки?", "Это один или несколько заказов одного клиента, которые нужно доставить вместе."], + ["Когда доставка готова к запуску?", "Когда все заказы в наборе прошли контроль качества и ещё не отгружены."], + ["Какие каналы связи используются?", "SMS и email. Остальные каналы не участвуют в текущем сценарии."], +]; + +export const ProductGuidePanel = () => { + return ( +
+ +
+
+

Как работает приложение

+
+
+ +
+ {workflowItems.map((item) => ( +
+
{item.title}
+

{item.text}

+
+ ))} +
+
+ + +

Кто за что отвечает

+
+ {roleItems.map(([role, text]) => ( +
+
{role}
+

{text}

+
+ ))} +
+
+ + +

Частые вопросы

+
+ {faqItems.map(([question, answer]) => ( +
+
{question}
+

{answer}

+
+ ))} +
+
+
+ ); +}; diff --git a/src/components/dashboard/ProductGuidePanel.test.jsx b/src/components/dashboard/ProductGuidePanel.test.jsx new file mode 100644 index 0000000..d0c4048 --- /dev/null +++ b/src/components/dashboard/ProductGuidePanel.test.jsx @@ -0,0 +1,17 @@ +import React from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; +import { ProductGuidePanel } from "./ProductGuidePanel"; + +describe("ProductGuidePanel", () => { + it("shows customer-facing help without internal presentation notes", () => { + const markup = renderToStaticMarkup(); + + expect(markup).toContain("Как работает приложение"); + expect(markup).toContain("контроль качества"); + expect(markup).not.toContain("Короткая карта продукта"); + expect(markup).not.toContain("Для показа"); + expect(markup).not.toContain("ОТК"); + expect(markup).not.toContain("Что показывать заказчику"); + }); +}); diff --git a/src/components/dashboard/RoleWorkspacePanel.jsx b/src/components/dashboard/RoleWorkspacePanel.jsx deleted file mode 100644 index 4de6daf..0000000 --- a/src/components/dashboard/RoleWorkspacePanel.jsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from "react"; -import { ROLE_LABELS } from "../../constants/roles"; -import { DELIVERY_SET_BUCKET_LABELS } from "../../services/deliverySetViews"; -import { Badge } from "../UI/Badge"; -import { Panel } from "../UI/Panel"; - -const ROLE_MODULES = { - manager: [ - "Поиск по заказу, клиенту и телефону", - "Просмотр состава и статуса заказа", - "Работа только с доставочным реестром", - ], - logistician: ["Готовность заказов на сегодня", "Слоты завтра и послезавтра", "Половины дня и статус доставки"], - driver: [ - "Назначенные доставки", - "Адрес, состав и слот доставки", - "Быстрые статусы по маршруту", - ], -}; - -export const RoleWorkspacePanel = ({ role, deliverySetBuckets }) => { - const modules = ROLE_MODULES[role] || []; - const totalSets = deliverySetBuckets - ? Object.values(deliverySetBuckets).reduce((sum, sets) => sum + sets.length, 0) - : null; - - return ( - -
-
-

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

-

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

-
-
- {ROLE_LABELS[role]} - {totalSets !== null ? ( - {totalSets} наборов доставки - ) : null} -
-
- -
- {modules.map((module) => ( -
- {module} -
- ))} -
- - {deliverySetBuckets && role === "logistician" ? ( -
- {Object.entries(DELIVERY_SET_BUCKET_LABELS).map(([key, label]) => { - const count = deliverySetBuckets[key]?.length || 0; - - return ( - 0 ? "accent" : "neutral"}> - {label}: {count} - - ); - })} -
- ) : null} -
- ); -}; diff --git a/src/components/logistics/BotControlPanel.jsx b/src/components/logistics/BotControlPanel.jsx index 199f2e2..10b1e1e 100644 --- a/src/components/logistics/BotControlPanel.jsx +++ b/src/components/logistics/BotControlPanel.jsx @@ -10,7 +10,7 @@ export const BotControlPanel = ({ onReschedule, canManageLogistics, }) => { - const [channel, setChannel] = React.useState(selectedOrder?.customer.messenger || "Телеграм"); + const [channel, setChannel] = React.useState(selectedOrder?.customer.messenger || "СМС"); const [message, setMessage] = React.useState( "Заказ готов к отгрузке. Выберите дату и половину дня для доставки.", ); @@ -18,7 +18,7 @@ export const BotControlPanel = ({ const [time, setTime] = React.useState("Первая половина дня"); React.useEffect(() => { - setChannel(selectedOrder?.customer.messenger || "Телеграм"); + setChannel(selectedOrder?.customer.messenger || "СМС"); }, [selectedOrder]); if (!selectedOrder) { @@ -28,7 +28,7 @@ export const BotControlPanel = ({ return (
-

Управление ботами

+

Уведомления клиенту

Отправка уведомлений и фиксация ответов клиента в истории заказа.

@@ -36,10 +36,8 @@ export const BotControlPanel = ({
setDate(event.target.value)} type="date" /> diff --git a/src/components/orders/OrderFilters.jsx b/src/components/orders/OrderFilters.jsx index 03aa1d8..5f1262f 100644 --- a/src/components/orders/OrderFilters.jsx +++ b/src/components/orders/OrderFilters.jsx @@ -1,15 +1,90 @@ import React from "react"; -import { ORDER_STATUSES } from "../../constants/orderStatuses"; +import { DELIVERY_REGISTRY_FILTER_STATUSES } from "../../constants/orderStatuses"; import { Badge } from "../UI/Badge"; import { Button } from "../UI/Button"; import { Input } from "../UI/Input"; import { Panel } from "../UI/Panel"; -import { Select } from "../UI/Select"; -const messengers = ["Телеграм", "ВКонтакте", "Макс", "СМС", "Эл. почта"]; +const messengers = ["СМС", "Эл. почта"]; +const statusOptions = [ + { value: "all", label: "Все статусы" }, + ...DELIVERY_REGISTRY_FILTER_STATUSES.map((status) => ({ value: status, label: status })), +]; +const messengerOptions = [ + { value: "all", label: "Все каналы" }, + ...messengers.map((messenger) => ({ value: messenger, label: messenger })), +]; + +const FilterMenu = ({ label, value, options, isOpen, onToggle, onChange, onClose }) => { + const selectedLabel = options.find((option) => option.value === value)?.label || label; + + return ( +
{ + if (!event.currentTarget.contains(event.relatedTarget)) { + onClose(); + } + }} + > + + + {isOpen ? ( +
+ {options.map((option) => { + const selected = option.value === value; + + return ( + + ); + })} +
+ ) : null} +
+ ); +}; export const OrderFilters = ({ filters, setFilters }) => { const [isMobileFiltersOpen, setIsMobileFiltersOpen] = React.useState(false); + const [openMenu, setOpenMenu] = React.useState(null); const activeChips = [ filters.status !== "all" ? { key: "status", label: filters.status } : null, @@ -36,27 +111,29 @@ export const OrderFilters = ({ filters, setFilters }) => {
{renderFilterField( "Статус", - , + setOpenMenu((current) => (current === "status" ? null : "status"))} + onChange={(value) => updateFilter("status", value)} + onClose={() => setOpenMenu(null)} + />, showLabels, )} {renderFilterField( "Канал", - , + setOpenMenu((current) => (current === "messenger" ? null : "messenger"))} + onChange={(value) => updateFilter("messenger", value)} + onClose={() => setOpenMenu(null)} + />, showLabels, )}
@@ -76,14 +153,18 @@ export const OrderFilters = ({ filters, setFilters }) => { Фильтры
-
-
- Активные фильтры + {activeChips.length ? ( +
+
+ Активные фильтры +
+
+ {activeChips.map((chip) => ( + {chip.label} + ))} +
-
- {activeChips.length ? activeChips.map((chip) => {chip.label}) : Нет} -
-
+ ) : null} {isMobileFiltersOpen ? renderAdvancedFilters({ className: "rounded-[20px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-3", @@ -92,18 +173,29 @@ export const OrderFilters = ({ filters, setFilters }) => {
-
+
updateFilter("query", event.target.value)} /> -
-
Активные фильтры
-
- {activeChips.length ? activeChips.map((chip) => {chip.label}) : Нет} + {activeChips.length ? ( +
+
+ Активные фильтры +
+
+ {activeChips.map((chip) => ( + {chip.label} + ))} +
-
+ ) : null}
{renderAdvancedFilters({ showLabels: true })}
diff --git a/src/components/orders/OrderFilters.test.jsx b/src/components/orders/OrderFilters.test.jsx index 3571a94..36e60bd 100644 --- a/src/components/orders/OrderFilters.test.jsx +++ b/src/components/orders/OrderFilters.test.jsx @@ -22,13 +22,47 @@ describe("OrderFilters", () => { ); expect(markup).toContain("Поиск по заявке, клиенту, телефону"); - expect(markup).toContain("Активные фильтры"); + expect(markup).not.toContain("Активные фильтры"); + expect(markup).not.toContain("Нет"); expect(markup).toContain("Статус"); expect(markup).toContain("Канал"); + expect(markup).toContain("aria-haspopup=\"listbox\""); + expect(markup).not.toContain(" { + const markup = renderToStaticMarkup( + {}} + />, + ); + + expect(markup).toContain("Активные фильтры"); + expect(markup).toContain("Доставка согласована"); + expect(markup).toContain("СМС"); + }); }); diff --git a/src/components/orders/OrdersTable.jsx b/src/components/orders/OrdersTable.jsx index 5415695..d110de2 100644 --- a/src/components/orders/OrdersTable.jsx +++ b/src/components/orders/OrdersTable.jsx @@ -4,6 +4,7 @@ import { demoUsers } from "../../data/mockAppData"; import { formatDateTime } from "../../utils/formatters"; import { Badge } from "../UI/Badge"; import { Panel } from "../UI/Panel"; +import { OrderFilters } from "./OrderFilters"; const getUsers = (users) => (Array.isArray(users) && users.length ? users : demoUsers); const resolveUserName = (users, userId) => getUsers(users).find((user) => user.id === userId)?.name || "Не назначен"; @@ -13,17 +14,21 @@ const buildOrderSummary = (order) => { return `${leadItem}. ${leadComment}`; }; -export const OrdersTable = ({ orders, selectedOrderId, onOpenOrder, users }) => { +export const OrdersTable = ({ orders, selectedOrderId, onOpenOrder, users, filters, setFilters }) => { return ( - -
-
-

Реестр заказов

-

- Клик по строке открывает карточку в модальном окне. -

+ +
+
+
+

Реестр заказов

+

+ Поиск по номеру, клиенту и телефону. +

+
+ {orders.length}
- {orders.length} + + {filters && setFilters ? : null}
diff --git a/src/components/orders/OrdersTable.test.jsx b/src/components/orders/OrdersTable.test.jsx index 198b88b..9dc4a72 100644 --- a/src/components/orders/OrdersTable.test.jsx +++ b/src/components/orders/OrdersTable.test.jsx @@ -10,7 +10,7 @@ const orders = [ customer: { name: "Мария Волкова", phone: "+7 978 000-12-31", - messenger: "Телеграм", + messenger: "СМС", }, items: ["Кухня | 1 шт"], orderNotes: [{ text: "Подъезд узкий" }], @@ -24,11 +24,19 @@ const orders = [ describe("OrdersTable", () => { it("renders desktop table and mobile card list", () => { const markup = renderToStaticMarkup( - {}} />, + {}} + filters={{ search: "", status: "all", messenger: "all" }} + setFilters={() => {}} + />, ); expect(markup).toContain("hidden overflow-x-auto md:block"); expect(markup).toContain("md:hidden"); + expect(markup).toContain("Поиск по номеру, клиенту и телефону."); + expect(markup).toContain("Поиск по заявке, клиенту, телефону"); expect(markup).toContain("CD-240031"); expect(markup).toContain("Мария Волкова"); }); diff --git a/src/constants/orderStatuses.js b/src/constants/orderStatuses.js index b73423e..2c1a71a 100644 --- a/src/constants/orderStatuses.js +++ b/src/constants/orderStatuses.js @@ -1 +1,13 @@ export { ORDER_STATUSES } from "./deliveryWorkflow"; + +export const DELIVERY_REGISTRY_FILTER_STATUSES = [ + "Готов к отгрузке", + "Ожидает согласования доставки", + "Доставка согласована", + "Назначен водитель", + "Загружен", + "В пути", + "Доставлен", + "Проблема доставки", + "Платное хранение", +]; diff --git a/src/context/AuthContext.jsx b/src/context/AuthContext.jsx index d22d0c2..6f57bd0 100644 --- a/src/context/AuthContext.jsx +++ b/src/context/AuthContext.jsx @@ -4,8 +4,6 @@ import { supabase, hasSupabaseConfig } from "../supabaseClient"; const AuthContext = createContext(null); const STORAGE_KEY = "construction-auth-local-user"; -export const DEMO_LOGIN_EMAIL = "local@local"; -export const ROLE_SWITCH_ENTRY_EMAIL = "roles@local"; export const PROFILE_LOAD_ERROR = "Не удалось загрузить профиль пользователя."; export const UNKNOWN_EMAIL_ERROR = "Email не найден в системе. Обратитесь к администратору."; @@ -42,12 +40,6 @@ export const resolveDemoUser = (email, roleHint) => { ); }; -export const isRoleSwitchEntryEmail = (email) => - String(email || "").trim().toLowerCase() === ROLE_SWITCH_ENTRY_EMAIL; - -export const resolveLoginEmail = (isDemoMode, email) => - isDemoMode ? DEMO_LOGIN_EMAIL : email; - export const mapProfileToAuthUser = (profile) => { if (!profile) { return null; @@ -122,7 +114,7 @@ export const AuthProvider = ({ children }) => { } }, [user]); - const requestOtp = async ({ email, roleHint }) => { + const requestOtp = async ({ email, roleHint = "manager" }) => { setIsLoading(true); setPendingEmail(email); setAuthError(""); @@ -168,7 +160,7 @@ export const AuthProvider = ({ children }) => { } if (otp !== "000000") { - throw new Error("Для локального входа используйте код 000000"); + throw new Error("Неверный код подтверждения"); } const roleHint = localStorage.getItem("construction-auth-role-hint"); @@ -194,15 +186,6 @@ export const AuthProvider = ({ children }) => { setAuthError(""); }; - const signInWithRole = async (roleHint) => { - const localUser = resolveDemoUser("", roleHint); - setUser(localUser); - setPendingEmail(ROLE_SWITCH_ENTRY_EMAIL); - setIsOtpSent(false); - setAuthError(""); - return { success: true, user: localUser }; - }; - const value = { user, pendingEmail, @@ -212,7 +195,6 @@ export const AuthProvider = ({ children }) => { isDemoMode: !hasSupabaseConfig, requestOtp, verifyOtp, - signInWithRole, signOut, }; diff --git a/src/context/AuthContext.test.js b/src/context/AuthContext.test.js index 75b718b..8f6d9eb 100644 --- a/src/context/AuthContext.test.js +++ b/src/context/AuthContext.test.js @@ -1,15 +1,11 @@ import { describe, expect, it } from "vitest"; import { - DEMO_LOGIN_EMAIL, - ROLE_SWITCH_ENTRY_EMAIL, UNKNOWN_EMAIL_ERROR, buildOtpRequestPayload, - isRoleSwitchEntryEmail, mapProfileToAuthUser, mapSessionUserToAuthUser, normalizeOtpError, resolveDemoUser, - resolveLoginEmail, } from "./AuthContext"; describe("resolveDemoUser", () => { @@ -19,26 +15,11 @@ describe("resolveDemoUser", () => { expect(user.role).toBe("driver"); expect(user.email).toBe("driver@local"); }); -}); -describe("resolveLoginEmail", () => { - it("forces a single shared email in demo mode", () => { - expect(resolveLoginEmail(true, "manager@local")).toBe(DEMO_LOGIN_EMAIL); - expect(resolveLoginEmail(true, "driver@local")).toBe(DEMO_LOGIN_EMAIL); - }); + it("falls back to the matching email when no role hint is provided", () => { + const user = resolveDemoUser("admin@local"); - it("keeps the entered email outside demo mode", () => { - expect(resolveLoginEmail(false, "user@company.ru")).toBe("user@company.ru"); - }); -}); - -describe("isRoleSwitchEntryEmail", () => { - it("recognizes the special service email regardless of case and spacing", () => { - expect(isRoleSwitchEntryEmail(` ${ROLE_SWITCH_ENTRY_EMAIL.toUpperCase()} `)).toBe(true); - }); - - it("ignores regular emails", () => { - expect(isRoleSwitchEntryEmail("user@company.ru")).toBe(false); + expect(user.email).toBe("admin@local"); }); }); diff --git a/src/data/mockAppData.js b/src/data/mockAppData.js index 48db85d..052eb42 100644 --- a/src/data/mockAppData.js +++ b/src/data/mockAppData.js @@ -86,7 +86,7 @@ const baseDemoOrders = [ customer: { name: "Мария Волкова", phone: "+7 978 000-12-31", - messenger: "Телеграм", + messenger: "СМС", address: "Симферополь, ул. Тургенева, 18", }, status: "Ожидает согласования доставки", @@ -158,14 +158,14 @@ const baseDemoOrders = [ { id: "c-1", sender: "bot", - channel: "Телеграм", + channel: "СМС", text: "Заказ CD-240031 готов. Выберите дату и половину дня доставки.", sentAt: "2026-03-12T09:42:00Z", }, { id: "c-2", sender: "client", - channel: "Телеграм", + channel: "СМС", text: "Подтвержу позже, вернусь после 16:00.", sentAt: "2026-03-12T10:05:00Z", }, @@ -196,7 +196,7 @@ const baseDemoOrders = [ customer: { name: "Александр Савин", phone: "+7 978 000-12-32", - messenger: "ВКонтакте", + messenger: "Эл. почта", address: "Ялта, ул. Чехова, 9", }, status: "Готов к отгрузке", @@ -255,7 +255,7 @@ const baseDemoOrders = [ customer: { name: "Екатерина Тарасова", phone: "+7 978 000-12-33", - messenger: "Макс", + messenger: "СМС", address: "Севастополь, пр. Октябрьской Революции, 51", }, status: "Проблема доставки", @@ -299,7 +299,7 @@ const baseDemoOrders = [ { id: "c-3", sender: "bot", - channel: "Макс", + channel: "СМС", text: "Напоминаем о необходимости выбрать дату доставки.", sentAt: "2026-03-12T06:35:00Z", }, @@ -406,7 +406,7 @@ const baseDemoOrders = [ customer: { name: "Ирина Лебедева", phone: "+7 978 000-12-36", - messenger: "Телеграм", + messenger: "СМС", address: "Симферополь, ул. Киевская, 112", }, status: "Назначен водитель", @@ -451,7 +451,7 @@ const baseDemoOrders = [ { id: "c-6", sender: "bot", - channel: "Телеграм", + channel: "СМС", text: "Доставка подтверждена на 14 марта, первая половина дня.", sentAt: "2026-03-13T15:22:00Z", }, @@ -623,30 +623,30 @@ const baseDemoOrders = [ ]; const extraOrderSeeds = [ - { suffix: 101, customerName: "Людмила Артемьева", status: "Новый", city: "Симферополь", item: "Шкаф распашной", messenger: "Телеграм", updatedAt: "2026-03-14T08:20:00Z" }, - { suffix: 102, customerName: "Павел Карпов", status: "Новый", city: "Ялта", item: "Стол обеденный", messenger: "ВКонтакте", updatedAt: "2026-03-14T10:10:00Z" }, + { suffix: 101, customerName: "Людмила Артемьева", status: "Новый", city: "Симферополь", item: "Шкаф распашной", messenger: "СМС", updatedAt: "2026-03-14T08:20:00Z" }, + { suffix: 102, customerName: "Павел Карпов", status: "Новый", city: "Ялта", item: "Стол обеденный", messenger: "Эл. почта", updatedAt: "2026-03-14T10:10:00Z" }, { suffix: 103, customerName: "Алёна Беспалова", status: "Требует уточнения", city: "Евпатория", item: "Тумба ТВ", messenger: "СМС", updatedAt: "2026-03-14T06:40:00Z" }, - { suffix: 104, customerName: "Георгий Храмов", status: "Требует уточнения", city: "Севастополь", item: "Комод", messenger: "Макс", updatedAt: "2026-03-13T17:50:00Z" }, - { suffix: 105, customerName: "Валерия Фролова", status: "Подтверждён менеджером", city: "Симферополь", item: "Кухонный фасад", messenger: "Телеграм", updatedAt: "2026-03-14T11:35:00Z" }, + { suffix: 104, customerName: "Георгий Храмов", status: "Требует уточнения", city: "Севастополь", item: "Комод", messenger: "СМС", updatedAt: "2026-03-13T17:50:00Z" }, + { suffix: 105, customerName: "Валерия Фролова", status: "Подтверждён менеджером", city: "Симферополь", item: "Кухонный фасад", messenger: "СМС", updatedAt: "2026-03-14T11:35:00Z" }, { suffix: 106, customerName: "Иван Мирошниченко", status: "Подтверждён менеджером", city: "Алушта", item: "Шкаф-купе", messenger: "Эл. почта", updatedAt: "2026-03-14T09:25:00Z" }, - { suffix: 107, customerName: "Марина Ермакова", status: "В очереди производства", city: "Симферополь", item: "Столешница", messenger: "Телеграм", updatedAt: "2026-03-13T12:10:00Z" }, - { suffix: 108, customerName: "Руслан Гладков", status: "В очереди производства", city: "Ялта", item: "Гардеробная секция", messenger: "ВКонтакте", updatedAt: "2026-03-13T08:00:00Z" }, + { suffix: 107, customerName: "Марина Ермакова", status: "В очереди производства", city: "Симферополь", item: "Столешница", messenger: "СМС", updatedAt: "2026-03-13T12:10:00Z" }, + { suffix: 108, customerName: "Руслан Гладков", status: "В очереди производства", city: "Ялта", item: "Гардеробная секция", messenger: "Эл. почта", updatedAt: "2026-03-13T08:00:00Z" }, { suffix: 109, customerName: "Светлана Коваль", status: "В очереди производства", city: "Севастополь", item: "Дверь межкомнатная", messenger: "СМС", updatedAt: "2026-03-12T13:45:00Z" }, - { suffix: 110, customerName: "Михаил Орлов", status: "В производстве", city: "Симферополь", item: "Кухня линейная", messenger: "Телеграм", updatedAt: "2026-03-13T09:30:00Z" }, - { suffix: 111, customerName: "Татьяна Шубина", status: "В производстве", city: "Ялта", item: "Стеллаж", messenger: "Макс", updatedAt: "2026-03-12T15:20:00Z" }, + { suffix: 110, customerName: "Михаил Орлов", status: "В производстве", city: "Симферополь", item: "Кухня линейная", messenger: "СМС", updatedAt: "2026-03-13T09:30:00Z" }, + { suffix: 111, customerName: "Татьяна Шубина", status: "В производстве", city: "Ялта", item: "Стеллаж", messenger: "СМС", updatedAt: "2026-03-12T15:20:00Z" }, { suffix: 112, customerName: "Андрей Беляев", status: "В производстве", city: "Евпатория", item: "Фасады МДФ", messenger: "Эл. почта", updatedAt: "2026-03-14T07:55:00Z" }, - { suffix: 113, customerName: "Елена Бондарь", status: "Готов к отгрузке", city: "Симферополь", item: "Пенал для кухни", messenger: "Телеграм", updatedAt: "2026-03-14T05:15:00Z" }, + { suffix: 113, customerName: "Елена Бондарь", status: "Готов к отгрузке", city: "Симферополь", item: "Пенал для кухни", messenger: "СМС", updatedAt: "2026-03-14T05:15:00Z" }, { suffix: 114, customerName: "Кирилл Нестеров", status: "Готов к отгрузке", city: "Ялта", item: "Стол письменный", messenger: "СМС", updatedAt: "2026-03-14T09:45:00Z" }, - { suffix: 115, customerName: "Наталья Зотова", status: "Готов к отгрузке", city: "Севастополь", item: "Шкаф угловой", messenger: "ВКонтакте", updatedAt: "2026-03-13T18:05:00Z" }, - { suffix: 116, customerName: "Константин Матвеев", status: "Ожидает согласования доставки", city: "Симферополь", item: "Комод высокий", messenger: "Телеграм", updatedAt: "2026-03-14T08:05:00Z" }, - { suffix: 117, customerName: "Лариса Шевцова", status: "Ожидает согласования доставки", city: "Ялта", item: "Стеллаж модульный", messenger: "Макс", updatedAt: "2026-03-13T06:50:00Z" }, + { suffix: 115, customerName: "Наталья Зотова", status: "Готов к отгрузке", city: "Севастополь", item: "Шкаф угловой", messenger: "Эл. почта", updatedAt: "2026-03-13T18:05:00Z" }, + { suffix: 116, customerName: "Константин Матвеев", status: "Ожидает согласования доставки", city: "Симферополь", item: "Комод высокий", messenger: "СМС", updatedAt: "2026-03-14T08:05:00Z" }, + { suffix: 117, customerName: "Лариса Шевцова", status: "Ожидает согласования доставки", city: "Ялта", item: "Стеллаж модульный", messenger: "СМС", updatedAt: "2026-03-13T06:50:00Z" }, { suffix: 118, customerName: "Евгений Филимонов", status: "Ожидает согласования доставки", city: "Севастополь", item: "Тумба под мойку", messenger: "СМС", updatedAt: "2026-03-12T08:15:00Z" }, - { suffix: 119, customerName: "Диана Рябова", status: "Доставка согласована", city: "Симферополь", item: "Навесной шкаф", messenger: "Телеграм", updatedAt: "2026-03-14T11:00:00Z" }, + { suffix: 119, customerName: "Диана Рябова", status: "Доставка согласована", city: "Симферополь", item: "Навесной шкаф", messenger: "СМС", updatedAt: "2026-03-14T11:00:00Z" }, { suffix: 120, customerName: "Олег Вишневский", status: "Доставка согласована", city: "Алушта", item: "Стол раскладной", messenger: "Эл. почта", updatedAt: "2026-03-14T04:20:00Z" }, - { suffix: 121, customerName: "Полина Исаева", status: "Назначен водитель", city: "Симферополь", item: "Кровать", messenger: "Телеграм", updatedAt: "2026-03-14T10:40:00Z" }, - { suffix: 122, customerName: "Роман Щукин", status: "Назначен водитель", city: "Ялта", item: "Прихожая", messenger: "ВКонтакте", updatedAt: "2026-03-14T09:05:00Z" }, + { suffix: 121, customerName: "Полина Исаева", status: "Назначен водитель", city: "Симферополь", item: "Кровать", messenger: "СМС", updatedAt: "2026-03-14T10:40:00Z" }, + { suffix: 122, customerName: "Роман Щукин", status: "Назначен водитель", city: "Ялта", item: "Прихожая", messenger: "Эл. почта", updatedAt: "2026-03-14T09:05:00Z" }, { suffix: 123, customerName: "Юлия Баранова", status: "Загружен", city: "Севастополь", item: "Шкаф-пенал", messenger: "СМС", updatedAt: "2026-03-14T07:30:00Z" }, - { suffix: 124, customerName: "Виктор Громыко", status: "В пути", city: "Симферополь", item: "Гарнитур в прихожую", messenger: "Телеграм", updatedAt: "2026-03-14T11:20:00Z" }, + { suffix: 124, customerName: "Виктор Громыко", status: "В пути", city: "Симферополь", item: "Гарнитур в прихожую", messenger: "СМС", updatedAt: "2026-03-14T11:20:00Z" }, { suffix: 125, customerName: "Инна Самойлова", status: "Доставлен", city: "Евпатория", item: "Шкаф в ванную", messenger: "Эл. почта", updatedAt: "2026-03-14T12:05:00Z" }, ]; diff --git a/src/layouts/AppShell.jsx b/src/layouts/AppShell.jsx index 77ded6c..2e6a15e 100644 --- a/src/layouts/AppShell.jsx +++ b/src/layouts/AppShell.jsx @@ -8,12 +8,16 @@ import { ThemeToggle } from "../components/UI/ThemeToggle"; export const AppShell = ({ user, onSignOut, + onOpenGuide, + isGuideOpen = false, navItems, activeSection, onSectionChange, sectionMeta, children, }) => { + const shouldShowMobileNav = !isGuideOpen && navItems.length > 1; + return (
@@ -45,6 +49,11 @@ export const AppShell = ({
+ {onOpenGuide ? ( + + ) : null} @@ -64,6 +73,11 @@ export const AppShell = ({

+ {onOpenGuide ? ( + + ) : null}
+ {onOpenGuide ? ( + + ) : null}
@@ -99,7 +118,8 @@ export const AppShell = ({
-
+ {shouldShowMobileNav ? ( +
{navItems.map((item) => (
+ ) : null}
); }; diff --git a/src/layouts/AppShell.test.jsx b/src/layouts/AppShell.test.jsx index 10c6e05..af87331 100644 --- a/src/layouts/AppShell.test.jsx +++ b/src/layouts/AppShell.test.jsx @@ -19,6 +19,7 @@ describe("AppShell", () => { ]} activeSection="orders" onSectionChange={() => {}} + onOpenGuide={() => {}} sectionMeta={{ label: "Заказы", description: "Рабочая область заказов" }} >
content
@@ -32,5 +33,44 @@ describe("AppShell", () => { expect(markup).toContain("min-w-0"); expect(markup).toContain("Рабочая область"); expect(markup).toContain("Заказы"); + expect(markup).toContain("aria-label=\"Справка\""); + }); + + it("hides bottom navigation and shows a back action when guide is open", () => { + const markup = renderToStaticMarkup( + {}} + onOpenGuide={() => {}} + isGuideOpen={true} + navItems={[{ key: "orders", label: "Заказы", badge: "7" }]} + activeSection="orders" + onSectionChange={() => {}} + sectionMeta={{ label: "Справка", description: "Описание продукта" }} + > +
content
+
, + ); + + expect(markup).toContain("Назад"); + expect(markup).not.toContain("fixed inset-x-0 bottom-0"); + }); + + it("hides bottom navigation when the role has only one section", () => { + const markup = renderToStaticMarkup( + {}} + onOpenGuide={() => {}} + navItems={[{ key: "orders", label: "Заказы", badge: "0" }]} + activeSection="orders" + onSectionChange={() => {}} + sectionMeta={{ label: "Заказы" }} + > +
content
+
, + ); + + expect(markup).not.toContain("fixed inset-x-0 bottom-0"); }); }); diff --git a/src/pages/DashboardPage.jsx b/src/pages/DashboardPage.jsx index 4d623f2..a803224 100644 --- a/src/pages/DashboardPage.jsx +++ b/src/pages/DashboardPage.jsx @@ -1,20 +1,19 @@ import React from "react"; import { Navigate } from "react-router-dom"; -import { DRIVER_STATUSES, LOGISTICS_STATUSES, getStatusTone } from "../constants/deliveryWorkflow"; +import { DRIVER_STATUSES } from "../constants/deliveryWorkflow"; import { DriverDeliveryDetail } from "../components/driver/DriverDeliveryDetail"; import { DriverDeliveryPlanner } from "../components/driver/DriverDeliveryPlanner"; +import { DeliverySetDetailPanel } from "../components/logistics/DeliverySetDetailPanel"; +import { LogisticsReadinessBoard } from "../components/logistics/LogisticsReadinessBoard"; import { OrderDetailPanel } from "../components/orders/OrderDetailPanel"; -import { OrderFilters } from "../components/orders/OrderFilters"; import { OrdersTable } from "../components/orders/OrdersTable"; -import { Badge } from "../components/UI/Badge"; import { Button } from "../components/UI/Button"; import { Modal } from "../components/UI/Modal"; import { Panel } from "../components/UI/Panel"; +import { ProductGuidePanel } from "../components/dashboard/ProductGuidePanel"; import { useAuth } from "../context/AuthContext"; import { useOrders } from "../hooks/useOrders"; import { AppShell } from "../layouts/AppShell"; -import { getDeliveryCity, getDeliveryDay, getDeliveryHalfDay } from "../services/driverDeliveries"; -import { RoleWorkspacePanel } from "../components/dashboard/RoleWorkspacePanel"; const ROLE_SECTION = { manager: { @@ -34,67 +33,14 @@ const ROLE_SECTION = { }, }; -const addDays = (date, days) => { - const next = new Date(date); - next.setDate(next.getDate() + days); - return next; -}; - -const formatDateKey = (date) => date.toISOString().slice(0, 10); - -const buildLogisticsBuckets = (orders) => { - const todayKey = formatDateKey(new Date()); - const tomorrowKey = formatDateKey(addDays(new Date(), 1)); - const dayAfterTomorrowKey = formatDateKey(addDays(new Date(), 2)); - - const buckets = { - today: [], - tomorrow: [], - dayAfterTomorrow: [], - }; - - for (const order of orders) { - const deliveryDay = getDeliveryDay(order); - if (deliveryDay === todayKey) { - buckets.today.push(order); - } else if (deliveryDay === tomorrowKey) { - buckets.tomorrow.push(order); - } else if (deliveryDay === dayAfterTomorrowKey) { - buckets.dayAfterTomorrow.push(order); - } - } - - return buckets; -}; - -const LogisticsOrderCard = ({ order, onOpenOrder }) => ( - -); - export const DashboardPage = () => { const { user, signOut } = useAuth(); const userRole = user?.role; const section = ROLE_SECTION[userRole] || ROLE_SECTION.manager; const [activeSection, setActiveSection] = React.useState(section.key); const [isOrderModalOpen, setIsOrderModalOpen] = React.useState(false); + const [isDeliverySetModalOpen, setIsDeliverySetModalOpen] = React.useState(false); + const [selectedDeliverySet, setSelectedDeliverySet] = React.useState(null); const { orders, @@ -106,9 +52,9 @@ export const DashboardPage = () => { setFilters, updateStatus, users, - isSupabaseBacked, isLoading, loadError, + deliverySetBuckets, } = useOrders(user); React.useEffect(() => { @@ -121,9 +67,10 @@ export const DashboardPage = () => { } }, [allOrders, selectedOrderId, setSelectedOrderId]); - if (!user) { - return ; - } + const openDeliverySetModal = React.useCallback((deliverySet) => { + setSelectedDeliverySet(deliverySet); + setIsDeliverySetModalOpen(true); + }, []); const navItems = [ { @@ -133,84 +80,48 @@ export const DashboardPage = () => { badge: String(allOrders.length || orders.length || 0), }, ]; + const guideSectionMeta = { + key: "guide", + label: "Справка", + description: "Карта продукта, роли, сценарии и частые вопросы.", + }; + const activeSectionMeta = activeSection === "guide" ? guideSectionMeta : navItems[0]; + const isGuideOpen = activeSection === "guide"; const openOrderModal = (orderId) => { setSelectedOrderId(orderId); setIsOrderModalOpen(true); }; - - const logisticsOrders = React.useMemo( - () => allOrders.filter((order) => LOGISTICS_STATUSES.includes(order.status)), - [allOrders], - ); - const logisticsBuckets = React.useMemo( - () => buildLogisticsBuckets(logisticsOrders), - [logisticsOrders], - ); const driverOrders = React.useMemo( () => allOrders.filter((order) => DRIVER_STATUSES.includes(order.status)), [allOrders], ); + if (!user) { + return ; + } + const renderManagerWorkspace = () => (
- - -
-

Реестр заказов

-

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

-
- -
- +
); const renderLogisticsWorkspace = () => (
- -
- {[ - { key: "today", title: "Сегодня", orders: logisticsBuckets.today }, - { key: "tomorrow", title: "Завтра", orders: logisticsBuckets.tomorrow }, - { key: "dayAfterTomorrow", title: "Послезавтра", orders: logisticsBuckets.dayAfterTomorrow }, - ].map((bucket) => ( - -
-
-

{bucket.title}

-

- {bucket.orders.length} {bucket.orders.length === 1 ? "доставка" : "доставки"} -

-
- {bucket.orders.length} -
-
- {bucket.orders.length ? ( - bucket.orders.map((order) => ( - - )) - ) : ( -

Нет доставок в этом окне.

- )} -
-
- ))} -
- -

Заказы в логистике

-

- Здесь видны только заказы, относящиеся к доставке. Никаких бот-панелей, справочников и внутренних каналов. -

-
+
); const renderDriverWorkspace = () => (
- { ); const renderActiveSection = () => { + if (activeSection === "guide") { + return ; + } + if (userRole === "driver") { return renderDriverWorkspace(); } @@ -235,24 +150,21 @@ export const DashboardPage = () => { setActiveSection((current) => (current === "guide" ? section.key : "guide"))} + isGuideOpen={isGuideOpen} navItems={navItems} activeSection={activeSection} onSectionChange={setActiveSection} - sectionMeta={navItems[0]} + sectionMeta={activeSectionMeta} > {isLoading ? ( - Загружаем данные из Supabase... - - ) : null} - {isSupabaseBacked ? ( - - Данные загружены из Supabase, живой контур активен. + Загружаем данные... ) : null} {loadError ? ( - {loadError} + Не удалось загрузить данные. Обратитесь к администратору. ) : null} @@ -285,7 +197,7 @@ export const DashboardPage = () => {

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

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

)} + + { + setIsDeliverySetModalOpen(false); + setSelectedDeliverySet(null); + }} + > +
+
+
+

Карточка набора доставки

+

+ Все связанные заказы, их производственные шаги и статус согласования. +

+
+ +
+ { + setIsDeliverySetModalOpen(false); + setSelectedDeliverySet(null); + }} + /> +
+
); }; diff --git a/src/pages/DashboardPage.test.jsx b/src/pages/DashboardPage.test.jsx index e5c2386..3e5eaa3 100644 --- a/src/pages/DashboardPage.test.jsx +++ b/src/pages/DashboardPage.test.jsx @@ -18,7 +18,15 @@ vi.mock("../hooks/useOrders", () => ({ })); vi.mock("../layouts/AppShell", () => ({ - AppShell: ({ children }) =>
{children}
, + AppShell: ({ children, navItems, onOpenGuide, isGuideOpen }) => ( +
+ + + {children} +
+ ), })); const baseOrder = { @@ -32,7 +40,7 @@ const baseOrder = { name: "Мария Волкова", phone: "+7 978 000-12-31", address: "Симферополь, ул. Ленина, 10", - messenger: "Телеграм", + messenger: "СМС", items: ["Кухонный гарнитур | 1 комплект"], }, items: ["Кухонный гарнитур | 1 комплект"], @@ -43,6 +51,18 @@ const baseOrder = { deliverySlots: [], }; +const baseDeliverySet = { + key: "set-1", + name: "Набор Марии Волковой", + status: "ready_to_launch", + readyAt: "2026-04-15T08:00:00Z", + readyReason: "all_accepted", + sourceCustomerCity: "Симферополь", + orderCount: 1, + linkedBillTexts: "УН-00031", + orders: [baseOrder], +}; + const mockOrdersState = { orders: [baseOrder], allOrders: [baseOrder], @@ -80,9 +100,12 @@ const mockOrdersState = { agingAlerts: [], agingSummary: { warning: 0, critical: 0 }, deliverySetBuckets: { - ready_to_launch: [baseOrder], - waiting: [], - problem: [], + approaching: [], + ready_to_launch: [baseDeliverySet], + awaiting_client: [], + manual_work: [], + agreed: [], + completed: [], }, users: [ { id: "u-manager", name: "Анна", role: "manager" }, @@ -114,6 +137,12 @@ describe("DashboardPage", () => { ); expect(markup).toContain("Реестр заказов"); + expect(markup).toContain("Поиск по номеру, клиенту и телефону."); + expect(markup).toContain("aria-label=\"Справка\""); + expect(markup).not.toContain("Справка"); + expect(markup).not.toContain("доставочный контур"); + expect(markup).not.toContain("Поиск по заказу, клиенту и телефону"); + expect(markup).not.toContain("Интерфейс показывает"); expect(markup).not.toContain("Производство"); expect(markup).not.toContain("Администрирование"); expect(markup).not.toContain("Справочники"); @@ -139,10 +168,11 @@ describe("DashboardPage", () => { , ); - expect(markup).toContain("Логист: рабочая панель"); - expect(markup).toContain("Сегодня"); + expect(markup).toContain("Наборы доставки"); + expect(markup).toContain("Готово к запуску"); expect(markup).not.toContain("Управление ботами"); - expect(markup).not.toContain("Логика каналов"); + expect(markup).not.toContain("рабочая панель"); + expect(markup).not.toContain("Сегодня"); expect(markup).not.toContain("Производство"); expect(markup).not.toContain("Администрирование"); expect(markup).not.toContain("Справочники"); diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx index 58bed88..6d73a73 100644 --- a/src/pages/LoginPage.jsx +++ b/src/pages/LoginPage.jsx @@ -1,49 +1,18 @@ import React from "react"; import { Navigate } from "react-router-dom"; import { OtpLoginForm } from "../components/auth/OtpLoginForm"; -import { - DEMO_LOGIN_EMAIL, - isRoleSwitchEntryEmail, - resolveLoginEmail, - useAuth, -} from "../context/AuthContext"; +import { useAuth } from "../context/AuthContext"; export const LoginPage = () => { - const { user, isOtpSent, isLoading, isDemoMode, authError, requestOtp, verifyOtp, signInWithRole } = useAuth(); - const [email, setEmail] = React.useState(() => (isDemoMode ? DEMO_LOGIN_EMAIL : "")); - const [roleHint, setRoleHint] = React.useState("manager"); + const { user, isOtpSent, isLoading, authError, requestOtp, verifyOtp } = useAuth(); + const [email, setEmail] = React.useState(""); const [otp, setOtp] = React.useState(""); const [error, setError] = React.useState(""); - const isRoleSwitchMode = !isOtpSent && isRoleSwitchEntryEmail(email); - - React.useEffect(() => { - if (isDemoMode) { - setEmail(DEMO_LOGIN_EMAIL); - } - }, [isDemoMode]); - - if (user) { - return ; - } const displayError = error || authError; const handleRequestOtp = async () => { - if (isRoleSwitchMode) { - const response = await signInWithRole(roleHint); - if (!response.success) { - setError(response.error?.message || "Не удалось выполнить вход"); - return; - } - setError(""); - return; - } - - const response = await requestOtp( - isDemoMode - ? { email: resolveLoginEmail(isDemoMode, email), roleHint } - : { email: resolveLoginEmail(isDemoMode, email) }, - ); + const response = await requestOtp({ email }); if (!response.success) { setError(response.error?.message || "Не удалось отправить код"); return; @@ -52,7 +21,7 @@ export const LoginPage = () => { }; const handleVerifyOtp = async () => { - const response = await verifyOtp({ email: resolveLoginEmail(isDemoMode, email), otp }); + const response = await verifyOtp({ email, otp }); if (!response.success) { setError(response.error?.message || "Не удалось подтвердить код"); return; @@ -60,19 +29,19 @@ export const LoginPage = () => { setError(""); }; + if (user) { + return ; + } + return (
JSON.parse(JSON.stringify(invitation)); const getLocalStorage = () => { try { return globalThis.localStorage || null; - } catch (error) { + } catch { return null; } }; @@ -36,7 +36,7 @@ const readStoredInvitation = (token) => { try { const raw = storage.getItem(getLocalStorageKey(token)); return raw ? JSON.parse(raw) : null; - } catch (error) { + } catch { return null; } }; @@ -49,7 +49,7 @@ const writeStoredInvitation = (invitation) => { try { storage.setItem(getLocalStorageKey(invitation.token), JSON.stringify(invitation)); - } catch (error) { + } catch { // Ignore storage quota and privacy mode failures. } }; diff --git a/src/services/orderService.test.js b/src/services/orderService.test.js index e706089..8cec76a 100644 --- a/src/services/orderService.test.js +++ b/src/services/orderService.test.js @@ -157,7 +157,7 @@ describe("orderService", () => { customerName: "Тест Клиент", customerPhone: "+7 978 777-00-00", customerAddress: "Симферополь", - messenger: "Телеграм", + messenger: "СМС", managerId: "u-manager", deliveryDate: "2026-03-15", items: "Тестовая позиция", diff --git a/src/services/supabase/orderRepository.js b/src/services/supabase/orderRepository.js index e2602de..dc7fee3 100644 --- a/src/services/supabase/orderRepository.js +++ b/src/services/supabase/orderRepository.js @@ -2,19 +2,12 @@ import { safeSupabaseCall } from "../safeSupabaseCall"; import { hasSupabaseConfig, supabase } from "../../supabaseClient"; const CHANNEL_CODES = { - "телеграм": "telegram", - "вконтакте": "vk", - "макс": "messenger_max", - max: "messenger_max", "смс": "sms", "эл. почта": "email", "эл почта": "email", "электронная почта": "email", - telegram: "telegram", - vk: "vk", sms: "sms", email: "email", - messenger_max: "messenger_max", }; const requireSupabase = () => { @@ -86,7 +79,7 @@ export const mapOrderRowToOrder = (row) => { customer: { name: customer.name || "Без имени", phone: customer.phone || "", - messenger: customer.messenger || "Телеграм", + messenger: customer.messenger || "СМС", address: customer.address || "", city: customer.city || "", items: Array.isArray(customer.items) ? customer.items : [], @@ -174,7 +167,7 @@ export const buildOrderPayload = (order) => { customer: { name: customer.name || "", phone: customer.phone || "", - messenger: customer.messenger || "Телеграм", + messenger: customer.messenger || "СМС", address: customer.address || "", city: customer.city || "", items: Array.isArray(order.items) ? order.items : Array.isArray(customer.items) ? customer.items : [], diff --git a/src/services/supabase/orderRepository.test.js b/src/services/supabase/orderRepository.test.js index 83ece5c..33e6e56 100644 --- a/src/services/supabase/orderRepository.test.js +++ b/src/services/supabase/orderRepository.test.js @@ -20,7 +20,7 @@ describe("orderRepository payloads", () => { customer: { name: "Покупатель", phone: "+7 978 000-00-00", - messenger: "Телеграм", + messenger: "СМС", address: "Симферополь, ул. Ленина, 1", city: "Симферополь", items: ["Позиция | 2 шт"], diff --git a/src/styles/designSystem.css b/src/styles/designSystem.css index b6658d5..36fa398 100644 --- a/src/styles/designSystem.css +++ b/src/styles/designSystem.css @@ -3,6 +3,7 @@ --color-base: #f4f7f5; --color-surface: rgba(255, 255, 255, 0.78); --color-surface-strong: #ffffff; + --color-dropdown-surface: #ffffff; --color-text: #10211b; --color-text-muted: #5d6d66; --color-accent: #12805c; @@ -19,6 +20,7 @@ --color-base: #09110f; --color-surface: rgba(16, 26, 23, 0.82); --color-surface-strong: #0f1b18; + --color-dropdown-surface: #0b1815; --color-text: #ecf5f2; --color-text-muted: #9eb0aa; --color-accent: #57d8a9;