Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9.2 KiB
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
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
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 шт.):
- Всего заявок за период
- Закрыто + конверсия %
- Выручка (сумма
final_costзакрытых заказов) - Средний чек
Графики:
FunnelChart— горизонтальный BarChart: количество заказов в каждой стадии + % переходаRevenueChart— LineChart: выручка по месяцам (закрытые заказы)ObjectTypeChart— PieChart (donut): распределение по типам объектов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=
Аудит текущего кода
Обнаруженные проблемы, которые нужно исправить в рамках задачи:
Estimator.jsx:8—windowsstate существует, но не участвует в расчёте стоимости (формула зависит только отareaиthickness). Удалить слайдер «Количество окон» из UI как вводящий в заблуждение.Navbar.jsx:52—href="tel:112"— заглушка. Требует реального номера (вне скоупа этой задачи, задокументировано).LeadForm.jsx— нет интеграции с Supabase. Добавляется в рамках этой задачи.App.jsx— нет роутера. ДобавляетсяBrowserRouter+Routes.