diff --git a/frontend/App.tsx b/frontend/App.tsx index 5d1234c..ebd95dc 100755 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -20,8 +20,11 @@ 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'; +type View = 'Home' | 'Analytics' | 'Campaigns' | 'WIP Reviewer' | 'CopyGenAI' | 'Settings' | 'Profile' | 'Auditing' | 'Knowledge Base' | 'User Management'; export interface DropdownOptions { campaigns: string[]; @@ -36,6 +39,43 @@ const App: React.FC = () => { 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 } = useUser(); + // Get initial state from URL const initialUrlState = parseUrlState(); @@ -46,12 +86,9 @@ const App: React.FC = () => { const [error, setError] = useState(null); const [isLoadingData, setIsLoadingData] = useState(true); - // Initialize API service with MSAL instance for authenticated requests - useEffect(() => { - if (msalInstance) { - apiService.setMsalInstance(msalInstance); - } - }, [msalInstance]); + // 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({ @@ -97,14 +134,14 @@ const App: React.FC = () => { const [campaigns, setCampaigns] = useState([]); const [campaignProofs, setCampaignProofs] = useState>({}); - // Load campaigns from API when authenticated + // Load campaigns from API when authenticated (re-fetch when agency filter changes) useEffect(() => { const loadCampaigns = async () => { - if (!isAuthenticated) return; + if (!isAuthenticated || isUserLoading) return; setIsLoadingData(true); try { - const response = await apiService.getCampaigns(); + const response = await apiService.getCampaigns(selectedAgencyId || undefined); setCampaigns(response.map(c => apiService.convertCampaignToFrontend(c))); } catch (error) { console.error('Failed to load campaigns:', error); @@ -115,23 +152,24 @@ const App: React.FC = () => { }; loadCampaigns(); - }, [isAuthenticated]); + }, [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 + // Load audit items from API when authenticated (re-fetch when agency filter changes) useEffect(() => { const loadAuditItems = async () => { - if (!isAuthenticated) return; + if (!isAuthenticated || isUserLoading) return; try { + const agencyFilter = selectedAgencyId || undefined; const [flagged, resolved, errors] = await Promise.all([ - apiService.getFlaggedItems(), - apiService.getResolvedItems(), - apiService.getErrorItems(), + apiService.getFlaggedItems(agencyFilter), + apiService.getResolvedItems(agencyFilter), + apiService.getErrorItems(agencyFilter), ]); setFlaggedItems(flagged.map(i => apiService.convertFlaggedItemToFrontend(i))); setResolvedItems(resolved.map(i => apiService.convertResolvedItemToFrontend(i))); @@ -142,7 +180,7 @@ const App: React.FC = () => { }; loadAuditItems(); - }, [isAuthenticated]); + }, [isAuthenticated, isUserLoading, selectedAgencyId]); // Sync state changes to URL useEffect(() => { @@ -791,10 +829,12 @@ const App: React.FC = () => { } }; + const readOnly = !canWrite; + const renderContent = () => { switch (currentView) { case 'Analytics': - return ; + return ; case 'Profile': return ; case 'CopyGenAI': @@ -820,18 +860,21 @@ const App: React.FC = () => { onResolveSubmit={handleResolveSubmit} flaggedItems={flaggedItems} resolvedItems={resolvedItems} + readOnly={readOnly} />; case 'WIP Reviewer': return ; case 'Auditing': - return ; case 'Knowledge Base': return ; + case 'User Management': + return ; case 'Settings': return { onRemoveSubChannel={handleRemoveSubChannel} onAddProofType={handleAddProofType} onRemoveProofType={handleRemoveProofType} + readOnly={!canEditSettings} />; case 'Home': default: @@ -855,8 +899,8 @@ const App: React.FC = () => { } }; - // Show loading spinner during MSAL authentication interactions - if (inProgress !== InteractionStatus.None) { + // Show loading spinner while user profile is loading + if (isUserLoading) { return (
@@ -864,16 +908,12 @@ const App: React.FC = () => { -

Authenticating...

+

Loading user profile...

); } - if (!isAuthenticated) { - return ; - } - // Determine background color based on view to avoid grey bar on Home view const mainBgColor = currentView === 'Home' ? 'bg-white' : 'bg-grey-100'; @@ -885,11 +925,17 @@ const App: React.FC = () => { handleNavigate(view as View)} - userName={userInfo?.name} - userEmail={userInfo?.email} - isAdmin={true} + userName={user?.name || userInfo?.name} + userEmail={user?.email || userInfo?.email} + userRole={user?.role || 'basic_user'} />
+ {showAgencyFilter && ( + + )}
{renderContent()}
diff --git a/frontend/components/AgencyFilterBar.tsx b/frontend/components/AgencyFilterBar.tsx new file mode 100644 index 0000000..bbc01d3 --- /dev/null +++ b/frontend/components/AgencyFilterBar.tsx @@ -0,0 +1,46 @@ +import React, { useState, useEffect } from 'react'; +import apiService from '../services/apiService'; +import type { AgencyResponse } from '../services/apiService'; +import { ChevronDownIcon } from './icons/ChevronDownIcon'; + +interface AgencyFilterBarProps { + selectedAgencyId: string | null; + onAgencyChange: (agencyId: string | null) => void; +} + +export const AgencyFilterBar: React.FC = ({ selectedAgencyId, onAgencyChange }) => { + const [agencies, setAgencies] = useState([]); + + useEffect(() => { + const loadAgencies = async () => { + try { + const data = await apiService.getAgencies(); + setAgencies(data); + } catch (err) { + console.error('Failed to load agencies for filter:', err); + } + }; + loadAgencies(); + }, []); + + return ( +
+ +
+ + +
+
+ ); +}; diff --git a/frontend/components/Analytics.tsx b/frontend/components/Analytics.tsx index 614669f..4dd9b7a 100755 --- a/frontend/components/Analytics.tsx +++ b/frontend/components/Analytics.tsx @@ -29,14 +29,15 @@ const TrendIndicator: React.FC<{ trend: 'up' | 'down' | 'stable' }> = ({ trend } return
Stable
; }; -export const Analytics: React.FC = () => { +export const Analytics: React.FC<{ agencyId?: string }> = ({ agencyId }) => { const [analytics, setAnalytics] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const loadAnalytics = async () => { + setIsLoading(true); try { - const data = await apiService.getAnalytics(); + const data = await apiService.getAnalytics(agencyId); setAnalytics(data); } catch (error) { console.error('Failed to load analytics:', error); @@ -45,7 +46,7 @@ export const Analytics: React.FC = () => { } }; loadAnalytics(); - }, []); + }, [agencyId]); // Calculate stats from API data // Exclude errors from denominator since they weren't successfully reviewed diff --git a/frontend/components/Campaigns.tsx b/frontend/components/Campaigns.tsx index 4cc785d..424d7b5 100755 --- a/frontend/components/Campaigns.tsx +++ b/frontend/components/Campaigns.tsx @@ -275,7 +275,8 @@ const CampaignList: React.FC<{ onOpenModal: () => void; onCampaignStatusChange: (campaignName: string, newStatus: 'In Progress' | 'Completed') => void; onDeleteCampaign: (campaign: typeof initialCampaigns[0]) => void; -}> = ({ onSelectCampaign, campaigns, onOpenModal, onCampaignStatusChange, onDeleteCampaign }) => { + readOnly?: boolean; +}> = ({ onSelectCampaign, campaigns, onOpenModal, onCampaignStatusChange, onDeleteCampaign, readOnly = false }) => { const [showCompleted, setShowCompleted] = useState(true); const [selectedCampaigns, setSelectedCampaigns] = useState>(new Set()); const [campaignToDelete, setCampaignToDelete] = useState(null); @@ -396,20 +397,22 @@ const CampaignList: React.FC<{
- + {!readOnly && ( + + )}
{/* Bulk Actions Bar */} - {selectedCampaigns.size > 0 && ( + {!readOnly && selectedCampaigns.size > 0 && (
@@ -438,6 +441,7 @@ const CampaignList: React.FC<{ + {!readOnly && ( + )} - + {!readOnly && } @@ -468,6 +473,7 @@ const CampaignList: React.FC<{ className={`hover:bg-info-light cursor-pointer ${isSelected ? 'bg-info-light' : index % 2 === 0 ? 'bg-white' : 'bg-grey-100'}`} onClick={() => onSelectCampaign(campaign.name)} > + {!readOnly && ( + )} + {!readOnly && ( + )} ); })} @@ -1714,6 +1724,7 @@ interface CampaignsProps { onResolveSubmit: (resolveData: Omit) => void; flaggedItems: FlaggedItem[]; resolvedItems: ResolvedItem[]; + readOnly?: boolean; } export const Campaigns: React.FC = ({ @@ -1736,6 +1747,7 @@ export const Campaigns: React.FC = ({ onResolveSubmit, flaggedItems, resolvedItems, + readOnly = false, }) => { const [isModalOpen, setIsModalOpen] = useState(false); @@ -1784,18 +1796,21 @@ export const Campaigns: React.FC = ({ return ( <> - setIsModalOpen(false)} - onAddCampaign={onAddNewCampaign} - brandGuidelines={dropdownOptions.brandGuidelines} - /> + {!readOnly && ( + setIsModalOpen(false)} + onAddCampaign={onAddNewCampaign} + brandGuidelines={dropdownOptions.brandGuidelines} + /> + )} setIsModalOpen(true)} - onCampaignStatusChange={onCampaignStatusChange} - onDeleteCampaign={(campaign) => onDeleteCampaign(campaign.name)} + onOpenModal={readOnly ? () => {} : () => setIsModalOpen(true)} + onCampaignStatusChange={readOnly ? () => {} : onCampaignStatusChange} + onDeleteCampaign={readOnly ? () => {} : (campaign) => onDeleteCampaign(campaign.name)} + readOnly={readOnly} /> ); diff --git a/frontend/components/Settings.tsx b/frontend/components/Settings.tsx index 9ee78c3..99c8acc 100755 --- a/frontend/components/Settings.tsx +++ b/frontend/components/Settings.tsx @@ -278,12 +278,13 @@ interface SettingsProps { onRemoveSubChannel: (channel: string, value: string) => void; onAddProofType: (channel: string, subChannel: string, value: string) => void; onRemoveProofType: (channel: string, subChannel: string, value: string) => void; + readOnly?: boolean; } type Tab = 'Campaigns' | 'Channels' | 'SubChannels' | 'ProofTypes' | 'Users'; -export const Settings: React.FC = ({ - options, +export const Settings: React.FC = ({ + options, onAddCampaign, onRemoveCampaign, onAddChannel, @@ -292,6 +293,7 @@ export const Settings: React.FC = ({ onRemoveSubChannel, onAddProofType, onRemoveProofType, + readOnly = false, }) => { const [activeTab, setActiveTab] = useState('Channels'); const [selectedChannel, setSelectedChannel] = useState(''); @@ -323,6 +325,12 @@ export const Settings: React.FC = ({

Configure application defaults and user access.

+ {readOnly && ( +
+ + View-only mode +
+ )}
@@ -364,6 +372,7 @@ export const Settings: React.FC = ({ onAdd={onAddCampaign} onRemove={onRemoveCampaign} placeholder="e.g. Q4 Marketing" + disabled={readOnly} />
)} @@ -376,6 +385,7 @@ export const Settings: React.FC = ({ onAdd={onAddChannel} onRemove={onRemoveChannel} placeholder="e.g. Social, OOH" + disabled={readOnly} /> )} @@ -402,7 +412,7 @@ export const Settings: React.FC = ({ items={selectedChannel ? Object.keys(options.channels[selectedChannel]) : []} onAdd={(val) => onAddSubChannel(selectedChannel, val)} onRemove={(val) => onRemoveSubChannel(selectedChannel, val)} - disabled={!selectedChannel} + disabled={readOnly || !selectedChannel} placeholder="e.g. Meta, Video" /> @@ -457,7 +467,7 @@ export const Settings: React.FC = ({ } onAdd={(val) => onAddProofType(selectedChannel, selectedSubChannel, val)} onRemove={(val) => onRemoveProofType(selectedChannel, selectedSubChannel, val)} - disabled={!selectedChannel || !selectedSubChannel} + disabled={readOnly || !selectedChannel || !selectedSubChannel} placeholder="e.g. In-feed 1x1, 300x600" /> diff --git a/frontend/components/Sidebar.tsx b/frontend/components/Sidebar.tsx index 7e4485f..c6dd862 100755 --- a/frontend/components/Sidebar.tsx +++ b/frontend/components/Sidebar.tsx @@ -8,16 +8,22 @@ import { UserIcon } from './icons/UserIcon'; import { CampaignsIcon } from './icons/CampaignsIcon'; import { AuditIcon } from './icons/AuditIcon'; import { KnowledgeBaseIcon } from './icons/KnowledgeBaseIcon'; +import type { UserRole } from '../types'; -const navigation = [ - { name: 'Home', icon: DashboardIcon }, - { name: 'Campaigns', icon: CampaignsIcon }, - // { name: 'WIP Reviewer', icon: ClipboardIcon }, // Hidden: Moved to Settings > Beta - // { name: 'CopyGenAI', icon: CopyGenAIIcon }, // Hidden: Moved to Settings > Beta - { name: 'Analytics', icon: AnalyticsIcon }, - { name: 'Auditing', icon: AuditIcon }, - { name: 'Knowledge Base', icon: KnowledgeBaseIcon, adminOnly: true }, - { name: 'Settings', icon: SettingsIcon }, +interface NavItem { + name: string; + icon: React.FC<{ className?: string }>; + roles: UserRole[]; +} + +const navigation: NavItem[] = [ + { name: 'Home', icon: DashboardIcon, roles: ['super_admin', 'oversight_admin', 'agency_admin', 'basic_user'] }, + { name: 'Campaigns', icon: CampaignsIcon, roles: ['super_admin', 'oversight_admin', 'agency_admin', 'basic_user'] }, + { name: 'Analytics', icon: AnalyticsIcon, roles: ['super_admin', 'oversight_admin', 'agency_admin'] }, + { name: 'Auditing', icon: AuditIcon, roles: ['super_admin', 'oversight_admin'] }, + { name: 'Knowledge Base', icon: KnowledgeBaseIcon, roles: ['super_admin'] }, + { name: 'User Management', icon: UserIcon, roles: ['super_admin'] }, + { name: 'Settings', icon: SettingsIcon, roles: ['super_admin', 'oversight_admin', 'agency_admin'] }, ]; interface SidebarProps { @@ -25,10 +31,10 @@ interface SidebarProps { onNavigate: (viewName: string) => void; userName?: string; userEmail?: string; - isAdmin?: boolean; + userRole: UserRole; } -export const Sidebar: React.FC = ({ activeItem, onNavigate, userName, userEmail, isAdmin = true }) => { +export const Sidebar: React.FC = ({ activeItem, onNavigate, userName, userEmail, userRole }) => { return (
Campaign Name Proofs Status Created By Owning Agency Last ModifiedActionsActions
{campaign.name} {campaign.proofs} @@ -488,6 +495,7 @@ const CampaignList: React.FC<{ onClick={(e) => e.stopPropagation()} className={getStatusSelectClasses(campaign.status)} aria-label={`Change status for ${campaign.name}`} + disabled={readOnly} > @@ -498,6 +506,7 @@ const CampaignList: React.FC<{ {campaign.agencyLead} {campaign.agency} {formatDate(campaign.lastModified)}
+ + + + + + + + + + + {users.map((user, index) => ( + + + + + + + + ))} + {users.length === 0 && ( + + + + )} + +
NameEmailRoleAgencyCreated
+
+
+ +
+
+
{user.name}
+
+
+
+ {user.email} + +
+ + +
+
+
+ + +
+
+ {formatDate(user.created_at)} +
+ No users found. +
+
+
+
+ + {/* Agency Management */} +
+

Agencies ({agencies.length})

+
+
+ setNewAgencyName(e.target.value)} + placeholder="New agency name..." + className="flex-grow p-2 border-2 border-grey-700 rounded-[10px] focus:ring-2 focus:ring-active-blue focus:border-active-blue transition text-black-title" + /> + +
+ +
+ {agencies.map(agency => ( +
+ {agency.name} + + {users.filter(u => u.agency_id === agency.id).length} user(s) + +
+ ))} + {agencies.length === 0 && ( +
+ No agencies created yet. +
+ )} +
+
+
+ + ); +}; diff --git a/frontend/contexts/UserContext.tsx b/frontend/contexts/UserContext.tsx new file mode 100644 index 0000000..4232563 --- /dev/null +++ b/frontend/contexts/UserContext.tsx @@ -0,0 +1,84 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import apiService from '../services/apiService'; +import type { UserRole, AppUser } from '../types'; + +interface UserContextValue { + user: AppUser | null; + isLoading: boolean; + /** Convenience booleans derived from user.role */ + isSuperAdmin: boolean; + isOversightAdmin: boolean; + canWrite: boolean; + canSeeAnalytics: boolean; + canSeeAuditing: boolean; + canSeeKnowledgeBase: boolean; + canSeeSettings: boolean; + canSeeUserManagement: boolean; + canEditSettings: boolean; + /** Re-fetch user from backend (e.g. after role change) */ + refresh: () => Promise; +} + +const UserContext = createContext({ + user: null, + isLoading: true, + isSuperAdmin: false, + isOversightAdmin: false, + canWrite: false, + canSeeAnalytics: false, + canSeeAuditing: false, + canSeeKnowledgeBase: false, + canSeeSettings: false, + canSeeUserManagement: false, + canEditSettings: false, + refresh: async () => {}, +}); + +export const useUser = () => useContext(UserContext); + +export const UserProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const fetchUser = async () => { + try { + const me = await apiService.getMe(); + setUser({ + id: me.id, + email: me.email, + name: me.name, + role: me.role as UserRole, + agencyId: me.agency_id, + agencyName: me.agency_name, + }); + } catch (error) { + console.error('Failed to fetch current user:', error); + setUser(null); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchUser(); + }, []); + + const role = user?.role; + + const value: UserContextValue = { + user, + isLoading, + isSuperAdmin: role === 'super_admin', + isOversightAdmin: role === 'oversight_admin', + canWrite: role !== 'oversight_admin' && role != null, + canSeeAnalytics: role === 'super_admin' || role === 'oversight_admin' || role === 'agency_admin', + canSeeAuditing: role === 'super_admin' || role === 'oversight_admin', + canSeeKnowledgeBase: role === 'super_admin', + canSeeSettings: role === 'super_admin' || role === 'oversight_admin' || role === 'agency_admin', + canSeeUserManagement: role === 'super_admin', + canEditSettings: role === 'super_admin', + refresh: fetchUser, + }; + + return {children}; +}; diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts index 38f4da3..fde78e4 100755 --- a/frontend/services/apiService.ts +++ b/frontend/services/apiService.ts @@ -138,9 +138,15 @@ class ApiService { return response.json(); } + // Current user endpoint + async getMe(): Promise { + return this.fetch('/me'); + } + // Campaign endpoints - async getCampaigns(): Promise { - return this.fetch('/campaigns'); + async getCampaigns(agencyId?: string): Promise { + const params = agencyId ? `?agency_id=${agencyId}` : ''; + return this.fetch(`/campaigns${params}`); } async getCampaign(id: string): Promise { @@ -241,21 +247,25 @@ class ApiService { }); } - async getFlaggedItems(): Promise { - return this.fetch('/audit/flagged'); + async getFlaggedItems(agencyId?: string): Promise { + const params = agencyId ? `?agency_id=${agencyId}` : ''; + return this.fetch(`/audit/flagged${params}`); } - async getResolvedItems(): Promise { - return this.fetch('/audit/resolved'); + async getResolvedItems(agencyId?: string): Promise { + const params = agencyId ? `?agency_id=${agencyId}` : ''; + return this.fetch(`/audit/resolved${params}`); } - async getErrorItems(): Promise { - return this.fetch('/audit/errors'); + async getErrorItems(agencyId?: string): Promise { + const params = agencyId ? `?agency_id=${agencyId}` : ''; + return this.fetch(`/audit/errors${params}`); } // Analytics endpoint - async getAnalytics(): Promise { - return this.fetch('/analytics'); + async getAnalytics(agencyId?: string): Promise { + const params = agencyId ? `?agency_id=${agencyId}` : ''; + return this.fetch(`/analytics${params}`); } // Helper to convert API response to frontend format @@ -385,6 +395,25 @@ class ApiService { }); } + // User management endpoints (super_admin only) + async getUsers(): Promise { + return this.fetch('/users'); + } + + async updateUser(userId: string, data: { role?: string; agency_id?: string | null }): Promise { + return this.fetch(`/users/${userId}`, { + method: 'PUT', + body: JSON.stringify(data), + }); + } + + async createAgency(name: string): Promise { + return this.fetch('/agencies', { + method: 'POST', + body: JSON.stringify({ name }), + }); + } + // Agency endpoints async getAgencies(): Promise { return this.fetch('/agencies'); @@ -480,5 +509,24 @@ export interface AgencyResponse { name: string; } +export interface CurrentUserResponse { + id: string; + email: string; + name: string; + role: string; + agency_id: string | null; + agency_name: string | null; +} + +export interface UserManagementResponse { + id: string; + email: string; + name: string; + role: string; + agency: string | null; + agency_id: string | null; + created_at: string; +} + export const apiService = new ApiService(); export default apiService; diff --git a/frontend/types.ts b/frontend/types.ts index ea7830e..d700561 100755 --- a/frontend/types.ts +++ b/frontend/types.ts @@ -1,6 +1,18 @@ // Fix: Broke a circular dependency by defining the AgentName type directly in this file instead of importing it. export type AgentName = 'Legal Agent' | 'Brand Agent' | 'Channel Best Practices Agent' | 'Channel Tech Specs Agent'; +// RBAC types +export type UserRole = 'super_admin' | 'oversight_admin' | 'agency_admin' | 'basic_user'; + +export interface AppUser { + id: string; + email: string; + name: string; + role: UserRole; + agencyId: string | null; + agencyName: string | null; +} + export type AgentStatus = 'pending' | 'in-progress' | 'complete' | 'issues-found' | 'error'; export type ReviewStatus = { diff --git a/frontend/utils/urlState.ts b/frontend/utils/urlState.ts index f86a7cf..1ac0616 100644 --- a/frontend/utils/urlState.ts +++ b/frontend/utils/urlState.ts @@ -1,4 +1,4 @@ -type View = 'Home' | 'Analytics' | 'Campaigns' | 'WIP Reviewer' | 'CopyGenAI' | 'Settings' | 'Profile' | 'Auditing'; +type View = 'Home' | 'Analytics' | 'Campaigns' | 'WIP Reviewer' | 'CopyGenAI' | 'Settings' | 'Profile' | 'Auditing' | 'Knowledge Base' | 'User Management'; export interface UrlNavigationState { view: View; @@ -6,7 +6,7 @@ export interface UrlNavigationState { proofId: string | null; } -const VALID_VIEWS: View[] = ['Home', 'Analytics', 'Campaigns', 'WIP Reviewer', 'CopyGenAI', 'Settings', 'Profile', 'Auditing']; +const VALID_VIEWS: View[] = ['Home', 'Analytics', 'Campaigns', 'WIP Reviewer', 'CopyGenAI', 'Settings', 'Profile', 'Auditing', 'Knowledge Base', 'User Management']; export function parseUrlState(): UrlNavigationState { const params = new URLSearchParams(window.location.search);