feat: Unified FeaturesPermissionsEditor component for plan and business permissions

- Create reusable FeaturesPermissionsEditor component with support for both
  subscription plan editing and individual business permission overrides
- Add can_use_contracts field to Tenant model for per-business contracts toggle
- Update PlatformSettings.tsx to use unified component for plan permissions
- Update BusinessEditModal.tsx to use unified component for business permissions
- Update PlatformBusinessUpdate API interface with all permission fields
- Add contracts permission mapping to tenant sync task

🤖 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-10 01:27:09 -05:00
parent 2f6ea82114
commit 485f86086b
9 changed files with 638 additions and 453 deletions

View File

@@ -46,6 +46,7 @@ import {
useUpdatePlatformOAuthSettings,
} from '../../hooks/usePlatformOAuth';
import { Link } from 'react-router-dom';
import FeaturesPermissionsEditor, { getPermissionKey, PERMISSION_DEFINITIONS } from '../../components/platform/FeaturesPermissionsEditor';
type TabType = 'general' | 'stripe' | 'tiers' | 'oauth';
@@ -241,6 +242,7 @@ const GeneralSettingsTab: React.FC = () => {
};
const StripeSettingsTab: React.FC = () => {
const { t } = useTranslation();
const { data: settings, isLoading, error } = usePlatformSettings();
const updateKeysMutation = useUpdateStripeKeys();
const validateKeysMutation = useValidateStripeKeys();
@@ -1254,25 +1256,6 @@ const PlanModal: React.FC<PlanModalProps> = ({ plan, onSave, onClose, isLoading
)}
</div>
{/* Contracts Feature */}
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<div className="flex items-center justify-between">
<div>
<h4 className="text-sm font-medium text-gray-900 dark:text-white">Contracts</h4>
<p className="text-xs text-gray-500 dark:text-gray-400">Allow tenants to create and manage contracts with customers</p>
</div>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={formData.contracts_enabled || false}
onChange={(e) => setFormData((prev) => ({ ...prev, contracts_enabled: e.target.checked }))}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Enabled</span>
</label>
</div>
</div>
{/* Default Credit Settings */}
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">Default Credit Settings</h4>
@@ -1422,238 +1405,25 @@ const PlanModal: React.FC<PlanModalProps> = ({ plan, onSave, onClose, isLoading
</div>
</div>
{/* Permissions Configuration */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-gray-900 dark:text-white border-b pb-2 dark:border-gray-700">
Features & Permissions
</h3>
<p className="text-xs text-gray-500 dark:text-gray-400">
Control which features are available to businesses on this plan.
</p>
{/* Payments & Revenue */}
<div>
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Payments & Revenue</h4>
<div className="grid grid-cols-3 gap-3">
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.can_accept_payments || false}
onChange={(e) => handlePermissionChange('can_accept_payments', e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Online Payments</span>
</label>
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.can_process_refunds || false}
onChange={(e) => handlePermissionChange('can_process_refunds', e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Process Refunds</span>
</label>
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.can_create_packages || false}
onChange={(e) => handlePermissionChange('can_create_packages', e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Service Packages</span>
</label>
</div>
</div>
{/* Communication */}
<div>
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Communication</h4>
<div className="grid grid-cols-3 gap-3">
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.sms_reminders || false}
onChange={(e) => handlePermissionChange('sms_reminders', e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">SMS Reminders</span>
</label>
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.can_use_masked_phone_numbers || false}
onChange={(e) => handlePermissionChange('can_use_masked_phone_numbers', e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Masked Calling</span>
</label>
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.can_use_email_templates || false}
onChange={(e) => handlePermissionChange('can_use_email_templates', e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Email Templates</span>
</label>
</div>
</div>
{/* Customization */}
<div>
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Customization</h4>
<div className="grid grid-cols-3 gap-3">
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.can_customize_booking_page || false}
onChange={(e) => handlePermissionChange('can_customize_booking_page', e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Custom Booking Page</span>
</label>
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.can_use_custom_domain || false}
onChange={(e) => handlePermissionChange('can_use_custom_domain', e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Custom Domains</span>
</label>
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.can_white_label || false}
onChange={(e) => handlePermissionChange('can_white_label', e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">White Labelling</span>
</label>
</div>
</div>
{/* Advanced Features */}
<div>
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Advanced Features</h4>
<div className="grid grid-cols-3 gap-3">
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.advanced_reporting || false}
onChange={(e) => handlePermissionChange('advanced_reporting', e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Advanced Analytics</span>
</label>
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.can_api_access || false}
onChange={(e) => handlePermissionChange('can_api_access', e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">API Access</span>
</label>
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.can_use_plugins || false}
onChange={(e) => {
handlePermissionChange('can_use_plugins', e.target.checked);
// If disabling plugins, also disable dependent permissions
if (!e.target.checked) {
handlePermissionChange('can_use_tasks', false);
handlePermissionChange('can_create_plugins', false);
}
}}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Use Plugins</span>
</label>
<label className={`flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer ${formData.permissions?.can_use_plugins ? 'hover:bg-gray-50 dark:hover:bg-gray-700/50' : 'opacity-50 cursor-not-allowed'}`}>
<input
type="checkbox"
checked={formData.permissions?.can_use_tasks || false}
onChange={(e) => handlePermissionChange('can_use_tasks', e.target.checked)}
disabled={!formData.permissions?.can_use_plugins}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Scheduled Tasks</span>
</label>
<label className={`flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer ${formData.permissions?.can_use_plugins ? 'hover:bg-gray-50 dark:hover:bg-gray-700/50' : 'opacity-50 cursor-not-allowed'}`}>
<input
type="checkbox"
checked={formData.permissions?.can_create_plugins || false}
onChange={(e) => handlePermissionChange('can_create_plugins', e.target.checked)}
disabled={!formData.permissions?.can_use_plugins}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Create Plugins</span>
</label>
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.can_export_data || false}
onChange={(e) => handlePermissionChange('can_export_data', e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Data Export</span>
</label>
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.can_use_webhooks || false}
onChange={(e) => handlePermissionChange('can_use_webhooks', e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Webhooks</span>
</label>
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.calendar_sync || false}
onChange={(e) => handlePermissionChange('calendar_sync', e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Calendar Sync</span>
</label>
</div>
</div>
{/* Support & Enterprise */}
<div>
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Support & Enterprise</h4>
<div className="grid grid-cols-3 gap-3">
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.priority_support || false}
onChange={(e) => handlePermissionChange('priority_support', e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Priority Support</span>
</label>
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.dedicated_support || false}
onChange={(e) => handlePermissionChange('dedicated_support', e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Dedicated Support</span>
</label>
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
<input
type="checkbox"
checked={formData.permissions?.sso_enabled || false}
onChange={(e) => handlePermissionChange('sso_enabled', e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">SSO / SAML</span>
</label>
</div>
</div>
{/* Permissions Configuration - Using unified FeaturesPermissionsEditor */}
<div className="space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<FeaturesPermissionsEditor
mode="plan"
values={{
...formData.permissions,
// Map contracts_enabled to the permission key used by the component
contracts_enabled: formData.contracts_enabled || false,
}}
onChange={(key, value) => {
// Handle contracts_enabled specially since it's a top-level plan field
if (key === 'contracts_enabled') {
setFormData((prev) => ({ ...prev, contracts_enabled: value }));
} else {
handlePermissionChange(key, value);
}
}}
headerTitle="Features & Permissions"
/>
</div>
{/* Display Features (List of strings) */}