From 2b9903a6bff3ff14aaeeb52fe953b6ffc5ab7dfc Mon Sep 17 00:00:00 2001 From: houseassassin Date: Sun, 10 May 2026 12:26:17 +0300 Subject: [PATCH] docs: add admin panel v2 design spec (user mgmt, pricing, order fields) --- .../specs/2026-05-10-admin-panel-v2-design.md | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-10-admin-panel-v2-design.md diff --git a/docs/superpowers/specs/2026-05-10-admin-panel-v2-design.md b/docs/superpowers/specs/2026-05-10-admin-panel-v2-design.md new file mode 100644 index 0000000..a673f13 --- /dev/null +++ b/docs/superpowers/specs/2026-05-10-admin-panel-v2-design.md @@ -0,0 +1,110 @@ +# 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`