feat: team-based visibility, roles management, unified users, UI fixes
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:
fahed
2026-03-04 15:55:15 +03:00
parent 7c6e8dce08
commit da161014af
14 changed files with 655 additions and 308 deletions
+18 -1
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useContext } from 'react'
import { X, Trash2, DollarSign, Eye, MousePointer, Target } from 'lucide-react'
import { useLanguage } from '../i18n/LanguageContext'
import { PLATFORMS, getBrandColor } from '../utils/api'
@@ -7,9 +7,11 @@ import Modal from './Modal'
import SlidePanel from './SlidePanel'
import CollapsibleSection from './CollapsibleSection'
import BudgetBar from './BudgetBar'
import { AppContext } from '../App'
export default function CampaignDetailPanel({ campaign, onClose, onSave, onDelete, brands, permissions }) {
const { t, lang, currencySymbol } = useLanguage()
const { teams } = useContext(AppContext)
const [form, setForm] = useState({})
const [dirty, setDirty] = useState(false)
const [saving, setSaving] = useState(false)
@@ -24,6 +26,7 @@ export default function CampaignDetailPanel({ campaign, onClose, onSave, onDelet
name: campaign.name || '',
description: campaign.description || '',
brand_id: campaign.brandId || campaign.brand_id || '',
team_id: campaign.team_id || '',
status: campaign.status || 'planning',
start_date: campaign.startDate ? new Date(campaign.startDate).toISOString().slice(0, 10) : (campaign.start_date || ''),
end_date: campaign.endDate ? new Date(campaign.endDate).toISOString().slice(0, 10) : (campaign.end_date || ''),
@@ -63,6 +66,7 @@ export default function CampaignDetailPanel({ campaign, onClose, onSave, onDelet
name: form.name,
description: form.description,
brand_id: form.brand_id ? Number(form.brand_id) : null,
team_id: form.team_id ? Number(form.team_id) : null,
status: form.status,
start_date: form.start_date,
end_date: form.end_date,
@@ -177,6 +181,19 @@ export default function CampaignDetailPanel({ campaign, onClose, onSave, onDelet
</div>
</div>
{/* Team */}
<div>
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('common.team')}</label>
<select
value={form.team_id}
onChange={e => update('team_id', 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"
>
<option value="">{t('common.noTeam')}</option>
{(teams || []).map(t => <option key={t.id || t._id} value={t.id || t._id}>{t.name}</option>)}
</select>
</div>
{/* Platforms */}
<div>
<label className="block text-xs font-medium text-text-tertiary mb-1">{t('campaigns.platforms')}</label>