diff --git a/frontend/src/pages/TenantOnboardPage.tsx b/frontend/src/pages/TenantOnboardPage.tsx index c36adbd..bd42399 100644 --- a/frontend/src/pages/TenantOnboardPage.tsx +++ b/frontend/src/pages/TenantOnboardPage.tsx @@ -1,9 +1,17 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { useSearchParams, useNavigate } from 'react-router-dom'; -import { CheckCircle, Mail, Lock, User, Building2, CreditCard, ArrowRight, ArrowLeft, Loader } from 'lucide-react'; +import { CheckCircle, Mail, Lock, User, Building2, CreditCard, ArrowRight, ArrowLeft, Loader, Database, Settings, Sparkles } from 'lucide-react'; import { useInvitationByToken, useAcceptInvitation } from '../hooks/usePlatform'; import { getBaseDomain, buildSubdomainUrl } from '../utils/domain'; +// Loading step configuration for the animated progress +const LOADING_STEPS = [ + { id: 'account', label: 'Creating your account', icon: User, duration: 800 }, + { id: 'database', label: 'Setting up your database', icon: Database, duration: 3000 }, + { id: 'config', label: 'Configuring your workspace', icon: Settings, duration: 1500 }, + { id: 'features', label: 'Activating features', icon: Sparkles, duration: 1000 }, +]; + const TenantOnboardPage: React.FC = () => { const [searchParams] = useSearchParams(); const navigate = useNavigate(); @@ -13,6 +21,10 @@ const TenantOnboardPage: React.FC = () => { const acceptInvitationMutation = useAcceptInvitation(); const [currentStep, setCurrentStep] = useState(1); + const [isCreating, setIsCreating] = useState(false); + const [creationComplete, setCreationComplete] = useState(false); + const [loadingStepIndex, setLoadingStepIndex] = useState(0); + const loadingIntervalRef = useRef(null); const [formData, setFormData] = useState({ // Step 1: Account email: '', @@ -113,10 +125,37 @@ const TenantOnboardPage: React.FC = () => { setCurrentStep(prev => prev - 1); }; + // Cleanup loading interval on unmount + useEffect(() => { + return () => { + if (loadingIntervalRef.current) { + clearTimeout(loadingIntervalRef.current); + } + }; + }, []); + + // Animate through loading steps + const startLoadingAnimation = () => { + setLoadingStepIndex(0); + let stepIndex = 0; + + const advanceStep = () => { + stepIndex++; + if (stepIndex < LOADING_STEPS.length) { + setLoadingStepIndex(stepIndex); + loadingIntervalRef.current = setTimeout(advanceStep, LOADING_STEPS[stepIndex].duration); + } + }; + + loadingIntervalRef.current = setTimeout(advanceStep, LOADING_STEPS[0].duration); + }; + const handleSubmit = () => { if (!token) return; setAcceptError(null); + setIsCreating(true); + startLoadingAnimation(); const data = { email: formData.email, @@ -133,9 +172,23 @@ const TenantOnboardPage: React.FC = () => { { token, data }, { onSuccess: () => { - setCurrentStep(4); // Go to completion step + // Clear the loading animation + if (loadingIntervalRef.current) { + clearTimeout(loadingIntervalRef.current); + } + // Show all steps as complete, then show success + setLoadingStepIndex(LOADING_STEPS.length); + setTimeout(() => { + setIsCreating(false); + setCreationComplete(true); + }, 500); }, onError: (error: any) => { + // Clear the loading animation + if (loadingIntervalRef.current) { + clearTimeout(loadingIntervalRef.current); + } + setIsCreating(false); setAcceptError( error.response?.data?.detail || error.response?.data?.subdomain?.[0] || @@ -238,7 +291,7 @@ const TenantOnboardPage: React.FC = () => { )} {/* Step 1: Account Setup */} - {currentStep === 1 && ( + {currentStep === 1 && !isCreating && !creationComplete && (

@@ -340,7 +393,7 @@ const TenantOnboardPage: React.FC = () => { )} {/* Step 2: Business Details */} - {currentStep === 2 && ( + {currentStep === 2 && !isCreating && !creationComplete && (

@@ -426,7 +479,7 @@ const TenantOnboardPage: React.FC = () => { )} {/* Step 3: Payment Setup (conditional) */} - {currentStep === 3 && invitation.permissions?.can_accept_payments && ( + {currentStep === 3 && invitation.permissions?.can_accept_payments && !isCreating && !creationComplete && (

@@ -452,8 +505,85 @@ const TenantOnboardPage: React.FC = () => {

)} - {/* Step 4: Complete */} - {currentStep === totalSteps && ( + {/* Creating Business - Animated Loading */} + {isCreating && ( +
+
+

+ Creating Your Business +

+

+ Please wait while we set everything up for you... +

+
+ +
+ {LOADING_STEPS.map((step, index) => { + const StepIcon = step.icon; + const isComplete = index < loadingStepIndex; + const isCurrent = index === loadingStepIndex; + + return ( +
+
+ {isComplete ? ( + + ) : isCurrent ? ( + + ) : ( + + )} +
+
+

+ {step.label} + {isComplete && ' ✓'} +

+
+
+ ); + })} +
+ + {/* Progress bar */} +
+
+
+
+ )} + + {/* Complete */} + {creationComplete && (
@@ -489,7 +619,7 @@ const TenantOnboardPage: React.FC = () => { )} {/* Navigation Buttons */} - {currentStep < totalSteps && ( + {currentStep < totalSteps && !isCreating && !creationComplete && (
)} diff --git a/smoothschedule/platform_admin/views.py b/smoothschedule/platform_admin/views.py index bfbea22..d1051a0 100644 --- a/smoothschedule/platform_admin/views.py +++ b/smoothschedule/platform_admin/views.py @@ -1059,6 +1059,7 @@ class TenantInvitationViewSet(viewsets.ModelViewSet): ) # Create Owner User + # Email is verified since they received the invitation email owner_user = User.objects.create_user( username=serializer.validated_data['email'], email=serializer.validated_data['email'], @@ -1067,6 +1068,7 @@ class TenantInvitationViewSet(viewsets.ModelViewSet): last_name=serializer.validated_data['last_name'], role=User.Role.TENANT_OWNER, tenant=tenant, + email_verified=True, ) # Mark invitation as accepted