All checks were successful
Deploy / deploy (push) Successful in 11s
- 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>
299 lines
12 KiB
JavaScript
299 lines
12 KiB
JavaScript
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
|