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

9.2 KiB
Raw Blame History

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 шт.):

  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:8windows state существует, но не участвует в расчёте стоимости (формула зависит только от area и thickness). Удалить слайдер «Количество окон» из UI как вводящий в заблуждение.
  2. Navbar.jsx:52href="tel:112" — заглушка. Требует реального номера (вне скоупа этой задачи, задокументировано).
  3. LeadForm.jsx — нет интеграции с Supabase. Добавляется в рамках этой задачи.
  4. App.jsx — нет роутера. Добавляется BrowserRouter + Routes.