import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { useParams, useSearchParams } from 'react-router-dom'; import apiClient from '../api/client'; import TemplateDetailWorkspaceModal from '../components/TemplateDetailWorkspaceModal'; import WhitelistModal from '../components/WhitelistModal'; import TestSmsModal from '../components/TestSmsModal'; const MANAGEABLE_TEMPLATE_STATUSES = new Set(['pending_whitelisting', 'whitelisted']); const CARD_APPEARANCE = { live: { pillLabel: 'Live', pillClassName: 'border-[#9dd3c5] bg-[#e9f6f1] text-[#2f7f74]', accentClassName: 'border-l-[#2f7f74]', switchTrackClassName: 'border-[#2f7f74] bg-[#2f7f74]', description: 'Published template is active for runtime sending.', }, paused: { pillLabel: 'Paused', pillClassName: 'border-[#d6dde6] bg-[#f3f6fa] text-[#5f6f82]', accentClassName: 'border-l-[#8fa0b3]', switchTrackClassName: 'border-[#8fa0b3] bg-[#8fa0b3]', description: 'Published template is available, but runtime sending is paused.', }, pending: { pillLabel: 'Pending Whitelisting', pillClassName: 'border-[#bfd0ff] bg-[#f2f6ff] text-[#4563d5]', accentClassName: 'border-l-[#4563d5]', switchTrackClassName: 'border-[#d8dee8] bg-[#eef1f5]', description: 'Selected template is awaiting whitelisting before it can go live.', }, }; function matchesTemplateSearch(template, profile, rawSearchTerm) { const searchTerm = rawSearchTerm.trim().toLowerCase(); if (!searchTerm) return true; const candidates = [ template?.eventLabel, template?.eventSlug, template?.templateId, profile?.name, profile?.id, ] .map((value) => String(value || '').toLowerCase()) .filter(Boolean); return candidates.some((value) => value.includes(searchTerm)); } function getTemplateDisplayName(template) { return template?.eventLabel || String(template?.eventSlug || '').replace(/_/g, ' ') || 'Template'; } function getCardAppearance(template) { if (template?.status === 'whitelisted') { return template?.isRuntimeEnabled === false ? CARD_APPEARANCE.paused : CARD_APPEARANCE.live; } return CARD_APPEARANCE.pending; } function formatDltTemplateId(templateId) { const value = String(templateId || '').trim(); return value || 'Pending'; } function getBoundProfileSummary(template, profile) { if (profile?.name) return profile.name; if (template?.curlProfileId) return 'Profile missing'; return 'Not bound yet'; } function getTemplateSortRank(template) { if (template?.status === 'whitelisted') { return template?.isRuntimeEnabled === false ? 1 : 0; } return 2; } export default function Templates() { const { businessId } = useParams(); const [searchParams] = useSearchParams(); const [templates, setTemplates] = useState([]); const [profilesById, setProfilesById] = useState({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [whitelistTarget, setWhitelistTarget] = useState(null); const [testTarget, setTestTarget] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [runtimeUpdatingSlug, setRuntimeUpdatingSlug] = useState(''); const [highlightedEventSlug, setHighlightedEventSlug] = useState(''); const [workspaceSlug, setWorkspaceSlug] = useState(''); const templateCardRefs = useRef({}); const highlightTimeoutRef = useRef(null); const handledFocusSlugRef = useRef(''); const loadTemplates = useCallback(async () => { setLoading(true); setError(''); try { const [templatesRes, profilesRes] = await Promise.all([ apiClient.get(`/api/businesses/${businessId}/templates`), apiClient.get(`/api/businesses/${businessId}/global-sms/profiles`).catch(() => ({ data: { profiles: [] } })), ]); const allTemplates = (templatesRes.data.templates || []).filter((template) => template.selectedTemplate); const profileMap = Object.fromEntries((profilesRes.data.profiles || []).map((profile) => [profile.id, profile])); setTemplates(allTemplates); setProfilesById(profileMap); } catch { setError('Failed to load templates'); } finally { setLoading(false); } }, [businessId]); useEffect(() => { loadTemplates(); }, [loadTemplates]); useEffect(() => () => { if (highlightTimeoutRef.current) { window.clearTimeout(highlightTimeoutRef.current); } }, []); const manageableTemplates = useMemo( () => templates.filter((template) => MANAGEABLE_TEMPLATE_STATUSES.has(template?.status)), [templates], ); const visibleTemplates = useMemo(() => { return manageableTemplates .filter((template) => ( matchesTemplateSearch(template, template.curlProfileId ? profilesById[template.curlProfileId] || null : null, searchTerm) )) .sort((leftTemplate, rightTemplate) => { const rankDifference = getTemplateSortRank(leftTemplate) - getTemplateSortRank(rightTemplate); if (rankDifference !== 0) return rankDifference; return getTemplateDisplayName(leftTemplate).localeCompare(getTemplateDisplayName(rightTemplate)); }); }, [manageableTemplates, profilesById, searchTerm]); useEffect(() => { const targetEventSlug = searchParams.get('event'); if (!targetEventSlug || manageableTemplates.length === 0) return; if (handledFocusSlugRef.current === targetEventSlug) return; const targetTemplate = manageableTemplates.find((template) => template.eventSlug === targetEventSlug); if (!targetTemplate) return; const targetCard = templateCardRefs.current[targetEventSlug]; if (!targetCard) return; handledFocusSlugRef.current = targetEventSlug; targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' }); setHighlightedEventSlug(targetEventSlug); if (highlightTimeoutRef.current) { window.clearTimeout(highlightTimeoutRef.current); } highlightTimeoutRef.current = window.setTimeout(() => { setHighlightedEventSlug((currentSlug) => (currentSlug === targetEventSlug ? '' : currentSlug)); highlightTimeoutRef.current = null; }, 2200); }, [manageableTemplates, searchParams]); useEffect(() => { if (!workspaceSlug) return; if (manageableTemplates.some((template) => template.eventSlug === workspaceSlug)) return; setWorkspaceSlug(''); }, [manageableTemplates, workspaceSlug]); async function handleWhitelistSuccess() { setWhitelistTarget(null); await loadTemplates(); } async function handleRuntimeToggle(template) { const nextRuntimeState = !(template?.isRuntimeEnabled !== false); setRuntimeUpdatingSlug(template.eventSlug); setError(''); try { const res = await apiClient.patch( `/api/businesses/${businessId}/templates/${template.eventSlug}/runtime`, { isRuntimeEnabled: nextRuntimeState }, ); setTemplates((currentTemplates) => currentTemplates.map((currentTemplate) => ( currentTemplate.eventSlug === template.eventSlug ? res.data : currentTemplate ))); } catch (err) { setError(err.response?.data?.error || 'Failed to update template runtime state'); } finally { setRuntimeUpdatingSlug(''); } } const currentWorkspaceTemplate = workspaceSlug ? manageableTemplates.find((template) => template.eventSlug === workspaceSlug) || null : null; if (loading) { return (
); } return (

Templates

Manage template runtime, whitelisting, and testing from one place.

{error && (
{error}
)}
setSearchTerm(event.target.value)} placeholder="Search by event, profile, or DLT template ID" className="w-full rounded-xl border border-gray-200 bg-white py-3 pl-4 pr-12 text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-blue" /> {searchTerm && ( )}
{manageableTemplates.length === 0 ? (

No Templates Yet

Generate and select templates in the Events section first.

) : visibleTemplates.length === 0 ? (

No templates match "{searchTerm.trim()}".

) : (
{visibleTemplates.map((template) => { const appearance = getCardAppearance(template); const boundProfile = template.curlProfileId ? profilesById[template.curlProfileId] || null : null; const isRuntimeEnabled = template.isRuntimeEnabled !== false; const isRuntimeUpdating = runtimeUpdatingSlug === template.eventSlug; const isBoundProfileMissing = !boundProfile; const isPublished = template.status === 'whitelisted'; return (
{ if (node) { templateCardRefs.current[template.eventSlug] = node; } else { delete templateCardRefs.current[template.eventSlug]; } }} className={`overflow-hidden rounded-[28px] border border-gray-200 border-l-4 bg-white px-6 py-5 shadow-[0_12px_30px_rgba(15,23,42,0.06)] transition-all duration-300 ${appearance.accentClassName} ${ highlightedEventSlug === template.eventSlug ? 'ring-2 ring-primary-blue/30' : 'hover:-translate-y-0.5 hover:shadow-[0_16px_34px_rgba(15,23,42,0.08)]' }`} >

{getTemplateDisplayName(template)}

{isPublished && ( )} {appearance.pillLabel}

{appearance.description}

Profile

{getBoundProfileSummary(template, boundProfile)}

DLT Template ID

{formatDltTemplateId(template.templateId)}

{!isBoundProfileMissing && template.status === 'pending_whitelisting' && ( )} {!isBoundProfileMissing && isPublished && ( )}
{isBoundProfileMissing && (

{template.curlProfileId ? 'The cURL profile used for this template no longer exists. Re-select this template from Events to continue.' : 'This template is not bound to a cURL profile. Re-select it from Events to continue.'}

)}
); })}
)} {whitelistTarget && ( setWhitelistTarget(null)} onSuccess={handleWhitelistSuccess} /> )} {testTarget && ( setTestTarget(null)} /> )} {workspaceSlug && ( setWorkspaceSlug('')} onRequestPublish={(template) => setWhitelistTarget(template)} onRequestTest={(template) => setTestTarget(template)} /> )}
); }