Frontend: - Add @azure/msal-browser and @azure/msal-react packages - Create authConfig.ts with MSAL configuration for PKCE flow - Create authService.ts for token acquisition and user info - Wrap App with MsalProvider in index.tsx - Replace dummy login with real MSAL loginPopup() in Login.tsx - Update App.tsx to use useIsAuthenticated/useMsal hooks - Update Profile.tsx to display real user data from claims - Update geminiService.ts to include access_token in WebSocket messages - Update WIPReviewer.tsx to pass msalInstance for auth Backend: - Add python-jose and httpx dependencies for JWT verification - Create auth_service.py with Azure AD JWKS fetching and token verification - Create auth.py FastAPI dependency for protected REST endpoints - Update main.py to verify tokens on WebSocket and protect /info endpoint - Add AZURE_TENANT_ID, AZURE_CLIENT_ID, DISABLE_AUTH to config 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
835 lines
33 KiB
TypeScript
835 lines
33 KiB
TypeScript
|
|
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 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, initialCampaigns, initialCampaignProofs } from './components/Campaigns';
|
|
import { Auditing } from './components/Auditing';
|
|
import { Login } from './components/Login';
|
|
import { WIPReviewer } from './components/WIPReviewer';
|
|
|
|
type View = 'Home' | 'Analytics' | 'Campaigns' | 'WIP Reviewer' | 'CopyGenAI' | 'Settings' | 'Profile' | 'Auditing';
|
|
|
|
export interface DropdownOptions {
|
|
campaigns: string[];
|
|
// Hierarchy: Channel -> SubChannel -> ProofType[]
|
|
channels: Record<string, Record<string, string[]>>;
|
|
}
|
|
|
|
const App: React.FC = () => {
|
|
// MSAL authentication state
|
|
const isAuthenticated = useIsAuthenticated();
|
|
const { instance: msalInstance, inProgress } = useMsal();
|
|
|
|
const [currentView, setCurrentView] = useState<View>('Home');
|
|
const [selectedCampaign, setSelectedCampaign] = useState<string | null>(null);
|
|
const [selectedProof, setSelectedProof] = useState<any | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const [dropdownOptions, setDropdownOptions] = useState<DropdownOptions>(() => {
|
|
try {
|
|
const storedOptions = localStorage.getItem('barclays_modcomms_dropdown_options_v3');
|
|
if (storedOptions) {
|
|
return JSON.parse(storedOptions);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error reading dropdown options from localStorage', error);
|
|
}
|
|
return {
|
|
campaigns: ["Barclays Q4 Social"],
|
|
channels: {
|
|
"Social": {
|
|
"Meta": ["In-feed 1x1", "In-feed 4x5", "Reels static 9x16", "Stories Static 9x16", "In-feed 1x1 video", "9x16 reels", "9x16 stories"]
|
|
},
|
|
"YouTube - (online video)": {
|
|
"Video": ["1x1", "16x9"]
|
|
},
|
|
"Google - Performance Max": {
|
|
"Video": ["1x1", "9x16", "16x9"],
|
|
"Static image": ["1080x1080", "4x5 (1080x1350)", "1.91x1 (1200x628)", "Logo 1x1 (1080x1080)"]
|
|
},
|
|
"Display": {
|
|
"Banner": ["300x600", "160x600", "970x250", "300x250"]
|
|
},
|
|
".co.uk Banner": {
|
|
"Web Banner Design + Static": ["720x540", "1316x740"]
|
|
},
|
|
"Ad Copy": {
|
|
"Copy Document": ["Text"]
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
useEffect(() => {
|
|
try {
|
|
localStorage.setItem('barclays_modcomms_dropdown_options_v3', JSON.stringify(dropdownOptions));
|
|
} catch (error) {
|
|
console.error('Error saving dropdown options to localStorage', error);
|
|
}
|
|
}, [dropdownOptions]);
|
|
|
|
|
|
const [campaigns, setCampaigns] = useState(() => {
|
|
try {
|
|
const storedCampaigns = localStorage.getItem('barclays_modcomms_campaigns_v3');
|
|
return storedCampaigns ? JSON.parse(storedCampaigns) : initialCampaigns;
|
|
} catch (error) {
|
|
console.error('Error reading campaigns from localStorage', error);
|
|
return initialCampaigns;
|
|
}
|
|
});
|
|
|
|
const [campaignProofs, setCampaignProofs] = useState(() => {
|
|
try {
|
|
const storedProofs = localStorage.getItem('barclays_modcomms_campaign_proofs_v3');
|
|
return storedProofs ? JSON.parse(storedProofs) : initialCampaignProofs;
|
|
} catch (error) {
|
|
console.error('Error reading campaign proofs from localStorage', error);
|
|
return initialCampaignProofs;
|
|
}
|
|
});
|
|
|
|
const [flaggedItems, setFlaggedItems] = useState<FlaggedItem[]>(() => {
|
|
try {
|
|
const storedFlags = localStorage.getItem('barclays_modcomms_flagged_items_v3');
|
|
return storedFlags ? JSON.parse(storedFlags) : [];
|
|
} catch (error) {
|
|
console.error('Error reading flagged items from localStorage', error);
|
|
return [];
|
|
}
|
|
});
|
|
|
|
const [resolvedItems, setResolvedItems] = useState<ResolvedItem[]>(() => {
|
|
try {
|
|
const storedResolutions = localStorage.getItem('barclays_modcomms_resolved_items_v3');
|
|
return storedResolutions ? JSON.parse(storedResolutions) : [];
|
|
} catch (error) {
|
|
console.error('Error reading resolved items from localStorage', error);
|
|
return [];
|
|
}
|
|
});
|
|
|
|
const [errorItems, setErrorItems] = useState<ErrorItem[]>(() => {
|
|
try {
|
|
const storedErrors = localStorage.getItem('barclays_modcomms_error_items_v3');
|
|
return storedErrors ? JSON.parse(storedErrors) : [];
|
|
} catch (error) {
|
|
console.error('Error reading error items from localStorage', error);
|
|
return [];
|
|
}
|
|
});
|
|
|
|
useEffect(() => {
|
|
try {
|
|
if (!localStorage.getItem('barclays_modcomms_campaigns_v3')) {
|
|
localStorage.setItem('barclays_modcomms_campaigns_v3', JSON.stringify(initialCampaigns));
|
|
}
|
|
if (!localStorage.getItem('barclays_modcomms_campaign_proofs_v3')) {
|
|
localStorage.setItem('barclays_modcomms_campaign_proofs_v3', JSON.stringify(initialCampaignProofs));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving initial data to localStorage', error);
|
|
}
|
|
}, []);
|
|
|
|
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]);
|
|
|
|
// New function to handle saving campaign proofs to avoid QuotaExceededError
|
|
const saveCampaignProofs = (proofs: any) => {
|
|
try {
|
|
// Deep clone to avoid mutating the state object used by React
|
|
const proofsToSave = JSON.parse(JSON.stringify(proofs));
|
|
|
|
// Iterate over all campaigns and their proofs
|
|
for (const campaignName in proofsToSave) {
|
|
if (Object.prototype.hasOwnProperty.call(proofsToSave, campaignName)) {
|
|
proofsToSave[campaignName].forEach((proof: any) => {
|
|
// For temporary placeholder proofs, remove the file object and progress
|
|
// as it cannot be stringified and is not needed for persistence.
|
|
if (proof.tempId) {
|
|
if ('file' in proof) delete proof.file;
|
|
if ('analysisProgress' in proof) delete proof.analysisProgress;
|
|
}
|
|
|
|
// For versioned proofs, remove large non-SVG preview URLs
|
|
if (proof.versions) {
|
|
proof.versions.forEach((version: any) => {
|
|
if (version.proofPreviewUrl && !version.proofPreviewUrl.startsWith('data:image/svg+xml')) {
|
|
delete version.proofPreviewUrl;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
localStorage.setItem('barclays_modcomms_campaign_proofs_v3', JSON.stringify(proofsToSave));
|
|
} catch (error) {
|
|
if ((error as DOMException).name === 'QuotaExceededError') {
|
|
console.error('LocalStorage quota exceeded. Could not save campaign proofs. The app might not persist data correctly across sessions until storage is cleared.', error);
|
|
setError('Could not save campaign changes, storage is full.');
|
|
} else {
|
|
console.error('Error saving campaign proofs to localStorage', error);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleAddNewCampaign = (campaignData: { name: string; workfrontId: string; clientLead: string; brandGuidelines: string; }) => {
|
|
const newCampaign = {
|
|
...campaignData,
|
|
agency: "OLIVER Agency",
|
|
agencyLead: "Steve O'Donoghue",
|
|
proofs: 0,
|
|
status: 'In Progress',
|
|
lastModified: new Date().toISOString().split('T')[0],
|
|
};
|
|
|
|
const updatedCampaigns = [...campaigns, newCampaign];
|
|
const updatedCampaignProofs = { ...campaignProofs, [newCampaign.name]: [] };
|
|
|
|
setCampaigns(updatedCampaigns);
|
|
setCampaignProofs(updatedCampaignProofs);
|
|
|
|
try {
|
|
localStorage.setItem('barclays_modcomms_campaigns_v3', JSON.stringify(updatedCampaigns));
|
|
saveCampaignProofs(updatedCampaignProofs);
|
|
} catch (error) {
|
|
console.error('Error saving new campaign to localStorage', error);
|
|
}
|
|
};
|
|
|
|
const fileToDataUrl = (file: File): Promise<string> => {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => resolve(reader.result as string);
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(file);
|
|
});
|
|
};
|
|
|
|
const handleAddNewError = (errorData: Omit<ErrorItem, 'id' | 'timestamp' | 'submitter' | 'submitAgency'>) => {
|
|
const newError: ErrorItem = {
|
|
...errorData,
|
|
id: `err_${Date.now()}`,
|
|
timestamp: new Date().toISOString(),
|
|
submitter: "Steve O'Donoghue", // Hardcoded for prototype
|
|
submitAgency: "OLIVER Agency", // Hardcoded for prototype
|
|
};
|
|
|
|
setErrorItems(prevItems => {
|
|
const updatedErrors = [newError, ...prevItems];
|
|
try {
|
|
localStorage.setItem('barclays_modcomms_error_items_v3', JSON.stringify(updatedErrors));
|
|
} catch (error) {
|
|
console.error('Error saving error items to localStorage', error);
|
|
setError('Could not save the analysis error log due to storage limitations.');
|
|
}
|
|
return updatedErrors;
|
|
});
|
|
};
|
|
|
|
const handleProofUploadForCampaign = async (
|
|
campaignName: string,
|
|
file: File,
|
|
proofName: string,
|
|
channel: string,
|
|
subChannel: string,
|
|
proofType?: string
|
|
) => {
|
|
setError(null);
|
|
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 {
|
|
const feedback = await analyzeProof(file, handleAgentUpdate, msalInstance);
|
|
const previewUrl = await fileToDataUrl(file);
|
|
|
|
if (feedback.overallStatus === 'Analysis Error') {
|
|
const currentCampaignProofsList = campaignProofs[campaignName] || [];
|
|
const existingProof = currentCampaignProofsList.find(a => a.proofName === proofName && !a.tempId);
|
|
const version = existingProof ? (existingProof.versions[0]?.version || 0) + 1 : 1;
|
|
|
|
handleAddNewError({
|
|
campaignName,
|
|
proofName,
|
|
version,
|
|
errorSummary: feedback.leadAgentSummary,
|
|
});
|
|
}
|
|
|
|
setCampaignProofs(prevCampaignProofs => {
|
|
const currentCampaignProofsList = prevCampaignProofs[campaignName] || [];
|
|
const existingProof = currentCampaignProofsList.find(a => a.proofName === proofName && !a.tempId);
|
|
|
|
if (existingProof) {
|
|
// UPDATE PROOF (NEW VERSION)
|
|
const latestVersionNumber = existingProof.versions[0]?.version || 0;
|
|
const newVersionNumber = latestVersionNumber + 1;
|
|
const baseWorkfrontId = existingProof.versions.length > 0
|
|
? existingProof.versions[existingProof.versions.length - 1].workfrontId.split('-V')[0]
|
|
: `#WF_${Math.floor(10000 + Math.random() * 90000)}`;
|
|
|
|
const newVersion = {
|
|
version: newVersionNumber,
|
|
timestamp: new Date().toISOString().split('T')[0],
|
|
workfrontId: `${baseWorkfrontId}-V${newVersionNumber}`,
|
|
proofPreviewUrl: previewUrl,
|
|
feedback: feedback,
|
|
overallStatus: feedback.overallStatus,
|
|
};
|
|
|
|
const updatedProof = {
|
|
...existingProof,
|
|
overallStatus: feedback.overallStatus,
|
|
versions: [newVersion, ...existingProof.versions]
|
|
};
|
|
|
|
const updatedProofsList = currentCampaignProofsList
|
|
.filter(proof => proof.tempId !== tempId)
|
|
.map(proof => proof.proofName === proofName ? updatedProof : proof);
|
|
|
|
const finalCampaignProofs = { ...prevCampaignProofs, [campaignName]: updatedProofsList };
|
|
|
|
setCampaigns(prevCampaigns => {
|
|
const updatedCampaigns = prevCampaigns.map(p =>
|
|
p.name === campaignName ? { ...p, lastModified: new Date().toISOString().split('T')[0] } : p
|
|
);
|
|
localStorage.setItem('barclays_modcomms_campaigns_v3', JSON.stringify(updatedCampaigns));
|
|
return updatedCampaigns;
|
|
});
|
|
|
|
saveCampaignProofs(finalCampaignProofs);
|
|
return finalCampaignProofs;
|
|
|
|
} else {
|
|
// CREATE NEW PROOF
|
|
const newWorkfrontId = `#WF_${Math.floor(10000 + Math.random() * 90000)}`;
|
|
const newProofWithVersion = {
|
|
proofName,
|
|
channel,
|
|
subChannel,
|
|
proofType,
|
|
status: 'completed',
|
|
overallStatus: feedback.overallStatus,
|
|
versions: [
|
|
{
|
|
version: 1,
|
|
timestamp: new Date().toISOString().split('T')[0],
|
|
workfrontId: `${newWorkfrontId}-V1`,
|
|
proofPreviewUrl: previewUrl,
|
|
feedback: feedback,
|
|
overallStatus: feedback.overallStatus,
|
|
}
|
|
]
|
|
};
|
|
|
|
const updatedProofsList = currentCampaignProofsList.map(proof =>
|
|
proof.tempId === tempId ? newProofWithVersion : proof
|
|
);
|
|
|
|
const finalCampaignProofs = { ...prevCampaignProofs, [campaignName]: updatedProofsList };
|
|
|
|
setCampaigns(prevCampaigns => {
|
|
const updatedCampaigns = prevCampaigns.map(p =>
|
|
p.name === campaignName ? { ...p, proofs: p.proofs + 1, lastModified: new Date().toISOString().split('T')[0] } : p
|
|
);
|
|
localStorage.setItem('barclays_modcomms_campaigns_v3', JSON.stringify(updatedCampaigns));
|
|
return updatedCampaigns;
|
|
});
|
|
|
|
saveCampaignProofs(finalCampaignProofs);
|
|
return finalCampaignProofs;
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.error("Failed to upload and analyze proof:", err);
|
|
setError("Failed to upload and analyze proof. Please try again.");
|
|
setCampaignProofs(prevProofs => {
|
|
const updatedProofs = {
|
|
...prevProofs,
|
|
[campaignName]: prevProofs[campaignName].map(proof =>
|
|
proof.tempId === tempId ? { ...proof, status: 'error' } : proof
|
|
)
|
|
};
|
|
saveCampaignProofs(updatedProofs);
|
|
return updatedProofs;
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleRetryAnalysis = async (campaignName: string, tempId: string) => {
|
|
const proofToRetry = campaignProofs[campaignName]?.find(proof => proof.tempId === tempId);
|
|
|
|
if (!proofToRetry || !proofToRetry.file) {
|
|
console.error("Proof to retry not found or file is missing");
|
|
return;
|
|
}
|
|
|
|
const { file, proofName, channel, subChannel, proofType } = proofToRetry;
|
|
|
|
setCampaignProofs(prevProofs => ({
|
|
...prevProofs,
|
|
[campaignName]: prevProofs[campaignName].map(proof =>
|
|
proof.tempId === tempId ? { ...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 (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 {
|
|
const feedback = await analyzeProof(file, handleAgentUpdateForRetry, msalInstance);
|
|
const previewUrl = await fileToDataUrl(file);
|
|
const newWorkfrontId = `#WF_${Math.floor(10000 + Math.random() * 90000)}`;
|
|
|
|
if (feedback.overallStatus === 'Analysis Error') {
|
|
handleAddNewError({
|
|
campaignName: campaignName,
|
|
proofName: proofName,
|
|
version: 1, // Retry always creates a V1
|
|
errorSummary: feedback.leadAgentSummary,
|
|
});
|
|
}
|
|
|
|
const newProofWithVersion = {
|
|
proofName,
|
|
channel,
|
|
subChannel,
|
|
proofType,
|
|
status: 'completed',
|
|
overallStatus: feedback.overallStatus,
|
|
versions: [
|
|
{
|
|
version: 1,
|
|
timestamp: new Date().toISOString().split('T')[0],
|
|
workfrontId: `${newWorkfrontId}-V1`,
|
|
proofPreviewUrl: previewUrl,
|
|
feedback: feedback,
|
|
overallStatus: feedback.overallStatus,
|
|
}
|
|
]
|
|
};
|
|
|
|
const updatedCampaigns = campaigns.map(p =>
|
|
p.name === campaignName ? { ...p, proofs: p.proofs + 1, lastModified: new Date().toISOString().split('T')[0] } : p
|
|
);
|
|
setCampaigns(updatedCampaigns);
|
|
localStorage.setItem('barclays_modcomms_campaigns_v3', JSON.stringify(updatedCampaigns));
|
|
|
|
setCampaignProofs(prevProofs => {
|
|
const updatedProofs = {
|
|
...prevProofs,
|
|
[campaignName]: prevProofs[campaignName].map(proof =>
|
|
proof.tempId === tempId ? newProofWithVersion : proof
|
|
)
|
|
};
|
|
saveCampaignProofs(updatedProofs);
|
|
return updatedProofs;
|
|
});
|
|
|
|
} catch (err) {
|
|
console.error("Failed to retry proof analysis:", err);
|
|
setCampaignProofs(prevProofs => {
|
|
const updatedProofs = {
|
|
...prevProofs,
|
|
[campaignName]: prevProofs[campaignName].map(proof =>
|
|
proof.tempId === tempId ? { ...proof, status: 'error' } : proof
|
|
)
|
|
};
|
|
saveCampaignProofs(updatedProofs);
|
|
return updatedProofs;
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleCampaignStatusChange = (campaignName: string, newStatus: 'In Progress' | 'Completed') => {
|
|
const updatedCampaigns = campaigns.map(p =>
|
|
p.name === campaignName ? { ...p, status: newStatus, lastModified: new Date().toISOString().split('T')[0] } : p
|
|
);
|
|
setCampaigns(updatedCampaigns);
|
|
try {
|
|
localStorage.setItem('barclays_modcomms_campaigns_v3', JSON.stringify(updatedCampaigns));
|
|
} catch (error) {
|
|
console.error('Error saving campaign status to localStorage', error);
|
|
}
|
|
};
|
|
|
|
const handleDeleteProof = (campaignName: string, proofName: string) => {
|
|
// Update campaign proofs
|
|
setCampaignProofs(prevProofs => {
|
|
const updatedProofsForCampaign = prevProofs[campaignName].filter(
|
|
proof => proof.proofName !== proofName
|
|
);
|
|
const finalProofs = { ...prevProofs, [campaignName]: updatedProofsForCampaign };
|
|
saveCampaignProofs(finalProofs);
|
|
return finalProofs;
|
|
});
|
|
|
|
// Update campaigns list (proof count)
|
|
setCampaigns(prevCampaigns => {
|
|
const updatedCampaigns = prevCampaigns.map(p =>
|
|
p.name === campaignName
|
|
? { ...p, proofs: p.proofs > 0 ? p.proofs - 1 : 0, lastModified: new Date().toISOString().split('T')[0] }
|
|
: p
|
|
);
|
|
localStorage.setItem('barclays_modcomms_campaigns_v3', JSON.stringify(updatedCampaigns));
|
|
return updatedCampaigns;
|
|
});
|
|
};
|
|
|
|
// --- SETTINGS HANDLERS (UPDATED) ---
|
|
|
|
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 = (channel: string) => {
|
|
setDropdownOptions(prev => {
|
|
if (prev.channels[channel]) return prev;
|
|
return {
|
|
...prev,
|
|
channels: {
|
|
...prev.channels,
|
|
[channel]: {}
|
|
}
|
|
};
|
|
});
|
|
};
|
|
|
|
const handleRemoveChannel = (channel: string) => {
|
|
setDropdownOptions(prev => {
|
|
const newChannels = { ...prev.channels };
|
|
delete newChannels[channel];
|
|
return { ...prev, channels: newChannels };
|
|
});
|
|
};
|
|
|
|
const handleAddSubChannel = (channel: string, subChannel: string) => {
|
|
setDropdownOptions(prev => {
|
|
const channelData = prev.channels[channel] || {};
|
|
if (channelData[subChannel]) return prev;
|
|
|
|
return {
|
|
...prev,
|
|
channels: {
|
|
...prev.channels,
|
|
[channel]: {
|
|
...channelData,
|
|
[subChannel]: []
|
|
}
|
|
}
|
|
};
|
|
});
|
|
};
|
|
|
|
const handleRemoveSubChannel = (channel: string, subChannel: string) => {
|
|
setDropdownOptions(prev => {
|
|
const channelData = { ...prev.channels[channel] };
|
|
delete channelData[subChannel];
|
|
return {
|
|
...prev,
|
|
channels: {
|
|
...prev.channels,
|
|
[channel]: channelData
|
|
}
|
|
};
|
|
});
|
|
};
|
|
|
|
const handleAddProofType = (channel: string, subChannel: string, proofType: string) => {
|
|
setDropdownOptions(prev => {
|
|
const channelData = prev.channels[channel];
|
|
if (!channelData) return prev;
|
|
const currentTypes = channelData[subChannel] || [];
|
|
if (currentTypes.includes(proofType)) return prev;
|
|
|
|
return {
|
|
...prev,
|
|
channels: {
|
|
...prev.channels,
|
|
[channel]: {
|
|
...channelData,
|
|
[subChannel]: [...currentTypes, proofType].sort()
|
|
}
|
|
}
|
|
};
|
|
});
|
|
};
|
|
|
|
const handleRemoveProofType = (channel: string, subChannel: string, proofType: string) => {
|
|
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)
|
|
}
|
|
}
|
|
};
|
|
});
|
|
};
|
|
|
|
|
|
const handleNavigate = (view: View) => {
|
|
setCurrentView(view);
|
|
setSelectedCampaign(null); // Reset campaign on any main navigation
|
|
setSelectedProof(null);
|
|
};
|
|
|
|
const handleSelectCampaign = (campaignName: string) => {
|
|
setSelectedCampaign(campaignName);
|
|
};
|
|
|
|
const handleSelectProof = (proof: any) => {
|
|
setSelectedProof(proof);
|
|
};
|
|
|
|
const handleBackToCampaignsList = () => {
|
|
setSelectedCampaign(null);
|
|
setSelectedProof(null);
|
|
};
|
|
|
|
const handleBackToCampaignDetails = () => {
|
|
setSelectedProof(null);
|
|
};
|
|
|
|
const handleFlagSubmit = (flagData: Omit<FlaggedItem, 'id' | 'timestamp' | 'submitter' | 'submitAgency'>) => {
|
|
const newFlag: FlaggedItem = {
|
|
...flagData,
|
|
id: `flag_${Date.now()}`,
|
|
timestamp: new Date().toISOString(),
|
|
submitter: "Steve O'Donoghue", // Hardcoded for prototype
|
|
submitAgency: "OLIVER Agency", // Hardcoded for prototype
|
|
};
|
|
|
|
const updatedFlags = [newFlag, ...flaggedItems];
|
|
setFlaggedItems(updatedFlags);
|
|
|
|
try {
|
|
localStorage.setItem('barclays_modcomms_flagged_items_v3', JSON.stringify(updatedFlags));
|
|
} catch (error) {
|
|
console.error('Error saving flagged items to localStorage', error);
|
|
setError('Could not save your feedback due to storage limitations.');
|
|
}
|
|
};
|
|
|
|
const handleResolveSubmit = (resolveData: Omit<ResolvedItem, 'id' | 'timestamp' | 'submitter' | 'submitAgency'>) => {
|
|
const newResolution: ResolvedItem = {
|
|
...resolveData,
|
|
id: `res_${Date.now()}`,
|
|
timestamp: new Date().toISOString(),
|
|
submitter: "Steve O'Donoghue", // Hardcoded for prototype
|
|
submitAgency: "OLIVER Agency", // Hardcoded for prototype
|
|
};
|
|
|
|
const updatedResolutions = [newResolution, ...resolvedItems];
|
|
setResolvedItems(updatedResolutions);
|
|
|
|
try {
|
|
localStorage.setItem('barclays_modcomms_resolved_items_v3', JSON.stringify(updatedResolutions));
|
|
} catch (error) {
|
|
console.error('Error saving resolved items to localStorage', error);
|
|
setError('Could not save your resolution due to storage limitations.');
|
|
}
|
|
};
|
|
|
|
|
|
const handleNavigateToAuditedItem = (item: { campaignName: string; proofName: string; version: number }) => {
|
|
const proofToSelect = campaignProofs[item.campaignName]?.find(a => a.proofName === item.proofName);
|
|
if (proofToSelect) {
|
|
const versionExists = proofToSelect.versions.some((v: any) => v.version === item.version);
|
|
if (versionExists) {
|
|
setSelectedCampaign(item.campaignName);
|
|
// Add a temporary property to the proof object to indicate which version to show.
|
|
setSelectedProof({ ...proofToSelect, initialVersion: item.version });
|
|
setCurrentView('Campaigns');
|
|
} else {
|
|
setError(`Version ${item.version} not found for proof ${item.proofName}. It may have been deleted.`);
|
|
}
|
|
} else {
|
|
setError(`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 renderContent = () => {
|
|
switch (currentView) {
|
|
case 'Analytics':
|
|
return <Analytics />;
|
|
case 'Profile':
|
|
return <Profile onLogout={handleLogout} msalInstance={msalInstance} />;
|
|
case 'CopyGenAI':
|
|
return <CopyGenAI />;
|
|
case 'Campaigns':
|
|
return <Campaigns
|
|
selectedCampaign={selectedCampaign}
|
|
selectedProof={selectedProof}
|
|
onSelectCampaign={handleSelectCampaign}
|
|
onSelectProof={handleSelectProof}
|
|
onBackToCampaignsList={handleBackToCampaignsList}
|
|
onBackToCampaignDetails={handleBackToCampaignDetails}
|
|
campaigns={campaigns}
|
|
campaignProofs={campaignProofs}
|
|
onAddNewCampaign={handleAddNewCampaign}
|
|
onProofUpload={handleProofUploadForCampaign}
|
|
dropdownOptions={dropdownOptions}
|
|
onRetryAnalysis={handleRetryAnalysis}
|
|
onCampaignStatusChange={handleCampaignStatusChange}
|
|
onDeleteProof={handleDeleteProof}
|
|
onFlagSubmit={handleFlagSubmit}
|
|
onResolveSubmit={handleResolveSubmit}
|
|
/>;
|
|
case 'WIP Reviewer':
|
|
return <WIPReviewer dropdownOptions={dropdownOptions} msalInstance={msalInstance} />;
|
|
case 'Auditing':
|
|
return <Auditing
|
|
flaggedItems={flaggedItems}
|
|
resolvedItems={resolvedItems}
|
|
errorItems={errorItems}
|
|
onNavigate={handleNavigateToAuditedItem}
|
|
/>;
|
|
case 'Settings':
|
|
return <Settings
|
|
options={dropdownOptions}
|
|
onAddCampaign={handleAddCampaignOption}
|
|
onRemoveCampaign={handleRemoveCampaignOption}
|
|
onAddChannel={handleAddChannel}
|
|
onRemoveChannel={handleRemoveChannel}
|
|
onAddSubChannel={handleAddSubChannel}
|
|
onRemoveSubChannel={handleRemoveSubChannel}
|
|
onAddProofType={handleAddProofType}
|
|
onRemoveProofType={handleRemoveProofType}
|
|
onNavigate={handleNavigate}
|
|
/>;
|
|
case 'Home':
|
|
default:
|
|
return (
|
|
<>
|
|
<Hero onGetStarted={() => handleNavigate('Campaigns')} />
|
|
<ChecksOverview />
|
|
</>
|
|
);
|
|
}
|
|
};
|
|
|
|
// Show loading spinner during MSAL authentication interactions
|
|
if (inProgress !== InteractionStatus.None) {
|
|
return (
|
|
<div className="fixed inset-0 bg-[#0f172a] flex items-center justify-center">
|
|
<div className="flex flex-col items-center gap-4">
|
|
<svg className="animate-spin h-12 w-12 text-brand-accent" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
<p className="text-white/80 text-sm">Authenticating...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!isAuthenticated) {
|
|
return <Login />;
|
|
}
|
|
|
|
// Determine background color based on view to avoid grey bar on Home view
|
|
const mainBgColor = currentView === 'Home' ? 'bg-white' : 'bg-brand-gray';
|
|
|
|
return (
|
|
<div className={`flex h-screen ${mainBgColor} font-sans text-gray-800 overflow-hidden`}>
|
|
<Sidebar activeItem={currentView} onNavigate={(view) => handleNavigate(view as View)} />
|
|
<div className="flex-1 flex flex-col overflow-y-auto">
|
|
<main className="flex-1 flex flex-col min-h-full">
|
|
{renderContent()}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App;
|