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:
@@ -1,8 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Save, Key, RefreshCw } from 'lucide-react';
|
||||
import { X, Save, RefreshCw } from 'lucide-react';
|
||||
import { useUpdateBusiness } from '../../../hooks/usePlatform';
|
||||
import { useSubscriptionPlans } from '../../../hooks/usePlatformSettings';
|
||||
import { PlatformBusiness } from '../../../api/platform';
|
||||
import FeaturesPermissionsEditor, { PERMISSION_DEFINITIONS, getPermissionKey } from '../../../components/platform/FeaturesPermissionsEditor';
|
||||
|
||||
// Default tier settings - used when no subscription plans are loaded
|
||||
const TIER_DEFAULTS: Record<string, {
|
||||
@@ -92,6 +93,15 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
||||
can_create_plugins: false,
|
||||
can_use_webhooks: false,
|
||||
can_use_calendar_sync: false,
|
||||
can_use_contracts: false,
|
||||
can_process_refunds: false,
|
||||
can_create_packages: false,
|
||||
can_use_email_templates: false,
|
||||
can_customize_booking_page: false,
|
||||
advanced_reporting: false,
|
||||
priority_support: false,
|
||||
dedicated_support: false,
|
||||
sso_enabled: false,
|
||||
});
|
||||
|
||||
// Get tier defaults from subscription plans or fallback to static defaults
|
||||
@@ -215,6 +225,15 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
||||
can_create_plugins: b.can_create_plugins || false,
|
||||
can_use_webhooks: b.can_use_webhooks || false,
|
||||
can_use_calendar_sync: b.can_use_calendar_sync || false,
|
||||
can_use_contracts: b.can_use_contracts || false,
|
||||
can_process_refunds: b.can_process_refunds || false,
|
||||
can_create_packages: b.can_create_packages || false,
|
||||
can_use_email_templates: b.can_use_email_templates || false,
|
||||
can_customize_booking_page: b.can_customize_booking_page || false,
|
||||
advanced_reporting: b.advanced_reporting || false,
|
||||
priority_support: b.priority_support || false,
|
||||
dedicated_support: b.dedicated_support || false,
|
||||
sso_enabled: b.sso_enabled || false,
|
||||
});
|
||||
}
|
||||
}, [business]);
|
||||
@@ -355,207 +374,18 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features & Permissions */}
|
||||
{/* Features & Permissions - Using unified FeaturesPermissionsEditor */}
|
||||
<div className="space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<Key size={16} className="text-purple-500" />
|
||||
Features & Permissions
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Control which features are available to this business.
|
||||
</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={editForm.can_accept_payments}
|
||||
onChange={(e) => setEditForm({ ...editForm, can_accept_payments: e.target.checked })}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Online Payments</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={editForm.can_use_sms_reminders}
|
||||
onChange={(e) => setEditForm({ ...editForm, can_use_sms_reminders: e.target.checked })}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-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={editForm.can_use_masked_phone_numbers}
|
||||
onChange={(e) => setEditForm({ ...editForm, can_use_masked_phone_numbers: e.target.checked })}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Masked Calling</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={editForm.can_use_custom_domain}
|
||||
onChange={(e) => setEditForm({ ...editForm, can_use_custom_domain: e.target.checked })}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-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={editForm.can_white_label}
|
||||
onChange={(e) => setEditForm({ ...editForm, can_white_label: e.target.checked })}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">White Labelling</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plugins & Automation */}
|
||||
<div>
|
||||
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Plugins & Automation</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={editForm.can_use_plugins}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked;
|
||||
setEditForm(prev => ({
|
||||
...prev,
|
||||
can_use_plugins: checked,
|
||||
// If disabling plugins, also disable tasks and create plugins
|
||||
...(checked ? {} : { can_use_tasks: false, can_create_plugins: false })
|
||||
}));
|
||||
}}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-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 ${editForm.can_use_plugins ? 'hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer' : 'opacity-50 cursor-not-allowed'}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editForm.can_use_tasks}
|
||||
onChange={(e) => setEditForm({ ...editForm, can_use_tasks: e.target.checked })}
|
||||
disabled={!editForm.can_use_plugins}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 disabled:opacity-50"
|
||||
/>
|
||||
<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 ${editForm.can_use_plugins ? 'hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer' : 'opacity-50 cursor-not-allowed'}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editForm.can_create_plugins}
|
||||
onChange={(e) => setEditForm({ ...editForm, can_create_plugins: e.target.checked })}
|
||||
disabled={!editForm.can_use_plugins}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 disabled:opacity-50"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Create Plugins</span>
|
||||
</label>
|
||||
</div>
|
||||
{!editForm.can_use_plugins && (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400 mt-2">
|
||||
Enable "Use Plugins" to allow Scheduled Tasks and Create Plugins
|
||||
</p>
|
||||
)}
|
||||
</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={editForm.can_api_access}
|
||||
onChange={(e) => setEditForm({ ...editForm, can_api_access: e.target.checked })}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-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={editForm.can_use_webhooks}
|
||||
onChange={(e) => setEditForm({ ...editForm, can_use_webhooks: e.target.checked })}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-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={editForm.can_use_calendar_sync}
|
||||
onChange={(e) => setEditForm({ ...editForm, can_use_calendar_sync: e.target.checked })}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Calendar Sync</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={editForm.can_export_data}
|
||||
onChange={(e) => setEditForm({ ...editForm, can_export_data: e.target.checked })}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-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={editForm.can_add_video_conferencing}
|
||||
onChange={(e) => setEditForm({ ...editForm, can_add_video_conferencing: e.target.checked })}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Video Conferencing</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enterprise */}
|
||||
<div>
|
||||
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">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={editForm.can_manage_oauth_credentials}
|
||||
onChange={(e) => setEditForm({ ...editForm, can_manage_oauth_credentials: e.target.checked })}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Manage OAuth</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={editForm.can_require_2fa}
|
||||
onChange={(e) => setEditForm({ ...editForm, can_require_2fa: e.target.checked })}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Require 2FA</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<FeaturesPermissionsEditor
|
||||
mode="business"
|
||||
values={Object.fromEntries(
|
||||
Object.entries(editForm).filter(([_, v]) => typeof v === 'boolean')
|
||||
) as Record<string, boolean>}
|
||||
onChange={(key, value) => {
|
||||
setEditForm(prev => ({ ...prev, [key]: value }));
|
||||
}}
|
||||
headerTitle="Features & Permissions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user