urmi-ai/frontend/src/features/whatsapp/WhatsAppTemplatesPage.tsx
2026-10-14 06:04:27 +05:30

188 lines
9.4 KiB
TypeScript

import { Loader2, Plus } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { waTemplateApi, whatsappApi, type WaManagedTemplate, type WhatsAppAccount } from '../../lib/api'
const STATUS_LABEL: Record<string, string> = {
APPROVED: 'Approved',
PENDING: 'Pending',
REJECTED: 'Rejected',
PAUSED: 'Paused',
DISABLED: 'Disabled',
IN_APPEAL: 'In appeal',
}
function statusLabel(status: string) {
return STATUS_LABEL[status] || status.toLowerCase()
}
export function WhatsAppTemplatesPage() {
const navigate = useNavigate()
const [accounts, setAccounts] = useState<WhatsAppAccount[]>([])
const [accountId, setAccountId] = useState('')
const [rows, setRows] = useState<WaManagedTemplate[]>([])
const [summary, setSummary] = useState({ total: 0, approved: 0, pending: 0, rejected: 0, paused: 0 })
const [warnings, setWarnings] = useState<string[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [guidelines, setGuidelines] = useState(false)
const [search, setSearch] = useState('')
const [category, setCategory] = useState('')
const [language, setLanguage] = useState('')
const [status, setStatus] = useState('')
const [from, setFrom] = useState('')
const [to, setTo] = useState('')
function load(nextAccount = accountId) {
setLoading(true)
setError('')
waTemplateApi.list(nextAccount || undefined)
.then((payload) => {
setRows(payload.templates)
setSummary(payload.summary)
setWarnings(payload.warnings)
})
.catch((reason) => setError(reason instanceof Error ? reason.message : 'Templates could not be loaded'))
.finally(() => setLoading(false))
}
useEffect(() => {
whatsappApi.accounts()
.then(setAccounts)
.catch(() => setAccounts([]))
load('')
// The first load is intentionally unfiltered.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const filtered = useMemo(() => rows.filter((row) => {
const haystack = `${row.name} ${row.meta_template_id || ''} ${(row.phones || []).join(' ')} ${row.display_phone || ''} ${row.connection_name || ''} ${row.account_name || ''}`.toLowerCase()
if (search && !haystack.includes(search.toLowerCase())) return false
if (category && row.category !== category) return false
if (language && row.language !== language) return false
if (status && row.status !== status) return false
const updated = row.updated_at?.slice(0, 10)
if (from && updated < from) return false
if (to && updated > to) return false
return true
}), [rows, search, category, language, status, from, to])
const languages = [...new Set(rows.map((row) => row.language))].sort()
const cards = [
['Total templates', summary.total],
['Approved', summary.approved],
['Pending review', summary.pending],
['Rejected', summary.rejected],
['Paused', summary.paused],
] as const
async function remove(row: WaManagedTemplate) {
if (!window.confirm(`Delete ${row.name}? Meta removes it from this WhatsApp account.`)) return
try {
await waTemplateApi.remove(row.id)
load()
} catch (reason) {
setError(reason instanceof Error ? reason.message : 'This template could not be deleted')
}
}
return (
<div className="wa-tpl">
<div className="account-heading wa-tpl-heading">
<div>
<span className="eyebrow">Communicate</span>
<h1>Message templates</h1>
<p>Create, manage, and monitor your WhatsApp message templates.</p>
</div>
<div className="wa-tpl-heading-actions">
<button type="button" className="secondary-button" onClick={() => setGuidelines((open) => !open)}>Template guidelines</button>
<Link className="primary-button" to="/console/whatsapp/templates/new"><Plus size={16} /> Create template</Link>
</div>
</div>
{guidelines && (
<section className="wa-biz-card wa-tpl-guidelines">
<h2>Template guidelines</h2>
<ul>
<li>Marketing is for offers and announcements. Utility is for an existing order or account. Authentication is only for one-time codes.</li>
<li>Names use lowercase letters, numbers, and underscores, and must be unique on the WhatsApp account for that language.</li>
<li>Meta reviews every template. A template stays pending until Meta approves it, and Meta can change the category.</li>
<li>Templates belong to the WhatsApp Business account, so every number on that account can use an approved template.</li>
</ul>
</section>
)}
{error && <p className="auth-error">{error}</p>}
{warnings.map((warning) => <p className="auth-error" key={warning}>{warning}</p>)}
<div className="wa-tpl-cards">
{cards.map(([label, value]) => (
<article key={label}>
<span>{label}</span>
<strong>{value}</strong>
</article>
))}
</div>
<section className="wa-biz-card">
<div className="wa-tpl-filters">
<input value={search} onChange={(event) => setSearch(event.target.value)} placeholder="Search by name or ID" aria-label="Search templates" />
<select value={accountId} onChange={(event) => { setAccountId(event.target.value); load(event.target.value) }} aria-label="WhatsApp account">
<option value="">All accounts</option>
{accounts.filter((account) => account.status !== 'disconnected').map((account) => {
const numbers = account.phones.map((phone) => phone.display_phone).filter(Boolean).join(', ')
return <option key={account.id} value={account.id}>{numbers ? `${account.name} · ${numbers}` : account.name}</option>
})}
</select>
<select value={category} onChange={(event) => setCategory(event.target.value)} aria-label="Category">
<option value="">Category</option>
<option value="MARKETING">Marketing</option>
<option value="UTILITY">Utility</option>
<option value="AUTHENTICATION">Authentication</option>
</select>
<select value={language} onChange={(event) => setLanguage(event.target.value)} aria-label="Language">
<option value="">Language</option>
{languages.map((item) => <option key={item} value={item}>{item}</option>)}
</select>
<select value={status} onChange={(event) => setStatus(event.target.value)} aria-label="Status">
<option value="">Status</option>
{Object.entries(STATUS_LABEL).map(([value, label]) => <option key={value} value={value}>{label}</option>)}
</select>
<input type="date" value={from} onChange={(event) => setFrom(event.target.value)} aria-label="Updated from" />
<input type="date" value={to} onChange={(event) => setTo(event.target.value)} aria-label="Updated to" />
</div>
{loading && <div className="auth-loading"><Loader2 size={16} className="wa-spin" /> Loading templates…</div>}
{!loading && accounts.length === 0 && <p>Connect a WhatsApp number before creating templates. <Link to="/console/whatsapp">Open WhatsApp</Link></p>}
{!loading && accounts.length > 0 && filtered.length === 0 && <p>{rows.length === 0 ? 'No templates yet. Create one and Meta will review it.' : 'No templates match these filters.'}</p>}
{!loading && filtered.length > 0 && (
<div className="wa-tpl-table">
<div className="wa-tpl-row wa-tpl-head">
<span>Template name</span><span>Number</span><span>Connection</span><span>Category</span><span>Language</span><span>Status</span><span>Quality</span><span>Messages sent</span><span>Last updated</span><span>Actions</span>
</div>
{filtered.map((row) => (
<div className="wa-tpl-row" key={row.id}>
<button type="button" className="wa-tpl-name" onClick={() => navigate(`/console/whatsapp/templates/${row.id}`)}>{row.name}</button>
<span className="wa-tpl-number">{(row.phones && row.phones.length ? row.phones : [row.display_phone]).filter(Boolean).join(', ') || 'Shared on the account'}</span>
<span className="wa-tpl-connection"><strong>{row.connection_name || row.account_name}</strong><small>{row.connection_method}</small></span>
<span>{row.category.toLowerCase()}</span>
<span>{row.language}</span>
<span className={`wa-tpl-status is-${row.status.toLowerCase()}`}>{statusLabel(row.status)}</span>
<span>{row.quality || '—'}</span>
<span>{row.messages_sent}</span>
<span>{row.updated_at ? new Date(row.updated_at).toLocaleDateString() : '—'}</span>
<span className="wa-tpl-actions">
<button type="button" onClick={() => navigate(`/console/whatsapp/templates/${row.id}`)}>View</button>
{['REJECTED', 'PAUSED', 'APPROVED'].includes(row.status) && <button type="button" onClick={() => navigate(`/console/whatsapp/templates/${row.id}/edit`)}>Edit</button>}
<button type="button" onClick={() => navigate(`/console/whatsapp/templates/new?duplicate=${row.id}`)}>Duplicate</button>
<button type="button" onClick={() => void remove(row)}>Delete</button>
</span>
</div>
))}
</div>
)}
</section>
</div>
)
}