'use client'; import { useEffect, useState } from 'react'; import { toast } from 'sonner'; import { Plus, Trash2, Loader2, RefreshCw } from 'lucide-react'; import { createClient } from '@/lib/supabase/client'; import { useAuth } from '@/hooks/use-auth'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import type { MessageTemplate } from '@/types'; const CATEGORIES = ['Marketing', 'Utility', 'Authentication'] as const; const HEADER_TYPES = ['text', 'image', 'video', 'document'] as const; const categoryColors: Record = { Marketing: 'bg-purple-600/20 text-purple-400 border-purple-600/30', Utility: 'bg-blue-600/20 text-blue-400 border-blue-600/30', Authentication: 'bg-amber-600/20 text-amber-400 border-amber-600/30', }; const statusColors: Record = { Draft: 'bg-slate-600/20 text-slate-400 border-slate-600/30', Pending: 'bg-yellow-600/20 text-yellow-400 border-yellow-600/30', Approved: 'bg-violet-600/20 text-violet-400 border-violet-600/30', Rejected: 'bg-red-600/20 text-red-400 border-red-600/30', }; interface TemplateFormData { name: string; category: MessageTemplate['category']; language: string; body_text: string; header_type: string; footer_text: string; } // Meta's language codes are exact — "en" and "en_US" are distinct and a // template approved under one will be rejected if you send with the other // (Graph API error #132001 "Template name does not exist in the // translation"). Default to en_US to match the DB default on // message_templates.language and the broadcasts sender's fallback. const emptyForm: TemplateFormData = { name: '', category: 'Marketing', language: 'en_US', body_text: '', header_type: '', footer_text: '', }; // Common Meta template language codes. The field still accepts any // string — this just offers autocomplete for the usual suspects. Full // list: https://developers.facebook.com/docs/whatsapp/api/messages/message-templates#supported-languages const COMMON_LANGUAGE_CODES = [ 'en_US', 'en_GB', 'en', 'es', 'es_ES', 'es_MX', 'fr', 'fr_FR', 'de', 'it', 'pt_BR', 'pt_PT', 'nl', 'pl', 'ru', 'tr', 'lt', ]; export function TemplateManager() { const supabase = createClient(); const { user, loading: authLoading } = useAuth(); const [loading, setLoading] = useState(true); const [templates, setTemplates] = useState([]); const [dialogOpen, setDialogOpen] = useState(false); const [saving, setSaving] = useState(false); const [syncing, setSyncing] = useState(false); const [form, setForm] = useState(emptyForm); useEffect(() => { if (authLoading) return; if (!user) { setLoading(false); return; } fetchTemplates(user.id); // eslint-disable-next-line react-hooks/exhaustive-deps }, [authLoading, user?.id]); async function fetchTemplates(userId: string) { try { setLoading(true); const { data, error } = await supabase .from('message_templates') .select('*') .eq('user_id', userId) .order('created_at', { ascending: false }); if (error) throw error; setTemplates(data || []); } catch (err) { console.error('Failed to fetch templates:', err); toast.error('Failed to load templates'); } finally { setLoading(false); } } async function handleSave() { if (!form.name.trim()) { toast.error('Template name is required'); return; } if (!form.body_text.trim()) { toast.error('Body text is required'); return; } try { setSaving(true); if (!user) { toast.error('Not authenticated'); return; } const payload = { user_id: user.id, name: form.name.trim(), category: form.category, language: form.language.trim() || 'en_US', body_text: form.body_text.trim(), header_type: form.header_type || null, footer_text: form.footer_text.trim() || null, status: 'Draft' as const, }; const { error } = await supabase .from('message_templates') .insert(payload); if (error) throw error; toast.success('Template created successfully'); setDialogOpen(false); setForm(emptyForm); if (user) await fetchTemplates(user.id); } catch (err) { console.error('Save error:', err); toast.error('Failed to create template'); } finally { setSaving(false); } } /** * Pull approved templates from Meta and upsert them into the local * catalog. After this runs, every local row is guaranteed to match * something Meta will actually accept on send — stops users getting * stuck on error #132001 "Template name does not exist". */ async function handleSyncFromMeta() { if (!user) return; setSyncing(true); try { const res = await fetch('/api/whatsapp/templates/sync', { method: 'POST', }); const data = await res.json(); if (!res.ok) { throw new Error(data?.error || `Sync failed (HTTP ${res.status})`); } toast.success( `Synced ${data.total} template${data.total === 1 ? '' : 's'} from Meta` + (data.inserted || data.updated ? ` (${data.inserted} new, ${data.updated} updated)` : ''), ); if (Array.isArray(data.errors) && data.errors.length > 0) { // Surface per-template failures so users don't trust a green // toast that hides silent drift. const preview = data.errors.slice(0, 3).map( (e: { name: string; language: string; message: string }) => `${e.name} (${e.language})`, ); const suffix = data.errors.length > 3 ? `, +${data.errors.length - 3} more` : ''; toast.error(`Failed to sync: ${preview.join(', ')}${suffix}`); } if (data.truncated) { toast.warning( 'Hit Meta pagination cap — more templates may exist. Contact support if this persists.', ); } await fetchTemplates(user.id); } catch (err) { console.error('Template sync error:', err); toast.error( err instanceof Error ? err.message : 'Failed to sync templates', ); } finally { setSyncing(false); } } async function handleDelete(id: string) { try { const { error } = await supabase .from('message_templates') .delete() .eq('id', id); if (error) throw error; toast.success('Template deleted'); setTemplates((prev) => prev.filter((t) => t.id !== id)); } catch (err) { console.error('Delete error:', err); toast.error('Failed to delete template'); } } if (loading) { return (
); } return (

Message Templates

Create and manage your WhatsApp message templates. Meta requires every template to be approved in the WhatsApp Manager before it can be sent — use "Sync from Meta" to pull your approved list.

{templates.length === 0 ? (

No templates yet.

Create your first message template to get started.

) : (
{templates.map((template) => (

{template.name}

{template.category} {template.status || 'Draft'} {template.language && ( {template.language} )}

{template.body_text}

{template.footer_text && (

{template.footer_text}

)}
))}
)} {/* New Template Dialog */} New Message Template Create a new WhatsApp message template.
setForm({ ...form, name: e.target.value })} className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" />
setForm({ ...form, language: e.target.value })} className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" /> {COMMON_LANGUAGE_CODES.map((code) => (

Must match the exact language code the template is approved under on Meta — e.g. en_US and en{' '} are distinct.