feat: document product and wire real client flow
This commit is contained in:
parent
b147d632e8
commit
ca72a4e662
31
README.md
31
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# Construction Delivery Control
|
# Construction Delivery Control
|
||||||
|
|
||||||
React-приложение для управления заказами, производством, логистикой и чатбот-коммуникацией через VK, Telegram и Messenger Max.
|
React-приложение для управления заказами, доставкой, ролями сотрудников и публичным согласованием доставки с клиентом.
|
||||||
|
|
||||||
## Запуск
|
## Запуск
|
||||||
|
|
||||||
|
|
@ -9,26 +9,25 @@ npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Что реализовано
|
## Главный документ
|
||||||
|
|
||||||
- OTP-вход по email через Supabase Auth с demo-режимом без backend-конфига.
|
- [Обзор системы](/Users/mihailkucer/Documents/super-sam/docs/product-overview.md) — назначение приложения, роли, сценарии, клиентский flow и подготовка к показу.
|
||||||
- Installable PWA-режим: приложение можно добавить на домашний экран и открыть как отдельное окно.
|
|
||||||
- Offline demo flow: после первого запуска дашборд и локальные demo-данные доступны без интернета.
|
## Что уже есть
|
||||||
- Role-based dashboard для менеджера, производства, логиста и администратора.
|
|
||||||
- Форма создания и редактирования заказов с автоназначением логиста.
|
- OTP-вход по email через Supabase Auth.
|
||||||
- Карточка заказа с историей статусов, действий, чата и слотов доставки.
|
- Role-based dashboard для менеджера, логиста, водителя и администратора.
|
||||||
- Панели очереди производства и администраторского обзора пользователей.
|
- Карточка заказа с историей, чатом и слотами доставки.
|
||||||
- Светлая и тёмная тема, адаптивный минималистичный UI.
|
- Публичная страница `/delivery/:token` для выбора даты и половины дня доставки.
|
||||||
- Supabase SQL-схема с RLS, аудитом и расширением под нескольких логистов.
|
- Supabase SQL-схема, таблицы приглашений и Edge Functions для invitation flow.
|
||||||
- Тестируемый сервисный слой для фильтрации, смены статусов и генерации новых заказов.
|
- Документация по архитектуре, сценариям и интеграциям.
|
||||||
- Документация по архитектуре, ботам и пользовательским сценариям.
|
|
||||||
|
|
||||||
## Структура
|
## Структура
|
||||||
|
|
||||||
- `src/` — интерфейс и клиентская логика.
|
- `src/` — интерфейс и клиентская логика.
|
||||||
- `public/` — PWA manifest, service worker и install icons.
|
|
||||||
- `supabase/schema.sql` — структура БД, роли, индексы, RLS, триггеры.
|
- `supabase/schema.sql` — структура БД, роли, индексы, RLS, триггеры.
|
||||||
- `supabase/functions/` — заготовки Edge Functions для webhook и отправки сообщений в боты.
|
- `supabase/functions/` — Edge Functions для приглашений, статусов и чат-коммуникаций.
|
||||||
|
- `supabase/seed/stage-1-demo.sql` — рабочий набор seed-данных для показа.
|
||||||
- `docs/architecture.md` — архитектура фронтенда и модулей.
|
- `docs/architecture.md` — архитектура фронтенда и модулей.
|
||||||
- `docs/chatbot-integration.md` — логика интеграции VK/Telegram/Messenger Max.
|
- `docs/product-overview.md` — общий обзор продукта, ролей и сценариев.
|
||||||
- `docs/scenarios.md` — сценарии жизненного цикла заказа.
|
- `docs/scenarios.md` — сценарии жизненного цикла заказа.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
# SuperSam: Обзор системы
|
||||||
|
|
||||||
|
`SuperSam` — это система управления доставкой заказов, которая объединяет внутренние рабочие кабинеты сотрудников и публичную страницу для клиента. Приложение помогает пройти путь от готовности заказа к отгрузке до согласования доставки, назначения исполнителей, фиксации результата и контроля исключений.
|
||||||
|
|
||||||
|
## Задачи приложения
|
||||||
|
|
||||||
|
- собрать в одном месте информацию по заказам, доставке, истории действий и коммуникациям;
|
||||||
|
- разделить рабочие зоны по ролям, чтобы каждый сотрудник видел только свой контур задач;
|
||||||
|
- дать логисту инструмент для запуска и контроля согласования доставки;
|
||||||
|
- дать клиенту простую ссылку, по которой он может выбрать дату и половину дня доставки;
|
||||||
|
- сохранить в Supabase историю изменений, статусы и интеграционные события.
|
||||||
|
|
||||||
|
## Роли
|
||||||
|
|
||||||
|
### Менеджер
|
||||||
|
|
||||||
|
Менеджер работает с заказами на ранних этапах:
|
||||||
|
- видит список заказов и карточки клиентов;
|
||||||
|
- следит за составом заказа и комментариями;
|
||||||
|
- передаёт заказ дальше по процессу после подтверждения.
|
||||||
|
|
||||||
|
### Логист
|
||||||
|
|
||||||
|
Логист отвечает за доставку:
|
||||||
|
- видит готовые к запуску и проблемные заказы;
|
||||||
|
- контролирует статусы согласования доставки;
|
||||||
|
- назначает и корректирует слоты;
|
||||||
|
- переводит заказ в ручную обработку, если клиент не ответил;
|
||||||
|
- отслеживает историю и связанные сообщения.
|
||||||
|
|
||||||
|
### Водитель
|
||||||
|
|
||||||
|
Водитель работает только со своими доставками:
|
||||||
|
- видит назначенные маршруты;
|
||||||
|
- открывает карточку точки доставки;
|
||||||
|
- фиксирует ход доставки и итоговый статус.
|
||||||
|
|
||||||
|
### Администратор
|
||||||
|
|
||||||
|
Администратор видит всю систему:
|
||||||
|
- пользователей и роли;
|
||||||
|
- общие списки заказов и событий;
|
||||||
|
- состояние интеграций и служебные данные.
|
||||||
|
|
||||||
|
### Клиент
|
||||||
|
|
||||||
|
Клиент не входит во внутренний кабинет. Он получает публичную ссылку вида `/delivery/:token` и по ней:
|
||||||
|
- видит номер заказа;
|
||||||
|
- выбирает удобную дату;
|
||||||
|
- выбирает половину дня: `До обеда` или `После обеда`;
|
||||||
|
- подтверждает выбор.
|
||||||
|
|
||||||
|
## Основные сценарии
|
||||||
|
|
||||||
|
### Внутренний сценарий
|
||||||
|
|
||||||
|
1. Заказ попадает в систему.
|
||||||
|
2. Менеджер и внутренние сотрудники ведут заказ по этапам.
|
||||||
|
3. Когда заказ готов к доставке, логист запускает приглашение клиенту.
|
||||||
|
4. Клиент выбирает слот по публичной ссылке.
|
||||||
|
5. Система переводит заказ в `Доставка согласована`.
|
||||||
|
6. Логист и водитель доводят доставку до результата.
|
||||||
|
|
||||||
|
### Сценарий клиента
|
||||||
|
|
||||||
|
Клиентская страница работает по token из таблицы `public.delivery_invitations`. Для рабочего показа используется заранее загруженный seed-набор данных.
|
||||||
|
|
||||||
|
После загрузки seed можно открыть ссылку:
|
||||||
|
|
||||||
|
`/delivery/client-flow-1001`
|
||||||
|
|
||||||
|
Эта ссылка должна показывать:
|
||||||
|
- заказ `CD-240031`;
|
||||||
|
- четыре варианта слота;
|
||||||
|
- две даты;
|
||||||
|
- две половины дня: `До обеда` и `После обеда`.
|
||||||
|
|
||||||
|
После подтверждения выбора:
|
||||||
|
- invitation переводится в состояние `agreed`;
|
||||||
|
- заказ переводится в `Доставка согласована`;
|
||||||
|
- в `order_history` появляется запись о подтверждении;
|
||||||
|
- в `delivery_slots` фиксируется подтверждённый слот.
|
||||||
|
|
||||||
|
## Что хранится в Supabase
|
||||||
|
|
||||||
|
### Основные таблицы
|
||||||
|
|
||||||
|
- `public.users` — пользователи и роли;
|
||||||
|
- `public.orders` — заказы и текущие статусы;
|
||||||
|
- `public.order_history` — история изменений;
|
||||||
|
- `public.delivery_slots` — возможные и подтверждённые слоты доставки;
|
||||||
|
- `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 развернуты:
|
||||||
|
- `get-delivery-invitation`
|
||||||
|
- `confirm-delivery-choice`
|
||||||
|
- `create-delivery-invitation`
|
||||||
|
5. Открыть внутренний кабинет.
|
||||||
|
6. Открыть клиентскую ссылку `/delivery/client-flow-1001`.
|
||||||
|
|
||||||
|
## Что показывать на встрече
|
||||||
|
|
||||||
|
- вход во внутренний кабинет;
|
||||||
|
- список заказов для менеджера, логиста и водителя;
|
||||||
|
- карточку заказа и статусы;
|
||||||
|
- клиентскую ссылку с выбором даты и половины дня;
|
||||||
|
- изменение статуса заказа после подтверждения клиентом.
|
||||||
|
|
||||||
|
## Полезные документы
|
||||||
|
|
||||||
|
- [README](/Users/mihailkucer/Documents/super-sam/README.md)
|
||||||
|
- [Архитектура](/Users/mihailkucer/Documents/super-sam/docs/architecture.md)
|
||||||
|
- [Сценарии](/Users/mihailkucer/Documents/super-sam/docs/scenarios.md)
|
||||||
|
- [Edge Functions](/Users/mihailkucer/Documents/super-sam/supabase/functions/README.md)
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
# Product Docs And Real Client Flow 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:** Создать один общий документ по приложению и ролям, подготовить рабочие Supabase seed-данные для клиентского приглашения и довести `/delivery/:token` до реального flow.
|
||||||
|
|
||||||
|
**Architecture:** Документация собирается в одном основном продуктово-ориентированном документе, на который начинает ссылаться `README`. Клиентский flow остаётся построен на `delivery_invitations` и Edge Functions, но приводится к фактическому фронтенд-контракту: фронтенд вызывает functions через Supabase invoke, а seed поднимает один реальный invitation token с рабочими слотами.
|
||||||
|
|
||||||
|
**Tech Stack:** React 18, Vite, Supabase SQL, Supabase Edge Functions, Vitest.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chunk 1: Real Client Invitation Contract
|
||||||
|
|
||||||
|
### Task 1: Зафиксировать тестами рабочий invitation contract
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/services/deliveryInvitationApi.test.js`
|
||||||
|
- Modify or Create: client page test if needed
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add failing tests for real token flow**
|
||||||
|
- [ ] **Step 2: Run targeted tests and confirm failure**
|
||||||
|
- [ ] **Step 3: Cover returned delivery date/time and real slot format**
|
||||||
|
|
||||||
|
### Task 2: Привести Edge Function и frontend API к одному контракту
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `supabase/functions/get-delivery-invitation/index.ts`
|
||||||
|
- Modify: `supabase/functions/confirm-delivery-choice/index.ts`
|
||||||
|
- Modify: `src/services/deliveryInvitationApi.js`
|
||||||
|
- Modify: `src/pages/ClientDeliveryPage.jsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Support frontend invoke contract in get-delivery-invitation**
|
||||||
|
- [ ] **Step 2: Return invitation delivery date/time fields**
|
||||||
|
- [ ] **Step 3: Keep confirm-delivery-choice compatible with selected slot data**
|
||||||
|
- [ ] **Step 4: Re-run targeted tests and confirm pass**
|
||||||
|
|
||||||
|
## Chunk 2: Supabase Seed For Demoable Client Flow
|
||||||
|
|
||||||
|
### Task 3: Подготовить реальный invitation token и слоты в seed
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `supabase/seed/stage-1-demo.sql`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add one known invitation token**
|
||||||
|
- [ ] **Step 2: Store stable slot values for tomorrow/after-tomorrow style choices**
|
||||||
|
- [ ] **Step 3: Keep seed re-runnable**
|
||||||
|
- [ ] **Step 4: Ensure order status matches awaiting client choice**
|
||||||
|
|
||||||
|
## Chunk 3: One General Product Document
|
||||||
|
|
||||||
|
### Task 4: Собрать единый общий документ по системе
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `docs/product-overview.md`
|
||||||
|
- Modify: `README.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the main product document**
|
||||||
|
- [ ] **Step 2: Cover goals, roles, scenarios, client flow and Supabase entities**
|
||||||
|
- [ ] **Step 3: Link README to the new main document**
|
||||||
|
|
||||||
|
## Chunk 4: Final Verification
|
||||||
|
|
||||||
|
### Task 5: Проверить итоговый сценарий
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Reference: `src/pages/ClientDeliveryPage.jsx`
|
||||||
|
- Reference: `supabase/seed/stage-1-demo.sql`
|
||||||
|
- Reference: `docs/product-overview.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run targeted tests**
|
||||||
|
|
||||||
|
Run: `npm test -- src/services/deliveryInvitationApi.test.js src/components/client/DeliveryChoiceFlow.test.jsx src/components/client/DeliverySlotsPicker.test.jsx`
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run auth/order regression tests**
|
||||||
|
|
||||||
|
Run: `npm test -- src/context/AuthContext.test.js src/components/auth/OtpLoginForm.test.jsx src/services/orderService.test.js src/components/orders/OrdersTable.test.jsx src/components/orders/OrderDetailPanel.test.jsx src/components/orders/OrderFilters.test.jsx`
|
||||||
|
Expected: PASS
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
# Product Docs And Real Client Flow Design
|
||||||
|
|
||||||
|
**Goal:** Подготовить один общий документ по приложению с описанием задач, ролей и пользовательских сценариев, а также довести публичную клиентскую страницу до рабочего Supabase flow на реальном invitation token и seed-данных.
|
||||||
|
|
||||||
|
## What We Are Building
|
||||||
|
|
||||||
|
Нужен один основной документ, который можно открыть первым и понять:
|
||||||
|
- зачем существует приложение;
|
||||||
|
- какие роли в нём есть;
|
||||||
|
- что видит каждая роль после входа;
|
||||||
|
- как проходит согласование доставки с клиентом;
|
||||||
|
- какие данные должны быть подготовлены в Supabase для показа.
|
||||||
|
|
||||||
|
Параллельно нужно довести клиентскую страницу `/delivery/:token` до реально работающего сценария на базе `delivery_invitations`, `orders` и `delivery_slots`, чтобы можно было показать не showcase-заглушку, а настоящий flow.
|
||||||
|
|
||||||
|
## Product Documentation Shape
|
||||||
|
|
||||||
|
Главный документ должен быть продуктовым, а не набором технических заметок. Рекомендованная структура:
|
||||||
|
|
||||||
|
1. Назначение системы
|
||||||
|
2. Основные задачи приложения
|
||||||
|
3. Роли и зоны ответственности
|
||||||
|
4. Ключевые рабочие сценарии
|
||||||
|
5. Сценарий клиента по публичной ссылке
|
||||||
|
6. Какие данные живут в Supabase
|
||||||
|
7. Как подготовить систему к показу
|
||||||
|
8. Полезные ссылки на более глубокие технические документы
|
||||||
|
|
||||||
|
Этот документ должен стать основной точкой входа из `README`.
|
||||||
|
|
||||||
|
## Real Client Flow
|
||||||
|
|
||||||
|
Клиентская страница должна работать на реальном token из `delivery_invitations`, без локального fallback как основного сценария.
|
||||||
|
|
||||||
|
Для этого:
|
||||||
|
- `get-delivery-invitation` должен корректно работать с тем способом вызова, который использует фронтенд;
|
||||||
|
- ответ invitation должен возвращать все данные, нужные клиентскому экрану: заказ, клиент, state, доступные слоты, выбранные `delivery_date` и `delivery_time`;
|
||||||
|
- `confirm-delivery-choice` должен фиксировать выбор клиента и обновлять заказ/историю;
|
||||||
|
- seed должен создавать один рабочий invitation token, который можно открыть в браузере и пройти до подтверждения.
|
||||||
|
|
||||||
|
## Seed Strategy
|
||||||
|
|
||||||
|
Нужен один понятный рабочий пример для показа:
|
||||||
|
- заказ в статусе `Ожидает ответа клиента`;
|
||||||
|
- invitation с известным токеном;
|
||||||
|
- слоты на две даты и две половины дня;
|
||||||
|
- логист и менеджер уже привязаны;
|
||||||
|
- после подтверждения выбор уходит в историю и переводит заказ в `Доставка согласована`.
|
||||||
|
|
||||||
|
Вместо текстовых слотов вида `14 апреля, первая половина дня` лучше хранить технически стабильный формат, который фронтенд однозначно разбирает, например `YYYY-MM-DD, До обеда`.
|
||||||
|
|
||||||
|
## UX Decisions
|
||||||
|
|
||||||
|
- В клиентском экране главное действие — выбор даты и половины дня без лишнего текста.
|
||||||
|
- Тексты должны звучать как рабочий сервис доставки.
|
||||||
|
- В документе роли описываются через задачи и результат, а не через внутреннюю реализацию интерфейса.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- Тесты API/клиента фиксируют рабочий invitation flow.
|
||||||
|
- После seed известный токен должен открываться на `/delivery/:token`.
|
||||||
|
- Общий документ должен быть связан с `README` как основной обзор проекта.
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
fetchDeliveryInvitation,
|
fetchDeliveryInvitation,
|
||||||
} from "../services/deliveryInvitationApi";
|
} from "../services/deliveryInvitationApi";
|
||||||
|
|
||||||
const groupSlotsFromInvitation = (invitation) => {
|
export const groupSlotsFromInvitation = (invitation) => {
|
||||||
if (!invitation) {
|
if (!invitation) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
@ -49,6 +49,15 @@ const groupSlotsFromInvitation = (invitation) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const buildDeliveryConfirmationPayload = ({
|
||||||
|
slot,
|
||||||
|
invitation,
|
||||||
|
searchDate,
|
||||||
|
}) => ({
|
||||||
|
deliveryDate: slot?.date || searchDate || invitation?.deliveryDate || undefined,
|
||||||
|
deliveryTime: slot?.time || invitation?.deliveryTime || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
export const ClientDeliveryPage = () => {
|
export const ClientDeliveryPage = () => {
|
||||||
const { token } = useParams();
|
const { token } = useParams();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
@ -103,7 +112,7 @@ export const ClientDeliveryPage = () => {
|
||||||
const invitationState = invitation?.state || "awaiting_choice";
|
const invitationState = invitation?.state || "awaiting_choice";
|
||||||
|
|
||||||
const handleConfirmChoice = React.useCallback(
|
const handleConfirmChoice = React.useCallback(
|
||||||
async (deliveryTime) => {
|
async ({ deliveryDate, deliveryTime }) => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +123,7 @@ export const ClientDeliveryPage = () => {
|
||||||
await confirmDeliveryChoice({
|
await confirmDeliveryChoice({
|
||||||
token,
|
token,
|
||||||
deliveryTime,
|
deliveryTime,
|
||||||
deliveryDate: searchParams.get("date") || invitation?.deliveryDate || undefined,
|
deliveryDate,
|
||||||
});
|
});
|
||||||
const loadedInvitation = await fetchDeliveryInvitation(token);
|
const loadedInvitation = await fetchDeliveryInvitation(token);
|
||||||
setInvitation(loadedInvitation);
|
setInvitation(loadedInvitation);
|
||||||
|
|
@ -124,15 +133,21 @@ export const ClientDeliveryPage = () => {
|
||||||
setError(confirmError instanceof Error ? confirmError.message : "Не удалось сохранить выбор");
|
setError(confirmError instanceof Error ? confirmError.message : "Не удалось сохранить выбор");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[searchParams, token, invitation],
|
[token, invitation],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSlotSelect = React.useCallback(
|
const handleSlotSelect = React.useCallback(
|
||||||
(slot) => {
|
(slot) => {
|
||||||
setSelectedSlotId(slot.id);
|
setSelectedSlotId(slot.id);
|
||||||
handleConfirmChoice(slot.time);
|
handleConfirmChoice(
|
||||||
|
buildDeliveryConfirmationPayload({
|
||||||
|
slot,
|
||||||
|
invitation,
|
||||||
|
searchDate: searchParams.get("date"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[handleConfirmChoice],
|
[handleConfirmChoice, invitation, searchParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRequestNewLink = React.useCallback(() => {
|
const handleRequestNewLink = React.useCallback(() => {
|
||||||
|
|
@ -208,4 +223,4 @@ export const ClientDeliveryPage = () => {
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
buildDeliveryConfirmationPayload,
|
||||||
|
groupSlotsFromInvitation,
|
||||||
|
} from "./ClientDeliveryPage";
|
||||||
|
|
||||||
|
describe("ClientDeliveryPage helpers", () => {
|
||||||
|
it("maps invitation slots in YYYY-MM-DD format to UI slots", () => {
|
||||||
|
expect(
|
||||||
|
groupSlotsFromInvitation({
|
||||||
|
availableSlots: [
|
||||||
|
"2026-04-15, До обеда",
|
||||||
|
"2026-04-15, После обеда",
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
id: "slot-0-2026-04-15, До обеда",
|
||||||
|
date: "2026-04-15",
|
||||||
|
time: "До обеда",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "slot-1-2026-04-15, После обеда",
|
||||||
|
date: "2026-04-15",
|
||||||
|
time: "После обеда",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds confirmation payload from the selected slot date and time", () => {
|
||||||
|
expect(
|
||||||
|
buildDeliveryConfirmationPayload({
|
||||||
|
slot: {
|
||||||
|
id: "slot-1",
|
||||||
|
date: "2026-04-16",
|
||||||
|
time: "После обеда",
|
||||||
|
},
|
||||||
|
invitation: {
|
||||||
|
deliveryDate: "2026-04-15",
|
||||||
|
},
|
||||||
|
searchDate: "2026-04-14",
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
deliveryDate: "2026-04-16",
|
||||||
|
deliveryTime: "После обеда",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -15,7 +15,7 @@ Deno.serve(async (request) => {
|
||||||
return new Response("ok", { headers: corsHeaders });
|
return new Response("ok", { headers: corsHeaders });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.method !== "GET") {
|
if (!["GET", "POST"].includes(request.method)) {
|
||||||
return new Response(JSON.stringify({ error: "Method not allowed" }), {
|
return new Response(JSON.stringify({ error: "Method not allowed" }), {
|
||||||
status: 405,
|
status: 405,
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -27,7 +27,9 @@ Deno.serve(async (request) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const token = url.searchParams.get("token") || "";
|
const token = request.method === "POST"
|
||||||
|
? ((await request.json()) as { token?: string })?.token || ""
|
||||||
|
: url.searchParams.get("token") || "";
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return new Response(JSON.stringify({ error: "token is required" }), {
|
return new Response(JSON.stringify({ error: "token is required" }), {
|
||||||
status: 400,
|
status: 400,
|
||||||
|
|
@ -96,6 +98,8 @@ Deno.serve(async (request) => {
|
||||||
customerName: order.customer?.name || invitation.customer_name || null,
|
customerName: order.customer?.name || invitation.customer_name || null,
|
||||||
customerPhone: order.customer?.phone || invitation.customer_phone || null,
|
customerPhone: order.customer?.phone || invitation.customer_phone || null,
|
||||||
availableSlots: invitation.available_slots || [],
|
availableSlots: invitation.available_slots || [],
|
||||||
|
deliveryDate: invitation.delivery_date || null,
|
||||||
|
deliveryTime: invitation.delivery_time || null,
|
||||||
orderStatus: order.status,
|
orderStatus: order.status,
|
||||||
deliveryAgreementStatus: order.delivery_agreement_status,
|
deliveryAgreementStatus: order.delivery_agreement_status,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -564,12 +564,19 @@ insert into public.delivery_slots (
|
||||||
)
|
)
|
||||||
select
|
select
|
||||||
o.id,
|
o.id,
|
||||||
'2026-04-14'::date,
|
slot.delivery_date,
|
||||||
'Первая половина дня',
|
slot.delivery_time,
|
||||||
(select id from public.users where email = 'mk7029953@yandex.ru' limit 1),
|
(select id from public.users where email = 'mk7029953@yandex.ru' limit 1),
|
||||||
'pending_confirmation',
|
'pending_confirmation',
|
||||||
null
|
null
|
||||||
from public.orders o
|
from public.orders o
|
||||||
|
cross join (
|
||||||
|
values
|
||||||
|
((current_date + 1), 'До обеда'),
|
||||||
|
((current_date + 1), 'После обеда'),
|
||||||
|
((current_date + 2), 'До обеда'),
|
||||||
|
((current_date + 2), 'После обеда')
|
||||||
|
) as slot(delivery_date, delivery_time)
|
||||||
where o.order_number = 'CD-240031';
|
where o.order_number = 'CD-240031';
|
||||||
|
|
||||||
insert into public.delivery_slots (
|
insert into public.delivery_slots (
|
||||||
|
|
@ -607,22 +614,22 @@ insert into public.delivery_invitations (
|
||||||
)
|
)
|
||||||
select
|
select
|
||||||
o.id,
|
o.id,
|
||||||
encode(digest('demo-invite-1001', 'sha256'), 'hex'),
|
encode(digest('client-flow-1001', 'sha256'), 'hex'),
|
||||||
'awaiting_choice',
|
'awaiting_choice',
|
||||||
o.order_number,
|
o.order_number,
|
||||||
o.customer ->> 'name',
|
o.customer ->> 'name',
|
||||||
o.customer ->> 'phone',
|
o.customer ->> 'phone',
|
||||||
o.customer ->> 'messenger',
|
o.customer ->> 'messenger',
|
||||||
array[
|
array[
|
||||||
'14 апреля, первая половина дня',
|
to_char(current_date + 1, 'YYYY-MM-DD') || ', До обеда',
|
||||||
'14 апреля, вторая половина дня',
|
to_char(current_date + 1, 'YYYY-MM-DD') || ', После обеда',
|
||||||
'15 апреля, первая половина дня',
|
to_char(current_date + 2, 'YYYY-MM-DD') || ', До обеда',
|
||||||
'15 апреля, вторая половина дня'
|
to_char(current_date + 2, 'YYYY-MM-DD') || ', После обеда'
|
||||||
],
|
],
|
||||||
'2026-04-14'::date,
|
(current_date + 1),
|
||||||
'Первая половина дня',
|
'До обеда',
|
||||||
'2026-04-13T09:00:00Z',
|
timezone('utc', now()),
|
||||||
'2026-04-13T09:10:00Z',
|
null,
|
||||||
null
|
null
|
||||||
from public.orders o
|
from public.orders o
|
||||||
where o.order_number = 'CD-240031'
|
where o.order_number = 'CD-240031'
|
||||||
|
|
@ -657,12 +664,12 @@ select
|
||||||
'seed',
|
'seed',
|
||||||
'success',
|
'success',
|
||||||
jsonb_build_object(
|
jsonb_build_object(
|
||||||
'token', 'demo-invite-1001',
|
'token', 'client-flow-1001',
|
||||||
'availableSlots', array[
|
'availableSlots', array[
|
||||||
'14 апреля, первая половина дня',
|
to_char(current_date + 1, 'YYYY-MM-DD') || ', До обеда',
|
||||||
'14 апреля, вторая половина дня',
|
to_char(current_date + 1, 'YYYY-MM-DD') || ', После обеда',
|
||||||
'15 апреля, первая половина дня',
|
to_char(current_date + 2, 'YYYY-MM-DD') || ', До обеда',
|
||||||
'15 апреля, вторая половина дня'
|
to_char(current_date + 2, 'YYYY-MM-DD') || ', После обеда'
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
null
|
null
|
||||||
|
|
@ -729,4 +736,4 @@ select
|
||||||
'telegram',
|
'telegram',
|
||||||
'Подтвержу позже, вернусь после 16:00.',
|
'Подтвержу позже, вернусь после 16:00.',
|
||||||
jsonb_build_object('source', 'seed')
|
jsonb_build_object('source', 'seed')
|
||||||
from public.orders o where o.order_number = 'CD-240031';
|
from public.orders o where o.order_number = 'CD-240031';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue