bolt-templates-sms-extensio.../client/src/pages/Templates.jsx
2026-03-26 14:19:26 +05:30

218 lines
10 KiB
JavaScript

import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import apiClient from '../api/client';
import WhitelistModal from '../components/WhitelistModal';
import TestSmsModal from '../components/TestSmsModal';
const STATUS_CONFIG = {
generated: { label: 'Generated', bg: 'bg-page-bg', text: 'text-text-muted', border: 'border-border-main' },
pending_whitelisting: { label: 'Pending Whitelisting', bg: 'bg-tags-bg', text: 'text-tags-text', border: 'border-tags-border' },
whitelisted: { label: 'Published', bg: 'bg-badge-bg', text: 'text-badge-text', border: 'border-badge-border' },
};
export default function Templates() {
const { businessId } = useParams();
const [templates, setTemplates] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [whitelistTarget, setWhitelistTarget] = useState(null);
const [testTarget, setTestTarget] = useState(null);
const [activeTab, setActiveTab] = useState('published'); // 'published' | 'pending'
async function loadTemplates() {
setLoading(true);
try {
const res = await apiClient.get(`/api/businesses/${businessId}/templates`);
// Show all templates that have a selected template (status != generated or status exists)
const all = (res.data.templates || []).filter(t => t.selectedTemplate);
setTemplates(all);
} catch {
setError('Failed to load templates');
} finally {
setLoading(false);
}
}
useEffect(() => { loadTemplates(); }, [businessId]);
function handleWhitelistSuccess(slug, templateId) {
setTemplates(ts => ts.map(t =>
t.eventSlug === slug ? { ...t, status: 'whitelisted', templateId } : t
));
setWhitelistTarget(null);
}
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="w-8 h-8 border-2 border-indigo-200 border-t-indigo-600 rounded-full animate-spin" />
</div>
);
}
return (
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="pb-5 mb-6 border-b border-gray-200">
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">Templates</h1>
<p className="text-sm text-gray-500 mt-1 font-medium">Track whitelisting status and test your SMS templates.</p>
</div>
{error && (
<div className="mb-6 px-4 py-3 rounded-md bg-delayed-bg border border-delayed-border text-error-text font-medium text-sm flex items-center justify-between">
{error}
<button onClick={() => setError('')} className="text-error-text hover:text-red-900 font-bold">&times;</button>
</div>
)}
{/* Tabs */}
<div className="flex space-x-4 mb-6 border-b border-border-main">
<button
onClick={() => setActiveTab('published')}
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
activeTab === 'published'
? 'border-primary-blue text-primary-dark'
: 'border-transparent text-text-muted hover:text-text-primary hover:border-border-main'
}`}
>
Published
</button>
<button
onClick={() => setActiveTab('pending')}
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
activeTab === 'pending'
? 'border-primary-blue text-primary-dark'
: 'border-transparent text-text-muted hover:text-text-primary hover:border-border-main'
}`}
>
Pending Whitelisting
</button>
</div>
{templates.length === 0 ? (
<div className="text-center py-16 bg-surface-white border border-border-main rounded-xl shadow-sm">
<div className="w-16 h-16 rounded-full bg-page-bg flex items-center justify-center mx-auto mb-4 border border-border-soft">
<svg className="w-8 h-8 text-text-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
</div>
<h3 className="text-lg font-bold text-text-primary">No Templates Yet</h3>
<p className="text-text-muted text-sm mt-2 font-medium">Generate and select templates in the Events section first.</p>
</div>
) : (() => {
const publishedTabs = templates.filter(t => t.status === 'whitelisted');
const pendingTabs = templates.filter(t => t.status === 'pending_whitelisting');
const visibleTemplates = activeTab === 'published' ? publishedTabs : pendingTabs;
if (visibleTemplates.length === 0) {
return (
<div className="text-center py-12 bg-surface-white border border-border-dashed rounded-xl">
<p className="text-text-muted text-sm font-medium">No templates in {activeTab === 'published' ? 'Published' : 'Pending'}.</p>
</div>
);
}
return (
<div className="space-y-4">
{visibleTemplates.map(tmpl => {
const statusCfg = STATUS_CONFIG[tmpl.status] || STATUS_CONFIG.generated;
return (
<div key={tmpl.eventSlug} className="rounded-xl bg-white border border-gray-200 shadow-sm overflow-hidden">
{/* Card header */}
<div className="px-6 py-4 border-b border-gray-100 bg-gray-50/50 flex items-center justify-between">
<div>
<h3 className="text-base font-bold text-gray-900 capitalize tracking-tight">
{tmpl.eventLabel || tmpl.eventSlug.replace(/_/g, ' ')}
</h3>
<p className="text-xs text-gray-500 font-mono mt-0.5">{tmpl.eventSlug}</p>
</div>
<span className={`px-3 py-1 rounded-full text-xs font-bold border ${statusCfg.bg} ${statusCfg.text} ${statusCfg.border}`}>
{statusCfg.label}
</span>
</div>
<div className="p-6 space-y-4">
{/* Template text */}
<div>
<label className="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">Selected Template</label>
<div className="p-4 rounded-lg bg-gray-50 border border-gray-200 font-mono text-sm text-gray-800 leading-relaxed break-words">
{tmpl.selectedTemplate}
</div>
</div>
{/* Template ID (if whitelisted) */}
{tmpl.templateId && (
<div>
<label className="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">DLT Template ID</label>
<p className="font-mono text-sm text-indigo-700 bg-indigo-50 border border-indigo-100 px-3 py-2 rounded-lg inline-block">
{tmpl.templateId}
</p>
</div>
)}
{/* Variable map */}
{tmpl.variableMap && Object.keys(tmpl.variableMap).length > 0 && (
<div>
<label className="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">Variable Mappings</label>
<div className="flex flex-wrap gap-2">
{Object.entries(tmpl.variableMap).map(([key, val]) => (
<div key={key} className="flex items-center gap-2 text-xs bg-gray-50 border border-gray-200 rounded-md px-3 py-1.5">
<span className="font-mono text-indigo-700 font-bold">{key}</span>
<span className="text-gray-400"></span>
<span className="font-medium text-gray-700">{val}</span>
</div>
))}
</div>
</div>
)}
{/* Actions */}
<div className="flex items-center gap-3 pt-2">
{tmpl.status === 'pending_whitelisting' && (
<button
onClick={() => setWhitelistTarget(tmpl)}
className="px-4 py-2 rounded-lg bg-orange-text hover:bg-[#c97b45] text-white text-sm font-semibold transition shadow-sm"
>
Publish
</button>
)}
{tmpl.status === 'whitelisted' && (
<button
onClick={() => setTestTarget(tmpl)}
className="px-4 py-2 rounded-lg bg-primary-blue hover:bg-primary-dark text-white text-sm font-semibold transition shadow-sm flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
Test SMS
</button>
)}
{tmpl.status === 'pending_whitelisting' && (
<p className="text-xs text-text-muted font-medium">Submit to DLT portal, then enter your Template ID here.</p>
)}
</div>
</div>
</div>
);
})}
</div>
);
})()}
{whitelistTarget && (
<WhitelistModal
businessId={businessId}
template={whitelistTarget}
onClose={() => setWhitelistTarget(null)}
onSuccess={handleWhitelistSuccess}
/>
)}
{testTarget && (
<TestSmsModal
businessId={businessId}
template={testTarget}
onClose={() => setTestTarget(null)}
/>
)}
</div>
);
}