Skip to content

Commit 0317c35

Browse files
[#713] Refine Advanced Modelling Tool input behavior and data synchronization
1 parent 4e82870 commit 0317c35

File tree

2 files changed

+149
-77
lines changed

2 files changed

+149
-77
lines changed

GEMINI.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,12 @@ Income Driver Calculator (IDC) is a web application designed to help companies t
146146
- Corrected driver-aware feasibility logic to properly handle Price/Volume (higher = better) vs. CoP (lower = better).
147147
- Enhanced guidance UI: implemented styled Alert-based hints positioned below calculation results for improved readability.
148148
- Implemented comprehensive regression tests in `modellingFixes.test.js` to verify raw value calculations and UI state transitions.
149+
- Implemented static locks for "Current" and "Feasible" scenarios in Step 5 to clearly indicate read-only data.
150+
- Refined the "Model" scenario UI to be unlocked by default with hidden icons, providing a clean modeling interface.
151+
- Implemented a "stale data" detection mechanism to automatically refresh model values from the "Current" scenario if they match the "Feasible" baseline.
152+
- Preserved background "lock flag" state and logic for future toggleability while hiding it from the current UI.
153+
- Resolved a `ReferenceError` (lexical declaration) by refactoring the data initialization lifecycle to ensure all functions are defined before use.
154+
- Optimized per-segment persistence to ensure that starting model values are always relevant to the current segment state.
149155
- **Visualization & Step 3/4 Fixes (Issue #719)**:
150156
- Resolved graph loading issues in "Understand Income Gap" and "Assess Impact Mitigation Strategies" by refining aggregator question identification for primary, secondary, and tertiary commodities.
151157
- Implemented absolute-wedge rendering in the shared `Pie.js` component to visualize surpluses (negative gaps) while maintaining signed labels and tooltips.

frontend/src/pages/cases/components/AdvancedModellingTool.js

Lines changed: 143 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,7 @@ import {
1212
Divider,
1313
Alert,
1414
} from "antd";
15-
import {
16-
LockOutlined,
17-
UnlockOutlined,
18-
QuestionCircleOutlined,
19-
} from "@ant-design/icons";
15+
import { LockOutlined, QuestionCircleOutlined } from "@ant-design/icons";
2016
import { CaseVisualState, CurrentCaseState } from "../store";
2117
import { thousandFormatter } from "../../../components/chart/options/common";
2218
import EquationVisualizer from "./EquationVisualizer";
@@ -48,19 +44,7 @@ const InputRow = ({
4844
</Space>
4945
</Col>
5046
<Col span={2} align="center">
51-
{isModel && (
52-
<Button
53-
type="text"
54-
icon={
55-
locked ? (
56-
<LockOutlined className="lock-icon" />
57-
) : (
58-
<UnlockOutlined className="unlock-icon" />
59-
)
60-
}
61-
onClick={() => toggleLock(field)}
62-
/>
63-
)}
47+
{!isModel && <LockOutlined className="lock-icon static-lock" />}
6448
</Col>
6549
<Col span={8}>
6650
<Input
@@ -69,7 +53,7 @@ const InputRow = ({
6953
const val = e.target.value.replace(/,/g, "");
7054
handleInputChange(field, val);
7155
}}
72-
disabled={!isModel || locked || isCalculationTarget}
56+
disabled={!isModel || isCalculationTarget}
7357
className="modelling-input"
7458
/>
7559
</Col>
@@ -106,13 +90,13 @@ const AdvancedModellingTool = () => {
10690
selectedDriver: "cop",
10791
activeScenario: "model",
10892
lockedFields: {
109-
price: true,
110-
volume: true,
111-
land: true,
112-
cop: true,
113-
odi: true,
114-
secondary: true,
115-
tertiary: true,
93+
price: false,
94+
volume: false,
95+
land: false,
96+
cop: false,
97+
odi: false,
98+
secondary: false,
99+
tertiary: false,
116100
},
117101
modelValues: {
118102
price: 0,
@@ -262,12 +246,57 @@ const AdvancedModellingTool = () => {
262246
) {
263247
setLockedFields(storedData.lockedFields);
264248
}
265-
if (
266-
storedData.modelValues &&
267-
!isEqual(storedData.modelValues, modelValues)
268-
) {
269-
setModelValues(storedData.modelValues);
249+
250+
if (storedData.modelValues) {
251+
// Check for stale data (matches feasible)
252+
const fields = [
253+
"price",
254+
"volume",
255+
"land",
256+
"cop",
257+
"odi",
258+
"secondary",
259+
"tertiary",
260+
];
261+
const targetSegment =
262+
dashboardData?.find((d) => d.id === selectedSegmentId) || {};
263+
264+
const isStale = fields.every((f) => {
265+
const val = storedData.modelValues[f];
266+
const feasible = getSegmentAnswer("feasible", f, targetSegment);
267+
return val === feasible;
268+
});
269+
270+
if (isStale) {
271+
const getPrefillValue = (field) => {
272+
const currentVal = getSegmentAnswer(
273+
"current",
274+
field,
275+
targetSegment
276+
);
277+
return currentVal !== 0
278+
? currentVal
279+
: getSegmentAnswer("feasible", field, targetSegment);
280+
};
281+
282+
const refreshedValues = {
283+
price: getPrefillValue("price"),
284+
volume: getPrefillValue("volume"),
285+
cop: getPrefillValue("cop"),
286+
land: getPrefillValue("land"),
287+
odi: getPrefillValue("odi"),
288+
secondary: getPrefillValue("secondary"),
289+
tertiary: getPrefillValue("tertiary"),
290+
};
291+
292+
if (!isEqual(refreshedValues, modelValues)) {
293+
setModelValues(refreshedValues);
294+
}
295+
} else if (!isEqual(storedData.modelValues, modelValues)) {
296+
setModelValues(storedData.modelValues);
297+
}
270298
}
299+
271300
if (
272301
storedData.calculationResult &&
273302
!isEqual(storedData.calculationResult, calculationResult)
@@ -278,7 +307,7 @@ const AdvancedModellingTool = () => {
278307
}
279308
}
280309
// eslint-disable-next-line react-hooks/exhaustive-deps
281-
}, [advancedModelingConfig]);
310+
}, [advancedModelingConfig, selectedSegmentId, dashboardData]);
282311

283312
// Find primary commodity QIDs
284313
const focusCommodityGroup = useMemo(() => {
@@ -441,14 +470,21 @@ const AdvancedModellingTool = () => {
441470
const isModelValuesEmpty = Object.values(modelValues).every((v) => v === 0);
442471

443472
if (segment && isModelValuesEmpty) {
473+
const getPrefillValue = (field) => {
474+
const currentVal = getSegmentAnswer("current", field);
475+
return currentVal !== 0
476+
? currentVal
477+
: getSegmentAnswer("feasible", field);
478+
};
479+
444480
setModelValues({
445-
price: getSegmentAnswer("feasible", "price"),
446-
volume: getSegmentAnswer("feasible", "volume"),
447-
cop: getSegmentAnswer("feasible", "cop"),
448-
land: getSegmentAnswer("feasible", "land"),
449-
odi: getSegmentAnswer("feasible", "odi"),
450-
secondary: getSegmentAnswer("feasible", "secondary"),
451-
tertiary: getSegmentAnswer("feasible", "tertiary"),
481+
price: getPrefillValue("price"),
482+
volume: getPrefillValue("volume"),
483+
cop: getPrefillValue("cop"),
484+
land: getPrefillValue("land"),
485+
odi: getPrefillValue("odi"),
486+
secondary: getPrefillValue("secondary"),
487+
tertiary: getPrefillValue("tertiary"),
452488
});
453489
}
454490
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -467,12 +503,51 @@ const AdvancedModellingTool = () => {
467503
};
468504

469505
const handleSegmentChange = (newSegmentId) => {
470-
// Check if we already have data for this segment in the store
471506
const storedData = advancedModelingConfig?.segmentData?.[newSegmentId];
507+
const newSegment = dashboardData?.find((d) => d.id === newSegmentId) || {};
472508

473-
if (storedData) {
474-
// Just switch the segment ID in the store
475-
// Effect 2 will handle loading the data into local state
509+
const getPrefillValue = (field, target) => {
510+
const currentVal = getSegmentAnswer("current", field, target);
511+
return currentVal !== 0
512+
? currentVal
513+
: getSegmentAnswer("feasible", field, target);
514+
};
515+
516+
let finalModelValues = {
517+
price: getPrefillValue("price", newSegment),
518+
volume: getPrefillValue("volume", newSegment),
519+
cop: getPrefillValue("cop", newSegment),
520+
land: getPrefillValue("land", newSegment),
521+
odi: getPrefillValue("odi", newSegment),
522+
secondary: getPrefillValue("secondary", newSegment),
523+
tertiary: getPrefillValue("tertiary", newSegment),
524+
};
525+
526+
// Detect stale data if storedData exists
527+
if (storedData?.modelValues) {
528+
const fields = [
529+
"price",
530+
"volume",
531+
"land",
532+
"cop",
533+
"odi",
534+
"secondary",
535+
"tertiary",
536+
];
537+
const isStale = fields.every((f) => {
538+
const val = storedData.modelValues[f];
539+
const feasible = getSegmentAnswer("feasible", f, newSegment);
540+
return val === feasible;
541+
});
542+
543+
if (!isStale) {
544+
// Keep user customisations if NOT stale
545+
finalModelValues = storedData.modelValues;
546+
}
547+
}
548+
549+
if (storedData && finalModelValues === storedData.modelValues) {
550+
// Just switch the segment ID in the store if data is NOT stale
476551
CaseVisualState.update((s) => ({
477552
...s,
478553
scenarioModeling: {
@@ -488,20 +563,7 @@ const AdvancedModellingTool = () => {
488563
},
489564
}));
490565
} else {
491-
// Generate defaults for the new segment
492-
const newSegment =
493-
dashboardData?.find((d) => d.id === newSegmentId) || {};
494-
495-
const defaultModelValues = {
496-
price: getSegmentAnswer("feasible", "price", newSegment),
497-
volume: getSegmentAnswer("feasible", "volume", newSegment),
498-
cop: getSegmentAnswer("feasible", "cop", newSegment),
499-
land: getSegmentAnswer("feasible", "land", newSegment),
500-
odi: getSegmentAnswer("feasible", "odi", newSegment),
501-
secondary: getSegmentAnswer("feasible", "secondary", newSegment),
502-
tertiary: getSegmentAnswer("feasible", "tertiary", newSegment),
503-
};
504-
566+
// Create or update segment data with refreshed values
505567
const defaultCalculationResult = {
506568
value: null,
507569
change: 0,
@@ -513,19 +575,18 @@ const AdvancedModellingTool = () => {
513575
};
514576

515577
const defaultLockedFields = {
516-
price: true,
517-
volume: true,
518-
land: true,
519-
cop: true,
520-
odi: true,
521-
secondary: true,
522-
tertiary: true,
578+
price: false,
579+
volume: false,
580+
land: false,
581+
cop: false,
582+
odi: false,
583+
secondary: false,
584+
tertiary: false,
523585
};
524586

525587
const defaultActiveScenario = "model";
526588
const defaultSelectedDriver = "cop";
527589

528-
// Update store with new ID AND new segment data
529590
CaseVisualState.update((s) => {
530591
const prevConfig = s.scenarioModeling?.config?.advancedModeling;
531592
return {
@@ -541,11 +602,16 @@ const AdvancedModellingTool = () => {
541602
segmentData: {
542603
...prevConfig?.segmentData,
543604
[newSegmentId]: {
544-
modelValues: defaultModelValues,
545-
calculationResult: defaultCalculationResult,
546-
lockedFields: defaultLockedFields,
547-
activeScenario: defaultActiveScenario,
548-
selectedDriver: defaultSelectedDriver,
605+
...(storedData || {}),
606+
modelValues: finalModelValues,
607+
calculationResult:
608+
storedData?.calculationResult || defaultCalculationResult,
609+
lockedFields:
610+
storedData?.lockedFields || defaultLockedFields,
611+
activeScenario:
612+
storedData?.activeScenario || defaultActiveScenario,
613+
selectedDriver:
614+
storedData?.selectedDriver || defaultSelectedDriver,
549615
},
550616
},
551617
},
@@ -874,13 +940,13 @@ const AdvancedModellingTool = () => {
874940
profit: 0,
875941
});
876942
setLockedFields({
877-
price: true,
878-
volume: true,
879-
land: true,
880-
cop: true,
881-
odi: true,
882-
secondary: true,
883-
tertiary: true,
943+
price: false,
944+
volume: false,
945+
land: false,
946+
cop: false,
947+
odi: false,
948+
secondary: false,
949+
tertiary: false,
884950
});
885951
}}
886952
>

0 commit comments

Comments
 (0)