From c2a73f77e408f0c7ad76229a41ffac6466f9551a Mon Sep 17 00:00:00 2001 From: brianbrix Date: Sat, 20 Jun 2026 19:06:14 +0300 Subject: [PATCH 01/14] AMP-31133 : Indiccator ME display and saving issues --- amp/TEMPLATE/reamp/package-lock.json | 8 +- amp/TEMPLATE/reamp/package.json | 2 +- .../AmpMEDisaggregationActualValuesPanel.java | 62 ++++--- ...AmpMEDisaggregationValuesFeaturePanel.java | 10 +- .../items/AmpMEIndicatorFeaturePanel.java | 4 +- .../singlecountry/AmpMEItemFeaturePanel.java | 15 +- .../amp/onepager/util/ActivityUtil.java | 56 +++++++ .../activity/ActivityInterchangeUtils.java | 154 +++++++++++++----- .../aim/dbentity/AmpIndicatorGlobalValue.java | 12 ++ .../dbentity/AmpIndicatorGlobalValue.hbm.xml | 3 + 10 files changed, 254 insertions(+), 72 deletions(-) diff --git a/amp/TEMPLATE/reamp/package-lock.json b/amp/TEMPLATE/reamp/package-lock.json index 79e681c1863..e0c9f36b9ed 100644 --- a/amp/TEMPLATE/reamp/package-lock.json +++ b/amp/TEMPLATE/reamp/package-lock.json @@ -22,7 +22,7 @@ "uglifyjs-webpack-plugin": "^1.3.0" }, "devDependencies": { - "amp-ui": "github:devgateway/amp-ui#add-missing-me-fields-to-preview-and-api", + "amp-ui": "github:devgateway/amp-ui#fix/AMP-31133/Indicator-values-display-fixes", "babel-core": "^6.26.3", "babel-jest": "^6.0.1", "babel-loader": "^6.3.2", @@ -309,7 +309,7 @@ }, "node_modules/amp-ui": { "version": "2.1.1", - "resolved": "git+ssh://git@github.com/devgateway/amp-ui.git#4fcbaa11e90187b93ce6b980d4e9e8045abdb614", + "resolved": "git+ssh://git@github.com/devgateway/amp-ui.git#b033cf93099abd3fd308a1d3080f95ac9ddbb5fd", "dev": true, "license": "MIT", "dependencies": { @@ -9493,9 +9493,9 @@ "dev": true }, "amp-ui": { - "version": "git+ssh://git@github.com/devgateway/amp-ui.git#4fcbaa11e90187b93ce6b980d4e9e8045abdb614", + "version": "git+ssh://git@github.com/devgateway/amp-ui.git#b033cf93099abd3fd308a1d3080f95ac9ddbb5fd", "dev": true, - "from": "amp-ui@github:devgateway/amp-ui#add-missing-me-fields-to-preview-and-api", + "from": "amp-ui@github:devgateway/amp-ui#fix/AMP-31133/Indicator-values-display-fixes", "requires": { "docx": "^4.7.1", "file-saver": "github:devgateway/FileSaver.js", diff --git a/amp/TEMPLATE/reamp/package.json b/amp/TEMPLATE/reamp/package.json index 7d081023c55..fceef280486 100644 --- a/amp/TEMPLATE/reamp/package.json +++ b/amp/TEMPLATE/reamp/package.json @@ -23,7 +23,7 @@ "author": "Alexei Savca", "license": "inherit", "devDependencies": { - "amp-ui": "github:devgateway/amp-ui#add-missing-me-fields-to-preview-and-api", + "amp-ui": "github:devgateway/amp-ui#fix/AMP-31133/Indicator-values-display-fixes", "babel-core": "^6.26.3", "babel-jest": "^6.0.1", "babel-loader": "^6.3.2", diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEDisaggregationActualValuesPanel.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEDisaggregationActualValuesPanel.java index 4314196fa0f..85472811d4e 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEDisaggregationActualValuesPanel.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEDisaggregationActualValuesPanel.java @@ -1,18 +1,19 @@ package org.dgfoundation.amp.onepager.components.features.items; import org.apache.wicket.ajax.AjaxRequestTarget; -import org.apache.wicket.markup.html.list.ListItem; -import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.model.IModel; -import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.PropertyModel; +import org.apache.wicket.util.convert.IConverter; import org.dgfoundation.amp.onepager.OnePagerUtil; +import org.dgfoundation.amp.onepager.components.ListEditor; +import org.dgfoundation.amp.onepager.components.ListItem; import org.dgfoundation.amp.onepager.components.ListEditorRemoveButton; import org.dgfoundation.amp.onepager.components.features.AmpFeaturePanel; -import org.dgfoundation.amp.onepager.components.features.me.singlecountry.AmpMEActualValuesFormTableFeaturePanel; import org.dgfoundation.amp.onepager.components.fields.AmpAjaxLinkField; import org.dgfoundation.amp.onepager.components.fields.AmpDatePickerFieldPanel; import org.dgfoundation.amp.onepager.components.fields.AmpTextFieldPanel; +import org.dgfoundation.amp.onepager.models.AbstractMixedSetModel; +import org.digijava.module.aim.dbentity.AmpActivityLocation; import org.digijava.module.aim.dbentity.AmpIndicatorDisaggregationValue; import org.digijava.module.aim.dbentity.AmpIndicatorGlobalValue; import org.dgfoundation.amp.onepager.converters.CustomDoubleConverter; @@ -25,39 +26,42 @@ */ public class AmpMEDisaggregationActualValuesPanel extends AmpFeaturePanel { - private final ListView listView; + private final ListEditor listView; + private final IModel activityLocationModel; public AmpMEDisaggregationActualValuesPanel(String id, IModel model) { + this(id, model, null); + } + + public AmpMEDisaggregationActualValuesPanel(String id, IModel model, + IModel activityLocationModel) { super(id, model, "Disaggregation Actual Values", true); + this.activityLocationModel = activityLocationModel; setOutputMarkupId(true); - // Use a List model for ListView (was Set, causing type mismatch) - IModel> listModel = new LoadableDetachableModel>() { + IModel> actualValuesModel = new PropertyModel<>(model, "actualValues"); + IModel> locationActualValuesModel = new AbstractMixedSetModel( + actualValuesModel) { @Override - protected List load() { - AmpIndicatorDisaggregationValue disagg = AmpMEDisaggregationActualValuesPanel.this.getModel().getObject(); - if (disagg.getActualValues() == null) { - disagg.setActualValues(new java.util.HashSet<>()); - } - return new java.util.ArrayList<>(disagg.getActualValues()); + public boolean condition(AmpIndicatorGlobalValue item) { + return matchesActivityLocation(item.getActivityLocation()); } }; - listView = new ListView("rows", listModel) { + listView = new ListEditor("rows", locationActualValuesModel) { @Override - protected void populateItem(ListItem item) { - AmpIndicatorGlobalValue val = item.getModelObject(); + protected void onPopulateItem(ListItem item) { item.setOutputMarkupId(true); item.add(new AmpTextFieldPanel("actualValue", new PropertyModel<>(item.getModel(), "originalValue"), "Actual Value") { - public org.apache.wicket.util.convert.IConverter getInternalConverter(java.lang.Class type) { + public IConverter getInternalConverter(java.lang.Class type) { return CustomDoubleConverter.INSTANCE; } }); item.add(new AmpDatePickerFieldPanel("actualDate", new PropertyModel<>(item.getModel(), "originalValueDate"), "Actual Date")); item.add(new ListEditorRemoveButton("delActualValue", "Delete", "Delete") { @Override - public void onClick(AjaxRequestTarget target) { - AmpMEDisaggregationActualValuesPanel.this.getModel().getObject().getActualValues().remove(val); + protected void onClick(AjaxRequestTarget target) { + super.onClick(target); target.appendJavaScript(OnePagerUtil.getToggleChildrenJS(AmpMEDisaggregationActualValuesPanel.this)); target.add(AmpMEDisaggregationActualValuesPanel.this); } @@ -76,12 +80,30 @@ public void onClick(AjaxRequestTarget target) { } AmpIndicatorGlobalValue val = new AmpIndicatorGlobalValue(AmpIndicatorGlobalValue.ACTUAL); val.setIndicator(disaggVal.getIndicator()); + val.setActivityLocation(getActivityLocation()); val.setOriginalValueDate(new Date()); - disaggVal.getActualValues().add(val); + listView.addItem(val); target.add(AmpMEDisaggregationActualValuesPanel.this); } }; addActual.setOutputMarkupId(true); add(addActual); } + + private boolean matchesActivityLocation(AmpActivityLocation itemLocation) { + AmpActivityLocation activityLocation = getActivityLocation(); + return activityLocation == null + || itemLocation == activityLocation + || (itemLocation != null && (Objects.equals(itemLocation.getId(), activityLocation.getId()) + || Objects.equals(getLocationId(itemLocation), getLocationId(activityLocation)))); + } + + private AmpActivityLocation getActivityLocation() { + return activityLocationModel != null ? activityLocationModel.getObject() : null; + } + + private Long getLocationId(AmpActivityLocation activityLocation) { + return activityLocation != null && activityLocation.getLocation() != null + ? activityLocation.getLocation().getId() : null; + } } diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEDisaggregationValuesFeaturePanel.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEDisaggregationValuesFeaturePanel.java index b59593c6038..df9b39ec122 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEDisaggregationValuesFeaturePanel.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEDisaggregationValuesFeaturePanel.java @@ -10,9 +10,9 @@ import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.Model; -import org.apache.wicket.model.PropertyModel; import org.dgfoundation.amp.onepager.components.features.AmpFeaturePanel; import org.digijava.kernel.persistence.PersistenceManager; +import org.digijava.module.aim.dbentity.AmpActivityLocation; import org.digijava.module.aim.dbentity.AmpIndicator; import org.digijava.module.aim.dbentity.AmpIndicatorDisaggregationValue; import org.digijava.module.aim.dbentity.AmpIndicatorGlobalValue; @@ -41,6 +41,11 @@ protected void onConfigure() { } public AmpMEDisaggregationValuesFeaturePanel(String id, String fmName, IModel indicatorModel) { + this(id, fmName, indicatorModel, null); + } + + public AmpMEDisaggregationValuesFeaturePanel(String id, String fmName, IModel indicatorModel, + IModel activityLocationModel) { super(id, fmName, true); this.indicatorModel = indicatorModel; logger.info("Initializing AmpMEDisaggregationValuesFeaturePanel for indicator: " + (indicatorModel.getObject() != null ? indicatorModel.getObject().getName() : "null")); @@ -150,7 +155,8 @@ protected void populateItem(ListItem childItem) actualValuesContainer.setOutputMarkupId(true); childItem.add(actualValuesContainer); - AmpMEDisaggregationActualValuesPanel actualPanel = new AmpMEDisaggregationActualValuesPanel("actualValuesPanel", Model.of(disaggVal)); + AmpMEDisaggregationActualValuesPanel actualPanel = new AmpMEDisaggregationActualValuesPanel( + "actualValuesPanel", Model.of(disaggVal), activityLocationModel); actualPanel.setOutputMarkupId(true); actualPanel.setOutputMarkupPlaceholderTag(true); actualValuesContainer.add(actualPanel); diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java index 20d45c50cd3..6ebe81da7a0 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java @@ -10,7 +10,6 @@ import org.dgfoundation.amp.onepager.components.QuarterInformationPanel; import org.dgfoundation.amp.onepager.components.features.AmpFeaturePanel; import org.dgfoundation.amp.onepager.components.features.tables.AmpMEActualValuesFormTableFeaturePanel; -import org.dgfoundation.amp.onepager.components.features.items.AmpMEDisaggregationValuesFeaturePanel; import org.dgfoundation.amp.onepager.components.fields.AmpAjaxLinkField; import org.dgfoundation.amp.onepager.components.fields.AmpCategorySelectFieldPanel; import org.dgfoundation.amp.onepager.components.fields.AmpSelectFieldPanel; @@ -177,7 +176,8 @@ public void onClick(AjaxRequestTarget target) { } add(baseValues); - AmpMEDisaggregationValuesFeaturePanel disaggPanel = new AmpMEDisaggregationValuesFeaturePanel("disaggregationValuesSubsection", "Disaggregation Values", indicator); + AmpMEDisaggregationValuesFeaturePanel disaggPanel = new AmpMEDisaggregationValuesFeaturePanel( + "disaggregationValuesSubsection", "Disaggregation Values", indicator, location); disaggPanel.setOutputMarkupId(true); disaggPanel.setVisible(hasDisaggregation); // Add disaggregation values subsection diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/me/singlecountry/AmpMEItemFeaturePanel.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/me/singlecountry/AmpMEItemFeaturePanel.java index a1317fc9277..d01018abe1b 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/me/singlecountry/AmpMEItemFeaturePanel.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/me/singlecountry/AmpMEItemFeaturePanel.java @@ -208,7 +208,20 @@ protected void onClick(AjaxRequestTarget art) { add(setValue); - AmpMEDisaggregationValuesFeaturePanel disaggPanel = new AmpMEDisaggregationValuesFeaturePanel("disaggregationValuesSubsection","Disaggregation Values", indicator); + IModel activityLocationModel = new LoadableDetachableModel() { + @Override + protected AmpActivityLocation load() { + IndicatorActivity indicatorActivity = conn.getObject(); + if (indicatorActivity.getActivityLocation() != null) { + return indicatorActivity.getActivityLocation(); + } + AmpActivityVersion activity = indicatorActivity.getActivity(); + return activity != null && activity.getLocations() != null && activity.getLocations().size() == 1 + ? activity.getLocations().iterator().next() : null; + } + }; + AmpMEDisaggregationValuesFeaturePanel disaggPanel = new AmpMEDisaggregationValuesFeaturePanel( + "disaggregationValuesSubsection", "Disaggregation Values", indicator, activityLocationModel); disaggPanel.setOutputMarkupId(true); disaggPanel.setOutputMarkupPlaceholderTag(true); disaggPanel.setVisible(hasDisaggregation); diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java b/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java index dce0a8c1061..65f4caba46a 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java @@ -321,6 +321,8 @@ public static AmpActivityVersion saveActivityNewVersion(AmpActivityVersion a, // session.saveOrUpdate(a); session.merge(a); } + saveIndicatorDisaggregationValues(a, session); + session.flush(); updatePerformanceRules(oldA, a); @@ -337,6 +339,60 @@ public static AmpActivityVersion saveActivityNewVersion(AmpActivityVersion a, return a; } + + private static void saveIndicatorDisaggregationValues(AmpActivityVersion activity, Session session) { + if (activity.getIndicators() == null) { + return; + } + for (IndicatorActivity indicatorActivity : activity.getIndicators()) { + AmpIndicator indicator = indicatorActivity.getIndicator(); + if (indicator == null || indicator.getDisaggregationValues() == null) { + continue; + } + for (AmpIndicatorDisaggregationValue disaggregationValue : indicator.getDisaggregationValues()) { + disaggregationValue.setIndicator(indicator); + saveIndicatorGlobalValue(disaggregationValue.getBaseValue(), indicator, session); + saveIndicatorGlobalValue(disaggregationValue.getTargetValue(), indicator, session); + if (disaggregationValue.getActualValues() != null) { + for (AmpIndicatorGlobalValue actualValue : disaggregationValue.getActualValues()) { + actualValue.setType(AmpIndicatorGlobalValue.ACTUAL); + actualValue.setActivityLocation(resolveActivityLocation(activity, + actualValue.getActivityLocation())); + saveIndicatorGlobalValue(actualValue, indicator, session); + } + } + session.saveOrUpdate(disaggregationValue); + } + } + } + + private static AmpActivityLocation resolveActivityLocation(AmpActivityVersion activity, + AmpActivityLocation activityLocation) { + if (activityLocation == null || activity.getLocations() == null) { + return activityLocation; + } + return activity.getLocations().stream() + .filter(candidate -> sameActivityLocation(candidate, activityLocation)) + .findFirst() + .orElse(activityLocation); + } + + private static boolean sameActivityLocation(AmpActivityLocation first, AmpActivityLocation second) { + return first == second + || Objects.equals(first.getId(), second.getId()) + || (first.getLocation() != null && second.getLocation() != null + && Objects.equals(first.getLocation().getId(), second.getLocation().getId())); + } + + private static void saveIndicatorGlobalValue(AmpIndicatorGlobalValue value, AmpIndicator indicator, + Session session) { + if (value == null) { + return; + } + value.setIndicator(indicator); + session.saveOrUpdate(value); + } + private static void cleanObjectFromSession(Session session, Class objectClass, Long id) { T object = session.get(objectClass, id); diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/activity/ActivityInterchangeUtils.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/activity/ActivityInterchangeUtils.java index 3b46a1afdb5..30b135581f9 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/activity/ActivityInterchangeUtils.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/activity/ActivityInterchangeUtils.java @@ -305,7 +305,7 @@ private static void addExtraFieldsToActivity(Map activityFields, .findFirst(); addActualIndicatorValues(indicatorsObject, projectId); - addDisaggregationValues(indicatorsObject); + addDisaggregationValues(indicatorsObject, projectId); resolveIndicatorActivityLocations(indicatorsObject); } } @@ -342,15 +342,17 @@ private static void resolveIndicatorActivityLocations(Optional indicator }); } - private static void addDisaggregationValues(Optional indicatorsObject) { + private static void addDisaggregationValues(Optional indicatorsObject, Long projectId) { indicatorsObject.ifPresent(indicators -> { if (indicators instanceof ArrayList) { List> indicatorsList = (ArrayList>) indicators; for (Map indicator : indicatorsList) { - Long indicatorId = (Long) indicator.get("indicator"); + Long indicatorId = parseLong(indicator.get("indicator")); if (indicatorId == null) { continue; } + Long activityLocationId = parseLong(indicator.get("activity_location")); + AmpActivityLocation activityLocation = getActivityLocation(projectId, activityLocationId); AmpIndicator ampIndicator; try { ampIndicator = IndicatorUtil.getIndicator(indicatorId); @@ -379,7 +381,9 @@ private static void addDisaggregationValues(Optional indicatorsObject) { List> actualValues = new ArrayList<>(); if (dv.getActualValues() != null) { for (AmpIndicatorGlobalValue av : dv.getActualValues()) { - actualValues.add(serializeGlobalValue(av)); + if (matchesActivityLocation(av.getActivityLocation(), activityLocation)) { + actualValues.add(serializeGlobalValue(av)); + } } } dvMap.put("actual_values", actualValues); @@ -391,69 +395,135 @@ private static void addDisaggregationValues(Optional indicatorsObject) { }); } - private static Map serializeGlobalValue(AmpIndicatorGlobalValue gv) { - if (gv == null) { + private static Map serializeGlobalValue(AmpIndicatorGlobalValue globalValue) { + if (globalValue == null) { return null; } SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); Map map = new LinkedHashMap<>(); - map.put("id", gv.getId()); - map.put("original_value", gv.getOriginalValue()); + map.put("id", globalValue.getId()); + map.put("original_value", globalValue.getOriginalValue()); map.put("original_value_date", - gv.getOriginalValueDate() != null ? dateFormat.format(gv.getOriginalValueDate()) : null); - map.put("revised_value", gv.getRevisedValue()); + globalValue.getOriginalValueDate() != null ? dateFormat.format(globalValue.getOriginalValueDate()) + : null); + map.put("revised_value", globalValue.getRevisedValue()); map.put("revised_value_date", - gv.getRevisedValueDate() != null ? dateFormat.format(gv.getRevisedValueDate()) : null); + globalValue.getRevisedValueDate() != null ? dateFormat.format(globalValue.getRevisedValueDate()) + : null); return map; } + private static Long getAmpActivityLocationId(AmpActivityLocation activityLocation) { + return activityLocation != null ? activityLocation.getId() : null; + } + + private static Long getLocationId(AmpActivityLocation activityLocation) { + return activityLocation != null && activityLocation.getLocation() != null + ? activityLocation.getLocation().getId() : null; + } + + private static AmpActivityLocation getActivityLocation(Long ampActivityLocationId) { + if (ampActivityLocationId == null) { + return null; + } + return (AmpActivityLocation) PersistenceManager.getSession().get(AmpActivityLocation.class, ampActivityLocationId); + } + + private static AmpActivityLocation getActivityLocation(Long projectId, Long ampActivityLocationId) { + AmpActivityLocation activityLocation = getActivityLocation(ampActivityLocationId); + if (activityLocation != null || projectId == null) { + return activityLocation; + } + AmpActivityVersion activity = loadActivity(projectId); + return activity.getLocations() != null && activity.getLocations().size() == 1 + ? activity.getLocations().iterator().next() : null; + } + + private static boolean matchesActivityLocation(AmpActivityLocation activityLocation, Long expectedActivityLocationId) { + return expectedActivityLocationId == null + || expectedActivityLocationId.equals(getAmpActivityLocationId(activityLocation)); + } + + private static boolean matchesActivityLocation(AmpActivityLocation activityLocation, + AmpActivityLocation expectedActivityLocation) { + if (expectedActivityLocation == null) { + return activityLocation == null; + } + return Objects.equals(getAmpActivityLocationId(activityLocation), getAmpActivityLocationId(expectedActivityLocation)) + || Objects.equals(getLocationId(activityLocation), getLocationId(expectedActivityLocation)); + } + + private static Long parseLong(Object value) { + if (value == null) { + return null; + } + if (value instanceof Number) { + return ((Number) value).longValue(); + } + return Long.valueOf(value.toString()); + } + private static void addActualIndicatorValues(Optional indicatorsObject, Long projectId){ // Loop through the elements of the ArrayList if present in indicators indicatorsObject.ifPresent(indicators -> { if (indicators instanceof ArrayList) { List> indicatorsList = (ArrayList>) indicators; for (Map indicator : indicatorsList) { - // If indicator already has actual data skip it, only add if actual data is missing - if(indicator.get("actual") == null) { - // Add the "actual" key with its array values to the indicator object - List actualValues = new ArrayList<>(); - - // Create an amp indicator class - AmpIndicator ind = new AmpIndicator(); - ind.setIndicatorId((Long) indicator.get("indicator")); - - List results = null; - AmpActivityVersion activity = null; - - try { - activity = ActivityUtil.loadActivity(projectId); - } catch (DgException e) { - throw new RuntimeException(e); + List actualValues = new ArrayList<>(); + Long activityLocationId = parseLong(indicator.get("activity_location")); + + AmpIndicator ind = new AmpIndicator(); + ind.setIndicatorId(parseLong(indicator.get("indicator"))); + + List results = null; + AmpActivityVersion activity = null; + + try { + activity = ActivityUtil.loadActivity(projectId); + } catch (DgException e) { + throw new RuntimeException(e); + } + results = IndicatorUtil.findActivityIndicatorConnections(activity, ind); + + for (IndicatorActivity result : results) { + if (result == null) { + continue; + } + if (!matchesActivityLocation(result.getActivityLocation(), activityLocationId)) { + continue; } - results = IndicatorUtil.findActivityIndicatorConnections(activity, ind); - - for (IndicatorActivity result : results) { - if (result != null && result.getValues() != null) { - for (AmpIndicatorValue indicatorValue : result.getValues()) { - actualValues.add(new HashMap() {{ - put("comment", indicatorValue.getComment()); - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); // Customize format as needed - Date valueDate = indicatorValue.getValueDate(); - String formattedDate = dateFormat.format(valueDate); - put("date", formattedDate); - put("value", indicatorValue.getValue()); - }}); + if (result.getValues() != null) { + for (AmpIndicatorValue indicatorValue : result.getValues()) { + if (indicatorValue.getValueType() == AmpIndicatorValue.ACTUAL + && matchesIndicatorValueActivityLocation(indicatorValue, result)) { + actualValues.add(serializeIndicatorActualValue(indicatorValue)); } } } - - indicator.put("actual", actualValues); } + + indicator.put("actual", actualValues); } } }); } + private static boolean matchesIndicatorValueActivityLocation(AmpIndicatorValue indicatorValue, + IndicatorActivity indicatorActivity) { + return indicatorValue.getActivityLocation() == null + || matchesActivityLocation(indicatorValue.getActivityLocation(), indicatorActivity.getActivityLocation()); + } + + private static Map serializeIndicatorActualValue(AmpIndicatorValue indicatorValue) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + Map actualValue = new LinkedHashMap<>(); + actualValue.put("comment", indicatorValue.getComment()); + actualValue.put("date", indicatorValue.getValueDate() != null + ? dateFormat.format(indicatorValue.getValueDate()) : null); + actualValue.put("value", indicatorValue.getValue()); + return actualValue; + } + private static void filterPropertyBasedOnUserPermission(Map activity, Long projectId) { final Long donorRole = DbUtil.getAmpRole(Constants.FUNDING_AGENCY).getAmpRoleId(); UserSessionInformation userInformation = SecurityService.getInstance().getUserSessionInformation(); diff --git a/amp/src/main/java/org/digijava/module/aim/dbentity/AmpIndicatorGlobalValue.java b/amp/src/main/java/org/digijava/module/aim/dbentity/AmpIndicatorGlobalValue.java index 172bee11ccf..d8c18962533 100644 --- a/amp/src/main/java/org/digijava/module/aim/dbentity/AmpIndicatorGlobalValue.java +++ b/amp/src/main/java/org/digijava/module/aim/dbentity/AmpIndicatorGlobalValue.java @@ -62,6 +62,9 @@ public class AmpIndicatorGlobalValue implements Serializable { @JsonIgnore private AmpIndicator indicator; + @JsonIgnore + private AmpActivityLocation activityLocation; + public AmpIndicatorGlobalValue() { } @@ -93,6 +96,14 @@ public void setIndicator(AmpIndicator indicator) { this.indicator = indicator; } + public AmpActivityLocation getActivityLocation() { + return activityLocation; + } + + public void setActivityLocation(AmpActivityLocation activityLocation) { + this.activityLocation = activityLocation; + } + public Double getOriginalValue() { return originalValue; } @@ -152,6 +163,7 @@ public void copyValuesTo(AmpIndicatorGlobalValue r) { r.setOriginalValueDate(originalValueDate); r.setRevisedValue(revisedValue); r.setRevisedValueDate(revisedValueDate); + r.setActivityLocation(activityLocation); r.setId(id); } } diff --git a/amp/src/main/resources/org/digijava/module/aim/dbentity/AmpIndicatorGlobalValue.hbm.xml b/amp/src/main/resources/org/digijava/module/aim/dbentity/AmpIndicatorGlobalValue.hbm.xml index fe5d2dfe03e..69a589c15d4 100644 --- a/amp/src/main/resources/org/digijava/module/aim/dbentity/AmpIndicatorGlobalValue.hbm.xml +++ b/amp/src/main/resources/org/digijava/module/aim/dbentity/AmpIndicatorGlobalValue.hbm.xml @@ -12,6 +12,9 @@ + + From 99646e857e1addf7b411370c6fa06c01e869abcc Mon Sep 17 00:00:00 2001 From: brianbrix Date: Sat, 20 Jun 2026 20:43:39 +0300 Subject: [PATCH 02/14] AMP-31133 : Indicator ME display and saving issues --- .../items/AmpMEIndicatorFeaturePanel.java | 28 ++++- .../amp/onepager/util/ActivityUtil.java | 100 +++++++++++++++--- 2 files changed, 113 insertions(+), 15 deletions(-) diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java index 6ebe81da7a0..42f97a989f6 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java @@ -136,7 +136,15 @@ protected String load() { }); add(indicatorTargetDateLabel); - AmpMEActualValuesFormTableFeaturePanel valuesTable = new AmpMEActualValuesFormTableFeaturePanel("valuesSubsection", indicator, conn, location,"Actual Values", false, 7); + AmpMEActualValuesFormTableFeaturePanel valuesTable = new AmpMEActualValuesFormTableFeaturePanel( + "valuesSubsection", indicator, conn, location,"Actual Values", false, 7) { + @Override + protected void onConfigure() { + super.onConfigure(); + boolean fmVisible = isVisible(); + setVisible(fmVisible && !hasDisaggregation); + } + }; valuesTable.setOutputMarkupId(true); valuesTable.setOutputMarkupPlaceholderTag(true); add(valuesTable); @@ -157,6 +165,13 @@ public void onClick(AjaxRequestTarget target) { target.add(valuesTable); target.appendJavaScript(QuarterInformationPanel.getJSUpdate(getSession())); } + + @Override + protected void onConfigure() { + super.onConfigure(); + boolean fmVisible = isVisible(); + setVisible(fmVisible && !hasDisaggregation); + } }; @@ -170,7 +185,16 @@ public void onClick(AjaxRequestTarget target) { AmpMEIndicatorBaseFeaturePanel baseValues = null; try { - baseValues = new AmpMEIndicatorBaseFeaturePanel("addBaseTargetValue", "Add Base Target Values", conn, indicator, values, location); + baseValues = new AmpMEIndicatorBaseFeaturePanel("addBaseTargetValue", "Add Base Target Values", conn, indicator, values, location){ + @Override + protected void onConfigure() { + super.onConfigure(); + if (isVisible()) { + boolean fmVisible = isVisible(); + setVisible(fmVisible && !hasDisaggregation); + } + } + }; } catch (Exception e) { throw new RuntimeException(e); } diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java b/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java index 65f4caba46a..1f86eb6305a 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java @@ -41,9 +41,7 @@ import org.digijava.module.translation.util.ContentTranslationUtil; import org.hibernate.*; import org.hibernate.query.Query; -import org.hibernate.type.IntegerType; import org.hibernate.type.LongType; -import org.hibernate.type.ObjectType; import javax.jcr.Node; import javax.jcr.RepositoryException; @@ -344,26 +342,102 @@ private static void saveIndicatorDisaggregationValues(AmpActivityVersion activit if (activity.getIndicators() == null) { return; } + Map disaggregationValues = new LinkedHashMap<>(); + Map indicators = new HashMap<>(); for (IndicatorActivity indicatorActivity : activity.getIndicators()) { AmpIndicator indicator = indicatorActivity.getIndicator(); if (indicator == null || indicator.getDisaggregationValues() == null) { continue; } for (AmpIndicatorDisaggregationValue disaggregationValue : indicator.getDisaggregationValues()) { - disaggregationValue.setIndicator(indicator); - saveIndicatorGlobalValue(disaggregationValue.getBaseValue(), indicator, session); - saveIndicatorGlobalValue(disaggregationValue.getTargetValue(), indicator, session); - if (disaggregationValue.getActualValues() != null) { - for (AmpIndicatorGlobalValue actualValue : disaggregationValue.getActualValues()) { - actualValue.setType(AmpIndicatorGlobalValue.ACTUAL); - actualValue.setActivityLocation(resolveActivityLocation(activity, - actualValue.getActivityLocation())); - saveIndicatorGlobalValue(actualValue, indicator, session); - } + Long disaggregationValueId = disaggregationValue.getId(); + if (disaggregationValueId == null) { + continue; + } + AmpIndicatorDisaggregationValue mergedValue = disaggregationValues.computeIfAbsent( + disaggregationValueId, ignored -> disaggregationValue); + if (mergedValue != disaggregationValue) { + mergeActualValues(mergedValue, disaggregationValue); } - session.saveOrUpdate(disaggregationValue); + indicators.put(disaggregationValueId, indicator); } } + + for (Map.Entry entry : disaggregationValues.entrySet()) { + AmpIndicator indicator = indicators.get(entry.getKey()); + AmpIndicatorDisaggregationValue disaggregationValue = entry.getValue(); + disaggregationValue.setIndicator(indicator); + saveIndicatorGlobalValue(disaggregationValue.getBaseValue(), indicator, session); + saveIndicatorGlobalValue(disaggregationValue.getTargetValue(), indicator, session); + if (disaggregationValue.getActualValues() != null) { + for (AmpIndicatorGlobalValue actualValue : disaggregationValue.getActualValues()) { + actualValue.setType(AmpIndicatorGlobalValue.ACTUAL); + actualValue.setActivityLocation(resolveActivityLocation(activity, + actualValue.getActivityLocation())); + saveIndicatorGlobalValue(actualValue, indicator, session); + } + } + session.saveOrUpdate(disaggregationValue); + } + + session.flush(); + Set activityLocationIds = getActivityLocationIds(activity); + for (AmpIndicatorDisaggregationValue disaggregationValue : disaggregationValues.values()) { + syncDisaggregationActualValueLinks(disaggregationValue, activityLocationIds, session); + } + } + + private static void mergeActualValues(AmpIndicatorDisaggregationValue target, + AmpIndicatorDisaggregationValue source) { + if (source.getActualValues() == null || source.getActualValues().isEmpty()) { + return; + } + if (target.getActualValues() == null) { + target.setActualValues(new HashSet<>()); + } + target.getActualValues().addAll(source.getActualValues()); + } + + private static Set getActivityLocationIds(AmpActivityVersion activity) { + if (activity.getLocations() == null) { + return Collections.emptySet(); + } + return activity.getLocations().stream() + .map(AmpActivityLocation::getId) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + private static void syncDisaggregationActualValueLinks(AmpIndicatorDisaggregationValue disaggregationValue, + Set activityLocationIds, Session session) { + if (disaggregationValue.getId() == null || activityLocationIds.isEmpty()) { + return; + } + + Query deleteLinks = session.createNativeQuery("DELETE FROM amp_disagg_actual_values links " + + "USING amp_indicator_global_value global_values " + + "WHERE links.global_value_id = global_values.id " + + "AND links.disagg_value_id = :disaggValueId " + + "AND global_values.activity_location IN (:activityLocationIds)"); + deleteLinks.setParameter("disaggValueId", disaggregationValue.getId(), LongType.INSTANCE); + deleteLinks.setParameterList("activityLocationIds", activityLocationIds); + deleteLinks.executeUpdate(); + + Set linkedValueIds = new HashSet<>(); + for (AmpIndicatorGlobalValue actualValue : disaggregationValue.getActualValues()) { + if (actualValue.getId() == null || actualValue.getActivityLocation() == null + || actualValue.getActivityLocation().getId() == null + || !activityLocationIds.contains(actualValue.getActivityLocation().getId()) + || !linkedValueIds.add(actualValue.getId())) { + continue; + } + + Query insertLink = session.createNativeQuery("INSERT INTO amp_disagg_actual_values " + + "(disagg_value_id, global_value_id) VALUES (:disaggValueId, :globalValueId)"); + insertLink.setParameter("disaggValueId", disaggregationValue.getId(), LongType.INSTANCE); + insertLink.setParameter("globalValueId", actualValue.getId(), LongType.INSTANCE); + insertLink.executeUpdate(); + } } private static AmpActivityLocation resolveActivityLocation(AmpActivityVersion activity, From 2920656ede30700336da838c7b2ece2658fdaab3 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Sat, 20 Jun 2026 22:18:44 +0300 Subject: [PATCH 03/14] AMP-31133 : Indicator ME display and saving issues --- .../features/items/AmpMEItemFeaturePanel.java | 45 ++++++---- .../amp/onepager/util/ActivityUtil.java | 85 +++++++++++++++---- 2 files changed, 97 insertions(+), 33 deletions(-) diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEItemFeaturePanel.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEItemFeaturePanel.java index 21fa24046cc..6704b78542d 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEItemFeaturePanel.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEItemFeaturePanel.java @@ -4,9 +4,7 @@ package org.dgfoundation.amp.onepager.components.features.items; import org.apache.wicket.ajax.AjaxRequestTarget; -import org.apache.wicket.event.Broadcast; import org.apache.wicket.markup.html.basic.Label; -import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; @@ -14,7 +12,6 @@ import org.apache.wicket.model.PropertyModel; import org.dgfoundation.amp.onepager.OnePagerUtil; import org.dgfoundation.amp.onepager.components.features.AmpFeaturePanel; -import org.dgfoundation.amp.onepager.components.features.sections.AmpMEFormSectionFeature; import org.dgfoundation.amp.onepager.components.fields.*; import org.dgfoundation.amp.onepager.events.ProgramSelectedEvent; import org.dgfoundation.amp.onepager.events.UpdateEventBehavior; @@ -22,30 +19,21 @@ import org.dgfoundation.amp.onepager.models.AmpMEIndicatorSearchModel; import org.dgfoundation.amp.onepager.models.PersistentObjectModel; import org.dgfoundation.amp.onepager.translation.TranslatorUtil; -import org.dgfoundation.amp.onepager.util.AmpFMTypes; import org.dgfoundation.amp.onepager.yui.AmpAutocompleteFieldPanel; import org.digijava.module.aim.dbentity.*; import org.digijava.module.aim.util.DbUtil; - -import org.apache.log4j.Logger; - import java.util.ArrayList; -import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; -import org.apache.log4j.Logger; - /** * @author aartimon@dginternational.org * @since Feb 10, 2011 */ public class AmpMEItemFeaturePanel extends AmpFeaturePanel { - - - private static final Logger logger = Logger.getLogger(AmpMEItemFeaturePanel.class); /** * @param id * @param fmName @@ -75,8 +63,9 @@ public AmpMEItemFeaturePanel(String id, String fmName, IModel> indicatorsModel = new PropertyModel<>(conn, "indicators"); final IModel> listModel = OnePagerUtil - .getReadOnlyListModelFromSetModel(new PropertyModel(conn, "indicators")); + .getReadOnlyListModelFromSetModel(indicatorsModel); final IModel> filteredListModel = new LoadableDetachableModel>() { @Override @@ -85,7 +74,7 @@ protected List load() { List filteredIndicators = new ArrayList<>(); for (IndicatorActivity indicatorActivity : allIndicators) { - if (indicatorActivity.getActivityLocation() != null && indicatorActivity.getActivityLocation() == location.getObject()) { + if (matchesActivityLocation(indicatorActivity.getActivityLocation(), location.getObject())) { filteredIndicators.add(indicatorActivity); } } @@ -94,12 +83,12 @@ protected List load() { } }; - parentModel = new PropertyModel<>(conn, "indicators"); + parentModel = indicatorsModel; setModel = new AbstractMixedSetModel(parentModel) { @Override public boolean condition(IndicatorActivity item) { - return item.getActivityLocation() == location.getObject(); + return matchesActivityLocation(item.getActivityLocation(), location.getObject()); } }; @@ -117,7 +106,12 @@ public Object getIdentifier(IndicatorActivity t) { protected void populateItem(org.apache.wicket.markup.html.list.ListItem item) { AmpMEIndicatorFeaturePanel indicatorItem = null; try { - indicatorItem = new AmpMEIndicatorFeaturePanel("item", "ME Item", item.getModel(), PersistentObjectModel.getModel(item.getModelObject().getIndicator()), new PropertyModel(item.getModel(), "values"), location); + @SuppressWarnings("unchecked") + IModel indicatorModel = PersistentObjectModel.getModel( + item.getModelObject().getIndicator()); + IModel> valuesModel = new PropertyModel<>(item.getModel(), "values"); + indicatorItem = new AmpMEIndicatorFeaturePanel("item", "ME Item", item.getModel(), + indicatorModel, valuesModel, location); } catch (Exception e) { throw new RuntimeException(e); } @@ -189,4 +183,19 @@ public Integer getTabIndex() { public void setTabIndex(Integer tabIndex) { this.tabIndex = tabIndex; } + + private static boolean matchesActivityLocation(AmpActivityLocation activityLocation, + AmpActivityLocation expectedActivityLocation) { + if (expectedActivityLocation == null) { + return activityLocation == null; + } + return activityLocation == expectedActivityLocation + || (activityLocation != null && (Objects.equals(activityLocation.getId(), expectedActivityLocation.getId()) + || Objects.equals(getLocationId(activityLocation), getLocationId(expectedActivityLocation)))); + } + + private static Long getLocationId(AmpActivityLocation activityLocation) { + return activityLocation != null && activityLocation.getLocation() != null + ? activityLocation.getLocation().getId() : null; + } } diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java b/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java index 1f86eb6305a..1598832b79e 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java @@ -286,6 +286,8 @@ public static AmpActivityVersion saveActivityNewVersion(AmpActivityVersion a, a.setAmpActivityGroup(group); updateMultiStakeholderField(a); + AmpActivityVersion indicatorDisaggregationSource = a; + if (createNewVersion && a.getAmpActivityId() == null) { a = (AmpActivityVersion) session.merge(a); if (!newActivity) { @@ -317,9 +319,9 @@ public static AmpActivityVersion saveActivityNewVersion(AmpActivityVersion a, session.save(a); } else { // session.saveOrUpdate(a); - session.merge(a); + a = (AmpActivityVersion) session.merge(a); } - saveIndicatorDisaggregationValues(a, session); + saveIndicatorDisaggregationValues(indicatorDisaggregationSource, a, createNewVersion, session); session.flush(); @@ -338,18 +340,24 @@ public static AmpActivityVersion saveActivityNewVersion(AmpActivityVersion a, return a; } - private static void saveIndicatorDisaggregationValues(AmpActivityVersion activity, Session session) { - if (activity.getIndicators() == null) { + private static void saveIndicatorDisaggregationValues(AmpActivityVersion indicatorSourceActivity, + AmpActivityVersion activity, boolean createNewVersion, + Session session) { + if (indicatorSourceActivity == null || indicatorSourceActivity.getIndicators() == null) { return; } Map disaggregationValues = new LinkedHashMap<>(); Map indicators = new HashMap<>(); - for (IndicatorActivity indicatorActivity : activity.getIndicators()) { - AmpIndicator indicator = indicatorActivity.getIndicator(); - if (indicator == null || indicator.getDisaggregationValues() == null) { + Set sourceActivityLocationIds = getActivityLocationIds(indicatorSourceActivity); + Set sourceLocationIds = getLocationIds(indicatorSourceActivity); + Map> actualValuesByDisaggregation = new HashMap<>(); + for (IndicatorActivity indicatorActivity : indicatorSourceActivity.getIndicators()) { + AmpIndicator submittedIndicator = indicatorActivity.getIndicator(); + if (submittedIndicator == null || submittedIndicator.getDisaggregationValues() == null) { continue; } - for (AmpIndicatorDisaggregationValue disaggregationValue : indicator.getDisaggregationValues()) { + AmpIndicator indicator = getManagedIndicator(submittedIndicator, session); + for (AmpIndicatorDisaggregationValue disaggregationValue : submittedIndicator.getDisaggregationValues()) { Long disaggregationValueId = disaggregationValue.getId(); if (disaggregationValueId == null) { continue; @@ -371,22 +379,41 @@ private static void saveIndicatorDisaggregationValues(AmpActivityVersion activit saveIndicatorGlobalValue(disaggregationValue.getTargetValue(), indicator, session); if (disaggregationValue.getActualValues() != null) { for (AmpIndicatorGlobalValue actualValue : disaggregationValue.getActualValues()) { - actualValue.setType(AmpIndicatorGlobalValue.ACTUAL); - actualValue.setActivityLocation(resolveActivityLocation(activity, - actualValue.getActivityLocation())); - saveIndicatorGlobalValue(actualValue, indicator, session); + if (!matchesActivityLocation(actualValue.getActivityLocation(), sourceActivityLocationIds, + sourceLocationIds)) { + continue; + } + AmpIndicatorGlobalValue valueToSave = getActualValueToSave(actualValue, createNewVersion); + valueToSave.setType(AmpIndicatorGlobalValue.ACTUAL); + valueToSave.setActivityLocation(resolveActivityLocation(activity, + valueToSave.getActivityLocation())); + saveIndicatorGlobalValue(valueToSave, indicator, session); + actualValuesByDisaggregation.computeIfAbsent(entry.getKey(), ignored -> new LinkedHashSet<>()) + .add(valueToSave); } } - session.saveOrUpdate(disaggregationValue); } session.flush(); Set activityLocationIds = getActivityLocationIds(activity); for (AmpIndicatorDisaggregationValue disaggregationValue : disaggregationValues.values()) { - syncDisaggregationActualValueLinks(disaggregationValue, activityLocationIds, session); + syncDisaggregationActualValueLinks(disaggregationValue, + actualValuesByDisaggregation.getOrDefault(disaggregationValue.getId(), Collections.emptySet()), + activityLocationIds, session); } } + private static AmpIndicatorGlobalValue getActualValueToSave(AmpIndicatorGlobalValue actualValue, + boolean createNewVersion) { + if (!createNewVersion || actualValue.getId() == null) { + return actualValue; + } + AmpIndicatorGlobalValue clonedValue = new AmpIndicatorGlobalValue(AmpIndicatorGlobalValue.ACTUAL); + actualValue.copyValuesTo(clonedValue); + clonedValue.setId(null); + return clonedValue; + } + private static void mergeActualValues(AmpIndicatorDisaggregationValue target, AmpIndicatorDisaggregationValue source) { if (source.getActualValues() == null || source.getActualValues().isEmpty()) { @@ -408,7 +435,28 @@ private static Set getActivityLocationIds(AmpActivityVersion activity) { .collect(Collectors.toCollection(LinkedHashSet::new)); } + private static Set getLocationIds(AmpActivityVersion activity) { + if (activity.getLocations() == null) { + return Collections.emptySet(); + } + return activity.getLocations().stream() + .map(AmpActivityLocation::getLocation) + .filter(Objects::nonNull) + .map(AmpCategoryValueLocations::getId) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + private static boolean matchesActivityLocation(AmpActivityLocation activityLocation, + Set activityLocationIds, Set locationIds) { + return activityLocation != null + && ((activityLocation.getId() != null && activityLocationIds.contains(activityLocation.getId())) + || (activityLocation.getLocation() != null + && locationIds.contains(activityLocation.getLocation().getId()))); + } + private static void syncDisaggregationActualValueLinks(AmpIndicatorDisaggregationValue disaggregationValue, + Collection actualValues, Set activityLocationIds, Session session) { if (disaggregationValue.getId() == null || activityLocationIds.isEmpty()) { return; @@ -424,7 +472,7 @@ private static void syncDisaggregationActualValueLinks(AmpIndicatorDisaggregatio deleteLinks.executeUpdate(); Set linkedValueIds = new HashSet<>(); - for (AmpIndicatorGlobalValue actualValue : disaggregationValue.getActualValues()) { + for (AmpIndicatorGlobalValue actualValue : actualValues) { if (actualValue.getId() == null || actualValue.getActivityLocation() == null || actualValue.getActivityLocation().getId() == null || !activityLocationIds.contains(actualValue.getActivityLocation().getId()) @@ -458,6 +506,13 @@ private static boolean sameActivityLocation(AmpActivityLocation first, AmpActivi && Objects.equals(first.getLocation().getId(), second.getLocation().getId())); } + private static AmpIndicator getManagedIndicator(AmpIndicator indicator, Session session) { + if (indicator == null || indicator.getIndicatorId() == null) { + return indicator; + } + return session.load(AmpIndicator.class, indicator.getIndicatorId()); + } + private static void saveIndicatorGlobalValue(AmpIndicatorGlobalValue value, AmpIndicator indicator, Session session) { if (value == null) { From fc9d7ec279feafb131ccbca10c4fb31bd4b4fc48 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Sat, 20 Jun 2026 23:38:19 +0300 Subject: [PATCH 04/14] AMP-31133 : Indicator ME display and saving issues --- .../amp/onepager/util/ActivityUtil.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java b/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java index 1598832b79e..64a0f5c7e78 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java @@ -387,9 +387,9 @@ private static void saveIndicatorDisaggregationValues(AmpActivityVersion indicat valueToSave.setType(AmpIndicatorGlobalValue.ACTUAL); valueToSave.setActivityLocation(resolveActivityLocation(activity, valueToSave.getActivityLocation())); - saveIndicatorGlobalValue(valueToSave, indicator, session); + AmpIndicatorGlobalValue savedValue = saveIndicatorGlobalValue(valueToSave, indicator, session); actualValuesByDisaggregation.computeIfAbsent(entry.getKey(), ignored -> new LinkedHashSet<>()) - .add(valueToSave); + .add(savedValue); } } } @@ -513,13 +513,20 @@ private static AmpIndicator getManagedIndicator(AmpIndicator indicator, Session return session.load(AmpIndicator.class, indicator.getIndicatorId()); } - private static void saveIndicatorGlobalValue(AmpIndicatorGlobalValue value, AmpIndicator indicator, - Session session) { + private static AmpIndicatorGlobalValue saveIndicatorGlobalValue(AmpIndicatorGlobalValue value, AmpIndicator indicator, + Session session) { if (value == null) { - return; + return null; } value.setIndicator(indicator); - session.saveOrUpdate(value); + if (value.getId() == null) { + session.save(value); + return value; + } + if (session.contains(value)) { + return value; + } + return (AmpIndicatorGlobalValue) session.merge(value); } private static void cleanObjectFromSession(Session session, Class objectClass, Long id) From 5c83968db4cbed68b4e265cfc702242ddfdeb97f Mon Sep 17 00:00:00 2001 From: brianbrix Date: Sat, 20 Jun 2026 23:52:34 +0300 Subject: [PATCH 05/14] AMP-31133 : Indicator ME display and saving issues --- .../org/dgfoundation/amp/onepager/util/ActivityUtil.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java b/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java index 64a0f5c7e78..e564825b8ec 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java @@ -357,7 +357,9 @@ private static void saveIndicatorDisaggregationValues(AmpActivityVersion indicat continue; } AmpIndicator indicator = getManagedIndicator(submittedIndicator, session); + logger.info("Count of disaggregation values for indicator " + indicator.getIndicatorId() + " is " + submittedIndicator.getDisaggregationValues().size()); for (AmpIndicatorDisaggregationValue disaggregationValue : submittedIndicator.getDisaggregationValues()) { + logger.info("Processing disaggregation value with id " + disaggregationValue.getId() + " for indicator " + indicator.getIndicatorId()); Long disaggregationValueId = disaggregationValue.getId(); if (disaggregationValueId == null) { continue; @@ -367,7 +369,11 @@ private static void saveIndicatorDisaggregationValues(AmpActivityVersion indicat if (mergedValue != disaggregationValue) { mergeActualValues(mergedValue, disaggregationValue); } + logger.info("Indicators with id " + indicator.getIndicatorId() + " and disaggregation value id " + disaggregationValueId + + " has " + mergedValue.getActualValues().size() + " actual values after merge"); indicators.put(disaggregationValueId, indicator); + logger.info("Values count this far: "+ indicators.size()); + } } From 9d99736859a6b58180991c75ec78fcae575dc151 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Sat, 20 Jun 2026 23:53:58 +0300 Subject: [PATCH 06/14] AMP-31133 : Indicator ME display and saving issues --- .../java/org/dgfoundation/amp/onepager/util/ActivityUtil.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java b/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java index e564825b8ec..1d365880c17 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java @@ -343,6 +343,8 @@ public static AmpActivityVersion saveActivityNewVersion(AmpActivityVersion a, private static void saveIndicatorDisaggregationValues(AmpActivityVersion indicatorSourceActivity, AmpActivityVersion activity, boolean createNewVersion, Session session) { + logger.info("Source activity: "+indicatorSourceActivity); + logger.info("Source activity indicators: "+(indicatorSourceActivity != null ? indicatorSourceActivity.getIndicators() : null)); if (indicatorSourceActivity == null || indicatorSourceActivity.getIndicators() == null) { return; } @@ -353,6 +355,7 @@ private static void saveIndicatorDisaggregationValues(AmpActivityVersion indicat Map> actualValuesByDisaggregation = new HashMap<>(); for (IndicatorActivity indicatorActivity : indicatorSourceActivity.getIndicators()) { AmpIndicator submittedIndicator = indicatorActivity.getIndicator(); + logger.info("Processing indicator with id " + submittedIndicator.getIndicatorId() + " for activity " + indicatorSourceActivity.getAmpActivityId()); if (submittedIndicator == null || submittedIndicator.getDisaggregationValues() == null) { continue; } From 9438616b0742f61ea010eea93ba821bf7ed5befe Mon Sep 17 00:00:00 2001 From: brianbrix Date: Sun, 21 Jun 2026 00:15:54 +0300 Subject: [PATCH 07/14] AMP-31133 : Indicator ME display and saving issues --- .../AmpMEDisaggregationActualValuesPanel.java | 17 ++++-- .../amp/onepager/util/ActivityUtil.java | 55 +++++++++++++------ .../activity/ActivityInterchangeUtils.java | 8 ++- 3 files changed, 56 insertions(+), 24 deletions(-) diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEDisaggregationActualValuesPanel.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEDisaggregationActualValuesPanel.java index 85472811d4e..2b86bada89a 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEDisaggregationActualValuesPanel.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEDisaggregationActualValuesPanel.java @@ -92,10 +92,19 @@ public void onClick(AjaxRequestTarget target) { private boolean matchesActivityLocation(AmpActivityLocation itemLocation) { AmpActivityLocation activityLocation = getActivityLocation(); - return activityLocation == null - || itemLocation == activityLocation - || (itemLocation != null && (Objects.equals(itemLocation.getId(), activityLocation.getId()) - || Objects.equals(getLocationId(itemLocation), getLocationId(activityLocation)))); + if (activityLocation == null) { + return itemLocation == null; + } + if (itemLocation == activityLocation) { + return true; + } + if (itemLocation == null) { + return false; + } + if (itemLocation.getId() != null || activityLocation.getId() != null) { + return Objects.equals(itemLocation.getId(), activityLocation.getId()); + } + return Objects.equals(getLocationId(itemLocation), getLocationId(activityLocation)); } private AmpActivityLocation getActivityLocation() { diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java b/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java index 1d365880c17..4704b3c8490 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java @@ -321,7 +321,8 @@ public static AmpActivityVersion saveActivityNewVersion(AmpActivityVersion a, // session.saveOrUpdate(a); a = (AmpActivityVersion) session.merge(a); } - saveIndicatorDisaggregationValues(indicatorDisaggregationSource, a, createNewVersion, session); + saveIndicatorDisaggregationValues(indicatorDisaggregationSource, + createNewVersion ? oldA : indicatorDisaggregationSource, a, createNewVersion, session); session.flush(); @@ -341,28 +342,25 @@ public static AmpActivityVersion saveActivityNewVersion(AmpActivityVersion a, } private static void saveIndicatorDisaggregationValues(AmpActivityVersion indicatorSourceActivity, + AmpActivityVersion sourceLocationActivity, AmpActivityVersion activity, boolean createNewVersion, Session session) { - logger.info("Source activity: "+indicatorSourceActivity); - logger.info("Source activity indicators: "+(indicatorSourceActivity != null ? indicatorSourceActivity.getIndicators() : null)); if (indicatorSourceActivity == null || indicatorSourceActivity.getIndicators() == null) { return; } Map disaggregationValues = new LinkedHashMap<>(); Map indicators = new HashMap<>(); - Set sourceActivityLocationIds = getActivityLocationIds(indicatorSourceActivity); - Set sourceLocationIds = getLocationIds(indicatorSourceActivity); + Set sourceActivityLocationIds = getActivityLocationIds(sourceLocationActivity); + Set sourceLocationIds = getLocationIds(sourceLocationActivity); Map> actualValuesByDisaggregation = new HashMap<>(); + Map>> processedActualValueKeysByDisaggregation = new HashMap<>(); for (IndicatorActivity indicatorActivity : indicatorSourceActivity.getIndicators()) { AmpIndicator submittedIndicator = indicatorActivity.getIndicator(); - logger.info("Processing indicator with id " + submittedIndicator.getIndicatorId() + " for activity " + indicatorSourceActivity.getAmpActivityId()); if (submittedIndicator == null || submittedIndicator.getDisaggregationValues() == null) { continue; } AmpIndicator indicator = getManagedIndicator(submittedIndicator, session); - logger.info("Count of disaggregation values for indicator " + indicator.getIndicatorId() + " is " + submittedIndicator.getDisaggregationValues().size()); for (AmpIndicatorDisaggregationValue disaggregationValue : submittedIndicator.getDisaggregationValues()) { - logger.info("Processing disaggregation value with id " + disaggregationValue.getId() + " for indicator " + indicator.getIndicatorId()); Long disaggregationValueId = disaggregationValue.getId(); if (disaggregationValueId == null) { continue; @@ -372,11 +370,7 @@ private static void saveIndicatorDisaggregationValues(AmpActivityVersion indicat if (mergedValue != disaggregationValue) { mergeActualValues(mergedValue, disaggregationValue); } - logger.info("Indicators with id " + indicator.getIndicatorId() + " and disaggregation value id " + disaggregationValueId - + " has " + mergedValue.getActualValues().size() + " actual values after merge"); indicators.put(disaggregationValueId, indicator); - logger.info("Values count this far: "+ indicators.size()); - } } @@ -392,10 +386,15 @@ private static void saveIndicatorDisaggregationValues(AmpActivityVersion indicat sourceLocationIds)) { continue; } + AmpActivityLocation activityLocation = resolveActivityLocation(activity, + actualValue.getActivityLocation()); + if (!processedActualValueKeysByDisaggregation.computeIfAbsent(entry.getKey(), ignored -> new HashSet<>()) + .add(getActualValueKey(actualValue, activityLocation))) { + continue; + } AmpIndicatorGlobalValue valueToSave = getActualValueToSave(actualValue, createNewVersion); valueToSave.setType(AmpIndicatorGlobalValue.ACTUAL); - valueToSave.setActivityLocation(resolveActivityLocation(activity, - valueToSave.getActivityLocation())); + valueToSave.setActivityLocation(activityLocation); AmpIndicatorGlobalValue savedValue = saveIndicatorGlobalValue(valueToSave, indicator, session); actualValuesByDisaggregation.computeIfAbsent(entry.getKey(), ignored -> new LinkedHashSet<>()) .add(savedValue); @@ -423,6 +422,17 @@ private static AmpIndicatorGlobalValue getActualValueToSave(AmpIndicatorGlobalVa return clonedValue; } + private static List getActualValueKey(AmpIndicatorGlobalValue actualValue, + AmpActivityLocation activityLocation) { + return Arrays.asList( + activityLocation != null ? activityLocation.getId() : null, + getLocationId(activityLocation), + actualValue.getOriginalValue(), + actualValue.getOriginalValueDate(), + actualValue.getRevisedValue(), + actualValue.getRevisedValueDate()); + } + private static void mergeActualValues(AmpIndicatorDisaggregationValue target, AmpIndicatorDisaggregationValue source) { if (source.getActualValues() == null || source.getActualValues().isEmpty()) { @@ -456,12 +466,21 @@ private static Set getLocationIds(AmpActivityVersion activity) { .collect(Collectors.toCollection(LinkedHashSet::new)); } + private static Long getLocationId(AmpActivityLocation activityLocation) { + return activityLocation != null && activityLocation.getLocation() != null + ? activityLocation.getLocation().getId() : null; + } + private static boolean matchesActivityLocation(AmpActivityLocation activityLocation, Set activityLocationIds, Set locationIds) { - return activityLocation != null - && ((activityLocation.getId() != null && activityLocationIds.contains(activityLocation.getId())) - || (activityLocation.getLocation() != null - && locationIds.contains(activityLocation.getLocation().getId()))); + if (activityLocation == null) { + return false; + } + if (activityLocation.getId() != null) { + return !activityLocationIds.isEmpty() && activityLocationIds.contains(activityLocation.getId()); + } + return activityLocation.getLocation() != null + && locationIds.contains(activityLocation.getLocation().getId()); } private static void syncDisaggregationActualValueLinks(AmpIndicatorDisaggregationValue disaggregationValue, diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/activity/ActivityInterchangeUtils.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/activity/ActivityInterchangeUtils.java index 30b135581f9..8da9bbdfb7b 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/activity/ActivityInterchangeUtils.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/activity/ActivityInterchangeUtils.java @@ -449,8 +449,12 @@ private static boolean matchesActivityLocation(AmpActivityLocation activityLocat if (expectedActivityLocation == null) { return activityLocation == null; } - return Objects.equals(getAmpActivityLocationId(activityLocation), getAmpActivityLocationId(expectedActivityLocation)) - || Objects.equals(getLocationId(activityLocation), getLocationId(expectedActivityLocation)); + Long activityLocationId = getAmpActivityLocationId(activityLocation); + Long expectedActivityLocationId = getAmpActivityLocationId(expectedActivityLocation); + if (activityLocationId != null || expectedActivityLocationId != null) { + return Objects.equals(activityLocationId, expectedActivityLocationId); + } + return Objects.equals(getLocationId(activityLocation), getLocationId(expectedActivityLocation)); } private static Long parseLong(Object value) { From 33006900bdb6da749daac77c5142efa56d6d8c45 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Sun, 21 Jun 2026 23:59:12 +0300 Subject: [PATCH 08/14] AMP-31133 : Disaggregated values cleared on admin indicator update; Actual values not being saved --- .../AmpMEValuesFormTableFeaturePanel.java | 31 ++++- .../activity/ActivityInterchangeUtils.java | 22 ++-- .../manager/IndicatorManagerService.java | 118 +++++++++++++----- 3 files changed, 126 insertions(+), 45 deletions(-) diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpMEValuesFormTableFeaturePanel.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpMEValuesFormTableFeaturePanel.java index bb40f4e972d..a640ff92c58 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpMEValuesFormTableFeaturePanel.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpMEValuesFormTableFeaturePanel.java @@ -1,6 +1,5 @@ package org.dgfoundation.amp.onepager.components.features.tables; -import org.apache.log4j.Logger; import org.apache.wicket.AttributeModifier; import org.apache.wicket.markup.html.panel.EmptyPanel; import org.apache.wicket.model.IModel; @@ -14,10 +13,10 @@ import org.digijava.module.aim.dbentity.AmpIndicator; import org.digijava.module.aim.dbentity.AmpIndicatorValue; import org.digijava.module.aim.dbentity.IndicatorActivity; +import java.util.Objects; import java.util.Set; public abstract class AmpMEValuesFormTableFeaturePanel extends AmpMEFormTableFeaturePanel { - private static Logger logger = Logger.getLogger(AmpMEValuesFormTableFeaturePanel.class); protected IModel> parentModel; protected IModel> setModel; @@ -35,18 +34,42 @@ public AmpMEValuesFormTableFeaturePanel( setModel = new AbstractMixedSetModel(parentModel) { @Override public boolean condition(AmpIndicatorValue item) { - return item.getValueType() == AmpIndicatorValue.ACTUAL && item.getActivityLocation() == location.getObject(); + return item.getValueType() == AmpIndicatorValue.ACTUAL + && matchesActivityLocation(item.getActivityLocation(), location.getObject()); } }; setBaseTargetModel = new AbstractMixedSetModel(parentModel) { @Override public boolean condition(AmpIndicatorValue item) { - return (item.getValueType() == AmpIndicatorValue.BASE || item.getValueType() == AmpIndicatorValue.TARGET) && item.getActivityLocation() == location.getObject(); + return (item.getValueType() == AmpIndicatorValue.BASE || item.getValueType() == AmpIndicatorValue.TARGET) + && matchesActivityLocation(item.getActivityLocation(), location.getObject()); } }; } + private static boolean matchesActivityLocation(AmpActivityLocation activityLocation, + AmpActivityLocation expectedActivityLocation) { + if (expectedActivityLocation == null) { + return activityLocation == null; + } + if (activityLocation == expectedActivityLocation) { + return true; + } + if (activityLocation == null) { + return false; + } + if (activityLocation.getId() != null || expectedActivityLocation.getId() != null) { + return Objects.equals(activityLocation.getId(), expectedActivityLocation.getId()); + } + return Objects.equals(getLocationId(activityLocation), getLocationId(expectedActivityLocation)); + } + + private static Long getLocationId(AmpActivityLocation activityLocation) { + return activityLocation != null && activityLocation.getLocation() != null + ? activityLocation.getLocation().getId() : null; + } + protected AmpTextFieldPanel getActualValue(ListItem item){ return new AmpTextFieldPanel("actualValue", new PropertyModel<>(item.getModel(), "value"), "Actual Value") { public IConverter getInternalConverter(java.lang.Class type) { diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/activity/ActivityInterchangeUtils.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/activity/ActivityInterchangeUtils.java index 8da9bbdfb7b..7f83e192450 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/activity/ActivityInterchangeUtils.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/activity/ActivityInterchangeUtils.java @@ -430,20 +430,23 @@ private static AmpActivityLocation getActivityLocation(Long ampActivityLocationI } private static AmpActivityLocation getActivityLocation(Long projectId, Long ampActivityLocationId) { - AmpActivityLocation activityLocation = getActivityLocation(ampActivityLocationId); - if (activityLocation != null || projectId == null) { - return activityLocation; + if (projectId == null) { + return getActivityLocation(ampActivityLocationId); } AmpActivityVersion activity = loadActivity(projectId); + if (activity.getLocations() != null && ampActivityLocationId != null) { + Optional activityLocation = activity.getLocations().stream() + .filter(location -> Objects.equals(getAmpActivityLocationId(location), ampActivityLocationId) + || Objects.equals(getLocationId(location), ampActivityLocationId)) + .findFirst(); + if (activityLocation.isPresent()) { + return activityLocation.get(); + } + } return activity.getLocations() != null && activity.getLocations().size() == 1 ? activity.getLocations().iterator().next() : null; } - private static boolean matchesActivityLocation(AmpActivityLocation activityLocation, Long expectedActivityLocationId) { - return expectedActivityLocationId == null - || expectedActivityLocationId.equals(getAmpActivityLocationId(activityLocation)); - } - private static boolean matchesActivityLocation(AmpActivityLocation activityLocation, AmpActivityLocation expectedActivityLocation) { if (expectedActivityLocation == null) { @@ -475,6 +478,7 @@ private static void addActualIndicatorValues(Optional indicatorsObject, for (Map indicator : indicatorsList) { List actualValues = new ArrayList<>(); Long activityLocationId = parseLong(indicator.get("activity_location")); + AmpActivityLocation activityLocation = getActivityLocation(projectId, activityLocationId); AmpIndicator ind = new AmpIndicator(); ind.setIndicatorId(parseLong(indicator.get("indicator"))); @@ -493,7 +497,7 @@ private static void addActualIndicatorValues(Optional indicatorsObject, if (result == null) { continue; } - if (!matchesActivityLocation(result.getActivityLocation(), activityLocationId)) { + if (!matchesActivityLocation(result.getActivityLocation(), activityLocation)) { continue; } if (result.getValues() != null) { diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java index 5eae3b3dd57..c5c9201f294 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java @@ -25,8 +25,10 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -564,44 +566,28 @@ public MEIndicatorDTO updateMEIndicator(final Long indicatorId, final MEIndicato session.update(indicator); // Update disaggregation values if (indRequest.getDisaggregationValues() != null) { - // Always remove all existing disaggregation values before updating - if (indicator.getDisaggregationValues() != null && !indicator.getDisaggregationValues().isEmpty()) { - for (AmpIndicatorDisaggregationValue existing : indicator.getDisaggregationValues()) { - session.delete(existing); - } - indicator.getDisaggregationValues().clear(); - session.update(indicator); - session.flush(); - } - // Now process incoming disaggregation values as usual + Map existingById = getDisaggregationValuesById(indicator); + Set valuesToKeep = new HashSet<>(); for (AmpIndicatorDisaggregationValueDto dto : indRequest.getDisaggregationValues()) { - AmpIndicatorDisaggregationValue disaggValue = new AmpIndicatorDisaggregationValue(); - if (dto.getParentCategoryId() != null) { - AmpCategoryValue parentCat = session.get(AmpCategoryValue.class, dto.getParentCategoryId()); - disaggValue.setParentCategory(parentCat); - } - if (dto.getChildCategoryId() != null) { - AmpCategoryValue childCat = (AmpCategoryValue) session.get(AmpCategoryValue.class, dto.getChildCategoryId()); - disaggValue.setChildCategory(childCat); - } - if (dto.getBaseValue() != null) { - disaggValue.setBaseValue(dto.getBaseValue()); - disaggValue.getBaseValue().setType(AmpIndicatorGlobalValue.BASE); + AmpIndicatorDisaggregationValue disaggValue = findDisaggregationValue(dto, indicator, + existingById); + if (disaggValue == null) { + disaggValue = new AmpIndicatorDisaggregationValue(); } - if (dto.getTargetValue() != null) { - disaggValue.setTargetValue(dto.getTargetValue()); - disaggValue.getTargetValue().setType(AmpIndicatorGlobalValue.TARGET); + updateDisaggregationValue(disaggValue, dto, indicator, session); + if (disaggValue.getId() == null) { + session.save(disaggValue); } - disaggValue.setIndicator(indicator); - session.save(disaggValue); indicator.getDisaggregationValues().add(disaggValue); + valuesToKeep.add(disaggValue); } - } else { - // If no disaggregation values in request, remove all - for (AmpIndicatorDisaggregationValue existing : indicator.getDisaggregationValues()) { - session.delete(existing); + Set valuesToRemove = indicator.getDisaggregationValues().stream() + .filter(existing -> !valuesToKeep.contains(existing)) + .collect(Collectors.toSet()); + for (AmpIndicatorDisaggregationValue valueToRemove : valuesToRemove) { + indicator.getDisaggregationValues().remove(valueToRemove); + session.delete(valueToRemove); } - indicator.getDisaggregationValues().clear(); } if (program != null) { try { @@ -618,6 +604,74 @@ public MEIndicatorDTO updateMEIndicator(final Long indicatorId, final MEIndicato ApiError.toError("Indicator with id " + indicatorId + " not found")); } + private Map getDisaggregationValuesById(AmpIndicator indicator) { + Map existingById = new HashMap<>(); + for (AmpIndicatorDisaggregationValue existing : indicator.getDisaggregationValues()) { + if (existing.getId() != null) { + existingById.put(existing.getId(), existing); + } + } + return existingById; + } + + private AmpIndicatorDisaggregationValue findDisaggregationValue(AmpIndicatorDisaggregationValueDto dto, + AmpIndicator indicator, + Map existingById) { + if (dto.getId() != null && existingById.containsKey(dto.getId())) { + return existingById.get(dto.getId()); + } + return indicator.getDisaggregationValues().stream() + .filter(existing -> sameDisaggregationCategories(existing, dto)) + .findFirst() + .orElse(null); + } + + private boolean sameDisaggregationCategories(AmpIndicatorDisaggregationValue existing, + AmpIndicatorDisaggregationValueDto dto) { + return Objects.equals(getCategoryId(existing.getParentCategory()), dto.getParentCategoryId()) + && Objects.equals(getCategoryId(existing.getChildCategory()), dto.getChildCategoryId()); + } + + private Long getCategoryId(AmpCategoryValue categoryValue) { + return categoryValue != null ? categoryValue.getId() : null; + } + + private void updateDisaggregationValue(AmpIndicatorDisaggregationValue disaggValue, + AmpIndicatorDisaggregationValueDto dto, + AmpIndicator indicator, + Session session) { + disaggValue.setIndicator(indicator); + if (dto.getParentCategoryId() != null) { + disaggValue.setParentCategory(session.get(AmpCategoryValue.class, dto.getParentCategoryId())); + } + if (dto.getChildCategoryId() != null) { + disaggValue.setChildCategory(session.get(AmpCategoryValue.class, dto.getChildCategoryId())); + } else { + disaggValue.setChildCategory(null); + } + disaggValue.setBaseValue(saveIndicatorGlobalValue(dto.getBaseValue(), indicator, + AmpIndicatorGlobalValue.BASE, session)); + disaggValue.setTargetValue(saveIndicatorGlobalValue(dto.getTargetValue(), indicator, + AmpIndicatorGlobalValue.TARGET, session)); + } + + private AmpIndicatorGlobalValue saveIndicatorGlobalValue(AmpIndicatorGlobalValue value, AmpIndicator indicator, + int type, Session session) { + if (value == null) { + return null; + } + value.setType(type); + value.setIndicator(indicator); + if (value.getId() == null) { + session.save(value); + return value; + } + if (session.contains(value)) { + return value; + } + return (AmpIndicatorGlobalValue) session.merge(value); + } + public void validateProgramSettingsAndGlobalValues(final MEIndicatorDTO indicatorRequest, final AmpIndicator indicator) { if (indicatorRequest.getProgramId() != null) { From 35b18327bc24337bc6c6ca373cbd5ff6a7e865d1 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 22 Jun 2026 00:42:25 +0300 Subject: [PATCH 09/14] AMP-31133 : Actual values not being saved ; Admin disaggregation values not being updated --- .../items/AmpMEIndicatorFeaturePanel.java | 24 ++++++++++++ .../amp/onepager/util/ActivityUtil.java | 39 +++++++++++++++++-- .../manager/IndicatorManagerService.java | 24 +++++++++--- 3 files changed, 78 insertions(+), 9 deletions(-) diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java index 42f97a989f6..e5787b1ee46 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java @@ -99,6 +99,12 @@ public AmpMEIndicatorFeaturePanel(String id, String fmName, final IModel Date: Mon, 22 Jun 2026 00:46:27 +0300 Subject: [PATCH 10/14] AMP-31133 : Actual values not being saved ; Admin disaggregation values not being updated --- .../features/items/AmpMEIndicatorFeaturePanel.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java index e5787b1ee46..d1b7a04898a 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java @@ -99,13 +99,14 @@ public AmpMEIndicatorFeaturePanel(String id, String fmName, final IModel() { @@ -118,13 +119,14 @@ protected String load() { return "N/A"; } } + }) { @Override protected void onConfigure() { super.onConfigure(); boolean fmVisible = isVisible(); setVisible(fmVisible && !hasDisaggregation); } - }); + }; add(indicatorBaseDateLabel); final Label indicatorTargetValueLabel = new Label("target", new LoadableDetachableModel() { @@ -132,13 +134,14 @@ protected void onConfigure() { protected String load() { return globalTargetVal.getOriginalValue() != null ? String.valueOf(globalTargetVal.getOriginalValue()) : "N/A"; } + }) { @Override protected void onConfigure() { super.onConfigure(); boolean fmVisible = isVisible(); setVisible(fmVisible && !hasDisaggregation); } - }); + }; add(indicatorTargetValueLabel); final Label indicatorTargetDateLabel = new Label("targetDate", new LoadableDetachableModel() { @@ -151,13 +154,14 @@ protected String load() { return "N/A"; } } + }) { @Override protected void onConfigure() { super.onConfigure(); boolean fmVisible = isVisible(); setVisible(fmVisible && !hasDisaggregation); } - }); + }; add(indicatorTargetDateLabel); AmpMEActualValuesFormTableFeaturePanel valuesTable = new AmpMEActualValuesFormTableFeaturePanel( From 2cf8632faf9ac4f50d4afd1c8b3f040b8f40cbd9 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 22 Jun 2026 01:18:02 +0300 Subject: [PATCH 11/14] AMP-31133 : Admin not able to update values ; Hide labels --- .../items/AmpMEIndicatorFeaturePanel.html | 2 +- .../items/AmpMEIndicatorFeaturePanel.java | 51 +++++--------- .../manager/IndicatorManagerService.java | 68 +++++++++++++++---- 3 files changed, 74 insertions(+), 47 deletions(-) diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.html b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.html index e1af40e4c4d..a43e011276d 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.html +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.html @@ -16,7 +16,7 @@
-
+
Base Values: Base Date: Target Values: Target Date:
diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java index d1b7a04898a..8c46b5c3d01 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEIndicatorFeaturePanel.java @@ -2,6 +2,7 @@ import org.apache.log4j.Logger; import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; @@ -94,12 +95,7 @@ public AmpMEIndicatorFeaturePanel(String id, String fmName, final IModel() { - @Override - protected String load() { - return globalBaseVal.getOriginalValue() != null ? String.valueOf(globalBaseVal.getOriginalValue()) : "N/A"; - } - }) { + final WebMarkupContainer baseTargetSummary = new WebMarkupContainer("baseTargetSummary") { @Override protected void onConfigure() { super.onConfigure(); @@ -107,7 +103,14 @@ protected void onConfigure() { setVisible(fmVisible && !hasDisaggregation); } }; - add(indicatorBaseValueLabel); + + final Label indicatorBaseValueLabel = new Label("base", new LoadableDetachableModel() { + @Override + protected String load() { + return globalBaseVal.getOriginalValue() != null ? String.valueOf(globalBaseVal.getOriginalValue()) : "N/A"; + } + }); + baseTargetSummary.add(indicatorBaseValueLabel); final Label indicatorBaseDateLabel = new Label("baseDate", new LoadableDetachableModel() { @Override @@ -119,30 +122,16 @@ protected String load() { return "N/A"; } } - }) { - @Override - protected void onConfigure() { - super.onConfigure(); - boolean fmVisible = isVisible(); - setVisible(fmVisible && !hasDisaggregation); - } - }; - add(indicatorBaseDateLabel); + }); + baseTargetSummary.add(indicatorBaseDateLabel); final Label indicatorTargetValueLabel = new Label("target", new LoadableDetachableModel() { @Override protected String load() { return globalTargetVal.getOriginalValue() != null ? String.valueOf(globalTargetVal.getOriginalValue()) : "N/A"; } - }) { - @Override - protected void onConfigure() { - super.onConfigure(); - boolean fmVisible = isVisible(); - setVisible(fmVisible && !hasDisaggregation); - } - }; - add(indicatorTargetValueLabel); + }); + baseTargetSummary.add(indicatorTargetValueLabel); final Label indicatorTargetDateLabel = new Label("targetDate", new LoadableDetachableModel() { @Override @@ -154,15 +143,9 @@ protected String load() { return "N/A"; } } - }) { - @Override - protected void onConfigure() { - super.onConfigure(); - boolean fmVisible = isVisible(); - setVisible(fmVisible && !hasDisaggregation); - } - }; - add(indicatorTargetDateLabel); + }); + baseTargetSummary.add(indicatorTargetDateLabel); + add(baseTargetSummary); AmpMEActualValuesFormTableFeaturePanel valuesTable = new AmpMEActualValuesFormTableFeaturePanel( "valuesSubsection", indicator, conn, location,"Actual Values", false, 7) { diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java index 712d2e809d1..b0611a29aeb 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java @@ -518,22 +518,16 @@ public MEIndicatorDTO updateMEIndicator(final Long indicatorId, final MEIndicato indicator.setProgram(program); } - Set updatedValues = new HashSet<>(); if (indRequest.getBaseValue() != null) { - AmpIndicatorGlobalValue validatedBaseValues = validateBaseValues(indRequest); - updatedValues.add(validatedBaseValues); - indicator.getIndicatorValues().add(validatedBaseValues); - indicator.getBaseValue().setIndicator(indicator); + validateBaseValues(indRequest); + updateTopLevelIndicatorGlobalValue(indicator, indRequest.getBaseValue(), AmpIndicatorGlobalValue.BASE, + session); } if (indRequest.getTargetValue() != null) { - AmpIndicatorGlobalValue validatedTargetValues = validateTargetValues(indRequest); - updatedValues.add(validatedTargetValues); - indicator.getIndicatorValues().add(validatedTargetValues); - indicator.getTargetValue().setIndicator(indicator); + validateTargetValues(indRequest); + updateTopLevelIndicatorGlobalValue(indicator, indRequest.getTargetValue(), + AmpIndicatorGlobalValue.TARGET, session); } - indicator.getIndicatorValues().clear(); - updatedValues.forEach(value -> value.setIndicator(indicator)); - indicator.getIndicatorValues().addAll(updatedValues); Set sectors = indRequest.getSectorIds().stream() .map(id -> (AmpSector) session.get(AmpSector.class, id)) @@ -578,6 +572,7 @@ public MEIndicatorDTO updateMEIndicator(final Long indicatorId, final MEIndicato if (disaggValue.getId() == null) { session.save(disaggValue); } + addDisaggregationGlobalValues(indicator, disaggValue); indicator.getDisaggregationValues().add(disaggValue); valuesToKeep.add(disaggValue); } @@ -586,6 +581,7 @@ public MEIndicatorDTO updateMEIndicator(final Long indicatorId, final MEIndicato .collect(Collectors.toSet()); for (AmpIndicatorDisaggregationValue valueToRemove : valuesToRemove) { indicator.getDisaggregationValues().remove(valueToRemove); + removeDisaggregationGlobalValues(indicator, valueToRemove); session.delete(valueToRemove); } } @@ -637,6 +633,54 @@ private Long getCategoryId(AmpCategoryValue categoryValue) { return categoryValue != null ? categoryValue.getId() : null; } + private void updateTopLevelIndicatorGlobalValue(AmpIndicator indicator, AmpIndicatorGlobalValue submittedValue, + int type, Session session) { + AmpIndicatorGlobalValue currentValue = findTopLevelIndicatorGlobalValue(indicator, type); + AmpIndicatorGlobalValue value = updateIndicatorGlobalValue(currentValue, submittedValue, indicator, type, + session); + if (value != null && !indicator.getIndicatorValues().contains(value)) { + indicator.getIndicatorValues().add(value); + } + } + + private AmpIndicatorGlobalValue findTopLevelIndicatorGlobalValue(AmpIndicator indicator, int type) { + return indicator.getIndicatorValues().stream() + .filter(value -> value.getType() == type) + .filter(value -> isTopLevelIndicatorGlobalValue(indicator, value)) + .findFirst() + .orElse(null); + } + + private boolean isTopLevelIndicatorGlobalValue(AmpIndicator indicator, AmpIndicatorGlobalValue value) { + return indicator.getDisaggregationValues() == null || indicator.getDisaggregationValues().stream() + .noneMatch(disaggregationValue -> value == disaggregationValue.getBaseValue() + || value == disaggregationValue.getTargetValue() + || Objects.equals(value.getId(), getGlobalValueId(disaggregationValue.getBaseValue())) + || Objects.equals(value.getId(), getGlobalValueId(disaggregationValue.getTargetValue()))); + } + + private Long getGlobalValueId(AmpIndicatorGlobalValue value) { + return value != null ? value.getId() : null; + } + + private void removeDisaggregationGlobalValues(AmpIndicator indicator, + AmpIndicatorDisaggregationValue disaggregationValue) { + indicator.getIndicatorValues().remove(disaggregationValue.getBaseValue()); + indicator.getIndicatorValues().remove(disaggregationValue.getTargetValue()); + } + + private void addDisaggregationGlobalValues(AmpIndicator indicator, + AmpIndicatorDisaggregationValue disaggregationValue) { + addIndicatorGlobalValue(indicator, disaggregationValue.getBaseValue()); + addIndicatorGlobalValue(indicator, disaggregationValue.getTargetValue()); + } + + private void addIndicatorGlobalValue(AmpIndicator indicator, AmpIndicatorGlobalValue value) { + if (value != null && !indicator.getIndicatorValues().contains(value)) { + indicator.getIndicatorValues().add(value); + } + } + private void updateDisaggregationValue(AmpIndicatorDisaggregationValue disaggValue, AmpIndicatorDisaggregationValueDto dto, AmpIndicator indicator, From 30cb2888a317401180b62db775127d0fa4c628a0 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 22 Jun 2026 07:40:59 +0300 Subject: [PATCH 12/14] AMP-31133 : Add delete confirmation modal for disaggregation options --- .../config/initialTranslations.json | 9 ++ .../pages/DisaggregationManagerPage.tsx | 118 +++++++++++++++++- .../service/DisaggregationService.java | 20 +++ 3 files changed, 144 insertions(+), 3 deletions(-) diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/config/initialTranslations.json b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/config/initialTranslations.json index c1b21c962b9..19ab125cdb7 100644 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/config/initialTranslations.json +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/config/initialTranslations.json @@ -197,12 +197,21 @@ "amp.disaggregationmanager:actions": "Actions", "amp.disaggregationmanager:edit": "Edit", "amp.disaggregationmanager:delete": "Delete", + "amp.disaggregationmanager:deleting": "Deleting", "amp.disaggregationmanager:no-options": "No options", "amp.disaggregationmanager:add-option": "Add Option", "amp.disaggregationmanager:edit-option-title": "Edit Option", "amp.disaggregationmanager:option-value": "Option Value", "amp.disaggregationmanager:option-value-placeholder": "Enter option value", "amp.disaggregationmanager:option-value-invalid": "Option value must not be empty or contain only spaces/HTML tags.", + "amp.disaggregationmanager:delete-option": "Delete Option", + "amp.disaggregationmanager:delete-option-confirm": "Are you sure you want to delete this disaggregation option", + "amp.disaggregationmanager:delete-option-warning": "This action cannot be undone.", + "amp.disaggregationmanager:delete-failed": "Failed to delete disaggregation option.", + "amp.disaggregationmanager:delete-unexpected-error": "An unexpected error occurred while deleting the disaggregation option.", + "amp.disaggregationmanager:cannot-delete-option": "Cannot delete option", + "amp.disaggregationmanager:delete-linked-option": "This disaggregation option cannot be deleted because it is used by an indicator. Remove the disaggregation from the indicator before deleting this option.", + "amp.disaggregationmanager:ok": "Ok", "amp.disaggregationmanager:cancel": "Cancel", "amp.disaggregationmanager:save": "Save", "amp.dashboard:disaggregation-management": "Disaggregation Management", diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/pages/DisaggregationManagerPage.tsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/pages/DisaggregationManagerPage.tsx index bceb40d681f..e12aedeaa2a 100644 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/pages/DisaggregationManagerPage.tsx +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/pages/DisaggregationManagerPage.tsx @@ -23,6 +23,11 @@ const DisaggregationManagerPage: React.FC = () => { const [editingChild, setEditingChild] = useState(null); const [optionsMap, setOptionsMap] = useState<{ [key: number]: any[] }>({}); const [optionValueError, setOptionValueError] = useState(''); + const [deleteCandidate, setDeleteCandidate] = useState<{ category: CategoryValue; child: CategoryValue } | null>(null); + const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); + const [showDeleteBlockedModal, setShowDeleteBlockedModal] = useState(false); + const [deleteErrorMessage, setDeleteErrorMessage] = useState(''); + const [isDeleting, setIsDeleting] = useState(false); const dispatch = useDispatch(); const categoriesReducer = useSelector((state: any) => state.fetchAmpCategoryReducer); @@ -72,9 +77,70 @@ const DisaggregationManagerPage: React.FC = () => { dispatch(getAmpCategories()); }; - const handleDeleteChild = async (category: CategoryValue, child: CategoryValue) => { - await fetch(`/rest/indicator_disaggregation/options/${child.id}`, { method: 'DELETE' }); - refreshCategories(); + const extractApiErrorMessage = async (response: Response, fallback: string): Promise => { + try { + const error = await response.json(); + if (error?.error) { + const firstKey = Object.keys(error.error)[0]; + if (firstKey && error.error[firstKey] && error.error[firstKey][0]) { + return error.error[firstKey][0]; + } + } + if (error?.message) { + return error.message; + } + } catch { + return fallback; + } + return fallback; + }; + + const handleDeleteChild = (category: CategoryValue, child: CategoryValue) => { + setDeleteCandidate({ category, child }); + setShowDeleteConfirmModal(true); + }; + + const handleCloseDeleteConfirm = () => { + if (!isDeleting) { + setShowDeleteConfirmModal(false); + setDeleteCandidate(null); + } + }; + + const handleConfirmDeleteChild = async () => { + if (!deleteCandidate) return; + + const candidate = deleteCandidate; + setIsDeleting(true); + try { + const response = await fetch(`/rest/indicator_disaggregation/options/${candidate.child.id}`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' } + }); + if (response.ok) { + setOptionsMap(previousOptionsMap => ({ + ...previousOptionsMap, + [candidate.category.id]: (previousOptionsMap[candidate.category.id] || []) + .filter(option => option.id !== candidate.child.id) + })); + setShowDeleteConfirmModal(false); + setDeleteCandidate(null); + refreshCategories(); + } else { + const errorMessage = await extractApiErrorMessage(response, t('amp.disaggregationmanager:delete-failed')); + setShowDeleteConfirmModal(false); + setDeleteCandidate(null); + setDeleteErrorMessage(errorMessage); + setShowDeleteBlockedModal(true); + } + } catch { + setShowDeleteConfirmModal(false); + setDeleteCandidate(null); + setDeleteErrorMessage(t('amp.disaggregationmanager:delete-unexpected-error')); + setShowDeleteBlockedModal(true); + } finally { + setIsDeleting(false); + } }; const handleClose = () => { @@ -217,6 +283,52 @@ const DisaggregationManagerPage: React.FC = () => { + + + {t('amp.disaggregationmanager:delete-option')} + + +

+ {t('amp.disaggregationmanager:delete-option-confirm')} {deleteCandidate?.child.value}? +

+

{t('amp.disaggregationmanager:delete-option-warning')}

+
+ + + + +
+ setShowDeleteBlockedModal(false)} + centered + animation={false} + backdropClassName={styles.modal_backdrop} + > + + {t('amp.disaggregationmanager:cannot-delete-option')} + + +

{deleteErrorMessage || t('amp.disaggregationmanager:delete-linked-option')}

+
+ + + +
); }; diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/service/DisaggregationService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/service/DisaggregationService.java index d844d6bd915..680f9c731d5 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/service/DisaggregationService.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/service/DisaggregationService.java @@ -1,7 +1,10 @@ package org.digijava.kernel.ampapi.endpoints.indicator.manager.service; +import org.digijava.kernel.ampapi.endpoints.errors.ApiError; +import org.digijava.kernel.ampapi.endpoints.errors.ApiRuntimeException; import org.digijava.kernel.ampapi.endpoints.indicator.manager.AmpCategoryValueDTO; import org.digijava.kernel.persistence.PersistenceManager; +import org.digijava.module.aim.dbentity.AmpIndicatorDisaggregationValue; import org.digijava.module.categorymanager.dbentity.AmpCategoryClass; import org.digijava.module.categorymanager.dbentity.AmpCategoryValue; import org.hibernate.Session; @@ -12,6 +15,8 @@ import java.util.List; import java.util.Optional; +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; + public class DisaggregationService { private static String sanitizeOptionValue(String raw) { @@ -90,6 +95,11 @@ public void deleteDisaggregationOption(Long optionId) { Session session = PersistenceManager.getSession(); AmpCategoryValue option = session.get(AmpCategoryValue.class, optionId); if (option != null) { + if (isOptionLinkedToIndicator(session, optionId)) { + throw new ApiRuntimeException(BAD_REQUEST, ApiError.toError("This disaggregation option cannot be " + + "deleted because it is used by an indicator. Remove the disaggregation from the " + + "indicator before deleting this option.")); + } // AmpCategoryClass.possibleValues is cascade="all-delete-orphan" + lazy="false". // The parent class is eagerly loaded and its possibleValues list is already in the // session, so we must remove this option from that list BEFORE deleting it — @@ -102,4 +112,14 @@ public void deleteDisaggregationOption(Long optionId) { session.flush(); } } + + private boolean isOptionLinkedToIndicator(Session session, Long optionId) { + Long linkedCount = session.createQuery("select count(disaggValue.id) from " + + AmpIndicatorDisaggregationValue.class.getName() + " disaggValue " + + "where disaggValue.parentCategory.id = :optionId " + + "or disaggValue.childCategory.id = :optionId", Long.class) + .setParameter("optionId", optionId) + .uniqueResult(); + return linkedCount != null && linkedCount > 0; + } } From 9912f03ce808b875fa9381f47afeaf851bca1b29 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 22 Jun 2026 08:22:25 +0300 Subject: [PATCH 13/14] AMP-31133 : Delete/replace disaggregation option --- .../manager/IndicatorManagerService.java | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java index b0611a29aeb..8b7802e7df4 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java @@ -580,9 +580,7 @@ public MEIndicatorDTO updateMEIndicator(final Long indicatorId, final MEIndicato .filter(existing -> !valuesToKeep.contains(existing)) .collect(Collectors.toSet()); for (AmpIndicatorDisaggregationValue valueToRemove : valuesToRemove) { - indicator.getDisaggregationValues().remove(valueToRemove); - removeDisaggregationGlobalValues(indicator, valueToRemove); - session.delete(valueToRemove); + deleteDisaggregationValue(indicator, valueToRemove, session); } } if (program != null) { @@ -663,10 +661,50 @@ private Long getGlobalValueId(AmpIndicatorGlobalValue value) { return value != null ? value.getId() : null; } - private void removeDisaggregationGlobalValues(AmpIndicator indicator, - AmpIndicatorDisaggregationValue disaggregationValue) { - indicator.getIndicatorValues().remove(disaggregationValue.getBaseValue()); - indicator.getIndicatorValues().remove(disaggregationValue.getTargetValue()); + private void deleteDisaggregationValue(AmpIndicator indicator, + AmpIndicatorDisaggregationValue disaggregationValue, + Session session) { + AmpIndicatorGlobalValue baseValue = disaggregationValue.getBaseValue(); + AmpIndicatorGlobalValue targetValue = disaggregationValue.getTargetValue(); + + indicator.getDisaggregationValues().remove(disaggregationValue); + disaggregationValue.setBaseValue(null); + disaggregationValue.setTargetValue(null); + disaggregationValue.getActualValues().clear(); + + removeDisaggregationGlobalValue(indicator, baseValue, session); + removeDisaggregationGlobalValue(indicator, targetValue, session); + session.delete(disaggregationValue); + } + + private void removeDisaggregationGlobalValue(AmpIndicator indicator, AmpIndicatorGlobalValue value, + Session session) { + if (value == null || isGlobalValueReferencedByDisaggregation(indicator, value)) { + return; + } + indicator.getIndicatorValues().removeIf(indicatorValue -> sameGlobalValue(indicatorValue, value)); + value.setIndicator(null); + if (value.getId() != null) { + AmpIndicatorGlobalValue valueToDelete = session.contains(value) + ? value : session.get(AmpIndicatorGlobalValue.class, value.getId()); + if (valueToDelete != null) { + session.delete(valueToDelete); + } + } + } + + private boolean isGlobalValueReferencedByDisaggregation(AmpIndicator indicator, AmpIndicatorGlobalValue value) { + return indicator.getDisaggregationValues().stream() + .anyMatch(disaggregationValue -> sameGlobalValue(disaggregationValue.getBaseValue(), value) + || sameGlobalValue(disaggregationValue.getTargetValue(), value)); + } + + private boolean sameGlobalValue(AmpIndicatorGlobalValue firstValue, AmpIndicatorGlobalValue secondValue) { + if (firstValue == null || secondValue == null) { + return false; + } + return firstValue == secondValue || firstValue.getId() != null + && Objects.equals(firstValue.getId(), secondValue.getId()); } private void addDisaggregationGlobalValues(AmpIndicator indicator, From c0e4ed83a4dca1b1053669e06859626f12bedd21 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 22 Jun 2026 08:41:39 +0300 Subject: [PATCH 14/14] AMP-31133 : API not returning actual values for single country --- .../activity/ActivityInterchangeUtils.java | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/activity/ActivityInterchangeUtils.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/activity/ActivityInterchangeUtils.java index 7f83e192450..7fe40f11666 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/activity/ActivityInterchangeUtils.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/activity/ActivityInterchangeUtils.java @@ -479,6 +479,7 @@ private static void addActualIndicatorValues(Optional indicatorsObject, List actualValues = new ArrayList<>(); Long activityLocationId = parseLong(indicator.get("activity_location")); AmpActivityLocation activityLocation = getActivityLocation(projectId, activityLocationId); + boolean singleLocationFallback = activityLocationId == null && activityLocation != null; AmpIndicator ind = new AmpIndicator(); ind.setIndicatorId(parseLong(indicator.get("indicator"))); @@ -497,13 +498,15 @@ private static void addActualIndicatorValues(Optional indicatorsObject, if (result == null) { continue; } - if (!matchesActivityLocation(result.getActivityLocation(), activityLocation)) { + if (!matchesIndicatorActivityLocation(result.getActivityLocation(), activityLocation, + singleLocationFallback)) { continue; } if (result.getValues() != null) { for (AmpIndicatorValue indicatorValue : result.getValues()) { if (indicatorValue.getValueType() == AmpIndicatorValue.ACTUAL - && matchesIndicatorValueActivityLocation(indicatorValue, result)) { + && matchesIndicatorValueActivityLocation(indicatorValue, result, activityLocation, + singleLocationFallback)) { actualValues.add(serializeIndicatorActualValue(indicatorValue)); } } @@ -516,10 +519,23 @@ && matchesIndicatorValueActivityLocation(indicatorValue, result)) { }); } + private static boolean matchesIndicatorActivityLocation(AmpActivityLocation indicatorActivityLocation, + AmpActivityLocation expectedActivityLocation, + boolean singleLocationFallback) { + return matchesActivityLocation(indicatorActivityLocation, expectedActivityLocation) + || (singleLocationFallback && indicatorActivityLocation == null); + } + private static boolean matchesIndicatorValueActivityLocation(AmpIndicatorValue indicatorValue, - IndicatorActivity indicatorActivity) { - return indicatorValue.getActivityLocation() == null - || matchesActivityLocation(indicatorValue.getActivityLocation(), indicatorActivity.getActivityLocation()); + IndicatorActivity indicatorActivity, + AmpActivityLocation expectedActivityLocation, + boolean singleLocationFallback) { + if (indicatorValue.getActivityLocation() == null) { + return true; + } + return matchesActivityLocation(indicatorValue.getActivityLocation(), indicatorActivity.getActivityLocation()) + || (singleLocationFallback + && matchesActivityLocation(indicatorValue.getActivityLocation(), expectedActivityLocation)); } private static Map serializeIndicatorActualValue(AmpIndicatorValue indicatorValue) {