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

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