feat: document product and wire real client flow

This commit is contained in:
Codex 2026-04-14 23:20:50 +03:00
parent b147d632e8
commit ca72a4e662
8 changed files with 385 additions and 42 deletions

View File

@ -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` — сценарии жизненного цикла заказа.

129
docs/product-overview.md Normal file
View File

@ -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)

View File

@ -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

View File

@ -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` как основной обзор проекта.

View File

@ -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>
); );
}; };

View File

@ -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: "После обеда",
});
});
});

View File

@ -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,
}, },

View File

@ -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';