feat: Whatsapp Integration
Some checks failed
Production Readiness / backend-contracts (push) Failing after 1m58s
Production Readiness / webos-typecheck (push) Successful in 1m37s
Production Readiness / ipad-parse (push) Successful in 2m17s

This commit is contained in:
Sagnik
2026-04-28 13:41:14 +05:30
parent 7ee51543d9
commit 3623bacbac
15 changed files with 2549 additions and 3 deletions

View File

@@ -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>