Demo Tenant: - Add block_emails field to Tenant model for demo accounts - Add is_email_blocked() and wrapper functions in email_service - Create reseed_demo management command with salon/spa theme - Add Celery beat task for daily reseed at midnight UTC - Create 100 appointments, 20 customers, 13 services, 12 resources Staff Roles: - Add StaffRole model with permission toggles - Create default roles: Full Access, Front Desk, Limited Staff - Add StaffRolesSettings page and hooks - Integrate role assignment in Staff management Bug Fixes: - Fix masquerade redirect using wrong role names (tenant_owner vs owner) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
358 lines
13 KiB
TypeScript
358 lines
13 KiB
TypeScript
import React 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,
|
|
Clock,
|
|
Plug,
|
|
FileSignature,
|
|
CalendarOff,
|
|
LayoutTemplate,
|
|
MapPin,
|
|
Image,
|
|
} from 'lucide-react';
|
|
import { Business, User } from '../types';
|
|
import { useLogout } from '../hooks/useAuth';
|
|
import { usePlanFeatures } from '../hooks/usePlanFeatures';
|
|
import SmoothScheduleLogo from './SmoothScheduleLogo';
|
|
import UnfinishedBadge from './ui/UnfinishedBadge';
|
|
import {
|
|
SidebarSection,
|
|
SidebarItem,
|
|
SidebarDivider,
|
|
} from './navigation/SidebarComponents';
|
|
|
|
interface SidebarProps {
|
|
business: Business;
|
|
user: User;
|
|
isCollapsed: boolean;
|
|
toggleCollapse: () => void;
|
|
}
|
|
|
|
const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCollapse }) => {
|
|
const { t } = useTranslation();
|
|
const { role } = user;
|
|
const logoutMutation = useLogout();
|
|
const { canUse } = usePlanFeatures();
|
|
|
|
// Helper to check if user has a specific staff permission
|
|
// Owners and managers always have all permissions
|
|
// Staff members check their effective_permissions (role + user overrides)
|
|
const hasPermission = (permissionKey: string): boolean => {
|
|
if (role === 'owner' || role === 'manager') {
|
|
return true;
|
|
}
|
|
if (role === 'staff') {
|
|
// Check effective_permissions which combines user overrides and staff role
|
|
return user.effective_permissions?.[permissionKey] === true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const canViewAdminPages = role === 'owner' || role === 'manager';
|
|
const canViewManagementPages = role === 'owner' || role === 'manager';
|
|
const isStaff = role === 'staff';
|
|
const canViewSettings = role === 'owner';
|
|
const canViewTickets = hasPermission('can_access_tickets');
|
|
const canSendMessages = hasPermission('can_access_messages') || user.can_send_messages === true;
|
|
|
|
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, var(--color-brand-600, ${business.primaryColor}), var(--color-brand-secondary, ${business.secondaryColor || business.primaryColor}))`
|
|
}}
|
|
>
|
|
{/* Header / Logo */}
|
|
<button
|
|
onClick={toggleCollapse}
|
|
className={`flex items-center gap-3 w-full text-left px-6 py-6 ${isCollapsed ? 'justify-center' : ''} hover:bg-white/5 transition-colors focus:outline-none`}
|
|
aria-label={isCollapsed ? t('nav.expandSidebar') : t('nav.collapseSidebar')}
|
|
>
|
|
{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-12 object-contain"
|
|
/>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{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 font-bold text-xl shrink-0"
|
|
style={{ color: 'var(--color-brand-600)' }}
|
|
>
|
|
{business.name.substring(0, 2).toUpperCase()}
|
|
</div>
|
|
)}
|
|
{!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>
|
|
|
|
{/* Navigation */}
|
|
<nav className="flex-1 px-3 space-y-6 overflow-y-auto pb-4">
|
|
{/* Core Features - Always visible */}
|
|
<SidebarSection isCollapsed={isCollapsed}>
|
|
<SidebarItem
|
|
to="/dashboard"
|
|
icon={LayoutDashboard}
|
|
label={t('nav.dashboard')}
|
|
isCollapsed={isCollapsed}
|
|
exact
|
|
/>
|
|
{hasPermission('can_access_scheduler') && (
|
|
<SidebarItem
|
|
to="/dashboard/scheduler"
|
|
icon={CalendarDays}
|
|
label={t('nav.scheduler')}
|
|
isCollapsed={isCollapsed}
|
|
/>
|
|
)}
|
|
{hasPermission('can_access_tasks') && (
|
|
<SidebarItem
|
|
to="/dashboard/tasks"
|
|
icon={Clock}
|
|
label={t('nav.tasks', 'Tasks')}
|
|
isCollapsed={isCollapsed}
|
|
locked={!canUse('automations') || !canUse('tasks')}
|
|
badgeElement={<UnfinishedBadge />}
|
|
/>
|
|
)}
|
|
{(isStaff && hasPermission('can_access_my_schedule')) && (
|
|
<SidebarItem
|
|
to="/dashboard/my-schedule"
|
|
icon={CalendarDays}
|
|
label={t('nav.mySchedule', 'My Schedule')}
|
|
isCollapsed={isCollapsed}
|
|
/>
|
|
)}
|
|
{(role === 'staff' || role === 'resource') && hasPermission('can_access_my_availability') && (
|
|
<SidebarItem
|
|
to="/dashboard/my-availability"
|
|
icon={CalendarOff}
|
|
label={t('nav.myAvailability', 'My Availability')}
|
|
isCollapsed={isCollapsed}
|
|
/>
|
|
)}
|
|
</SidebarSection>
|
|
|
|
{/* Manage Section - Show if user has any manage-related permission */}
|
|
{(canViewManagementPages ||
|
|
hasPermission('can_access_site_builder') ||
|
|
hasPermission('can_access_gallery') ||
|
|
hasPermission('can_access_customers') ||
|
|
hasPermission('can_access_services') ||
|
|
hasPermission('can_access_resources') ||
|
|
hasPermission('can_access_staff') ||
|
|
hasPermission('can_access_contracts') ||
|
|
hasPermission('can_access_time_blocks') ||
|
|
hasPermission('can_access_locations')
|
|
) && (
|
|
<SidebarSection title={t('nav.sections.manage', 'Manage')} isCollapsed={isCollapsed}>
|
|
{hasPermission('can_access_site_builder') && (
|
|
<SidebarItem
|
|
to="/dashboard/site-editor"
|
|
icon={LayoutTemplate}
|
|
label={t('nav.siteBuilder', 'Site Builder')}
|
|
isCollapsed={isCollapsed}
|
|
/>
|
|
)}
|
|
{hasPermission('can_access_gallery') && (
|
|
<SidebarItem
|
|
to="/dashboard/gallery"
|
|
icon={Image}
|
|
label={t('nav.gallery', 'Media Gallery')}
|
|
isCollapsed={isCollapsed}
|
|
/>
|
|
)}
|
|
{hasPermission('can_access_customers') && (
|
|
<SidebarItem
|
|
to="/dashboard/customers"
|
|
icon={Users}
|
|
label={t('nav.customers')}
|
|
isCollapsed={isCollapsed}
|
|
badgeElement={<UnfinishedBadge />}
|
|
/>
|
|
)}
|
|
{hasPermission('can_access_services') && (
|
|
<SidebarItem
|
|
to="/dashboard/services"
|
|
icon={Briefcase}
|
|
label={t('nav.services', 'Services')}
|
|
isCollapsed={isCollapsed}
|
|
/>
|
|
)}
|
|
{hasPermission('can_access_resources') && (
|
|
<SidebarItem
|
|
to="/dashboard/resources"
|
|
icon={ClipboardList}
|
|
label={t('nav.resources')}
|
|
isCollapsed={isCollapsed}
|
|
/>
|
|
)}
|
|
{hasPermission('can_access_staff') && (
|
|
<SidebarItem
|
|
to="/dashboard/staff"
|
|
icon={Users}
|
|
label={t('nav.staff')}
|
|
isCollapsed={isCollapsed}
|
|
badgeElement={<UnfinishedBadge />}
|
|
/>
|
|
)}
|
|
{hasPermission('can_access_contracts') && canUse('contracts') && (
|
|
<SidebarItem
|
|
to="/dashboard/contracts"
|
|
icon={FileSignature}
|
|
label={t('nav.contracts', 'Contracts')}
|
|
isCollapsed={isCollapsed}
|
|
badgeElement={<UnfinishedBadge />}
|
|
/>
|
|
)}
|
|
{hasPermission('can_access_time_blocks') && (
|
|
<SidebarItem
|
|
to="/dashboard/time-blocks"
|
|
icon={CalendarOff}
|
|
label={t('nav.timeBlocks', 'Time Blocks')}
|
|
isCollapsed={isCollapsed}
|
|
/>
|
|
)}
|
|
{hasPermission('can_access_locations') && (
|
|
<SidebarItem
|
|
to="/dashboard/locations"
|
|
icon={MapPin}
|
|
label={t('nav.locations', 'Locations')}
|
|
isCollapsed={isCollapsed}
|
|
locked={!canUse('multi_location')}
|
|
/>
|
|
)}
|
|
</SidebarSection>
|
|
)}
|
|
|
|
{/* Communicate Section - Tickets + Messages */}
|
|
{(canViewTickets || canSendMessages) && (
|
|
<SidebarSection title={t('nav.sections.communicate', 'Communicate')} isCollapsed={isCollapsed}>
|
|
{canSendMessages && (
|
|
<SidebarItem
|
|
to="/dashboard/messages"
|
|
icon={MessageSquare}
|
|
label={t('nav.messages')}
|
|
isCollapsed={isCollapsed}
|
|
/>
|
|
)}
|
|
{canViewTickets && (
|
|
<SidebarItem
|
|
to="/dashboard/tickets"
|
|
icon={Ticket}
|
|
label={t('nav.tickets')}
|
|
isCollapsed={isCollapsed}
|
|
/>
|
|
)}
|
|
</SidebarSection>
|
|
)}
|
|
|
|
{/* Money Section - Payments */}
|
|
{hasPermission('can_access_payments') && (
|
|
<SidebarSection title={t('nav.sections.money', 'Money')} isCollapsed={isCollapsed}>
|
|
<SidebarItem
|
|
to="/dashboard/payments"
|
|
icon={CreditCard}
|
|
label={t('nav.payments')}
|
|
isCollapsed={isCollapsed}
|
|
disabled={!business.paymentsEnabled && role !== 'owner'}
|
|
/>
|
|
</SidebarSection>
|
|
)}
|
|
|
|
{/* Extend Section - Automations */}
|
|
{hasPermission('can_access_automations') && (
|
|
<SidebarSection title={t('nav.sections.extend', 'Extend')} isCollapsed={isCollapsed}>
|
|
<SidebarItem
|
|
to="/dashboard/automations/my-automations"
|
|
icon={Plug}
|
|
label={t('nav.automations', 'Automations')}
|
|
isCollapsed={isCollapsed}
|
|
locked={!canUse('automations')}
|
|
badgeElement={<UnfinishedBadge />}
|
|
/>
|
|
</SidebarSection>
|
|
)}
|
|
|
|
{/* Footer Section - Settings & Help */}
|
|
<SidebarDivider isCollapsed={isCollapsed} />
|
|
|
|
<SidebarSection isCollapsed={isCollapsed}>
|
|
{canViewSettings && (
|
|
<SidebarItem
|
|
to="/dashboard/settings"
|
|
icon={Settings}
|
|
label={t('nav.businessSettings')}
|
|
isCollapsed={isCollapsed}
|
|
/>
|
|
)}
|
|
<SidebarItem
|
|
to="/dashboard/help"
|
|
icon={HelpCircle}
|
|
label={t('nav.helpDocs', 'Help & Docs')}
|
|
isCollapsed={isCollapsed}
|
|
/>
|
|
</SidebarSection>
|
|
</nav>
|
|
|
|
{/* User Section */}
|
|
<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-3 hover:text-white/80 transition-colors ${isCollapsed ? 'justify-center' : ''}`}
|
|
>
|
|
<SmoothScheduleLogo className="w-5 h-5 text-white" />
|
|
{!isCollapsed && (
|
|
<span className="text-white/60">{t('nav.smoothSchedule')}</span>
|
|
)}
|
|
</a>
|
|
<button
|
|
onClick={handleSignOut}
|
|
disabled={logoutMutation.isPending}
|
|
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium text-white/70 hover:text-white hover:bg-white/5 w-full transition-colors rounded-lg ${isCollapsed ? 'justify-center' : ''} disabled:opacity-50`}
|
|
>
|
|
<LogOut size={18} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('auth.signOut')}</span>}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Sidebar;
|