diff --git a/client/src/pages/Events.jsx b/client/src/pages/Events.jsx index ad66558..d980c1d 100644 --- a/client/src/pages/Events.jsx +++ b/client/src/pages/Events.jsx @@ -2,6 +2,81 @@ 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([]); @@ -11,6 +86,7 @@ export default function Events() { 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); @@ -18,12 +94,20 @@ export default function Events() { const loadEvents = useCallback(async () => { setLoading(true); try { - const [eventsRes, activeProfileRes] = await Promise.all([ + 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 { @@ -66,27 +150,82 @@ export default function Events() { setError('Configure and activate a cURL profile before generating templates.'); return; } - setGenState(s => ({ ...s, [slug]: 'loading' })); + + setGenState((state) => ({ ...state, [slug]: 'loading' })); setError(''); + try { const res = await apiClient.post(`/api/businesses/${businessId}/events/${slug}/generate`); - setVariants(v => ({ ...v, [slug]: res.data.variants })); - setGenState(s => ({ ...s, [slug]: 'done' })); + 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(s => ({ ...s, [slug]: 'error' })); + 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 = `${slug}:${variantIndex}`; + const variantKey = getVariantKey(slug, variantIndex); setSelectingVariantKey(variantKey); setError(''); + try { await apiClient.post(`/api/businesses/${businessId}/templates/${slug}/select`, { selectedVariant: variant }); - // Clear variants display after selection - setVariants(v => ({ ...v, [slug]: [] })); - setGenState(s => ({ ...s, [slug]: 'selected' })); + 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 { @@ -94,6 +233,32 @@ export default function Events() { } } + 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 (
Generate SMS templates for each order event.
Pick a Variant
+Review, edit, and choose a variant
{v}
-