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}
setError('')} className="font-bold text-error-text hover:text-red-900">
×
)}
{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}
isPublished && handleRuntimeToggle(template)}
className={`relative inline-flex h-7 w-12 items-center rounded-full border transition ${
isPublished
? `${appearance.switchTrackClassName} ${isRuntimeUpdating ? 'cursor-wait opacity-70' : 'cursor-pointer'}`
: 'cursor-not-allowed border-[#d8dee8] bg-[#eef1f5] opacity-95'
}`}
>
Profile
{getBoundProfileSummary(template, boundProfile)}
DLT Template ID
{formatDltTemplateId(template.templateId)}
setWorkspaceSlug(template.eventSlug)}
className="text-sm font-semibold text-primary-blue transition hover:text-primary-dark"
>
View Template →
{!isBoundProfileMissing && template.status === 'pending_whitelisting' && (
setWhitelistTarget(template)}
className="rounded-full border border-orange-200 bg-[#fff4ea] px-4 py-2 text-sm font-semibold text-orange-700 transition hover:border-orange-300 hover:bg-[#ffeddc]"
>
Publish
)}
{!isBoundProfileMissing && isPublished && (
setTestTarget(template)}
className="rounded-full border border-[#c7d6ff] bg-[#f5f8ff] px-4 py-2 text-sm font-semibold text-[#4563d5] transition hover:border-[#afc3ff] hover:bg-[#ebf1ff]"
>
Test SMS
)}
{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)}
/>
)}
);
}