From 6b6b929feaa57c44f5652b3a31788ae35ba6ccf9 Mon Sep 17 00:00:00 2001 From: debu-2 Date: Thu, 15 Oct 2026 02:09:25 +0530 Subject: [PATCH] template --- .../whatsapp/WhatsAppBroadcastPage.tsx | 198 +++++++++++------- frontend/src/index.css | 2 + 2 files changed, 125 insertions(+), 75 deletions(-) diff --git a/frontend/src/features/whatsapp/WhatsAppBroadcastPage.tsx b/frontend/src/features/whatsapp/WhatsAppBroadcastPage.tsx index 48ae011..bfb0c75 100644 --- a/frontend/src/features/whatsapp/WhatsAppBroadcastPage.tsx +++ b/frontend/src/features/whatsapp/WhatsAppBroadcastPage.tsx @@ -2,7 +2,7 @@ import { Loader2 } from 'lucide-react' import { useEffect, useState, type FormEvent } from 'react' import { Link, useSearchParams } from 'react-router-dom' import { Bar, BarChart, CartesianGrid, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' -import { waTemplateApi, whatsappApi, type WaManagedTemplate, type WhatsAppAccount, type WhatsAppBroadcastJob, type WhatsAppTemplateRow } from '../../lib/api' +import { waTemplateApi, whatsappApi, type WaManagedTemplate, type WhatsAppAccount, type WhatsAppBroadcastJob } from '../../lib/api' const COUNTRIES = [ { code: '91', label: 'India' }, @@ -12,6 +12,25 @@ const COUNTRIES = [ { code: '65', label: 'Singapore' }, ] +const MEDIA_HEADERS = ['IMAGE', 'VIDEO', 'DOCUMENT'] + +function headerKind(template: WaManagedTemplate | undefined) { + if (!template) return '' + const direct = (template.header_format || '').toUpperCase() + if (MEDIA_HEADERS.includes(direct)) return direct + const drafted = (template.definition?.header?.type || '').toUpperCase() + if (MEDIA_HEADERS.includes(drafted)) return drafted + const components = Array.isArray(template.components) ? template.components : [] + for (const component of components) { + if (!component || typeof component !== 'object') continue + const row = component as { type?: string; format?: string } + if (String(row.type || '').toUpperCase() !== 'HEADER') continue + const format = String(row.format || '').toUpperCase() + if (MEDIA_HEADERS.includes(format)) return format + } + return '' +} + const TEAM_TEMPLATE_NAME = 'urmi_team_aircheck' const TEAM_TEMPLATE_BODY = "Team, the broadcast desk is on air. This is the first note Urmi is sending through Meta's marketing line, and it is written for the people building it. Read it on your phone, then bring it to standup so we can close the test." @@ -31,7 +50,7 @@ export function WhatsAppBroadcastPage() { const [params] = useSearchParams() const [accounts, setAccounts] = useState([]) const [accountId, setAccountId] = useState('') - const [templates, setTemplates] = useState([]) + const [phoneId, setPhoneId] = useState('') const [managed, setManaged] = useState([]) const [templatesReady, setTemplatesReady] = useState(false) const [useSample, setUseSample] = useState(true) @@ -62,38 +81,23 @@ export function WhatsAppBroadcastPage() { .then((rows) => { const ready = rows.filter((row) => row.messaging_status === 'active' && row.phones.some((phone) => phone.status === 'connected')) setAccounts(ready) - if (ready[0]) setAccountId(ready[0].id) + const first = ready.flatMap((account) => account.phones.filter((phone) => phone.status === 'connected').map((phone) => ({ account, phone })))[0] + if (first) { + setAccountId(first.account.id) + setPhoneId(first.phone.phone_number_id) + } }) .catch((reason) => setError(reason instanceof Error ? reason.message : 'Could not load WhatsApp accounts')) .finally(() => setLoading(false)) }, []) useEffect(() => { - if (!accountId) return setTemplatesReady(false) - whatsappApi.templates(accountId) - .then((payload) => { - const marketing = payload.templates.filter((item) => (item.category || '').toUpperCase() === 'MARKETING') - setTemplates(marketing) - setTemplatesReady(payload.available) - const requested = params.get('template') - const requestedLanguage = params.get('language') - const approved = marketing.find((item) => item.name === requested) || marketing.find((item) => (item.status || '').toUpperCase() === 'APPROVED' && item.name) - if (requested) setTemplateName(requested) - if (requestedLanguage) setTemplateLanguage(requestedLanguage) - if (approved?.name && !requested) { - setTemplateName(approved.name) - setTemplateLanguage(approved.language || 'en') - } - }) - .catch(() => { - setTemplates([]) - setTemplatesReady(false) - }) - waTemplateApi.list(accountId) + waTemplateApi.list() .then((payload) => setManaged(payload.templates)) .catch(() => setManaged([])) - }, [accountId, templateTick]) + .finally(() => setTemplatesReady(true)) + }, [templateTick]) useEffect(() => { if (!accountId) return @@ -169,7 +173,7 @@ export function WhatsAppBroadcastPage() { recipients: numbers, body_parameters: parameters, product_policy: strict ? 'STRICT' : 'CLOUD_API_FALLBACK', - use_sample_header: Boolean(mediaHeader && useSample && savedSample), + use_sample_header: sampleOn, }) setResult(payload) } catch (reason) { @@ -179,13 +183,46 @@ export function WhatsAppBroadcastPage() { } } - const approved = templates.filter((item) => (item.status || '').toUpperCase() === 'APPROVED' && item.name) - const pending = templates.filter((item) => (item.status || '').toUpperCase() !== 'APPROVED' && item.name) - const selectedTemplate = managed.find((item) => item.name === templateName && item.language === templateLanguage) - || managed.find((item) => item.name === templateName) - const mediaHeader = ['IMAGE', 'VIDEO', 'DOCUMENT'].includes(selectedTemplate?.header_format || '') + const numbers = accounts.flatMap((account) => account.phones.filter((phone) => phone.status === 'connected').map((phone) => ({ account, phone }))) + const selectedNumber = numbers.find((item) => item.phone.phone_number_id === phoneId) || numbers[0] + const approved = managed.filter((item) => ( + item.status === 'APPROVED' + && item.category.toUpperCase() === 'MARKETING' + && Boolean(item.name) + && (!selectedNumber || item.account_id === selectedNumber.account.id) + )) + const pending = managed.filter((item) => item.status !== 'APPROVED' && item.category.toUpperCase() === 'MARKETING' && (!selectedNumber || item.account_id === selectedNumber.account.id)) + const selectedTemplate = approved.find((item) => item.name === templateName && item.language === templateLanguage) + || approved.find((item) => item.name === templateName) + + useEffect(() => { + if (!templatesReady || !numbers.length) return + const current = numbers.find((item) => item.phone.phone_number_id === phoneId) || numbers[0] + const hasApproved = managed.some((item) => item.status === 'APPROVED' && item.category.toUpperCase() === 'MARKETING' && item.account_id === current.account.id) + if (hasApproved) return + const next = numbers.find((item) => managed.some((template) => template.status === 'APPROVED' && template.category.toUpperCase() === 'MARKETING' && template.account_id === item.account.id)) + if (!next || next.phone.phone_number_id === current.phone.phone_number_id) return + setPhoneId(next.phone.phone_number_id) + setAccountId(next.account.id) + }, [templatesReady, managed, numbers, phoneId]) + + useEffect(() => { + if (!approved.length) return + const requested = params.get('template') + const requestedLanguage = params.get('language') + const current = approved.find((item) => item.name === templateName && item.language === templateLanguage) + const requestedMatch = approved.find((item) => item.name === requested && (!requestedLanguage || item.language === requestedLanguage)) + const next = current || requestedMatch || approved[0] + if (next.name !== templateName || next.language !== templateLanguage) { + setTemplateName(next.name) + setTemplateLanguage(next.language) + } + }, [approved, params, templateLanguage, templateName]) + const headerFormat = headerKind(selectedTemplate) || (selectedTemplate?.sample_stored ? 'IMAGE' : '') + const mediaHeader = MEDIA_HEADERS.includes(headerFormat) const savedSample = Boolean(selectedTemplate?.sample_stored) - const headerLabel = selectedTemplate?.header_format === 'VIDEO' ? 'video' : selectedTemplate?.header_format === 'DOCUMENT' ? 'document' : 'image' + const headerLabel = headerFormat === 'VIDEO' ? 'video' : headerFormat === 'DOCUMENT' ? 'document' : 'image' + const sampleOn = Boolean(mediaHeader && useSample && savedSample) function downloadSampleCsv() { const blob = new Blob(['phone_number\n'], { type: 'text/csv;charset=utf-8' }) @@ -223,7 +260,7 @@ export function WhatsAppBroadcastPage() { template_name: templateName.trim(), template_language: templateLanguage.trim() || 'en', product_policy: strict ? 'STRICT' : 'CLOUD_API_FALLBACK', - use_sample_header: Boolean(mediaHeader && useSample && savedSample), + use_sample_header: sampleOn, body_parameters: bodyValues, }) setJobs((current) => [job, ...current.filter((item) => item.id !== job.id)]) @@ -286,12 +323,20 @@ export function WhatsAppBroadcastPage() {
)} diff --git a/frontend/src/index.css b/frontend/src/index.css index b0a5fb1..0aaaa46 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1848,6 +1848,8 @@ main { padding: 30px clamp(20px, 3vw, 44px) 50px; } .wa-token-embed h2 { margin-top: 0; } .wa-token-form { display: grid; gap: 12px; margin-top: 16px; } .wa-token-form .primary-button { justify-self: start; display: inline-flex; align-items: center; gap: 8px; } +.wa-broadcast-actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-self: start; } +.wa-broadcast-actions .primary-button, .wa-broadcast-actions .secondary-button { justify-self: auto; } .wa-token-page { max-width: 720px; } .wa-broadcast-steps { margin: 0; padding-left: 18px; display: grid; gap: 6px; } .wa-broadcast-results { display: grid; gap: 8px; margin-top: 12px; }