Improve staff management UI and add sorting functionality
- Remove WIP badge from staff sidebar navigation - Make action buttons consistent between Customers and Staff pages - Edit button: icon + text with gray border - Masquerade button: icon + text with indigo border - Verify email button: icon-only with colored border (green/amber) - Add sortable columns to Staff list (name and role) - Include migrations for tenant manager role removal 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
a80b35a806
commit
92019aac7e
@@ -55,18 +55,18 @@ const testUsers: TestUser[] = [
|
||||
category: 'business',
|
||||
},
|
||||
{
|
||||
email: 'manager@demo.com',
|
||||
email: 'staff@demo.com',
|
||||
password: 'test123',
|
||||
role: 'TENANT_MANAGER',
|
||||
label: 'Business Manager',
|
||||
role: 'TENANT_STAFF',
|
||||
label: 'Staff (Full Access)',
|
||||
color: 'bg-pink-600 hover:bg-pink-700',
|
||||
category: 'business',
|
||||
},
|
||||
{
|
||||
email: 'staff@demo.com',
|
||||
email: 'limited-staff@demo.com',
|
||||
password: 'test123',
|
||||
role: 'TENANT_STAFF',
|
||||
label: 'Staff Member',
|
||||
label: 'Staff (Limited)',
|
||||
color: 'bg-teal-600 hover:bg-teal-700',
|
||||
category: 'business',
|
||||
},
|
||||
|
||||
@@ -46,10 +46,10 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
|
||||
const { canUse } = usePlanFeatures();
|
||||
|
||||
// Helper to check if user has a specific staff permission
|
||||
// Owners and managers always have all permissions
|
||||
// Owners always have all permissions
|
||||
// Staff members check their effective_permissions (role + user overrides)
|
||||
const hasPermission = (permissionKey: string): boolean => {
|
||||
if (role === 'owner' || role === 'manager') {
|
||||
if (role === 'owner') {
|
||||
return true;
|
||||
}
|
||||
if (role === 'staff') {
|
||||
@@ -59,10 +59,11 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
|
||||
return false;
|
||||
};
|
||||
|
||||
const canViewAdminPages = role === 'owner' || role === 'manager';
|
||||
const canViewManagementPages = role === 'owner' || role === 'manager';
|
||||
// Admin/management access is based on effective permissions for staff
|
||||
const canViewAdminPages = role === 'owner' || hasPermission('can_access_staff');
|
||||
const canViewManagementPages = role === 'owner' || hasPermission('can_access_scheduler');
|
||||
const isStaff = role === 'staff';
|
||||
const canViewSettings = role === 'owner';
|
||||
const canViewSettings = role === 'owner' || hasPermission('can_access_settings');
|
||||
const canViewTickets = hasPermission('can_access_tickets');
|
||||
const canSendMessages = hasPermission('can_access_messages') || user.can_send_messages === true;
|
||||
|
||||
@@ -191,7 +192,6 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
|
||||
icon={Users}
|
||||
label={t('nav.customers')}
|
||||
isCollapsed={isCollapsed}
|
||||
badgeElement={<UnfinishedBadge />}
|
||||
/>
|
||||
)}
|
||||
{hasPermission('can_access_services') && (
|
||||
@@ -216,7 +216,6 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
|
||||
icon={Users}
|
||||
label={t('nav.staff')}
|
||||
isCollapsed={isCollapsed}
|
||||
badgeElement={<UnfinishedBadge />}
|
||||
/>
|
||||
)}
|
||||
{hasPermission('can_access_contracts') && canUse('contracts') && (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
|
||||
export interface PermissionConfig {
|
||||
key: string;
|
||||
@@ -8,20 +9,134 @@ export interface PermissionConfig {
|
||||
hintKey: string;
|
||||
hintDefault: string;
|
||||
defaultValue: boolean;
|
||||
roles: ('manager' | 'staff')[];
|
||||
}
|
||||
|
||||
// Business Settings sub-permissions
|
||||
export const SETTINGS_PERMISSION_CONFIGS: PermissionConfig[] = [
|
||||
{
|
||||
key: 'can_access_settings_general',
|
||||
labelKey: 'staff.canAccessSettingsGeneral',
|
||||
labelDefault: 'General Settings',
|
||||
hintKey: 'staff.canAccessSettingsGeneralHint',
|
||||
hintDefault: 'Business name, timezone, and basic configuration',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'can_access_settings_business_hours',
|
||||
labelKey: 'staff.canAccessSettingsBusinessHours',
|
||||
labelDefault: 'Business Hours',
|
||||
hintKey: 'staff.canAccessSettingsBusinessHoursHint',
|
||||
hintDefault: 'Set regular operating hours',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'can_access_settings_branding',
|
||||
labelKey: 'staff.canAccessSettingsBranding',
|
||||
labelDefault: 'Branding',
|
||||
hintKey: 'staff.canAccessSettingsBrandingHint',
|
||||
hintDefault: 'Logo, colors, and visual identity',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'can_access_settings_booking',
|
||||
labelKey: 'staff.canAccessSettingsBooking',
|
||||
labelDefault: 'Booking Settings',
|
||||
hintKey: 'staff.canAccessSettingsBookingHint',
|
||||
hintDefault: 'Booking policies and rules',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'can_access_settings_communication',
|
||||
labelKey: 'staff.canAccessSettingsCommunication',
|
||||
labelDefault: 'Communication',
|
||||
hintKey: 'staff.canAccessSettingsCommunicationHint',
|
||||
hintDefault: 'Notification preferences and reminders',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'can_access_settings_embed_widget',
|
||||
labelKey: 'staff.canAccessSettingsEmbedWidget',
|
||||
labelDefault: 'Embed Widget',
|
||||
hintKey: 'staff.canAccessSettingsEmbedWidgetHint',
|
||||
hintDefault: 'Configure booking widget for websites',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'can_access_settings_email_templates',
|
||||
labelKey: 'staff.canAccessSettingsEmailTemplates',
|
||||
labelDefault: 'Email Templates',
|
||||
hintKey: 'staff.canAccessSettingsEmailTemplatesHint',
|
||||
hintDefault: 'Customize automated emails',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'can_access_settings_staff_roles',
|
||||
labelKey: 'staff.canAccessSettingsStaffRoles',
|
||||
labelDefault: 'Staff Roles',
|
||||
hintKey: 'staff.canAccessSettingsStaffRolesHint',
|
||||
hintDefault: 'Create and manage permission roles',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'can_access_settings_resource_types',
|
||||
labelKey: 'staff.canAccessSettingsResourceTypes',
|
||||
labelDefault: 'Resource Types',
|
||||
hintKey: 'staff.canAccessSettingsResourceTypesHint',
|
||||
hintDefault: 'Configure resource categories',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'can_access_settings_api',
|
||||
labelKey: 'staff.canAccessSettingsApi',
|
||||
labelDefault: 'API & Integrations',
|
||||
hintKey: 'staff.canAccessSettingsApiHint',
|
||||
hintDefault: 'Manage API tokens and webhooks',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'can_access_settings_custom_domains',
|
||||
labelKey: 'staff.canAccessSettingsCustomDomains',
|
||||
labelDefault: 'Custom Domains',
|
||||
hintKey: 'staff.canAccessSettingsCustomDomainsHint',
|
||||
hintDefault: 'Configure custom domain settings',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'can_access_settings_authentication',
|
||||
labelKey: 'staff.canAccessSettingsAuthentication',
|
||||
labelDefault: 'Authentication',
|
||||
hintKey: 'staff.canAccessSettingsAuthenticationHint',
|
||||
hintDefault: 'OAuth and social login configuration',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'can_access_settings_email',
|
||||
labelKey: 'staff.canAccessSettingsEmail',
|
||||
labelDefault: 'Email Setup',
|
||||
hintKey: 'staff.canAccessSettingsEmailHint',
|
||||
hintDefault: 'Configure email addresses for tickets',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'can_access_settings_sms_calling',
|
||||
labelKey: 'staff.canAccessSettingsSmsCalling',
|
||||
labelDefault: 'SMS & Calling',
|
||||
hintKey: 'staff.canAccessSettingsSmsCallingHint',
|
||||
hintDefault: 'Manage credits and phone numbers',
|
||||
defaultValue: false,
|
||||
},
|
||||
];
|
||||
|
||||
// Define all available permissions in one place
|
||||
// All permissions are now available to staff (via staff roles)
|
||||
export const PERMISSION_CONFIGS: PermissionConfig[] = [
|
||||
// Manager-only permissions
|
||||
{
|
||||
key: 'can_invite_staff',
|
||||
labelKey: 'staff.canInviteStaff',
|
||||
labelDefault: 'Can invite new staff members',
|
||||
hintKey: 'staff.canInviteStaffHint',
|
||||
hintDefault: 'Allow this manager to send invitations to new staff members',
|
||||
hintDefault: 'Allow this staff member to send invitations to new staff members',
|
||||
defaultValue: false,
|
||||
roles: ['manager'],
|
||||
},
|
||||
{
|
||||
key: 'can_manage_resources',
|
||||
@@ -29,8 +144,7 @@ export const PERMISSION_CONFIGS: PermissionConfig[] = [
|
||||
labelDefault: 'Can manage resources',
|
||||
hintKey: 'staff.canManageResourcesHint',
|
||||
hintDefault: 'Create, edit, and delete bookable resources',
|
||||
defaultValue: true,
|
||||
roles: ['manager'],
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'can_manage_services',
|
||||
@@ -38,8 +152,7 @@ export const PERMISSION_CONFIGS: PermissionConfig[] = [
|
||||
labelDefault: 'Can manage services',
|
||||
hintKey: 'staff.canManageServicesHint',
|
||||
hintDefault: 'Create, edit, and delete service offerings',
|
||||
defaultValue: true,
|
||||
roles: ['manager'],
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
key: 'can_view_reports',
|
||||
@@ -47,17 +160,7 @@ export const PERMISSION_CONFIGS: PermissionConfig[] = [
|
||||
labelDefault: 'Can view reports',
|
||||
hintKey: 'staff.canViewReportsHint',
|
||||
hintDefault: 'Access business analytics and financial reports',
|
||||
defaultValue: true,
|
||||
roles: ['manager'],
|
||||
},
|
||||
{
|
||||
key: 'can_access_settings',
|
||||
labelKey: 'staff.canAccessSettings',
|
||||
labelDefault: 'Can access business settings',
|
||||
hintKey: 'staff.canAccessSettingsHint',
|
||||
hintDefault: 'Modify business profile, branding, and configuration',
|
||||
defaultValue: false,
|
||||
roles: ['manager'],
|
||||
},
|
||||
{
|
||||
key: 'can_refund_payments',
|
||||
@@ -66,7 +169,6 @@ export const PERMISSION_CONFIGS: PermissionConfig[] = [
|
||||
hintKey: 'staff.canRefundPaymentsHint',
|
||||
hintDefault: 'Process refunds for customer payments',
|
||||
defaultValue: false,
|
||||
roles: ['manager'],
|
||||
},
|
||||
{
|
||||
key: 'can_send_messages',
|
||||
@@ -74,10 +176,8 @@ export const PERMISSION_CONFIGS: PermissionConfig[] = [
|
||||
labelDefault: 'Can send broadcast messages',
|
||||
hintKey: 'staff.canSendMessagesHint',
|
||||
hintDefault: 'Send messages to groups of staff and customers',
|
||||
defaultValue: true,
|
||||
roles: ['manager'],
|
||||
defaultValue: false,
|
||||
},
|
||||
// Staff-only permissions
|
||||
{
|
||||
key: 'can_view_all_schedules',
|
||||
labelKey: 'staff.canViewAllSchedules',
|
||||
@@ -85,7 +185,6 @@ export const PERMISSION_CONFIGS: PermissionConfig[] = [
|
||||
hintKey: 'staff.canViewAllSchedulesHint',
|
||||
hintDefault: 'View schedules of other staff members (otherwise only their own)',
|
||||
defaultValue: false,
|
||||
roles: ['staff'],
|
||||
},
|
||||
{
|
||||
key: 'can_manage_own_appointments',
|
||||
@@ -94,112 +193,132 @@ export const PERMISSION_CONFIGS: PermissionConfig[] = [
|
||||
hintKey: 'staff.canManageOwnAppointmentsHint',
|
||||
hintDefault: 'Create, reschedule, and cancel their own appointments',
|
||||
defaultValue: true,
|
||||
roles: ['staff'],
|
||||
},
|
||||
{
|
||||
key: 'can_self_approve_time_off',
|
||||
labelKey: 'staff.canSelfApproveTimeOff',
|
||||
labelDefault: 'Can self-approve time off',
|
||||
hintKey: 'staff.canSelfApproveTimeOffHint',
|
||||
hintDefault: 'Add time off without requiring manager/owner approval',
|
||||
hintDefault: 'Add time off without requiring owner approval',
|
||||
defaultValue: false,
|
||||
roles: ['staff'],
|
||||
},
|
||||
// Shared permissions (both manager and staff)
|
||||
{
|
||||
key: 'can_access_tickets',
|
||||
labelKey: 'staff.canAccessTickets',
|
||||
labelDefault: 'Can access support tickets',
|
||||
hintKey: 'staff.canAccessTicketsHint',
|
||||
hintDefault: 'View and manage customer support tickets',
|
||||
defaultValue: true, // Default for managers; staff will override to false
|
||||
roles: ['manager', 'staff'],
|
||||
defaultValue: false,
|
||||
},
|
||||
];
|
||||
|
||||
// Get default permissions for a role
|
||||
export const getDefaultPermissions = (role: 'manager' | 'staff'): Record<string, boolean> => {
|
||||
// Get default permissions for staff
|
||||
export const getDefaultPermissions = (): Record<string, boolean> => {
|
||||
const defaults: Record<string, boolean> = {};
|
||||
PERMISSION_CONFIGS.forEach((config) => {
|
||||
if (config.roles.includes(role)) {
|
||||
// Staff members have ticket access disabled by default
|
||||
if (role === 'staff' && config.key === 'can_access_tickets') {
|
||||
defaults[config.key] = false;
|
||||
} else {
|
||||
defaults[config.key] = config.defaultValue;
|
||||
}
|
||||
}
|
||||
defaults[config.key] = config.defaultValue;
|
||||
});
|
||||
SETTINGS_PERMISSION_CONFIGS.forEach((config) => {
|
||||
defaults[config.key] = config.defaultValue;
|
||||
});
|
||||
defaults['can_access_settings'] = false;
|
||||
return defaults;
|
||||
};
|
||||
|
||||
interface StaffPermissionsProps {
|
||||
role: 'manager' | 'staff';
|
||||
role: 'staff';
|
||||
permissions: Record<string, boolean>;
|
||||
onChange: (permissions: Record<string, boolean>) => void;
|
||||
variant?: 'invite' | 'edit';
|
||||
}
|
||||
|
||||
const StaffPermissions: React.FC<StaffPermissionsProps> = ({
|
||||
role,
|
||||
permissions,
|
||||
onChange,
|
||||
variant = 'edit',
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Filter permissions for this role
|
||||
const rolePermissions = PERMISSION_CONFIGS.filter((config) =>
|
||||
config.roles.includes(role)
|
||||
);
|
||||
|
||||
const handleToggle = (key: string, checked: boolean) => {
|
||||
onChange({ ...permissions, [key]: checked });
|
||||
};
|
||||
const [settingsExpanded, setSettingsExpanded] = useState(false);
|
||||
|
||||
// Get the current value, falling back to default
|
||||
const getValue = (config: PermissionConfig): boolean => {
|
||||
if (permissions[config.key] !== undefined) {
|
||||
return permissions[config.key];
|
||||
const getValue = (key: string, defaultValue: boolean = false): boolean => {
|
||||
if (permissions[key] !== undefined) {
|
||||
return permissions[key];
|
||||
}
|
||||
// Staff have ticket access disabled by default
|
||||
if (role === 'staff' && config.key === 'can_access_tickets') {
|
||||
return false;
|
||||
}
|
||||
return config.defaultValue;
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
// Different styling for manager vs staff permissions
|
||||
const isManagerPermission = (config: PermissionConfig) =>
|
||||
config.roles.includes('manager') && !config.roles.includes('staff');
|
||||
const hasSettingsAccess = getValue('can_access_settings', false);
|
||||
|
||||
const getPermissionStyle = (config: PermissionConfig) => {
|
||||
if (isManagerPermission(config) || role === 'manager') {
|
||||
return 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900/30';
|
||||
// Auto-expand settings section if any settings permissions are enabled
|
||||
useEffect(() => {
|
||||
if (hasSettingsAccess) {
|
||||
const hasAnySettingEnabled = SETTINGS_PERMISSION_CONFIGS.some(
|
||||
(config) => getValue(config.key, false)
|
||||
);
|
||||
if (hasAnySettingEnabled) {
|
||||
setSettingsExpanded(true);
|
||||
}
|
||||
}
|
||||
return 'bg-gray-50 dark:bg-gray-700/50 border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700';
|
||||
}, []);
|
||||
|
||||
const handleToggle = (key: string, checked: boolean) => {
|
||||
const newPermissions = { ...permissions, [key]: checked };
|
||||
|
||||
// If turning off main settings access, turn off all sub-settings
|
||||
if (key === 'can_access_settings' && !checked) {
|
||||
SETTINGS_PERMISSION_CONFIGS.forEach((config) => {
|
||||
newPermissions[config.key] = false;
|
||||
});
|
||||
}
|
||||
|
||||
onChange(newPermissions);
|
||||
};
|
||||
|
||||
if (rolePermissions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const handleSettingsMainToggle = (checked: boolean) => {
|
||||
const newPermissions = { ...permissions, can_access_settings: checked };
|
||||
|
||||
// If turning off, disable all sub-settings
|
||||
if (!checked) {
|
||||
SETTINGS_PERMISSION_CONFIGS.forEach((config) => {
|
||||
newPermissions[config.key] = false;
|
||||
});
|
||||
setSettingsExpanded(false);
|
||||
} else {
|
||||
// If turning on, expand the section
|
||||
setSettingsExpanded(true);
|
||||
}
|
||||
|
||||
onChange(newPermissions);
|
||||
};
|
||||
|
||||
const handleSelectAllSettings = (selectAll: boolean) => {
|
||||
const newPermissions = { ...permissions };
|
||||
SETTINGS_PERMISSION_CONFIGS.forEach((config) => {
|
||||
newPermissions[config.key] = selectAll;
|
||||
});
|
||||
onChange(newPermissions);
|
||||
};
|
||||
|
||||
// Count how many settings sub-permissions are enabled
|
||||
const enabledSettingsCount = SETTINGS_PERMISSION_CONFIGS.filter((config) =>
|
||||
getValue(config.key, false)
|
||||
).length;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
{role === 'manager'
|
||||
? t('staff.managerPermissions', 'Manager Permissions')
|
||||
: t('staff.staffPermissions', 'Staff Permissions')}
|
||||
{t('staff.staffPermissions', 'Staff Permissions')}
|
||||
</h4>
|
||||
|
||||
{rolePermissions.map((config) => (
|
||||
{/* Regular permissions */}
|
||||
{PERMISSION_CONFIGS.map((config) => (
|
||||
<label
|
||||
key={config.key}
|
||||
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${getPermissionStyle(config)}`}
|
||||
className="flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors bg-gray-50 dark:bg-gray-700/50 border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={getValue(config)}
|
||||
checked={getValue(config.key, config.defaultValue)}
|
||||
onChange={(e) => handleToggle(config.key, e.target.checked)}
|
||||
className="w-4 h-4 mt-0.5 rounded border-gray-300 text-brand-600 focus:ring-brand-500"
|
||||
/>
|
||||
@@ -213,6 +332,113 @@ const StaffPermissions: React.FC<StaffPermissionsProps> = ({
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
|
||||
{/* Business Settings Section */}
|
||||
<div className="border rounded-lg border-gray-200 dark:border-gray-600 overflow-hidden">
|
||||
{/* Main Business Settings Toggle */}
|
||||
<div
|
||||
className={`flex items-start gap-3 p-3 cursor-pointer transition-colors ${
|
||||
hasSettingsAccess
|
||||
? 'bg-brand-50 dark:bg-brand-900/20'
|
||||
: 'bg-gray-50 dark:bg-gray-700/50 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={hasSettingsAccess}
|
||||
onChange={(e) => handleSettingsMainToggle(e.target.checked)}
|
||||
className="w-4 h-4 mt-0.5 rounded border-gray-300 text-brand-600 focus:ring-brand-500"
|
||||
/>
|
||||
<div
|
||||
className="flex-1"
|
||||
onClick={() => hasSettingsAccess && setSettingsExpanded(!settingsExpanded)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{t('staff.canAccessSettings', 'Can access business settings')}
|
||||
</span>
|
||||
{hasSettingsAccess && enabledSettingsCount > 0 && (
|
||||
<span className="ml-2 text-xs text-brand-600 dark:text-brand-400">
|
||||
({enabledSettingsCount}/{SETTINGS_PERMISSION_CONFIGS.length} enabled)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{hasSettingsAccess && (
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 hover:bg-gray-200 dark:hover:bg-gray-600 rounded"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSettingsExpanded(!settingsExpanded);
|
||||
}}
|
||||
>
|
||||
{settingsExpanded ? (
|
||||
<ChevronDown size={16} className="text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight size={16} className="text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
{t(
|
||||
'staff.canAccessSettingsHint',
|
||||
'Access to business settings pages (select specific pages below)'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sub-permissions (collapsible) */}
|
||||
{hasSettingsAccess && settingsExpanded && (
|
||||
<div className="border-t border-gray-200 dark:border-gray-600 bg-gray-25 dark:bg-gray-800/50">
|
||||
{/* Select All / None buttons */}
|
||||
<div className="px-3 py-2 border-b border-gray-200 dark:border-gray-600 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelectAllSettings(true)}
|
||||
className="text-xs text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300 font-medium"
|
||||
>
|
||||
{t('staff.selectAll', 'Select All')}
|
||||
</button>
|
||||
<span className="text-gray-300 dark:text-gray-600">|</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelectAllSettings(false)}
|
||||
className="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
|
||||
>
|
||||
{t('staff.selectNone', 'Select None')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Individual settings permissions */}
|
||||
<div className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
{SETTINGS_PERMISSION_CONFIGS.map((config) => (
|
||||
<label
|
||||
key={config.key}
|
||||
className="flex items-start gap-3 px-3 py-2.5 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700/50 transition-colors"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={getValue(config.key, config.defaultValue)}
|
||||
onChange={(e) => handleToggle(config.key, e.target.checked)}
|
||||
className="w-4 h-4 mt-0.5 rounded border-gray-300 text-brand-600 focus:ring-brand-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{t(config.labelKey, config.labelDefault)}
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
{t(config.hintKey, config.hintDefault)}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('MasqueradeBanner', () => {
|
||||
it('shows return to previous user text when previousUser exists', () => {
|
||||
const propsWithPrevious = {
|
||||
...defaultProps,
|
||||
previousUser: { id: '3', name: 'Manager', email: 'manager@test.com', role: 'manager' as const },
|
||||
previousUser: { id: '3', name: 'Manager', email: 'manager@test.com', role: 'owner' as const },
|
||||
};
|
||||
render(<MasqueradeBanner {...propsWithPrevious} />);
|
||||
expect(screen.getByText(/platform.masquerade.returnTo/)).toBeInTheDocument();
|
||||
|
||||
@@ -518,8 +518,8 @@ describe('TopBar', () => {
|
||||
expect(screen.getByTestId('user-profile-dropdown')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render for manager role', () => {
|
||||
const user = createMockUser({ role: 'manager' });
|
||||
it('should render for staff with permissions', () => {
|
||||
const user = createMockUser({ role: 'staff' });
|
||||
|
||||
renderWithRouter(
|
||||
<TopBar
|
||||
|
||||
Reference in New Issue
Block a user