mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Add full support for WhatsApp Cloud API interactive messages across the inbox, automations, and a new quick-replies feature. Extends the interactive plumbing that previously existed only for the Flows engine. Sending - send-message core handles `interactive` (reply buttons ≤3, list ≤10 rows) via the existing Meta senders; validated server-side before send. - Inbox composer gains a "+" menu → builder dialog with live WhatsApp-style preview + Meta char limits (button 20, list row 24, body 1024). - Automation "Send Buttons" / "Send List" steps in the builder. - Public v1 /messages accepts type=interactive. Storage / round-trip - migration 035: messages.interactive_payload JSONB + quick_replies table. - Outbound interactive persists the structured payload (composer, automations, and Flows) so the thread re-renders the buttons/rows. Receiving - Bubble renders outbound interactive via the shared preview; inbound taps (button_reply/list_reply) keep the "chosen option" affordance. Automations - New `interactive_reply` trigger matches on tapped button/list IDs (exact), enabling chained multi-step menus. Quick replies - New account-scoped quick_replies (text or interactive snippets), managed in Settings and insertable from the composer; "Save as quick reply" in the builder. Shared core in src/lib/whatsapp/interactive.ts + src/components/interactive/. Includes unit tests for the validator, send-param validation, the interactive_reply trigger match, and the new automation step/trigger validation. typecheck + lint clean; full suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
88 lines
3.3 KiB
TypeScript
88 lines
3.3 KiB
TypeScript
'use client';
|
|
|
|
import { useMemo, type ReactNode } from 'react';
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
import { useTranslations } from 'next-intl';
|
|
|
|
import { useAuth } from '@/hooks/use-auth';
|
|
import { useTheme } from '@/hooks/use-theme';
|
|
import { SettingsRail } from '@/components/settings/settings-rail';
|
|
import { SettingsOverview } from '@/components/settings/settings-overview';
|
|
import { ProfileForm } from '@/components/settings/profile-form';
|
|
import { SecurityPanel } from '@/components/settings/security-panel';
|
|
import { AppearancePanel } from '@/components/settings/appearance-panel';
|
|
import { WhatsAppConfig } from '@/components/settings/whatsapp-config';
|
|
import { TemplateManager } from '@/components/settings/template-manager';
|
|
import { QuickRepliesManager } from '@/components/settings/quick-replies-manager';
|
|
import { FieldsAndTagsPanel } from '@/components/settings/fields-and-tags-panel';
|
|
import { DealsSettings } from '@/components/settings/deals-settings';
|
|
import { MembersTab } from '@/components/settings/members-tab';
|
|
import { ApiKeysSettings } from '@/components/settings/api-keys-settings';
|
|
import {
|
|
resolveSection,
|
|
type SettingsSection,
|
|
} from '@/components/settings/settings-sections';
|
|
|
|
export default function SettingsPage() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const { defaultCurrency } = useAuth();
|
|
const { mode } = useTheme();
|
|
const t = useTranslations('Settings');
|
|
|
|
// The URL (`?tab=`) is the single source of truth for the active
|
|
// section — deep-linkable, and it keeps the existing links in the
|
|
// app sidebar/header working. Legacy tab values (tags, custom-fields)
|
|
// resolve onto their new home; unknown/empty → the Overview landing.
|
|
const section = resolveSection(searchParams.get('tab'));
|
|
|
|
const go = (next: SettingsSection) => {
|
|
const params = new URLSearchParams(searchParams.toString());
|
|
params.set('tab', next);
|
|
router.replace(`/settings?${params.toString()}`, { scroll: false });
|
|
};
|
|
|
|
// Cheap, fetch-free rail hints. The Overview landing carries the
|
|
// full live status/counts; the rail just surfaces the two that are
|
|
// already in context.
|
|
const hints: Partial<Record<SettingsSection, ReactNode>> = useMemo(
|
|
() => ({
|
|
appearance: mode.charAt(0).toUpperCase() + mode.slice(1),
|
|
deals: defaultCurrency,
|
|
}),
|
|
[mode, defaultCurrency],
|
|
);
|
|
|
|
const panel: Record<SettingsSection, ReactNode> = {
|
|
overview: <SettingsOverview onSelect={go} />,
|
|
profile: <ProfileForm />,
|
|
security: <SecurityPanel />,
|
|
appearance: <AppearancePanel />,
|
|
whatsapp: <WhatsAppConfig />,
|
|
templates: <TemplateManager />,
|
|
'quick-replies': <QuickRepliesManager />,
|
|
fields: <FieldsAndTagsPanel />,
|
|
deals: <DealsSettings />,
|
|
members: <MembersTab />,
|
|
api: <ApiKeysSettings />,
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div>
|
|
<h1 className="text-2xl font-bold tracking-tight text-foreground">
|
|
{t('pageTitle')}
|
|
</h1>
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
{t('pageDesc')}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="mt-6 grid gap-6 lg:grid-cols-[236px_minmax(0,1fr)] lg:items-start">
|
|
<SettingsRail active={section} onSelect={go} hints={hints} />
|
|
<div className="min-w-0">{panel[section]}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|