Add staff permission controls for editing staff and customers
- Add can_edit_staff and can_edit_customers dangerous permissions - Move Site Builder, Services, Locations, Time Blocks, Payments to Settings permissions - Link Edit Others' Schedules and Edit Own Schedule permissions - Add permission checks to StaffViewSet (partial_update, toggle_active, verify_email) - Add permission checks to CustomerViewSet (update, partial_update, verify_email) - Fix CustomerViewSet permission key mismatch (can_access_customers) - Hide Edit/Verify buttons on Staff and Customers pages without permission - Make dangerous permissions section more visually distinct (darker red) - Fix StaffDashboard links to use correct paths (/dashboard/my-schedule) - Disable settings sub-permissions when Access Settings is unchecked 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,8 @@ export interface PermissionSectionProps {
|
||||
variant?: 'default' | 'settings' | 'dangerous';
|
||||
readOnly?: boolean;
|
||||
columns?: 1 | 2;
|
||||
lockedPermissions?: Record<string, string>; // key -> reason (forced on)
|
||||
disabledPermissions?: Record<string, string>; // key -> reason (grayed out until parent enabled)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,6 +38,8 @@ export const PermissionSection: React.FC<PermissionSectionProps> = ({
|
||||
variant = 'default',
|
||||
readOnly = false,
|
||||
columns = 2,
|
||||
lockedPermissions = {},
|
||||
disabledPermissions = {},
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -51,9 +55,9 @@ export const PermissionSection: React.FC<PermissionSectionProps> = ({
|
||||
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',
|
||||
container: 'p-3 bg-red-100 dark:bg-red-900/30 rounded-lg border border-red-200 dark:border-red-800/50',
|
||||
checkbox: 'text-red-600 focus:ring-red-500',
|
||||
hover: 'hover:bg-red-100/50 dark:hover:bg-red-900/20',
|
||||
hover: 'hover:bg-red-200/70 dark:hover:bg-red-900/40',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -105,6 +109,10 @@ export const PermissionSection: React.FC<PermissionSectionProps> = ({
|
||||
checkboxClass={styles.checkbox}
|
||||
hoverClass={styles.hover}
|
||||
readOnly={readOnly}
|
||||
locked={!!lockedPermissions[key]}
|
||||
lockedReason={lockedPermissions[key]}
|
||||
disabled={!!disabledPermissions[key]}
|
||||
disabledReason={disabledPermissions[key]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -120,6 +128,10 @@ interface PermissionCheckboxProps {
|
||||
checkboxClass?: string;
|
||||
hoverClass?: string;
|
||||
readOnly?: boolean;
|
||||
locked?: boolean;
|
||||
lockedReason?: string;
|
||||
disabled?: boolean;
|
||||
disabledReason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,21 +145,34 @@ export const PermissionCheckbox: React.FC<PermissionCheckboxProps> = ({
|
||||
checkboxClass = 'text-brand-600 focus:ring-brand-500',
|
||||
hoverClass = 'hover:bg-gray-50 dark:hover:bg-gray-700/50',
|
||||
readOnly = false,
|
||||
locked = false,
|
||||
lockedReason,
|
||||
disabled = false,
|
||||
disabledReason,
|
||||
}) => {
|
||||
const isDisabled = readOnly || locked || disabled;
|
||||
const tooltipReason = locked ? lockedReason : disabled ? disabledReason : undefined;
|
||||
|
||||
return (
|
||||
<label
|
||||
className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer ${readOnly ? 'opacity-60 cursor-default' : hoverClass}`}
|
||||
className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer ${isDisabled ? 'opacity-60 cursor-default' : hoverClass}`}
|
||||
title={tooltipReason}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
disabled={readOnly}
|
||||
disabled={isDisabled}
|
||||
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">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white flex items-center gap-1.5">
|
||||
{definition.label}
|
||||
{locked && (
|
||||
<span className="text-[10px] text-gray-400 dark:text-gray-500 font-normal">
|
||||
(required)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{definition.description}
|
||||
@@ -198,6 +223,21 @@ export const RolePermissionsEditor: React.FC<RolePermissionsEditorProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
// Schedule editing permissions are linked:
|
||||
// - If enabling "edit others' schedules", also enable "edit own schedule"
|
||||
// - "edit own schedule" can be disabled independently only if "edit others'" is off
|
||||
if (value && key === 'can_edit_others_schedules') {
|
||||
updates['can_edit_own_schedule'] = true;
|
||||
}
|
||||
|
||||
// Prevent disabling "edit own schedule" if "edit others' schedules" is enabled
|
||||
if (!value && key === 'can_edit_own_schedule') {
|
||||
if (permissions['can_edit_others_schedules']) {
|
||||
// Keep it enabled - can't disable own schedule editing while others' is enabled
|
||||
updates['can_edit_own_schedule'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
onChange({ ...permissions, ...updates });
|
||||
};
|
||||
|
||||
@@ -213,9 +253,39 @@ export const RolePermissionsEditor: React.FC<RolePermissionsEditorProps> = ({
|
||||
updates['can_access_settings'] = true;
|
||||
}
|
||||
|
||||
// If enabling menu permissions including edit_others, ensure edit_own is also enabled
|
||||
if (category === 'menu' && enable && updates['can_edit_others_schedules']) {
|
||||
updates['can_edit_own_schedule'] = true;
|
||||
}
|
||||
|
||||
onChange({ ...permissions, ...updates });
|
||||
};
|
||||
|
||||
// Calculate which permissions are locked (cannot be unchecked due to dependencies)
|
||||
const lockedPermissions: Record<string, string> = {};
|
||||
|
||||
// "Edit Own Schedule" is locked when "Edit Others' Schedules" is enabled
|
||||
if (permissions['can_edit_others_schedules']) {
|
||||
lockedPermissions['can_edit_own_schedule'] = t(
|
||||
'settings.staffRoles.lockedByEditOthers',
|
||||
'Required when "Edit Others\' Schedules" is enabled'
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate which settings permissions are disabled (require "Access Settings" to be enabled first)
|
||||
const disabledSettingsPermissions: Record<string, string> = {};
|
||||
if (!permissions['can_access_settings']) {
|
||||
// Disable all settings sub-permissions when main settings access is off
|
||||
Object.keys(availablePermissions.settings).forEach((key) => {
|
||||
if (key !== 'can_access_settings') {
|
||||
disabledSettingsPermissions[key] = t(
|
||||
'settings.staffRoles.requiresAccessSettings',
|
||||
'Enable "Access Settings" first'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Menu Permissions */}
|
||||
@@ -230,6 +300,7 @@ export const RolePermissionsEditor: React.FC<RolePermissionsEditorProps> = ({
|
||||
variant="default"
|
||||
readOnly={readOnly}
|
||||
columns={columns}
|
||||
lockedPermissions={lockedPermissions}
|
||||
/>
|
||||
|
||||
{/* Settings Permissions */}
|
||||
@@ -244,6 +315,8 @@ export const RolePermissionsEditor: React.FC<RolePermissionsEditorProps> = ({
|
||||
variant="settings"
|
||||
readOnly={readOnly}
|
||||
columns={columns}
|
||||
lockedPermissions={lockedPermissions}
|
||||
disabledPermissions={disabledSettingsPermissions}
|
||||
/>
|
||||
|
||||
{/* Dangerous Permissions */}
|
||||
@@ -256,6 +329,7 @@ export const RolePermissionsEditor: React.FC<RolePermissionsEditorProps> = ({
|
||||
onSelectAll={() => toggleAllInCategory('dangerous', true)}
|
||||
onClearAll={() => toggleAllInCategory('dangerous', false)}
|
||||
variant="dangerous"
|
||||
lockedPermissions={lockedPermissions}
|
||||
readOnly={readOnly}
|
||||
columns={columns}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user