Add demo tenant reseed, staff roles, and fix masquerade redirect

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>
This commit is contained in:
poduck
2025-12-16 15:20:59 -05:00
parent cfb626b595
commit 79b76bf2dc
30 changed files with 2973 additions and 100 deletions

View File

@@ -45,12 +45,26 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
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 = role === 'owner' || role === 'manager' || (role === 'staff' && user.can_access_tickets);
const canSendMessages = user.can_send_messages === true;
const canViewTickets = hasPermission('can_access_tickets');
const canSendMessages = hasPermission('can_access_messages') || user.can_send_messages === true;
const handleSignOut = () => {
logoutMutation.mutate();
@@ -116,7 +130,7 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
isCollapsed={isCollapsed}
exact
/>
{!isStaff && (
{hasPermission('can_access_scheduler') && (
<SidebarItem
to="/dashboard/scheduler"
icon={CalendarDays}
@@ -124,7 +138,7 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
isCollapsed={isCollapsed}
/>
)}
{!isStaff && (
{hasPermission('can_access_tasks') && (
<SidebarItem
to="/dashboard/tasks"
icon={Clock}
@@ -134,7 +148,7 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
badgeElement={<UnfinishedBadge />}
/>
)}
{isStaff && (
{(isStaff && hasPermission('can_access_my_schedule')) && (
<SidebarItem
to="/dashboard/my-schedule"
icon={CalendarDays}
@@ -142,7 +156,7 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
isCollapsed={isCollapsed}
/>
)}
{(role === 'staff' || role === 'resource') && (
{(role === 'staff' || role === 'resource') && hasPermission('can_access_my_availability') && (
<SidebarItem
to="/dashboard/my-availability"
icon={CalendarOff}
@@ -152,72 +166,94 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
)}
</SidebarSection>
{/* Manage Section - Staff+ */}
{canViewManagementPages && (
{/* 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}>
<SidebarItem
to="/dashboard/site-editor"
icon={LayoutTemplate}
label={t('nav.siteBuilder', 'Site Builder')}
isCollapsed={isCollapsed}
/>
<SidebarItem
to="/dashboard/gallery"
icon={Image}
label={t('nav.gallery', 'Media Gallery')}
isCollapsed={isCollapsed}
/>
<SidebarItem
to="/dashboard/customers"
icon={Users}
label={t('nav.customers')}
isCollapsed={isCollapsed}
badgeElement={<UnfinishedBadge />}
/>
<SidebarItem
to="/dashboard/services"
icon={Briefcase}
label={t('nav.services', 'Services')}
isCollapsed={isCollapsed}
/>
<SidebarItem
to="/dashboard/resources"
icon={ClipboardList}
label={t('nav.resources')}
isCollapsed={isCollapsed}
/>
{canViewAdminPages && (
<>
<SidebarItem
to="/dashboard/staff"
icon={Users}
label={t('nav.staff')}
isCollapsed={isCollapsed}
badgeElement={<UnfinishedBadge />}
/>
{canUse('contracts') && (
<SidebarItem
to="/dashboard/contracts"
icon={FileSignature}
label={t('nav.contracts', 'Contracts')}
isCollapsed={isCollapsed}
badgeElement={<UnfinishedBadge />}
/>
)}
<SidebarItem
to="/dashboard/time-blocks"
icon={CalendarOff}
label={t('nav.timeBlocks', 'Time Blocks')}
isCollapsed={isCollapsed}
/>
<SidebarItem
to="/dashboard/locations"
icon={MapPin}
label={t('nav.locations', 'Locations')}
isCollapsed={isCollapsed}
locked={!canUse('multi_location')}
/>
</>
{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>
)}
@@ -245,7 +281,7 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
)}
{/* Money Section - Payments */}
{canViewAdminPages && (
{hasPermission('can_access_payments') && (
<SidebarSection title={t('nav.sections.money', 'Money')} isCollapsed={isCollapsed}>
<SidebarItem
to="/dashboard/payments"
@@ -258,7 +294,7 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
)}
{/* Extend Section - Automations */}
{canViewAdminPages && (
{hasPermission('can_access_automations') && (
<SidebarSection title={t('nav.sections.extend', 'Extend')} isCollapsed={isCollapsed}>
<SidebarItem
to="/dashboard/automations/my-automations"