From 902582f4bad29afabaa61cfc8722cd66bcbc91be Mon Sep 17 00:00:00 2001 From: poduck Date: Wed, 3 Dec 2025 20:45:29 -0500 Subject: [PATCH] feat(platform): Redesign tenant invite modal with tier-based permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../platform/components/TenantInviteModal.tsx | 781 +++++++++++------- 1 file changed, 471 insertions(+), 310 deletions(-) diff --git a/frontend/src/pages/platform/components/TenantInviteModal.tsx b/frontend/src/pages/platform/components/TenantInviteModal.tsx index 0955b22..caa88dd 100644 --- a/frontend/src/pages/platform/components/TenantInviteModal.tsx +++ b/frontend/src/pages/platform/components/TenantInviteModal.tsx @@ -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 = { + 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 = ({ 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: { - 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, // 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_use_masked_phone_numbers: false, - can_use_pos: false, - can_use_mobile_app: false, - }, + // Limits + max_users: 15, + max_resources: 50, + // Permissions + 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, personal_message: '', }); const [inviteError, setInviteError] = useState(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 = { + '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 = ({ 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 = ({ isOpen, onClose } +

+ Tier defaults are loaded from platform subscription settings +

- {/* Custom Limits */} -
- - {inviteForm.use_custom_limits && ( -
+ {/* Override Tier Limits Toggle */} +
+ + + {/* Sliding Custom Limits Panel */} +
+
+ {/* Limits Configuration */}
- - setInviteForm({ ...inviteForm, custom_max_users: e.target.value ? parseInt(e.target.value) : null })} - 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" - /> -
-
- - setInviteForm({ ...inviteForm, custom_max_resources: e.target.value ? parseInt(e.target.value) : null })} - 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" - /> -
-
- )} -
- - {/* Platform Permissions */} -
- -
- - - - - -
-
- - {/* Feature Limits & Capabilities */} -
-
- -
-
- {/* Video Conferencing */} - - - {/* Event Types Limit */} -
- setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, max_event_types: e.target.checked ? null : 10 } })} - className="rounded border-gray-300 dark:border-gray-600 mt-1" - /> -
-
- Unlimited event types +

+ Limits Configuration +

+

+ Use -1 for unlimited +

+
+
+ + 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" + /> +
+
+ + 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" + /> +
- 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" - />
-
- {/* Calendars Connected Limit */} -
- setInviteForm({ ...inviteForm, limits: { ...inviteForm.limits, max_calendars_connected: e.target.checked ? null : 5 } })} - className="rounded border-gray-300 dark:border-gray-600 mt-1" - /> -
-
- Unlimited calendar connections + {/* Payments & Revenue */} +
+

+ Payments & Revenue +

+
+ +
+
+ + {/* Communication */} +
+

+ Communication +

+
+ + +
+
+ + {/* Customization */} +
+

+ Customization +

+
+ + +
+
+ + {/* Plugins & Automation */} +
+

+ Plugins & Automation +

+
+ + + +
+
+ + {/* Advanced Features */} +
+

+ Advanced Features +

+
+ + + + + +
+
+ + {/* Enterprise */} +
+

+ Enterprise +

+
+ +
- 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" - />
- - {/* API Access */} - - - {/* Repeated Events */} - - - {/* 2FA */} - - - {/* Download Logs */} - - - {/* Delete Data */} - - - {/* Masked Phone Numbers */} - - - {/* POS Integration */} - - - {/* Mobile App */} -