feat: Whatsapp Integration
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState, type ChangeEvent } from 'react';
|
||||
import { useEffect, useRef, useState, type ChangeEvent } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
User,
|
||||
@@ -16,12 +16,15 @@ import {
|
||||
ChevronDown,
|
||||
LogOut,
|
||||
Pencil,
|
||||
MessageCircle,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { useStore } from '@/store/useStore';
|
||||
import { useCurrency, CURRENCY_OPTIONS } from '@/store/useCurrencyStore';
|
||||
import type { CurrencyCode } from '@/store/useCurrencyStore';
|
||||
import { API_URL } from '@/lib/api';
|
||||
import { fetchCommsSettings, testCommsProviderConnection, updateCommsSettings } from '@/lib/commsApi';
|
||||
import type { CommsProvider, CommsSettings } from '@/types/commsTypes';
|
||||
import {
|
||||
clearVelocityToken,
|
||||
getVelocityToken,
|
||||
@@ -613,6 +616,160 @@ function DisplaySettings() {
|
||||
}
|
||||
|
||||
// ── Data & Privacy ───────────────────────────────────────────────────────────
|
||||
function CommunicationsSettings() {
|
||||
const [settings, setSettings] = useState<CommsSettings | null>(null);
|
||||
const [draft, setDraft] = useState<Partial<CommsSettings>>({});
|
||||
const [statusText, setStatusText] = useState('Loading provider settings...');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const current: CommsSettings = {
|
||||
provider: 'mock',
|
||||
providerBaseUrl: '',
|
||||
providerApiKey: '',
|
||||
instanceId: '',
|
||||
phoneNumberId: '',
|
||||
webhookCallbackUrl: '/api/comms/webhooks/{provider}',
|
||||
webhookSecretSet: false,
|
||||
autoLinkByPhone: true,
|
||||
createCrmInteractionOnInbound: true,
|
||||
defaultCountryCode: '91',
|
||||
transcriptionProvider: 'none',
|
||||
...(settings ?? {}),
|
||||
...draft,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void fetchCommsSettings()
|
||||
.then((value) => {
|
||||
if (cancelled) return;
|
||||
setSettings(value);
|
||||
setStatusText('Settings loaded from backend.');
|
||||
})
|
||||
.catch((error) => {
|
||||
if (cancelled) return;
|
||||
setStatusText(error instanceof Error ? error.message : 'Unable to load comms settings.');
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const update = <K extends keyof CommsSettings>(key: K, value: CommsSettings[K]) => {
|
||||
setDraft((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await updateCommsSettings(draft);
|
||||
const latest = await fetchCommsSettings();
|
||||
setSettings(latest);
|
||||
setDraft({});
|
||||
setStatusText('Communications settings saved.');
|
||||
} catch (error) {
|
||||
setStatusText(error instanceof Error ? error.message : 'Failed to save communications settings.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const test = async () => {
|
||||
try {
|
||||
const result = await testCommsProviderConnection();
|
||||
setStatusText(result.message || (result.success ? 'Provider connection succeeded.' : 'Provider connection failed.'));
|
||||
} catch (error) {
|
||||
setStatusText(error instanceof Error ? error.message : 'Provider test failed.');
|
||||
}
|
||||
};
|
||||
|
||||
const fieldClass = "w-64 rounded-xl px-3 py-2 text-sm text-white placeholder-zinc-500 outline-none";
|
||||
|
||||
return (
|
||||
<GlassCard delay={0.3}>
|
||||
<SectionHeader icon={MessageCircle} title="Communications" accent="#22d3ee" />
|
||||
<div>
|
||||
<SettingsRow label="Provider" description="Mock is local preview. WAHA and Evolution require a running provider service.">
|
||||
<DarkSelect
|
||||
value={current.provider}
|
||||
onChange={(v) => update('provider', v as CommsProvider)}
|
||||
options={[
|
||||
{ value: 'mock', label: 'Mock' },
|
||||
{ value: 'waha', label: 'WAHA' },
|
||||
{ value: 'evolution', label: 'Evolution API' },
|
||||
{ value: 'meta_cloud', label: 'Meta Cloud API' },
|
||||
]}
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Provider Base URL" description="Internal or public base URL for WAHA/Evolution.">
|
||||
<input
|
||||
className={fieldClass}
|
||||
style={INNER_SURFACE}
|
||||
value={current.providerBaseUrl ?? ''}
|
||||
onChange={(event) => update('providerBaseUrl', event.target.value)}
|
||||
placeholder="http://127.0.0.1:3000"
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="API Key" description="Stored in backend comms settings. Masked when read back.">
|
||||
<input
|
||||
className={fieldClass}
|
||||
style={INNER_SURFACE}
|
||||
type="password"
|
||||
value={current.providerApiKey ?? ''}
|
||||
onChange={(event) => update('providerApiKey', event.target.value)}
|
||||
placeholder="Provider API key"
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Instance / Session" description="WAHA session name or Evolution instance name.">
|
||||
<input
|
||||
className={fieldClass}
|
||||
style={INNER_SURFACE}
|
||||
value={current.instanceId ?? ''}
|
||||
onChange={(event) => update('instanceId', event.target.value)}
|
||||
placeholder="default"
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Webhook URL" description="Point provider inbound webhooks here.">
|
||||
<span className="text-xs font-mono text-zinc-300">{current.webhookCallbackUrl || '/api/comms/webhooks/{provider}'}</span>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Auto-link by Phone" description="Match inbound numbers to crm_people.primary_phone.">
|
||||
<Toggle enabled={Boolean(current.autoLinkByPhone)} onChange={(v) => update('autoLinkByPhone', v)} />
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Create CRM Interaction" description="Mirror inbound messages into canonical intelligence tables.">
|
||||
<Toggle enabled={Boolean(current.createCrmInteractionOnInbound)} onChange={(v) => update('createCrmInteractionOnInbound', v)} />
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Transcription Provider" description="Recording intake is stored now; transcription worker can be added later.">
|
||||
<DarkSelect
|
||||
value={current.transcriptionProvider ?? 'none'}
|
||||
onChange={(v) => update('transcriptionProvider', v as CommsSettings['transcriptionProvider'])}
|
||||
options={[
|
||||
{ value: 'none', label: 'None' },
|
||||
{ value: 'local', label: 'Local Whisper' },
|
||||
{ value: 'openai', label: 'OpenAI' },
|
||||
]}
|
||||
/>
|
||||
</SettingsRow>
|
||||
<div className="px-6 py-4 flex items-center justify-between gap-3">
|
||||
<p className="text-xs text-zinc-400">{statusText}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<GhostButton onClick={test}>Test</GhostButton>
|
||||
<motion.button
|
||||
type="button"
|
||||
onClick={save}
|
||||
disabled={saving || Object.keys(draft).length === 0}
|
||||
className="px-4 py-2 rounded-xl text-sm font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{ background: 'hsl(var(--accent))', color: 'hsl(var(--accent-fg))' }}
|
||||
whileHover={{ scale: saving ? 1 : 1.02 }}
|
||||
whileTap={{ scale: saving ? 1 : 0.97 }}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
);
|
||||
}
|
||||
|
||||
function DataSettings() {
|
||||
const [retention, setRetention] = useState('90');
|
||||
const { leads, messages, units, status } = useStore();
|
||||
@@ -728,9 +885,14 @@ export function Settings() {
|
||||
<DisplaySettings />
|
||||
</div>
|
||||
|
||||
{/* Row 4: Data + About */}
|
||||
{/* Row 4: Communications + Data */}
|
||||
<div className="grid grid-cols-2 gap-4 relative z-10">
|
||||
<CommunicationsSettings />
|
||||
<DataSettings />
|
||||
</div>
|
||||
|
||||
{/* Row 5: About */}
|
||||
<div className="grid grid-cols-1 gap-4 relative z-0">
|
||||
<AboutSection />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user