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:
poduck
2025-12-24 20:46:36 -05:00
parent d8d3a4e846
commit 464726ee3e
47 changed files with 7826 additions and 2723 deletions

View 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;