Files
marketing-app/client/src/App.jsx
fahed 6cdec2b4b5
All checks were successful
Deploy / deploy (push) Successful in 11s
Restrict team_role and brands to admin-only editing
- Remove team_role and brands from profile completion wizard
- Lock team_role and brands fields when user edits own profile
- Remove team_role and brands from PATCH /users/me/profile endpoint
- Profile completeness now checks name instead of team_role

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:36:48 +03:00

299 lines
12 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { ToastProvider } from './components/ToastContainer'
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 Budgets from './pages/Budgets'
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 Brands from './pages/Brands'
import Login from './pages/Login'
import Artefacts from './pages/Artefacts'
import PostCalendar from './pages/PostCalendar'
import PublicReview from './pages/PublicReview'
import Issues from './pages/Issues'
import PublicIssueSubmit from './pages/PublicIssueSubmit'
import PublicIssueTracker from './pages/PublicIssueTracker'
import Tutorial from './components/Tutorial'
import Modal from './components/Modal'
import { api } from './utils/api'
import { useLanguage } from './i18n/LanguageContext'
const TEAM_ROLES = [
{ value: 'manager', label: 'Manager' },
{ value: 'approver', label: 'Approver' },
{ value: 'publisher', label: 'Publisher' },
{ value: 'content_creator', label: 'Content Creator' },
{ value: 'producer', label: 'Producer' },
{ value: 'designer', label: 'Designer' },
{ value: 'content_writer', label: 'Content Writer' },
{ value: 'social_media_manager', label: 'Social Media Manager' },
{ value: 'photographer', label: 'Photographer' },
{ value: 'videographer', label: 'Videographer' },
{ value: 'strategist', label: 'Strategist' },
]
export const AppContext = createContext()
function AppContent() {
const { user, loading: authLoading, checkAuth, hasModule } = useAuth()
const { t, lang } = useLanguage()
const [teamMembers, setTeamMembers] = useState([])
const [brands, setBrands] = useState([])
const [teams, setTeams] = useState([])
const [loading, setLoading] = useState(true)
const [showTutorial, setShowTutorial] = useState(false)
const [showProfilePrompt, setShowProfilePrompt] = useState(false)
const [showProfileModal, setShowProfileModal] = useState(false)
const [profileForm, setProfileForm] = useState({ name: '', team_role: '', phone: '', brands: '' })
const [profileSaving, setProfileSaving] = 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 {
setShowProfilePrompt(false)
}
} else if (!authLoading) {
setLoading(false)
}
}, [user, authLoading])
const getBrandName = (brandId) => {
if (!brandId) return null
const brand = brands.find(b => String(b._id || b.id) === String(brandId))
if (!brand) return null
return lang === 'ar' && brand.name_ar ? brand.name_ar : brand.name
}
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 loadTeams = async () => {
try {
const data = await api.get('/teams')
setTeams(Array.isArray(data) ? data : (data.data || []))
} catch (err) {
console.error('Failed to load teams:', err)
}
}
const loadInitialData = async () => {
try {
const [, brandsData] = await Promise.all([
loadTeam(),
api.get('/brands').then(d => Array.isArray(d) ? d : (d.data || [])).catch(() => []),
loadTeams(),
])
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, getBrandName, teams, loadTeams }}>
{/* 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">
<button
onClick={() => {
setProfileForm({
name: user?.name || '',
team_role: user?.teamRole || user?.team_role || '',
phone: user?.phone || '',
brands: Array.isArray(user?.brands) ? user.brands.join(', ') : '',
})
setShowProfileModal(true)
}}
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')}
</button>
<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>
)}
{/* Profile completion modal */}
<Modal isOpen={showProfileModal} onClose={() => setShowProfileModal(false)} title={t('profile.completeYourProfile')} size="md">
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-1">{t('team.name')}</label>
<input
type="text"
value={profileForm.name}
onChange={e => setProfileForm(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('team.fullName')}
/>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">{t('team.phone')} {t('team.optional')}</label>
<input
type="text"
value={profileForm.phone}
onChange={e => setProfileForm(f => ({ ...f, phone: 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"
/>
</div>
<div className="flex items-center justify-end gap-3 pt-4 border-t border-border">
<button
onClick={() => setShowProfileModal(false)}
className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-surface-tertiary rounded-lg"
>
{t('common.cancel')}
</button>
<button
onClick={async () => {
setProfileSaving(true)
try {
await api.patch('/users/me/profile', {
name: profileForm.name,
phone: profileForm.phone || null,
})
await checkAuth()
setShowProfileModal(false)
setShowProfilePrompt(false)
} catch (err) {
console.error('Profile save failed:', err)
} finally {
setProfileSaving(false)
}
}}
disabled={!profileForm.name || profileSaving}
className="px-5 py-2 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"
>
{profileSaving ? t('common.loading') : t('team.saveProfile')}
</button>
</div>
</div>
</Modal>
{/* Tutorial overlay */}
{showTutorial && <Tutorial onComplete={handleTutorialComplete} />}
<Routes>
<Route path="/login" element={user ? <Navigate to="/" replace /> : <Login />} />
<Route path="/review/:token" element={<PublicReview />} />
<Route path="/submit-issue" element={<PublicIssueSubmit />} />
<Route path="/track/:token" element={<PublicIssueTracker />} />
<Route path="/" element={user ? <Layout /> : <Navigate to="/login" replace />}>
<Route index element={<Dashboard />} />
{hasModule('marketing') && <>
<Route path="posts" element={<PostProduction />} />
<Route path="calendar" element={<PostCalendar />} />
<Route path="artefacts" element={<Artefacts />} />
<Route path="assets" element={<Assets />} />
<Route path="campaigns" element={<Campaigns />} />
<Route path="campaigns/:id" element={<CampaignDetail />} />
<Route path="brands" element={<Brands />} />
</>}
{hasModule('finance') && (user?.role === 'superadmin' || user?.role === 'manager') && <>
<Route path="finance" element={<Finance />} />
<Route path="budgets" element={<Budgets />} />
</>}
{hasModule('projects') && <>
<Route path="projects" element={<Projects />} />
<Route path="projects/:id" element={<ProjectDetail />} />
<Route path="tasks" element={<Tasks />} />
</>}
{hasModule('issues') && <Route path="issues" element={<Issues />} />}
<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>
<ToastProvider>
<AppContent />
</ToastProvider>
</AuthProvider>
</LanguageProvider>
)
}
export default App