cre-sync/components/Dashboard.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

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>
);
};