From 3b95cf8f815d342240396b1a2b30a4dfa0d10fe3 Mon Sep 17 00:00:00 2001 From: Bhanu Prakash Sai Potteri Date: Fri, 26 Jun 2026 12:16:59 +0530 Subject: [PATCH] fix: port designer field rules into bespoke activity forms Custom frontend skipped designer custom_js / field_rules, so workflow gates relying on computed fields never passed. - Recommend: compute & send sa_valid (+ real rate-card premium) so the Meeting Scheduled -> Product Recommended stage gate passes; add inline validity hint; success copy now reflects whether the gate advanced - Assign: thread form_data through LookupField so owner_agent options are region-filtered (field_rules filter_options cascade); clear agent on region change - add src/lib/recommend.ts (faithful port of the Recommend custom_js) Verified via live UI E2E: lead reaches Product Recommended (sa_valid=true, premium=21000) and owner agent matches the chosen region. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 3 + src/api/client.ts | 16 +++- src/api/config.ts | 1 + src/components/activities/AssignBody.tsx | 6 +- src/components/activities/RecommendBody.tsx | 41 +++++----- src/components/form/LookupField.tsx | 10 ++- src/lib/recommend.ts | 85 +++++++++++++++++++++ 7 files changed, 138 insertions(+), 24 deletions(-) create mode 100644 src/lib/recommend.ts diff --git a/.gitignore b/.gitignore index 272ebba..4442163 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ yarn-error.log* .env *.tsbuildinfo + +# Local MCP config (contains credentials) +.mcp.json \ No newline at end of file diff --git a/src/api/client.ts b/src/api/client.ts index 9a005ef..bd32372 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -206,10 +206,20 @@ export class ZinoClient { // --- RDBMS lookup records (owner-agent picker etc.) --- - /** Search an RDBMS lookup template. Returns the matching rows. */ + /** Search an RDBMS lookup template. Returns the matching rows. `formData` + * carries the current form values so the server can apply the activity's + * `field_rules` filter_options (dependent/cascading lookups, e.g. owner + * agents scoped to the chosen region). */ lookupRecords( templateUid: string, - opts: { activityId: string; fieldId: string; search?: string; instanceId?: number | string; limit?: number }, + opts: { + activityId: string; + fieldId: string; + search?: string; + instanceId?: number | string; + limit?: number; + formData?: Record; + }, ): Promise<{ records: Array> }> { return this.request('POST', `/app/${APP_ID}/rdbms-templates/${templateUid}/records`, { workflow_uuid: WORKFLOW_UUID, @@ -219,7 +229,7 @@ export class ZinoClient { search: opts.search ?? '', limit: opts.limit ?? 50, offset: 0, - form_data: {}, + form_data: opts.formData ?? {}, }); } diff --git a/src/api/config.ts b/src/api/config.ts index 3d7b6f1..3940331 100644 --- a/src/api/config.ts +++ b/src/api/config.ts @@ -32,6 +32,7 @@ export const ACTIVITIES = { premiumFrequency: 'premium_frequency_input', // select riders: 'riders_input', // multiselect notes: 'recommendation_notes', // longtext + saValid: 'sa_valid_input', // boolean — computed (see lib/recommend.ts); gates Product Recommended }, }, COLLECT_DOCUMENTS: { diff --git a/src/components/activities/AssignBody.tsx b/src/components/activities/AssignBody.tsx index be5647f..713da3b 100644 --- a/src/components/activities/AssignBody.tsx +++ b/src/components/activities/AssignBody.tsx @@ -79,7 +79,10 @@ export function AssignBody({ instanceId, record, onSuccess }: ActivityBodyProps) fieldId={REGION.id} instanceId={instanceId} value={region} - onChange={setRegion} + onChange={(v) => { + setRegion(v); + setAgent(null); // region drives the agent list — drop a stale pick + }} /> )} {OWNER && ( @@ -93,6 +96,7 @@ export function AssignBody({ instanceId, record, onSuccess }: ActivityBodyProps) instanceId={instanceId} value={agent} onChange={setAgent} + formData={{ assigned_region_input: region }} /> )} {schemaQ.loading &&
Loading form…
} diff --git a/src/components/activities/RecommendBody.tsx b/src/components/activities/RecommendBody.tsx index 4ad8ea5..d2d53e6 100644 --- a/src/components/activities/RecommendBody.tsx +++ b/src/components/activities/RecommendBody.tsx @@ -16,25 +16,12 @@ import { useQuery, useZino } from '../../api/provider'; import { decisionToCard } from '../../api/adapters'; import { fieldFromSchema } from '../../api/schema'; import { ACTIVITIES } from '../../api/config'; +import { recommendCalc, saValidIssues } from '../../lib/recommend'; import type { ActivityBodyProps } from './types'; import type { LeadRecord } from '../../api/types'; const F = ACTIVITIES.RECOMMEND_PRODUCT.fields; -function indicativePremium(sum: number, freq: string): number { - const base = Math.round(sum * 0.00142); - switch (freq) { - case 'monthly': - return Math.round((base / 12) * 1.04); - case 'half_yearly': - return Math.round((base / 2) * 1.02); - case 'quarterly': - return Math.round((base / 4) * 1.03); - default: - return base; - } -} - function toRiderValues(raw: LeadRecord['riders']): string[] { if (!raw) return []; if (Array.isArray(raw)) return raw as string[]; @@ -74,8 +61,11 @@ export function RecommendBody({ instanceId, record, onSuccess }: ActivityBodyPro } }, [record]); - const computed = indicativePremium(sum, freq); - const effectivePremium = premiumTouched ? premium : computed; + // Mirror the designer custom_js: rate-card premium + sa_valid (the stage-gate + // field). Without sa_valid the lead can't advance past Meeting Scheduled. + const calc = recommendCalc(product, record?.date_of_birth, sum, record?.annual_income); + const effectivePremium = premiumTouched ? premium : calc.premium; + const issues = saValidIssues(calc); const [submitting, setSubmitting] = useState(false); const [result, setResult] = useState<{ ok: boolean; msg: string } | null>(null); @@ -97,8 +87,14 @@ export function RecommendBody({ instanceId, record, onSuccess }: ActivityBodyPro [F.premiumFrequency]: freq, [F.riders]: riders, [F.notes]: notes, + [F.saValid]: calc.saValid, + }); + setResult({ + ok: true, + msg: calc.saValid + ? 'Recommendation submitted — lead advanced to Product Recommended.' + : 'Recommendation saved, but the sum assured is outside the plan’s eligible range — the lead stays at Meeting Scheduled until corrected.', }); - setResult({ ok: true, msg: 'Recommendation submitted — lead advanced to Product Recommended.' }); onSuccess?.(res); } catch (e) { setResult({ ok: false, msg: e instanceof Error ? e.message : ((e as { message?: string })?.message ?? 'Submit failed') }); @@ -157,7 +153,7 @@ export function RecommendBody({ instanceId, record, onSuccess }: ActivityBodyPro prefix="₹" type="number" value={effectivePremium} - hint={premiumTouched ? undefined : `Indicative — ₹${formatINR(computed)}`} + hint={premiumTouched ? undefined : calc.evaluated ? `Rate-card — ₹${formatINR(calc.premium)}` : undefined} onChange={(e) => { setPremium(Number(e.target.value) || 0); setPremiumTouched(true); @@ -184,6 +180,15 @@ export function RecommendBody({ instanceId, record, onSuccess }: ActivityBodyPro + {issues.length > 0 && ( +
+ +
+
Sum assured not yet eligible — lead won’t advance
+
{issues.join('; ')}.
+
+
+ )}