From ad539fd7f4631d65fb2b872efd83fb693b66c3a3 Mon Sep 17 00:00:00 2001 From: fahed Date: Wed, 4 Mar 2026 23:30:03 +0300 Subject: [PATCH] feat: admin password change with confirmation in team panel Add "Admin Actions" section (superadmin-only, collapsed by default) with password + confirm fields, eye toggle, mismatch validation, and success toast. Delete button moved here too. Co-Authored-By: Claude Opus 4.6 --- client/src/components/TeamMemberPanel.jsx | 121 ++++++++++++++++++---- client/src/i18n/ar.json | 4 + client/src/i18n/en.json | 4 + 3 files changed, 107 insertions(+), 22 deletions(-) diff --git a/client/src/components/TeamMemberPanel.jsx b/client/src/components/TeamMemberPanel.jsx index b6866da..f7942e6 100644 --- a/client/src/components/TeamMemberPanel.jsx +++ b/client/src/components/TeamMemberPanel.jsx @@ -1,7 +1,8 @@ import { useState, useEffect, useRef, useContext } from 'react' -import { X, Trash2, ChevronDown, Check } from 'lucide-react' +import { X, Trash2, ChevronDown, Check, ShieldAlert, Eye, EyeOff } from 'lucide-react' import { useLanguage } from '../i18n/LanguageContext' import { api } from '../utils/api' +import { useToast } from './ToastContainer' import Modal from './Modal' import SlidePanel from './SlidePanel' import CollapsibleSection from './CollapsibleSection' @@ -18,12 +19,16 @@ const MODULE_COLORS = { export default function TeamMemberPanel({ member, isEditingSelf, onClose, onSave, onDelete, canManageTeam, userRole, teams, brands: brandsList }) { const { t, lang } = useLanguage() + const toast = useToast() const { roles } = useContext(AppContext) const [form, setForm] = useState({}) const [dirty, setDirty] = useState(false) const [saving, setSaving] = useState(false) const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) const [showBrandsDropdown, setShowBrandsDropdown] = useState(false) + const [confirmPassword, setConfirmPassword] = useState('') + const [showPassword, setShowPassword] = useState(false) + const [passwordSaving, setPasswordSaving] = useState(false) const brandsDropdownRef = useRef(null) // Workload state (loaded internally) @@ -47,6 +52,8 @@ export default function TeamMemberPanel({ member, isEditingSelf, onClose, onSave team_ids: Array.isArray(member.teams) ? member.teams.map(t => t.id) : [], }) setDirty(false) + setConfirmPassword('') + setShowPassword(false) if (memberId) loadWorkload() } }, [member]) @@ -101,7 +108,6 @@ export default function TeamMemberPanel({ member, isEditingSelf, onClose, onSave await onSave(memberId, { name: form.name, email: form.email, - password: form.password, role: form.permission_level, role_id: form.role_id || null, brands: form.brands || [], @@ -115,6 +121,22 @@ export default function TeamMemberPanel({ member, isEditingSelf, onClose, onSave } } + const handlePasswordChange = async () => { + if (!form.password || form.password !== confirmPassword) return + setPasswordSaving(true) + try { + await onSave(memberId, { password: form.password }, false) + setForm(f => ({ ...f, password: '' })) + setConfirmPassword('') + setShowPassword(false) + toast.success(t('team.passwordChanged')) + } finally { + setPasswordSaving(false) + } + } + + const passwordMismatch = confirmPassword && form.password !== confirmPassword + const confirmDelete = async () => { setShowDeleteConfirm(false) await onDelete(memberId) @@ -354,26 +376,15 @@ export default function TeamMemberPanel({ member, isEditingSelf, onClose, onSave )} -
- {dirty && ( - - )} - {!isEditingSelf && canManageTeam && onDelete && ( - - )} -
+ {dirty && ( + + )} @@ -437,6 +448,72 @@ export default function TeamMemberPanel({ member, isEditingSelf, onClose, onSave )} + + {/* Admin Actions Section (superadmin only, not self) */} + {!isEditingSelf && userRole === 'superadmin' && ( + {t('team.adminActions')}} + defaultOpen={false} + noBorder + > +
+ {/* Change password */} +
+ +
+ update('password', e.target.value)} + className="w-full px-3 py-2 pe-9 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary" + placeholder={t('team.newPassword')} + autoComplete="new-password" + /> + +
+
+
+ + setConfirmPassword(e.target.value)} + className={`w-full px-3 py-2 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary ${passwordMismatch ? 'border-red-400' : 'border-border'}`} + placeholder={t('team.confirmPassword')} + autoComplete="new-password" + /> + {passwordMismatch && ( +

{t('team.passwordsDoNotMatch')}

+ )} +
+ + + {/* Delete member */} + {canManageTeam && onDelete && ( + + )} +
+
+ )}