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/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/dgfoundation/amp/onepager/components/features/items/AmpMEDisaggregationActualValuesPanel.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/items/AmpMEDisaggregationActualValuesPanel.java index 4314196fa0f..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 @@ -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,39 @@ 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(); + 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() { + 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.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 20d45c50cd3..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; @@ -10,7 +11,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; @@ -95,13 +95,22 @@ public AmpMEIndicatorFeaturePanel(String id, String fmName, final IModel() { @Override protected String load() { return globalBaseVal.getOriginalValue() != null ? String.valueOf(globalBaseVal.getOriginalValue()) : "N/A"; } }); - add(indicatorBaseValueLabel); + baseTargetSummary.add(indicatorBaseValueLabel); final Label indicatorBaseDateLabel = new Label("baseDate", new LoadableDetachableModel() { @Override @@ -114,7 +123,7 @@ protected String load() { } } }); - add(indicatorBaseDateLabel); + baseTargetSummary.add(indicatorBaseDateLabel); final Label indicatorTargetValueLabel = new Label("target", new LoadableDetachableModel() { @Override @@ -122,7 +131,7 @@ protected String load() { return globalTargetVal.getOriginalValue() != null ? String.valueOf(globalTargetVal.getOriginalValue()) : "N/A"; } }); - add(indicatorTargetValueLabel); + baseTargetSummary.add(indicatorTargetValueLabel); final Label indicatorTargetDateLabel = new Label("targetDate", new LoadableDetachableModel() { @Override @@ -135,9 +144,18 @@ protected String load() { } } }); - add(indicatorTargetDateLabel); + baseTargetSummary.add(indicatorTargetDateLabel); + add(baseTargetSummary); - 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); @@ -158,6 +176,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); + } }; @@ -171,13 +196,23 @@ 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); } 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/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/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/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/dgfoundation/amp/onepager/util/ActivityUtil.java b/amp/src/main/java/org/dgfoundation/amp/onepager/util/ActivityUtil.java index dce0a8c1061..9f2860d074b 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; @@ -288,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) { @@ -295,6 +295,8 @@ public static AmpActivityVersion saveActivityNewVersion(AmpActivityVersion a, } } + normalizeIndicatorActivityValues(a); + if (isActivityForm) { saveActivityResources(a, session); saveActivityGPINiResources(a, session); @@ -319,8 +321,11 @@ public static AmpActivityVersion saveActivityNewVersion(AmpActivityVersion a, session.save(a); } else { // session.saveOrUpdate(a); - session.merge(a); + a = (AmpActivityVersion) session.merge(a); } + saveIndicatorDisaggregationValues(indicatorDisaggregationSource, + createNewVersion ? oldA : indicatorDisaggregationSource, a, createNewVersion, session); + session.flush(); updatePerformanceRules(oldA, a); @@ -337,6 +342,252 @@ public static AmpActivityVersion saveActivityNewVersion(AmpActivityVersion a, return a; } + + private static void saveIndicatorDisaggregationValues(AmpActivityVersion indicatorSourceActivity, + AmpActivityVersion sourceLocationActivity, + AmpActivityVersion activity, boolean createNewVersion, + Session session) { + if (indicatorSourceActivity == null || indicatorSourceActivity.getIndicators() == null) { + return; + } + Map disaggregationValues = new LinkedHashMap<>(); + Map indicators = new HashMap<>(); + 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(); + if (submittedIndicator == null || submittedIndicator.getDisaggregationValues() == null) { + continue; + } + AmpIndicator indicator = getManagedIndicator(submittedIndicator, session); + for (AmpIndicatorDisaggregationValue disaggregationValue : submittedIndicator.getDisaggregationValues()) { + Long disaggregationValueId = disaggregationValue.getId(); + if (disaggregationValueId == null) { + continue; + } + AmpIndicatorDisaggregationValue mergedValue = disaggregationValues.computeIfAbsent( + disaggregationValueId, ignored -> disaggregationValue); + if (mergedValue != disaggregationValue) { + mergeActualValues(mergedValue, 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()) { + if (!matchesActivityLocation(actualValue.getActivityLocation(), sourceActivityLocationIds, + 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(activityLocation); + AmpIndicatorGlobalValue savedValue = saveIndicatorGlobalValue(valueToSave, indicator, session); + actualValuesByDisaggregation.computeIfAbsent(entry.getKey(), ignored -> new LinkedHashSet<>()) + .add(savedValue); + } + } + } + + session.flush(); + Set activityLocationIds = getActivityLocationIds(activity); + for (AmpIndicatorDisaggregationValue disaggregationValue : disaggregationValues.values()) { + 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 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()) { + 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 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 Long getLocationId(AmpActivityLocation activityLocation) { + return activityLocation != null && activityLocation.getLocation() != null + ? activityLocation.getLocation().getId() : null; + } + + private static boolean matchesActivityLocation(AmpActivityLocation activityLocation, + Set activityLocationIds, Set locationIds) { + 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, + Collection actualValues, + 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 : actualValues) { + 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, + 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) { + if (first == second) { + return true; + } + if (first == null || second == null) { + return false; + } + if (first.getId() != null && second.getId() != null && Objects.equals(first.getId(), second.getId())) { + return true; + } + return first.getLocation() != null && second.getLocation() != null + && Objects.equals(first.getLocation().getId(), second.getLocation().getId()); + } + + private static void normalizeIndicatorActivityValues(AmpActivityVersion activity) { + if (activity == null || activity.getIndicators() == null) { + return; + } + for (IndicatorActivity indicatorActivity : activity.getIndicators()) { + AmpActivityLocation indicatorLocation = resolveActivityLocation(activity, + indicatorActivity.getActivityLocation()); + indicatorActivity.setActivityLocation(indicatorLocation); + if (indicatorActivity.getValues() == null) { + continue; + } + for (AmpIndicatorValue value : indicatorActivity.getValues()) { + value.setIndicatorConnection(indicatorActivity); + AmpActivityLocation valueLocation = resolveActivityLocation(activity, value.getActivityLocation()); + if (valueLocation == null || sameActivityLocation(valueLocation, indicatorLocation)) { + valueLocation = indicatorLocation; + } + value.setActivityLocation(valueLocation); + } + } + } + + 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 AmpIndicatorGlobalValue saveIndicatorGlobalValue(AmpIndicatorGlobalValue value, AmpIndicator indicator, + Session session) { + if (value == null) { + return null; + } + value.setIndicator(indicator); + 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) { 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..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 @@ -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,159 @@ 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) { + 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, + AmpActivityLocation expectedActivityLocation) { + if (expectedActivityLocation == null) { + return activityLocation == null; + } + 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) { + 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")); + AmpActivityLocation activityLocation = getActivityLocation(projectId, activityLocationId); + boolean singleLocationFallback = activityLocationId == null && activityLocation != null; + + 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 (!matchesIndicatorActivityLocation(result.getActivityLocation(), activityLocation, + singleLocationFallback)) { + 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, activityLocation, + singleLocationFallback)) { + actualValues.add(serializeIndicatorActualValue(indicatorValue)); } } } - - indicator.put("actual", actualValues); } + + indicator.put("actual", actualValues); } } }); } + 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, + 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) { + 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/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java index 5eae3b3dd57..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 @@ -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; @@ -516,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)) @@ -564,44 +560,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); + addDisaggregationGlobalValues(indicator, 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) { + deleteDisaggregationValue(indicator, valueToRemove, session); } - indicator.getDisaggregationValues().clear(); } if (program != null) { try { @@ -612,12 +592,182 @@ public MEIndicatorDTO updateMEIndicator(final Long indicatorId, final MEIndicato } } + session.flush(); return new MEIndicatorDTO(indicator); } throw new ApiRuntimeException(BAD_REQUEST, 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 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 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, + 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, + 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(updateIndicatorGlobalValue(disaggValue.getBaseValue(), dto.getBaseValue(), indicator, + AmpIndicatorGlobalValue.BASE, session)); + disaggValue.setTargetValue(updateIndicatorGlobalValue(disaggValue.getTargetValue(), dto.getTargetValue(), indicator, + AmpIndicatorGlobalValue.TARGET, session)); + } + + private AmpIndicatorGlobalValue updateIndicatorGlobalValue(AmpIndicatorGlobalValue currentValue, + AmpIndicatorGlobalValue submittedValue, + AmpIndicator indicator, int type, Session session) { + if (submittedValue == null) { + return null; + } + AmpIndicatorGlobalValue value = currentValue != null ? currentValue : submittedValue; + if (currentValue != null && currentValue != submittedValue) { + copyIndicatorGlobalValue(submittedValue, value); + } + 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); + } + + private void copyIndicatorGlobalValue(AmpIndicatorGlobalValue source, AmpIndicatorGlobalValue target) { + target.setOriginalValue(source.getOriginalValue()); + target.setOriginalValueDate(source.getOriginalValueDate()); + target.setRevisedValue(source.getRevisedValue()); + target.setRevisedValueDate(source.getRevisedValueDate()); + target.setActivityLocation(source.getActivityLocation()); + } + public void validateProgramSettingsAndGlobalValues(final MEIndicatorDTO indicatorRequest, final AmpIndicator indicator) { if (indicatorRequest.getProgramId() != null) { 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; + } } 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 @@ + +