From b9b227e52440ef4d91943d74506e803a1a900875 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 27 May 2026 11:23:37 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20UserManagementPanel=20=E2=80=94=20theme?= =?UTF-8?q?d=20dropdown,=20inline=20edit=20name/email/role?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/UserManagementPanel.jsx | 373 +++++++++---------- 1 file changed, 179 insertions(+), 194 deletions(-) diff --git a/src/components/admin/UserManagementPanel.jsx b/src/components/admin/UserManagementPanel.jsx index fe951d5..f671937 100644 --- a/src/components/admin/UserManagementPanel.jsx +++ b/src/components/admin/UserManagementPanel.jsx @@ -1,9 +1,7 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Panel } from '../UI/Panel'; import { Badge } from '../UI/Badge'; import { Input } from '../UI/Input'; -import { Select } from '../UI/Select'; - import { supabase } from '../../supabaseClient'; @@ -25,17 +23,56 @@ const ROLE_TONES = { driver: 'accent', }; -const useIsMobile = () => { - const [mobile, setMobile] = useState(false); +/* ── Custom dropdown (matches app design system) ── */ +function RoleDropdown({ value, onChange, onClose }) { + const [open, setOpen] = useState(false); + const ref = useRef(null); + useEffect(() => { - const mq = window.matchMedia('(max-width: 640px)'); - setMobile(mq.matches); - const handler = (e) => setMobile(e.matches); - mq.addEventListener('change', handler); - return () => mq.removeEventListener('change', handler); - }, []); - return mobile; -}; + if (!open) return; + const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) { setOpen(false); onClose?.(); } }; + const onKey = (e) => { if (e.key === 'Escape') { setOpen(false); onClose?.(); } }; + document.addEventListener('pointerdown', onDown); + document.addEventListener('keydown', onKey); + return () => { document.removeEventListener('pointerdown', onDown); document.removeEventListener('keydown', onKey); }; + }, [open, onClose]); + + const pick = (role) => { onChange(role); setOpen(false); }; + + return ( +
+ + {open && ( +
+ {ROLES.map((r) => { + const sel = r === value; + return ( + + ); + })} +
+ )} +
+ ); +} export default function UserManagementPanel() { const [users, setUsers] = useState([]); @@ -43,15 +80,13 @@ export default function UserManagementPanel() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showAddForm, setShowAddForm] = useState(false); - const [editingRoleId, setEditingRoleId] = useState(null); + const [editingId, setEditingId] = useState(null); + const [editForm, setEditForm] = useState({ name: '', email: '', role: '' }); + const [saving, setSaving] = useState(false); const [deleteConfirmId, setDeleteConfirmId] = useState(null); const [addForm, setAddForm] = useState({ name: '', email: '', role: '' }); const [addSubmitting, setAddSubmitting] = useState(false); const [addError, setAddError] = useState(null); - const mobile = useIsMobile(); - - - const fetchRoles = useCallback(async () => { const { data, error: err } = await supabase.from('roles').select('id, name').order('name'); @@ -79,25 +114,18 @@ export default function UserManagementPanel() { const match = roles.find((r) => r.id === user.role_id); return match ? match.name : 'unknown'; }; - const getRoleId = (roleName) => { const match = roles.find((r) => r.name === roleName); return match ? match.id : null; }; + /* ── Add ── */ const handleAddUser = async (e) => { e.preventDefault(); setAddError(null); - if (!addForm.name || !addForm.email || !addForm.role) { - setAddError('Все поля обязательны.'); - return; - } + if (!addForm.name || !addForm.email || !addForm.role) { setAddError('Все поля обязательны.'); return; } setAddSubmitting(true); try { - if (!supabase) { - setAddError('Требуется VITE_SUPABASE_SERVICE_ROLE_KEY для управления пользователями.'); - return; - } const roleId = getRoleId(addForm.role); const { data: authData, error: authErr } = await supabase.auth.admin.createUser({ email: addForm.email, @@ -114,67 +142,56 @@ export default function UserManagementPanel() { setAddForm({ name: '', email: '', role: '' }); } catch (err) { setAddError(err.message || 'Не удалось добавить пользователя.'); - } finally { - setAddSubmitting(false); - } + } finally { setAddSubmitting(false); } }; - const handleRoleChange = async (userId, newRoleName) => { - const roleId = getRoleId(newRoleName); - if (!roleId) return; + /* ── Edit ── */ + const startEdit = (user) => { + setEditingId(user.id); + setEditForm({ name: user.name || '', email: user.email || '', role: getRoleName(user) }); + setDeleteConfirmId(null); + }; + const cancelEdit = () => { setEditingId(null); setEditForm({ name: '', email: '', role: '' }); }; + const saveEdit = async () => { + setSaving(true); try { + const roleId = getRoleId(editForm.role); const { error: err } = await supabase .from('users') - .update({ role_id: roleId }) - .eq('id', userId); + .update({ name: editForm.name, email: editForm.email, role_id: roleId }) + .eq('id', editingId); if (err) throw err; - setEditingRoleId(null); + setEditingId(null); await fetchUsers(); - } catch (err) { - setError(err.message || 'Не удалось обновить роль.'); - } + } catch (err) { setError(err.message || 'Не удалось сохранить.'); } + finally { setSaving(false); } }; + /* ── Delete ── */ const handleDeleteUser = async (userId) => { try { const { error: err } = await supabase.from('users').delete().eq('id', userId); if (err) throw err; - if (supabase) await supabase.auth.admin.deleteUser(userId); + try { await supabase.auth.admin.deleteUser(userId); } catch {} setDeleteConfirmId(null); await fetchUsers(); - } catch (err) { - setError(err.message || 'Не удалось удалить пользователя.'); - } + } catch (err) { setError(err.message || 'Не удалось удалить.'); } }; const fmtDate = (iso) => iso ? new Date(iso).toLocaleString('ru-RU') : '—'; - // Mobile: horizontal scroll for table - const mobileTableStyle = mobile ? { - overflowX: 'auto', - WebkitOverflowScrolling: 'touch', - msOverflowStyle: '-ms-autohiding-scrollbar', - } : {}; + if (loading) { + return
Загрузка…
; + } return ( {/* Toolbar */} -
- - {users.length} пользователей - +
+ {users.length} пользователей @@ -182,16 +199,8 @@ export default function UserManagementPanel() { {/* Add form */} {showAddForm && ( -
-
+ +
setAddForm((f) => ({ ...f, email: e.target.value }))} className="flex-1 min-w-[180px]!" /> - + setAddForm((f) => ({ ...f, role: r }))} + /> +
+ {addError &&
{addError}
} +
+
- {addError && ( -
{addError}
- )} - )} {/* Error */} {error && ( -
+
{error}
)} - {loading ? ( -
- Загрузка& -
- ) : ( -
-
- {/* Header */} -
- EmailИмяРольСоздан -
+ {/* User list */} +
+ {users.length === 0 && ( +
Нет пользователей
+ )} + {users.map((user) => { + const rn = getRoleName(user); + const isEditing = editingId === user.id; - {users.length === 0 && ( -
- Нет пользователей -
- )} - - {users.map((user) => { - const rn = getRoleName(user); - return ( -
- {user.email} - {user.name || '—'} - - {editingRoleId === user.id ? ( - - ) : ( - setEditingRoleId(user.id)} style={{ cursor: 'pointer' }}> - {ROLE_LABELS[rn] || rn} - - )} - - {fmtDate(user.created_at)} - -
- {deleteConfirmId === user.id ? ( - <> - - - - ) : ( - - )} -
+ if (isEditing) { + return ( +
+
+ setEditForm((f) => ({ ...f, name: e.target.value }))} + className="flex-1 min-w-[140px]!" + /> + setEditForm((f) => ({ ...f, email: e.target.value }))} + className="flex-1 min-w-[180px]!" + /> + setEditForm((f) => ({ ...f, role: r }))} + onClose={undefined} + />
- ); - })} -
-
- )} +
+ + +
+
+ ); + } + + return ( +
+
+
{user.name || '—'}
+
{user.email}
+
{fmtDate(user.created_at)}
+
+
+ {ROLE_LABELS[rn] || rn} +
+ + {deleteConfirmId === user.id ? ( + <> + + + + ) : ( + + )} +
+
+
+ ); + })} +
); } \ No newline at end of file