- 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>
411 lines
14 KiB
TypeScript
411 lines
14 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
import { OnboardingDataLegacy, SystemState, GoalPrimary, Channel, ExternalSystem } from '../types';
|
|
import { ClayCard } from './ClayCard';
|
|
import { Button } from './Button';
|
|
|
|
// Use legacy interface which has snake_case properties
|
|
type OnboardingData = OnboardingDataLegacy;
|
|
import {
|
|
Upload,
|
|
MessageSquare,
|
|
Mail,
|
|
Rocket,
|
|
CheckCircle,
|
|
Smartphone,
|
|
Users,
|
|
Target,
|
|
BarChart3,
|
|
Layers,
|
|
Link2,
|
|
Sparkles,
|
|
BookOpen,
|
|
ClipboardList
|
|
} from 'lucide-react';
|
|
|
|
interface DashboardProps {
|
|
onboardingData: OnboardingData;
|
|
systemState: SystemState;
|
|
onSetupClick: (setupType: string) => void;
|
|
onQuizClick: () => void;
|
|
}
|
|
|
|
interface TodoItem {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
icon: React.ReactNode;
|
|
iconBgColor: string;
|
|
iconTextColor: string;
|
|
setupType: string;
|
|
isCompleted: boolean;
|
|
}
|
|
|
|
export const Dashboard: React.FC<DashboardProps> = ({
|
|
onboardingData,
|
|
systemState,
|
|
onSetupClick,
|
|
onQuizClick
|
|
}) => {
|
|
const {
|
|
user_first_name,
|
|
goals_selected,
|
|
channels_selected,
|
|
systems_to_connect,
|
|
has_lead_source
|
|
} = onboardingData;
|
|
|
|
const { sms_configured, email_configured, contacts_imported } = systemState;
|
|
|
|
// Calculate setup progress
|
|
const progressData = useMemo(() => {
|
|
const items: { label: string; completed: boolean }[] = [];
|
|
|
|
if (channels_selected.includes(Channel.SMS)) {
|
|
items.push({ label: 'SMS', completed: sms_configured });
|
|
}
|
|
if (channels_selected.includes(Channel.EMAIL)) {
|
|
items.push({ label: 'Email', completed: email_configured });
|
|
}
|
|
if (has_lead_source) {
|
|
items.push({ label: 'Contacts', completed: contacts_imported });
|
|
}
|
|
if (systems_to_connect.length > 0) {
|
|
items.push({ label: 'Integrations', completed: false }); // Assuming integrations tracking
|
|
}
|
|
|
|
const completedCount = items.filter(i => i.completed).length;
|
|
const percentage = items.length > 0 ? Math.round((completedCount / items.length) * 100) : 0;
|
|
|
|
return { items, completedCount, percentage };
|
|
}, [channels_selected, systems_to_connect, has_lead_source, sms_configured, email_configured, contacts_imported]);
|
|
|
|
// Generate subtitle based on goals
|
|
const subtitle = useMemo(() => {
|
|
if (goals_selected.length === 0) {
|
|
return "Let's get your CRM set up!";
|
|
}
|
|
|
|
const goalDescriptions: Record<string, string> = {
|
|
[GoalPrimary.NEW_SELLER_LEADS]: 'seller leads',
|
|
[GoalPrimary.NEW_BUYER_LEADS]: 'buyer leads',
|
|
[GoalPrimary.GET_ORGANIZED]: 'organization',
|
|
[GoalPrimary.FOLLOW_UP_CAMPAIGNS]: 'follow-up campaigns',
|
|
[GoalPrimary.TRACK_METRICS]: 'tracking metrics'
|
|
};
|
|
|
|
const goalSummary = goals_selected
|
|
.slice(0, 2)
|
|
.map(g => goalDescriptions[g] || g)
|
|
.join(' and ');
|
|
|
|
return `Let's help you with ${goalSummary}${goals_selected.length > 2 ? ' and more' : ''}!`;
|
|
}, [goals_selected]);
|
|
|
|
// Build dynamic to-do items
|
|
const todoItems: TodoItem[] = useMemo(() => {
|
|
const items: TodoItem[] = [];
|
|
|
|
// Goal-based to-dos
|
|
if (goals_selected.includes(GoalPrimary.NEW_SELLER_LEADS)) {
|
|
items.push({
|
|
id: 'seller-leads-campaign',
|
|
title: 'Start campaign to get new seller leads',
|
|
description: 'Launch an automated campaign targeting potential sellers in your market.',
|
|
icon: <Target size={28} />,
|
|
iconBgColor: 'bg-emerald-100',
|
|
iconTextColor: 'text-emerald-600',
|
|
setupType: 'SELLER_CAMPAIGN',
|
|
isCompleted: false
|
|
});
|
|
}
|
|
|
|
if (goals_selected.includes(GoalPrimary.NEW_BUYER_LEADS)) {
|
|
items.push({
|
|
id: 'buyer-leads-campaign',
|
|
title: 'Start campaign to get new buyer leads',
|
|
description: 'Reach potential buyers with targeted messaging and automation.',
|
|
icon: <Users size={28} />,
|
|
iconBgColor: 'bg-blue-100',
|
|
iconTextColor: 'text-blue-600',
|
|
setupType: 'BUYER_CAMPAIGN',
|
|
isCompleted: false
|
|
});
|
|
}
|
|
|
|
if (goals_selected.includes(GoalPrimary.FOLLOW_UP_CAMPAIGNS)) {
|
|
items.push({
|
|
id: 'follow-up-campaigns',
|
|
title: 'Set up follow-up campaigns',
|
|
description: 'Create automated sequences to nurture your leads over time.',
|
|
icon: <MessageSquare size={28} />,
|
|
iconBgColor: 'bg-violet-100',
|
|
iconTextColor: 'text-violet-600',
|
|
setupType: 'FOLLOW_UP_CAMPAIGN',
|
|
isCompleted: false
|
|
});
|
|
}
|
|
|
|
if (goals_selected.includes(GoalPrimary.GET_ORGANIZED)) {
|
|
items.push({
|
|
id: 'configure-pipeline',
|
|
title: 'Configure pipeline stages',
|
|
description: 'Customize your pipeline to match your sales process.',
|
|
icon: <Layers size={28} />,
|
|
iconBgColor: 'bg-amber-100',
|
|
iconTextColor: 'text-amber-600',
|
|
setupType: 'PIPELINE_CONFIG',
|
|
isCompleted: false
|
|
});
|
|
}
|
|
|
|
if (goals_selected.includes(GoalPrimary.TRACK_METRICS)) {
|
|
items.push({
|
|
id: 'reporting-dashboard',
|
|
title: 'Set up reporting dashboard',
|
|
description: 'Track your key metrics and performance indicators.',
|
|
icon: <BarChart3 size={28} />,
|
|
iconBgColor: 'bg-rose-100',
|
|
iconTextColor: 'text-rose-600',
|
|
setupType: 'REPORTING_SETUP',
|
|
isCompleted: false
|
|
});
|
|
}
|
|
|
|
// Channel-based to-dos
|
|
if (channels_selected.includes(Channel.SMS) && !sms_configured) {
|
|
items.push({
|
|
id: 'sms-setup',
|
|
title: 'Set up SMS (A2P registration)',
|
|
description: 'Configure your phone number and complete carrier registration to start texting.',
|
|
icon: <Smartphone size={28} />,
|
|
iconBgColor: 'bg-purple-100',
|
|
iconTextColor: 'text-purple-600',
|
|
setupType: 'SMS_SETUP',
|
|
isCompleted: false
|
|
});
|
|
}
|
|
|
|
if (channels_selected.includes(Channel.EMAIL) && !email_configured) {
|
|
items.push({
|
|
id: 'email-setup',
|
|
title: 'Configure email settings',
|
|
description: 'Connect your domain and set up authentication for deliverability.',
|
|
icon: <Mail size={28} />,
|
|
iconBgColor: 'bg-orange-100',
|
|
iconTextColor: 'text-orange-600',
|
|
setupType: 'EMAIL_SETUP',
|
|
isCompleted: false
|
|
});
|
|
}
|
|
|
|
// Systems integration to-do
|
|
if (systems_to_connect.length > 0) {
|
|
const systemNames = systems_to_connect.map(s => {
|
|
const names: Record<string, string> = {
|
|
[ExternalSystem.DIALER]: 'Dialer',
|
|
[ExternalSystem.TRANSACTION_MANAGEMENT]: 'Transaction Management',
|
|
[ExternalSystem.OTHER_CRM]: 'CRM',
|
|
[ExternalSystem.MARKETING_PLATFORM]: 'Marketing Platform'
|
|
};
|
|
return names[s] || s;
|
|
});
|
|
|
|
items.push({
|
|
id: 'connect-systems',
|
|
title: 'Connect your systems',
|
|
description: `Integrate with ${systemNames.join(', ')} for seamless workflow.`,
|
|
icon: <Link2 size={28} />,
|
|
iconBgColor: 'bg-cyan-100',
|
|
iconTextColor: 'text-cyan-600',
|
|
setupType: 'INTEGRATIONS',
|
|
isCompleted: false
|
|
});
|
|
}
|
|
|
|
// Lead upload to-do
|
|
if (has_lead_source && !contacts_imported) {
|
|
items.push({
|
|
id: 'upload-leads',
|
|
title: 'Upload your lead list',
|
|
description: 'Import your existing contacts to start automating your outreach.',
|
|
icon: <Upload size={28} />,
|
|
iconBgColor: 'bg-teal-100',
|
|
iconTextColor: 'text-teal-600',
|
|
setupType: 'UPLOAD_LEADS',
|
|
isCompleted: false
|
|
});
|
|
}
|
|
|
|
return items;
|
|
}, [goals_selected, channels_selected, systems_to_connect, has_lead_source, sms_configured, email_configured, contacts_imported]);
|
|
|
|
// Completed items (for showing completion status)
|
|
const completedItems = useMemo(() => {
|
|
const items: { id: string; label: string }[] = [];
|
|
|
|
if (channels_selected.includes(Channel.SMS) && sms_configured) {
|
|
items.push({ id: 'sms-done', label: 'SMS configured' });
|
|
}
|
|
if (channels_selected.includes(Channel.EMAIL) && email_configured) {
|
|
items.push({ id: 'email-done', label: 'Email configured' });
|
|
}
|
|
if (has_lead_source && contacts_imported) {
|
|
items.push({ id: 'leads-done', label: 'Leads uploaded' });
|
|
}
|
|
|
|
return items;
|
|
}, [channels_selected, has_lead_source, sms_configured, email_configured, contacts_imported]);
|
|
|
|
return (
|
|
<div className="max-w-4xl mx-auto py-10 px-4">
|
|
{/* Welcome Header */}
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-bold text-gray-800">
|
|
Welcome, {user_first_name || 'there'}!
|
|
</h1>
|
|
<p className="text-gray-500 mt-2 text-lg">{subtitle}</p>
|
|
</div>
|
|
|
|
{/* Setup Progress Card */}
|
|
<ClayCard className="mb-8">
|
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
|
<div>
|
|
<h2 className="text-xl font-bold text-gray-800">Setup Progress</h2>
|
|
<p className="text-gray-500 text-sm mt-1">
|
|
{progressData.completedCount} of {progressData.items.length} steps completed
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-48 h-3 bg-gray-200 rounded-full overflow-hidden shadow-inner">
|
|
<div
|
|
className="h-full bg-gradient-to-r from-indigo-500 to-indigo-600 rounded-full transition-all duration-500 ease-out"
|
|
style={{ width: `${progressData.percentage}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-2xl font-bold text-indigo-600">{progressData.percentage}%</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress indicators */}
|
|
<div className="flex flex-wrap gap-3 mt-4">
|
|
{progressData.items.map((item, index) => (
|
|
<div
|
|
key={index}
|
|
className={`flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-medium ${
|
|
item.completed
|
|
? 'bg-green-100 text-green-700'
|
|
: 'bg-gray-100 text-gray-500'
|
|
}`}
|
|
>
|
|
{item.completed && <CheckCircle size={14} />}
|
|
{item.label}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ClayCard>
|
|
|
|
{/* Completed Items */}
|
|
{completedItems.length > 0 && (
|
|
<div className="space-y-3 mb-6">
|
|
{completedItems.map(item => (
|
|
<div
|
|
key={item.id}
|
|
className="p-4 rounded-2xl border-2 border-green-100 bg-green-50/50 flex items-center gap-3 text-green-700"
|
|
>
|
|
<CheckCircle size={20} />
|
|
<span className="font-medium">{item.label}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Dynamic To-Do Items */}
|
|
<div className="space-y-4 mb-10">
|
|
<h2 className="text-xl font-bold text-gray-800 flex items-center gap-2">
|
|
<ClipboardList size={24} className="text-indigo-600" />
|
|
Your To-Do List
|
|
</h2>
|
|
|
|
{todoItems.length === 0 ? (
|
|
<ClayCard className="text-center py-8">
|
|
<div className="text-6xl mb-4">🎉</div>
|
|
<h3 className="text-xl font-bold text-gray-800">All caught up!</h3>
|
|
<p className="text-gray-500 mt-2">You've completed all your setup tasks.</p>
|
|
</ClayCard>
|
|
) : (
|
|
<div className="grid gap-4">
|
|
{todoItems.map((item) => (
|
|
<ClayCard key={item.id} className="flex flex-col gap-4">
|
|
<div className="flex flex-col md:flex-row gap-4 items-start md:items-center">
|
|
<div className={`${item.iconBgColor} p-4 rounded-2xl ${item.iconTextColor} shrink-0`}>
|
|
{item.icon}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="text-lg font-bold text-gray-800">{item.title}</h3>
|
|
<p className="text-gray-500 text-sm mt-1">{item.description}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col sm:flex-row gap-3">
|
|
<Button
|
|
variant="secondary"
|
|
fullWidth
|
|
onClick={() => onSetupClick(`${item.setupType}_DIY`)}
|
|
>
|
|
DIY Setup
|
|
</Button>
|
|
<Button
|
|
fullWidth
|
|
onClick={() => onSetupClick(`${item.setupType}_DFY`)}
|
|
className="gap-2"
|
|
>
|
|
<Sparkles size={18} />
|
|
Done For You
|
|
</Button>
|
|
</div>
|
|
</ClayCard>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Quick Links Section */}
|
|
<div className="space-y-4">
|
|
<h2 className="text-xl font-bold text-gray-800">Quick Links</h2>
|
|
<div className="grid md:grid-cols-2 gap-4">
|
|
{/* Performance Quiz Card */}
|
|
<ClayCard
|
|
onClick={onQuizClick}
|
|
className="cursor-pointer hover:transform hover:-translate-y-1 transition-all duration-300"
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<div className="bg-gradient-to-br from-indigo-500 to-purple-600 p-4 rounded-2xl text-white">
|
|
<BarChart3 size={28} />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-bold text-gray-800">Take the Performance Quiz</h3>
|
|
<p className="text-gray-500 text-sm mt-1">See how you compare to your peers</p>
|
|
</div>
|
|
</div>
|
|
</ClayCard>
|
|
|
|
{/* Browse Resources Card */}
|
|
<ClayCard
|
|
onClick={() => onSetupClick('BROWSE_RESOURCES')}
|
|
className="cursor-pointer hover:transform hover:-translate-y-1 transition-all duration-300"
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<div className="bg-gradient-to-br from-emerald-500 to-teal-600 p-4 rounded-2xl text-white">
|
|
<BookOpen size={28} />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-bold text-gray-800">Browse Resources</h3>
|
|
<p className="text-gray-500 text-sm mt-1">Tutorials, guides, and best practices</p>
|
|
</div>
|
|
</div>
|
|
</ClayCard>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|