Files
glass/docs/superpowers/specs/2026-05-07-admin-panel-design.md
T
2026-05-07 20:35:08 +03:00

188 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 ← обёртка: сайдбар + шапка + <Outlet />
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`.