feat: Quota overage system, updated tier pricing, and communication addons

Quota Overage System:
- Add QuotaOverage model for tracking resource/user quota overages
- Implement 30-day grace period with email notifications (immediate, 7-day, 1-day)
- Add QuotaWarningBanner component in BusinessLayout
- Add QuotaSettings page for managing overages and archiving resources
- Add Celery tasks for automated quota checks and expiration handling
- Add quota management API endpoints

Updated Tier Pricing (Stripe: 2.9% + $0.30):
- Free: No payments (requires addon)
- Starter: 4% + $0.40
- Professional: 3.5% + $0.35
- Business: 3.25% + $0.32
- Enterprise: 3% + $0.30

New Subscription Addons:
- Online Payments ($5/mo + 5% + $0.50) - for Free tier
- SMS Notifications ($10/mo) - enables SMS reminders
- Masked Calling ($15/mo) - enables anonymous calling

BusinessEditModal Improvements:
- Increased width to match PlanModal (max-w-3xl)
- Added all tier options with auto-update on tier change
- Added limits configuration and permissions sections

Backend Fixes:
- Fixed SubscriptionPlan serializer to include all communication fields
- Allow blank business_tier for addon plans
- Added migration for business_tier field changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-02 13:05:02 -05:00
parent dc3210927a
commit 08b51d1a5f
36 changed files with 3469 additions and 350 deletions

View File

