diff --git a/docs/superpowers/specs/2026-05-07-admin-panel-design.md b/docs/superpowers/specs/2026-05-07-admin-panel-design.md new file mode 100644 index 0000000..7896b13 --- /dev/null +++ b/docs/superpowers/specs/2026-05-07-admin-panel-design.md @@ -0,0 +1,187 @@ +# Admin Panel Design — Осколкам.Нет + +**Дата:** 2026-05-07 +**Статус:** Implemented +**Проект:** glass (бронирование окон) + +--- + +## Контекст + +Публичный сайт `Осколкам.Нет` — React 19 + Vite, single-page маркетинговый лендинг на русском языке. Форма заявки отправляет данные в Telegram Bot API. Бекенда нет. Цель — добавить внутреннюю систему управления заявками и заказами, не меняя публичную часть. + +--- + +## Технический стек + +| Компонент | Решение | +|-----------|---------| +| Роутинг | `react-router-dom` — маршрут `/admin/*` в том же приложении | +| База данных | Supabase (PostgreSQL) | +| Авторизация | Supabase Auth — email + пароль | +| Drag-and-drop | `@dnd-kit/core` + `@dnd-kit/sortable` | +| Графики | `recharts` | +| Стиль | Те же CSS-переменные и шрифты, что у публичного сайта | + +Новые зависимости: `@supabase/supabase-js`, `react-router-dom`, `recharts`, `@dnd-kit/core`, `@dnd-kit/sortable` + +--- + +## Схема базы данных + +### Таблица `orders` +```sql +id uuid PRIMARY KEY DEFAULT gen_random_uuid() +created_at timestamptz DEFAULT now() +name text NOT NULL -- ФИО / организация +phone text NOT NULL +object_type text -- industrial | commercial | infrastructure | residential | other +area_sqm numeric -- площадь из калькулятора +film_thickness integer -- 200 | 300 микрон +estimated_cost numeric -- из калькулятора +final_cost numeric -- фактическая стоимость после замера +status text DEFAULT 'новая' -- новая | замер | согласование | монтаж | закрыт +notes text +assigned_to text -- email ответственного +closed_at timestamptz +source text DEFAULT 'form' -- form | manual +``` + +### Таблица `order_events` +```sql +id uuid PRIMARY KEY DEFAULT gen_random_uuid() +order_id uuid REFERENCES orders(id) ON DELETE CASCADE +created_at timestamptz DEFAULT now() +created_by text -- email пользователя +event_type text -- status_change | note | edit +description text -- человекочитаемое описание события +``` + +RLS политики: все операции только для аутентифицированных пользователей. + +--- + +## Структура файлов + +``` +src/ + lib/ + supabase.js ← единственный Supabase-клиент + + admin/ + auth/ + LoginPage.jsx + ProtectedRoute.jsx + useAuth.js ← хук: сессия, login, logout, onAuthStateChange + + layout/ + AdminLayout.jsx ← обёртка: сайдбар + шапка + + Sidebar.jsx ← навигация, кнопка выхода + + orders/ + KanbanBoard.jsx ← DndContext, 5 колонок + KanbanColumn.jsx ← SortableContext, карточки + OrderCard.jsx ← краткая инфо, drag handle + OrderModal.jsx ← детали + редактирование + лог событий + useOrders.js ← CRUD, смена статуса, realtime subscription + + analytics/ + AnalyticsDashboard.jsx ← KPI-карточки + 4 графика + CompletedOrdersTable.jsx ← таблица закрытых заказов + экспорт CSV + useAnalytics.js ← агрегированные запросы по периоду + charts/ + FunnelChart.jsx + RevenueChart.jsx + ObjectTypeChart.jsx + AvgTimeChart.jsx + + App.jsx ← добавляются маршруты /admin/* +``` + +--- + +## Маршруты + +``` +/admin → redirect → /admin/kanban +/admin/login → LoginPage (public) +/admin/kanban → AdminLayout > KanbanBoard (protected) +/admin/analytics → AdminLayout > AnalyticsDashboard (protected) +/admin/completed → AdminLayout > CompletedOrdersTable (protected) +``` + +`ProtectedRoute` проверяет сессию через `supabase.auth.getSession()`. Если сессии нет — редирект на `/admin/login` с сохранением `returnUrl`. + +--- + +## Канбан-доска + +**Стадии:** `новая → замер → согласование → монтаж → закрыт` + +- Drag-and-drop через `@dnd-kit`. При drop: `UPDATE orders SET status = $1` + INSERT в `order_events`. +- `useOrders` подписывается на Supabase Realtime — изменения от другого пользователя появляются без перезагрузки. +- Карточка `OrderCard` показывает: имя, тип объекта, площадь, ориентировочная стоимость, дата создания. +- Клик на карточку → `OrderModal`: все поля редактируемые, лог событий снизу, кнопка смены статуса (для мобильного). +- Кнопка «+ Заявка» в шапке → модалка создания заказа вручную (`source: 'manual'`). + +**Интеграция с формой сайта:** +`LeadForm.jsx` после успешной отправки в Telegram делает `supabase.from('orders').insert(...)` со статусом `новая`. Ошибка вставки в Supabase не блокирует отправку в Telegram (try/catch раздельно). + +--- + +## Аналитика + +**Фильтр периода:** 7 дней / 30 дней / 90 дней / 365 дней / произвольный диапазон дат. + +**KPI-карточки (4 шт.):** +1. Всего заявок за период +2. Закрыто + конверсия % +3. Выручка (сумма `final_cost` закрытых заказов) +4. Средний чек + +**Графики:** +1. `FunnelChart` — горизонтальный BarChart: количество заказов в каждой стадии + % перехода +2. `RevenueChart` — LineChart: выручка по месяцам (закрытые заказы) +3. `ObjectTypeChart` — PieChart (donut): распределение по типам объектов +4. `AvgTimeChart` — BarChart: среднее время (дни) в каждой стадии. Рассчитывается из `order_events`: разница `created_at` между событиями смены статуса для каждого заказа, усреднённая по всем заказам в периоде. + +**Вкладка «Завершённые»:** +Таблица закрытых заказов с колонками: дата закрытия, имя, тип объекта, площадь, итоговая стоимость, ответственный. Сортировка по любой колонке. Экспорт в CSV через `Blob` + `URL.createObjectURL`. + +--- + +## Layout + +**Сайдбар** (фиксированный, 240px): +- Фон `#0f172a` (тёмный, соответствует `--text-main` сайта) +- Логотип «Осколкам.Нет» вверху +- Навигация: Канбан / Аналитика / Завершённые +- Внизу: email пользователя + кнопка «Выйти» + +**Шапка** (фиксированная, 60px): +- Название текущего раздела +- Badge с количеством новых необработанных заявок +- Кнопка «+ Заявка» + +**Стиль:** Те же CSS-переменные (`--accent-blue`, `--accent-orange`, `--font-heading`, etc.), те же компоненты `.glass-panel`, `.btn-primary`. + +--- + +## Переменные окружения + +Добавляются в `.env` и `.env.example`: +``` +VITE_SUPABASE_URL= +VITE_SUPABASE_ANON_KEY= +``` + +--- + +## Аудит текущего кода + +Обнаруженные проблемы, которые нужно исправить в рамках задачи: + +1. **`Estimator.jsx:8`** — `windows` state существует, но не участвует в расчёте стоимости (формула зависит только от `area` и `thickness`). Удалить слайдер «Количество окон» из UI как вводящий в заблуждение. +2. **`Navbar.jsx:52`** — `href="tel:112"` — заглушка. Требует реального номера (вне скоупа этой задачи, задокументировано). +3. **`LeadForm.jsx`** — нет интеграции с Supabase. Добавляется в рамках этой задачи. +4. **`App.jsx`** — нет роутера. Добавляется `BrowserRouter` + `Routes`.