feat: use modals for creation, side panels for editing
All checks were successful
Deploy / deploy (push) Successful in 11s

- Team page: add member via modal with password confirmation,
  keep SlidePanel for editing existing members only
- Settings: add role via modal with color picker presets,
  keep inline editing for existing roles
- Remove create-mode code from TeamMemberPanel
- Add i18n keys: confirmPassword, passwordsDoNotMatch, memberAdded,
  roleColor (en + ar)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
fahed
2026-03-04 16:37:37 +03:00
parent da161014af
commit 959bd6066d
5 changed files with 362 additions and 151 deletions

View File

@@ -24,8 +24,6 @@ export default function TeamMemberPanel({ member, isEditingSelf, onClose, onSave
const [saving, setSaving] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [showBrandsDropdown, setShowBrandsDropdown] = useState(false)
const [confirmPassword, setConfirmPassword] = useState('')
const [passwordError, setPasswordError] = useState('')
const brandsDropdownRef = useRef(null)
// Workload state (loaded internally)
@@ -34,7 +32,6 @@ export default function TeamMemberPanel({ member, isEditingSelf, onClose, onSave
const [loadingWorkload, setLoadingWorkload] = useState(false)
const memberId = member?._id || member?.id
const isCreateMode = !memberId
useEffect(() => {
if (member) {
@@ -49,10 +46,8 @@ export default function TeamMemberPanel({ member, isEditingSelf, onClose, onSave
modules: Array.isArray(member.modules) ? member.modules : ALL_MODULES,
team_ids: Array.isArray(member.teams) ? member.teams.map(t => t.id) : [],
})
setDirty(isCreateMode)
setConfirmPassword('')
setPasswordError('')
if (!isCreateMode) loadWorkload()
setDirty(false)
if (memberId) loadWorkload()
}
}, [member])
@@ -101,14 +96,9 @@ export default function TeamMemberPanel({ member, isEditingSelf, onClose, onSave
}
const handleSave = async () => {
setPasswordError('')
if (isCreateMode && form.password && form.password !== confirmPassword) {
setPasswordError('Passwords do not match')
return
}
setSaving(true)
try {
await onSave(isCreateMode ? null : memberId, {
await onSave(memberId, {
name: form.name,
email: form.email,
password: form.password,
@@ -120,7 +110,6 @@ export default function TeamMemberPanel({ member, isEditingSelf, onClose, onSave
team_ids: form.team_ids,
}, isEditingSelf)
setDirty(false)
if (isCreateMode) onClose()
} finally {
setSaving(false)
}
@@ -176,51 +165,15 @@ export default function TeamMemberPanel({ member, isEditingSelf, onClose, onSave
<CollapsibleSection title={t('team.details')}>
<div className="px-5 pb-4 space-y-3">
{!isEditingSelf && (
<>
<div>
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('team.email')} *</label>
<input
type="email"
value={form.email}
onChange={e => update('email', e.target.value)}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
placeholder="email@example.com"
disabled={!isCreateMode}
/>
</div>
{isCreateMode && (
<div>
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('team.password')}</label>
<input
type="password"
value={form.password}
onChange={e => update('password', e.target.value)}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
placeholder="••••••••"
/>
{!form.password && (
<p className="text-xs text-text-tertiary mt-1">{t('team.defaultPassword')}</p>
)}
</div>
)}
{isCreateMode && form.password && (
<div>
<label className="block text-xs font-medium text-text-tertiary mb-1">Confirm Password</label>
<input
type="password"
value={confirmPassword}
onChange={e => { setConfirmPassword(e.target.value); setPasswordError('') }}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
placeholder="••••••••"
/>
{passwordError && (
<p className="text-xs text-red-500 mt-1">{passwordError}</p>
)}
</div>
)}
</>
<div>
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('team.email')}</label>
<input
type="email"
value={form.email}
disabled
className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-surface-tertiary text-text-tertiary cursor-not-allowed"
/>
</div>
)}
<div className="grid grid-cols-2 gap-3">
@@ -405,13 +358,13 @@ export default function TeamMemberPanel({ member, isEditingSelf, onClose, onSave
{dirty && (
<button
onClick={handleSave}
disabled={!form.name || (!isEditingSelf && isCreateMode && !form.email) || saving}
disabled={!form.name || saving}
className={`flex-1 px-4 py-2 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light disabled:opacity-50 disabled:cursor-not-allowed shadow-sm ${saving ? 'btn-loading' : ''}`}
>
{isEditingSelf ? t('team.saveProfile') : (isCreateMode ? t('team.addMember') : t('team.saveChanges'))}
{isEditingSelf ? t('team.saveProfile') : t('team.saveChanges')}
</button>
)}
{!isCreateMode && !isEditingSelf && canManageTeam && onDelete && (
{!isEditingSelf && canManageTeam && onDelete && (
<button
onClick={() => setShowDeleteConfirm(true)}
className="p-2 text-text-tertiary hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
@@ -424,9 +377,8 @@ export default function TeamMemberPanel({ member, isEditingSelf, onClose, onSave
</div>
</CollapsibleSection>
{/* Workload Section (hidden in create mode) */}
{!isCreateMode && (
<CollapsibleSection title={t('team.workload')} noBorder>
{/* Workload Section */}
<CollapsibleSection title={t('team.workload')} noBorder>
<div className="px-5 pb-4 space-y-3">
{/* Stats */}
<div className="grid grid-cols-4 gap-2">
@@ -485,7 +437,6 @@ export default function TeamMemberPanel({ member, isEditingSelf, onClose, onSave
)}
</div>
</CollapsibleSection>
)}
</SlidePanel>
<Modal

View File

@@ -209,6 +209,7 @@
"team.title": "الفريق",
"team.members": "أعضاء الفريق",
"team.addMember": "إضافة عضو",
"team.memberAdded": "تمت إضافة العضو بنجاح",
"team.newMember": "عضو جديد",
"team.editMember": "تعديل العضو",
"team.myProfile": "ملفي الشخصي",
@@ -231,6 +232,8 @@
"team.membersPlural": "أعضاء فريق",
"team.fullName": "الاسم الكامل",
"team.defaultPassword": "افتراضياً: changeme123",
"team.confirmPassword": "تأكيد كلمة المرور",
"team.passwordsDoNotMatch": "كلمتا المرور غير متطابقتين",
"team.optional": "(اختياري)",
"team.fixedRole": "دور ثابت للمديرين",
"team.remove": "إزالة",
@@ -673,6 +676,7 @@
"settings.rolesDesc": "حدد أدوار العمل مثل مصمم، استراتيجي، إلخ. يتم تعيينها لأعضاء الفريق بشكل منفصل عن مستويات الصلاحية.",
"settings.addRole": "إضافة دور",
"settings.roleName": "اسم الدور",
"settings.roleColor": "اللون",
"settings.deleteRoleConfirm": "هل أنت متأكد من حذف هذا الدور؟",
"settings.noRoles": "لم يتم تحديد أدوار بعد. أضف أول دور."
}

View File

@@ -209,6 +209,7 @@
"team.title": "Team",
"team.members": "Team Members",
"team.addMember": "Add Member",
"team.memberAdded": "Member added successfully",
"team.newMember": "New Team Member",
"team.editMember": "Edit Team Member",
"team.myProfile": "My Profile",
@@ -231,6 +232,8 @@
"team.membersPlural": "team members",
"team.fullName": "Full name",
"team.defaultPassword": "Default: changeme123",
"team.confirmPassword": "Confirm Password",
"team.passwordsDoNotMatch": "Passwords do not match",
"team.optional": "(optional)",
"team.fixedRole": "Fixed role for managers",
"team.remove": "Remove",
@@ -673,6 +676,7 @@
"settings.rolesDesc": "Define job roles like Designer, Strategist, etc. These are assigned to team members separately from permission levels.",
"settings.addRole": "Add Role",
"settings.roleName": "Role name",
"settings.roleColor": "Color",
"settings.deleteRoleConfirm": "Are you sure you want to delete this role?",
"settings.noRoles": "No roles defined yet. Add your first role."
}

View File

@@ -6,6 +6,7 @@ import { useToast } from '../components/ToastContainer'
import { CURRENCIES } from '../i18n/LanguageContext'
import { AppContext } from '../App'
import { useAuth } from '../contexts/AuthContext'
import Modal from '../components/Modal'
const ROLE_COLORS = [
'#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6',
@@ -194,20 +195,34 @@ export default function Settings() {
function RolesSection({ roles, loadRoles, t, toast }) {
const [editingRole, setEditingRole] = useState(null)
const [newRole, setNewRole] = useState(null)
const [showAddModal, setShowAddModal] = useState(false)
const [modalForm, setModalForm] = useState({ name: '', color: ROLE_COLORS[0] })
const [saving, setSaving] = useState(false)
const openAddModal = () => {
setModalForm({ name: '', color: ROLE_COLORS[roles.length % ROLE_COLORS.length] })
setShowAddModal(true)
}
const handleCreate = async () => {
setSaving(true)
try {
await api.post('/roles', { name: modalForm.name, color: modalForm.color })
await loadRoles()
setShowAddModal(false)
} catch (err) {
toast.error(err.message || t('common.error'))
} finally {
setSaving(false)
}
}
const handleSave = async (role) => {
setSaving(true)
try {
if (role.Id || role.id) {
await api.patch(`/roles/${role.Id || role.id}`, { name: role.name, color: role.color })
} else {
await api.post('/roles', { name: role.name, color: role.color })
}
await api.patch(`/roles/${role.Id || role.id}`, { name: role.name, color: role.color })
await loadRoles()
setEditingRole(null)
setNewRole(null)
} catch (err) {
toast.error(err.message || t('common.error'))
} finally {
@@ -226,78 +241,90 @@ function RolesSection({ roles, loadRoles, t, toast }) {
}
return (
<div className="bg-white dark:bg-surface-primary rounded-xl border border-border overflow-hidden">
<div className="px-6 py-4 border-b border-border flex items-center justify-between">
<h2 className="text-lg font-semibold text-text-primary flex items-center gap-2">
<Tag className="w-5 h-5 text-brand-primary" />
{t('settings.roles')}
</h2>
<button
onClick={() => setNewRole({ name: '', color: ROLE_COLORS[roles.length % ROLE_COLORS.length] })}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium bg-brand-primary text-white rounded-lg hover:bg-brand-primary-light transition-colors"
>
<Plus className="w-4 h-4" />
{t('settings.addRole')}
</button>
</div>
<div className="p-6">
<p className="text-sm text-text-tertiary mb-4">{t('settings.rolesDesc')}</p>
<div className="space-y-2">
{roles.map(role => (
<div key={role.Id || role.id} className="flex items-center gap-3 p-3 rounded-lg border border-border hover:bg-surface-secondary transition-colors">
{editingRole?.Id === role.Id ? (
<RoleForm role={editingRole} onChange={setEditingRole} onSave={() => handleSave(editingRole)} onCancel={() => setEditingRole(null)} saving={saving} t={t} />
) : (
<>
<div className="w-4 h-4 rounded-full shrink-0" style={{ backgroundColor: role.color || '#94A3B8' }} />
<span className="flex-1 text-sm font-medium text-text-primary">{role.name}</span>
<button onClick={() => setEditingRole({ ...role })} className="p-1.5 text-text-tertiary hover:text-brand-primary rounded-lg hover:bg-surface-tertiary transition-colors">
<Pencil className="w-4 h-4" />
</button>
<button onClick={() => handleDelete(role)} className="p-1.5 text-text-tertiary hover:text-red-500 rounded-lg hover:bg-red-50 transition-colors">
<Trash2 className="w-4 h-4" />
</button>
</>
)}
</div>
))}
{newRole && (
<div className="p-3 rounded-lg border-2 border-dashed border-brand-primary/30 bg-brand-primary/5">
<RoleForm role={newRole} onChange={setNewRole} onSave={() => handleSave(newRole)} onCancel={() => setNewRole(null)} saving={saving} t={t} />
</div>
)}
{roles.length === 0 && !newRole && (
<p className="text-sm text-text-tertiary text-center py-6">{t('settings.noRoles')}</p>
)}
<>
<div className="bg-white dark:bg-surface-primary rounded-xl border border-border overflow-hidden">
<div className="px-6 py-4 border-b border-border flex items-center justify-between">
<h2 className="text-lg font-semibold text-text-primary flex items-center gap-2">
<Tag className="w-5 h-5 text-brand-primary" />
{t('settings.roles')}
</h2>
<button
onClick={openAddModal}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium bg-brand-primary text-white rounded-lg hover:bg-brand-primary-light transition-colors"
>
<Plus className="w-4 h-4" />
{t('settings.addRole')}
</button>
</div>
<div className="p-6">
<p className="text-sm text-text-tertiary mb-4">{t('settings.rolesDesc')}</p>
<div className="space-y-2">
{roles.map(role => (
<div key={role.Id || role.id} className="flex items-center gap-3 p-3 rounded-lg border border-border hover:bg-surface-secondary transition-colors">
{editingRole && (editingRole.Id || editingRole.id) === (role.Id || role.id) ? (
<div className="flex items-center gap-3 flex-1">
<input type="color" value={editingRole.color || '#94A3B8'} onChange={e => setEditingRole({ ...editingRole, color: e.target.value })}
className="w-8 h-8 rounded-lg border border-border cursor-pointer" />
<input type="text" value={editingRole.name} onChange={e => setEditingRole({ ...editingRole, name: e.target.value })}
placeholder={t('settings.roleName')} autoFocus
className="flex-1 px-3 py-1.5 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary" />
<button onClick={() => handleSave(editingRole)} disabled={!editingRole.name || saving}
className="px-3 py-1.5 text-sm font-medium bg-brand-primary text-white rounded-lg hover:bg-brand-primary-light disabled:opacity-50 transition-colors">
{saving ? '...' : t('common.save')}
</button>
<button onClick={() => setEditingRole(null)} className="p-1.5 text-text-tertiary hover:text-text-primary rounded-lg hover:bg-surface-tertiary transition-colors">
<X className="w-4 h-4" />
</button>
</div>
) : (
<>
<div className="w-4 h-4 rounded-full shrink-0" style={{ backgroundColor: role.color || '#94A3B8' }} />
<span className="flex-1 text-sm font-medium text-text-primary">{role.name}</span>
<button onClick={() => setEditingRole({ ...role })} className="p-1.5 text-text-tertiary hover:text-brand-primary rounded-lg hover:bg-surface-tertiary transition-colors">
<Pencil className="w-4 h-4" />
</button>
<button onClick={() => handleDelete(role)} className="p-1.5 text-text-tertiary hover:text-red-500 rounded-lg hover:bg-red-50 transition-colors">
<Trash2 className="w-4 h-4" />
</button>
</>
)}
</div>
))}
{roles.length === 0 && (
<p className="text-sm text-text-tertiary text-center py-6">{t('settings.noRoles')}</p>
)}
</div>
</div>
</div>
</div>
)
}
function RoleForm({ role, onChange, onSave, onCancel, saving, t }) {
return (
<div className="flex items-center gap-3 flex-1">
<input
type="color"
value={role.color || '#94A3B8'}
onChange={e => onChange({ ...role, color: e.target.value })}
className="w-8 h-8 rounded-lg border border-border cursor-pointer"
/>
<input
type="text"
value={role.name}
onChange={e => onChange({ ...role, name: e.target.value })}
placeholder={t('settings.roleName')}
className="flex-1 px-3 py-1.5 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
autoFocus
/>
<button onClick={onSave} disabled={!role.name || saving} className="px-3 py-1.5 text-sm font-medium bg-brand-primary text-white rounded-lg hover:bg-brand-primary-light disabled:opacity-50 transition-colors">
{saving ? '...' : t('common.save')}
</button>
<button onClick={onCancel} className="p-1.5 text-text-tertiary hover:text-text-primary rounded-lg hover:bg-surface-tertiary transition-colors">
<X className="w-4 h-4" />
</button>
</div>
<Modal isOpen={showAddModal} onClose={() => setShowAddModal(false)} title={t('settings.addRole')} size="sm">
<div className="space-y-4">
<div>
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('settings.roleName')}</label>
<input type="text" value={modalForm.name} onChange={e => setModalForm(f => ({ ...f, name: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
placeholder={t('settings.roleName')} autoFocus />
</div>
<div>
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('settings.roleColor') || 'Color'}</label>
<div className="flex items-center gap-3">
<input type="color" value={modalForm.color} onChange={e => setModalForm(f => ({ ...f, color: e.target.value }))}
className="w-10 h-10 rounded-lg border border-border cursor-pointer" />
<div className="flex flex-wrap gap-1.5">
{ROLE_COLORS.map(c => (
<button key={c} type="button" onClick={() => setModalForm(f => ({ ...f, color: c }))}
className={`w-6 h-6 rounded-full border-2 transition-colors ${modalForm.color === c ? 'border-text-primary scale-110' : 'border-transparent'}`}
style={{ backgroundColor: c }} />
))}
</div>
</div>
</div>
<button onClick={handleCreate} disabled={!modalForm.name || saving}
className={`w-full px-4 py-2.5 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light disabled:opacity-50 disabled:cursor-not-allowed shadow-sm ${saving ? 'btn-loading' : ''}`}>
{t('settings.addRole')}
</button>
</div>
</Modal>
</>
)
}

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useContext } from 'react'
import { Plus, Users, ArrowLeft, User as UserIcon, Edit2, LayoutGrid, Network, Link2 } from 'lucide-react'
import { useState, useEffect, useContext, useRef } from 'react'
import { Plus, Users, ArrowLeft, User as UserIcon, Edit2, LayoutGrid, Network, Link2, ChevronDown, Check, X } from 'lucide-react'
import { getInitials } from '../utils/api'
import { AppContext } from '../App'
import { AppContext, PERMISSION_LEVELS } from '../App'
import { useAuth } from '../contexts/AuthContext'
import { useLanguage } from '../i18n/LanguageContext'
import { api } from '../utils/api'
@@ -10,12 +10,26 @@ import StatusBadge from '../components/StatusBadge'
import BrandBadge from '../components/BrandBadge'
import TeamMemberPanel from '../components/TeamMemberPanel'
import TeamPanel from '../components/TeamPanel'
import Modal from '../components/Modal'
import { useToast } from '../components/ToastContainer'
const ALL_MODULES = ['marketing', 'projects', 'finance']
const MODULE_LABELS = { marketing: 'Marketing', projects: 'Projects', finance: 'Finance' }
const MODULE_COLORS = {
marketing: { on: 'bg-emerald-100 text-emerald-700 border-emerald-300', off: 'bg-gray-100 text-gray-400 border-gray-200' },
projects: { on: 'bg-blue-100 text-blue-700 border-blue-300', off: 'bg-gray-100 text-gray-400 border-gray-200' },
finance: { on: 'bg-amber-100 text-amber-700 border-amber-300', off: 'bg-gray-100 text-gray-400 border-gray-200' },
}
const EMPTY_MEMBER = {
name: '', email: '', password: '', permission_level: 'contributor',
role_id: '', brands: [], phone: '', modules: [...ALL_MODULES], team_ids: [],
}
export default function Team() {
const { t } = useLanguage()
const { t, lang } = useLanguage()
const toast = useToast()
const { teamMembers, loadTeam, currentUser, teams, loadTeams, brands } = useContext(AppContext)
const { teamMembers, loadTeam, currentUser, teams, loadTeams, brands, roles } = useContext(AppContext)
const { user } = useAuth()
const [panelMember, setPanelMember] = useState(null)
const [panelIsEditingSelf, setPanelIsEditingSelf] = useState(false)
@@ -27,6 +41,15 @@ export default function Team() {
const [teamFilter, setTeamFilter] = useState(null)
const [viewMode, setViewMode] = useState('grid') // 'grid' | 'teams'
// Add member modal state
const [showAddModal, setShowAddModal] = useState(false)
const [addForm, setAddForm] = useState({ ...EMPTY_MEMBER })
const [addConfirmPassword, setAddConfirmPassword] = useState('')
const [addPasswordError, setAddPasswordError] = useState('')
const [addSaving, setAddSaving] = useState(false)
const [showAddBrandsDropdown, setShowAddBrandsDropdown] = useState(false)
const addBrandsRef = useRef(null)
const canManageTeam = user?.role === 'superadmin' || user?.role === 'manager'
const copyIssueLink = (teamId) => {
@@ -36,9 +59,68 @@ export default function Team() {
toast.success(t('issues.linkCopied'))
}
// Close brands dropdown on outside click
useEffect(() => {
const handler = (e) => {
if (addBrandsRef.current && !addBrandsRef.current.contains(e.target)) setShowAddBrandsDropdown(false)
}
if (showAddBrandsDropdown) document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [showAddBrandsDropdown])
const openNew = () => {
setPanelMember({ role: 'content_writer' })
setPanelIsEditingSelf(false)
setAddForm({ ...EMPTY_MEMBER })
setAddConfirmPassword('')
setAddPasswordError('')
setShowAddModal(true)
}
const handleAddMember = async () => {
setAddPasswordError('')
if (addForm.password && addForm.password !== addConfirmPassword) {
setAddPasswordError(t('team.passwordsDoNotMatch'))
return
}
setAddSaving(true)
try {
const payload = {
name: addForm.name,
email: addForm.email,
role: addForm.permission_level,
role_id: addForm.role_id || null,
brands: addForm.brands,
phone: addForm.phone,
modules: addForm.modules,
}
if (addForm.password) payload.password = addForm.password
const created = await api.post('/users/team', payload)
const memberId = created?.id || created?.Id
// Sync team memberships
if (addForm.team_ids.length > 0 && memberId) {
for (const teamId of addForm.team_ids) {
await api.post(`/teams/${teamId}/members`, { user_id: memberId })
}
}
await loadTeam()
await loadTeams()
setShowAddModal(false)
toast.success(t('team.memberAdded') || 'Member added')
} catch (err) {
console.error('Add member failed:', err)
toast.error(err.message || t('common.failedToSave'))
} finally {
setAddSaving(false)
}
}
const updateAdd = (field, value) => setAddForm(f => ({ ...f, [field]: value }))
const toggleAddBrand = (name) => {
setAddForm(f => ({
...f,
brands: f.brands.includes(name) ? f.brands.filter(b => b !== name) : [...f.brands, name],
}))
}
const openEdit = (member) => {
@@ -562,7 +644,150 @@ export default function Team() {
</div>
)}
{/* Team Member Panel */}
{/* Add Member Modal */}
<Modal isOpen={showAddModal} onClose={() => setShowAddModal(false)} title={t('team.addMember')} size="md">
<div className="space-y-4">
<div>
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('team.fullName')} *</label>
<input type="text" value={addForm.name} onChange={e => updateAdd('name', e.target.value)}
className="w-full px-3 py-2 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.fullName')} />
</div>
<div>
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('team.email')} *</label>
<input type="email" value={addForm.email} onChange={e => updateAdd('email', e.target.value)}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary" placeholder="email@example.com" />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('team.password')}</label>
<input type="password" value={addForm.password} onChange={e => updateAdd('password', e.target.value)}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary" placeholder="••••••••" />
{!addForm.password && <p className="text-xs text-text-tertiary mt-1">{t('team.defaultPassword')}</p>}
</div>
<div>
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('team.confirmPassword')}</label>
<input type="password" value={addConfirmPassword}
onChange={e => { setAddConfirmPassword(e.target.value); setAddPasswordError('') }}
disabled={!addForm.password}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary disabled:opacity-50" placeholder="••••••••" />
{addPasswordError && <p className="text-xs text-red-500 mt-1">{addPasswordError}</p>}
</div>
</div>
<div className="grid grid-cols-2 gap-3">
{user?.role === 'superadmin' && (
<div>
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('team.permissionLevel')}</label>
<select value={addForm.permission_level} onChange={e => updateAdd('permission_level', e.target.value)}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary">
{PERMISSION_LEVELS.map(p => <option key={p.value} value={p.value}>{p.label}</option>)}
</select>
</div>
)}
<div>
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('team.role')}</label>
<select value={addForm.role_id || ''} onChange={e => updateAdd('role_id', e.target.value ? Number(e.target.value) : null)}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary">
<option value="">{t('team.selectRole')}</option>
{roles.map(r => <option key={r.Id || r.id} value={r.Id || r.id}>{r.name}</option>)}
</select>
</div>
</div>
<div>
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('team.phone')}</label>
<input type="text" value={addForm.phone} onChange={e => updateAdd('phone', e.target.value)}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary" placeholder="+966 ..." />
</div>
{/* Brands multi-select */}
<div ref={addBrandsRef} className="relative">
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('team.brands')}</label>
<button type="button" onClick={() => setShowAddBrandsDropdown(p => !p)}
className="w-full flex items-center justify-between px-3 py-2 text-sm border border-border rounded-lg bg-white text-left focus:outline-none focus:ring-2 focus:ring-brand-primary/20">
<span className={`flex-1 truncate ${addForm.brands.length === 0 ? 'text-text-tertiary' : 'text-text-primary'}`}>
{addForm.brands.length === 0 ? t('team.selectBrands') : addForm.brands.join(', ')}
</span>
<ChevronDown className={`w-4 h-4 text-text-tertiary shrink-0 transition-transform ${showAddBrandsDropdown ? 'rotate-180' : ''}`} />
</button>
{addForm.brands.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1.5">
{addForm.brands.map(b => (
<span key={b} className="inline-flex items-center gap-1 text-[10px] px-2 py-0.5 rounded-full bg-brand-primary/10 text-brand-primary font-medium">
{b}
<button type="button" onClick={() => toggleAddBrand(b)} className="hover:text-red-500"><X className="w-2.5 h-2.5" /></button>
</span>
))}
</div>
)}
{showAddBrandsDropdown && (
<div className="absolute z-20 mt-1 w-full bg-white border border-border rounded-lg shadow-lg max-h-48 overflow-y-auto">
{brands.map(brand => {
const name = lang === 'ar' && brand.name_ar ? brand.name_ar : brand.name
const checked = addForm.brands.includes(name)
return (
<button key={brand.id || brand._id} type="button" onClick={() => toggleAddBrand(name)}
className={`w-full flex items-center gap-2.5 px-3 py-2 hover:bg-surface-secondary transition-colors text-left ${checked ? 'bg-brand-primary/5' : ''}`}>
<div className={`w-4 h-4 rounded border flex items-center justify-center shrink-0 ${checked ? 'bg-brand-primary border-brand-primary' : 'border-border'}`}>
{checked && <Check className="w-3 h-3 text-white" />}
</div>
<span className="text-sm text-text-primary">{brand.icon ? `${brand.icon} ` : ''}{name}</span>
</button>
)
})}
</div>
)}
</div>
{/* Modules */}
<div>
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('team.modules')}</label>
<div className="flex flex-wrap gap-2">
{ALL_MODULES.map(mod => {
const active = addForm.modules.includes(mod)
const colors = MODULE_COLORS[mod]
return (
<button key={mod} type="button"
onClick={() => updateAdd('modules', active ? addForm.modules.filter(m => m !== mod) : [...addForm.modules, mod])}
className={`text-xs px-3 py-1.5 rounded-full border font-medium transition-colors ${active ? colors.on : colors.off}`}>
{MODULE_LABELS[mod]}
</button>
)
})}
</div>
</div>
{/* Teams */}
{teams.length > 0 && (
<div>
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('teams.teams')}</label>
<div className="flex flex-wrap gap-2">
{teams.map(team => {
const tid = team.id || team._id
const active = addForm.team_ids.includes(tid)
return (
<button key={tid} type="button"
onClick={() => updateAdd('team_ids', active ? addForm.team_ids.filter(id => id !== tid) : [...addForm.team_ids, tid])}
className={`text-xs px-3 py-1.5 rounded-full border font-medium transition-colors ${active ? 'bg-blue-100 text-blue-700 border-blue-300' : 'bg-gray-100 text-gray-400 border-gray-200'}`}>
{team.name}
</button>
)
})}
</div>
</div>
)}
<button onClick={handleAddMember}
disabled={!addForm.name || !addForm.email || addSaving}
className={`w-full px-4 py-2.5 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light disabled:opacity-50 disabled:cursor-not-allowed shadow-sm ${addSaving ? 'btn-loading' : ''}`}>
{t('team.addMember')}
</button>
</div>
</Modal>
{/* Team Member Panel (edit only) */}
{panelMember && (
<TeamMemberPanel
member={panelMember}