fix: modal styling - solid bg, full-width dropdown, labels
This commit is contained in:
parent
e04485c446
commit
cee5acab1d
|
|
@ -49,7 +49,7 @@ async function adminApi(method, body) {
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Custom dropdown (matches app design system) ── */
|
/* ── Custom dropdown (matches app design system, full-width) ── */
|
||||||
function RoleDropdown({ value, onChange }) {
|
function RoleDropdown({ value, onChange }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
|
@ -66,17 +66,17 @@ function RoleDropdown({ value, onChange }) {
|
||||||
const pick = (role) => { onChange(role); setOpen(false); };
|
const pick = (role) => { onChange(role); setOpen(false); };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="relative">
|
<div ref={ref} className="relative w-full">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpen((v) => !v)}
|
onClick={() => setOpen((v) => !v)}
|
||||||
className="flex h-[38px] items-center gap-1 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-3 text-sm transition hover:border-[var(--color-accent)]"
|
className="flex w-full h-[46px] items-center justify-between gap-2 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface-strong)] px-4 text-sm transition hover:border-[var(--color-accent)]"
|
||||||
>
|
>
|
||||||
<Badge tone={ROLE_TONES[value] || 'neutral'}>{ROLE_LABELS[value] || value}</Badge>
|
<Badge tone={ROLE_TONES[value] || 'neutral'}>{ROLE_LABELS[value] || value}</Badge>
|
||||||
<span className="ml-1 text-[var(--color-text-muted)] text-xs">▾</span>
|
<span className="text-[var(--color-text-muted)] text-xs">▾</span>
|
||||||
</button>
|
</button>
|
||||||
{open && (
|
{open && (
|
||||||
<div className="absolute left-0 top-full z-30 mt-2 overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-dropdown-surface)] shadow-soft min-w-[180px]">
|
<div className="absolute left-0 right-0 top-full z-50 mt-1.5 overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-dropdown-surface)] shadow-soft">
|
||||||
{ROLES.map((r) => {
|
{ROLES.map((r) => {
|
||||||
const sel = r === value;
|
const sel = r === value;
|
||||||
return (
|
return (
|
||||||
|
|
@ -85,8 +85,8 @@ function RoleDropdown({ value, onChange }) {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => pick(r)}
|
onClick={() => pick(r)}
|
||||||
className={[
|
className={[
|
||||||
'flex w-full items-center justify-between px-4 py-2.5 text-left text-sm transition',
|
'flex w-full items-center justify-between px-4 py-3 text-left text-sm transition',
|
||||||
sel ? 'bg-[var(--color-accent-soft)] text-[var(--color-text)]' : 'text-[var(--color-text)] hover:bg-[var(--color-surface-strong)]',
|
sel ? 'bg-[var(--color-accent-soft)] text-[var(--color-accent)]' : 'text-[var(--color-text)] hover:bg-[var(--color-surface)]',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
<span>{ROLE_LABELS[r]}</span>
|
<span>{ROLE_LABELS[r]}</span>
|
||||||
|
|
@ -100,6 +100,80 @@ function RoleDropdown({ value, onChange }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Add-user modal ── */
|
||||||
|
function AddUserModal({ onSubmit, onClose, submitting, error }) {
|
||||||
|
const [form, setForm] = useState({ name: '', email: '', role: 'manager' });
|
||||||
|
const nameRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => { nameRef.current?.focus(); }, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-[400px] max-w-[92vw] rounded-[28px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-6 shadow-panel"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<h3 className="mb-5 text-lg font-semibold text-[var(--color-text)]">Новый пользователь</h3>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onSubmit(form);
|
||||||
|
}}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1.5 block text-xs font-medium text-[var(--color-text-muted)]">Имя</label>
|
||||||
|
<Input
|
||||||
|
ref={nameRef}
|
||||||
|
placeholder="Иван Петров"
|
||||||
|
value={form.name}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1.5 block text-xs font-medium text-[var(--color-text-muted)]">Email</label>
|
||||||
|
<Input
|
||||||
|
placeholder="ivan@company.ru"
|
||||||
|
type="email"
|
||||||
|
value={form.email}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1.5 block text-xs font-medium text-[var(--color-text-muted)]">Роль</label>
|
||||||
|
<RoleDropdown
|
||||||
|
value={form.role}
|
||||||
|
onChange={(r) => setForm((f) => ({ ...f, role: r }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-xl bg-[rgba(201,61,61,0.12)] px-4 py-2.5 text-sm text-[var(--color-danger)]">{error}</div>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="flex-1 rounded-full border border-[var(--color-border)] px-4 py-2.5 text-sm font-semibold text-[var(--color-text-muted)] hover:bg-[var(--color-surface)] transition"
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={submitting}
|
||||||
|
className="flex-1 rounded-full bg-[var(--color-accent)] px-4 py-2.5 text-sm font-semibold text-white hover:opacity-90 transition disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{submitting ? 'Добавление…' : 'Добавить'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function UserManagementPanel() {
|
export default function UserManagementPanel() {
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [roles, setRoles] = useState([]);
|
const [roles, setRoles] = useState([]);
|
||||||
|
|
@ -110,7 +184,6 @@ export default function UserManagementPanel() {
|
||||||
const [editForm, setEditForm] = useState({ name: '', email: '', role: '' });
|
const [editForm, setEditForm] = useState({ name: '', email: '', role: '' });
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [deleteConfirmId, setDeleteConfirmId] = useState(null);
|
const [deleteConfirmId, setDeleteConfirmId] = useState(null);
|
||||||
const [addForm, setAddForm] = useState({ name: '', email: '', role: '' });
|
|
||||||
const [addSubmitting, setAddSubmitting] = useState(false);
|
const [addSubmitting, setAddSubmitting] = useState(false);
|
||||||
const [addError, setAddError] = useState(null);
|
const [addError, setAddError] = useState(null);
|
||||||
|
|
||||||
|
|
@ -146,16 +219,14 @@ export default function UserManagementPanel() {
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ── Add via edge function ── */
|
/* ── Add via edge function ── */
|
||||||
const handleAddUser = async (e) => {
|
const handleAddUser = async (form) => {
|
||||||
e.preventDefault();
|
|
||||||
setAddError(null);
|
setAddError(null);
|
||||||
if (!addForm.name || !addForm.email || !addForm.role) { setAddError('Все поля обязательны.'); return; }
|
if (!form.name || !form.email || !form.role) { setAddError('Все поля обязательны.'); return; }
|
||||||
setAddSubmitting(true);
|
setAddSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await adminApi('POST', { email: addForm.email, name: addForm.name, role: addForm.role });
|
await adminApi('POST', { email: form.email, name: form.name, role: form.role });
|
||||||
await fetchUsers();
|
await fetchUsers();
|
||||||
setShowAddForm(false);
|
setShowAddForm(false);
|
||||||
setAddForm({ name: '', email: '', role: '' });
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setAddError(err.message || 'Не удалось добавить пользователя.');
|
setAddError(err.message || 'Не удалось добавить пользователя.');
|
||||||
} finally { setAddSubmitting(false); }
|
} finally { setAddSubmitting(false); }
|
||||||
|
|
@ -199,54 +270,21 @@ export default function UserManagementPanel() {
|
||||||
<div className="flex items-center justify-between gap-2 flex-wrap mb-4">
|
<div className="flex items-center justify-between gap-2 flex-wrap mb-4">
|
||||||
<span className="text-sm text-[var(--color-text-muted)]">{users.length} пользователей</span>
|
<span className="text-sm text-[var(--color-text-muted)]">{users.length} пользователей</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => { setShowAddForm(!showAddForm); setAddError(null); }}
|
onClick={() => { setShowAddForm(true); setAddError(null); }}
|
||||||
className="rounded-full bg-[var(--color-accent)] px-4 py-2 text-sm font-semibold text-white hover:opacity-90 transition"
|
className="rounded-full bg-[var(--color-accent)] px-4 py-2 text-sm font-semibold text-white hover:opacity-90 transition"
|
||||||
>
|
>
|
||||||
{showAddForm ? 'Отмена' : '+ Добавить'}
|
+ Добавить
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Add user modal */}
|
{/* Add user modal */}
|
||||||
{showAddForm && (
|
{showAddForm && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm" onClick={() => { setShowAddForm(false); setAddError(null); }}>
|
<AddUserModal
|
||||||
<div className="w-[340px] rounded-[28px] border border-[var(--color-border)] bg-[var(--color-surface)] p-6 shadow-soft" onClick={(e) => e.stopPropagation()}>
|
onSubmit={handleAddUser}
|
||||||
<h3 className="mb-4 text-base font-semibold">Новый пользователь</h3>
|
onClose={() => { setShowAddForm(false); setAddError(null); }}
|
||||||
<form onSubmit={handleAddUser} className="space-y-3">
|
submitting={addSubmitting}
|
||||||
<Input
|
error={addError}
|
||||||
placeholder="Имя"
|
|
||||||
value={addForm.name}
|
|
||||||
onChange={(e) => setAddForm((f) => ({ ...f, name: e.target.value }))}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
|
||||||
placeholder="Email"
|
|
||||||
type="email"
|
|
||||||
value={addForm.email}
|
|
||||||
onChange={(e) => setAddForm((f) => ({ ...f, email: e.target.value }))}
|
|
||||||
/>
|
|
||||||
<RoleDropdown
|
|
||||||
value={addForm.role || 'manager'}
|
|
||||||
onChange={(r) => setAddForm((f) => ({ ...f, role: r }))}
|
|
||||||
/>
|
|
||||||
{addError && <div className="text-sm text-[var(--color-danger)]">{addError}</div>}
|
|
||||||
<div className="flex justify-end gap-2 pt-1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => { setShowAddForm(false); setAddError(null); }}
|
|
||||||
className="rounded-full border border-[var(--color-border)] px-4 py-2 text-sm font-semibold text-[var(--color-text-muted)] hover:bg-[var(--color-surface-strong)] transition"
|
|
||||||
>
|
|
||||||
Отмена
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={addSubmitting}
|
|
||||||
className="rounded-full bg-[var(--color-accent)] px-4 py-2 text-sm font-semibold text-white hover:opacity-90 transition disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{addSubmitting ? 'Добавление…' : 'Добавить'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Error */}
|
{/* Error */}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue