feat(platform): Redesign tenant invite modal with tier-based permissions
- Simplified UI with Email, Business Name, and Subscription Tier fields - Added collapsible "Override Tier Limits" section with sliding animation - Permission options match platform settings structure (Payments, Communication, Customization, Plugins, Advanced, Enterprise) - Permissions are loaded from subscription plans or fallback to static defaults - Custom limits/permissions only sent to backend when override is checked 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,76 +1,213 @@
|
||||
import React, { useState } from 'react';
|
||||
import { X, Send, Mail, Building2 } from 'lucide-react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Send, Mail, Building2, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { useCreateTenantInvitation } from '../../../hooks/usePlatform';
|
||||
import { useSubscriptionPlans } from '../../../hooks/usePlatformSettings';
|
||||
|
||||
interface TenantInviteModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// 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;
|
||||
can_add_video_conferencing: boolean;
|
||||
can_use_sms_reminders: boolean;
|
||||
can_use_masked_phone_numbers: boolean;
|
||||
can_use_plugins: boolean;
|
||||
can_use_tasks: boolean;
|
||||
can_create_plugins: boolean;
|
||||
can_use_webhooks: boolean;
|
||||
can_use_calendar_sync: boolean;
|
||||
can_export_data: boolean;
|
||||
can_require_2fa: 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,
|
||||
can_add_video_conferencing: false,
|
||||
can_use_sms_reminders: false,
|
||||
can_use_masked_phone_numbers: false,
|
||||
can_use_plugins: true,
|
||||
can_use_tasks: true,
|
||||
can_create_plugins: false,
|
||||
can_use_webhooks: false,
|
||||
can_use_calendar_sync: false,
|
||||
can_export_data: false,
|
||||
can_require_2fa: 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,
|
||||
can_add_video_conferencing: false,
|
||||
can_use_sms_reminders: false,
|
||||
can_use_masked_phone_numbers: false,
|
||||
can_use_plugins: true,
|
||||
can_use_tasks: true,
|
||||
can_create_plugins: false,
|
||||
can_use_webhooks: false,
|
||||
can_use_calendar_sync: false,
|
||||
can_export_data: false,
|
||||
can_require_2fa: 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,
|
||||
can_add_video_conferencing: true,
|
||||
can_use_sms_reminders: true,
|
||||
can_use_masked_phone_numbers: false,
|
||||
can_use_plugins: true,
|
||||
can_use_tasks: true,
|
||||
can_create_plugins: false,
|
||||
can_use_webhooks: true,
|
||||
can_use_calendar_sync: true,
|
||||
can_export_data: true,
|
||||
can_require_2fa: false,
|
||||
},
|
||||
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,
|
||||
can_add_video_conferencing: true,
|
||||
can_use_sms_reminders: true,
|
||||
can_use_masked_phone_numbers: true,
|
||||
can_use_plugins: true,
|
||||
can_use_tasks: true,
|
||||
can_create_plugins: true,
|
||||
can_use_webhooks: true,
|
||||
can_use_calendar_sync: true,
|
||||
can_export_data: true,
|
||||
can_require_2fa: true,
|
||||
},
|
||||
};
|
||||
|
||||
const TenantInviteModal: React.FC<TenantInviteModalProps> = ({ isOpen, onClose }) => {
|
||||
const createInvitationMutation = useCreateTenantInvitation();
|
||||
const { data: subscriptionPlans } = useSubscriptionPlans();
|
||||
|
||||
const [inviteForm, setInviteForm] = useState({
|
||||
email: '',
|
||||
suggested_business_name: '',
|
||||
subscription_tier: 'PROFESSIONAL' as 'FREE' | 'STARTER' | 'PROFESSIONAL' | 'ENTERPRISE',
|
||||
custom_max_users: null as number | null,
|
||||
custom_max_resources: null as number | null,
|
||||
use_custom_limits: false,
|
||||
permissions: {
|
||||
// Limits
|
||||
max_users: 15,
|
||||
max_resources: 50,
|
||||
// Permissions
|
||||
can_manage_oauth_credentials: false,
|
||||
can_accept_payments: false,
|
||||
can_use_custom_domain: false,
|
||||
can_accept_payments: true,
|
||||
can_use_custom_domain: true,
|
||||
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, // null = unlimited
|
||||
max_calendars_connected: null as number | null, // null = unlimited
|
||||
can_connect_to_api: false,
|
||||
can_book_repeated_events: false,
|
||||
can_require_2fa: false,
|
||||
can_download_logs: false,
|
||||
can_delete_data: false,
|
||||
can_api_access: true,
|
||||
can_add_video_conferencing: true,
|
||||
can_use_sms_reminders: true,
|
||||
can_use_masked_phone_numbers: false,
|
||||
can_use_pos: false,
|
||||
can_use_mobile_app: false,
|
||||
},
|
||||
can_use_plugins: true,
|
||||
can_use_tasks: true,
|
||||
can_create_plugins: false,
|
||||
can_use_webhooks: true,
|
||||
can_use_calendar_sync: true,
|
||||
can_export_data: true,
|
||||
can_require_2fa: false,
|
||||
personal_message: '',
|
||||
});
|
||||
const [inviteError, setInviteError] = useState<string | null>(null);
|
||||
const [inviteSuccess, setInviteSuccess] = useState(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 {
|
||||
max_users: plan.limits?.max_users ?? staticDefaults.max_users,
|
||||
max_resources: plan.limits?.max_resources ?? staticDefaults.max_resources,
|
||||
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,
|
||||
can_add_video_conferencing: plan.permissions?.video_conferencing ?? staticDefaults.can_add_video_conferencing,
|
||||
can_use_sms_reminders: plan.permissions?.sms_reminders ?? staticDefaults.can_use_sms_reminders,
|
||||
can_use_masked_phone_numbers: plan.permissions?.masked_calling ?? staticDefaults.can_use_masked_phone_numbers,
|
||||
can_use_plugins: plan.permissions?.plugins ?? staticDefaults.can_use_plugins,
|
||||
can_use_tasks: plan.permissions?.tasks ?? staticDefaults.can_use_tasks,
|
||||
can_create_plugins: plan.permissions?.can_create_plugins ?? staticDefaults.can_create_plugins,
|
||||
can_use_webhooks: plan.permissions?.webhooks ?? staticDefaults.can_use_webhooks,
|
||||
can_use_calendar_sync: plan.permissions?.calendar_sync ?? staticDefaults.can_use_calendar_sync,
|
||||
can_export_data: plan.permissions?.export_data ?? staticDefaults.can_export_data,
|
||||
can_require_2fa: plan.permissions?.two_factor_auth ?? staticDefaults.can_require_2fa,
|
||||
};
|
||||
}
|
||||
}
|
||||
// Fallback to static defaults
|
||||
return TIER_DEFAULTS[tier] || TIER_DEFAULTS.FREE;
|
||||
};
|
||||
|
||||
// Handle subscription tier change - auto-update limits and permissions
|
||||
const handleTierChange = (newTier: string) => {
|
||||
const defaults = getTierDefaults(newTier);
|
||||
setInviteForm(prev => ({
|
||||
...prev,
|
||||
subscription_tier: newTier as any,
|
||||
...defaults,
|
||||
}));
|
||||
};
|
||||
|
||||
// Initialize defaults when modal opens or subscription plans load
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const defaults = getTierDefaults(inviteForm.subscription_tier);
|
||||
setInviteForm(prev => ({
|
||||
...prev,
|
||||
...defaults,
|
||||
}));
|
||||
}
|
||||
}, [isOpen, subscriptionPlans]);
|
||||
|
||||
const resetForm = () => {
|
||||
const defaults = getTierDefaults('PROFESSIONAL');
|
||||
setInviteForm({
|
||||
email: '',
|
||||
suggested_business_name: '',
|
||||
subscription_tier: 'PROFESSIONAL',
|
||||
custom_max_users: null,
|
||||
custom_max_resources: null,
|
||||
use_custom_limits: false,
|
||||
permissions: {
|
||||
can_manage_oauth_credentials: false,
|
||||
can_accept_payments: false,
|
||||
can_use_custom_domain: false,
|
||||
can_white_label: false,
|
||||
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,
|
||||
},
|
||||
...defaults,
|
||||
personal_message: '',
|
||||
});
|
||||
setInviteError(null);
|
||||
@@ -106,29 +243,29 @@ const TenantInviteModal: React.FC<TenantInviteModalProps> = ({ isOpen, onClose }
|
||||
data.suggested_business_name = inviteForm.suggested_business_name.trim();
|
||||
}
|
||||
|
||||
// If using custom limits, include all the overrides
|
||||
if (inviteForm.use_custom_limits) {
|
||||
if (inviteForm.custom_max_users !== null && inviteForm.custom_max_users > 0) {
|
||||
data.custom_max_users = inviteForm.custom_max_users;
|
||||
}
|
||||
if (inviteForm.custom_max_resources !== null && inviteForm.custom_max_resources > 0) {
|
||||
data.custom_max_resources = inviteForm.custom_max_resources;
|
||||
}
|
||||
}
|
||||
|
||||
// Only include permissions if at least one is enabled
|
||||
const hasPermissions = Object.values(inviteForm.permissions).some(v => v === true);
|
||||
if (hasPermissions) {
|
||||
data.permissions = inviteForm.permissions;
|
||||
}
|
||||
|
||||
// Only include limits if at least one is enabled (boolean true or numeric value set)
|
||||
const hasLimits = Object.entries(inviteForm.limits).some(([key, value]) => {
|
||||
if (typeof value === 'boolean') return value === true;
|
||||
if (typeof value === 'number') return true; // numeric limits are meaningful even if 0
|
||||
return false;
|
||||
});
|
||||
if (hasLimits) {
|
||||
data.limits = inviteForm.limits;
|
||||
data.custom_max_users = inviteForm.max_users;
|
||||
data.custom_max_resources = inviteForm.max_resources;
|
||||
data.permissions = {
|
||||
can_manage_oauth_credentials: inviteForm.can_manage_oauth_credentials,
|
||||
can_accept_payments: inviteForm.can_accept_payments,
|
||||
can_use_custom_domain: inviteForm.can_use_custom_domain,
|
||||
can_white_label: inviteForm.can_white_label,
|
||||
can_api_access: inviteForm.can_api_access,
|
||||
};
|
||||
data.limits = {
|
||||
can_add_video_conferencing: inviteForm.can_add_video_conferencing,
|
||||
can_use_sms_reminders: inviteForm.can_use_sms_reminders,
|
||||
can_use_masked_phone_numbers: inviteForm.can_use_masked_phone_numbers,
|
||||
can_use_plugins: inviteForm.can_use_plugins,
|
||||
can_use_tasks: inviteForm.can_use_tasks,
|
||||
can_create_plugins: inviteForm.can_create_plugins,
|
||||
can_use_webhooks: inviteForm.can_use_webhooks,
|
||||
can_use_calendar_sync: inviteForm.can_use_calendar_sync,
|
||||
can_export_data: inviteForm.can_export_data,
|
||||
can_require_2fa: inviteForm.can_require_2fa,
|
||||
};
|
||||
}
|
||||
|
||||
if (inviteForm.personal_message.trim()) {
|
||||
@@ -227,7 +364,7 @@ const TenantInviteModal: React.FC<TenantInviteModalProps> = ({ isOpen, onClose }
|
||||
</label>
|
||||
<select
|
||||
value={inviteForm.subscription_tier}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, subscription_tier: e.target.value as any })}
|
||||
onChange={(e) => handleTierChange(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
>
|
||||
<option value="FREE">Free Trial</option>
|
||||
@@ -235,257 +372,281 @@ const TenantInviteModal: React.FC<TenantInviteModalProps> = ({ isOpen, onClose }
|
||||
<option value="PROFESSIONAL">Professional</option>
|
||||
<option value="ENTERPRISE">Enterprise</option>
|
||||
</select>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Tier defaults are loaded from platform subscription settings
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Custom Limits */}
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{/* Override Tier Limits Toggle */}
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setInviteForm({ ...inviteForm, use_custom_limits: !inviteForm.use_custom_limits })}
|
||||
className="flex items-center justify-between w-full p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.use_custom_limits}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, use_custom_limits: e.target.checked })}
|
||||
className="rounded border-gray-300 dark:border-gray-600"
|
||||
className="rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
Override tier limits with custom values
|
||||
</label>
|
||||
{inviteForm.use_custom_limits && (
|
||||
<div className="grid grid-cols-2 gap-4 mt-2">
|
||||
<div className="text-left">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Override Tier Limits
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Customize limits and permissions for this tenant
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{inviteForm.use_custom_limits ? (
|
||||
<ChevronUp size={20} className="text-gray-400" />
|
||||
) : (
|
||||
<ChevronDown size={20} className="text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Sliding Custom Limits Panel */}
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ease-in-out ${
|
||||
inviteForm.use_custom_limits ? 'max-h-[2000px] opacity-100 mt-4' : 'max-h-0 opacity-0'
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-4 p-4 bg-gray-50 dark:bg-gray-700/30 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
{/* Limits Configuration */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">
|
||||
Limits Configuration
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
Use -1 for unlimited
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 dark:text-gray-400 mb-1">Max Users</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={inviteForm.custom_max_users || ''}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, custom_max_users: e.target.value ? parseInt(e.target.value) : null })}
|
||||
min="-1"
|
||||
value={inviteForm.max_users}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, 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"
|
||||
placeholder="Leave empty for tier default"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 dark:text-gray-400 mb-1">Max Resources</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={inviteForm.custom_max_resources || ''}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, custom_max_resources: e.target.value ? parseInt(e.target.value) : null })}
|
||||
min="-1"
|
||||
value={inviteForm.max_resources}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, 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"
|
||||
placeholder="Leave empty for tier default"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Platform Permissions */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
Platform Permissions
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
{/* Payments & Revenue */}
|
||||
<div className="pt-3 border-t border-gray-200 dark:border-gray-600">
|
||||
<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-2 gap-2">
|
||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-white dark:hover:bg-gray-700 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.permissions.can_manage_oauth_credentials}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, permissions: { ...inviteForm.permissions, can_manage_oauth_credentials: e.target.checked } })}
|
||||
className="rounded border-gray-300 dark:border-gray-600"
|
||||
checked={inviteForm.can_accept_payments}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, can_accept_payments: e.target.checked })}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
Can manage OAuth credentials
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.permissions.can_accept_payments}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, permissions: { ...inviteForm.permissions, can_accept_payments: e.target.checked } })}
|
||||
className="rounded border-gray-300 dark:border-gray-600"
|
||||
/>
|
||||
Can accept online payments (Stripe Connect)
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.permissions.can_use_custom_domain}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, permissions: { ...inviteForm.permissions, can_use_custom_domain: e.target.checked } })}
|
||||
className="rounded border-gray-300 dark:border-gray-600"
|
||||
/>
|
||||
Can use custom domain
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.permissions.can_white_label}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, permissions: { ...inviteForm.permissions, can_white_label: e.target.checked } })}
|
||||
className="rounded border-gray-300 dark:border-gray-600"
|
||||
/>
|
||||
Can remove SmoothSchedule branding
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.permissions.can_api_access}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, permissions: { ...inviteForm.permissions, can_api_access: e.target.checked } })}
|
||||
className="rounded border-gray-300 dark:border-gray-600"
|
||||
/>
|
||||
Can access API for integrations
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Online Payments</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature Limits & Capabilities */}
|
||||
<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>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{/* Video Conferencing */}
|
||||
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
{/* Communication */}
|
||||
<div className="pt-3 border-t border-gray-200 dark:border-gray-600">
|
||||
<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-2 gap-2">
|
||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-white dark:hover:bg-gray-700 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.limits.can_add_video_conferencing}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, can_add_video_conferencing: e.target.checked } })}
|
||||
className="rounded border-gray-300 dark:border-gray-600"
|
||||
checked={inviteForm.can_use_sms_reminders}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, can_use_sms_reminders: e.target.checked })}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
Can add video conferencing to events
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">SMS Reminders</span>
|
||||
</label>
|
||||
|
||||
{/* Event Types Limit */}
|
||||
<div className="flex items-start gap-3">
|
||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-white dark:hover:bg-gray-700 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.limits.max_event_types === null}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, max_event_types: e.target.checked ? null : 10 } })}
|
||||
className="rounded border-gray-300 dark:border-gray-600 mt-1"
|
||||
/>
|
||||
<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={inviteForm.limits.max_event_types === null}
|
||||
value={inviteForm.limits.max_event_types || ''}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, max_event_types: e.target.value ? parseInt(e.target.value) : null } })}
|
||||
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 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
checked={inviteForm.can_use_masked_phone_numbers}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, 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>
|
||||
|
||||
{/* Calendars Connected Limit */}
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Customization */}
|
||||
<div className="pt-3 border-t border-gray-200 dark:border-gray-600">
|
||||
<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-2 gap-2">
|
||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-white dark:hover:bg-gray-700 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.limits.max_calendars_connected === null}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, max_calendars_connected: e.target.checked ? null : 5 } })}
|
||||
className="rounded border-gray-300 dark:border-gray-600 mt-1"
|
||||
checked={inviteForm.can_use_custom_domain}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, can_use_custom_domain: e.target.checked })}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
<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>
|
||||
<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-600 rounded-lg hover:bg-white dark:hover:bg-gray-700 cursor-pointer">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
disabled={inviteForm.limits.max_calendars_connected === null}
|
||||
value={inviteForm.limits.max_calendars_connected || ''}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, max_calendars_connected: e.target.value ? parseInt(e.target.value) : null } })}
|
||||
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 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
type="checkbox"
|
||||
checked={inviteForm.can_white_label}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, 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>
|
||||
|
||||
{/* API Access */}
|
||||
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
{/* Plugins & Automation */}
|
||||
<div className="pt-3 border-t border-gray-200 dark:border-gray-600">
|
||||
<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-2 gap-2">
|
||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-white dark:hover:bg-gray-700 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.limits.can_connect_to_api}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, can_connect_to_api: e.target.checked } })}
|
||||
className="rounded border-gray-300 dark:border-gray-600"
|
||||
checked={inviteForm.can_use_plugins}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked;
|
||||
setInviteForm(prev => ({
|
||||
...prev,
|
||||
can_use_plugins: checked,
|
||||
...(checked ? {} : { can_use_tasks: false, can_create_plugins: false })
|
||||
}));
|
||||
}}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
Can connect to external APIs
|
||||
<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-600 rounded-lg ${inviteForm.can_use_plugins ? 'hover:bg-white dark:hover:bg-gray-700 cursor-pointer' : 'opacity-50 cursor-not-allowed'}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.can_use_tasks}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, can_use_tasks: e.target.checked })}
|
||||
disabled={!inviteForm.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-600 rounded-lg ${inviteForm.can_use_plugins ? 'hover:bg-white dark:hover:bg-gray-700 cursor-pointer' : 'opacity-50 cursor-not-allowed'}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.can_create_plugins}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, can_create_plugins: e.target.checked })}
|
||||
disabled={!inviteForm.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>
|
||||
</div>
|
||||
|
||||
{/* Repeated Events */}
|
||||
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
{/* Advanced Features */}
|
||||
<div className="pt-3 border-t border-gray-200 dark:border-gray-600">
|
||||
<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-2 gap-2">
|
||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-white dark:hover:bg-gray-700 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.limits.can_book_repeated_events}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, can_book_repeated_events: e.target.checked } })}
|
||||
className="rounded border-gray-300 dark:border-gray-600"
|
||||
checked={inviteForm.can_api_access}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, can_api_access: e.target.checked })}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
Can book repeated/recurring events
|
||||
<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-600 rounded-lg hover:bg-white dark:hover:bg-gray-700 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.can_use_webhooks}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, 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-600 rounded-lg hover:bg-white dark:hover:bg-gray-700 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.can_use_calendar_sync}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, 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-600 rounded-lg hover:bg-white dark:hover:bg-gray-700 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.can_export_data}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, 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-600 rounded-lg hover:bg-white dark:hover:bg-gray-700 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.can_add_video_conferencing}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, 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>
|
||||
|
||||
{/* 2FA */}
|
||||
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
{/* Enterprise */}
|
||||
<div className="pt-3 border-t border-gray-200 dark:border-gray-600">
|
||||
<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-2 gap-2">
|
||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-white dark:hover:bg-gray-700 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.limits.can_require_2fa}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, can_require_2fa: e.target.checked } })}
|
||||
className="rounded border-gray-300 dark:border-gray-600"
|
||||
checked={inviteForm.can_manage_oauth_credentials}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, can_manage_oauth_credentials: e.target.checked })}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
Can require 2FA for users
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Manage OAuth</span>
|
||||
</label>
|
||||
|
||||
{/* Download Logs */}
|
||||
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-white dark:hover:bg-gray-700 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inviteForm.limits.can_download_logs}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, can_download_logs: e.target.checked } })}
|
||||
className="rounded border-gray-300 dark:border-gray-600"
|
||||
checked={inviteForm.can_require_2fa}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, can_require_2fa: e.target.checked })}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
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={inviteForm.limits.can_delete_data}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, can_delete_data: e.target.checked } })}
|
||||
className="rounded border-gray-300 dark:border-gray-600"
|
||||
/>
|
||||
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={inviteForm.limits.can_use_masked_phone_numbers}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, can_use_masked_phone_numbers: e.target.checked } })}
|
||||
className="rounded border-gray-300 dark:border-gray-600"
|
||||
/>
|
||||
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={inviteForm.limits.can_use_pos}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, can_use_pos: e.target.checked } })}
|
||||
className="rounded border-gray-300 dark:border-gray-600"
|
||||
/>
|
||||
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={inviteForm.limits.can_use_mobile_app}
|
||||
onChange={(e) => setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, can_use_mobile_app: e.target.checked } })}
|
||||
className="rounded border-gray-300 dark:border-gray-600"
|
||||
/>
|
||||
Can use mobile app
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Require 2FA</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Personal Message */}
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user