- 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:
+104
-77
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user