import React, { useState, useEffect, useRef, useMemo } from 'react'; import { Outlet, useLocation, useSearchParams, useNavigate } from 'react-router-dom'; import Sidebar from '../components/Sidebar'; import TopBar from '../components/TopBar'; import TrialBanner from '../components/TrialBanner'; import SandboxBanner from '../components/SandboxBanner'; import QuotaWarningBanner from '../components/QuotaWarningBanner'; import { Business, User } from '../types'; import MasqueradeBanner from '../components/MasqueradeBanner'; import OnboardingWizard from '../components/OnboardingWizard'; import TicketModal from '../components/TicketModal'; import { useStopMasquerade } from '../hooks/useAuth'; import { useNotificationWebSocket } from '../hooks/useNotificationWebSocket'; import { useTicket } from '../hooks/useTickets'; import { MasqueradeStackEntry } from '../api/auth'; import { useScrollToTop } from '../hooks/useScrollToTop'; import { SandboxProvider, useSandbox } from '../contexts/SandboxContext'; import { applyColorPalette, applyBrandColors, defaultColorPalette } from '../utils/colorUtils'; interface BusinessLayoutProps { business: Business; user: User; darkMode: boolean; toggleTheme: () => void; onSignOut: () => void; updateBusiness: (updates: Partial) => void; } /** * Wrapper component for SandboxBanner that uses the sandbox context */ const SandboxBannerWrapper: React.FC = () => { const { isSandbox, toggleSandbox, isToggling } = useSandbox(); return ( toggleSandbox(false)} isSwitching={isToggling} /> ); }; const BusinessLayoutContent: React.FC = ({ business, user, darkMode, toggleTheme, onSignOut, updateBusiness }) => { const [isCollapsed, setIsCollapsed] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [showOnboarding, setShowOnboarding] = useState(false); const [ticketModalId, setTicketModalId] = useState(null); const mainContentRef = useRef(null); const location = useLocation(); const [searchParams] = useSearchParams(); const navigate = useNavigate(); useScrollToTop(); // Fetch ticket data when modal is opened from notification const { data: ticketFromNotification } = useTicket(ticketModalId || undefined); const handleTicketClick = (ticketId: string) => { setTicketModalId(ticketId); }; const closeTicketModal = () => { setTicketModalId(null); }; // Set CSS custom properties for brand colors (primary palette + secondary color) useEffect(() => { applyBrandColors( business.primaryColor || '#2563eb', business.secondaryColor || business.primaryColor || '#2563eb' ); // Cleanup: reset to defaults when component unmounts return () => { applyColorPalette(defaultColorPalette); document.documentElement.style.setProperty('--color-brand-secondary', '#0ea5e9'); }; }, [business.primaryColor, business.secondaryColor]); // Check for trial expiration and redirect useEffect(() => { // Don't check if already on trial-expired page if (location.pathname === '/trial-expired') { return; } // Redirect to trial-expired page if trial has expired if (business.isTrialExpired && business.status === 'Trial') { navigate('/trial-expired', { replace: true }); } }, [business.isTrialExpired, business.status, location.pathname, navigate]); // Masquerade logic - now using the stack system const [masqueradeStack, setMasqueradeStack] = useState([]); const stopMasqueradeMutation = useStopMasquerade(); useEffect(() => { const stackJson = localStorage.getItem('masquerade_stack'); if (stackJson) { try { setMasqueradeStack(JSON.parse(stackJson)); } catch (e) { console.error('Failed to parse masquerade stack data', e); } } }, []); const handleStopMasquerade = () => { stopMasqueradeMutation.mutate(); }; useNotificationWebSocket(); // Activate the notification WebSocket listener // Get the previous user from the stack (the one we'll return to) const previousUser = masqueradeStack.length > 0 ? { id: masqueradeStack[masqueradeStack.length - 1].user_id, username: masqueradeStack[masqueradeStack.length - 1].username, name: masqueradeStack[masqueradeStack.length - 1].username, role: masqueradeStack[masqueradeStack.length - 1].role, email: '', is_staff: false, is_superuser: false, } as User : null; // Get the original user (first in the stack) const originalUser = masqueradeStack.length > 0 ? { id: masqueradeStack[0].user_id, username: masqueradeStack[0].username, name: masqueradeStack[0].username, role: masqueradeStack[0].role, email: '', is_staff: false, is_superuser: false, } as User : null; useEffect(() => { mainContentRef.current?.focus(); setIsMobileMenuOpen(false); }, [location.pathname]); // Check if returning from Stripe Connect onboarding useEffect(() => { const isOnboardingReturn = searchParams.get('onboarding') === 'true'; // Only show onboarding if returning from Stripe Connect if (isOnboardingReturn) { setShowOnboarding(true); } }, [searchParams]); const handleOnboardingComplete = () => { setShowOnboarding(false); // Update local state immediately so wizard doesn't re-appear updateBusiness({ initialSetupComplete: true }); }; const handleOnboardingSkip = () => { setShowOnboarding(false); // If they skip Stripe setup, disable payments updateBusiness({ paymentsEnabled: false }); }; return (
{ }} />
{isMobileMenuOpen &&
setIsMobileMenuOpen(false)}>
}
setIsCollapsed(!isCollapsed)} />
{originalUser && ( )} {/* Quota overage warning banner - show for owners and managers */} {user.quota_overages && user.quota_overages.length > 0 && ( )} {/* Sandbox mode banner */} {/* Show trial banner if trial is active and payments not yet enabled */} {business.isTrialActive && !business.paymentsEnabled && business.plan !== 'Free' && ( )} setIsMobileMenuOpen(true)} onTicketClick={handleTicketClick} />
{/* Pass all necessary context down to child routes */}
{/* Onboarding wizard for paid-tier businesses */} {showOnboarding && ( )} {/* Ticket modal opened from notification */} {ticketModalId && ticketFromNotification && ( )}
); }; /** * Business Layout with Sandbox Provider */ const BusinessLayout: React.FC = (props) => { return ( ); }; export default BusinessLayout;