feat: use modals for creation, side panels for editing
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
+104 -77
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>
</>
)
}