Marketing Hub: RBAC, i18n (AR/EN), tasks overhaul, team/user merge, tutorial

Features:
- Full RBAC with 3 roles (superadmin/manager/contributor)
- Ownership tracking on posts, tasks, campaigns, projects
- Task system: assign to anyone, filter combobox, visibility scoping
- Team members merged into users table (single source of truth)
- Post thumbnails on kanban cards from attachments
- Publication link validation before publishing
- Interactive onboarding tutorial with Settings restart
- Full Arabic/English i18n with RTL layout support
- Language toggle in sidebar, IBM Plex Sans Arabic font
- Brand-based visibility filtering for non-superadmins
- Manager can only create contributors
- Profile completion flow for new users
- Cookie-based sessions (express-session + SQLite)
This commit is contained in:
fahed
2026-02-08 20:46:58 +03:00
commit 35d84b6bff
2240 changed files with 846749 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
import BrandBadge from './BrandBadge'
const ROLE_BADGES = {
manager: { bg: 'bg-indigo-50', text: 'text-indigo-700', label: 'Manager' },
approver: { bg: 'bg-emerald-50', text: 'text-emerald-700', label: 'Approver' },
publisher: { bg: 'bg-blue-50', text: 'text-blue-700', label: 'Publisher' },
content_creator: { bg: 'bg-amber-50', text: 'text-amber-700', label: 'Content Creator' },
producer: { bg: 'bg-purple-50', text: 'text-purple-700', label: 'Producer' },
designer: { bg: 'bg-pink-50', text: 'text-pink-700', label: 'Designer' },
content_writer: { bg: 'bg-orange-50', text: 'text-orange-700', label: 'Content Writer' },
social_media_manager: { bg: 'bg-teal-50', text: 'text-teal-700', label: 'Social Media Manager' },
photographer: { bg: 'bg-cyan-50', text: 'text-cyan-700', label: 'Photographer' },
videographer: { bg: 'bg-sky-50', text: 'text-sky-700', label: 'Videographer' },
strategist: { bg: 'bg-rose-50', text: 'text-rose-700', label: 'Strategist' },
default: { bg: 'bg-gray-50', text: 'text-gray-700', label: 'Team Member' },
}
export default function MemberCard({ member, onClick }) {
const getInitials = (name) => {
if (!name) return '?'
return name.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase()
}
const role = ROLE_BADGES[member.team_role || member.role] || ROLE_BADGES.default
const avatarColors = [
'from-indigo-400 to-purple-500',
'from-pink-400 to-rose-500',
'from-emerald-400 to-teal-500',
'from-amber-400 to-orange-500',
'from-cyan-400 to-blue-500',
]
const colorIndex = (member.name?.charCodeAt(0) || 0) % avatarColors.length
return (
<div
onClick={() => onClick?.(member)}
className="bg-white rounded-xl border border-border p-5 card-hover cursor-pointer text-center"
>
{/* Avatar */}
<div className={`w-16 h-16 rounded-full bg-gradient-to-br ${avatarColors[colorIndex]} flex items-center justify-center text-white text-xl font-bold mx-auto mb-3`}>
{getInitials(member.name)}
</div>
{/* Name */}
<h4 className="text-base font-semibold text-text-primary">{member.name}</h4>
{/* Role badge */}
<span className={`inline-block text-xs font-medium px-2 py-0.5 rounded-full mt-1 ${role.bg} ${role.text}`}>
{role.label}
</span>
{/* Email */}
{member.email && (
<p className="text-xs text-text-tertiary mt-2">{member.email}</p>
)}
{/* Brands */}
{member.brands && member.brands.length > 0 && (
<div className="flex flex-wrap gap-1 justify-center mt-3 pt-3 border-t border-border-light">
{member.brands.map((brand) => (
<BrandBadge key={brand} brand={brand} />
))}
</div>
)}
</div>
)
}