feat: add advanced analytics and territory management system
- Add comprehensive analytics components with export functionality - Implement territory management with manager performance tracking - Add seatmap components for venue layout management - Create customer management features with modal interface - Add advanced hooks for dashboard flags and territory data - Implement seat selection and venue management utilities - Add type definitions for ticketing and seatmap systems 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,9 +2,10 @@ import { useState, useRef, useEffect } from 'react';
|
||||
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Menu, ChevronRight, Settings, LogOut, Sun, Moon } from 'lucide-react';
|
||||
import { Menu, ChevronRight, Settings, LogOut, Sun, Moon, Building2 } from 'lucide-react';
|
||||
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import { useCurrentOrg } from '../../stores/currentOrg';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useTheme } from '../../hooks/useTheme';
|
||||
import { Button } from '../ui/Button';
|
||||
|
||||
@@ -16,6 +17,7 @@ export function Header({ onToggleSidebar }: HeaderProps) {
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const { user, logout, isLoading } = useAuth();
|
||||
const { org, loading: orgLoading } = useCurrentOrg();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const userMenuRef = useRef<HTMLDivElement>(null);
|
||||
@@ -84,9 +86,9 @@ export function Header({ onToggleSidebar }: HeaderProps) {
|
||||
.slice(0, 2);
|
||||
|
||||
return (
|
||||
<div className="h-16 border-b border-slate-200 dark:border-slate-700 bg-white/80 dark:bg-slate-900/80 backdrop-blur-md">
|
||||
<div className="h-16 border-b border-org-default bg-org-surface backdrop-blur-md">
|
||||
<div className="h-full px-4 lg:px-6 flex items-center justify-between">
|
||||
{/* Left section: Mobile menu button + Breadcrumbs */}
|
||||
{/* Left section: Organization branding + Mobile menu button + Breadcrumbs */}
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Mobile menu button */}
|
||||
<Button
|
||||
@@ -99,22 +101,92 @@ export function Header({ onToggleSidebar }: HeaderProps) {
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
{/* Organization branding */}
|
||||
{!orgLoading && org ? (
|
||||
<div className="flex items-center space-x-3">
|
||||
{org.branding?.logoUrl ? (
|
||||
<img
|
||||
src={org.branding.logoUrl}
|
||||
alt={`${org.name} logo`}
|
||||
className="h-8 w-auto max-w-32 object-contain"
|
||||
onError={(e) => {
|
||||
// Hide image on error and show fallback
|
||||
const img = e.target as HTMLImageElement;
|
||||
img.style.display = 'none';
|
||||
// Show monogram fallback
|
||||
const fallback = img.parentElement?.querySelector('.org-fallback');
|
||||
if (fallback) fallback.classList.remove('hidden');
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* Fallback monogram when no logo */}
|
||||
{!org.branding?.logoUrl && (
|
||||
<div className="h-8 w-8 rounded-lg bg-org-accent flex items-center justify-center text-org-canvas text-sm font-bold">
|
||||
{org.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hidden fallback for logo error */}
|
||||
<div className="org-fallback hidden h-8 w-8 rounded-lg bg-org-accent flex items-center justify-center text-org-canvas text-sm font-bold">
|
||||
{org.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
|
||||
<div className="hidden md:block">
|
||||
<h1 className="text-sm font-semibold text-org-primary truncate max-w-48">
|
||||
{org.name}
|
||||
</h1>
|
||||
{org.domains?.length && (
|
||||
<p className="text-xs text-org-secondary truncate max-w-48">
|
||||
{org.domains.find(d => d.primary)?.host || org.domains[0]?.host}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : orgLoading ? (
|
||||
// Loading skeleton
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="h-8 w-8 rounded-lg bg-org-surface animate-pulse" />
|
||||
<div className="hidden md:block space-y-1">
|
||||
<div className="h-4 w-24 bg-org-surface animate-pulse rounded" />
|
||||
<div className="h-3 w-16 bg-org-surface animate-pulse rounded" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// No organization fallback
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="h-8 w-8 rounded-lg bg-gray-400 flex items-center justify-center">
|
||||
<Building2 className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<div className="hidden md:block">
|
||||
<h1 className="text-sm font-semibold text-org-primary">
|
||||
Black Canyon Tickets
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Separator */}
|
||||
{org && (
|
||||
<div className="hidden lg:block w-px h-6 bg-org-primary/20" />
|
||||
)}
|
||||
|
||||
{/* Breadcrumbs */}
|
||||
<nav aria-label="Breadcrumb" className="hidden sm:flex">
|
||||
<ol className="flex items-center space-x-2 text-sm">
|
||||
{breadcrumbs.map((crumb, index) => (
|
||||
<li key={crumb.path} className="flex items-center">
|
||||
{index > 0 && (
|
||||
<ChevronRight className="h-4 w-4 text-slate-400 mx-2" aria-hidden="true" />
|
||||
<ChevronRight className="h-4 w-4 text-org-secondary mx-2" aria-hidden="true" />
|
||||
)}
|
||||
{index === breadcrumbs.length - 1 ? (
|
||||
<span className="text-slate-900 dark:text-slate-100 font-medium">
|
||||
<span className="text-org-primary font-medium">
|
||||
{crumb.label}
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
to={crumb.path}
|
||||
className="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100
|
||||
className="text-org-secondary hover:text-org-primary
|
||||
transition-colors duration-200"
|
||||
>
|
||||
{crumb.label}
|
||||
@@ -134,7 +206,7 @@ export function Header({ onToggleSidebar }: HeaderProps) {
|
||||
size="sm"
|
||||
onClick={toggleTheme}
|
||||
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} theme`}
|
||||
className="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100"
|
||||
className="text-org-secondary hover:text-org-primary"
|
||||
>
|
||||
{theme === 'light' ? (
|
||||
<Moon className="h-5 w-5" />
|
||||
@@ -152,8 +224,9 @@ export function Header({ onToggleSidebar }: HeaderProps) {
|
||||
onClick={() => setUserMenuOpen(!userMenuOpen)}
|
||||
aria-expanded={userMenuOpen}
|
||||
aria-haspopup="true"
|
||||
className="flex items-center space-x-2 text-slate-600 dark:text-slate-400
|
||||
hover:text-slate-900 dark:hover:text-slate-100"
|
||||
data-testid="user-menu-button"
|
||||
className="flex items-center space-x-2 text-org-secondary
|
||||
hover:text-org-primary"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{user.avatar ? (
|
||||
@@ -161,10 +234,14 @@ export function Header({ onToggleSidebar }: HeaderProps) {
|
||||
src={user.avatar}
|
||||
alt={user.name}
|
||||
className="h-8 w-8 rounded-full object-cover"
|
||||
onError={(e) => {
|
||||
// Hide image on error and show initials fallback
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-gold-400 to-gold-600
|
||||
flex items-center justify-center text-white text-sm font-medium">
|
||||
<div className="h-8 w-8 rounded-full bg-org-accent
|
||||
flex items-center justify-center text-org-canvas text-sm font-medium">
|
||||
{getInitials(user.name)}
|
||||
</div>
|
||||
)}
|
||||
@@ -175,39 +252,70 @@ export function Header({ onToggleSidebar }: HeaderProps) {
|
||||
|
||||
{/* User dropdown menu */}
|
||||
{userMenuOpen && (
|
||||
<div className="absolute right-0 mt-2 w-56 bg-white dark:bg-slate-800 rounded-lg
|
||||
shadow-lg border border-slate-200 dark:border-slate-700 py-1 z-50">
|
||||
<div className="px-4 py-3 border-b border-slate-200 dark:border-slate-700">
|
||||
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">
|
||||
<div className="absolute right-0 mt-2 w-56 bg-org-surface rounded-lg
|
||||
shadow-lg border border-org-default py-1 z-50 org-glass">
|
||||
<div className="px-4 py-3 border-b border-org-default">
|
||||
<p className="text-sm font-medium text-org-primary">
|
||||
{user.name}
|
||||
</p>
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
<p className="text-xs text-org-secondary">
|
||||
{user.email}
|
||||
</p>
|
||||
<div className="flex items-center mt-1">
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
|
||||
bg-gold-100 dark:bg-gold-900 text-gold-800 dark:text-gold-200">
|
||||
bg-org-accent text-org-canvas">
|
||||
{user.role}
|
||||
</span>
|
||||
<span className="ml-2 text-xs text-slate-500 dark:text-slate-400">
|
||||
{user.organization.name}
|
||||
</span>
|
||||
{org && (
|
||||
<span className="ml-2 text-xs text-org-secondary truncate">
|
||||
{org.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to="/settings"
|
||||
className="flex items-center px-4 py-2 text-sm text-slate-700 dark:text-slate-300
|
||||
hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200"
|
||||
className="flex items-center px-4 py-2 text-sm text-org-secondary
|
||||
hover:bg-org-accent-light transition-colors duration-200"
|
||||
onClick={() => setUserMenuOpen(false)}
|
||||
>
|
||||
<Settings className="h-4 w-4 mr-3" />
|
||||
Account Settings
|
||||
</Link>
|
||||
|
||||
{org && (
|
||||
<>
|
||||
<div className="border-t border-org-default my-1" />
|
||||
<div className="px-4 py-2">
|
||||
<p className="text-xs font-medium text-org-secondary uppercase tracking-wide">
|
||||
Organization
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
to={`/admin/branding`}
|
||||
className="flex items-center px-4 py-2 text-sm text-org-secondary
|
||||
hover:bg-org-accent-light transition-colors duration-200"
|
||||
onClick={() => setUserMenuOpen(false)}
|
||||
>
|
||||
<div className="h-4 w-4 mr-3 rounded bg-org-accent" />
|
||||
Branding Settings
|
||||
</Link>
|
||||
<Link
|
||||
to={`/admin/domains`}
|
||||
className="flex items-center px-4 py-2 text-sm text-org-secondary
|
||||
hover:bg-org-accent-light transition-colors duration-200"
|
||||
onClick={() => setUserMenuOpen(false)}
|
||||
>
|
||||
<div className="h-4 w-4 mr-3 rounded bg-gradient-to-br from-green-400 to-teal-500" />
|
||||
Domains
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="w-full flex items-center px-4 py-2 text-sm text-slate-700 dark:text-slate-300
|
||||
hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200
|
||||
className="w-full flex items-center px-4 py-2 text-sm text-org-secondary
|
||||
hover:bg-org-accent-light transition-colors duration-200
|
||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onClick={handleLogout}
|
||||
disabled={isLoading}
|
||||
|
||||
Reference in New Issue
Block a user