import axios from 'axios'; import { useState, useEffect, useCallback, useRef } from 'react'; import { useParams } from 'react-router-dom'; import apiClient from '../api/client'; import TemplateDetailWorkspaceModal from '../components/TemplateDetailWorkspaceModal'; import { useBusiness } from '../context/BusinessContext'; const MAX_SMS_LENGTH = 160; const DLT_VARIABLE_OPTIONS = [ { label: 'Generic Text', token: '{#var#}' }, { label: 'Numeric', token: '{#numeric#}' }, { label: 'URL', token: '{#url#}' }, { label: 'URL OTT', token: '{#urlott#}' }, { label: 'Callback Number', token: '{#cbn#}' }, { label: 'Email', token: '{#email#}' }, { label: 'Alphanumeric', token: '{#alphanumeric#}' }, ]; const SUPPORTED_DLT_VARIABLE_OPTIONS = DLT_VARIABLE_OPTIONS; const DLT_TOKEN_SET = new Set(SUPPORTED_DLT_VARIABLE_OPTIONS.map((option) => option.token)); const DLT_TOKEN_REGEX = /\{#(?:var|numeric|url|urlott|cbn|email|alphanumeric)#\}/g; const DLT_TOKEN_LIKE_REGEX = /\{#[^{}]*#\}/g; const ORDER_PAYMENT_EVENT_SLUGS = [ 'placed', 'payment_failed', ]; const DELIVERY_EVENT_SLUGS = [ 'out_for_delivery', 'delivery_attempt_failed', 'delivery_done', ]; const CANCELLATION_EVENT_SLUGS = [ 'cancelled_at_dp', 'cancelled_customer', 'rejected_by_customer', ]; const RETURN_EVENT_SLUGS = [ 'return_initiated', 'return_bag_picked', 'return_bag_delivered', ]; const REFUND_EVENT_SLUGS = [ 'refund_completed', 'refund_failed', 'refund_initiated', ]; const CUSTOMER_EVENT_SECTIONS = [ { id: 'order_payment', label: 'Order & Payment', description: 'Core order confirmation and critical payment updates customers genuinely care about.', slugs: ORDER_PAYMENT_EVENT_SLUGS, }, { id: 'delivery', label: 'Delivery Journey', description: 'The moments that matter most once an order is close to the doorstep.', slugs: DELIVERY_EVENT_SLUGS, }, { id: 'cancellations', label: 'Cancellations & Rejections', description: 'Critical order-stop events that customers should be notified about immediately.', slugs: CANCELLATION_EVENT_SLUGS, }, { id: 'returns_refunds', label: 'Returns & Refunds', description: 'Only the key return and refund milestones worth notifying customers about.', slugs: [...RETURN_EVENT_SLUGS, ...REFUND_EVENT_SLUGS], }, { id: 'custom', label: 'Custom Events', description: 'Business-specific events you added manually for your own messaging flows.', slugs: [], }, ]; const CUSTOMER_EVENT_SECTION_BY_SLUG = new Map( CUSTOMER_EVENT_SECTIONS.flatMap((section) => section.slugs.map((slug) => [slug, section.id])), ); const CUSTOMER_EVENT_SECTION_ORDER = CUSTOMER_EVENT_SECTIONS.reduce((acc, section) => { acc[section.id] = new Map(section.slugs.map((slug, index) => [slug, index])); return acc; }, {}); const EVENT_TEMPLATE_STATUS_CONFIG = { unselected: { label: 'Not Selected', badge: 'border-gray-200 bg-white text-gray-500', dot: 'bg-gray-400', }, pending_whitelisting: { label: 'Pending Whitelisting', badge: 'border-amber-200 bg-amber-50 text-amber-700', dot: 'bg-amber-500', }, whitelisted: { label: 'Published', badge: 'border-emerald-200 bg-emerald-50 text-emerald-700', dot: 'bg-emerald-500', }, }; function normalizeTemplateStatus(status) { return status === 'whitelisted' ? 'whitelisted' : 'pending_whitelisting'; } function buildSelectedTemplatePreview(template = {}) { const selectedTemplate = String(template?.selectedTemplate || '').trim(); if (!selectedTemplate) return null; return { eventSlug: String(template?.eventSlug || '').trim(), selectedTemplate, status: normalizeTemplateStatus(template?.status), templateId: String(template?.templateId || '').trim(), variableMap: template?.variableMap && typeof template.variableMap === 'object' ? template.variableMap : {}, curlProfileId: String(template?.curlProfileId || '').trim(), }; } function getCustomerFacingSectionId(event) { const slug = String(event?.slug || ''); if (!event?.isDefault) return 'custom'; return CUSTOMER_EVENT_SECTION_BY_SLUG.get(slug) || null; } function matchesEventSearch(event, searchTerm) { const query = String(searchTerm || '').trim().toLowerCase(); if (!query) return true; return [event?.label, event?.slug] .filter(Boolean) .some((value) => String(value).toLowerCase().includes(query)); } function sortSectionEvents(sectionId, events) { if (sectionId === 'custom') { return [...events].sort((left, right) => String(left?.label || '').localeCompare(String(right?.label || ''))); } const orderMap = CUSTOMER_EVENT_SECTION_ORDER[sectionId] || new Map(); return [...events].sort((left, right) => { const leftRank = orderMap.get(String(left?.slug || '')) ?? Number.MAX_SAFE_INTEGER; const rightRank = orderMap.get(String(right?.slug || '')) ?? Number.MAX_SAFE_INTEGER; if (leftRank !== rightRank) return leftRank - rightRank; return String(left?.label || '').localeCompare(String(right?.label || '')); }); } function buildVisibleEventSections(events, searchTerm) { return CUSTOMER_EVENT_SECTIONS.map((section) => { const filteredEvents = events.filter((event) => ( getCustomerFacingSectionId(event) === section.id && matchesEventSearch(event, searchTerm) )); return { ...section, events: sortSectionEvents(section.id, filteredEvents), }; }).filter((section) => section.events.length > 0); } function getVariantKey(slug, index) { return `${slug}:${index}`; } function getSelectedDraftKey(slug) { return `${slug}:selected`; } function getDraftSuffix(draftKey = '') { const separatorIndex = String(draftKey).lastIndexOf(':'); return separatorIndex === -1 ? '' : String(draftKey).slice(separatorIndex + 1); } function getVariantIndexFromDraftKey(draftKey = '') { const suffix = getDraftSuffix(draftKey); if (!suffix) return null; if (suffix === 'selected') return null; const index = Number(suffix); return Number.isInteger(index) ? index : null; } function resolveDraftBaseText(slug, draftKey, variantsBySlug, selectedTemplateBySlug) { if (!draftKey) return ''; const draftSuffix = getDraftSuffix(draftKey); if (draftSuffix === 'selected') { return String(selectedTemplateBySlug?.[slug]?.selectedTemplate || ''); } const variantIndex = Number(draftSuffix); if (!Number.isInteger(variantIndex)) return ''; return String(variantsBySlug?.[slug]?.[variantIndex] || ''); } function createVariantDraft(text = '') { return { originalText: text, currentText: text, validationStatus: 'idle', why: '', lastCheckedText: '', }; } function getDltTokens(text = '') { return String(text).match(DLT_TOKEN_REGEX) || []; } function countDltTokens(text = '') { return getDltTokens(text).length; } function getInvalidDltTokens(text = '') { return (String(text).match(DLT_TOKEN_LIKE_REGEX) || []).filter((token) => !DLT_TOKEN_SET.has(token)); } function hasMalformedDltFragments(text = '') { const strippedText = String(text).replace(DLT_TOKEN_LIKE_REGEX, ''); return strippedText.includes('{#') || strippedText.includes('#}'); } 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 = {}; const nextTemplateStatusBySlug = {}; const nextSelectedTemplateBySlug = {}; templates.forEach((template) => { if (!template?.eventSlug) return; if (template.selectedTemplate) { const normalizedStatus = normalizeTemplateStatus(template.status); nextTemplateStatusBySlug[template.eventSlug] = normalizedStatus; nextGenState[template.eventSlug] = 'selected'; nextSelectedTemplateBySlug[template.eventSlug] = buildSelectedTemplatePreview(template); } if (Array.isArray(template.generatedVariants) && template.generatedVariants.length > 0) { nextVariants[template.eventSlug] = template.generatedVariants; if (!template.selectedTemplate) nextGenState[template.eventSlug] = 'done'; } }); return { nextVariants, nextGenState, nextTemplateStatusBySlug, nextSelectedTemplateBySlug }; } function TemplateGenerationWorkspaceModal({ eventSlug, eventLabel, statusConfig, loading, generatedVariants, selectedTemplatePreview, activeDraftKey, activeDraft, openVariableMenuKey, onClose, onChooseVariant, onDraftChange, onTrackSelection, onToggleVariableMenu, onInsertVariable, onRevert, onCheck, onSelect, onRegenerate, setVariableMenuRef, setTextareaRef, workspaceError, selectingDraftKey, showClosePrompt, closePromptTitle, closePromptDescription, discardingWorkspace, onKeepWorkspace, onDiscardWorkspace, }) { const currentText = activeDraft?.currentText || ''; const originalText = activeDraft?.originalText || ''; const validationStatus = activeDraft?.validationStatus || 'idle'; const currentMatchesCheckedText = activeDraft?.lastCheckedText === currentText; const isEdited = currentText !== originalText; const dltTokenCount = countDltTokens(currentText); const invalidDltTokens = getInvalidDltTokens(currentText); const hasMalformedDltToken = hasMalformedDltFragments(currentText); const hasInvalidPlaceholder = invalidDltTokens.length > 0 || hasMalformedDltToken; const tooLong = currentText.length > MAX_SMS_LENGTH; const hasChosenDraft = !!activeDraftKey; const canRunCheck = hasChosenDraft && !tooLong && !hasInvalidPlaceholder && validationStatus !== 'checking'; const failedCurrentCheck = validationStatus === 'rejected' && currentMatchesCheckedText; const canSelectDraft = hasChosenDraft && !tooLong && !hasInvalidPlaceholder && !failedCurrentCheck && (!isEdited || (validationStatus === 'approved' && currentMatchesCheckedText)); const canInsertVariable = hasChosenDraft; const activeVariantIndex = getVariantIndexFromDraftKey(activeDraftKey); return (

{eventLabel}

{statusConfig && ( {statusConfig.label} )}
{loading ? (
) : ( <>
{workspaceError && (
{workspaceError}
)} {generatedVariants.length > 0 ? (

Variants

{generatedVariants.length} option{generatedVariants.length === 1 ? '' : 's'}
{generatedVariants.map((variant, index) => { const variantKey = getVariantKey(eventSlug, index); const isActive = activeVariantIndex === index; return ( ); })}
) : selectedTemplatePreview ? (
{selectedTemplatePreview.templateId ? ( Template ID {selectedTemplatePreview.templateId} ) : ( Template ID pending )}
) : null}
{hasChosenDraft ? (
{getDraftSuffix(activeDraftKey) === 'selected' ? 'Current Template' : `Variant ${activeVariantIndex + 1}`} {isEdited ? 'Edited Draft' : 'Working Draft'} {validationStatus === 'checking' && ( Checking )} {validationStatus === 'approved' && currentMatchesCheckedText && ( Passed )} {validationStatus === 'rejected' && currentMatchesCheckedText && ( Needs changes )}
{openVariableMenuKey === activeDraftKey && (

Insert variable

{DLT_VARIABLE_OPTIONS.map((option) => ( ))}
)}