import { useState, useEffect, useCallback } from 'react'; import { useParams } from 'react-router-dom'; import apiClient from '../api/client'; const MAX_SMS_LENGTH = 160; const PLACEHOLDER_TOKEN = '{#var#}'; const PLACEHOLDER_REGEX = /\{#var#\}/g; function getVariantKey(slug, index) { return `${slug}:${index}`; } function createVariantDraft(text = '') { return { originalText: text, currentText: text, validationStatus: 'idle', why: '', lastCheckedText: '', }; } function countPlaceholders(text = '') { return (String(text).match(PLACEHOLDER_REGEX) || []).length; } function buildDraftsForVariants(slug, generatedVariants = []) { const nextDrafts = {}; generatedVariants.forEach((variant, index) => { nextDrafts[getVariantKey(slug, index)] = createVariantDraft(variant); }); return nextDrafts; } function removeDraftsForSlug(drafts, slug) { const nextDrafts = { ...drafts }; Object.keys(nextDrafts).forEach((key) => { if (key.startsWith(`${slug}:`)) delete nextDrafts[key]; }); return nextDrafts; } function syncDraftsWithVariants(existingDrafts, variantsBySlug) { const nextDrafts = {}; Object.entries(variantsBySlug).forEach(([slug, generatedVariants]) => { generatedVariants.forEach((variant, index) => { const key = getVariantKey(slug, index); const existing = existingDrafts[key]; nextDrafts[key] = existing && existing.originalText === variant ? existing : createVariantDraft(variant); }); }); return nextDrafts; } function buildTemplateUiState(templates = []) { const nextVariants = {}; const nextGenState = {}; templates.forEach((template) => { if (!template?.eventSlug) return; if (template.selectedTemplate) { nextGenState[template.eventSlug] = 'selected'; return; } if (Array.isArray(template.generatedVariants) && template.generatedVariants.length > 0) { nextVariants[template.eventSlug] = template.generatedVariants; nextGenState[template.eventSlug] = 'done'; } }); return { nextVariants, nextGenState }; } export default function Events() { const { businessId } = useParams(); const [events, setEvents] = useState([]); const [loading, setLoading] = useState(true); const [newLabel, setNewLabel] = useState(''); const [addingEvent, setAddingEvent] = useState(false); const [showAddForm, setShowAddForm] = useState(false); const [genState, setGenState] = useState({}); const [variants, setVariants] = useState({}); const [variantDrafts, setVariantDrafts] = useState({}); const [selectingVariantKey, setSelectingVariantKey] = useState(''); const [error, setError] = useState(''); const [readyToGenerate, setReadyToGenerate] = useState(false); const loadEvents = useCallback(async () => { setLoading(true); try { const [eventsRes, activeProfileRes, templatesRes] = await Promise.all([ apiClient.get(`/api/businesses/${businessId}/events`), apiClient.get(`/api/businesses/${businessId}/global-sms/active`).catch(() => ({ data: {} })), apiClient.get(`/api/businesses/${businessId}/templates`).catch(() => ({ data: { templates: [] } })), ]); const templates = templatesRes.data.templates || []; const { nextVariants, nextGenState } = buildTemplateUiState(templates); setEvents(eventsRes.data.events || []); setReadyToGenerate(!!activeProfileRes.data?.activeProfile?.rawCurl); setVariants(nextVariants); setGenState(nextGenState); setVariantDrafts((currentDrafts) => syncDraftsWithVariants(currentDrafts, nextVariants)); } catch { setError('Failed to load events'); } finally { setLoading(false); } }, [businessId]); useEffect(() => { loadEvents(); }, [loadEvents]); async function handleAddEvent(e) { e.preventDefault(); if (!newLabel.trim()) return; setAddingEvent(true); setError(''); try { await apiClient.post(`/api/businesses/${businessId}/events`, { label: newLabel.trim() }); setNewLabel(''); setShowAddForm(false); await loadEvents(); } catch (err) { setError(err.response?.data?.error || 'Failed to add event'); } finally { setAddingEvent(false); } } async function handleDelete(slug) { try { await apiClient.delete(`/api/businesses/${businessId}/events/${slug}`); await loadEvents(); } catch (err) { setError(err.response?.data?.error || 'Failed to delete event'); } } async function handleGenerate(slug) { if (!readyToGenerate) { setError('Configure and activate a cURL profile before generating templates.'); return; } setGenState((state) => ({ ...state, [slug]: 'loading' })); setError(''); try { const res = await apiClient.post(`/api/businesses/${businessId}/events/${slug}/generate`); const generatedVariants = res.data.variants || []; setVariants((currentVariants) => ({ ...currentVariants, [slug]: generatedVariants })); setVariantDrafts((currentDrafts) => ({ ...removeDraftsForSlug(currentDrafts, slug), ...buildDraftsForVariants(slug, generatedVariants), })); setGenState((state) => ({ ...state, [slug]: 'done' })); } catch (err) { setError(err.response?.data?.error || 'Generation failed'); setGenState((state) => ({ ...state, [slug]: 'error' })); } } async function handleValidateEdit(slug, variantIndex) { const variantKey = getVariantKey(slug, variantIndex); const draft = variantDrafts[variantKey]; const editedTemplate = draft?.currentText || ''; if (!editedTemplate) return; setVariantDrafts((currentDrafts) => ({ ...currentDrafts, [variantKey]: { ...(currentDrafts[variantKey] || createVariantDraft(editedTemplate)), validationStatus: 'checking', why: '', lastCheckedText: '', }, })); setError(''); try { const res = await apiClient.post(`/api/businesses/${businessId}/templates/${slug}/validate-edit`, { editedTemplate, }); setVariantDrafts((currentDrafts) => ({ ...currentDrafts, [variantKey]: { ...(currentDrafts[variantKey] || createVariantDraft(editedTemplate)), validationStatus: res.data?.approved ? 'approved' : 'rejected', why: res.data?.why || '', lastCheckedText: editedTemplate, }, })); } catch (err) { setError(err.response?.data?.error || 'Failed to validate edited template'); setVariantDrafts((currentDrafts) => ({ ...currentDrafts, [variantKey]: { ...(currentDrafts[variantKey] || createVariantDraft(editedTemplate)), validationStatus: 'idle', why: '', lastCheckedText: '', }, })); } } async function handleSelect(slug, variant, variantIndex) { const variantKey = getVariantKey(slug, variantIndex); setSelectingVariantKey(variantKey); setError(''); try { await apiClient.post(`/api/businesses/${businessId}/templates/${slug}/select`, { selectedVariant: variant }); setVariants((currentVariants) => ({ ...currentVariants, [slug]: [] })); setVariantDrafts((currentDrafts) => removeDraftsForSlug(currentDrafts, slug)); setGenState((state) => ({ ...state, [slug]: 'selected' })); } catch (err) { setError(err.response?.data?.error || 'Failed to select template'); } finally { setSelectingVariantKey(''); } } function handleVariantChange(slug, variantIndex, nextText) { const variantKey = getVariantKey(slug, variantIndex); const originalText = variantDrafts[variantKey]?.originalText || variants[slug]?.[variantIndex] || ''; setVariantDrafts((currentDrafts) => ({ ...currentDrafts, [variantKey]: { originalText, currentText: nextText, validationStatus: 'idle', why: '', lastCheckedText: '', }, })); } function handleRevertVariant(slug, variantIndex) { const variantKey = getVariantKey(slug, variantIndex); const originalText = variantDrafts[variantKey]?.originalText || variants[slug]?.[variantIndex] || ''; setVariantDrafts((currentDrafts) => ({ ...currentDrafts, [variantKey]: createVariantDraft(originalText), })); } if (loading) { return (
); } return (

Events

Generate SMS templates for each order event.

{!readyToGenerate && (
⚠️ Set up and activate a cURL profile before generating templates.
)} {error && (
{error}
)} {showAddForm && (
setNewLabel(e.target.value)} placeholder="Event name (e.g. Return Initiated)" className="flex-1 px-4 py-2.5 rounded-lg bg-white border border-gray-300 text-gray-900 placeholder-gray-400 font-medium focus:outline-none focus:ring-2 focus:ring-indigo-600 text-sm shadow-sm" autoFocus />
)}
{events.map((event) => { const state = genState[event.slug] || 'idle'; const eventVariants = variants[event.slug] || []; return (
{event.isDefault ? (
) : ( )}

{event.label}

{event.slug}

{state === 'selected' && ( ✓ Template Selected )}
{eventVariants.length > 0 && (

Review, edit, and choose a variant

{eventVariants.map((variant, index) => { const variantKey = getVariantKey(event.slug, index); const draft = variantDrafts[variantKey] || createVariantDraft(variant); const currentText = draft.currentText; const originalText = draft.originalText; const validationStatus = draft.validationStatus; const currentMatchesCheckedText = draft.lastCheckedText === currentText; const isEdited = currentText !== originalText; const placeholderCount = countPlaceholders(currentText); const originalPlaceholderCount = countPlaceholders(originalText); const placeholderMismatch = placeholderCount !== originalPlaceholderCount; const tooLong = currentText.length > MAX_SMS_LENGTH; const isSelectingThis = selectingVariantKey === variantKey; const isSelectingAnotherVariant = !!selectingVariantKey && selectingVariantKey !== variantKey && selectingVariantKey.startsWith(`${event.slug}:`); const canRunCheck = isEdited && !tooLong && !placeholderMismatch && validationStatus !== 'checking'; const canUseEdited = isEdited && validationStatus === 'approved' && currentMatchesCheckedText && !tooLong && !placeholderMismatch; return (
{isEdited ? 'Edited Draft' : 'Original Draft'} {validationStatus === 'checking' && ( Checking edit… )} {validationStatus === 'approved' && currentMatchesCheckedText && ( Edit passed check )} {validationStatus === 'rejected' && currentMatchesCheckedText && ( Needs changes )}