feat: team-based visibility, roles management, unified users, UI fixes
Deploy / deploy (push) Successful in 12s
Deploy / deploy (push) Successful in 12s
- Add Roles table with CRUD routes and Settings page management - Unify user management: remove Users page, enhance Team page with permission level + role dropdowns - Add team-based visibility scoping to projects, campaigns, posts, tasks, issues, artefacts, and dashboard - Add team_id to projects and campaigns (create + edit forms) - Add getUserTeamIds/getUserVisibilityContext helpers - Fix Budgets modal horizontal scroll (separate linked-to row) - Add collapsible filter bar to PostProduction page Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,22 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Settings as SettingsIcon, Play, CheckCircle, Languages, Coins, Upload } from 'lucide-react'
|
||||
import { useState, useEffect, useContext } from 'react'
|
||||
import { Settings as SettingsIcon, Play, CheckCircle, Languages, Coins, Upload, Tag, Plus, Pencil, Trash2, X } from 'lucide-react'
|
||||
import { api } from '../utils/api'
|
||||
import { useLanguage } from '../i18n/LanguageContext'
|
||||
import { useToast } from '../components/ToastContainer'
|
||||
import { CURRENCIES } from '../i18n/LanguageContext'
|
||||
import { AppContext } from '../App'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
|
||||
const ROLE_COLORS = [
|
||||
'#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6',
|
||||
'#EC4899', '#06B6D4', '#F97316', '#6366F1', '#14B8A6',
|
||||
]
|
||||
|
||||
export default function Settings() {
|
||||
const { t, lang, setLang, currency, setCurrency } = useLanguage()
|
||||
const toast = useToast()
|
||||
const { user } = useAuth()
|
||||
const { roles, loadRoles } = useContext(AppContext)
|
||||
const [restarting, setRestarting] = useState(false)
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [maxSizeMB, setMaxSizeMB] = useState(50)
|
||||
@@ -176,6 +185,119 @@ export default function Settings() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Roles Management (Superadmin only) */}
|
||||
{user?.role === 'superadmin' && <RolesSection roles={roles} loadRoles={loadRoles} t={t} toast={toast} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RolesSection({ roles, loadRoles, t, toast }) {
|
||||
const [editingRole, setEditingRole] = useState(null)
|
||||
const [newRole, setNewRole] = useState(null)
|
||||
const [saving, setSaving] = useState(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 loadRoles()
|
||||
setEditingRole(null)
|
||||
setNewRole(null)
|
||||
} catch (err) {
|
||||
toast.error(err.message || t('common.error'))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (role) => {
|
||||
if (!confirm(t('settings.deleteRoleConfirm'))) return
|
||||
try {
|
||||
await api.delete(`/roles/${role.Id || role.id}`)
|
||||
await loadRoles()
|
||||
} catch (err) {
|
||||
toast.error(err.message || t('common.error'))
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user