111 lines
4.8 KiB
Markdown
111 lines
4.8 KiB
Markdown
# Admin Panel v2 — Design Spec
|
|
|
|
**Date:** 2026-05-10
|
|
**Features:** User management, configurable pricing, new order contact fields
|
|
|
|
---
|
|
|
|
## Goal
|
|
|
|
Extend the existing Осколкам.Нет admin panel with three capabilities: full admin user management (invite/delete) via Supabase Edge Functions, dynamic calculator pricing stored in the database, and three new contact fields on orders (Telegram, Max messenger, address).
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
Three independent feature blocks, each with isolated DB changes, hooks, and UI. No changes to the public LeadForm. The Estimator (public) reads prices from DB via a new `usePricing` hook. User management uses Edge Functions as a secure server-side proxy so `service_role` is never exposed in the browser bundle.
|
|
|
|
**Tech stack:** React 19 + Vite, Supabase (PostgreSQL + Auth + Edge Functions), react-router-dom v6, inline styles, lucide-react icons, vitest + @testing-library/react.
|
|
|
|
---
|
|
|
|
## Database Changes
|
|
|
|
### `settings` table (new)
|
|
|
|
```sql
|
|
CREATE TABLE settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
INSERT INTO settings (key, value) VALUES
|
|
('price_200', '2000'),
|
|
('price_300', '3000');
|
|
|
|
ALTER TABLE settings ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Anyone can read settings" ON settings FOR SELECT USING (true);
|
|
CREATE POLICY "Auth users can update settings" ON settings FOR UPDATE USING (auth.role() = 'authenticated');
|
|
```
|
|
|
|
### `orders` table additions
|
|
|
|
```sql
|
|
ALTER TABLE orders
|
|
ADD COLUMN telegram_username TEXT,
|
|
ADD COLUMN max_username TEXT,
|
|
ADD COLUMN address TEXT;
|
|
```
|
|
|
|
---
|
|
|
|
## Edge Functions
|
|
|
|
Located in `supabase/functions/`. Each function:
|
|
- Reads `Authorization` header, verifies the JWT via Supabase Auth
|
|
- Returns 401 if unauthenticated
|
|
- Uses the built-in `service_role` (available via `SUPABASE_SERVICE_ROLE_KEY` env var in the Supabase runtime)
|
|
|
|
| Function | Method | Body | Action |
|
|
|---|---|---|---|
|
|
| `list-admin-users` | GET | — | `auth.admin.listUsers()` → `[{id, email, created_at, last_sign_in_at}]` |
|
|
| `invite-admin-user` | POST | `{email}` | `auth.admin.inviteUserByEmail(email)` — Supabase sends the invite email |
|
|
| `delete-admin-user` | POST | `{userId}` | `auth.admin.deleteUser(userId)` |
|
|
|
|
Deploy with: `supabase functions deploy list-admin-users && supabase functions deploy invite-admin-user && supabase functions deploy delete-admin-user`
|
|
|
|
---
|
|
|
|
## Frontend
|
|
|
|
### New files
|
|
|
|
| File | Responsibility |
|
|
|---|---|
|
|
| `src/hooks/usePricing.js` | Public hook — reads `price_200` and `price_300` from `settings` table. Returns `{prices: {200: number, 300: number}, loading}`. Used by `Estimator.jsx`. |
|
|
| `src/admin/settings/useSettings.js` | Admin hook — reads and updates prices in `settings`. Returns `{prices, loading, error, updatePrice(key, value)}`. |
|
|
| `src/admin/settings/SettingsPage.jsx` | Two number inputs (цена 200 мкм, цена 300 мкм) with a save button. Shows current prices, optimistic update on save. |
|
|
| `src/admin/users/useAdminUsers.js` | Hook — calls Edge Functions via `supabase.functions.invoke()`. Returns `{users, loading, error, inviteUser(email), deleteUser(userId)}`. |
|
|
| `src/admin/users/UserManagementPage.jsx` | Table of current admin users (email, created_at, last_sign_in). Invite form (email input + button). Delete button per row with confirmation dialog. |
|
|
|
|
### Modified files
|
|
|
|
| File | Change |
|
|
|---|---|
|
|
| `src/admin/layout/Sidebar.jsx` | Add nav items: «Пользователи» → `/admin/users` (Users icon), «Настройки» → `/admin/settings` (Settings icon) |
|
|
| `src/App.jsx` | Add lazy-loaded routes for `/admin/users` and `/admin/settings` inside the existing admin auth guard |
|
|
| `src/components/Estimator.jsx` | Replace hardcoded `costPerSqM` with `usePricing()`. Show skeleton/fallback while prices load. |
|
|
| `src/admin/orders/OrderModal.jsx` | Add three fields: `telegram_username` (text input, placeholder `@username`), `max_username` (text input, placeholder `@username`), `address` (textarea) |
|
|
| `src/admin/orders/useOrders.js` | Include `telegram_username`, `max_username`, `address` in `select('*')` (already covered), `createOrder`, and `updateOrder` payloads |
|
|
|
|
---
|
|
|
|
## Scope Exclusions
|
|
|
|
- Public `LeadForm.jsx` is **not** modified — new fields are admin-only
|
|
- Only two thickness options exist (200 µm, 300 µm) — no UI for adding/removing thickness tiers
|
|
- User roles beyond "authenticated = admin" are out of scope — all authenticated users have equal access
|
|
- Password reset is handled by Supabase's built-in email flow, not from the panel
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
Each new hook gets a vitest unit test following the existing pattern (vi.hoisted() for mock variables, vi.mock() for supabase, renderHook + act). UI components get smoke tests (renders without crash, key elements present).
|
|
|
|
Files:
|
|
- `src/hooks/usePricing.test.js`
|
|
- `src/admin/settings/useSettings.test.js`
|
|
- `src/admin/users/useAdminUsers.test.js`
|