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

175
client/src/App.jsx Normal file
View File

@@ -0,0 +1,175 @@
import { Routes, Route, Navigate } from 'react-router-dom'
import { useState, useEffect, createContext } from 'react'
import { AuthProvider, useAuth } from './contexts/AuthContext'
import { LanguageProvider } from './i18n/LanguageContext'
import Layout from './components/Layout'
import Dashboard from './pages/Dashboard'
import PostProduction from './pages/PostProduction'
import Assets from './pages/Assets'
import Campaigns from './pages/Campaigns'
import CampaignDetail from './pages/CampaignDetail'
import Finance from './pages/Finance'
import Projects from './pages/Projects'
import ProjectDetail from './pages/ProjectDetail'
import Tasks from './pages/Tasks'
import Team from './pages/Team'
import Users from './pages/Users'
import Settings from './pages/Settings'
import Login from './pages/Login'
import Tutorial from './components/Tutorial'
import { api } from './utils/api'
import { useLanguage } from './i18n/LanguageContext'
export const AppContext = createContext()
function AppContent() {
const { user, loading: authLoading } = useAuth()
const { t } = useLanguage()
const [teamMembers, setTeamMembers] = useState([])
const [brands, setBrands] = useState([])
const [loading, setLoading] = useState(true)
const [showTutorial, setShowTutorial] = useState(false)
const [showProfilePrompt, setShowProfilePrompt] = useState(false)
useEffect(() => {
if (user && !authLoading) {
loadInitialData()
// Check if tutorial should be shown
if (user.tutorial_completed === 0) {
setShowTutorial(true)
}
// Check if profile is incomplete
if (!user.profileComplete && user.role !== 'superadmin') {
setShowProfilePrompt(true)
}
} else if (!authLoading) {
setLoading(false)
}
}, [user, authLoading])
const loadTeam = async () => {
try {
const data = await api.get('/users/team')
const members = Array.isArray(data) ? data : (data.data || [])
setTeamMembers(members)
return members
} catch (err) {
console.error('Failed to load team:', err)
return []
}
}
const loadInitialData = async () => {
try {
const [members, brandsData] = await Promise.all([
loadTeam(),
api.get('/brands').then(d => Array.isArray(d) ? d : (d.data || [])).catch(() => []),
])
setTeamMembers(members)
setBrands(brandsData)
} catch (err) {
console.error('Failed to load initial data:', err)
} finally {
setLoading(false)
}
}
const handleTutorialComplete = async () => {
try {
await api.patch('/users/me/tutorial', { completed: true })
setShowTutorial(false)
} catch (err) {
console.error('Failed to complete tutorial:', err)
}
}
if (authLoading || loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-surface-secondary">
<div className="text-center">
<div className="w-12 h-12 border-4 border-brand-primary border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-text-secondary font-medium">{t('dashboard.loadingHub')}</p>
</div>
</div>
)
}
return (
<AppContext.Provider value={{ currentUser: user, teamMembers, brands, loadTeam }}>
{/* Profile completion prompt */}
{showProfilePrompt && (
<div className="fixed top-4 right-4 z-50 bg-amber-50 border-2 border-amber-400 rounded-xl shadow-lg p-4 max-w-md animate-fade-in">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-full bg-amber-400 flex items-center justify-center text-white shrink-0">
</div>
<div className="flex-1">
<h3 className="font-semibold text-amber-900 mb-1">{t('profile.completeYourProfile')}</h3>
<p className="text-sm text-amber-800 mb-3">
{t('profile.completeDesc')}
</p>
<div className="flex gap-2">
<a
href="/team"
className="px-3 py-1.5 bg-amber-400 text-white text-sm font-medium rounded-lg hover:bg-amber-500 transition-colors"
>
{t('profile.completeProfileBtn')}
</a>
<button
onClick={() => setShowProfilePrompt(false)}
className="px-3 py-1.5 text-sm font-medium text-amber-800 hover:bg-amber-100 rounded-lg transition-colors"
>
{t('profile.later')}
</button>
</div>
</div>
<button
onClick={() => setShowProfilePrompt(false)}
className="text-amber-600 hover:text-amber-800 transition-colors"
>
</button>
</div>
</div>
)}
{/* Tutorial overlay */}
{showTutorial && <Tutorial onComplete={handleTutorialComplete} />}
<Routes>
<Route path="/login" element={user ? <Navigate to="/" replace /> : <Login />} />
<Route path="/" element={user ? <Layout /> : <Navigate to="/login" replace />}>
<Route index element={<Dashboard />} />
<Route path="posts" element={<PostProduction />} />
<Route path="assets" element={<Assets />} />
<Route path="campaigns" element={<Campaigns />} />
<Route path="campaigns/:id" element={<CampaignDetail />} />
{(user?.role === 'superadmin' || user?.role === 'manager') && (
<Route path="finance" element={<Finance />} />
)}
<Route path="projects" element={<Projects />} />
<Route path="projects/:id" element={<ProjectDetail />} />
<Route path="tasks" element={<Tasks />} />
<Route path="team" element={<Team />} />
<Route path="settings" element={<Settings />} />
{user?.role === 'superadmin' && (
<Route path="users" element={<Users />} />
)}
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</AppContext.Provider>
)
}
function App() {
return (
<LanguageProvider>
<AuthProvider>
<AppContent />
</AuthProvider>
</LanguageProvider>
)
}
export default App