From ce1e7a5bc8fdb2b12e7665365d273c2f167de90d Mon Sep 17 00:00:00 2001 From: Ramkrishna-egov Date: Tue, 30 Sep 2025 15:04:47 +0530 Subject: [PATCH 1/8] Patch of Navigation and Dependent Fields Changes --- .../AppConfigurationWrapper.js | 7 +- .../AppFieldScreenWrapper.js | 26 +- .../DependentFieldsWrapper.js | 964 +++++++++++++++ .../DrawerFieldComposer.js | 5 +- .../NavigationLogicWrapper.js | 1096 +++++++++++++++++ .../RenderConditionalField.js | 17 +- .../appConfigurationRedesign/useCustomT.js | 54 +- 7 files changed, 2139 insertions(+), 30 deletions(-) create mode 100644 health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/DependentFieldsWrapper.js create mode 100644 health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/NavigationLogicWrapper.js diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationWrapper.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationWrapper.js index fa26458f268..f97cd53933a 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationWrapper.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationWrapper.js @@ -288,6 +288,7 @@ const reducer = (state = initialState, action, updateLocalization) => { const MODULE_CONSTANTS = "HCM-ADMIN-CONSOLE"; function AppConfigurationWrapper({ screenConfig, localeModule, pageTag }) { + const useT = useCustomT(); const queryClient = useQueryClient(); const { locState, addMissingKey, updateLocalization, onSubmit, back, showBack, parentDispatch } = useAppLocalisationContext(); const [state, dispatch] = useReducer((state, action) => reducer(state, action, updateLocalization), initialState); @@ -298,7 +299,7 @@ function AppConfigurationWrapper({ screenConfig, localeModule, pageTag }) { const [popupData, setPopupData] = useState(null); const [addFieldData, setAddFieldData] = useState(null); const addFieldDataLabel = useMemo(() => { - return addFieldData?.label ? useCustomT(addFieldData?.label) : null; + return addFieldData?.label ? useT(addFieldData?.label) : null; }, [addFieldData]); const searchParams = new URLSearchParams(location.search); const fieldMasterName = searchParams.get("fieldType"); @@ -554,7 +555,7 @@ function AppConfigurationWrapper({ screenConfig, localeModule, pageTag }) { return ( {loading && } - +
+ { + dispatch({ + type: "PATCH_PAGE_CONDITIONAL_NAV", + pageName: currentCard?.name, // optional but safer + data, // array built by NavigationLogicWrapper on Submit + }); + }} + />
{`${t("APP_CONFIG_ACTION_BUTTON_LABEL")}`} @@ -203,7 +217,7 @@ function AppFieldScreenWrapper() { { updateLocalization( currentCard?.actionLabel && currentCard?.actionLabel !== true diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/DependentFieldsWrapper.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/DependentFieldsWrapper.js new file mode 100644 index 00000000000..170c1351697 --- /dev/null +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/DependentFieldsWrapper.js @@ -0,0 +1,964 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + Dropdown, + Card, + LabelFieldPair, + TextInput, + Button, + PopUp, + Tag, + SVG, + CheckBox, +} from "@egovernments/digit-ui-components"; +import ReactDOM from "react-dom"; +import { useCustomT } from "./useCustomT"; + +/** Portal so the popup escapes side panels and fills the viewport layer */ +function BodyPortal({ children }) { + if (typeof document === "undefined") return null; // SSR guard + return ReactDOM.createPortal(children, document.body); +} + +function MdmsValueDropdown({ schemaCode, value, onChange, t }) { + const tenantId = Digit?.ULBService?.getCurrentTenantId?.(); + const [module = "", master = ""] = (schemaCode || "").split("."); + + const { isLoading, data: list = [] } = Digit.Hooks.useCustomMDMS( + tenantId, + module, + [{ name: master }], + { + cacheTime: Infinity, + staleTime: Infinity, + select: (data) => data?.[module]?.[master] || [], + }, + { schemaCode: "DROPDOWN_MASTER_DATA" }, + true // mdmsv2 + ); + + const options = React.useMemo( + () => (Array.isArray(list) ? list.map((it) => ({ code: it.code, name: it.name })) : []), + [list] + ); + + // normalize the selected value to an option object + const selectedOption = React.useMemo(() => { + if (!value) return undefined; + const match = options.find((o) => String(o.code) === String(value)); + return match || { code: value, name: value }; + }, [options, value]); + + return ( + onChange(e.code)} + disabled={isLoading || !module || !master} + selected={selectedOption} + /> + ); +} + +function DependentFieldsWrapper({ + t, + parentState, + currentState, + onExpressionChange, + screenConfig, + selectedFieldItem, +}) { + const useT = useCustomT(); + + // ---------- labels ---------- + const displayLogicLabel = t("DISPLAY_LOGIC") || "Display Logic"; + const noLogicAddedLabel = t("NO_LOGIC_ADDED") || "No logic added yet."; + const addDisplayLogicLabel = t("ADD_DISPLAY_LOGIC") || "Add Display Logic"; + const editLabel = t("EDIT") || "Edit"; + const deleteRuleLabel = t("HCM_REMOVE_RULE") || "Delete Rule"; + const joinWithLabel = t("HCM_JOIN_WITH") || "Join with"; + const selectPageLabel = t("HCM_SELECT_PAGE") || "Select Page"; + const selectFieldLabel = t("HCM_SELECT_FIELD") || "Select Field"; + const comparisonTypeLabel = t("HCM_COMPARISION_TYPE") || "Comparison"; + const selectValueLabel = t("HCM_SELECT_VALUE") || "Select Value"; + const enterValueLabel = t("ENTER_VALUE") || "Enter value"; + const closeLabel = t("CLOSE") || "Cancel"; + const submitLabel = t("SUBMIT") || "Submit"; + const andText = t("AND") || "And"; + const orText = t("OR") || "Or"; + const completeAllMsg = + t("PLEASE_COMPLETE_ALL_CONDITIONS") || + "Please complete all condition fields before confirming."; + const logicLabel = t("HCM_LOGIC") || "Logic"; + + // ---------- constants & helpers ---------- + const LOGICALS = [ + { code: "&&", name: t("AND") || "AND" }, + { code: "||", name: t("OR") || "OR" }, + ]; + const ALL_OPERATOR_OPTIONS = [ + { code: "==", name: t("EQUALS_TO") || "equals to" }, + { code: "!=", name: t("NOT_EQUALS_TO") || "not equals to" }, + { code: ">=", name: t("GREATER_THAN_OR_EQUALS_TO") || "greater than or equals to" }, + { code: "<=", name: t("LESS_THAN_OR_EQUALS_TO") || "less than or equals to" }, + { code: ">", name: t("GREATER_THAN") || "greater than" }, + { code: "<", name: t("LESS_THAN") || "less than" }, + ]; + const PARSE_OPERATORS = useMemo( + () => ["!=", ">=", "<=", "==", ">", "<"].sort((a, b) => b.length - a.length), + [] + ); + + const currentPage = screenConfig?.[0]?.name; + const currentTemplate = parentState?.currentTemplate || []; + + // page helpers + const parsePageOrder = (p) => { + const raw = p?.order ?? p?.pageOrder; + const n = Number(raw); + if (!Number.isNaN(n)) return n; + const match = String(p?.name ?? "").match(/^(\d+(?:\.\d+)?)/); + return match ? Number(match[1]) : NaN; + }; + const currPageObj = (parentState?.currentTemplate || []).find( + (p) => p?.name === screenConfig?.[0]?.name + ); + const currOrder = parsePageOrder(currPageObj || {}); + const currIsDecimal = Number.isFinite(currOrder) && !Number.isInteger(currOrder); + + const pageOptions = useMemo(() => { + const withoutTemplates = (parentState?.currentTemplate || []).filter( + (p) => (p?.type || "").toLowerCase() !== "template" + ); + const idx = withoutTemplates.findIndex((p) => p?.name === currentPage); + const upto = idx === -1 ? withoutTemplates : withoutTemplates.slice(0, idx + 1); + const filtered = currIsDecimal + ? upto.filter((p) => { + if (p?.name === currentPage) return true; + const ord = parsePageOrder(p); + return Number.isNaN(ord) || Number.isInteger(ord); + }) + : upto; + return filtered.map((p) => ({ code: p.name, name: p.name, type: p.type })); + }, [parentState?.currentTemplate, currentPage, currIsDecimal]); + + const getPageObj = (pageCode) => + pageCode === currentPage + ? currentState?.screenData?.[0]?.cards?.[0] + : currentTemplate.find((p) => p.name === pageCode)?.cards?.[0]; + + // fields filter (hide template/dynamic/custom; only show not-hidden OR hidden+includeInForm) + const getFieldOptions = (pageCode) => { + const pageObj = getPageObj(pageCode); + if (!pageObj?.fields) return []; + return pageObj.fields + .filter((f) => !["template", "dynamic", "custom"].includes(String(f?.type || "").toLowerCase())) + .filter((f) => { + const isHidden = + f?.hidden === true || f?.isHidden === true || f?.hide === true; + const includeInForm = f?.includeInForm; + if (includeInForm === false) return false; + return !isHidden || includeInForm === true; + }) + .filter( + (f) => + pageCode !== currentPage || + f?.order < (selectedFieldItem?.order || pageObj.fields.length) + ) + .map((f) => ({ + code: f.jsonPath, + name: f.jsonPath, + label: f.label, + format: f.format || f.appType, + type: f.type || f.datatype || f.format || "string", + schemaCode: f.schemaCode, + enums: f.dropDownOptions || [], + })); + }; + + // type helpers + const getFieldMeta = (pageCode, fieldCode) => { + const pageObj = getPageObj(pageCode); + const field = pageObj?.fields?.find((f) => f.jsonPath === fieldCode); + return { pageObj, field }; + }; + const isStringLike = (field) => { + const tpe = (field?.type || "").toLowerCase(); + const fmt = (field?.format || "").toLowerCase(); + if (fmt === "dropdown" || fmt === "radio" || tpe === "selection") return true; + return ["string", "text", "textinput", "textarea"].includes(tpe); + }; + const isCheckboxField = (field) => (field?.type || "").toLowerCase() === "checkbox"; + const isNumericField = (field) => { + const tpe = (field?.type || "").toLowerCase(); + const fmt = (field?.format || "").toLowerCase(); + const numericTags = ["number", "numeric", "integer"]; + return numericTags.includes(tpe) || numericTags.includes(fmt); + }; + const isDobLike = (field) => { + const tpe = (field?.type || "").toLowerCase(); + const fmt = (field?.format || "").toLowerCase(); + return tpe === "datepicker" && fmt === "dob"; + }; + const isDatePickerNotDob = (field) => { + const tpe = (field?.type || "").toLowerCase(); + const fmt = (field?.format || "").toLowerCase(); + return tpe === "datepicker" && fmt !== "dob"; + }; + const isSelectLike = (field) => { + const fmt = (field?.format || "").toLowerCase(); + const tpe = (field?.type || "").toLowerCase(); + return ( + fmt === "dropdown" || + fmt === "radio" || + tpe === "selection" || + (Array.isArray(field?.enums) && field.enums.length > 0) || + !!field?.schemaCode + ); + }; + + const toDDMMYYYY = (iso) => { + if (!iso) return ""; + const [y, m, d] = String(iso).split("-"); + if (!y || !m || !d) return ""; + return `${d.padStart(2, "0")}/${m.padStart(2, "0")}/${y}`; + }; + const toISOFromDDMMYYYY = (ddmmyyyy) => { + if (!ddmmyyyy) return ""; + const [d, m, y] = String(ddmmyyyy).split("/"); + if (!y || !m || !d) return ""; + return `${y}-${m.padStart(2, "0")}-${d.padStart(2, "0")}`; + }; + + // inputs + const sanitizeIntegerInput = (raw) => { + const s = String(raw ?? ""); + if (s === "" || s === "+" || s === "-") return s; + if (/^[+-]?\d+$/.test(s)) return s; + const sign = s[0] === "+" || s[0] === "-" ? s[0] : ""; + const digits = s.replace(/[^0-9]/g, ""); + return sign + digits; + }; + + // operator options per field type + const getOperatorOptions = (field) => { + if (!field) return ALL_OPERATOR_OPTIONS.filter((o) => o.code === "==" || o.code === "!="); + + if (isCheckboxField(field)) { + return ALL_OPERATOR_OPTIONS.filter((o) => o.code === "==" || o.code === "!="); + } + if (isDobLike(field) || isDatePickerNotDob(field) || isNumericField(field)) { + return ALL_OPERATOR_OPTIONS; + } + if (isSelectLike(field) || isStringLike(field)) { + return ALL_OPERATOR_OPTIONS.filter((o) => o.code === "==" || o.code === "!="); + } + return ALL_OPERATOR_OPTIONS; + }; + + // ensure selected operator comes from the option list (so label shows correctly) + const getSelectedOperatorFromOptions = (field, comp) => { + const opts = getOperatorOptions(field); + if (!comp?.code) return undefined; + return opts.find((o) => o.code === comp.code); + }; + + // ---------- parse / serialize ---------- + const serializeRule = (r) => { + if (!r?.selectedPage?.code || !r?.selectedField?.code || !r?.comparisonType?.code) + return ""; + const { field } = getFieldMeta(r.selectedPage.code, r.selectedField.code); + + if (field && isDobLike(field)) { + const months = String(r?.fieldValue ?? "").trim(); + if (months === "") return ""; + const left = `calculateAgeInMonths(${r.selectedPage.code}.${r.selectedField.code})`; + return `${left}${r.comparisonType.code}${months}`; + } + if (field && isDatePickerNotDob(field)) { + const ddmmyyyy = String(r?.fieldValue ?? "").trim(); + if (ddmmyyyy === "") return ""; + return `${r.selectedPage.code}.${r.selectedField.code}${r.comparisonType.code}${ddmmyyyy}`; + } + if (String(r?.fieldValue ?? "").trim() === "") return ""; + return `${r.selectedPage.code}.${r.selectedField.code}${r.comparisonType.code}${r.fieldValue}`; + }; + + const buildFinalExpression = (rulesArr) => + rulesArr + .map((r, i) => (i > 0 ? `${r.joiner?.code || "&&"} ${serializeRule(r)}` : serializeRule(r))) + .filter((seg) => !!seg) + .join(" "); + + const tokenize = (expr = "") => { + if (!expr) return []; + const tokens = []; + let i = 0; + while (i < expr.length) { + const andPos = expr.indexOf("&&", i); + const orPos = expr.indexOf("||", i); + const hasAnd = andPos !== -1; + const hasOr = orPos !== -1; + if (!hasAnd && !hasOr) { + const last = expr.slice(i).trim(); + if (last) tokens.push({ type: "cond", value: last }); + break; + } + let nextOp = null; + let nextIdx = -1; + if (hasAnd && hasOr) { + if (andPos < orPos) { + nextOp = "&&"; + nextIdx = andPos; + } else { + nextOp = "||"; + nextIdx = orPos; + } + } else if (hasAnd) { + nextOp = "&&"; + nextIdx = andPos; + } else { + nextOp = "||"; + nextIdx = orPos; + } + const before = expr.slice(i, nextIdx).trim(); + if (before) tokens.push({ type: "cond", value: before }); + tokens.push({ type: "op", value: nextOp }); + i = nextIdx + nextOp.length; + } + return tokens; + }; + + const parseSingle = (expression = "") => { + for (const operator of PARSE_OPERATORS) { + const i = expression.indexOf(operator); + if (i !== -1) { + const leftRaw = expression.slice(0, i).trim(); + const right = expression.slice(i + operator.length).trim(); + + // unwrap calculateAgeInMonths(page.field) + let left = leftRaw; + const ageFn = "calculateAgeInMonths("; + if (leftRaw.startsWith(ageFn) && leftRaw.endsWith(")")) { + left = leftRaw.slice(ageFn.length, -1); + } + + const [pageCode = "", fieldCode = ""] = (left || "") + .split(".") + .map((s) => (s || "").trim()); + + return { + selectedPage: pageCode ? { code: pageCode, name: pageCode } : {}, + selectedField: fieldCode ? { code: fieldCode, name: fieldCode } : {}, + comparisonType: { code: operator, name: operator }, // normalized later + fieldValue: (right || "").trim(), + }; + } + } + return { selectedPage: {}, selectedField: {}, comparisonType: {}, fieldValue: "" }; + }; + + // ---------- rules state (list of single-condition rules) ---------- + const committedExpression = (selectedFieldItem?.visibilityCondition?.expression || "").trim(); + + const rulesFromExisting = useMemo(() => { + if (!committedExpression) return []; + const tokens = tokenize(committedExpression); + if (!tokens.length) return []; + const out = []; + let pendingJoin = "&&"; + tokens.forEach((t) => { + if (t.type === "op") pendingJoin = t.value; + else { + const base = parseSingle(t.value); + out.push({ + ...base, + joiner: + out.length === 0 + ? { code: "&&", name: "AND" } + : { code: pendingJoin, name: pendingJoin === "||" ? "OR" : "AND" }, + }); + } + }); + return out; + }, [committedExpression]); + + const [rules, setRules] = useState(() => rulesFromExisting); + const [editorIndex, setEditorIndex] = useState(null); // number for edit, "new" for add + const [draftRule, setDraftRule] = useState(null); + const [globalFormError, setGlobalFormError] = useState(""); + const [validationStarted, setValidationStarted] = useState(false); + + const showPopUp = editorIndex !== null; + + useEffect(() => { + // if the upstream expression changed (e.g. switching fields), reseed + setRules(rulesFromExisting); + }, [rulesFromExisting]); + + const isRuleComplete = (r) => + Boolean(r?.selectedPage?.code) && + Boolean(r?.selectedField?.code) && + Boolean(r?.comparisonType?.code) && + String(r?.fieldValue ?? "").trim() !== ""; + + // ---------- actions ---------- + const openEditorForNew = () => { + // do NOT push to rules yet — just open a draft + const jr = + rules.length > 0 + ? { selectedPage: {}, selectedField: {}, comparisonType: {}, fieldValue: "", joiner: { code: "&&", name: "AND" } } + : { selectedPage: {}, selectedField: {}, comparisonType: {}, fieldValue: "", joiner: { code: "&&", name: "AND" } }; + setDraftRule(jr); + setValidationStarted(false); + setGlobalFormError(""); + setEditorIndex("new"); + }; + + const openEditor = (idx) => { + setDraftRule({ ...rules[idx] }); + setValidationStarted(false); + setGlobalFormError(""); + setEditorIndex(idx); + }; + + const discardAndCloseEditor = () => { + setDraftRule(null); + setValidationStarted(false); + setGlobalFormError(""); + setEditorIndex(null); + }; + + const deleteRuleFromList = (idx) => + setRules((prev) => { + const next = prev.filter((_, i) => i !== idx); + onExpressionChange?.(buildFinalExpression(next)); + return next; + }); + + // apply (add or update) + const submitAndClose = () => { + setValidationStarted(true); + if (!isRuleComplete(draftRule)) { + setGlobalFormError(completeAllMsg); + return; + } + if (editorIndex === "new") { + const next = [...rules, draftRule]; + setRules(next); + onExpressionChange?.(buildFinalExpression(next)); + } else if (typeof editorIndex === "number") { + const next = rules.map((r, i) => (i === editorIndex ? draftRule : r)); + setRules(next); + onExpressionChange?.(buildFinalExpression(next)); + } + discardAndCloseEditor(); + }; + + // close button behavior: show toast if incomplete; otherwise close + const handleFooterClose = () => { + if (!isRuleComplete(draftRule)) { + setValidationStarted(true); + setGlobalFormError(completeAllMsg); // show toast + return; // do not close + } + discardAndCloseEditor(); + }; + + // ---------- small UI helpers to render the list nicely ---------- + const JoinerRow = ({ code }) => ( +
+ + {code === "||" ? orText.toUpperCase() : andText.toUpperCase()} + +
+ ); + + const RuleRow = ({ idx, onEdit, onDelete }) => ( +
+ {/* Tag (uniform CSS for every condition) */} + + + {/* Actions pinned right */} +
+
onEdit(idx)} + style={{ display: "inline-flex", alignItems: "center", cursor: "pointer" }} + > + {SVG?.Edit ? ( + + ) : ( +
+ +
onDelete(idx)} + style={{ display: "inline-flex", alignItems: "center", cursor: "pointer" }} + > + {SVG?.Delete ? ( + + ) : ( +
+
+
+ ); + + // ---------- UI ---------- + return ( + + {/* Title */} +
+

{displayLogicLabel}

+
+ + {/* Rules list with center-placed joiners */} +
+ {(!rules || rules.length === 0) ? ( +

{noLogicAddedLabel}

+ ) : ( + <> + {/* First condition row */} + + + {/* Subsequent conditions: joiner centered, then condition row */} + {rules.slice(1).map((r, i) => ( + + + + + ))} + + )} +
+ + {/* Add Logic button */} +
+
+ + {/* Single-condition editor popup */} + {showPopUp && draftRule && ( + +
+ +
+ {/* Join-with: only when adding and there is at least one existing rule */} + {editorIndex === "new" && rules.length > 0 && ( +
+ {joinWithLabel} +
+ + setDraftRule((prev) => ({ + ...prev, + joiner: { code: e.code, name: e.code === "||" ? "OR" : "AND" }, + })) + } + selected={draftRule.joiner} + /> +
+
+ )} + +
+ {/* Page */} +
+ +

{selectPageLabel}

+
+ + setDraftRule((prev) => ({ + ...prev, + selectedPage: e, + selectedField: {}, + comparisonType: {}, + fieldValue: "", + })) + } + selected={ + draftRule?.selectedPage?.code + ? pageOptions.find((p) => p.code === draftRule.selectedPage.code) || + draftRule.selectedPage + : draftRule.selectedPage + } + /> +
+
+
+ + {/* Field */} +
+ +

{selectFieldLabel}

+
+ { + const nextOps = getOperatorOptions(e); + const canKeep = + draftRule?.comparisonType?.code && + nextOps.some((o) => o.code === draftRule.comparisonType.code); + + const isCk = isCheckboxField(e); + setDraftRule((prev) => ({ + ...prev, + selectedField: e, + fieldValue: isCk + ? (["true", "false"].includes(String(prev.fieldValue).toLowerCase()) + ? prev.fieldValue + : "false") + : "", + // store the actual option object so Dropdown shows the label properly + comparisonType: canKeep + ? nextOps.find((o) => o.code === prev.comparisonType.code) + : (isCk ? nextOps.find((o) => o.code === "==") : {}), + })); + }} + selected={ + draftRule?.selectedField?.code + ? (draftRule?.selectedPage?.code ? getFieldOptions(draftRule.selectedPage.code) : []).find( + (f) => f.code === draftRule.selectedField.code + ) || draftRule.selectedField + : draftRule.selectedField + } + disabled={!draftRule?.selectedPage?.code} + /> +
+
+
+ + {/* Operator */} +
+ +

{comparisonTypeLabel}

+
+ {(() => { + const selectedFieldObj = + draftRule?.selectedPage?.code && draftRule?.selectedField?.code + ? getFieldOptions(draftRule.selectedPage.code).find( + (f) => f.code === draftRule.selectedField.code + ) + : null; + const operatorOptions = getOperatorOptions(selectedFieldObj); + const selectedOperator = getSelectedOperatorFromOptions( + selectedFieldObj, + draftRule?.comparisonType + ); + + return ( + setDraftRule((prev) => ({ ...prev, comparisonType: e }))} + disabled={!draftRule?.selectedField?.code} + selected={selectedOperator} + /> + ); + })()} +
+
+
+ + {/* Value */} +
+ +

{selectValueLabel}

+
+ {(() => { + const selectedFieldObj = + draftRule?.selectedPage?.code && draftRule?.selectedField?.code + ? getFieldOptions(draftRule.selectedPage.code).find( + (f) => f.code === draftRule.selectedField.code + ) + : null; + + if (selectedFieldObj && isCheckboxField(selectedFieldObj)) { + const boolVal = String(draftRule.fieldValue).toLowerCase() === "true"; + return ( + { + const checked = typeof v === "boolean" ? v : !!v?.target?.checked; + setDraftRule((prev) => ({ ...prev, fieldValue: checked ? "true" : "false" })); + }} + value={boolVal} + label={t(selectedFieldObj?.label) || selectedFieldObj?.label || ""} + isLabelFirst={false} + disabled={!draftRule?.selectedField?.code} + /> + ); + } + + if (selectedFieldObj && isDobLike(selectedFieldObj)) { + return ( + + setDraftRule((prev) => ({ + ...prev, + fieldValue: sanitizeIntegerInput(event.target.value), + })) + } + disabled={!draftRule?.selectedField?.code} + /> + ); + } + + if (selectedFieldObj && isDatePickerNotDob(selectedFieldObj)) { + const iso = toISOFromDDMMYYYY(draftRule.fieldValue); + return ( + + setDraftRule((prev) => ({ + ...prev, + fieldValue: toDDMMYYYY(event?.target?.value), + })) + } + disabled={!draftRule?.selectedField?.code} + /> + ); + } + + const isSelect = + selectedFieldObj && isSelectLike(selectedFieldObj); + + if (isSelect) { + if (Array.isArray(selectedFieldObj.enums) && selectedFieldObj.enums.length > 0) { + const enumOptions = selectedFieldObj.enums.map((en) => ({ + code: String(en.code), + name: en.name, + })); + const selectedEnum = + enumOptions.find((o) => String(o.code) === String(draftRule.fieldValue)) || + (draftRule.fieldValue + ? { code: String(draftRule.fieldValue), name: String(draftRule.fieldValue) } + : undefined); + return ( + + setDraftRule((prev) => ({ ...prev, fieldValue: e.code })) + } + disabled={!draftRule?.selectedField?.code} + selected={selectedEnum} + /> + ); + } + if (selectedFieldObj.schemaCode) { + return ( + setDraftRule((prev) => ({ ...prev, fieldValue: code }))} + t={useT} + /> + ); + } + return ( + + setDraftRule((prev) => ({ ...prev, fieldValue: event.target.value })) + } + disabled={!draftRule?.selectedField?.code} + /> + ); + } + + const numericValue = isNumericField(selectedFieldObj); + return ( + { + const raw = event.target.value; + const next = numericValue ? sanitizeIntegerInput(raw) : raw; + setDraftRule((prev) => ({ ...prev, fieldValue: next })); + }} + disabled={!draftRule?.selectedField?.code} + /> + ); + })()} +
+
+
+ + {/* Per-condition inline helper */} + {validationStarted && !isRuleComplete(draftRule) && ( +
+ {completeAllMsg} + {SVG?.Close ? ( + { + setGlobalFormError(null); + setValidationStarted(false); + }} + style={{ cursor: "pointer" }} + /> + ) : ( + { + setGlobalFormError(null); + setValidationStarted(false); + }} + > + × + + )} +
+ )} +
+
+ +
, + ]} + onOverlayClick={discardAndCloseEditor} + onClose={discardAndCloseEditor} + equalWidthButtons={false} + footerChildren={[ +
+ + )} + + ); +} + +export default React.memo(DependentFieldsWrapper); \ No newline at end of file diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/DrawerFieldComposer.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/DrawerFieldComposer.js index 85ae3497f3d..641169cc796 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/DrawerFieldComposer.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/DrawerFieldComposer.js @@ -85,6 +85,7 @@ const RenderField = ({ state, panelItem, drawerState, setDrawerState, updateLoca const tenantId = searchParams.get("tenantId"); const shouldShow = whenToShow(panelItem, drawerState); const flowName = useMemo(() => state?.screenConfig?.[0]?.parent, [state?.screenConfig?.[0]]); + const useT = useCustomT(); const reqCriteriaResource = useMemo( () => @@ -192,7 +193,7 @@ const RenderField = ({ state, panelItem, drawerState, setDrawerState, updateLoca label={t(Digit.Utils.locale.getTransformedLocale(`FIELD_DRAWER_LABEL_${panelItem?.label}`))} value={ isLocalisable - ? useCustomT(drawerState?.[panelItem?.bindTo]) + ? useT(drawerState?.[panelItem?.bindTo]) : drawerState?.[panelItem?.bindTo] === true ? "" : drawerState?.[panelItem?.bindTo] @@ -351,7 +352,7 @@ const RenderField = ({ state, panelItem, drawerState, setDrawerState, updateLoca
{ diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/NavigationLogicWrapper.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/NavigationLogicWrapper.js new file mode 100644 index 00000000000..00f0cde2be9 --- /dev/null +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/NavigationLogicWrapper.js @@ -0,0 +1,1096 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + Dropdown, + Card, + LabelFieldPair, + TextInput, + Button, + PopUp, + Tag, + SVG, + CheckBox, +} from "@egovernments/digit-ui-components"; +import ReactDOM from "react-dom"; +import { useCustomT } from "./useCustomT"; + +function MdmsValueDropdown({ schemaCode, value, onChange, t }) { + const tenantId = Digit?.ULBService?.getCurrentTenantId?.(); + const [module = "", master = ""] = (schemaCode || "").split("."); + + const { isLoading, data: list = [] } = Digit.Hooks.useCustomMDMS( + tenantId, + module, + [{ name: master }], + { + cacheTime: Infinity, + staleTime: Infinity, + select: (data) => data?.[module]?.[master] || [], + }, + { schemaCode: "DROPDOWN_MASTER_DATA" }, + true // mdmsv2 + ); + + const options = React.useMemo( + () => (Array.isArray(list) ? list.map((it) => ({ code: it.code, name: it.name })) : []), + [list] + ); + + const selectedOption = React.useMemo(() => { + if (!value) return undefined; + const match = options.find((o) => String(o.code) === String(value)); + return match || { code: value, name: value }; + }, [options, value]); + + return ( + onChange(e.code)} + disabled={isLoading || !module || !master} + selected={selectedOption} + /> + ); +} + +/** Portal so the popup escapes side panels and fills the viewport layer */ +function BodyPortal({ children }) { + if (typeof document === "undefined") return null; + return ReactDOM.createPortal(children, document.body); +} + +function NavigationLogicWrapper({ + t, + parentState, + currentState, + onConditionalNavigateChange, +}) { + const customT = useCustomT(); + + // ----- labels ----- + const navLogicTitle = t("NAVIGATION_LOGIC") || "Navigation Logic"; + const addRuleLabel = t("HCM_ADD_RULE") || "Add Logic"; + const editLabel = t("EDIT") || "Edit"; + const deleteRuleLabel = t("HCM_REMOVE_RULE") || "Delete Rule"; + const noRulesYet = t("HCM_NO_RULES_YET") || "No navigation rules added yet."; + const joinWithLabel = t("HCM_JOIN_WITH") || "Join with"; + const selectFieldLabel = t("HCM_SELECT_FIELD") || "Select Field"; + const comparisonTypeLabel = t("HCM_COMPARISION_TYPE") || "Comparison"; + const selectValueLabel = t("HCM_SELECT_VALUE") || "Select Value"; + const enterValueLabel = t("ENTER_VALUE") || "Enter value"; + const targetPageLabel = t("HCM_TARGET_PAGE") || "Navigate to page"; + const removeConditionLabel = t("REMOVE_CONDITION") || "Delete Condition"; + const addConditionLabel = t("ADD_CONDITION") || "Add Condition"; + const closeLabel = t("CLOSE") || "Cancel"; + const submitLabel = t("SUBMIT") || "Submit"; + const andText = t("AND") || "And"; + const orText = t("OR") || "Or"; + const incompleteExprLabel = t("INCOMPLETE_EXPRESSION") || "(incomplete)"; + const completeAllMsg = + t("PLEASE_COMPLETE_ALL_CONDITIONS") || + "Please complete all conditions and select a target page before confirming."; + const logicLabel = t("HCM_LOGIC") || "Logic"; + + // ----- constants / helpers ----- + const LOGICALS = [ + { code: "&&", name: t("AND") || "AND" }, + { code: "||", name: t("OR") || "OR" }, + ]; + const ALL_OPERATOR_OPTIONS = [ + { code: "==", name: t("EQUALS_TO") || "equals to" }, + { code: "!=", name: t("NOT_EQUALS_TO") || "not equals to" }, + { code: ">=", name: t("GREATER_THAN_OR_EQUALS_TO") || "greater than or equals to" }, + { code: "<=", name: t("LESS_THAN_OR_EQUALS_TO") || "less than or equals to" }, + { code: ">", name: t("GREATER_THAN") || "greater than" }, + { code: "<", name: t("LESS_THAN") || "less than" }, + ]; + + const PARSE_OPERATORS = useMemo( + () => ["!=", ">=", "<=", "==", ">", "<"].sort((a, b) => b.length - a.length), + [] + ); + + const currentPage = currentState?.name; + const currentTemplate = parentState?.currentTemplate || []; + const currentPageObj = currentState?.cards?.[0]; + const existingConditional = Array.isArray(currentState?.conditionalNavigateTo) + ? currentState.conditionalNavigateTo + : []; + + // All fields from current page (no order restriction, skip template fields) + const currentPageFieldOptions = useMemo(() => { + const fields = currentPageObj?.fields || []; + return fields + .filter((f) => f?.type !== "template" && f?.includeInForm !== false) + .map((f) => ({ + code: f.jsonPath, + name: f.jsonPath, + label: f.label, + format: f.format || f.appType, + type: f.type || f.datatype || f.format || "string", + enums: f.dropDownOptions || [], + schemaCode: f.schemaCode, + })); + }, [currentPageObj]); + + // ---- date helpers ---- + const isDobLike = (field) => { + const tpe = (field?.type || "").toLowerCase(); + const fmt = (field?.format || "").toLowerCase(); + return tpe === "datepicker" && fmt === "dob"; + }; + const isDatePickerNotDob = (field) => { + const tpe = (field?.type || "").toLowerCase(); + const fmt = (field?.format || "").toLowerCase(); + return tpe === "datepicker" && fmt !== "dob"; + }; + + const toDDMMYYYY = (iso) => { + if (!iso) return ""; + const [y, m, d] = String(iso).split("-"); + if (!y || !m || !d) return ""; + return `${d.padStart(2, "0")}/${m.padStart(2, "0")}/${y}`; + }; + const toISOFromDDMMYYYY = (ddmmyyyy) => { + if (!ddmmyyyy) return ""; + const [d, m, y] = String(ddmmyyyy).split("/"); + if (!y || !m || !d) return ""; + return `${y}-${m.padStart(2, "0")}-${d.padStart(2, "0")}`; + }; + + const getFieldMeta = (fieldCode) => + currentPageObj?.fields?.find((f) => f.jsonPath === fieldCode) || null; + + const isStringLike = (field) => { + const tpe = (field?.type || "").toLowerCase(); + const fmt = (field?.format || "").toLowerCase(); + if (fmt === "dropdown" || fmt === "radio" || tpe === "selection") return true; + return ["string", "text", "textinput", "textarea"].includes(tpe); + }; + + const isCheckboxField = (field) => { + const tpe = (field?.type || "").toLowerCase(); + return tpe === "checkbox"; + }; + + const isNumericLike = (field) => { + const tpe = (field?.type || "").toLowerCase(); + const fmt = (field?.format || "").toLowerCase(); + return ( + ["number", "numeric", "integer"].includes(tpe) || + ["number", "numeric", "integer"].includes(fmt) + ); + }; + + const sanitizeIntegerInput = (raw) => { + const s = String(raw ?? ""); + if (s === "" || s === "+" || s === "-") return s; + if (/^[+-]?\d+$/.test(s)) return s; + const sign = s[0] === "+" || s[0] === "-" ? s[0] : ""; + const digits = s.replace(/[^0-9]/g, ""); + return sign + digits; + }; + + const getOperatorOptions = (field) => { + if (isCheckboxField(field)) { + return ALL_OPERATOR_OPTIONS.filter((o) => o.code === "==" || o.code === "!="); + } + if (isDobLike(field) || isDatePickerNotDob(field) || isNumericLike(field)) { + return ALL_OPERATOR_OPTIONS; + } + if (!field || isStringLike(field)) + return ALL_OPERATOR_OPTIONS.filter((o) => o.code === "==" || o.code === "!="); + return ALL_OPERATOR_OPTIONS; + }; + + // ----- parse / serialize expression strings ----- + const parseSingle = (expression = "") => { + for (const operator of PARSE_OPERATORS) { + const i = expression.indexOf(operator); + if (i !== -1) { + const left = expression.slice(0, i).trim(); + const right = expression.slice(i + operator.length).trim(); + + // handle calculateAgeInMonths(.) + let leftPath = left; + const ageFn = "calculateAgeInMonths("; + if (left.startsWith(ageFn) && left.endsWith(")")) { + leftPath = left.slice(ageFn.length, -1); + } + + const parts = (leftPath || "").split(".").map((s) => (s || "").trim()); + const fieldCode = parts.length > 1 ? parts.slice(1).join(".") : parts[0]; + + return { + selectedField: fieldCode ? { code: fieldCode, name: fieldCode } : {}, + comparisonType: { code: operator, name: operator }, + fieldValue: (right || "").trim(), + }; + } + } + return { selectedField: {}, comparisonType: {}, fieldValue: "" }; + }; + + const tokenize = (expr = "") => { + if (!expr) return []; + const tokens = []; + let i = 0; + while (i < expr.length) { + const andPos = expr.indexOf("&&", i); + const orPos = expr.indexOf("||", i); + const hasAnd = andPos !== -1; + const hasOr = orPos !== -1; + if (!hasAnd && !hasOr) { + const last = expr.slice(i).trim(); + if (last) tokens.push({ type: "cond", value: last }); + break; + } + let nextOp; + let nextIdx; + if (hasAnd && hasOr) { + if (andPos < orPos) { + nextOp = "&&"; + nextIdx = andPos; + } else { + nextOp = "||"; + nextIdx = orPos; + } + } else if (hasAnd) { + nextOp = "&&"; + nextIdx = andPos; + } else { + nextOp = "||"; + nextIdx = orPos; + } + const before = expr.slice(i, nextIdx).trim(); + if (before) tokens.push({ type: "cond", value: before }); + tokens.push({ type: "op", value: nextOp }); + i = nextIdx + nextOp.length; + } + return tokens; + }; + + const serializeSingle = (c) => { + if (!currentPage) return ""; + const field = getFieldMeta(c?.selectedField?.code); + if (!c?.selectedField?.code || !c?.comparisonType?.code) return ""; + + if (field && isDobLike(field)) { + const months = String(c?.fieldValue ?? "").trim(); + if (months === "") return ""; + const left = `calculateAgeInMonths(${currentPage}.${c.selectedField.code})`; + return `${left}${c.comparisonType.code}${months}`; + } + + if (field && isDatePickerNotDob(field)) { + const ddmmyyyy = String(c?.fieldValue ?? "").trim(); + if (ddmmyyyy === "") return ""; + return `${currentPage}.${c.selectedField.code}${c.comparisonType.code}${ddmmyyyy}`; + } + + if (c?.fieldValue === "") return ""; + return `${currentPage}.${c.selectedField.code}${c.comparisonType.code}${c.fieldValue}`; + }; + + const serializeAll = (conds) => { + const out = []; + conds.forEach((c, i) => { + const seg = serializeSingle(c); + if (!seg) return; + if (i > 0) out.push(c.joiner?.code || "&&"); + out.push(seg); + }); + return out.join(" "); + }; + + const initialEmptyCondition = () => ({ + selectedField: {}, + comparisonType: {}, + fieldValue: "", + joiner: { code: "&&", name: "AND" }, + }); + + const initialEmptyRule = () => ({ + conds: [initialEmptyCondition()], + targetPage: {}, + }); + + // ---------- normalization helpers ---------- + const allPageOptions = useMemo(() => { + const seen = new Set(); + const list = []; + const exclude = new Set([currentPage]); // don't include current page + const add = (p) => { + if (!p?.name) return; + if (exclude.has(p.name)) return; + if (seen.has(p.name)) return; + seen.add(p.name); + list.push({ code: p.name, name: p.name, type: p.type }); + }; + currentTemplate.forEach(add); + return list; + }, [currentTemplate, currentPage]); + + const findFieldOptionByCode = (code) => + currentPageFieldOptions.find((f) => f.code === code) || (code ? { code, name: code, label: code } : {}); + const findPageOptionByCode = (code) => + allPageOptions.find((p) => p.code === code) || + (code ? { code, name: code, type: currentTemplate.find((p) => p?.name === code)?.type } : {}); + + const normalizeRule = (r) => { + const conds = (r?.conds || []).map((c, idx) => { + const normalizedField = c?.selectedField?.code ? findFieldOptionByCode(c.selectedField.code) : {}; + return { + ...c, + selectedField: normalizedField, + joiner: idx === 0 ? { code: "&&", name: "AND" } : c.joiner || { code: "&&", name: "AND" }, + }; + }); + const name = r?.targetPage?.code || r?.targetPage?.name || ""; + return { + ...r, + conds: conds.length ? conds : [initialEmptyCondition()], + targetPage: name ? findPageOptionByCode(name) : {}, + }; + }; + + // ----- seed & syncing ----- + const makeRulesFromExisting = () => { + const existing = existingConditional; + if (!existing.length) return []; + const seeded = existing.map((r) => { + const expr = (r?.condition || "").trim(); + const tokens = tokenize(expr); + let conds = []; + if (!tokens.length) { + conds = [initialEmptyCondition()]; + } else { + let pendingJoin = "&&"; + tokens.forEach((t) => { + if (t.type === "op") pendingJoin = t.value; + else { + const base = parseSingle(t.value); + conds.push( + conds.length === 0 + ? { ...base, joiner: { code: "&&", name: "AND" } } + : { ...base, joiner: { code: pendingJoin, name: pendingJoin === "||" ? "OR" : "AND" } } + ); + } + }); + if (!conds.length) conds = [initialEmptyCondition()]; + } + const name = r?.navigateTo?.name || ""; + return { + conds, + targetPage: name ? { code: name, name } : {}, + }; + }); + return seeded.map(normalizeRule); + }; + + const [rules, setRules] = useState(() => makeRulesFromExisting()); + const [editorIndex, setEditorIndex] = useState(null); + const [globalFormError, setGlobalFormError] = useState(""); + const [validationStarted, setValidationStarted] = useState(false); + + const showPopUp = editorIndex !== null; + + useEffect(() => { + if (!showPopUp) { + const fresh = makeRulesFromExisting(); + setRules(fresh); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(existingConditional), showPopUp]); + + // clear errors when popup closes + useEffect(() => { + if (!showPopUp) { + if (globalFormError) setGlobalFormError(""); + if (validationStarted) setValidationStarted(false); + } + }, [showPopUp, globalFormError, validationStarted]); + + const openEditor = (idx) => { + setRules((prev) => prev.map((r, i) => (i === idx ? normalizeRule(r) : r))); + setGlobalFormError(""); + setValidationStarted(false); + setEditorIndex(idx); + }; + + const addRule = () => { + setGlobalFormError(""); + setValidationStarted(false); + setRules((prev) => { + const next = [...prev, initialEmptyRule()]; + const normalized = next.map((r, i) => (i === next.length - 1 ? normalizeRule(r) : r)); + setEditorIndex(normalized.length - 1); + return normalized; + }); + }; + + const discardAndCloseEditor = () => { + setRules(makeRulesFromExisting()); + setGlobalFormError(""); + setValidationStarted(false); + setEditorIndex(null); + }; + + const updateRule = (idx, patch) => + setRules((prev) => prev.map((r, i) => (i === idx ? { ...r, ...patch } : r))); + + const deleteRuleFromList = (idx) => + setRules((prev) => { + const next = prev.filter((_, i) => i !== idx); + if (editorIndex !== null) { + if (idx === editorIndex) setEditorIndex(null); + else if (idx < editorIndex) setEditorIndex(editorIndex - 1); + } + syncParent(next); + return next; + }); + + // ----- condition operations ----- + const updateCond = (ruleIdx, condIdx, patch) => + setRules((prev) => + prev.map((r, i) => + i !== ruleIdx + ? r + : { ...r, conds: r.conds.map((c, j) => (j === condIdx ? { ...c, ...patch } : c)) } + ) + ); + + const changeJoiner = (ruleIdx, condIdx, joinCode) => + setRules((prev) => + prev.map((r, i) => + i !== ruleIdx + ? r + : { + ...r, + conds: r.conds.map((c, j) => + j === condIdx + ? { ...c, joiner: { code: joinCode, name: joinCode === "||" ? "OR" : "AND" } } + : c + ), + } + ) + ); + + const addCondition = (ruleIdx) => + setValidationStarted(true) || + setRules((prev) => + prev.map((r, i) => (i === ruleIdx ? { ...r, conds: [...r.conds, initialEmptyCondition()] } : r)) + ); + + const removeCondition = (ruleIdx, condIdx) => + setRules((prev) => { + const next = prev.map((r, i) => { + if (i !== ruleIdx) return r; + const nextConds = r.conds.filter((_, j) => j !== condIdx); + if (!nextConds.length) nextConds.push(initialEmptyCondition()); + nextConds[0] = { ...nextConds[0], joiner: { code: "&&", name: "AND" } }; + return { ...r, conds: nextConds }; + }); + syncParent(next); + return next; + }); + + // ----- validation ----- + const isCondComplete = (c) => + Boolean(c?.selectedField?.code) && + Boolean(c?.comparisonType?.code) && + String(c?.fieldValue ?? "").trim() !== ""; + + const isRuleComplete = (r) => + r?.conds?.every(isCondComplete) && + Boolean(r?.targetPage?.code || r?.targetPage?.name); + + const canSubmit = showPopUp && editorIndex !== null ? isRuleComplete(rules[editorIndex]) : false; + + useEffect(() => { + if (!showPopUp || editorIndex === null) return; + if (globalFormError && isRuleComplete(rules[editorIndex])) { + setGlobalFormError(""); + } + }, [rules, showPopUp, editorIndex, globalFormError]); + + // ----- payload builder & submit ----- + const buildPayload = (rs) => + rs + .map((r) => { + const name = r.targetPage?.code || r.targetPage?.name || ""; + const page = + allPageOptions.find((p) => p.code === name) || + currentTemplate.find((p) => p?.name === name); + const navigateType = page?.type === "template" ? "template" : "form"; + + return { + condition: serializeAll(r.conds), + navigateTo: { + name, + type: navigateType, + }, + }; + }) + .filter((r) => r.condition && r.navigateTo.name); + + const syncParent = (nextRules) => { + const payload = buildPayload(nextRules); + onConditionalNavigateChange?.(payload); + }; + + const submitAndClose = () => { + setValidationStarted(true); + const isValid = editorIndex !== null && isRuleComplete(rules[editorIndex]); + if (!isValid) { + setGlobalFormError(completeAllMsg); + return; + } + const next = buildPayload(rules); + onConditionalNavigateChange?.(next); + setGlobalFormError(""); + setValidationStarted(false); + setEditorIndex(null); + }; + + // ----- display helpers ----- + const formatConditionLabel = (c) => { + if (!c?.selectedField?.code) return incompleteExprLabel; + const field = getFieldMeta(c.selectedField.code); + const fieldLabel = field?.label ? t(field.label) || field.label : c.selectedField.code; + const op = c?.comparisonType?.code || ""; + let valueText = c?.fieldValue || ""; + valueText = `${valueText}`.replace(/[()]/g, ""); + return `${customT(fieldLabel)} ${t(op)} ${ + field?.format === "dropdown" || + field?.format === "radio" || + field?.type === "selection" || + field?.type === "checkbox" + ? customT(valueText) + : valueText + }`.trim(); + }; + + const formatRuleSummary = (_rule, idx) => `${logicLabel} ${idx + 1}`; + + // ---- small UI helpers to render the outside list with OR separators ---- + const JoinerRow = () => ( +
+ + {orText.toUpperCase()} + +
+ ); + + const RuleRow = ({ idx }) => ( +
+ + +
+
openEditor(idx)} + style={{ display: "inline-flex", alignItems: "center", cursor: "pointer" }} + > + {SVG?.Edit ? ( + + ) : ( +
+ +
deleteRuleFromList(idx)} + style={{ display: "inline-flex", alignItems: "center", cursor: "pointer" }} + > + +
+
+
+ ); + + // ----- UI ----- + return ( + + {/* Title */} +
+

{navLogicTitle}

+
+ + {/* Rules list separated by centered OR */} +
+ {rules.length === 0 ? ( +

{noRulesYet}

+ ) : ( + <> + + {rules.slice(1).map((_, i) => ( + + + + + ))} + + )} +
+ + {/* Add Logic button */} +
+
+ + {/* Single-rule editor popup */} + {showPopUp && editorIndex !== null && rules[editorIndex] && ( + +
+ + {(() => { + const rule = rules[editorIndex]; + + return ( +
+ {/* Conditions */} + {rule.conds.map((cond, idx) => { + const selectedFieldObj = cond?.selectedField?.code + ? currentPageFieldOptions.find((f) => f.code === cond.selectedField.code) + : undefined; + + const operatorOptions = getOperatorOptions(selectedFieldObj); + const selectedOperator = cond?.comparisonType?.code + ? operatorOptions.find((o) => o.code === cond.comparisonType.code) + : undefined; + + const numericField = isNumericLike(selectedFieldObj); + + return ( +
+ {idx > 0 && ( +
+ {joinWithLabel} +
+ changeJoiner(editorIndex, idx, e.code)} + selected={cond.joiner} + /> +
+
+ )} + +
+ {/* Field */} +
+ +

{selectFieldLabel}

+
+ { + const nextOps = getOperatorOptions(e); + const canKeep = + cond?.comparisonType?.code && + nextOps.some((o) => o.code === cond.comparisonType.code); + + const isCk = isCheckboxField(e); + updateCond(editorIndex, idx, { + selectedField: e, + fieldValue: isCk + ? (["true", "false"].includes(String(cond.fieldValue).toLowerCase()) + ? cond.fieldValue + : "false") + : "", + comparisonType: canKeep + ? cond.comparisonType + : (isCk ? nextOps.find((o) => o.code === "==") : {}), + }); + }} + selected={ + cond?.selectedField?.code + ? currentPageFieldOptions.find((f) => f.code === cond.selectedField.code) + : cond.selectedField + } + /> +
+
+
+ + {/* Operator */} +
+ +

{comparisonTypeLabel}

+
+ updateCond(editorIndex, idx, { comparisonType: e })} + disabled={!cond?.selectedField?.code} + selected={selectedOperator} + /> +
+
+
+ + {/* Value */} +
+ +

{selectValueLabel}

+
+ {(() => { + if (selectedFieldObj && isCheckboxField(selectedFieldObj)) { + const boolVal = String(cond.fieldValue).toLowerCase() === "true"; + return ( + { + const checked = typeof v === "boolean" ? v : !!v?.target?.checked; + updateCond(editorIndex, idx, { fieldValue: checked ? "true" : "false" }); + }} + value={boolVal} + label={t(selectedFieldObj?.label) || selectedFieldObj?.label || ""} + isLabelFirst={false} + disabled={!cond?.selectedField?.code} + /> + ); + } + + if (selectedFieldObj && isDobLike(selectedFieldObj)) { + return ( + + updateCond(editorIndex, idx, { + fieldValue: sanitizeIntegerInput(event.target.value), + }) + } + disabled={!cond?.selectedField?.code} + /> + ); + } + + if (selectedFieldObj && isDatePickerNotDob(selectedFieldObj)) { + const iso = toISOFromDDMMYYYY(cond.fieldValue); + return ( + + updateCond(editorIndex, idx, { + fieldValue: toDDMMYYYY(event?.target?.value), + }) + } + /> + ); + } + + const isSelect = + selectedFieldObj && + (selectedFieldObj.format === "dropdown" || + selectedFieldObj.format === "radio" || + selectedFieldObj.type === "selection"); + + if (isSelect) { + if (Array.isArray(selectedFieldObj.enums) && selectedFieldObj.enums.length > 0) { + const enumOptions = selectedFieldObj.enums.map((en) => ({ + code: String(en.code), + name: en.name, + })); + const selectedEnum = + enumOptions.find((o) => String(o.code) === String(cond.fieldValue)) || + (cond.fieldValue + ? { code: String(cond.fieldValue), name: String(cond.fieldValue) } + : undefined); + return ( + updateCond(editorIndex, idx, { fieldValue: e.code })} + disabled={!cond?.selectedField?.code} + selected={selectedEnum} + /> + ); + } + + if (selectedFieldObj.schemaCode) { + return ( + updateCond(editorIndex, idx, { fieldValue: code })} + t={customT} + /> + ); + } + + return ( + updateCond(editorIndex, idx, { fieldValue: event.target.value })} + disabled={!cond?.selectedField?.code} + /> + ); + } + + if (numericField) { + return ( + + updateCond(editorIndex, idx, { + fieldValue: sanitizeIntegerInput(event.target.value), + }) + } + disabled={!cond?.selectedField?.code} + /> + ); + } + + return ( + updateCond(editorIndex, idx, { fieldValue: event.target.value })} + disabled={!cond?.selectedField?.code} + /> + ); + })()} +
+
+
+ + {/* Remove condition */} +
removeCondition(editorIndex, idx)} + title={removeConditionLabel} + aria-label={removeConditionLabel} + role="button" + > + + + {removeConditionLabel} + +
+
+ + {/* Per-condition error */} + {validationStarted && !isCondComplete(cond) && ( +
+ {completeAllMsg} + { + setValidationStarted(false); + setGlobalFormError(null) + }} + tabIndex={0} + style={{ cursor: "pointer" }} + /> +
+ )} +
+ ); + })} + + {/* Add condition */} +
+
+ + {/* Target page */} +
+ +

{targetPageLabel}

+
+ updateRule(editorIndex, { targetPage: e })} + selected={ + rules[editorIndex]?.targetPage?.code + ? allPageOptions.find((p) => p.code === rules[editorIndex].targetPage.code) || + rules[editorIndex].targetPage + : rules[editorIndex].targetPage + } + /> +
+
+
+ + {/* GLOBAL ERROR (shows when Submit clicked with missing target page or incomplete conditions) */} + {globalFormError ? ( +
+ {globalFormError} + setGlobalFormError("")} + tabIndex={0} + style={{ cursor: "pointer" }} + /> +
+ ) : null} +
+ ); + })()} +
, + ]} + onOverlayClick={discardAndCloseEditor} + onClose={discardAndCloseEditor} + equalWidthButtons={false} + footerChildren={[ +
+ + )} + + ); +} + +export default React.memo(NavigationLogicWrapper); \ No newline at end of file diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/RenderConditionalField.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/RenderConditionalField.js index 78445a86968..d998963dbb0 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/RenderConditionalField.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/RenderConditionalField.js @@ -3,6 +3,7 @@ import { TextInput, Dropdown, RadioButtons, Button, FieldV1 } from "@egovernment import { useTranslation } from "react-i18next"; import { useCustomT } from "./useCustomT"; import { DustbinIcon } from "../../../components/icons/DustbinIcon"; +import DependentFieldsWrapper from "./DependentFieldsWrapper"; export const RenderConditionalField = ({ cField, @@ -16,6 +17,7 @@ export const RenderConditionalField = ({ disabled, }) => { const { t } = useTranslation(); + const useT = useCustomT(); const isLocalisable = AppScreenLocalisationConfig?.fields ?.find((i) => i.fieldType === (drawerState?.appType || drawerState?.type)) ?.localisableProperties?.includes(cField?.bindTo?.split(".")?.at(-1)); @@ -33,7 +35,7 @@ export const RenderConditionalField = ({ label={cField?.label} withoutLabel={Boolean(!cField?.label)} value={ - isLocalisable ? useCustomT(drawerState?.[cField?.bindTo]) : drawerState?.[cField?.bindTo] === true ? "" : drawerState?.[cField?.bindTo] + isLocalisable ? useT(drawerState?.[cField?.bindTo]) : drawerState?.[cField?.bindTo] === true ? "" : drawerState?.[cField?.bindTo] } config={{ step: "", @@ -85,7 +87,7 @@ export const RenderConditionalField = ({ className="" type={"text"} name="title" - value={useCustomT(item?.name)} + value={useT(item?.name)} onChange={(event) => { setDrawerState((prev) => ({ ...prev, @@ -214,6 +216,17 @@ export const RenderConditionalField = ({ optionsKey="code" /> ); + case "dependencyFieldWrapper": + return ( + + ); default: return null; } diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/useCustomT.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/useCustomT.js index 171c1e3b43d..f6021ffd474 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/useCustomT.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/useCustomT.js @@ -1,20 +1,40 @@ +import { useMemo } from "react"; import { useAppLocalisationContext } from "./AppLocalisationWrapper"; -export const useCustomT = (code) => { - if (!code) { - console.warn("useCustomT: code parameter is required"); - return ""; - } +/** + * useCustomT + * + * SAFE usage (preferred): + * const tt = useCustomT(); + * const label = tt("SOME_CODE"); + + */ +export const useCustomT = (maybeCode) => { const { locState, addMissingKey } = useAppLocalisationContext(); - const currentLocale = Digit?.SessionStorage.get("locale") || Digit?.SessionStorage.get("initData")?.selectedLanguage; - if (!Array.isArray(locState)) { - console.warn("useCustomT: locState is not an array"); - return ""; - } - const entry = locState?.find((item) => item.code === code); - if (!entry) { - addMissingKey(code); // Add the missing key - return ""; // Return the key as a placeholder - } - return entry[currentLocale] || ""; // Return the message or fallback to the key -}; + const currentLocale = + Digit?.SessionStorage.get("locale") || + Digit?.SessionStorage.get("initData")?.selectedLanguage; + + const translate = useMemo(() => { + const list = Array.isArray(locState) ? locState : []; + return (code) => { + if (!code) { + console.warn("useCustomT: code parameter is required"); + return ""; + } + const entry = list.find((item) => item?.code === code); + if (!entry) { + addMissingKey(code); + return ""; + } + const msg = entry?.[currentLocale]; + return msg || ""; + }; + }, [locState, addMissingKey, currentLocale]); + + // Back-compat: allow direct call useCustomT("KEY") + if (typeof maybeCode === "string") return translate(maybeCode); + + // Preferred: return a stable translator function + return translate; +}; \ No newline at end of file From 85641f36506e448ca3e562653c4a0a9774f16e9c Mon Sep 17 00:00:00 2001 From: Ramkrishna-egov Date: Tue, 30 Sep 2025 17:12:02 +0530 Subject: [PATCH 2/8] Fixes for adding navigation condition --- .../AppConfigurationWrapper.js | 24 +++++++++++++++++++ .../AppFieldScreenWrapper.js | 15 +++++++++--- .../src/utils/appConfigHelpers.js | 2 ++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationWrapper.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationWrapper.js index f97cd53933a..70deb4e2511 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationWrapper.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationWrapper.js @@ -280,6 +280,30 @@ const reducer = (state = initialState, action, updateLocalization) => { }, ], }; + case "PATCH_PAGE_CONDITIONAL_NAV": { + const { pageName, data } = action; // data is the array from onConditionalNavigateChange + + const patchArray = (arr) => { + if (!Array.isArray(arr) || arr.length === 0) return arr; + + // If pageName is provided, try to patch by name + if (pageName) { + const idx = arr.findIndex((p) => p?.name === pageName); + if (idx !== -1) { + return arr.map((p, i) => (i === idx ? { ...p, conditionalNavigateTo: data } : p)); + } + } + + // Fallback: patch the first page (your “current page is first” invariant) + return arr; + }; + + return { + ...state, + screenConfig: patchArray(state.screenConfig), + screenData: patchArray(state.screenData), + }; + } default: return state; } diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppFieldScreenWrapper.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppFieldScreenWrapper.js index 27191d40f94..be85c5f97c0 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppFieldScreenWrapper.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppFieldScreenWrapper.js @@ -192,11 +192,11 @@ function AppFieldScreenWrapper() { /> )} - {currentCard?.type !== "template" && ( + {currentCard?.type !== "template" && ( <>
-
{t("APPCONFIG_SUBHEAD_BUTTONS")}
- +
{t("APPCONFIG_NAVIGATION_LOGIC")}
+
+ + )} + + {currentCard?.type !== "template" && ( + <> +
+
{t("APPCONFIG_SUBHEAD_BUTTONS")}
+ +
{`${t("APP_CONFIG_ACTION_BUTTON_LABEL")}`} diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/appConfigHelpers.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/appConfigHelpers.js index 656124cb7bf..2214b57325b 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/appConfigHelpers.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/appConfigHelpers.js @@ -222,6 +222,7 @@ export const restructure = (data1, fieldTypeMasterData = [], parent) => { allowCommentsAdditionAt: ["body"], }, navigateTo: page?.navigateTo || {}, + conditionalNavigateTo: page?.conditionalNavigateTo, parent: parent?.name || "", }; }); @@ -294,6 +295,7 @@ export const reverseRestructure = (updatedData, fieldTypeMasterData = []) => { order: index + 1, properties, navigateTo: section?.navigateTo || {}, + conditionalNavigateTo: section?.conditionalNavigateTo, }; }); }; From 843271adf5c91eaf2d434692263bcd5af8be4d3e Mon Sep 17 00:00:00 2001 From: Ramkrishna-egov Date: Tue, 30 Sep 2025 17:14:21 +0530 Subject: [PATCH 3/8] Enabled Local LocalisationWrapper --- .../ImpelComponentWrapper.js | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/ImpelComponentWrapper.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/ImpelComponentWrapper.js index 8eec884d392..85f803fd705 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/ImpelComponentWrapper.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/ImpelComponentWrapper.js @@ -2,7 +2,7 @@ import React, { Fragment, useEffect, useState } from "react"; // import { dummyMaster } from "../../../configs/dummyMaster"; //production mode import { AppLocalisationWrapper, Loader, useCustomT } from "@egovernments/digit-ui-components"; -// import AppLocalisationWrapperDev from "./AppLocalisationWrapper"; +import AppLocalisationWrapperDev from "./AppLocalisationWrapper"; //development mode import AppPreview from "../../../components/AppPreview"; @@ -176,33 +176,33 @@ function ImpelComponentWrapper({ variant, screenConfig, submit, back, showBack, }; // if (process.env.NODE_ENV === "development") { - // return ( - // //development mode - // - // ); + return ( + //development mode + + ); // } - return ( - //production mode - - - - ); + // return ( + // //production mode + // + // + // + // ); } export default React.memo(ImpelComponentWrapper); From fad6773ff5f051122e408d59938511e58c69dda3 Mon Sep 17 00:00:00 2001 From: Ramkrishna-egov Date: Wed, 1 Oct 2025 10:25:25 +0530 Subject: [PATCH 4/8] Added missing parentState from local components --- .../appConfigurationRedesign/AppConfigurationParentLayer.js | 1 + .../appConfigurationRedesign/AppConfigurationWrapper.js | 4 ++-- .../appConfigurationRedesign/AppFieldScreenWrapper.js | 2 +- .../appConfigurationRedesign/AppLocalisationWrapper.js | 4 ++-- .../appConfigurationRedesign/ImpelComponentWrapper.js | 3 ++- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationParentLayer.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationParentLayer.js index feb69b21809..e4549a08f8d 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationParentLayer.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationParentLayer.js @@ -477,6 +477,7 @@ const AppConfigurationParentRedesign = ({ back={back} showBack={true} parentDispatch={parentDispatch} + parentState={parentState} AppConfigMdmsData={AppConfigMdmsData} localeModule={localeModule} pageTag={`${t("CMN_PAGE")} ${currentStep} / ${stepper?.length}`} diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationWrapper.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationWrapper.js index 70deb4e2511..08f1fbf289c 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationWrapper.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationWrapper.js @@ -311,7 +311,7 @@ const reducer = (state = initialState, action, updateLocalization) => { const MODULE_CONSTANTS = "HCM-ADMIN-CONSOLE"; -function AppConfigurationWrapper({ screenConfig, localeModule, pageTag }) { +function AppConfigurationWrapper({ screenConfig, localeModule, pageTag , parentState}) { const useT = useCustomT(); const queryClient = useQueryClient(); const { locState, addMissingKey, updateLocalization, onSubmit, back, showBack, parentDispatch } = useAppLocalisationContext(); @@ -662,7 +662,7 @@ function AppConfigurationWrapper({ screenConfig, localeModule, pageTag }) { ) : ( - + )} diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppFieldScreenWrapper.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppFieldScreenWrapper.js index be85c5f97c0..25362e68294 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppFieldScreenWrapper.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppFieldScreenWrapper.js @@ -23,7 +23,7 @@ import { InfoOutline } from "@egovernments/digit-ui-svg-components"; import ConsoleTooltip from "../../../components/ConsoleToolTip"; import NavigationLogicWrapper from "./NavigationLogicWrapper"; -function AppFieldScreenWrapper() { +function AppFieldScreenWrapper({ parentState }) { const { state, dispatch, openAddFieldPopup } = useAppConfigContext(); const { locState, updateLocalization } = useAppLocalisationContext(); const searchParams = new URLSearchParams(location.search); diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppLocalisationWrapper.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppLocalisationWrapper.js index 7c96d2b1d4a..79c9a3ca4c6 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppLocalisationWrapper.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppLocalisationWrapper.js @@ -38,7 +38,7 @@ const locReducer = (state = initialState, action) => { const MODULE_CONSTANTS = "HCM-ADMIN-CONSOLE"; //TODO @nabeel @jagan move this component to ui-component repo & clean up -function AppLocalisationWrapperDev({ onSubmit, localeModule, screenConfig, back, showBack, parentDispatch, ...props }) { +function AppLocalisationWrapperDev({ onSubmit, localeModule, screenConfig, back, showBack, parentDispatch, parentState, ...props }) { if (!localeModule) { return ; } @@ -129,7 +129,7 @@ function AppLocalisationWrapperDev({ onSubmit, localeModule, screenConfig, back, localeModule, }} > - + ); } diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/ImpelComponentWrapper.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/ImpelComponentWrapper.js index 85f803fd705..420abfc6a42 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/ImpelComponentWrapper.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/ImpelComponentWrapper.js @@ -6,7 +6,7 @@ import AppLocalisationWrapperDev from "./AppLocalisationWrapper"; //development mode import AppPreview from "../../../components/AppPreview"; -function ImpelComponentWrapper({ variant, screenConfig, submit, back, showBack, parentDispatch, localeModule, pageTag, ...props }) { +function ImpelComponentWrapper({ variant, screenConfig, submit, back, showBack, parentDispatch, localeModule, pageTag, parentState, ...props }) { const MODULE_CONSTANTS = "HCM-ADMIN-CONSOLE"; const searchParams = new URLSearchParams(location.search); const fieldMasterName = searchParams.get("fieldType"); @@ -186,6 +186,7 @@ function ImpelComponentWrapper({ variant, screenConfig, submit, back, showBack, parentDispatch={parentDispatch} localeModule={localeModule} pageTag={pageTag} + parentState={parentState} /> ); // } From 5e0a345820cd99c632fe4ee9575cfde5b8379d4c Mon Sep 17 00:00:00 2001 From: Ramkrishna-egov Date: Wed, 1 Oct 2025 13:21:12 +0530 Subject: [PATCH 5/8] Fixed Complaints module components --- .../src/components/AppPreview.js | 7 +- .../DependentFieldsWrapper.js | 14 +- .../NavigationLogicWrapper.js | 10 +- .../src/utils/appConfigHelpers.js | 4 + .../RegistrationComponents.js | 331 ++++++++++-------- 5 files changed, 208 insertions(+), 158 deletions(-) diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/components/AppPreview.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/components/AppPreview.js index a87d726fc39..0ca062574d7 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/components/AppPreview.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/components/AppPreview.js @@ -137,7 +137,7 @@ const getFieldType = (field) => { return "text"; case "number": return "number"; - case "textarea": + case "textArea": return "textarea"; case "time": return "time"; @@ -232,6 +232,8 @@ const AppPreview = ({ data = {}, selectedField, t }) => { placeholder={t(field?.innerLabel) || ""} populators={{ t: field?.isMdms ? null : t, + prefix: field?.prefixText, + suffix: field?.suffixText, title: field?.label, fieldPairClassName: `app-preview-field-pair ${ selectedField?.jsonPath && selectedField?.jsonPath === field?.jsonPath @@ -239,7 +241,7 @@ const AppPreview = ({ data = {}, selectedField, t }) => { : selectedField?.id && selectedField?.id === field?.id ? `app-preview-selected` : `` - }`, + } ${field?.["toArray.required"] && getFieldType(field) !== "custom" ? `required` : ``}`, mdmsConfig: field?.isMdms ? { moduleName: field?.schemaCode?.split(".")[0], @@ -253,7 +255,6 @@ const AppPreview = ({ data = {}, selectedField, t }) => { ? renderField(field, t) : null, }} - required={getFieldType(field) === "custom" ? null : field?.["toArray.required"]} type={getFieldType(field) === "button" || getFieldType(field) === "select" ? "custom" : getFieldType(field) || "text"} value={field?.value === true ? "" : field?.value || ""} disabled={field?.readOnly || false} diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/DependentFieldsWrapper.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/DependentFieldsWrapper.js index 170c1351697..49d00568f1b 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/DependentFieldsWrapper.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/DependentFieldsWrapper.js @@ -116,10 +116,10 @@ function DependentFieldsWrapper({ // page helpers const parsePageOrder = (p) => { - const raw = p?.order ?? p?.pageOrder; + const raw = p?.order || p?.pageOrder; const n = Number(raw); if (!Number.isNaN(n)) return n; - const match = String(p?.name ?? "").match(/^(\d+(?:\.\d+)?)/); + const match = String(p?.name || "").match(/^(\d+(?:\.\d+)?)/); return match ? Number(match[1]) : NaN; }; const currPageObj = (parentState?.currentTemplate || []).find( @@ -234,7 +234,7 @@ function DependentFieldsWrapper({ // inputs const sanitizeIntegerInput = (raw) => { - const s = String(raw ?? ""); + const s = String(raw || ""); if (s === "" || s === "+" || s === "-") return s; if (/^[+-]?\d+$/.test(s)) return s; const sign = s[0] === "+" || s[0] === "-" ? s[0] : ""; @@ -272,17 +272,17 @@ function DependentFieldsWrapper({ const { field } = getFieldMeta(r.selectedPage.code, r.selectedField.code); if (field && isDobLike(field)) { - const months = String(r?.fieldValue ?? "").trim(); + const months = String(r?.fieldValue || "").trim(); if (months === "") return ""; const left = `calculateAgeInMonths(${r.selectedPage.code}.${r.selectedField.code})`; return `${left}${r.comparisonType.code}${months}`; } if (field && isDatePickerNotDob(field)) { - const ddmmyyyy = String(r?.fieldValue ?? "").trim(); + const ddmmyyyy = String(r?.fieldValue || "").trim(); if (ddmmyyyy === "") return ""; return `${r.selectedPage.code}.${r.selectedField.code}${r.comparisonType.code}${ddmmyyyy}`; } - if (String(r?.fieldValue ?? "").trim() === "") return ""; + if (String(r?.fieldValue || "").trim() === "") return ""; return `${r.selectedPage.code}.${r.selectedField.code}${r.comparisonType.code}${r.fieldValue}`; }; @@ -402,7 +402,7 @@ function DependentFieldsWrapper({ Boolean(r?.selectedPage?.code) && Boolean(r?.selectedField?.code) && Boolean(r?.comparisonType?.code) && - String(r?.fieldValue ?? "").trim() !== ""; + String(r?.fieldValue || "").trim() !== ""; // ---------- actions ---------- const openEditorForNew = () => { diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/NavigationLogicWrapper.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/NavigationLogicWrapper.js index 00f0cde2be9..1b7526a5ffc 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/NavigationLogicWrapper.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/NavigationLogicWrapper.js @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState, Fragment } from "react"; import { Dropdown, Card, @@ -184,7 +184,7 @@ function NavigationLogicWrapper({ }; const sanitizeIntegerInput = (raw) => { - const s = String(raw ?? ""); + const s = String(raw || ""); if (s === "" || s === "+" || s === "-") return s; if (/^[+-]?\d+$/.test(s)) return s; const sign = s[0] === "+" || s[0] === "-" ? s[0] : ""; @@ -277,14 +277,14 @@ function NavigationLogicWrapper({ if (!c?.selectedField?.code || !c?.comparisonType?.code) return ""; if (field && isDobLike(field)) { - const months = String(c?.fieldValue ?? "").trim(); + const months = String(c?.fieldValue || "").trim(); if (months === "") return ""; const left = `calculateAgeInMonths(${currentPage}.${c.selectedField.code})`; return `${left}${c.comparisonType.code}${months}`; } if (field && isDatePickerNotDob(field)) { - const ddmmyyyy = String(c?.fieldValue ?? "").trim(); + const ddmmyyyy = String(c?.fieldValue || "").trim(); if (ddmmyyyy === "") return ""; return `${currentPage}.${c.selectedField.code}${c.comparisonType.code}${ddmmyyyy}`; } @@ -500,7 +500,7 @@ function NavigationLogicWrapper({ const isCondComplete = (c) => Boolean(c?.selectedField?.code) && Boolean(c?.comparisonType?.code) && - String(c?.fieldValue ?? "").trim() !== ""; + String(c?.fieldValue || "").trim() !== ""; const isRuleComplete = (r) => r?.conds?.every(isCondComplete) && diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/appConfigHelpers.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/appConfigHelpers.js index 2214b57325b..fc358326ef9 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/appConfigHelpers.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/appConfigHelpers.js @@ -178,6 +178,8 @@ export const restructure = (data1, fieldTypeMasterData = [], parent) => { includeInForm: field?.includeInForm === false ? false : true, includeInSummary: field?.includeInSummary === false ? false : true, helpText: typeof field?.helpText === "string" ? field.helpText : "", + prefixText: field?.prefixText || "", + suffixText: field?.suffixText || "", visibilityCondition: { ...field?.visibilityCondition } || null, })); @@ -282,6 +284,8 @@ export const reverseRestructure = (updatedData, fieldTypeMasterData = []) => { enums: field?.dropDownOptions, validations: toArrayFields, helpText: typeof field?.helpText === "string" ? field.helpText : "", + prefixText: field?.prefixText || "", + suffixText: field?.suffixText || "", visibilityCondition: { ...field?.visibilityCondition } || null, }; }); diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/template_components/RegistrationComponents.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/template_components/RegistrationComponents.js index 2421de05dac..696101b3ec4 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/template_components/RegistrationComponents.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/template_components/RegistrationComponents.js @@ -1,12 +1,11 @@ -import { ResultsDataTable, TableMolecule, Button, Switch, FieldV1, RoundedLabel, CustomSVG, SummaryCardFieldPair, PanelCard, Header } from "@egovernments/digit-ui-components"; -import React, { useEffect, useMemo } from "react"; +import { ResultsDataTable, Button, Switch, FieldV1, CustomSVG, SummaryCardFieldPair, PanelCard, PopUp, SVG } from "@egovernments/digit-ui-components"; +import React, { useEffect, useMemo, useState } from "react"; import { registerComponent } from "./RegistrationRegistry"; - - +import RenderSelectionField from "../../components/RenderSelectionField"; const responsePanelComponent = ({ components, t }) => { - const titleField = components.find(f => f.jsonPath === "AcknowledgementTitle" && !f.hidden); - const descField = components.find(f => f.jsonPath === "AcknowledgementDescription" && !f.hidden); + const titleField = components.find((f) => f.jsonPath === "AcknowledgementTitle" && !f.hidden); + const descField = components.find((f) => f.jsonPath === "AcknowledgementDescription" && !f.hidden); const message = titleField ? t(titleField?.label) : ""; const description = descField ? t(descField?.label) : ""; @@ -26,42 +25,66 @@ const SearchBar = (props) => (
); - -const FilterIcon = () => ( - - +const FilterIcon = (props) => ( + + - -); -const Filter = (props) => ( -
- {/* */} - {props.t(props.field.label) || "LABEL"} -
-); - - -const Toggle = (props) => ( - ); +const Filter = (props) => { + const [showPopUp, setShowPopUp] = useState(false); + return ( +
+ setShowPopUp(true)} /> + + {props.t(props.field.label) || "LABEL"} + + + {showPopUp && ( + setShowPopUp(false)} + style={{ + width: "100%", // Full width popup + maxWidth: "100%", // Prevents shrinking + height: "auto", + margin: 0, + padding: 0, + }} + footerChildren={[]} + sortFooterChildren={true} + > +
+ +
+
+ )} +
+ ); +}; +const Toggle = (props) => ; // household @@ -75,8 +98,6 @@ const EditIcon = () => ( ); const TextButton = (props) => { - - if (props.hidden) return null; const outerStyle = { @@ -122,45 +143,44 @@ const TextButton = (props) => { className="digit-search-action" onClick={props.onClick} > - {props.addMember ? : - - } - {(props?.label || "EDIT_LABEL")} + {props.addMember ? : } + {props?.label || "EDIT_LABEL"}
); }; - - // household member card const HouseHoldDetailsCard = (props) => { - const householdDetails = props.beneficiaryDetails ? props.beneficiaryDetails : [ - //TODO: Need this to be moved to config @Pitabsh, @ram - // { label: "HOUSEHOLD_HEAD", value: "Value" }, - // { label: "ADMINSTRATIVE_AREA", value: "value" }, - // { label: "MEMBER_COUNT", value: 5 }, - - { label: "HouseHold Head", value: "Rohit" }, - { label: "Adminstrative Area", value: "Boundary A" }, - { label: "Member Count", value: 5 }, - ]; + const householdDetails = props.beneficiaryDetails + ? props.beneficiaryDetails + : [ + //TODO: Need this to be moved to config @Pitabsh, @ram + // { label: "HOUSEHOLD_HEAD", value: "Value" }, + // { label: "ADMINSTRATIVE_AREA", value: "value" }, + // { label: "MEMBER_COUNT", value: 5 }, + + { label: "HouseHold Head", value: "Rohit" }, + { label: "Adminstrative Area", value: "Boundary A" }, + { label: "Member Count", value: 5 }, + ]; return (
{householdDetails.map((pair, index) => ( -
+
))} @@ -169,17 +189,24 @@ const HouseHoldDetailsCard = (props) => { }; const HouseholdOverViewMemberCard = (props) => { - const attributes = props.attributes || [{ label: "Gender", value: "Male" }, - { label: "Age", value: "30 years" }, - { label: "Relationship", value: "Father" }, - { label: "Status", value: "Verified" }]; + const attributes = props.attributes || [ + { label: "Gender", value: "Male" }, + { label: "Age", value: "30 years" }, + { label: "Relationship", value: "Father" }, + { label: "Status", value: "Verified" }, + ]; return (
{props.name}
-
{/* Dynamically Render Attributes */} @@ -192,12 +219,10 @@ const HouseholdOverViewMemberCard = (props) => { ))}
- {/* Two Center Buttons */} -
- {props.primaryBtn && Object.keys(props.primaryBtn).length > 0 && (!(props.primaryBtn?.hidden)) && ( + {props.primaryBtn && Object.keys(props.primaryBtn).length > 0 && !props.primaryBtn?.hidden && (
- - -
); }; @@ -234,7 +256,7 @@ const styles = { flexDirection: "column", alignItems: "center", // horizontally center the buttons gap: "8px", - marginTop: "10px", + marginTop: "10px", }, card: { overflowX: "hidden", @@ -293,18 +315,16 @@ const styles = { }, }; - - export const getTemplateRenderer = (templateName) => { + if (templateName?.toUpperCase()?.includes("ACKNOWLEDGEMENT")) { + return responsePanelComponent; + } - switch (templateName) { - case "BeneficiaryAcknowledgement": - case "HouseholdAcknowledgement": - return responsePanelComponent; - - case "HouseholdOverview": + switch (templateName?.toUpperCase()) { + case "HOUSEHOLDOVERVIEW": return HouseHoldOverviewSection; - + case "COMPLAINTSINBOX": + return SimpleSearchFilterRow; // case "AnotherTemplate": return anotherRenderer; @@ -313,11 +333,7 @@ export const getTemplateRenderer = (templateName) => { } }; - - export const HouseHoldOverviewSection = ({ components = [], t }) => { - - const formatMap = {}; components.forEach((item) => { formatMap[item.jsonPath] = item; @@ -340,43 +356,24 @@ export const HouseHoldOverviewSection = ({ components = [], t }) => { `}
- { }} - hidden={editHousehold.hidden} - alignment="flex-end" - /> + {}} hidden={editHousehold.hidden} alignment="flex-end" /> - {detailsCard?.hidden != true && } + {detailsCard?.hidden != true && } - + {addMember && (
-
)}
@@ -384,8 +381,6 @@ export const HouseHoldOverviewSection = ({ components = [], t }) => { ); }; - - const injectTableStyles = () => { const styleId = "dose-table-override-style"; if (!document.getElementById(styleId)) { @@ -452,13 +447,11 @@ const BeneficiaryTableWrapper = ({ columns = [], data = [], finalTableHeading = sortable: false, minWidth: "200px", })); - }, [columns, t]); // + }, [columns, t]); // return (
-

