All files / src/layouts CustomerLayout.tsx

100% Statements 18/18
100% Branches 10/10
100% Functions 4/4
100% Lines 18/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129                                    2x 31x 31x 31x     31x 31x     31x   1x     31x 29x 29x 3x 3x   1x         31x 1x       31x                       31x                                                                                                                                      
import React, { useState, useEffect, useRef } from 'react';
import { Outlet, Link, useNavigate } from 'react-router-dom';
import { User, Business } from '../types';
import { LayoutDashboard, CalendarPlus, CreditCard, HelpCircle, Sun, Moon } from 'lucide-react';
import MasqueradeBanner from '../components/MasqueradeBanner';
import UserProfileDropdown from '../components/UserProfileDropdown';
import NotificationDropdown from '../components/NotificationDropdown';
import { useStopMasquerade } from '../hooks/useAuth';
import { MasqueradeStackEntry } from '../api/auth';
import { useScrollToTop } from '../hooks/useScrollToTop';
 
interface CustomerLayoutProps {
  business: Business;
  user: User;
  darkMode: boolean;
  toggleTheme: () => void;
}
 
const CustomerLayout: React.FC<CustomerLayoutProps> = ({ business, user, darkMode, toggleTheme }) => {
  const navigate = useNavigate();
  const mainContentRef = useRef<HTMLElement>(null);
  useScrollToTop(mainContentRef);
 
  // Masquerade logic
  const [masqueradeStack, setMasqueradeStack] = useState<MasqueradeStackEntry[]>([]);
  const stopMasqueradeMutation = useStopMasquerade();
 
  // Handle ticket notification click - navigate to support page
  const handleTicketClick = (ticketId: string) => {
    // Navigate to support page - the CustomerSupport component will handle showing tickets
    navigate('/support');
  };
 
  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();
  };
 
  // 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;
 
  return (
    <div className="h-full flex flex-col bg-gray-50 dark:bg-gray-900">
      {originalUser && (
        <MasqueradeBanner
          effectiveUser={user}
          originalUser={originalUser}
          previousUser={null}
          onStop={handleStopMasquerade}
        />
      )}
      <header
        className="text-white shadow-md"
        style={{ backgroundColor: business.primaryColor }}
      >
        <div className="container mx-auto px-4 sm:px-6 lg:px-8">
          <div className="flex items-center justify-between h-16">
            {/* Logo and Business Name */}
            <div className="flex items-center gap-3">
              <div className="flex items-center justify-center w-8 h-8 bg-white rounded-lg font-bold text-lg" style={{ color: business.primaryColor }}>
                {business.name.charAt(0)}
              </div>
              <span className="font-bold text-lg">{business.name}</span>
            </div>
 
            {/* Navigation and User Menu */}
            <div className="flex items-center gap-6">
              <nav className="hidden md:flex gap-1">
                <Link to="/" className="text-sm font-medium text-white/80 hover:text-white transition-colors flex items-center gap-2 px-3 py-2 rounded-md hover:bg-white/10">
                  <LayoutDashboard size={16} /> Dashboard
                </Link>
                <Link to="/book" className="text-sm font-medium text-white/80 hover:text-white transition-colors flex items-center gap-2 px-3 py-2 rounded-md hover:bg-white/10">
                  <CalendarPlus size={16} /> Book Appointment
                </Link>
                <Link to="/payments" className="text-sm font-medium text-white/80 hover:text-white transition-colors flex items-center gap-2 px-3 py-2 rounded-md hover:bg-white/10">
                  <CreditCard size={16} /> Billing
                </Link>
                <Link to="/support" className="text-sm font-medium text-white/80 hover:text-white transition-colors flex items-center gap-2 px-3 py-2 rounded-md hover:bg-white/10">
                  <HelpCircle size={16} /> Support
                </Link>
              </nav>
 
              {/* Notifications */}
              <NotificationDropdown variant="light" onTicketClick={handleTicketClick} />
 
              {/* Dark Mode Toggle */}
              <button
                onClick={toggleTheme}
                className="p-2 rounded-md text-white/80 hover:text-white hover:bg-white/10 transition-colors"
                aria-label={darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
              >
                {darkMode ? <Sun size={20} /> : <Moon size={20} />}
              </button>
 
              <UserProfileDropdown user={user} variant="light" />
            </div>
          </div>
        </div>
      </header>
      <main ref={mainContentRef} className="flex-1 overflow-y-auto">
        <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
          <Outlet context={{ business, user }} />
        </div>
      </main>
    </div>
  );
};
 
export default CustomerLayout;