4.8 KiB
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)
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
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
Authorizationheader, verifies the JWT via Supabase Auth - Returns 401 if unauthenticated
- Uses the built-in
service_role(available viaSUPABASE_SERVICE_ROLE_KEYenv 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.jsxis 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.jssrc/admin/settings/useSettings.test.jssrc/admin/users/useAdminUsers.test.js