- {finalTableHeading} -

+

{finalTableHeading}

{ }} + onSelectedRowsChange={() => {}} progressPending={false} isPaginationRequired={false} showTableTitle={false} @@ -495,25 +488,20 @@ const BeneficiaryTableWrapper = ({ columns = [], data = [], finalTableHeading = export default BeneficiaryTableWrapper; - - // Independent wrapper for DetailsCard const DetailsCardSection = ({ field, t }) => { - - const heading = t - ? t(field?.label || "BENEFICIARY_DETAILS_TITLE") - : field?.label || ""; + const heading = t ? t(field?.label || "BENEFICIARY_DETAILS_TITLE") : field?.label || ""; const beneficiaryDetails = field?.dropDownOptions?.map((item) => ({ label: item.code, - value: "****" + value: "****", })) || []; if (!beneficiaryDetails.length) return null; return ( -
+
-
-

- {heading} -

+
+

{heading}

@@ -542,11 +526,10 @@ const Table = ({ field, t }) => { const columns = field?.dropDownOptions?.map((item) => { - return { ...item, name: item.name, - code: item.code + code: item.code, }; }) || []; @@ -557,17 +540,81 @@ const Table = ({ field, t }) => { { DOSENO: "Dose 1", STATUS: "Administered", - COMPLETED_ON: "14 June 2024" - } + COMPLETED_ON: "14 June 2024", + }, ]; + return ; +}; + +/** + * SimpleSearchFilterRow + * A minimal row with: [ Search icon + label ] | [ Filter ] | [ Sort icon ] + * + * Uses your existing , , and components. + */ +const SimpleSearchFilterRow = ({ + components = [], + t, + // t, + // label = "Search", + // primaryColor = "currentColor", + // size = "20px", + // onSearchClick, + // onSortClick, + // // Pass anything your needs via this prop + // filterProps = {}, +}) => { + const formatMap = {}; + components.forEach((item) => { + formatMap[item.jsonPath] = item; + }); + + const searchIcon = formatMap["searchComplaints"] || { label: "", hidden: true }; + const filter = formatMap["filter"] || {}; + const sortIcon = formatMap["sortComplaints"] || {}; + + const cellStyle = { + minWidth: 0, + display: "flex", + alignItems: "center", + gap: "0.375rem", // tighter gap between icon and label + color: "inherit", + }; + return ( - +
+ {/* Left: Search icon + label */} +
+ {/* Use currentColor so it won't disappear on white backgrounds */} + + {t?.(searchIcon?.label || "")} +
+ + {/* Middle: Filter (full-width inside its cell) */} +
+
+ +
+
+ + {/* Right: Sort icon + label (optional) */} +
+ + {sortIcon?.label ? ( + {t?.(sortIcon?.label || "")} + ) : null} +
+
); }; @@ -578,5 +625,3 @@ registerComponent("searchByProximity", Toggle); registerComponent("searchByID", Toggle); registerComponent("DetailsCard", DetailsCardSection); registerComponent("Table", Table); - - From ddfe94c3fa9c4b42cb5a8c128b1a402ff0ce1485 Mon Sep 17 00:00:00 2001 From: Ramkrishna-egov Date: Wed, 1 Oct 2025 16:29:18 +0530 Subject: [PATCH 6/8] Fixed page ordering issues for sub form flow --- .../AppConfigurationParentLayer.js | 164 ++++++++++++------ .../src/utils/appConfigHelpers.js | 4 +- 2 files changed, 114 insertions(+), 54 deletions(-) diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationParentLayer.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationParentLayer.js index e4549a08f8d..5d9c2426f38 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationParentLayer.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationParentLayer.js @@ -66,16 +66,6 @@ const AppConfigurationParentRedesign = ({ const [localeModule, setLocaleModule] = useState(null); const [changeLoader, setChangeLoader] = useState(false); - useEffect(() => { - if (currentStep === parentState?.currentTemplate?.length) { - const event = new CustomEvent("lastButtonDisabled", { detail: true }); - window.dispatchEvent(event); - } else { - const event = new CustomEvent("lastButtonDisabled", { detail: false }); - window.dispatchEvent(event); - } - }, [currentStep, parentState]); - useEffect(() => { const handleResetStep = () => { setCurrentStep(1); @@ -100,7 +90,7 @@ const AppConfigurationParentRedesign = ({ const { isLoading: isLoadingAppConfigMdmsData, data: AppConfigMdmsData } = Digit.Hooks.useCustomMDMS( Digit.ULBService.getCurrentTenantId(), MODULE_CONSTANTS, - [{ name: fieldTypeMaster, limit: 100 }], + [{ name: fieldTypeMaster, limit: 1000 }], { cacheTime: Infinity, staleTime: Infinity, @@ -136,7 +126,7 @@ const AppConfigurationParentRedesign = ({ }, }; - const { isLoading: isCacheLoading, data: cacheData, refetch: refetchCache, revalidate } = Digit.Hooks.useCustomAPIHook(reqCriteriaForm); + const { isLoading: isCacheLoading, data: cacheData, refetch: refetchCache } = Digit.Hooks.useCustomAPIHook(reqCriteriaForm); const { mutate: updateMutate } = Digit.Hooks.campaign.useUpdateAppConfig(tenantId); @@ -167,6 +157,7 @@ const AppConfigurationParentRedesign = ({ formId && AppConfigMdmsData?.[fieldTypeMaster]?.length > 0 ) { + const fieldTypeMasterData = AppConfigMdmsData?.[fieldTypeMaster] || []; const temp = restructure(formData?.data?.pages, fieldTypeMasterData, formData?.data); parentDispatch({ @@ -187,19 +178,86 @@ const AppConfigurationParentRedesign = ({ ); }, [parentState?.currentTemplate]); + const currentTabPages = React.useMemo(() => { + const activeParent = numberTabs.find((j) => j.active)?.parent; + return (parentState?.currentTemplate || []) + .filter((i) => i.parent === activeParent) + .sort((a, b) => Number(a.order) - Number(b.order)); + }, [parentState?.currentTemplate, numberTabs]); + + useEffect(() => { + const last = currentTabPages.length + ? Number(currentTabPages[currentTabPages.length - 1].order) + : null; + + const isLast = last != null && Math.abs(Number(currentStep) - last) < 1e-6; + + window.dispatchEvent(new CustomEvent("lastButtonDisabled", { detail: isLast })); +}, [currentStep, currentTabPages]); + + + // Build the ordered list of valid steps once. + // 👉 Replace p.step / p.order / p.name with whatever your source-of-truth field is. + const availableSteps = React.useMemo(() => { + const raw = (parentState?.steps + || parentState?.stepOrder + || (parentState?.currentTemplate || []).map((p) => p?.step || p?.order || p?.name) + ); + + return (raw || []) + .map((x) => parseFloat(String(x))) + .filter((n) => Number.isFinite(n)) + .sort((a, b) => a - b); + }, [parentState]); + + const round1 = (n) => Number(n.toFixed(1)); + + const nextStepFrom = (current) => { + const cur = Number(current); + + // Prefer the next known step from the canonical list + if (availableSteps.length) { + const next = availableSteps.find((s) => s > cur + 1e-9); + if (next != null) return round1(next); + } + + // Fallbacks if no canonical "next" exists: + // - if we're on an integer, try the nearest .1 + // - otherwise jump to next integer + const frac10 = Math.round((cur - Math.floor(cur)) * 10); + if (frac10 === 0) return round1(cur + 0.1); + return Math.floor(cur) + 1; + }; + + const prevStepFrom = (current) => { + const cur = Number(current); + let prev = null; + for (const s of availableSteps) { + if (s < cur - 1e-9) prev = s; else break; + } + return prev != null ? round1(prev) : cur; + }; + useEffect(() => { setStepper( - (parentState?.currentTemplate || []) - ?.filter((i) => i.parent === numberTabs.find((j) => j.active)?.parent) - .sort((a, b) => a.order - b.order) - ?.map((k, j, t) => ({ - name: k.name, - isLast: j === t.length - 1 ? true : false, - isFirst: j === 0 ? true : false, - active: j === currentStep - 1 ? true : false, - })) + currentTabPages.map((k, j, t) => ({ + name: k.name, + isLast: j === t.length - 1, + isFirst: j === 0, + // active by exact order match (works for 4.1, 4.2, …) + active: Number(k.order) === Number(currentStep), + })) ); - }, [parentState?.currentTemplate, numberTabs, currentStep]); + }, [currentTabPages, currentStep]); + + const mainPagesCount = React.useMemo(() => { + const ints = new Set(); + for (const p of currentTabPages) { + const n = parseFloat(String(p?.order || p?.step || p?.name)); + if (Number.isFinite(n)) ints.add(Math.floor(n)); + } + return ints.size; + }, [currentTabPages]); useEffect(() => { if (variant === "app" && parentState?.currentTemplate?.length > 0 && currentStep && numberTabs?.length > 0) { @@ -210,11 +268,13 @@ const AppConfigurationParentRedesign = ({ } }, [parentState?.currentTemplate, currentStep, numberTabs]); + if (isCacheLoading || isLoadingAppConfigMdmsData || !parentState?.currentTemplate || parentState?.currentTemplate?.length === 0) { return ; } const submit = async (screenData, finalSubmit, tabChange) => { + parentDispatch({ key: "SETBACK", data: screenData, @@ -233,15 +293,15 @@ const AppConfigurationParentRedesign = ({ const reverseFormat = cacheData && cacheData?.filteredCache?.data?.data ? { - ...parentState?.actualTemplate?.actualTemplate, - version: parentState?.actualTemplate?.version + 1, - pages: reverseData, - } + ...parentState?.actualTemplate?.actualTemplate, + version: parentState?.actualTemplate?.version + 1, + pages: reverseData, + } : { - ...parentState?.actualTemplate, - version: parentState?.actualTemplate?.version + 1, - pages: reverseData, - }; + ...parentState?.actualTemplate, + version: parentState?.actualTemplate?.version + 1, + pages: reverseData, + }; const updatedFormData = { ...formData, data: reverseFormat }; @@ -340,15 +400,15 @@ const AppConfigurationParentRedesign = ({ const reverseFormat = cacheData && cacheData?.filteredCache?.data?.data ? { - ...parentState?.actualTemplate?.actualTemplate, - version: parentState?.actualTemplate?.version + 1, - pages: reverseData, - } + ...parentState?.actualTemplate?.actualTemplate, + version: parentState?.actualTemplate?.version + 1, + pages: reverseData, + } : { - ...parentState?.actualTemplate, - version: parentState?.actualTemplate?.version + 1, - pages: reverseData, - }; + ...parentState?.actualTemplate, + version: parentState?.actualTemplate?.version + 1, + pages: reverseData, + }; const updatedFormData = { ...formData, data: reverseFormat }; @@ -392,7 +452,6 @@ const AppConfigurationParentRedesign = ({ setShowToast({ key: "success", label: "APP_CONFIGURATION_SUCCESS" }); setChangeLoader(false); revalidateForm(); - revalidate(); }, } ); @@ -426,21 +485,22 @@ const AppConfigurationParentRedesign = ({ }, } ); - setCurrentStep((prev) => prev + 1); + setCurrentStep((prev) => nextStepFrom(prev)); } }; const back = () => { - if (stepper?.find((i) => i.active)?.isFirst && isPreviousTabAvailable) { - tabStateDispatch({ key: "PREVIOUS_TAB" }); - setCurrentStep(1); - return; - } else if (stepper?.find((i) => i.active)?.isFirst && !isPreviousTabAvailable) { - setShowToast({ key: "error", label: "CANNOT_GO_BACK" }); - } else { - setCurrentStep((prev) => prev - 1); - } - }; + const activeStep = stepper?.find((i) => i.active); + if (activeStep?.isFirst && isPreviousTabAvailable) { + tabStateDispatch({ key: "PREVIOUS_TAB" }); + setCurrentStep(availableSteps[0] || 1); + return; + } else if (activeStep?.isFirst && !isPreviousTabAvailable) { + setShowToast({ key: "error", label: "CANNOT_GO_BACK" }); + } else { + setCurrentStep((prev) => prevStepFrom(prev)); + } +}; if (changeLoader) { return ; } @@ -477,10 +537,10 @@ const AppConfigurationParentRedesign = ({ back={back} showBack={true} parentDispatch={parentDispatch} - parentState={parentState} AppConfigMdmsData={AppConfigMdmsData} localeModule={localeModule} - pageTag={`${t("CMN_PAGE")} ${currentStep} / ${stepper?.length}`} + parentState={parentState} + pageTag={`${t("CMN_PAGE")} ${currentStep} / ${mainPagesCount}`} />
diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/appConfigHelpers.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/appConfigHelpers.js index fc358326ef9..09512f8c7ab 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/appConfigHelpers.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/appConfigHelpers.js @@ -266,7 +266,7 @@ export const reverseRestructure = (updatedData, fieldTypeMasterData = []) => { return { ...typeAndFormat, label: field?.label || "", - order: fieldIndex + 1, + order: field?.order, value: field?.value || "", // required: field.Mandatory || false, hidden: field?.hidden || false, @@ -296,7 +296,7 @@ export const reverseRestructure = (updatedData, fieldTypeMasterData = []) => { label: section.cards?.[0]?.headerFields?.find((i) => i.jsonPath === "ScreenHeading")?.value, description: section.cards?.[0]?.headerFields?.find((i) => i.jsonPath === "Description")?.value, actionLabel: section?.actionLabel || "", - order: index + 1, + order: section.order, properties, navigateTo: section?.navigateTo || {}, conditionalNavigateTo: section?.conditionalNavigateTo, From 856733bf3e184960688c523d0979306fa348fc61 Mon Sep 17 00:00:00 2001 From: Ramkrishna-egov Date: Tue, 7 Oct 2025 16:33:43 +0530 Subject: [PATCH 7/8] Validations array fixes --- .../src/utils/appConfigHelpers.js | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/appConfigHelpers.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/appConfigHelpers.js index 09512f8c7ab..c6b2d1ca504 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/appConfigHelpers.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/utils/appConfigHelpers.js @@ -132,6 +132,37 @@ function flattenValidationsToField(validationsArray) { return result; } +function flattenValidationsToField2(validationsArray, groupKey = "validation") { + const result = {}; + if (!Array.isArray(validationsArray)) return result; + for (const rule of validationsArray) { + if (!rule || typeof rule !== "object") continue; + const { type, value, message } = rule; + if (!type || value === undefined || value === null) continue; + if (!result[groupKey]) result[groupKey] = {}; + result[groupKey][type] = value; + if (message !== undefined && message !== null) { + result[groupKey][`${type}.message`] = message; + } + } + return result; +} +function flattenConfigArrays(configObj) { + const result = {}; + for (const key in configObj) { + if (key === "validations") { + continue; + } + const value = configObj[key]; + // Handle array of {type, value} objects (like validations) + if (Array.isArray(value) && value.every((v) => typeof v === "object" && v.type)) { + const flattened = flattenValidationsToField2(value, key); + Object.assign(result, flattened); // Only merged part (e.g., { validations: { ... } }) + } + } + return result; // :white_check_mark: Only changed keys +} + const addValidationArrayToConfig = (field, fieldTypeMasterData = []) => { const validationArray = []; if (field && field.pattern) { @@ -151,6 +182,7 @@ export const restructure = (data1, fieldTypeMasterData = [], parent) => { ?.sort((a, b) => a.order - b.order) ?.map((field, index) => ({ ...getTypeAndMetaData(field, fieldTypeMasterData), + ...flattenConfigArrays(field), ...flattenValidationsToField(field?.validations || []), label: field?.label || "", value: field?.value || "", @@ -175,12 +207,14 @@ export const restructure = (data1, fieldTypeMasterData = [], parent) => { MdmsDropdown: field?.schemaCode ? true : false, isMdms: field?.schemaCode ? true : false, isMultiSelect: field?.isMultiSelect ? true : false, + schemaCode: field?.schemaCode || "", includeInForm: field?.includeInForm === false ? false : true, includeInSummary: field?.includeInSummary === false ? false : true, helpText: typeof field?.helpText === "string" ? field.helpText : "", prefixText: field?.prefixText || "", suffixText: field?.suffixText || "", visibilityCondition: { ...field?.visibilityCondition } || null, + autoFillCondition: field?.autoFillCondition, })); return { @@ -287,6 +321,7 @@ export const reverseRestructure = (updatedData, fieldTypeMasterData = []) => { prefixText: field?.prefixText || "", suffixText: field?.suffixText || "", visibilityCondition: { ...field?.visibilityCondition } || null, + autoFillCondition: field?.autoFillCondition, }; }); From f7b7ee06c88d83a576b2ebd7d4baa511b6951f92 Mon Sep 17 00:00:00 2001 From: Ramkrishna-egov Date: Fri, 10 Oct 2025 16:31:12 +0530 Subject: [PATCH 8/8] Fixes for Next button when only 1 page there in the flow --- .../AppConfigurationParentLayer.js | 70 ++++++++++++++----- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationParentLayer.js b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationParentLayer.js index 5d9c2426f38..62f28844b81 100644 --- a/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationParentLayer.js +++ b/health/micro-ui/web/micro-ui-internals/packages/modules/campaign-manager/src/pages/employee/appConfigurationRedesign/AppConfigurationParentLayer.js @@ -185,17 +185,40 @@ const AppConfigurationParentRedesign = ({ .sort((a, b) => Number(a.order) - Number(b.order)); }, [parentState?.currentTemplate, numberTabs]); - useEffect(() => { +useEffect(() => { const last = currentTabPages.length ? Number(currentTabPages[currentTabPages.length - 1].order) : null; const isLast = last != null && Math.abs(Number(currentStep) - last) < 1e-6; - - window.dispatchEvent(new CustomEvent("lastButtonDisabled", { detail: isLast })); + + // Check if there's only one page in the current tab - if so, disable Next + const isSinglePage = currentTabPages.length === 1; + + // Dispatch immediately - this runs synchronously + window.dispatchEvent(new CustomEvent("lastButtonDisabled", { + detail: isLast || isSinglePage + })); }, [currentStep, currentTabPages]); +// ALSO ADD: Dispatch the event immediately when currentTabPages changes +// Add this new useEffect right after the above one: + +useEffect(() => { + // This ensures the button state is set immediately when pages load + if (currentTabPages.length > 0) { + const last = Number(currentTabPages[currentTabPages.length - 1].order); + const isLast = Math.abs(Number(currentStep) - last) < 1e-6; + const isSinglePage = currentTabPages.length === 1; + + window.dispatchEvent(new CustomEvent("lastButtonDisabled", { + detail: isLast || isSinglePage + })); + } +}, [currentTabPages]); // Only dep + + // Build the ordered list of valid steps once. // 👉 Replace p.step / p.order / p.name with whatever your source-of-truth field is. const availableSteps = React.useMemo(() => { @@ -274,16 +297,17 @@ const AppConfigurationParentRedesign = ({ } const submit = async (screenData, finalSubmit, tabChange) => { - parentDispatch({ key: "SETBACK", data: screenData, isSubmit: finalSubmit ? true : false, }); + const mergedTemplate = parentState.currentTemplate.map((item) => { const updated = screenData.find((d) => d.name === item.name); return updated ? updated : item; }); + if (finalSubmit) { const mergedTemplate = parentState.currentTemplate.map((item) => { const updated = screenData.find((d) => d.name === item.name); @@ -373,7 +397,6 @@ const AppConfigurationParentRedesign = ({ ); } - // All updates succeeded setShowToast({ key: "success", label: "APP_CONFIGURATION_SUCCESS" }); setChangeLoader(false); history.push(`/${window.contextPath}/employee/campaign/response?isSuccess=true`, { @@ -457,6 +480,21 @@ const AppConfigurationParentRedesign = ({ ); return; } else { + // Check if we're on the last page before proceeding + const lastPageOrder = currentTabPages.length > 0 + ? Number(currentTabPages[currentTabPages.length - 1].order) + : null; + const isOnLastPage = lastPageOrder != null && Math.abs(Number(currentStep) - lastPageOrder) < 1e-6; + + // Also check if this is a single page + const isSinglePage = currentTabPages.length === 1; + + if (isOnLastPage || isSinglePage) { + // Already on the last page or only one page exists, don't proceed + setShowToast({ key: "info", label: "HCM_ALREADY_ON_LAST_PAGE" }); + return; + } + await updateMutate( { moduleName: "HCM-ADMIN-CONSOLE", @@ -490,17 +528,17 @@ const AppConfigurationParentRedesign = ({ }; const back = () => { - const activeStep = stepper?.find((i) => i.active); - if (activeStep?.isFirst && isPreviousTabAvailable) { - tabStateDispatch({ key: "PREVIOUS_TAB" }); - setCurrentStep(availableSteps[0] || 1); - return; - } else if (activeStep?.isFirst && !isPreviousTabAvailable) { - setShowToast({ key: "error", label: "CANNOT_GO_BACK" }); - } else { - setCurrentStep((prev) => prevStepFrom(prev)); - } -}; + const activeStep = stepper?.find((i) => i.active); + if (activeStep?.isFirst && isPreviousTabAvailable) { + tabStateDispatch({ key: "PREVIOUS_TAB" }); + setCurrentStep(availableSteps[0] || 1); + return; + } else if (activeStep?.isFirst && !isPreviousTabAvailable) { + setShowToast({ key: "error", label: "CANNOT_GO_BACK" }); + } else { + setCurrentStep((prev) => prevStepFrom(prev)); + } + }; if (changeLoader) { return ; }