# 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`.