- 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>
217 lines
8.1 KiB
TypeScript
217 lines
8.1 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { Button } from '@/components/Button';
|
|
import { ClayCard } from '@/components/ClayCard';
|
|
import { Eye, EyeOff, Check, X, Loader2, Save } from 'lucide-react';
|
|
|
|
interface SettingsFormProps {
|
|
initialSettings: Record<string, string>;
|
|
onSave: (settings: Record<string, string>) => Promise<void>;
|
|
onTestGHL: () => Promise<{ success: boolean; error?: string }>;
|
|
onTestStripe: () => Promise<{ success: boolean; error?: string }>;
|
|
}
|
|
|
|
export function SettingsForm({
|
|
initialSettings,
|
|
onSave,
|
|
onTestGHL,
|
|
onTestStripe
|
|
}: SettingsFormProps) {
|
|
const [settings, setSettings] = useState(initialSettings);
|
|
const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({});
|
|
const [saving, setSaving] = useState(false);
|
|
const [testingGHL, setTestingGHL] = useState(false);
|
|
const [testingStripe, setTestingStripe] = useState(false);
|
|
const [ghlStatus, setGhlStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
|
const [stripeStatus, setStripeStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
|
|
|
const handleChange = (key: string, value: string) => {
|
|
setSettings(prev => ({ ...prev, [key]: value }));
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
setSaving(true);
|
|
try {
|
|
await onSave(settings);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleTestGHL = async () => {
|
|
setTestingGHL(true);
|
|
setGhlStatus('idle');
|
|
try {
|
|
const result = await onTestGHL();
|
|
setGhlStatus(result.success ? 'success' : 'error');
|
|
} finally {
|
|
setTestingGHL(false);
|
|
}
|
|
};
|
|
|
|
const handleTestStripe = async () => {
|
|
setTestingStripe(true);
|
|
setStripeStatus('idle');
|
|
try {
|
|
const result = await onTestStripe();
|
|
setStripeStatus(result.success ? 'success' : 'error');
|
|
} finally {
|
|
setTestingStripe(false);
|
|
}
|
|
};
|
|
|
|
const toggleSecret = (key: string) => {
|
|
setShowSecrets(prev => ({ ...prev, [key]: !prev[key] }));
|
|
};
|
|
|
|
const renderSecretInput = (key: string, label: string, placeholder: string) => (
|
|
<div className="space-y-2">
|
|
<label className="block text-sm font-medium text-gray-700">{label}</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showSecrets[key] ? 'text' : 'password'}
|
|
value={settings[key] || ''}
|
|
onChange={(e) => handleChange(key, e.target.value)}
|
|
placeholder={placeholder}
|
|
className="w-full p-3 pr-10 rounded-xl bg-gray-50 border-none shadow-inner focus:ring-2 focus:ring-indigo-500 outline-none"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => toggleSecret(key)}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
>
|
|
{showSecrets[key] ? <EyeOff size={18} /> : <Eye size={18} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderTextInput = (key: string, label: string, placeholder: string) => (
|
|
<div className="space-y-2">
|
|
<label className="block text-sm font-medium text-gray-700">{label}</label>
|
|
<input
|
|
type="text"
|
|
value={settings[key] || ''}
|
|
onChange={(e) => handleChange(key, e.target.value)}
|
|
placeholder={placeholder}
|
|
className="w-full p-3 rounded-xl bg-gray-50 border-none shadow-inner focus:ring-2 focus:ring-indigo-500 outline-none"
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* GHL Configuration */}
|
|
<ClayCard>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h3 className="text-lg font-bold text-gray-800">GoHighLevel Configuration</h3>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={handleTestGHL}
|
|
disabled={testingGHL}
|
|
>
|
|
{testingGHL ? (
|
|
<Loader2 className="animate-spin" size={16} />
|
|
) : ghlStatus === 'success' ? (
|
|
<Check className="text-green-600" size={16} />
|
|
) : ghlStatus === 'error' ? (
|
|
<X className="text-red-600" size={16} />
|
|
) : null}
|
|
Test Connection
|
|
</Button>
|
|
</div>
|
|
<div className="grid gap-4">
|
|
{renderSecretInput('ghlAgencyApiKey', 'Agency API Key', 'Enter your GHL Agency API key')}
|
|
{renderTextInput('ghlAgencyId', 'Agency ID', 'Enter your GHL Agency ID')}
|
|
{renderSecretInput('ghlPrivateToken', 'Private Integration Token', 'Optional: Private integration token')}
|
|
{renderTextInput('ghlOwnerLocationId', 'Owner Location ID', 'Location ID for tagging (Henry\'s account)')}
|
|
{renderSecretInput('ghlWebhookSecret', 'Webhook Secret', 'Secret for validating webhooks')}
|
|
</div>
|
|
</ClayCard>
|
|
|
|
{/* Tag Configuration */}
|
|
<ClayCard>
|
|
<h3 className="text-lg font-bold text-gray-800 mb-6">Tag Configuration</h3>
|
|
<p className="text-sm text-gray-500 mb-4">
|
|
These tags will be applied to contacts in the owner's GHL account to trigger automations.
|
|
</p>
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
{renderTextInput('tagHighGCI', 'High GCI Tag', 'e.g., high-gci-lead')}
|
|
{renderTextInput('tagOnboardingComplete', 'Onboarding Complete Tag', 'e.g., onboarding-done')}
|
|
{renderTextInput('tagDFYRequested', 'DFY Requested Tag', 'e.g., dfy-requested')}
|
|
</div>
|
|
</ClayCard>
|
|
|
|
{/* Stripe Configuration */}
|
|
<ClayCard>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h3 className="text-lg font-bold text-gray-800">Stripe Configuration</h3>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={handleTestStripe}
|
|
disabled={testingStripe}
|
|
>
|
|
{testingStripe ? (
|
|
<Loader2 className="animate-spin" size={16} />
|
|
) : stripeStatus === 'success' ? (
|
|
<Check className="text-green-600" size={16} />
|
|
) : stripeStatus === 'error' ? (
|
|
<X className="text-red-600" size={16} />
|
|
) : null}
|
|
Test Connection
|
|
</Button>
|
|
</div>
|
|
<div className="grid gap-4">
|
|
{renderSecretInput('stripeSecretKey', 'Stripe Secret Key', 'sk_live_... or sk_test_...')}
|
|
{renderSecretInput('stripeWebhookSecret', 'Stripe Webhook Secret', 'whsec_...')}
|
|
</div>
|
|
</ClayCard>
|
|
|
|
{/* Pricing Configuration */}
|
|
<ClayCard>
|
|
<h3 className="text-lg font-bold text-gray-800 mb-6">DFY Pricing (in cents)</h3>
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
{renderTextInput('dfyPriceFullSetup', 'Full Setup Price', 'e.g., 29700 for $297')}
|
|
{renderTextInput('dfyPriceSmsSetup', 'SMS Setup Price', 'e.g., 9900 for $99')}
|
|
{renderTextInput('dfyPriceEmailSetup', 'Email Setup Price', 'e.g., 9900 for $99')}
|
|
</div>
|
|
</ClayCard>
|
|
|
|
{/* Calendly Links */}
|
|
<ClayCard>
|
|
<h3 className="text-lg font-bold text-gray-800 mb-6">Calendly Links</h3>
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
{renderTextInput('calendlyCoachingLink', 'Coaching Calendly Link', 'https://calendly.com/...')}
|
|
{renderTextInput('calendlyTeamLink', 'Join Team Calendly Link', 'https://calendly.com/...')}
|
|
</div>
|
|
</ClayCard>
|
|
|
|
{/* Notifications */}
|
|
<ClayCard>
|
|
<h3 className="text-lg font-bold text-gray-800 mb-6">Notifications</h3>
|
|
<div className="grid gap-4">
|
|
{renderTextInput('notificationEmail', 'Notification Email', 'Email for high-GCI alerts')}
|
|
</div>
|
|
</ClayCard>
|
|
|
|
{/* ClickUp Integration */}
|
|
<ClayCard>
|
|
<h3 className="text-lg font-bold text-gray-800 mb-6">ClickUp Integration (for DFY tasks)</h3>
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
{renderSecretInput('clickupApiKey', 'ClickUp API Key', 'Enter your ClickUp API key')}
|
|
{renderTextInput('clickupListId', 'ClickUp List ID', 'List ID for DFY tasks')}
|
|
</div>
|
|
</ClayCard>
|
|
|
|
{/* Save Button */}
|
|
<div className="flex justify-end">
|
|
<Button onClick={handleSave} disabled={saving}>
|
|
{saving ? <Loader2 className="animate-spin mr-2" size={16} /> : <Save className="mr-2" size={16} />}
|
|
Save Settings
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|