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