Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions amp/TEMPLATE/reamp/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion amp/TEMPLATE/reamp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ const DisaggregationManagerPage: React.FC = () => {
const [editingChild, setEditingChild] = useState<CategoryValue | null>(null);
const [optionsMap, setOptionsMap] = useState<{ [key: number]: any[] }>({});
const [optionValueError, setOptionValueError] = useState<string>('');
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);
Expand Down Expand Up @@ -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<string> => {
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 = () => {
Expand Down Expand Up @@ -217,6 +283,52 @@ const DisaggregationManagerPage: React.FC = () => {
</Modal.Footer>
</Form>
</Modal>
<Modal
show={showDeleteConfirmModal}
onHide={handleCloseDeleteConfirm}
centered
animation={false}
backdropClassName={styles.modal_backdrop}
backdrop="static"
keyboard={!isDeleting}
>
<Modal.Header closeButton={!isDeleting}>
<Modal.Title>{t('amp.disaggregationmanager:delete-option')}</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>
{t('amp.disaggregationmanager:delete-option-confirm')} <strong>{deleteCandidate?.child.value}</strong>?
</p>
<p>{t('amp.disaggregationmanager:delete-option-warning')}</p>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={handleCloseDeleteConfirm} disabled={isDeleting}>
{t('amp.disaggregationmanager:cancel')}
</Button>
<Button variant="danger" onClick={handleConfirmDeleteChild} disabled={isDeleting}>
{isDeleting ? t('amp.disaggregationmanager:deleting') : t('amp.disaggregationmanager:delete')}
</Button>
</Modal.Footer>
</Modal>
<Modal
show={showDeleteBlockedModal}
onHide={() => setShowDeleteBlockedModal(false)}
centered
animation={false}
backdropClassName={styles.modal_backdrop}
>
<Modal.Header closeButton>
<Modal.Title>{t('amp.disaggregationmanager:cannot-delete-option')}</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>{deleteErrorMessage || t('amp.disaggregationmanager:delete-linked-option')}</p>
</Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={() => setShowDeleteBlockedModal(false)}>
{t('amp.disaggregationmanager:ok')}
</Button>
</Modal.Footer>
</Modal>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -25,39 +26,42 @@
*/
public class AmpMEDisaggregationActualValuesPanel extends AmpFeaturePanel<AmpIndicatorDisaggregationValue> {

private final ListView<AmpIndicatorGlobalValue> listView;
private final ListEditor<AmpIndicatorGlobalValue> listView;
private final IModel<AmpActivityLocation> activityLocationModel;

public AmpMEDisaggregationActualValuesPanel(String id, IModel<AmpIndicatorDisaggregationValue> model) {
this(id, model, null);
}

public AmpMEDisaggregationActualValuesPanel(String id, IModel<AmpIndicatorDisaggregationValue> model,
IModel<AmpActivityLocation> 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<List<AmpIndicatorGlobalValue>> listModel = new LoadableDetachableModel<List<AmpIndicatorGlobalValue>>() {
IModel<Set<AmpIndicatorGlobalValue>> actualValuesModel = new PropertyModel<>(model, "actualValues");
IModel<Set<AmpIndicatorGlobalValue>> locationActualValuesModel = new AbstractMixedSetModel<AmpIndicatorGlobalValue>(
actualValuesModel) {
@Override
protected List<AmpIndicatorGlobalValue> 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<AmpIndicatorGlobalValue>("rows", listModel) {
listView = new ListEditor<AmpIndicatorGlobalValue>("rows", locationActualValuesModel) {
@Override
protected void populateItem(ListItem<AmpIndicatorGlobalValue> item) {
AmpIndicatorGlobalValue val = item.getModelObject();
protected void onPopulateItem(ListItem<AmpIndicatorGlobalValue> item) {
item.setOutputMarkupId(true);
item.add(new AmpTextFieldPanel<Double>("actualValue", new PropertyModel<>(item.getModel(), "originalValue"), "Actual Value") {
public org.apache.wicket.util.convert.IConverter getInternalConverter(java.lang.Class<?> type) {
public IConverter<Double> 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);
}
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -41,6 +41,11 @@ protected void onConfigure() {
}

public AmpMEDisaggregationValuesFeaturePanel(String id, String fmName, IModel<AmpIndicator> indicatorModel) {
this(id, fmName, indicatorModel, null);
}

public AmpMEDisaggregationValuesFeaturePanel(String id, String fmName, IModel<AmpIndicator> indicatorModel,
IModel<AmpActivityLocation> activityLocationModel) {
super(id, fmName, true);
this.indicatorModel = indicatorModel;
logger.info("Initializing AmpMEDisaggregationValuesFeaturePanel for indicator: " + (indicatorModel.getObject() != null ? indicatorModel.getObject().getName() : "null"));
Expand Down Expand Up @@ -150,7 +155,8 @@ protected void populateItem(ListItem<AmpIndicatorDisaggregationValue> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<tr>
<td colspan="2">
<div>
<div>
<div wicket:id="baseTargetSummary">
<b><span>Base Values: </span></b><span wicket:id = "base"></span> <b><span>Base Date: </span></b> <span wicket:id = "baseDate"></span>
<b><span>Target Values: </span></b><span wicket:id = "target"></span> <b><span>Target Date: </span></b> <span wicket:id = "targetDate"></span>
</div>
Expand Down
Loading
Loading