- Add SMTP fields to TicketEmailSettings model (host, port, TLS/SSL, credentials, from email/name) - Update serializers with SMTP fields and is_smtp_configured flag - Add TicketEmailTestSmtpView for testing SMTP connections - Update frontend API types and hooks for SMTP settings - Add collapsible IMAP and SMTP configuration sections with "Configured" badges - Fix TypeScript errors in mockData.ts (missing required fields, type mismatches) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
350 lines
16 KiB
TypeScript
350 lines
16 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Link, useLocation } from 'react-router-dom';
|
|
import {
|
|
LayoutDashboard,
|
|
CalendarDays,
|
|
Settings,
|
|
Users,
|
|
CreditCard,
|
|
MessageSquare,
|
|
LogOut,
|
|
ClipboardList,
|
|
Briefcase,
|
|
Ticket,
|
|
HelpCircle,
|
|
Code,
|
|
ChevronDown,
|
|
BookOpen,
|
|
FileQuestion,
|
|
LifeBuoy,
|
|
Zap,
|
|
Plug,
|
|
Package,
|
|
Clock,
|
|
Store,
|
|
Mail
|
|
} from 'lucide-react';
|
|
import { Business, User } from '../types';
|
|
import { useLogout } from '../hooks/useAuth';
|
|
import SmoothScheduleLogo from './SmoothScheduleLogo';
|
|
|
|
interface SidebarProps {
|
|
business: Business;
|
|
user: User;
|
|
isCollapsed: boolean;
|
|
toggleCollapse: () => void;
|
|
}
|
|
|
|
const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCollapse }) => {
|
|
const { t } = useTranslation();
|
|
const location = useLocation();
|
|
const { role } = user;
|
|
const logoutMutation = useLogout();
|
|
const [isHelpOpen, setIsHelpOpen] = useState(location.pathname.startsWith('/help') || location.pathname === '/support');
|
|
const [isPluginsOpen, setIsPluginsOpen] = useState(location.pathname.startsWith('/plugins') || location.pathname === '/plugins/marketplace');
|
|
|
|
const getNavClass = (path: string, exact: boolean = false, disabled: boolean = false) => {
|
|
const isActive = exact
|
|
? location.pathname === path
|
|
: location.pathname.startsWith(path);
|
|
|
|
const baseClasses = `flex items-center gap-3 py-3 text-base font-medium rounded-lg transition-colors`;
|
|
const collapsedClasses = isCollapsed ? 'px-3 justify-center' : 'px-4';
|
|
const activeClasses = 'bg-white/10 text-white';
|
|
const inactiveClasses = 'text-white/70 hover:text-white hover:bg-white/5';
|
|
const disabledClasses = 'text-white/30 cursor-not-allowed';
|
|
|
|
if (disabled) {
|
|
return `${baseClasses} ${collapsedClasses} ${disabledClasses}`;
|
|
}
|
|
|
|
return `${baseClasses} ${collapsedClasses} ${isActive ? activeClasses : inactiveClasses}`;
|
|
};
|
|
|
|
const canViewAdminPages = role === 'owner' || role === 'manager';
|
|
const canViewManagementPages = role === 'owner' || role === 'manager' || role === 'staff';
|
|
const canViewSettings = role === 'owner';
|
|
// Tickets: owners/managers always, staff only with permission
|
|
const canViewTickets = role === 'owner' || role === 'manager' || (role === 'staff' && user.can_access_tickets);
|
|
|
|
const getDashboardLink = () => {
|
|
if (role === 'resource') return '/';
|
|
return '/';
|
|
};
|
|
|
|
const handleSignOut = () => {
|
|
logoutMutation.mutate();
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={`flex flex-col h-full text-white shrink-0 transition-all duration-300 ${isCollapsed ? 'w-20' : 'w-64'}`}
|
|
style={{
|
|
background: `linear-gradient(to bottom right, ${business.primaryColor}, ${business.secondaryColor || business.primaryColor})`
|
|
}}
|
|
>
|
|
<button
|
|
onClick={toggleCollapse}
|
|
className={`flex items-center gap-3 w-full text-left px-6 py-8 ${isCollapsed ? 'justify-center' : ''} hover:bg-white/5 transition-colors focus:outline-none`}
|
|
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
>
|
|
{/* Logo-only mode: full width */}
|
|
{business.logoDisplayMode === 'logo-only' && business.logoUrl ? (
|
|
<div className="flex items-center justify-center w-full">
|
|
<img
|
|
src={business.logoUrl}
|
|
alt={business.name}
|
|
className="max-w-full max-h-16 object-contain"
|
|
/>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* Logo/Icon display */}
|
|
{business.logoUrl && business.logoDisplayMode !== 'text-only' ? (
|
|
<div className="flex items-center justify-center w-10 h-10 shrink-0">
|
|
<img
|
|
src={business.logoUrl}
|
|
alt={business.name}
|
|
className="w-full h-full object-contain"
|
|
/>
|
|
</div>
|
|
) : business.logoDisplayMode !== 'logo-only' && (
|
|
<div className="flex items-center justify-center w-10 h-10 bg-white rounded-lg text-brand-600 font-bold text-xl shrink-0" style={{ color: business.primaryColor }}>
|
|
{business.name.substring(0, 2).toUpperCase()}
|
|
</div>
|
|
)}
|
|
|
|
{/* Text display */}
|
|
{!isCollapsed && business.logoDisplayMode !== 'logo-only' && (
|
|
<div className="overflow-hidden">
|
|
<h1 className="font-bold leading-tight truncate">{business.name}</h1>
|
|
<p className="text-xs text-white/60 truncate">{business.subdomain}.smoothschedule.com</p>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
<nav className="flex-1 px-4 space-y-1 overflow-y-auto">
|
|
<Link to={getDashboardLink()} className={getNavClass('/', true)} title={t('nav.dashboard')}>
|
|
<LayoutDashboard size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.dashboard')}</span>}
|
|
</Link>
|
|
|
|
<Link to="/scheduler" className={getNavClass('/scheduler')} title={t('nav.scheduler')}>
|
|
<CalendarDays size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.scheduler')}</span>}
|
|
</Link>
|
|
|
|
<Link to="/tasks" className={getNavClass('/tasks')} title={t('nav.tasks', 'Tasks')}>
|
|
<Clock size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.tasks', 'Tasks')}</span>}
|
|
</Link>
|
|
|
|
{canViewManagementPages && (
|
|
<>
|
|
<Link to="/customers" className={getNavClass('/customers')} title={t('nav.customers')}>
|
|
<Users size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.customers')}</span>}
|
|
</Link>
|
|
<Link to="/services" className={getNavClass('/services')} title={t('nav.services', 'Services')}>
|
|
<Briefcase size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.services', 'Services')}</span>}
|
|
</Link>
|
|
<Link to="/resources" className={getNavClass('/resources')} title={t('nav.resources')}>
|
|
<ClipboardList size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.resources')}</span>}
|
|
</Link>
|
|
</>
|
|
)}
|
|
|
|
{canViewTickets && (
|
|
<Link to="/tickets" className={getNavClass('/tickets')} title={t('nav.tickets')}>
|
|
<Ticket size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.tickets')}</span>}
|
|
</Link>
|
|
)}
|
|
|
|
{canViewAdminPages && (
|
|
<>
|
|
{/* Payments link: always visible for owners, only visible for others if enabled */}
|
|
{(role === 'owner' || business.paymentsEnabled) && (
|
|
business.paymentsEnabled ? (
|
|
<Link to="/payments" className={getNavClass('/payments')} title={t('nav.payments')}>
|
|
<CreditCard size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.payments')}</span>}
|
|
</Link>
|
|
) : (
|
|
<div
|
|
className={getNavClass('/payments', false, true)}
|
|
title={t('nav.paymentsDisabledTooltip')}
|
|
>
|
|
<CreditCard size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.payments')}</span>}
|
|
</div>
|
|
)
|
|
)}
|
|
<Link to="/messages" className={getNavClass('/messages')} title={t('nav.messages')}>
|
|
<MessageSquare size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.messages')}</span>}
|
|
</Link>
|
|
<Link to="/staff" className={getNavClass('/staff')} title={t('nav.staff')}>
|
|
<Users size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.staff')}</span>}
|
|
</Link>
|
|
|
|
{/* Plugins Dropdown */}
|
|
<div>
|
|
<button
|
|
onClick={() => setIsPluginsOpen(!isPluginsOpen)}
|
|
className={`flex items-center gap-3 py-3 text-base font-medium rounded-lg transition-colors w-full ${isCollapsed ? 'px-3 justify-center' : 'px-4'} ${location.pathname.startsWith('/plugins') ? 'bg-white/10 text-white' : 'text-white/70 hover:text-white hover:bg-white/5'}`}
|
|
title={t('nav.plugins', 'Plugins')}
|
|
>
|
|
<Plug size={20} className="shrink-0" />
|
|
{!isCollapsed && (
|
|
<>
|
|
<span className="flex-1 text-left">{t('nav.plugins', 'Plugins')}</span>
|
|
<ChevronDown size={16} className={`shrink-0 transition-transform ${isPluginsOpen ? 'rotate-180' : ''}`} />
|
|
</>
|
|
)}
|
|
</button>
|
|
{isPluginsOpen && !isCollapsed && (
|
|
<div className="ml-4 mt-1 space-y-1 border-l border-white/20 pl-4">
|
|
<Link
|
|
to="/plugins/marketplace"
|
|
className={`flex items-center gap-3 py-2 text-sm font-medium rounded-lg transition-colors px-3 ${location.pathname === '/plugins/marketplace' ? 'bg-white/10 text-white' : 'text-white/60 hover:text-white hover:bg-white/5'}`}
|
|
title={t('nav.marketplace', 'Marketplace')}
|
|
>
|
|
<Store size={16} className="shrink-0" />
|
|
<span>{t('nav.marketplace', 'Marketplace')}</span>
|
|
</Link>
|
|
<Link
|
|
to="/plugins/my-plugins"
|
|
className={`flex items-center gap-3 py-2 text-sm font-medium rounded-lg transition-colors px-3 ${location.pathname === '/plugins/my-plugins' ? 'bg-white/10 text-white' : 'text-white/60 hover:text-white hover:bg-white/5'}`}
|
|
title={t('nav.myPlugins', 'My Plugins')}
|
|
>
|
|
<Package size={16} className="shrink-0" />
|
|
<span>{t('nav.myPlugins', 'My Plugins')}</span>
|
|
</Link>
|
|
<Link
|
|
to="/email-templates"
|
|
className={`flex items-center gap-3 py-2 text-sm font-medium rounded-lg transition-colors px-3 ${location.pathname === '/email-templates' ? 'bg-white/10 text-white' : 'text-white/60 hover:text-white hover:bg-white/5'}`}
|
|
title={t('nav.emailTemplates', 'Email Templates')}
|
|
>
|
|
<Mail size={16} className="shrink-0" />
|
|
<span>{t('nav.emailTemplates', 'Email Templates')}</span>
|
|
</Link>
|
|
<Link
|
|
to="/help/plugins"
|
|
className={`flex items-center gap-3 py-2 text-sm font-medium rounded-lg transition-colors px-3 ${location.pathname === '/help/plugins' ? 'bg-white/10 text-white' : 'text-white/60 hover:text-white hover:bg-white/5'}`}
|
|
title={t('nav.pluginDocs', 'Plugin Documentation')}
|
|
>
|
|
<Zap size={16} className="shrink-0" />
|
|
<span>{t('nav.pluginDocs', 'Plugin Docs')}</span>
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Help Dropdown */}
|
|
<div>
|
|
<button
|
|
onClick={() => setIsHelpOpen(!isHelpOpen)}
|
|
className={`flex items-center gap-3 py-3 text-base font-medium rounded-lg transition-colors w-full ${isCollapsed ? 'px-3 justify-center' : 'px-4'} ${location.pathname.startsWith('/help') || location.pathname === '/support' ? 'bg-white/10 text-white' : 'text-white/70 hover:text-white hover:bg-white/5'}`}
|
|
title={t('nav.help', 'Help')}
|
|
>
|
|
<HelpCircle size={20} className="shrink-0" />
|
|
{!isCollapsed && (
|
|
<>
|
|
<span className="flex-1 text-left">{t('nav.help', 'Help')}</span>
|
|
<ChevronDown size={16} className={`shrink-0 transition-transform ${isHelpOpen ? 'rotate-180' : ''}`} />
|
|
</>
|
|
)}
|
|
</button>
|
|
{isHelpOpen && !isCollapsed && (
|
|
<div className="ml-4 mt-1 space-y-1 border-l border-white/20 pl-4">
|
|
<Link
|
|
to="/help/guide"
|
|
className={`flex items-center gap-3 py-2 text-sm font-medium rounded-lg transition-colors px-3 ${location.pathname === '/help/guide' ? 'bg-white/10 text-white' : 'text-white/60 hover:text-white hover:bg-white/5'}`}
|
|
title={t('nav.platformGuide', 'Platform Guide')}
|
|
>
|
|
<BookOpen size={16} className="shrink-0" />
|
|
<span>{t('nav.platformGuide', 'Platform Guide')}</span>
|
|
</Link>
|
|
<Link
|
|
to="/help/ticketing"
|
|
className={`flex items-center gap-3 py-2 text-sm font-medium rounded-lg transition-colors px-3 ${location.pathname === '/help/ticketing' ? 'bg-white/10 text-white' : 'text-white/60 hover:text-white hover:bg-white/5'}`}
|
|
title={t('nav.ticketingHelp', 'Ticketing System')}
|
|
>
|
|
<FileQuestion size={16} className="shrink-0" />
|
|
<span>{t('nav.ticketingHelp', 'Ticketing System')}</span>
|
|
</Link>
|
|
{role === 'owner' && (
|
|
<Link
|
|
to="/help/api"
|
|
className={`flex items-center gap-3 py-2 text-sm font-medium rounded-lg transition-colors px-3 ${location.pathname === '/help/api' ? 'bg-white/10 text-white' : 'text-white/60 hover:text-white hover:bg-white/5'}`}
|
|
title={t('nav.apiDocs', 'API Documentation')}
|
|
>
|
|
<Code size={16} className="shrink-0" />
|
|
<span>{t('nav.apiDocs', 'API Docs')}</span>
|
|
</Link>
|
|
)}
|
|
<div className="pt-2 mt-2 border-t border-white/10">
|
|
<Link
|
|
to="/support"
|
|
className={`flex items-center gap-3 py-2 text-sm font-medium rounded-lg transition-colors px-3 ${location.pathname === '/support' ? 'bg-white/10 text-white' : 'text-white/60 hover:text-white hover:bg-white/5'}`}
|
|
title={t('nav.support', 'Support')}
|
|
>
|
|
<MessageSquare size={16} className="shrink-0" />
|
|
<span>{t('nav.support', 'Support')}</span>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{canViewSettings && (
|
|
<div className="pt-8 mt-8 border-t border-white/10">
|
|
{canViewSettings && (
|
|
<Link to="/settings" className={getNavClass('/settings', true)} title={t('nav.businessSettings')}>
|
|
<Settings size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.businessSettings')}</span>}
|
|
</Link>
|
|
)}
|
|
</div>
|
|
)}
|
|
</nav>
|
|
|
|
<div className="p-4 border-t border-white/10">
|
|
<a
|
|
href={`${window.location.protocol}//${window.location.host.split('.').slice(-2).join('.')}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={`flex items-center gap-2 text-xs text-white/60 mb-4 hover:text-white/80 transition-colors ${isCollapsed ? 'justify-center' : ''}`}
|
|
>
|
|
<SmoothScheduleLogo className="w-6 h-6 text-white" />
|
|
{!isCollapsed && (
|
|
<div>
|
|
<span className="block">{t('common.poweredBy')}</span>
|
|
<span className="font-semibold text-white/80">Smooth Schedule</span>
|
|
</div>
|
|
)}
|
|
</a>
|
|
<button
|
|
onClick={handleSignOut}
|
|
disabled={logoutMutation.isPending}
|
|
className={`flex items-center gap-3 px-4 py-2 text-sm font-medium text-white/70 hover:text-white w-full transition-colors rounded-lg ${isCollapsed ? 'justify-center' : ''} disabled:opacity-50`}
|
|
>
|
|
<LogOut size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('auth.signOut')}</span>}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Sidebar;
|