@@ -1,8 +1,57 @@
import React, { useState, useEffect } from 'react';
import { X, Save, Key } from 'lucide-react';
import { X, Save, Key, RefreshCw } from 'lucide-react';
import { useUpdateBusiness } from '../../../hooks/usePlatform';
import { useSubscriptionPlans } from '../../../hooks/usePlatformSettings';
import { PlatformBusiness } from '../../../api/platform';
// Default tier settings - used when no subscription plans are loaded
const TIER_DEFAULTS: Record<string, {
max_users: number;
max_resources: number;
can_manage_oauth_credentials: boolean;
can_accept_payments: boolean;
can_use_custom_domain: boolean;
can_white_label: boolean;
can_api_access: boolean;
}> = {
FREE: {
max_users: 2,
max_resources: 5,
can_manage_oauth_credentials: false,
can_accept_payments: false,
can_use_custom_domain: false,
can_white_label: false,
can_api_access: false,
},
STARTER: {
max_users: 5,
max_resources: 15,
can_manage_oauth_credentials: false,
can_accept_payments: true,
can_use_custom_domain: false,
can_white_label: false,
can_api_access: false,
},
PROFESSIONAL: {
max_users: 15,
max_resources: 50,
can_manage_oauth_credentials: false,
can_accept_payments: true,
can_use_custom_domain: true,
can_white_label: false,
can_api_access: true,
},
ENTERPRISE: {
max_users: -1, // unlimited
max_resources: -1, // unlimited
can_manage_oauth_credentials: true,
can_accept_payments: true,
can_use_custom_domain: true,
can_white_label: true,
can_api_access: true,
},
};
interface BusinessEditModalProps {
business: PlatformBusiness | null;
isOpen: boolean;
@@ -11,34 +60,144 @@ interface BusinessEditModalProps {
const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen, onClose }) => {
const updateBusinessMutation = useUpdateBusiness();
const { data: subscriptionPlans } = useSubscriptionPlans();
const [editForm, setEditForm] = useState({
name: '',
is_active: true,
subscription_tier: 'FREE',
// Limits
max_users: 5,
max_resources: 10,
max_services: 0,
max_appointments: 0,
max_email_templates: 0,
max_automated_tasks: 0,
// Platform Permissions
can_manage_oauth_credentials: false,
can_accept_payments: false,
can_use_custom_domain: false,
can_white_label: false,
can_api_access: false,
// New feature limits (not yet implemented)
limits: {
can_add_video_conferencing: false,
max_event_types: null as number | null,
max_calendars_connected: null as number | null,
can_connect_to_api: false,
can_book_repeated_events: false,
can_require_2fa: false,
can_download_logs: false,
can_delete_data: false,
// Extended Permissions
permissions: {
// Payments & Revenue
can_process_refunds: false,
can_create_packages: false,
// Communication
sms_reminders: false,
can_use_masked_phone_numbers: false,
can_use_pos: false,
can_use_mobile_app: false,
can_use_email_templates: false,
// Customization
can_customize_booking_page: false,
// Advanced Features
advanced_reporting: false,
can_create_plugins: false,
can_export_data: false,
can_use_webhooks: false,
calendar_sync: false,
// Support & Enterprise
priority_support: false,
dedicated_support: false,
sso_enabled: false,
},
});
// Get tier defaults from subscription plans or fallback to static defaults
const getTierDefaults = (tier: string) => {
// Try to find matching subscription plan
if (subscriptionPlans) {
const tierNameMap: Record<string, string> = {
'FREE': 'Free',
'STARTER': 'Starter',
'PROFESSIONAL': 'Professional',
'ENTERPRISE': 'Enterprise',
};
const plan = subscriptionPlans.find(p =>
p.business_tier === tierNameMap[tier] || p.business_tier === tier
);
if (plan) {
const staticDefaults = TIER_DEFAULTS[tier] || TIER_DEFAULTS.FREE;
return {
// Limits
max_users: plan.limits?.max_users ?? staticDefaults.max_users,
max_resources: plan.limits?.max_resources ?? staticDefaults.max_resources,
max_services: plan.limits?.max_services ?? 0,
max_appointments: plan.limits?.max_appointments ?? 0,
max_email_templates: plan.limits?.max_email_templates ?? 0,
max_automated_tasks: plan.limits?.max_automated_tasks ?? 0,
// Platform Permissions
can_manage_oauth_credentials: plan.permissions?.can_manage_oauth_credentials ?? staticDefaults.can_manage_oauth_credentials,
can_accept_payments: plan.permissions?.can_accept_payments ?? staticDefaults.can_accept_payments,
can_use_custom_domain: plan.permissions?.can_use_custom_domain ?? staticDefaults.can_use_custom_domain,
can_white_label: plan.permissions?.can_white_label ?? staticDefaults.can_white_label,
can_api_access: plan.permissions?.can_api_access ?? staticDefaults.can_api_access,
// Extended Permissions
permissions: {
can_process_refunds: plan.permissions?.can_process_refunds ?? false,
can_create_packages: plan.permissions?.can_create_packages ?? false,
sms_reminders: plan.permissions?.sms_reminders ?? false,
can_use_masked_phone_numbers: plan.permissions?.can_use_masked_phone_numbers ?? false,
can_use_email_templates: plan.permissions?.can_use_email_templates ?? false,
can_customize_booking_page: plan.permissions?.can_customize_booking_page ?? false,
advanced_reporting: plan.permissions?.advanced_reporting ?? false,
can_create_plugins: plan.permissions?.can_create_plugins ?? false,
can_export_data: plan.permissions?.can_export_data ?? false,
can_use_webhooks: plan.permissions?.can_use_webhooks ?? false,
calendar_sync: plan.permissions?.calendar_sync ?? false,
priority_support: plan.permissions?.priority_support ?? false,
dedicated_support: plan.permissions?.dedicated_support ?? false,
sso_enabled: plan.permissions?.sso_enabled ?? false,
},
};
}
}
// Fallback to static defaults
const staticDefaults = TIER_DEFAULTS[tier] || TIER_DEFAULTS.FREE;
return {
...staticDefaults,
max_services: 0,
max_appointments: 0,
max_email_templates: 0,
max_automated_tasks: 0,
permissions: {
can_process_refunds: false,
can_create_packages: false,
sms_reminders: false,
can_use_masked_phone_numbers: false,
can_use_email_templates: false,
can_customize_booking_page: false,
advanced_reporting: false,
can_create_plugins: false,
can_export_data: false,
can_use_webhooks: false,
calendar_sync: false,
priority_support: false,
dedicated_support: false,
sso_enabled: false,
},
};
};
// Handle subscription tier change - auto-update limits and permissions
const handleTierChange = (newTier: string) => {
const defaults = getTierDefaults(newTier);
setEditForm(prev => ({
...prev,
subscription_tier: newTier,
...defaults,
}));
};
// Reset to tier defaults button handler
const handleResetToTierDefaults = () => {
const defaults = getTierDefaults(editForm.subscription_tier);
setEditForm(prev => ({
...prev,
...defaults,
}));
};
// Update form when business changes
useEffect(() => {
if (business) {
@@ -46,30 +205,51 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
name: business.name,
is_active: business.is_active,
subscription_tier: business.tier,
// Limits
max_users: business.max_users || 5,
max_resources: business.max_resources || 10,
max_services: (business as any).max_services || 0,
max_appointments: (business as any).max_appointments || 0,
max_email_templates: (business as any).max_email_templates || 0,
max_automated_tasks: (business as any).max_automated_tasks || 0,
// Platform Permissions
can_manage_oauth_credentials: business.can_manage_oauth_credentials || false,
can_accept_payments: business.can_accept_payments || false,
can_use_custom_domain: business.can_use_custom_domain || false,
can_white_label: business.can_white_label || false,
can_api_access: business.can_api_access || false,
limits: {
can_add_video_conferencing: false,
max_event_types: null,
max_calendars_connected: null,
can_connect_to_api: false,
can_book_repeated_events: false,
can_require_2fa: false,
can_download_logs: false,
can_delete_data: false,
can_use_masked_phone_numbers: false,
can_use_pos: false,
can_use_mobile_app: false,
// Extended Permissions
permissions: {
can_process_refunds: (business as any).permissions?.can_process_refunds || false,
can_create_packages: (business as any).permissions?.can_create_packages || false,
sms_reminders: (business as any).permissions?.sms_reminders || false,
can_use_masked_phone_numbers: (business as any).permissions?.can_use_masked_phone_numbers || false,
can_use_email_templates: (business as any).permissions?.can_use_email_templates || false,
can_customize_booking_page: (business as any).permissions?.can_customize_booking_page || false,
advanced_reporting: (business as any).permissions?.advanced_reporting || false,
can_create_plugins: (business as any).permissions?.can_create_plugins || false,
can_export_data: (business as any).permissions?.can_export_data || false,
can_use_webhooks: (business as any).permissions?.can_use_webhooks || false,
calendar_sync: (business as any).permissions?.calendar_sync || false,
priority_support: (business as any).permissions?.priority_support || false,
dedicated_support: (business as any).permissions?.dedicated_support || false,
sso_enabled: (business as any).permissions?.sso_enabled || false,
},
});
}
}, [business]);
// Helper for permission changes
const handlePermissionChange = (key: string, value: boolean) => {
setEditForm(prev => ({
...prev,
permissions: {
...prev.permissions,
[key]: value,
},
}));
};
const handleEditSave = () => {
if (!business) return;
@@ -90,7 +270,7 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl w-full max-w-lg mx-4 max-h-[90vh] overflow-y-auto">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl w-full max-w-3xl mx-4 max-h-[90vh] overflow-y-auto">
{/* Modal Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
@@ -141,12 +321,22 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
{/* Subscription Tier */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Subscription Tier
</label>
<div className="flex items-center justify-between mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Subscription Tier
</label>
<button
type="button"
onClick={handleResetToTierDefaults}
className="flex items-center gap-1 text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-300"
>
<RefreshCw size={12} />
Reset to tier defaults
</button>
</div>
<select
value={editForm.subscription_tier}
onChange={(e) => setEditForm({ ...editForm, subscription_tier: e.target.value })}
onChange={(e) => handleTierChange(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
>
<option value="FREE">Free Trial</option>
@@ -154,303 +344,312 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
<option value="PROFESSIONAL">Professional</option>
<option value="ENTERPRISE">Enterprise</option>
</select>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Changing tier will auto-update limits and permissions to tier defaults
</p>
</div>
{/* Limits */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Max Users
</label>
<input
type="number"
min="1"
value={editForm.max_users}
onChange={(e) => setEditForm({ ...editForm, max_users: parseInt(e.target.value) || 1 })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Max Resources
</label>
<input
type="number"
min="1"
value={editForm.max_resources}
onChange={(e) => setEditForm({ ...editForm, max_resources: parseInt(e.target.value) || 1 })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
/>
{/* Limits Configuration */}
<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">
Limits Configuration
</h3>
<p className="text-xs text-gray-500 dark:text-gray-400">
Use -1 for unlimited. These limits control what this business can create.
</p>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Max Users
</label>
<input
type="number"
min="-1"
value={editForm.max_users}
onChange={(e) => setEditForm({ ...editForm, max_users: parseInt(e.target.value) || 0 })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Max Resources
</label>
<input
type="number"
min="-1"
value={editForm.max_resources}
onChange={(e) => setEditForm({ ...editForm, max_resources: parseInt(e.target.value) || 0 })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Max Services
</label>
<input
type="number"
min="-1"
value={editForm.max_services}
onChange={(e) => setEditForm({ ...editForm, max_services: parseInt(e.target.value) || 0 })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Max Appointments / Month
</label>
<input
type="number"
min="-1"
value={editForm.max_appointments}
onChange={(e) => setEditForm({ ...editForm, max_appointments: parseInt(e.target.value) || 0 })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Max Email Templates
</label>
<input
type="number"
min="-1"
value={editForm.max_email_templates}
onChange={(e) => setEditForm({ ...editForm, max_email_templates: parseInt(e.target.value) || 0 })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Max Automated Tasks
</label>
<input
type="number"
min="-1"
value={editForm.max_automated_tasks}
onChange={(e) => setEditForm({ ...editForm, max_automated_tasks: parseInt(e.target.value) || 0 })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
</div>
</div>
{/* Permissions Section */}
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-3 flex items-center gap-2">
{/* Features & Permissions */}
<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" />
Platform Permissions
</h4>
Features & Permissions
</h3>
<p className="text-xs text-gray-500 dark:text-gray-400">
Control which features are available to this business.
</p>
<div className="space-y-3">
{/* Can Manage OAuth Credentials */}
<div className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Manage OAuth Credentials
</label>
<p className="text-xs text-gray-500 dark:text-gray-400">
Allow this business to configure their own OAuth app credentials
</p>
</div>
<button
type="button"
onClick={() => setEditForm({ ...editForm, can_manage_oauth_credentials: !editForm.can_manage_oauth_credentials })}
className={`${editForm.can_manage_oauth_credentials ? 'bg-purple-600' : 'bg-gray-300 dark:bg-gray-600'} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-purple-500`}
role="switch"
>
<span className={`${editForm.can_manage_oauth_credentials ? 'translate-x-5' : 'translate-x-0'} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out`} />
</button>
{/* 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>
<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.permissions.can_process_refunds}
onChange={(e) => handlePermissionChange('can_process_refunds', 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">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={editForm.permissions.can_create_packages}
onChange={(e) => handlePermissionChange('can_create_packages', 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">Service Packages</span>
</label>
</div>
</div>
{/* Can Accept Payments */}
<div className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Accept Online Payments
</label>
<p className="text-xs text-gray-500 dark:text-gray-400">
Enable Stripe Connect for payment processing
</p>
</div>
<button
type="button"
onClick={() => setEditForm({ ...editForm, can_accept_payments: !editForm.can_accept_payments })}
className={`${editForm.can_accept_payments ? 'bg-purple-600' : 'bg-gray-300 dark:bg-gray-600'} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-purple-500`}
role="switch"
>
<span className={`${editForm.can_accept_payments ? 'translate-x-5' : 'translate-x-0'} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out`} />
</button>
{/* 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.permissions.sms_reminders}
onChange={(e) => handlePermissionChange('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.permissions.can_use_masked_phone_numbers}
onChange={(e) => handlePermissionChange('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>
<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.permissions.can_use_email_templates}
onChange={(e) => handlePermissionChange('can_use_email_templates', 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">Email Templates</span>
</label>
</div>
</div>
{/* Can Use Custom Domain */}
<div className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Use Custom Domain
</label>
<p className="text-xs text-gray-500 dark:text-gray-400">
Allow custom domain configuration
</p>
</div>
<button
type="button"
onClick={() => setEditForm({ ...editForm, can_use_custom_domain: !editForm.can_use_custom_domain })}
className={`${editForm.can_use_custom_domain ? 'bg-purple-600' : 'bg-gray-300 dark:bg-gray-600'} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-purple-500`}
role="switch"
>
<span className={`${editForm.can_use_custom_domain ? 'translate-x-5' : 'translate-x-0'} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out`} />
</button>
{/* 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.permissions.can_customize_booking_page}
onChange={(e) => handlePermissionChange('can_customize_booking_page', 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 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={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>
{/* Can White Label */}
<div className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Remove Branding
</label>
<p className="text-xs text-gray-500 dark:text-gray-400">
Allow removal of SmoothSchedule branding
</p>
</div>
<button
type="button"
onClick={() => setEditForm({ ...editForm, can_white_label: !editForm.can_white_label })}
className={`${editForm.can_white_label ? 'bg-purple-600' : 'bg-gray-300 dark:bg-gray-600'} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-purple-500`}
role="switch"
>
<span className={`${editForm.can_white_label ? 'translate-x-5' : 'translate-x-0'} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out`} />
</button>
{/* 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.permissions.advanced_reporting}
onChange={(e) => handlePermissionChange('advanced_reporting', 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">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={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.permissions.can_create_plugins}
onChange={(e) => handlePermissionChange('can_create_plugins', 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">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={editForm.permissions.can_export_data}
onChange={(e) => handlePermissionChange('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.permissions.can_use_webhooks}
onChange={(e) => handlePermissionChange('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.permissions.calendar_sync}
onChange={(e) => handlePermissionChange('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>
</div>
</div>
{/* Can API Access */}
<div className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
API Access
</label>
<p className="text-xs text-gray-500 dark:text-gray-400">
Enable API access for integrations
</p>
</div>
<button
type="button"
onClick={() => setEditForm({ ...editForm, can_api_access: !editForm.can_api_access })}
className={`${editForm.can_api_access ? 'bg-purple-600' : 'bg-gray-300 dark:bg-gray-600'} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-purple-500`}
role="switch"
>
<span className={`${editForm.can_api_access ? 'translate-x-5' : 'translate-x-0'} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out`} />
</button>
{/* 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={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.permissions.priority_support}
onChange={(e) => handlePermissionChange('priority_support', 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">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={editForm.permissions.dedicated_support}
onChange={(e) => handlePermissionChange('dedicated_support', 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">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={editForm.permissions.sso_enabled}
onChange={(e) => handlePermissionChange('sso_enabled', 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">SSO / SAML</span>
</label>
</div>
</div>
</div>
{/* Feature Limits (Not Yet Implemented) */}
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
<div className="flex items-center justify-between mb-3">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Feature Limits & Capabilities
</label>
<span className="text-xs px-2 py-1 bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 rounded-full">
Coming Soon
</span>
</div>
<div className="space-y-3 opacity-50">
{/* Video Conferencing */}
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
type="checkbox"
checked={editForm.limits.can_add_video_conferencing}
disabled
className="rounded border-gray-300 dark:border-gray-600 cursor-not-allowed"
/>
Can add video conferencing to events
</label>
{/* Event Types Limit */}
<div className="flex items-start gap-3">
<input
type="checkbox"
checked={editForm.limits.max_event_types === null}
disabled
className="rounded border-gray-300 dark:border-gray-600 mt-1 cursor-not-allowed"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="text-sm text-gray-700 dark:text-gray-300">Unlimited event types</span>
</div>
<input
type="number"
min="1"
disabled
value={editForm.limits.max_event_types || ''}
placeholder="Or set a limit"
className="mt-1 w-32 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white cursor-not-allowed"
/>
</div>
</div>
{/* Calendars Connected Limit */}
<div className="flex items-start gap-3">
<input
type="checkbox"
checked={editForm.limits.max_calendars_connected === null}
disabled
className="rounded border-gray-300 dark:border-gray-600 mt-1 cursor-not-allowed"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="text-sm text-gray-700 dark:text-gray-300">Unlimited calendar connections</span>
</div>
<input
type="number"
min="1"
disabled
value={editForm.limits.max_calendars_connected || ''}
placeholder="Or set a limit"
className="mt-1 w-32 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white cursor-not-allowed"
/>
</div>
</div>
{/* API Access */}
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
type="checkbox"
checked={editForm.limits.can_connect_to_api}
disabled
className="rounded border-gray-300 dark:border-gray-600 cursor-not-allowed"
/>
Can connect to external APIs
</label>
{/* Repeated Events */}
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
type="checkbox"
checked={editForm.limits.can_book_repeated_events}
disabled
className="rounded border-gray-300 dark:border-gray-600 cursor-not-allowed"
/>
Can book repeated/recurring events
</label>
{/* 2FA */}
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
type="checkbox"
checked={editForm.limits.can_require_2fa}
disabled
className="rounded border-gray-300 dark:border-gray-600 cursor-not-allowed"
/>
Can require 2FA for users
</label>
{/* Download Logs */}
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
type="checkbox"
checked={editForm.limits.can_download_logs}
disabled
className="rounded border-gray-300 dark:border-gray-600 cursor-not-allowed"
/>
Can download system logs
</label>
{/* Delete Data */}
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
type="checkbox"
checked={editForm.limits.can_delete_data}
disabled
className="rounded border-gray-300 dark:border-gray-600 cursor-not-allowed"
/>
Can permanently delete data
</label>
{/* Masked Phone Numbers */}
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
type="checkbox"
checked={editForm.limits.can_use_masked_phone_numbers}
disabled
className="rounded border-gray-300 dark:border-gray-600 cursor-not-allowed"
/>
Can use masked phone numbers for privacy
</label>
{/* POS Integration */}
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
type="checkbox"
checked={editForm.limits.can_use_pos}
disabled
className="rounded border-gray-300 dark:border-gray-600 cursor-not-allowed"
/>
Can use Point of Sale (POS) system
</label>
{/* Mobile App */}
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
type="checkbox"
checked={editForm.limits.can_use_mobile_app}
disabled
className="rounded border-gray-300 dark:border-gray-600 cursor-not-allowed"
/>
Can use mobile app
</label>
</div>
</div>
</div>
{/* Modal Footer */}