From ff864a965d756e061b33f3178a42de6e2e2be4d9 Mon Sep 17 00:00:00 2001 From: Ritul Date: Mon, 30 Mar 2026 16:30:17 +0530 Subject: [PATCH] Sidebar better visuals for stepper and resolving template on code level instead of llm --- client/src/components/Sidebar.jsx | 106 ++++++++++-------- server/routes/businesses.js | 171 +++++++++++++++++++++++++++++- 2 files changed, 226 insertions(+), 51 deletions(-) diff --git a/client/src/components/Sidebar.jsx b/client/src/components/Sidebar.jsx index 726c0ec..3c9881b 100644 --- a/client/src/components/Sidebar.jsx +++ b/client/src/components/Sidebar.jsx @@ -69,6 +69,7 @@ export default function Sidebar() { id: 'globalSms', to: globalSmsPath, label: 'Omni-channel SMS', + enabled: true, done: isSetupComplete && !isGlobalSmsRoute, active: isGlobalSmsRoute, expanded: isGlobalSmsRoute, @@ -78,6 +79,7 @@ export default function Sidebar() { id: 'events', to: eventsPath, label: 'Events', + enabled: isSetupComplete, done: hasSelectedTemplates && !isEventsRoute, active: isEventsRoute, }, @@ -85,6 +87,7 @@ export default function Sidebar() { id: 'templates', to: templatesPath, label: 'Templates', + enabled: hasSelectedTemplates, done: false, active: isTemplatesRoute, }, @@ -124,56 +127,65 @@ export default function Sidebar() {
{stepItems.map((item) => (
- - {SVG_ICONS[item.id]} - {item.label} -
- - {item.substeps && ( - - - - )} + {item.enabled ? ( + + {SVG_ICONS[item.id]} + {item.label} +
+ {!item.substeps && } + {item.substeps && ( + + + + )} +
+
+ ) : ( +
+ {SVG_ICONS[item.id]} + {item.label} +
+ {!item.substeps && } +
- + )} {item.expanded && item.substeps && ( -
- {item.substeps.map((substep) => ( -
- - {substep.label} -
- ))} +
+
+
+ {item.substeps.map((substep) => ( +
+
+ {substep.active &&
} +
+
+ {substep.label} +
+
+ ))} +
)}
diff --git a/server/routes/businesses.js b/server/routes/businesses.js index 603dd7d..952f012 100644 --- a/server/routes/businesses.js +++ b/server/routes/businesses.js @@ -200,6 +200,163 @@ function getShipmentToNumber(body) { return normalizeText(body?.payload?.shipment?.user?.mobile); } +const DLT_PLACEHOLDER_REGEX = /\{#(?:var|numeric|url|cbn)#\}/g; + +function normalizeRenderableValue(value) { + if (typeof value === 'string') return value.replace(/\s+/g, ' ').trim(); + if (typeof value === 'number' && Number.isFinite(value)) return String(value); + if (typeof value === 'boolean') return value ? 'true' : 'false'; + return ''; +} + +function toCamelCase(text) { + return String(text || '') + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + .replace(/[^a-zA-Z0-9]+/g, ' ') + .trim() + .split(/\s+/) + .filter(Boolean) + .map((part, index) => { + const lower = part.toLowerCase(); + return index === 0 ? lower : lower.charAt(0).toUpperCase() + lower.slice(1); + }) + .join(''); +} + +function setValueIndexEntry(valueIndex, key, value) { + if (!key || valueIndex.has(key)) return; + valueIndex.set(key, value); +} + +function indexShipmentValues(value, pathParts = [], valueIndex = new Map()) { + if (Array.isArray(value)) { + value.forEach((entry) => indexShipmentValues(entry, pathParts, valueIndex)); + return valueIndex; + } + + if (value && typeof value === 'object') { + Object.entries(value).forEach(([key, entry]) => { + indexShipmentValues(entry, [...pathParts, key], valueIndex); + }); + return valueIndex; + } + + const normalizedValue = normalizeRenderableValue(value); + if (!normalizedValue || pathParts.length === 0) return valueIndex; + + const leafKey = toCamelCase(pathParts[pathParts.length - 1]); + const fullKey = toCamelCase(pathParts.join(' ')); + setValueIndexEntry(valueIndex, leafKey, normalizedValue); + setValueIndexEntry(valueIndex, fullKey, normalizedValue); + + return valueIndex; +} + +function buildShipmentValueIndex(shipment) { + const valueIndex = indexShipmentValues(shipment); + const firstBag = shipment?.bags?.[0] || {}; + const primaryTrackingUrl = normalizeRenderableValue( + shipment?.delivery_partner_details?.track_url + || shipment?.meta?.tracking_url + || firstBag?.meta?.tracking_url + || shipment?.affiliate_details?.shipment_meta?.tracking_url + || shipment?.article_details?.dp_details?.track_url + ); + const primaryAwbNumber = normalizeRenderableValue( + shipment?.delivery_partner_details?.awb_no + || shipment?.meta?.awb_number + || shipment?.article_details?.dp_details?.awb_no + ); + const primaryCourierName = normalizeRenderableValue( + shipment?.delivery_partner_details?.display_name + || shipment?.delivery_partner_details?.name + || shipment?.meta?.courier_partner_name + || shipment?.meta?.dp_name + || firstBag?.meta?.dp_name + ); + const brandName = normalizeRenderableValue( + shipment?.bags?.[0]?.brand?.brand_name + || shipment?.bags?.[0]?.item?.attributes?.brand_name + || shipment?.affiliate_details?.company_affiliate_tag + ); + + setValueIndexEntry(valueIndex, 'firstName', normalizeRenderableValue(shipment?.user?.first_name)); + setValueIndexEntry(valueIndex, 'lastName', normalizeRenderableValue(shipment?.user?.last_name)); + setValueIndexEntry(valueIndex, 'fullName', normalizeRenderableValue( + `${normalizeRenderableValue(shipment?.user?.first_name)} ${normalizeRenderableValue(shipment?.user?.last_name)}`.trim() + )); + setValueIndexEntry(valueIndex, 'phone', normalizeRenderableValue(shipment?.user?.mobile || shipment?.delivery_address?.phone)); + setValueIndexEntry(valueIndex, 'mobile', normalizeRenderableValue(shipment?.user?.mobile || shipment?.delivery_address?.phone)); + setValueIndexEntry(valueIndex, 'toNumber', normalizeRenderableValue(shipment?.user?.mobile || shipment?.delivery_address?.phone)); + setValueIndexEntry(valueIndex, 'orderId', normalizeRenderableValue(shipment?.order_id)); + setValueIndexEntry(valueIndex, 'shipmentId', normalizeRenderableValue(shipment?.shipment_id)); + setValueIndexEntry(valueIndex, 'event', normalizeRenderableValue(shipment?.status)); + setValueIndexEntry(valueIndex, 'status', normalizeRenderableValue(shipment?.status)); + setValueIndexEntry(valueIndex, 'eventDisplayName', normalizeRenderableValue(shipment?.shipment_status?.display_name)); + setValueIndexEntry(valueIndex, 'displayName', normalizeRenderableValue(shipment?.shipment_status?.display_name)); + setValueIndexEntry(valueIndex, 'brandName', brandName); + setValueIndexEntry(valueIndex, 'trackingUrl', primaryTrackingUrl); + setValueIndexEntry(valueIndex, 'trackUrl', primaryTrackingUrl); + setValueIndexEntry(valueIndex, 'url', primaryTrackingUrl); + setValueIndexEntry(valueIndex, 'awbNo', primaryAwbNumber); + setValueIndexEntry(valueIndex, 'awbNumber', primaryAwbNumber); + setValueIndexEntry(valueIndex, 'dpName', primaryCourierName); + setValueIndexEntry(valueIndex, 'courierName', primaryCourierName); + setValueIndexEntry(valueIndex, 'deliveryPartnerName', primaryCourierName); + + return valueIndex; +} + +function validateRenderedPlaceholderValue(token, value, fieldName) { + if (!value) { + throw createHttpError(422, `No shipment value found for placeholder field "${fieldName}"`); + } + + if (token === '{#numeric#}' && !/^\d+$/.test(value)) { + throw createHttpError(422, `Field "${fieldName}" resolved to a non-numeric value for ${token}`); + } + + if (token === '{#url#}' && !/^https?:\/\//i.test(value)) { + throw createHttpError(422, `Field "${fieldName}" resolved to an invalid URL for ${token}`); + } + + if (token === '{#cbn#}' && !/^\+?[0-9][0-9\s-]{5,}$/.test(value)) { + throw createHttpError(422, `Field "${fieldName}" resolved to an invalid callback number for ${token}`); + } +} + +function renderShipmentTemplate(template, shipment, variableMap = {}) { + const normalizedTemplate = normalizeText(template); + const placeholderMatches = normalizedTemplate.match(DLT_PLACEHOLDER_REGEX) || []; + + if (placeholderMatches.length === 0) { + return normalizedTemplate; + } + + if (!variableMap || typeof variableMap !== 'object' || Object.keys(variableMap).length === 0) { + throw createHttpError(422, 'Template has placeholders but no variableMap was found on the stored template'); + } + + const shipmentValueIndex = buildShipmentValueIndex(shipment); + let placeholderIndex = 0; + + return normalizedTemplate.replace(DLT_PLACEHOLDER_REGEX, (token) => { + const mappingKey = `${token}[${placeholderIndex}]`; + const mappedFieldName = normalizeText(variableMap[mappingKey]); + + if (!mappedFieldName) { + throw createHttpError(422, `No variable mapping found for placeholder ${mappingKey}`, { + details: { mappingKey, variableMap }, + }); + } + + const resolvedValue = shipmentValueIndex.get(toCamelCase(mappedFieldName)) || ''; + validateRenderedPlaceholderValue(token, resolvedValue, mappedFieldName); + placeholderIndex += 1; + return resolvedValue; + }); +} + function parseWorkflowPayload(data) { if (typeof data === 'string') { const trimmed = data.trim(); @@ -214,7 +371,7 @@ function parseWorkflowPayload(data) { return data && typeof data === 'object' ? data : {}; } -async function sendResolveTemplateWorkflow({ template, toNumber, sourcePayload }) { +async function sendResolveTemplateWorkflow({ content, toNumber }) { const workflowUrl = normalizeText(process.env.WORKFLOW_URL_RESOLVE_TEMPLATE); if (!workflowUrl) { throw createHttpError(500, 'WORKFLOW_URL_RESOLVE_TEMPLATE is not configured'); @@ -222,7 +379,7 @@ async function sendResolveTemplateWorkflow({ template, toNumber, sourcePayload } const response = await axios.post( workflowUrl, - { template, toNumber, sourcePayload }, + { content, toNumber }, { timeout: 30000, headers: { 'Content-Type': 'application/json' }, @@ -589,10 +746,15 @@ router.post('/resolve-template', async (req, res) => { return res.status(404).json({ error: 'Whitelisted template not found' }); } + const resolvedTemplate = renderShipmentTemplate( + tmpl.selectedTemplate, + shipment, + tmpl.variableMap || {} + ); + const workflowResult = await sendResolveTemplateWorkflow({ - template: tmpl.selectedTemplate, + content: resolvedTemplate, toNumber, - sourcePayload: req.body, }); res.json({ @@ -603,6 +765,7 @@ router.post('/resolve-template', async (req, res) => { event: eventSlug, templateId: normalizeText(tmpl.templateId), template: tmpl.selectedTemplate, + content: resolvedTemplate, toNumber, workflowResult, });