import axios from 'axios'; import { useState, useEffect, useCallback, useRef } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import apiClient from '../api/client'; 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 DELIVERY_EVENT_SLUGS = new Set([ 'out_for_pickup', 'bag_picked', 'bag_reached_drop_point', 'in_transit', 'out_for_delivery', 'delivery_attempt_failed', 'delivery_done', 'handed_over_to_customer', 'bag_lost', ]); const CANCELLATION_EVENT_SLUGS = new Set([ 'bag_not_confirmed', 'cancelled_at_dp', 'cancelled_customer', 'cancelled_failed_at_dp', 'cancelled_fynd', 'rejected_by_customer', ]); const REFUND_EVENT_SLUGS = new Set([ 'credit_note_generated', 'partial_refund_completed', 'refund_acknowledged', 'refund_approved', 'refund_completed', 'refund_failed', 'refund_initiated', 'refund_on_hold', 'refund_pending', 'refund_pending_for_approval', 'refund_retry', ]); const RETURN_EVENT_SLUGS = new Set([ 'assigning_return_dp', 'internal_return_dp_reassign', 'deadstock_defective', 'deadstock_defective_lost', ]); const EVENT_GROUPS = [ { id: 'fulfillment', label: 'Order & Fulfillment', description: 'Core order confirmation, allocation, packing, and dispatch readiness stages.', defaultExpanded: false, }, { id: 'delivery', label: 'Delivery Journey', description: 'Courier pickup, in-transit updates, and final handover milestones.', defaultExpanded: false, }, { id: 'cancellations', label: 'Cancellations & Rejections', description: 'Customer, merchant, and delivery-partner driven cancellations and rejections.', defaultExpanded: false, }, { id: 'returns', label: 'Returns', description: 'Return initiation, pickup, transit, and merchant-side return handling.', defaultExpanded: false, }, { id: 'refunds', label: 'Refunds', description: 'Refund processing and credit-note states across payment flows.', defaultExpanded: false, }, { id: 'rto', label: 'RTO', description: 'Return-to-origin movement and completion states after failed delivery.', defaultExpanded: false, }, { id: 'custom', label: 'Custom Events', description: 'Business-specific events you added manually for your own messaging flows.', defaultExpanded: false, }, ]; const DEFAULT_EXPANDED_GROUPS = EVENT_GROUPS.reduce((acc, group) => { acc[group.id] = group.defaultExpanded; 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', }, }; const EVENT_GROUP_STYLE_CONFIG = { fulfillment: { markerShell: 'border-slate-200 bg-slate-50', markerDot: 'bg-slate-500', }, delivery: { markerShell: 'border-sky-200 bg-sky-50', markerDot: 'bg-sky-500', }, cancellations: { markerShell: 'border-rose-200 bg-rose-50', markerDot: 'bg-rose-500', }, returns: { markerShell: 'border-indigo-200 bg-indigo-50', markerDot: 'bg-indigo-500', }, refunds: { markerShell: 'border-emerald-200 bg-emerald-50', markerDot: 'bg-emerald-500', }, rto: { markerShell: 'border-fuchsia-200 bg-fuchsia-50', markerDot: 'bg-fuchsia-500', }, custom: { markerShell: 'border-indigo-200 bg-indigo-50', markerDot: 'bg-indigo-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 getEventGroupId(event) { const slug = String(event?.slug || ''); if (!event?.isDefault) return 'custom'; if (slug.startsWith('rto_') || slug === 'return_to_origin') return 'rto'; if (slug.startsWith('return_') || RETURN_EVENT_SLUGS.has(slug)) return 'returns'; if (slug.startsWith('refund_') || REFUND_EVENT_SLUGS.has(slug)) return 'refunds'; if (CANCELLATION_EVENT_SLUGS.has(slug)) return 'cancellations'; if (DELIVERY_EVENT_SLUGS.has(slug)) return 'delivery'; return 'fulfillment'; } 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 buildGroupedEvents(events, searchTerm) { return EVENT_GROUPS.map((group) => ({ ...group, events: events.filter((event) => getEventGroupId(event) === group.id && matchesEventSearch(event, searchTerm)), })).filter((group) => group.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 TemplateWorkspaceModal({ 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) => ( ))}
)}