cre-sync/components/OnboardingFlow.tsx
BusyBee3333 4e6467ffb0 Add CRESync CRM application with Setup page
- Build complete Next.js CRM for commercial real estate
- Add authentication with JWT sessions and role-based access
- Add GoHighLevel API integration for contacts, conversations, opportunities
- Add AI-powered Control Center with tool calling
- Add Setup page with onboarding checklist (/setup)
- Add sidebar navigation with Setup menu item
- Fix type errors in onboarding API, GHL services, and control center tools
- Add Prisma schema with SQLite for local development
- Add UI components with clay morphism design system

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 17:30:55 -05:00

507 lines
17 KiB
TypeScript

import React, { useState } from 'react';
import {
OnboardingDataLegacy,
GoalPrimary,
LeadType,
Channel,
ExperienceLevel,
GCIRange,
CRMPainPoint,
ExternalSystem
} from '../types';
import { Button } from './Button';
import { ClayCard } from './ClayCard';
import { ArrowRight, Check } from 'lucide-react';
// Use legacy interface which has snake_case properties
type OnboardingData = OnboardingDataLegacy;
interface Props {
onComplete: (data: OnboardingData) => void;
}
const INITIAL_DATA: OnboardingData = {
// Experience questions
years_in_business: null,
gci_last_12_months: null,
// CRM questions
using_other_crm: null,
current_crm_name: '',
crm_pain_points: [],
// Goals
goals_selected: [],
// Lead questions
has_lead_source: null,
lead_source_name: '',
wants_more_leads: null,
lead_type_desired: [],
leads_per_month_target: '',
// Channels
channels_selected: [],
// Systems to connect
systems_to_connect: [],
// Custom values for personalization
user_first_name: '',
user_last_name: '',
user_email: '',
brokerage_name: '',
};
const TOTAL_STEPS = 7;
export const OnboardingFlow: React.FC<Props> = ({ onComplete }) => {
const [step, setStep] = useState(1);
const [data, setData] = useState<OnboardingData>(INITIAL_DATA);
const updateData = (key: keyof OnboardingData, value: any) => {
setData(prev => ({ ...prev, [key]: value }));
};
const toggleArrayItem = <T,>(key: keyof OnboardingData, item: T) => {
setData(prev => {
const arr = prev[key] as T[];
if (arr.includes(item)) {
return { ...prev, [key]: arr.filter(i => i !== item) };
}
return { ...prev, [key]: [...arr, item] };
});
};
const nextStep = () => setStep(prev => prev + 1);
// Step 1: Experience Questions
const renderStep1 = () => (
<div className="space-y-6 animate-fadeIn">
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-gray-800">Tell us about your experience</h2>
<p className="text-gray-500 mt-2">This helps us personalize your setup</p>
</div>
<div className="space-y-6">
<div className="space-y-3">
<label className="block text-gray-700 font-medium">How long have you been in the business?</label>
<div className="grid gap-3">
{Object.values(ExperienceLevel).map((level) => (
<ClayCard
key={level}
selected={data.years_in_business === level}
onClick={() => updateData('years_in_business', level)}
className="flex items-center justify-between"
>
<span className="font-medium text-gray-700">{level}</span>
{data.years_in_business === level && <Check className="text-indigo-600 w-6 h-6" />}
</ClayCard>
))}
</div>
</div>
<div className="space-y-3">
<label className="block text-gray-700 font-medium">How much GCI have you done in the last 12 months?</label>
<div className="grid gap-3">
{Object.values(GCIRange).map((range) => (
<ClayCard
key={range}
selected={data.gci_last_12_months === range}
onClick={() => updateData('gci_last_12_months', range)}
className="flex items-center justify-between"
>
<span className="font-medium text-gray-700">{range}</span>
{data.gci_last_12_months === range && <Check className="text-indigo-600 w-6 h-6" />}
</ClayCard>
))}
</div>
</div>
</div>
<Button
fullWidth
disabled={!data.years_in_business || !data.gci_last_12_months}
onClick={nextStep}
className="mt-8"
>
Continue <ArrowRight size={20} />
</Button>
</div>
);
// Step 2: CRM Questions
const renderStep2 = () => (
<div className="space-y-6 animate-fadeIn">
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-gray-800">Your Current CRM</h2>
<p className="text-gray-500 mt-2">Tell us about your current setup</p>
</div>
<div className="space-y-4">
<label className="block text-gray-700 font-medium">Are you currently using a CRM?</label>
<div className="flex gap-4">
<ClayCard
className="flex-1 text-center"
selected={data.using_other_crm === true}
onClick={() => updateData('using_other_crm', true)}
>
Yes
</ClayCard>
<ClayCard
className="flex-1 text-center"
selected={data.using_other_crm === false}
onClick={() => updateData('using_other_crm', false)}
>
No
</ClayCard>
</div>
</div>
{data.using_other_crm === true && (
<>
<div className="space-y-2">
<label className="block text-gray-700 font-medium">Which CRM are you using?</label>
<input
type="text"
value={data.current_crm_name}
onChange={(e) => updateData('current_crm_name', e.target.value)}
placeholder="Enter your CRM name..."
className="w-full p-4 rounded-xl bg-gray-50 border-none shadow-inner focus:ring-2 focus:ring-indigo-500 outline-none"
/>
<div className="flex gap-2 flex-wrap mt-2">
{['Follow Up Boss', 'KvCore', 'Salesforce', 'HubSpot', 'Other'].map(crm => (
<button
key={crm}
onClick={() => updateData('current_crm_name', crm)}
className="text-xs px-3 py-1 bg-gray-200 rounded-full text-gray-600 hover:bg-gray-300"
>
{crm}
</button>
))}
</div>
</div>
<div className="space-y-2">
<label className="block text-gray-700 font-medium">What are you looking for that it doesn't do?</label>
<p className="text-sm text-gray-500">Select all that apply</p>
<div className="grid grid-cols-2 gap-3">
{Object.values(CRMPainPoint).map((painPoint) => (
<ClayCard
key={painPoint}
selected={data.crm_pain_points.includes(painPoint)}
onClick={() => toggleArrayItem('crm_pain_points', painPoint)}
className="py-3 px-4 text-center text-sm"
>
{painPoint}
{data.crm_pain_points.includes(painPoint) && (
<Check className="inline-block ml-1 text-indigo-600 w-4 h-4" />
)}
</ClayCard>
))}
</div>
</div>
</>
)}
<Button
fullWidth
disabled={data.using_other_crm === null || (data.using_other_crm && !data.current_crm_name)}
onClick={nextStep}
>
Continue <ArrowRight size={20} />
</Button>
</div>
);
// Step 3: Goals
const renderStep3 = () => (
<div className="space-y-6 animate-fadeIn">
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-gray-800">What are you looking to accomplish?</h2>
<p className="text-gray-500 mt-2">Select all that apply</p>
</div>
<div className="grid gap-4">
{Object.values(GoalPrimary).map((goal) => (
<ClayCard
key={goal}
selected={data.goals_selected.includes(goal)}
onClick={() => toggleArrayItem('goals_selected', goal)}
className="flex items-center justify-between"
>
<span className="font-medium text-gray-700">{goal}</span>
{data.goals_selected.includes(goal) && <Check className="text-indigo-600 w-6 h-6" />}
</ClayCard>
))}
</div>
<Button
fullWidth
disabled={data.goals_selected.length === 0}
onClick={nextStep}
className="mt-8"
>
Continue <ArrowRight size={20} />
</Button>
</div>
);
// Step 4: Lead Source
const renderStep4 = () => (
<div className="space-y-6 animate-fadeIn">
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-gray-800">Lead Sources</h2>
<p className="text-gray-500 mt-2">Tell us about your lead generation</p>
</div>
<div className="space-y-4">
<label className="block text-gray-700 font-medium">Do you currently have a lead source?</label>
<div className="flex gap-4">
<ClayCard
className="flex-1 text-center"
selected={data.has_lead_source === true}
onClick={() => updateData('has_lead_source', true)}
>
Yes
</ClayCard>
<ClayCard
className="flex-1 text-center"
selected={data.has_lead_source === false}
onClick={() => updateData('has_lead_source', false)}
>
No
</ClayCard>
</div>
</div>
{data.has_lead_source === true && (
<div className="space-y-2">
<label className="block text-gray-700 font-medium">Where are your leads coming from?</label>
<input
type="text"
value={data.lead_source_name}
onChange={(e) => updateData('lead_source_name', e.target.value)}
placeholder="e.g. Zillow, Referrals, Meta..."
className="w-full p-4 rounded-xl bg-gray-50 border-none shadow-inner focus:ring-2 focus:ring-indigo-500 outline-none"
/>
</div>
)}
<div className="space-y-4">
<label className="block text-gray-700 font-medium">Do you want more leads?</label>
<div className="flex gap-4">
<ClayCard
className="flex-1 text-center"
selected={data.wants_more_leads === true}
onClick={() => updateData('wants_more_leads', true)}
>
Yes
</ClayCard>
<ClayCard
className="flex-1 text-center"
selected={data.wants_more_leads === false}
onClick={() => updateData('wants_more_leads', false)}
>
No
</ClayCard>
</div>
</div>
{data.wants_more_leads === true && (
<div className="space-y-2">
<label className="block text-gray-700 font-medium">What type of leads?</label>
<p className="text-sm text-gray-500">Select all that apply</p>
<div className="grid grid-cols-3 gap-3">
{[LeadType.SELLER, LeadType.BUYER, LeadType.COMMERCIAL].map((type) => (
<ClayCard
key={type}
selected={data.lead_type_desired.includes(type)}
onClick={() => toggleArrayItem('lead_type_desired', type)}
className="py-3 px-4 text-center text-sm"
>
{type}
{data.lead_type_desired.includes(type) && (
<Check className="inline-block ml-1 text-indigo-600 w-4 h-4" />
)}
</ClayCard>
))}
</div>
</div>
)}
<Button
fullWidth
disabled={
data.has_lead_source === null ||
data.wants_more_leads === null ||
(data.has_lead_source && !data.lead_source_name) ||
(data.wants_more_leads && data.lead_type_desired.length === 0)
}
onClick={nextStep}
>
Continue <ArrowRight size={20} />
</Button>
</div>
);
// Step 5: Systems to Connect
const renderStep5 = () => (
<div className="space-y-6 animate-fadeIn">
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-gray-800">Systems to Connect</h2>
<p className="text-gray-500 mt-2">What other systems would you like to connect?</p>
</div>
<div className="grid gap-4">
{Object.values(ExternalSystem).map((system) => (
<ClayCard
key={system}
selected={data.systems_to_connect.includes(system)}
onClick={() => toggleArrayItem('systems_to_connect', system)}
className="flex items-center justify-between"
>
<span className="font-medium text-gray-700">{system}</span>
{data.systems_to_connect.includes(system) && <Check className="text-indigo-600 w-6 h-6" />}
</ClayCard>
))}
</div>
<Button
fullWidth
onClick={nextStep}
className="mt-8"
>
Continue <ArrowRight size={20} />
</Button>
</div>
);
// Step 6: Contact Channels
const renderStep6 = () => (
<div className="space-y-6 animate-fadeIn">
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-gray-800">Contact Channels</h2>
<p className="text-gray-500 mt-2">How do you want to reach leads?</p>
</div>
<div className="grid gap-4">
{Object.values(Channel).map((channel) => (
<ClayCard
key={channel}
selected={data.channels_selected.includes(channel)}
onClick={() => toggleArrayItem('channels_selected', channel)}
className="flex items-center justify-between"
>
<span className="font-medium text-gray-700">{channel}</span>
{data.channels_selected.includes(channel) && <Check className="text-indigo-600 w-6 h-6" />}
</ClayCard>
))}
</div>
<Button
fullWidth
disabled={data.channels_selected.length === 0}
onClick={nextStep}
className="mt-8"
>
Continue <ArrowRight size={20} />
</Button>
</div>
);
// Step 7: Custom Values (Profile Info)
const renderStep7 = () => {
const isStep7Valid =
data.user_first_name.trim() !== '' &&
data.user_last_name.trim() !== '' &&
data.user_email.trim() !== '' &&
data.brokerage_name.trim() !== '';
return (
<div className="space-y-6 animate-fadeIn">
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-gray-800">Let's personalize your experience</h2>
<p className="text-gray-500 mt-2">Tell us a bit about yourself</p>
</div>
<div className="space-y-4">
<div className="space-y-2">
<label className="block text-gray-700 font-medium">First Name *</label>
<input
type="text"
value={data.user_first_name}
onChange={(e) => updateData('user_first_name', e.target.value)}
placeholder="Enter your first name"
className="w-full p-4 rounded-xl bg-gray-50 border-none shadow-inner focus:ring-2 focus:ring-indigo-500 outline-none"
/>
</div>
<div className="space-y-2">
<label className="block text-gray-700 font-medium">Last Name *</label>
<input
type="text"
value={data.user_last_name}
onChange={(e) => updateData('user_last_name', e.target.value)}
placeholder="Enter your last name"
className="w-full p-4 rounded-xl bg-gray-50 border-none shadow-inner focus:ring-2 focus:ring-indigo-500 outline-none"
/>
</div>
<div className="space-y-2">
<label className="block text-gray-700 font-medium">Email *</label>
<input
type="email"
value={data.user_email}
onChange={(e) => updateData('user_email', e.target.value)}
placeholder="Enter your email address"
className="w-full p-4 rounded-xl bg-gray-50 border-none shadow-inner focus:ring-2 focus:ring-indigo-500 outline-none"
/>
</div>
<div className="space-y-2">
<label className="block text-gray-700 font-medium">Brokerage Name *</label>
<input
type="text"
value={data.brokerage_name}
onChange={(e) => updateData('brokerage_name', e.target.value)}
placeholder="Enter your brokerage name"
className="w-full p-4 rounded-xl bg-gray-50 border-none shadow-inner focus:ring-2 focus:ring-indigo-500 outline-none"
/>
</div>
</div>
<Button
fullWidth
disabled={!isStep7Valid}
onClick={() => onComplete(data)}
className="mt-8"
>
Finish Setup <Check size={20} />
</Button>
</div>
);
};
return (
<div className="max-w-md mx-auto py-10 px-4">
<div className="mb-8 flex justify-center space-x-2">
{Array.from({ length: TOTAL_STEPS }, (_, i) => i + 1).map(s => (
<div
key={s}
className={`h-2 rounded-full transition-all duration-500 ${s <= step ? 'w-8 bg-indigo-600' : 'w-2 bg-gray-300'}`}
/>
))}
</div>
{step === 1 && renderStep1()}
{step === 2 && renderStep2()}
{step === 3 && renderStep3()}
{step === 4 && renderStep4()}
{step === 5 && renderStep5()}
{step === 6 && renderStep6()}
{step === 7 && renderStep7()}
</div>
);
};