import React, { useState, useEffect } from 'react'; import { useIsAuthenticated, useMsal } from '@azure/msal-react'; import { InteractionStatus } from '@azure/msal-browser'; import { Hero } from './components/Hero'; import { analyzeProof } from './services/geminiService'; import { parseUrlState, pushUrlState } from './utils/urlState'; import { getUserInfo } from './services/authService'; import apiService from './services/apiService'; import type { AgentReview, AgentName, FlaggedItem, ResolvedItem, ErrorItem } from './types'; import { AGENT_NAMES } from './constants'; import { Sidebar } from './components/Sidebar'; import { ChecksOverview } from './components/ChecksOverview'; import { Analytics } from './components/Analytics'; import { Profile } from './components/Profile'; import { CopyGenAI } from './components/CopyGenAI'; import { Settings } from './components/Settings'; import { Campaigns } from './components/Campaigns'; import { Auditing } from './components/Auditing'; import { Login } from './components/Login'; import { WIPReviewer } from './components/WIPReviewer'; import { KnowledgeBase } from './components/KnowledgeBase'; import { UserManagement } from './components/UserManagement'; import { AgencyFilterBar } from './components/AgencyFilterBar'; import { UserProvider, useUser } from './contexts/UserContext'; type View = 'Home' | 'Analytics' | 'Campaigns' | 'WIP Reviewer' | 'CopyGenAI' | 'Settings' | 'Profile' | 'Auditing' | 'Knowledge Base' | 'User Management'; export interface DropdownOptions { campaigns: string[]; // Hierarchy: Channel -> SubChannel -> ProofType[] channels: Record>; // Brand guidelines for campaign creation brandGuidelines: string[]; } const App: React.FC = () => { // MSAL authentication state const isAuthenticated = useIsAuthenticated(); const { instance: msalInstance, inProgress } = useMsal(); // Initialize API service with MSAL instance BEFORE rendering UserProvider useEffect(() => { if (msalInstance) { apiService.setMsalInstance(msalInstance); } }, [msalInstance]); // Show loading spinner during MSAL authentication interactions if (inProgress !== InteractionStatus.None) { return (

Authenticating...

); } if (!isAuthenticated) { return ; } return ( ); }; const AppContent: React.FC<{ msalInstance: any }> = ({ msalInstance }) => { const isAuthenticated = true; // We're inside the authenticated boundary const { user, isLoading: isUserLoading, canWrite, canSeeAnalytics, canSeeAuditing, canSeeKnowledgeBase, canSeeSettings, canSeeUserManagement, canEditSettings, isSuperAdmin, isOversightAdmin, isUnassigned } = useUser(); // Get initial state from URL const initialUrlState = parseUrlState(); const [currentView, setCurrentView] = useState(initialUrlState.view); const [selectedCampaign, setSelectedCampaign] = useState(initialUrlState.campaignName); const [selectedProof, setSelectedProof] = useState(null); const [pendingProofId, setPendingProofId] = useState(initialUrlState.proofId); const [error, setError] = useState(null); const [isLoadingData, setIsLoadingData] = useState(true); const [notification, setNotification] = useState(null); const notificationTimerRef = React.useRef | null>(null); const showNotification = (message: string) => { setNotification(message); if (notificationTimerRef.current) clearTimeout(notificationTimerRef.current); notificationTimerRef.current = setTimeout(() => setNotification(null), 8000); }; // Agency filter state (session-level, for oversight_admin / super_admin) const [selectedAgencyId, setSelectedAgencyId] = useState(null); const showAgencyFilter = isSuperAdmin || isOversightAdmin; // Dropdown options now loaded from API const [dropdownOptions, setDropdownOptions] = useState({ campaigns: [], channels: {}, brandGuidelines: [] }); // Load dropdown options from API when authenticated useEffect(() => { const loadDropdownOptions = async () => { if (!isAuthenticated) return; try { const options = await apiService.getDropdownOptions(); console.log('[DEBUG App.tsx] Loaded dropdown options from API'); console.log('[DEBUG App.tsx] options.channels:', JSON.stringify(options.channels, null, 2)); console.log('[DEBUG App.tsx] Social.Meta proof types:', options.channels?.Social?.Meta); setDropdownOptions({ campaigns: options.campaigns || [], channels: options.channels || {}, brandGuidelines: options.brand_guidelines || [] }); } catch (error) { console.error('[DEBUG App.tsx] Failed to load dropdown options:', error); // Fall back to default options if API fails setDropdownOptions({ campaigns: [], channels: { "Social": { "Meta": ["In-feed 1x1", "In-feed 4x5"] }, "Display": { "Banner": ["300x600", "300x250"] } }, brandGuidelines: ["Barclays", "Barclaycard"] }); } }; loadDropdownOptions(); }, [isAuthenticated]); // Campaigns and proofs now loaded from API instead of localStorage const [campaigns, setCampaigns] = useState([]); const [campaignProofs, setCampaignProofs] = useState>({}); // Load campaigns from API when authenticated (re-fetch when agency filter changes) useEffect(() => { const loadCampaigns = async () => { if (!isAuthenticated || isUserLoading) return; setIsLoadingData(true); try { const response = await apiService.getCampaigns(selectedAgencyId || undefined); setCampaigns(response.map(c => apiService.convertCampaignToFrontend(c))); } catch (error) { console.error('Failed to load campaigns:', error); setError('Failed to load campaigns. Please try again.'); } finally { setIsLoadingData(false); } }; loadCampaigns(); }, [isAuthenticated, isUserLoading, selectedAgencyId]); // Audit items now loaded from API instead of localStorage const [flaggedItems, setFlaggedItems] = useState([]); const [resolvedItems, setResolvedItems] = useState([]); const [errorItems, setErrorItems] = useState([]); // Load audit items from API when authenticated (re-fetch when agency filter changes) useEffect(() => { const loadAuditItems = async () => { if (!isAuthenticated || isUserLoading) return; try { const agencyFilter = selectedAgencyId || undefined; const [flagged, resolved, errors] = await Promise.all([ apiService.getFlaggedItems(agencyFilter), apiService.getResolvedItems(agencyFilter), apiService.getErrorItems(agencyFilter), ]); setFlaggedItems(flagged.map(i => apiService.convertFlaggedItemToFrontend(i))); setResolvedItems(resolved.map(i => apiService.convertResolvedItemToFrontend(i))); setErrorItems(errors.map(i => apiService.convertErrorItemToFrontend(i))); } catch (error) { console.error('Failed to load audit items:', error); } }; loadAuditItems(); }, [isAuthenticated, isUserLoading, selectedAgencyId]); // Sync state changes to URL useEffect(() => { pushUrlState({ view: currentView, campaignName: selectedCampaign, proofId: selectedProof?._id || null, }); }, [currentView, selectedCampaign, selectedProof]); // Handle browser back/forward useEffect(() => { const handlePopState = () => { const state = parseUrlState(); setCurrentView(state.view); setSelectedCampaign(state.campaignName); if (!state.proofId) setSelectedProof(null); setPendingProofId(state.proofId); }; window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); }, []); // Load proofs for campaign selected from URL (deep linking support) useEffect(() => { const loadProofsForUrlCampaign = async () => { if (!selectedCampaign || !isAuthenticated || campaigns.length === 0) return; // Skip if proofs already loaded for this campaign if (campaignProofs[selectedCampaign]) return; const campaign = campaigns.find(c => c.name === selectedCampaign); if (!campaign?._id) return; try { const proofs = await apiService.getProofs(campaign._id); setCampaignProofs(prev => ({ ...prev, [selectedCampaign]: proofs.map(p => apiService.convertProofToFrontend(p)) })); } catch (error) { console.error('Failed to load proofs for URL campaign:', selectedCampaign, error); } }; loadProofsForUrlCampaign(); }, [selectedCampaign, isAuthenticated, campaigns, campaignProofs]); // Restore proof selection from URL after proofs are loaded useEffect(() => { if (pendingProofId && selectedCampaign && campaignProofs[selectedCampaign]) { const proof = campaignProofs[selectedCampaign].find(p => p._id === pendingProofId); if (proof) { setSelectedProof(proof); } setPendingProofId(null); } }, [pendingProofId, selectedCampaign, campaignProofs]); useEffect(() => { // Keep selectedProof in sync with the master list in campaignProofs. // This ensures that when a new version is added, the detail view refreshes. if (selectedCampaign && selectedProof && campaignProofs[selectedCampaign]) { const currentCampaignProofs = campaignProofs[selectedCampaign]; const freshProof = currentCampaignProofs.find(a => !a.tempId && a.proofName === selectedProof.proofName); // A simple stringify check to prevent re-renders if the object is the same. if (freshProof && JSON.stringify(freshProof) !== JSON.stringify(selectedProof)) { setSelectedProof(freshProof); } } }, [campaignProofs, selectedCampaign, selectedProof]); const handleAddNewCampaign = async (campaignData: { name: string; workfrontId: string; clientLead: string; agencyLead: string; brandGuidelines: string; }) => { try { const response = await apiService.createCampaign({ name: campaignData.name, workfront_id: campaignData.workfrontId, client_lead: campaignData.clientLead, agency_lead: campaignData.agencyLead, brand_guidelines: campaignData.brandGuidelines, }); const newCampaign = apiService.convertCampaignToFrontend(response); setCampaigns(prev => [...prev, newCampaign]); setCampaignProofs(prev => ({ ...prev, [newCampaign.name]: [] })); } catch (error) { console.error('Error creating campaign:', error); setError('Failed to create campaign. Please try again.'); } }; const fileToDataUrl = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result as string); reader.onerror = reject; reader.readAsDataURL(file); }); }; const handleProofUploadForCampaign = async ( campaignName: string, file: File, proofName: string, channel: string, subChannel: string, proofType?: string ) => { setError(null); // Find the campaign to get its ID for API persistence const campaign = campaigns.find(c => c.name === campaignName); if (!campaign?._id) { setError('Campaign not found. Please refresh and try again.'); return; } const tempId = `temp_${Date.now()}`; const newProofPlaceholder = { tempId, proofName, channel, subChannel, proofType, status: 'analyzing', analysisProgress: { completed: 0, total: AGENT_NAMES.length + 1 }, file, versions: [], }; setCampaignProofs(prevProofs => ({ ...prevProofs, [campaignName]: [newProofPlaceholder, ...(prevProofs[campaignName] || [])] })); const handleAgentUpdate = (agentName: AgentName | 'Summary') => { setCampaignProofs(prevProofs => { const currentProofs = prevProofs[campaignName] || []; const updatedProofs = currentProofs.map(proof => { if (proof.tempId === tempId && proof.status === 'analyzing') { const newCompleted = (proof.analysisProgress?.completed ?? 0) + 1; return { ...proof, analysisProgress: { ...proof.analysisProgress, completed: newCompleted } }; } return proof; }); return { ...prevProofs, [campaignName]: updatedProofs }; }); }; try { // Pass campaign context to persist proof in database const result = await analyzeProof(file, handleAgentUpdate, msalInstance, { campaignId: campaign._id, proofName, channel, subChannel, proofType, brand: campaign.brandGuidelines, }, showNotification); const feedback = result.review; // Refresh proofs from API to get the persisted data // This ensures we have the correct IDs and data from the database try { const proofs = await apiService.getProofs(campaign._id); setCampaignProofs(prev => ({ ...prev, [campaignName]: proofs.map(p => apiService.convertProofToFrontend(p)) })); } catch (refreshError) { console.error('Failed to refresh proofs after analysis:', refreshError); // Fallback: remove the temp placeholder since analysis succeeded setCampaignProofs(prevProofs => ({ ...prevProofs, [campaignName]: prevProofs[campaignName].filter(p => p.tempId !== tempId) })); } // Refresh campaigns list to get updated proof count try { const campaignsResponse = await apiService.getCampaigns(); setCampaigns(campaignsResponse.map(c => apiService.convertCampaignToFrontend(c))); } catch (refreshError) { console.error('Failed to refresh campaigns after analysis:', refreshError); } // If analysis resulted in error, log it (error items are now tracked on backend) if (feedback.overallStatus === 'Analysis Error') { console.warn('Proof analysis resulted in error status:', feedback.leadAgentSummary); } } catch (err) { console.error("Failed to upload and analyze proof:", err); setError("Failed to upload and analyse proof. Please try again."); setCampaignProofs(prevProofs => ({ ...prevProofs, [campaignName]: prevProofs[campaignName].map(proof => proof.tempId === tempId ? { ...proof, status: 'error' } : proof ) })); } }; const handleRetryAnalysis = async (campaignName: string, proofId: string) => { const matchProof = (proof: any) => proof.tempId === proofId || proof._id === proofId; const proofToRetry = campaignProofs[campaignName]?.find(matchProof); if (!proofToRetry) { console.error("Proof to retry not found"); return; } // Find the campaign to get its ID for API persistence const campaign = campaigns.find(c => c.name === campaignName); if (!campaign?._id) { setError('Campaign not found. Please refresh and try again.'); return; } let file = proofToRetry.file; // Fetch from backend if not in memory if (!file) { const storageKey = proofToRetry.fileStorageKey || proofToRetry.versions?.[0]?.fileStorageKey; if (!storageKey) { setError('Cannot retry: original file is not available.'); return; } try { file = await apiService.getFile(storageKey); } catch (err) { console.error("Failed to fetch file for retry:", err); setError('Failed to retrieve original file for retry.'); return; } } const { proofName, channel, subChannel, proofType } = proofToRetry; setCampaignProofs(prevProofs => ({ ...prevProofs, [campaignName]: prevProofs[campaignName].map(proof => matchProof(proof) ? { ...proof, status: 'analyzing', analysisProgress: { completed: 0, total: AGENT_NAMES.length + 1 } } : proof ) })); const handleAgentUpdateForRetry = (agentName: AgentName | 'Summary') => { setCampaignProofs(prevProofs => { const currentProofs = prevProofs[campaignName] || []; const updatedProofs = currentProofs.map(proof => { if (matchProof(proof) && proof.status === 'analyzing') { const newCompleted = (proof.analysisProgress?.completed ?? 0) + 1; return { ...proof, analysisProgress: { ...proof.analysisProgress, completed: newCompleted } }; } return proof; }); return { ...prevProofs, [campaignName]: updatedProofs }; }); }; try { // Pass campaign context to persist proof in database const result = await analyzeProof(file, handleAgentUpdateForRetry, msalInstance, { campaignId: campaign._id, proofName, channel, subChannel, proofType, brand: campaign.brandGuidelines, }, showNotification); // Refresh proofs from API to get the persisted data try { const proofs = await apiService.getProofs(campaign._id); setCampaignProofs(prev => ({ ...prev, [campaignName]: proofs.map(p => apiService.convertProofToFrontend(p)) })); } catch (refreshError) { console.error('Failed to refresh proofs after retry:', refreshError); setCampaignProofs(prevProofs => ({ ...prevProofs, [campaignName]: prevProofs[campaignName].filter(p => !matchProof(p)) })); } // Refresh campaigns list to get updated proof count try { const campaignsResponse = await apiService.getCampaigns(); setCampaigns(campaignsResponse.map(c => apiService.convertCampaignToFrontend(c))); } catch (refreshError) { console.error('Failed to refresh campaigns after retry:', refreshError); } if (result.review.overallStatus === 'Analysis Error') { console.warn('Retry analysis resulted in error status:', result.review.leadAgentSummary); } } catch (err) { console.error("Failed to retry proof analysis:", err); setCampaignProofs(prevProofs => ({ ...prevProofs, [campaignName]: prevProofs[campaignName].map(proof => matchProof(proof) ? { ...proof, status: 'error' } : proof ) })); } }; const handleCampaignStatusChange = async (campaignName: string, newStatus: 'In Progress' | 'Completed') => { const campaign = campaigns.find(c => c.name === campaignName); if (!campaign?._id) { setError('Campaign not found.'); return; } const previousStatus = campaign.status; const previousLastModified = campaign.lastModified; // Optimistically update local state immediately setCampaigns(prev => prev.map(p => p.name === campaignName ? { ...p, status: newStatus, lastModified: new Date().toISOString() } : p )); try { await apiService.updateCampaign(campaign._id, { status: newStatus }); } catch (error) { console.error('Error updating campaign status:', error); setError('Failed to update campaign status.'); // Rollback on error setCampaigns(prev => prev.map(p => p.name === campaignName ? { ...p, status: previousStatus, lastModified: previousLastModified } : p )); } }; const handleDeleteProof = async (campaignName: string, proofName: string) => { // Find the proof to get its ID const proofToDelete = campaignProofs[campaignName]?.find(p => p.proofName === proofName); if (!proofToDelete?._id) { // If no _id, it might be a temp proof that hasn't been saved to DB yet // Just remove it from local state setCampaignProofs(prevProofs => ({ ...prevProofs, [campaignName]: prevProofs[campaignName].filter(p => p.proofName !== proofName) })); return; } try { await apiService.deleteProof(proofToDelete._id); // Update local state after successful deletion setCampaignProofs(prevProofs => ({ ...prevProofs, [campaignName]: prevProofs[campaignName].filter(p => p.proofName !== proofName) })); // Refresh campaigns to get updated proof count const campaignsResponse = await apiService.getCampaigns(); setCampaigns(campaignsResponse.map(c => apiService.convertCampaignToFrontend(c))); } catch (error) { console.error('Error deleting proof:', error); setError('Failed to delete proof.'); } }; const handleDeleteCampaign = async (campaignName: string) => { const campaign = campaigns.find(c => c.name === campaignName); if (!campaign?._id) { setError('Campaign not found.'); return; } try { await apiService.deleteCampaign(campaign._id); // Update local state after successful deletion setCampaigns(prev => prev.filter(c => c.name !== campaignName)); setCampaignProofs(prev => { const newProofs = { ...prev }; delete newProofs[campaignName]; return newProofs; }); } catch (error) { console.error('Error deleting campaign:', error); setError('Failed to delete campaign. Please try again.'); } }; // --- SETTINGS HANDLERS (NOW USE API) --- // Helper to refresh dropdown options from API const refreshDropdownOptions = async () => { try { const options = await apiService.getDropdownOptions(); setDropdownOptions({ campaigns: options.campaigns || [], channels: options.channels || {}, brandGuidelines: options.brand_guidelines || [] }); } catch (error) { console.error('Failed to refresh dropdown options:', error); } }; // Campaign options are not stored in dropdown_options table - keeping local for now const handleAddCampaignOption = (value: string) => { setDropdownOptions(prev => { if (!prev.campaigns.includes(value)) { return { ...prev, campaigns: [...prev.campaigns, value].sort() }; } return prev; }); }; const handleRemoveCampaignOption = (value: string) => { setDropdownOptions(prev => ({ ...prev, campaigns: prev.campaigns.filter(c => c !== value) })); }; const handleAddChannel = async (channel: string) => { try { await apiService.addChannel(channel); // Optimistically update local state setDropdownOptions(prev => ({ ...prev, channels: { ...prev.channels, [channel]: {} } })); } catch (error) { console.error('Error adding channel:', error); setError('Failed to add channel.'); } }; const handleRemoveChannel = async (channel: string) => { try { await apiService.deleteChannel(channel); // Optimistically update local state setDropdownOptions(prev => { const newChannels = { ...prev.channels }; delete newChannels[channel]; return { ...prev, channels: newChannels }; }); } catch (error) { console.error('Error removing channel:', error); setError('Failed to remove channel.'); } }; const handleAddSubChannel = async (channel: string, subChannel: string) => { try { await apiService.addSubChannel(channel, subChannel); // Optimistically update local state setDropdownOptions(prev => ({ ...prev, channels: { ...prev.channels, [channel]: { ...prev.channels[channel], [subChannel]: [] } } })); } catch (error) { console.error('Error adding sub-channel:', error); setError('Failed to add sub-channel.'); } }; const handleRemoveSubChannel = async (channel: string, subChannel: string) => { try { await apiService.deleteSubChannel(channel, subChannel); // Optimistically update local state setDropdownOptions(prev => { const channelData = { ...prev.channels[channel] }; delete channelData[subChannel]; return { ...prev, channels: { ...prev.channels, [channel]: channelData } }; }); } catch (error) { console.error('Error removing sub-channel:', error); setError('Failed to remove sub-channel.'); } }; const handleAddProofType = async (channel: string, subChannel: string, proofType: string) => { try { await apiService.addProofType(channel, subChannel, proofType); // Optimistically update local state setDropdownOptions(prev => { const channelData = prev.channels[channel] || {}; const currentTypes = channelData[subChannel] || []; return { ...prev, channels: { ...prev.channels, [channel]: { ...channelData, [subChannel]: [...currentTypes, proofType].sort() } } }; }); } catch (error) { console.error('Error adding proof type:', error); setError('Failed to add proof type.'); } }; const handleRemoveProofType = async (channel: string, subChannel: string, proofType: string) => { try { await apiService.deleteProofType(channel, subChannel, proofType); // Optimistically update local state setDropdownOptions(prev => { const channelData = prev.channels[channel]; if (!channelData) return prev; const currentTypes = channelData[subChannel] || []; return { ...prev, channels: { ...prev.channels, [channel]: { ...channelData, [subChannel]: currentTypes.filter(t => t !== proofType) } } }; }); } catch (error) { console.error('Error removing proof type:', error); setError('Failed to remove proof type.'); } }; const handleNavigate = (view: View) => { setCurrentView(view); setSelectedCampaign(null); // Reset campaign on any main navigation setSelectedProof(null); }; const handleSelectCampaign = async (campaignName: string) => { setSelectedCampaign(campaignName); // Find the campaign to get its ID for API calls const campaign = campaigns.find(c => c.name === campaignName); if (!campaign?._id) { console.error('Campaign not found or missing ID:', campaignName); return; } // Load proofs from API if not already cached if (!campaignProofs[campaignName]) { try { const proofs = await apiService.getProofs(campaign._id); setCampaignProofs(prev => ({ ...prev, [campaignName]: proofs.map(p => apiService.convertProofToFrontend(p)) })); } catch (error) { console.error('Failed to load proofs for campaign:', campaignName, error); setError('Failed to load proofs. Please try again.'); } } }; const handleSelectProof = (proof: any) => { setSelectedProof(proof); }; const handleBackToCampaignsList = () => { setSelectedCampaign(null); setSelectedProof(null); }; const handleBackToCampaignDetails = () => { setSelectedProof(null); }; const handleFlagSubmit = async (flagData: Omit) => { // Find the proof to get its ID const proof = campaignProofs[flagData.campaignName]?.find(p => p.proofName === flagData.proofName); if (!proof?._id) { setError('Proof not found. Unable to submit flag.'); return; } try { const response = await apiService.flagProofVersion( proof._id, flagData.version, { agent_flagged: flagData.agentFlagged, comments: flagData.comments } ); const newFlag = apiService.convertFlaggedItemToFrontend(response); setFlaggedItems(prev => [newFlag, ...prev]); } catch (error) { console.error('Error flagging proof version:', error); setError('Failed to submit flag. Please try again.'); } }; const handleResolveSubmit = async (resolveData: Omit) => { // Find the proof to get its ID const proof = campaignProofs[resolveData.campaignName]?.find(p => p.proofName === resolveData.proofName); if (!proof?._id) { setError('Proof not found. Unable to submit resolution.'); return; } try { const response = await apiService.resolveProofVersion( proof._id, resolveData.version, { agent: resolveData.agent, issue: resolveData.issue, resolution: resolveData.resolution } ); const newResolution = apiService.convertResolvedItemToFrontend(response); setResolvedItems(prev => [newResolution, ...prev]); } catch (error) { console.error('Error resolving proof version:', error); setError('Failed to submit resolution. Please try again.'); } }; const handleNavigateToAuditedItem = async (item: { campaignName: string; proofName: string; version: number }) => { // Use cached proofs or fetch from API (e.g. Oversight Admin going straight to Auditing) let proofs = campaignProofs[item.campaignName]; if (!proofs) { const campaign = campaigns.find(c => c.name === item.campaignName); if (!campaign?._id) { alert(`Campaign "${item.campaignName}" not found. It may have been deleted.`); return; } try { const rawProofs = await apiService.getProofs(campaign._id); proofs = rawProofs.map(p => apiService.convertProofToFrontend(p)); setCampaignProofs(prev => ({ ...prev, [item.campaignName]: proofs! })); } catch (error) { console.error('Failed to load proofs for campaign:', item.campaignName, error); alert('Failed to load proofs. Please try again.'); return; } } const proofToSelect = proofs.find(a => a.proofName === item.proofName); if (proofToSelect) { const versionExists = proofToSelect.versions.some((v: any) => v.version === item.version); if (versionExists) { setSelectedCampaign(item.campaignName); setSelectedProof({ ...proofToSelect, initialVersion: item.version }); setCurrentView('Campaigns'); } else { alert(`Version ${item.version} not found for proof "${item.proofName}". It may have been deleted.`); } } else { alert(`Proof "${item.proofName}" not found in campaign "${item.campaignName}". It may have been deleted.`); } }; const handleLogout = async () => { try { await msalInstance.logoutPopup({ postLogoutRedirectUri: window.location.origin, }); } catch (error) { console.error('Logout failed:', error); } }; const readOnly = !canWrite || (isOversightAdmin && !user?.agencyId); const renderContent = () => { switch (currentView) { case 'Analytics': return ; case 'Profile': return ; case 'CopyGenAI': return ; case 'Campaigns': if (isUnassigned) { return (
🏢

No Agency Assigned

You are not currently assigned to an agency. Please contact your administrator to be assigned before you can view or create campaigns.

); } return ; case 'WIP Reviewer': return ; case 'Auditing': return ; case 'Knowledge Base': return ; case 'User Management': return ; case 'Settings': return ; case 'Home': default: return ( <> handleNavigate('Campaigns')} /> {/* Hidden per Oliver design — client may want it back */} {/* */} ); } }; // Show loading spinner while user profile is loading if (isUserLoading) { return (

Loading user profile...

); } // Determine background color based on view to avoid grey bar on Home view const mainBgColor = currentView === 'Home' ? 'bg-white' : 'bg-oliver-grey'; // Get user info from MSAL for sidebar display const userInfo = getUserInfo(msalInstance); return (
handleNavigate(view as View)} userName={user?.name || userInfo?.name} userEmail={user?.email || userInfo?.email} userRole={user?.role || 'basic_user'} />
{showAgencyFilter && ( )}
{renderContent()}
{/* Model fallback notification toast */} {notification && (

AI Model Notice

{notification}

)}
); }; export default App;