Update staff roles, documentation, and add tenant API
Staff Roles: - Remove Support Staff default role (now Manager and Staff only) - Add position field for custom role ordering - Update StaffRolesSettings with improved permission UI - Add RolePermissions component for visual permission display Documentation Updates: - HelpStaff: Explain two-tier permission system (User Roles + Staff Roles) - HelpSettingsStaffRoles: Update default roles, add settings access permissions - HelpComprehensive: Update staff roles section with correct role structure - HelpCustomers: Add customer creation and onboarding sections - HelpContracts: Add lifecycle, snapshotting, and signing experience docs - HelpSettingsAppearance: Update with 20 color palettes and navigation text Tenant API: - Add new isolated API at /tenant-api/v1/ for third-party integrations - Token-based authentication with scope permissions - Endpoints: business, services, resources, availability, bookings, customers, webhooks Tests: - Add test coverage for Celery tasks across modules - Reorganize schedule view tests for better maintainability 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
266
frontend/src/components/staff/RolePermissions.tsx
Normal file
266
frontend/src/components/staff/RolePermissions.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* Shared Role Permission Components
|
||||
*
|
||||
* Reusable components for displaying and editing staff role permissions.
|
||||
* Used in both StaffRolesSettings and the Invite Staff modal.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PermissionDefinition } from '../../types';
|
||||
|
||||
export interface PermissionSectionProps {
|
||||
title: string;
|
||||
description: string;
|
||||
permissions: Record<string, PermissionDefinition>;
|
||||
values: Record<string, boolean>;
|
||||
onChange: (key: string, value: boolean) => void;
|
||||
onSelectAll?: () => void;
|
||||
onClearAll?: () => void;
|
||||
variant?: 'default' | 'settings' | 'dangerous';
|
||||
readOnly?: boolean;
|
||||
columns?: 1 | 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* A section of permission checkboxes with header and select/clear all buttons
|
||||
*/
|
||||
export const PermissionSection: React.FC<PermissionSectionProps> = ({
|
||||
title,
|
||||
description,
|
||||
permissions,
|
||||
values,
|
||||
onChange,
|
||||
onSelectAll,
|
||||
onClearAll,
|
||||
variant = 'default',
|
||||
readOnly = false,
|
||||
columns = 2,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const variantStyles = {
|
||||
default: {
|
||||
container: '',
|
||||
checkbox: 'text-brand-600 focus:ring-brand-500',
|
||||
hover: 'hover:bg-gray-50 dark:hover:bg-gray-700/50',
|
||||
},
|
||||
settings: {
|
||||
container: 'p-3 bg-blue-50/50 dark:bg-blue-900/10 rounded-lg border border-blue-100 dark:border-blue-900/30',
|
||||
checkbox: 'text-blue-600 focus:ring-blue-500',
|
||||
hover: 'hover:bg-blue-100/50 dark:hover:bg-blue-900/20',
|
||||
},
|
||||
dangerous: {
|
||||
container: 'p-3 bg-red-50/50 dark:bg-red-900/10 rounded-lg border border-red-100 dark:border-red-900/30',
|
||||
checkbox: 'text-red-600 focus:ring-red-500',
|
||||
hover: 'hover:bg-red-100/50 dark:hover:bg-red-900/20',
|
||||
},
|
||||
};
|
||||
|
||||
const styles = variantStyles[variant];
|
||||
const gridCols = columns === 1 ? 'grid-cols-1' : 'grid-cols-2';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
{title}
|
||||
{variant === 'dangerous' && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] font-medium bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 rounded">
|
||||
{t('common.caution', 'Caution')}
|
||||
</span>
|
||||
)}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">{description}</p>
|
||||
</div>
|
||||
{!readOnly && onSelectAll && onClearAll && (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSelectAll}
|
||||
className="text-xs text-brand-600 dark:text-brand-400 hover:underline"
|
||||
>
|
||||
{t('common.selectAll', 'Select All')}
|
||||
</button>
|
||||
<span className="text-gray-300 dark:text-gray-600">|</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClearAll}
|
||||
className="text-xs text-gray-500 dark:text-gray-400 hover:underline"
|
||||
>
|
||||
{t('common.clearAll', 'Clear All')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`grid ${gridCols} gap-2 ${styles.container}`}>
|
||||
{Object.entries(permissions).map(([key, def]) => (
|
||||
<PermissionCheckbox
|
||||
key={key}
|
||||
permissionKey={key}
|
||||
definition={def}
|
||||
checked={values[key] || false}
|
||||
onChange={(value) => onChange(key, value)}
|
||||
checkboxClass={styles.checkbox}
|
||||
hoverClass={styles.hover}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface PermissionCheckboxProps {
|
||||
permissionKey: string;
|
||||
definition: PermissionDefinition;
|
||||
checked: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
checkboxClass?: string;
|
||||
hoverClass?: string;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Individual permission checkbox with label and description
|
||||
*/
|
||||
export const PermissionCheckbox: React.FC<PermissionCheckboxProps> = ({
|
||||
permissionKey,
|
||||
definition,
|
||||
checked,
|
||||
onChange,
|
||||
checkboxClass = 'text-brand-600 focus:ring-brand-500',
|
||||
hoverClass = 'hover:bg-gray-50 dark:hover:bg-gray-700/50',
|
||||
readOnly = false,
|
||||
}) => {
|
||||
return (
|
||||
<label
|
||||
className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer ${readOnly ? 'opacity-60 cursor-default' : hoverClass}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
disabled={readOnly}
|
||||
className={`w-4 h-4 border-gray-300 dark:border-gray-600 rounded ${checkboxClass} disabled:opacity-50`}
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{definition.label}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{definition.description}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
interface RolePermissionsEditorProps {
|
||||
permissions: Record<string, boolean>;
|
||||
onChange: (permissions: Record<string, boolean>) => void;
|
||||
availablePermissions: {
|
||||
menu: Record<string, PermissionDefinition>;
|
||||
settings: Record<string, PermissionDefinition>;
|
||||
dangerous: Record<string, PermissionDefinition>;
|
||||
};
|
||||
readOnly?: boolean;
|
||||
columns?: 1 | 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full role permissions editor with all three sections
|
||||
*/
|
||||
export const RolePermissionsEditor: React.FC<RolePermissionsEditorProps> = ({
|
||||
permissions,
|
||||
onChange,
|
||||
availablePermissions,
|
||||
readOnly = false,
|
||||
columns = 2,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const togglePermission = (key: string, value: boolean) => {
|
||||
const updates: Record<string, boolean> = { [key]: value };
|
||||
|
||||
// If enabling any settings sub-permission, also enable the main settings access
|
||||
if (value && key.startsWith('can_access_settings_')) {
|
||||
updates['can_access_settings'] = true;
|
||||
}
|
||||
|
||||
// If disabling the main settings access, disable all sub-permissions
|
||||
if (!value && key === 'can_access_settings') {
|
||||
Object.keys(availablePermissions.settings).forEach((settingKey) => {
|
||||
if (settingKey !== 'can_access_settings') {
|
||||
updates[settingKey] = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onChange({ ...permissions, ...updates });
|
||||
};
|
||||
|
||||
const toggleAllInCategory = (category: 'menu' | 'settings' | 'dangerous', enable: boolean) => {
|
||||
const categoryPerms = availablePermissions[category];
|
||||
const updates: Record<string, boolean> = {};
|
||||
Object.keys(categoryPerms).forEach((key) => {
|
||||
updates[key] = enable;
|
||||
});
|
||||
|
||||
// If enabling settings permissions, ensure main settings access is also enabled
|
||||
if (category === 'settings' && enable) {
|
||||
updates['can_access_settings'] = true;
|
||||
}
|
||||
|
||||
onChange({ ...permissions, ...updates });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Menu Permissions */}
|
||||
<PermissionSection
|
||||
title={t('settings.staffRoles.menuPermissions', 'Menu Access')}
|
||||
description={t('settings.staffRoles.menuPermissionsDescription', 'Control which pages staff can see in the sidebar.')}
|
||||
permissions={availablePermissions.menu}
|
||||
values={permissions}
|
||||
onChange={togglePermission}
|
||||
onSelectAll={() => toggleAllInCategory('menu', true)}
|
||||
onClearAll={() => toggleAllInCategory('menu', false)}
|
||||
variant="default"
|
||||
readOnly={readOnly}
|
||||
columns={columns}
|
||||
/>
|
||||
|
||||
{/* Settings Permissions */}
|
||||
<PermissionSection
|
||||
title={t('settings.staffRoles.settingsPermissions', 'Business Settings Access')}
|
||||
description={t('settings.staffRoles.settingsPermissionsDescription', 'Control which settings pages staff can access.')}
|
||||
permissions={availablePermissions.settings}
|
||||
values={permissions}
|
||||
onChange={togglePermission}
|
||||
onSelectAll={() => toggleAllInCategory('settings', true)}
|
||||
onClearAll={() => toggleAllInCategory('settings', false)}
|
||||
variant="settings"
|
||||
readOnly={readOnly}
|
||||
columns={columns}
|
||||
/>
|
||||
|
||||
{/* Dangerous Permissions */}
|
||||
<PermissionSection
|
||||
title={t('settings.staffRoles.dangerousPermissions', 'Dangerous Operations')}
|
||||
description={t('settings.staffRoles.dangerousPermissionsDescription', 'Allow staff to perform destructive or sensitive actions.')}
|
||||
permissions={availablePermissions.dangerous}
|
||||
values={permissions}
|
||||
onChange={togglePermission}
|
||||
onSelectAll={() => toggleAllInCategory('dangerous', true)}
|
||||
onClearAll={() => toggleAllInCategory('dangerous', false)}
|
||||
variant="dangerous"
|
||||
readOnly={readOnly}
|
||||
columns={columns}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RolePermissionsEditor;
|
||||
Reference in New Issue
Block a user