diff --git a/amp/src/main/java/org/dgfoundation/amp/newreports/ReportsDashboard.java b/amp/src/main/java/org/dgfoundation/amp/newreports/ReportsDashboard.java index 4ea27503294..50024c1688e 100644 --- a/amp/src/main/java/org/dgfoundation/amp/newreports/ReportsDashboard.java +++ b/amp/src/main/java/org/dgfoundation/amp/newreports/ReportsDashboard.java @@ -10,11 +10,12 @@ public class ReportsDashboard { private String implementationLevel; private String procurementSystem; private String location; + private String country; private String status; private String reportingSystem; private String typeOfAssistance; private String currency; - //private String year; + private String year; private BigDecimal actualCommitment; private BigDecimal actualDisbursement; private Long activityCount; @@ -64,15 +65,14 @@ public void setActivityIds(String activityIds) { this.activityIds = activityIds; } - /* - public String getYear() { - return year; - } + public String getYear() { + return year; + } + + public void setYear(String year) { + this.year = year; + } - public void setYear(String year) { - this.year = year; - } - */ public BigDecimal getActualCommitment() { return actualCommitment; } @@ -82,11 +82,21 @@ public void setActualCommitment(BigDecimal actualCommitment) { } public String getLocation() { - return location; + return location != null ? location : country; } public void setLocation(String location) { this.location = location; + this.country = location; + } + + public String getCountry() { + return country != null ? country : location; + } + + public void setCountry(String country) { + this.country = country; + this.location = country; } public String getImplementationLevel() { diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/FiltersEndpoint.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/FiltersEndpoint.java index 15c5d623c94..c314f364578 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/FiltersEndpoint.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/FiltersEndpoint.java @@ -385,10 +385,23 @@ public Response getProcurementSystem() { } /** - * List the possible values of 'Funding Status' filter. + * List the possible values of 'Reporting System' filter. * - * @return filter definition and values of 'funding-status' filter. + * @return filter definition and values of 'reporting-system' filter. */ + @GET + @Path("/reportingSystem/") + @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") + @ApiMethod(id = FiltersConstants.REPORTING_SYSTEM, name = ColumnConstants.REPORTING_SYSTEM) + @ApiOperation(value = "Retrieve the data needed for building the 'Reporting System' filter.", + notes = "The response contains 2 objects - the filter definition and the values. \n" + + "The filter widget should create a tree for 'Reporting System' values.") + @FilterDefinition(tab = EPConstants.TAB_OTHER, columns = ColumnConstants.REPORTING_SYSTEM) + public Response getReportingSystem() { + return PublicServices.buildOkResponseWithOriginHeaders( + FiltersManager.getInstance().getCategoryValueFilter(FiltersConstants.REPORTING_SYSTEM)); + } + @OPTIONS @Path("/procurementSystem") @ApiOperation( @@ -397,6 +410,22 @@ public Response getProcurementSystem() { public Response describeProcurementSystem() { return PublicServices.buildOkResponseWithOriginHeaders(""); } + + @OPTIONS + @Path("/reportingSystem") + @ApiOperation( + value = "Describe options for endpoint", + notes = "Enables Cross-Origin Resource Sharing for endpoint") + public Response describeReportingSystem() { + return PublicServices.buildOkResponseWithOriginHeaders(""); + } + + /** + * List the possible values of 'Funding Status' filter. + * + * @return filter definition and values of 'funding-status' filter. + */ + @GET @Path("/fundingStatus/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/filters/CategoryValueFilterListManager.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/filters/CategoryValueFilterListManager.java index 3d76fc018f8..5522fb2d0a0 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/filters/CategoryValueFilterListManager.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/filters/CategoryValueFilterListManager.java @@ -63,6 +63,9 @@ public final class CategoryValueFilterListManager implements FilterListManager { .put(FiltersConstants.PROCUREMENT_SYSTEM, new CategoryBean(ColumnConstants.PROCUREMENT_SYSTEM, CategoryConstants.PROCUREMENT_SYSTEM_KEY)) + .put(FiltersConstants.REPORTING_SYSTEM, + new CategoryBean(ColumnConstants.REPORTING_SYSTEM, + CategoryConstants.REPORTING_SYSTEM_KEY)) .build(); public static CategoryValueFilterListManager getInstance() { diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/publicportal/PublicEndpoint.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/publicportal/PublicEndpoint.java index 84a1250c578..9bbc65dfff3 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/publicportal/PublicEndpoint.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/publicportal/PublicEndpoint.java @@ -11,6 +11,7 @@ import org.digijava.kernel.ampapi.endpoints.dashboards.services.PublicServices; import org.digijava.kernel.ampapi.endpoints.errors.ApiErrorResponse; import org.digijava.kernel.ampapi.endpoints.gis.SettingsAndFiltersParameters; +import org.digijava.kernel.ampapi.endpoints.publicportal.dto.PublicDonorCommitmentsByYear; import org.digijava.kernel.ampapi.endpoints.publicportal.dto.PublicTotalsByMeasure; import org.digijava.kernel.ampapi.endpoints.reports.ReportFormParameters; import org.digijava.kernel.ampapi.endpoints.reports.ReportsUtil; @@ -126,6 +127,31 @@ public Response describeTotalByMeasure() { } + @POST + @Path("/donorCommitmentsByYear") + @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") + @ApiMethod(ui = false, id = "donorCommitmentsByYear") + @ApiOperation("Commitments by donor up to selected year") + @ApiResponses(@ApiResponse(code = HttpServletResponse.SC_OK, message = "Donor commitments by year", + response = PublicDonorCommitmentsByYear.class)) + public Response getDonorCommitmentsByYear(SettingsAndFiltersParameters config, + @ApiParam(value = "Include commitments up to this year", required = false) + @QueryParam("year") Integer year) { + return PublicServices.buildOkResponseWithOriginHeaders( + PublicPortalService.getDonorCommitmentsByYear(config, year)); + } + + + @OPTIONS + @Path("/donorCommitmentsByYear") + @ApiOperation( + value = "Describe options for endpoint", + notes = "Enables Cross-Origin Resource Sharing for endpoint") + public Response describeDonorCommitmentsByYear() { + return PublicServices.buildOkResponseWithOriginHeaders(""); + } + + @POST @Path("/projectCount") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/publicportal/PublicPortalService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/publicportal/PublicPortalService.java index a7f663bda5d..528de9f188e 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/publicportal/PublicPortalService.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/publicportal/PublicPortalService.java @@ -20,6 +20,8 @@ import org.digijava.kernel.ampapi.endpoints.errors.ApiErrorResponse; import org.digijava.kernel.ampapi.endpoints.errors.ApiRuntimeException; import org.digijava.kernel.ampapi.endpoints.gis.SettingsAndFiltersParameters; +import org.digijava.kernel.ampapi.endpoints.publicportal.dto.PublicDonorCommitment; +import org.digijava.kernel.ampapi.endpoints.publicportal.dto.PublicDonorCommitmentsByYear; import org.digijava.kernel.ampapi.endpoints.publicportal.dto.PublicTotalsByMeasure; import org.digijava.kernel.ampapi.endpoints.reports.ReportFormParameters; import org.digijava.kernel.ampapi.endpoints.reports.ReportsUtil; @@ -32,9 +34,12 @@ import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -49,6 +54,14 @@ */ public class PublicPortalService { protected static final Logger logger = Logger.getLogger(PublicPortalService.class); + private static final String DONOR_AGENCY_FILTER = "donor-agency"; + private static final String DONOR_AGENCY_CAMEL_FILTER = "donorAgency"; + private static final String DONOR_FILTER = "donor"; + private static final String REPORTING_SYSTEM_FILTER = "reporting-system"; + private static final String REPORTING_SYSTEM_CAMEL_FILTER = "reportingSystem"; + private static final String DATE_FILTER = "date"; + private static final String DATE_START = "start"; + private static final String DATE_END = "end"; /** * Retrieves top 'count' projects based on fixed requirements.
@@ -376,6 +389,133 @@ private static void applyFilterRules(Map filters, ReportSpecific } } + public static PublicDonorCommitmentsByYear getDonorCommitmentsByYear(SettingsAndFiltersParameters config, + Integer year) { + PublicDonorCommitmentsByYear result = new PublicDonorCommitmentsByYear(); + int targetYear = year == null ? Calendar.getInstance().get(Calendar.YEAR) : year; + result.setYear(targetYear); + + Map settings = copyMap(config != null ? config.getSettings() : null); + if (!settings.containsKey(SettingsConstants.FUNDING_TYPE_ID)) { + settings.put(SettingsConstants.FUNDING_TYPE_ID, SettingsConstants.DEFAULT_FUNDING_TYPE_ID); + } + if (!settings.containsKey(SettingsConstants.CURRENCY_ID)) { + settings.put(SettingsConstants.CURRENCY_ID, "USD"); + } + result.setCurrency((String) settings.get(SettingsConstants.CURRENCY_ID)); + + Map filters = copyMap(config != null ? config.getFilters() : null); + normalizeKnownFilterAliases(filters); + + List donorIds = extractDonorIds(filters); + if (donorIds.isEmpty()) { + return result; + } + + Set requestedDonorIds = new LinkedHashSet<>(); + for (Object donorId : donorIds) { + Long parsedDonorId = parseLong(donorId); + if (parsedDonorId != null) { + requestedDonorIds.add(parsedDonorId); + } + } + + Map baseFilters = copyMap(filters); + baseFilters.put(DATE_FILTER, buildDateFilter(baseFilters.get(DATE_FILTER), targetYear)); + + for (Long donorId : requestedDonorIds) { + Map donorFilters = copyMap(baseFilters); + donorFilters.put(DONOR_AGENCY_FILTER, Collections.singletonList(donorId)); + BigDecimal donorTotal = getTotalForFilters(settings, donorFilters); + + PublicDonorCommitment donorCommitment = new PublicDonorCommitment(); + donorCommitment.setDonorId(donorId); + donorCommitment.setTotal(donorTotal); + + result.getDonorTotals().add(donorCommitment); + result.setTotal(result.getTotal().add(donorTotal)); + } + + return result; + } + + private static BigDecimal getTotalForFilters(Map settings, Map filters) { + ReportSpecificationImpl spec = new ReportSpecificationImpl("PublicPortal_getDonorCommitmentsByYearSingle", + ArConstants.DONOR_TYPE); + + SettingsUtils.applyExtendedSettings(spec, settings); + ReportsUtil.configureFilters(spec, filters); + spec.setSummaryReport(true); + + GeneratedReport report = EndpointUtils.runReport(spec); + if (report == null || report.reportContents == null || report.reportContents.getContents() == null) { + return BigDecimal.ZERO; + } + + BigDecimal total = BigDecimal.ZERO; + for (ReportCell cell : report.reportContents.getContents().values()) { + if (cell != null && cell.value instanceof BigDecimal) { + total = total.add((BigDecimal) cell.value); + } + } + + return total; + } + + private static Map copyMap(Map source) { + return source == null ? new HashMap<>() : new HashMap<>(source); + } + + private static void normalizeKnownFilterAliases(Map filters) { + if (filters.containsKey(DONOR_AGENCY_CAMEL_FILTER) && !filters.containsKey(DONOR_AGENCY_FILTER)) { + filters.put(DONOR_AGENCY_FILTER, filters.get(DONOR_AGENCY_CAMEL_FILTER)); + } + if (filters.containsKey(DONOR_FILTER) && !filters.containsKey(DONOR_AGENCY_FILTER)) { + filters.put(DONOR_AGENCY_FILTER, filters.get(DONOR_FILTER)); + } + if (filters.containsKey(REPORTING_SYSTEM_CAMEL_FILTER) && !filters.containsKey(REPORTING_SYSTEM_FILTER)) { + filters.put(REPORTING_SYSTEM_FILTER, filters.get(REPORTING_SYSTEM_CAMEL_FILTER)); + } + } + + private static List extractDonorIds(Map filters) { + Object rawDonors = filters.get(DONOR_AGENCY_FILTER); + if (rawDonors instanceof List) { + return new ArrayList<>((List) rawDonors); + } + if (rawDonors != null) { + List singleValue = new ArrayList<>(); + singleValue.add(rawDonors); + return singleValue; + } + return new ArrayList<>(); + } + + private static Map buildDateFilter(Object existingDateFilter, Integer year) { + Map dateFilter = new HashMap<>(); + if (existingDateFilter instanceof Map) { + ((Map) existingDateFilter).forEach((key, value) -> { + if (key != null && !DATE_START.equals(String.valueOf(key))) { + dateFilter.put(String.valueOf(key), value); + } + }); + } + dateFilter.put(DATE_END, year + "-12-31"); + return dateFilter; + } + + private static Long parseLong(Object donorId) { + if (donorId == null) { + return null; + } + + try { + return Long.parseLong(String.valueOf(donorId)); + } catch (NumberFormatException ex) { + return null; + } + } + public static PublicTotalsByMeasure getCountByMeasure(SettingsAndFiltersParameters config) { PublicTotalsByMeasure result = new PublicTotalsByMeasure(); result.getMeasure().put("original", "Total Activities"); diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/publicportal/dto/PublicDonorCommitment.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/publicportal/dto/PublicDonorCommitment.java new file mode 100644 index 00000000000..29be144db67 --- /dev/null +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/publicportal/dto/PublicDonorCommitment.java @@ -0,0 +1,33 @@ +package org.digijava.kernel.ampapi.endpoints.publicportal.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; + +import java.math.BigDecimal; + +public class PublicDonorCommitment { + + @JsonProperty("donorId") + @ApiModelProperty(example = "43") + private Long donorId; + + @JsonProperty("total") + @ApiModelProperty(example = "1085.4") + private BigDecimal total; + + public Long getDonorId() { + return donorId; + } + + public void setDonorId(Long donorId) { + this.donorId = donorId; + } + + public BigDecimal getTotal() { + return total; + } + + public void setTotal(BigDecimal total) { + this.total = total; + } +} diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/publicportal/dto/PublicDonorCommitmentsByYear.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/publicportal/dto/PublicDonorCommitmentsByYear.java new file mode 100644 index 00000000000..6050c0b576f --- /dev/null +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/publicportal/dto/PublicDonorCommitmentsByYear.java @@ -0,0 +1,58 @@ +package org.digijava.kernel.ampapi.endpoints.publicportal.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +public class PublicDonorCommitmentsByYear { + + @JsonProperty("year") + @ApiModelProperty(example = "2025") + private Integer year; + + @JsonProperty("currency") + @ApiModelProperty(example = "USD") + private String currency; + + @JsonProperty("total") + @ApiModelProperty(example = "30480.2") + private BigDecimal total = BigDecimal.ZERO; + + @JsonProperty("donorTotals") + private List donorTotals = new ArrayList<>(); + + public Integer getYear() { + return year; + } + + public void setYear(Integer year) { + this.year = year; + } + + public String getCurrency() { + return currency; + } + + public void setCurrency(String currency) { + this.currency = currency; + } + + public BigDecimal getTotal() { + return total; + } + + public void setTotal(BigDecimal total) { + this.total = total; + } + + public List getDonorTotals() { + return donorTotals; + } + + public void setDonorTotals(List donorTotals) { + this.donorTotals = donorTotals; + } +} diff --git a/amp/src/main/java/org/digijava/module/message/jobs/AmpDonorFundingJob.java b/amp/src/main/java/org/digijava/module/message/jobs/AmpDonorFundingJob.java index 84c625badb7..98ed8d816ba 100644 --- a/amp/src/main/java/org/digijava/module/message/jobs/AmpDonorFundingJob.java +++ b/amp/src/main/java/org/digijava/module/message/jobs/AmpDonorFundingJob.java @@ -38,7 +38,6 @@ import java.io.IOException; import java.io.OutputStream; -import java.math.BigDecimal; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; @@ -81,8 +80,6 @@ private List getFundingByCurrency(String currencyCode) { List ampDashboardFunding = processReportData(fundingReport, currencyCode); // The ampDashboardFunding data contains objects for commitments and disbursment differently in // separate objects. We need to combine them in same object combining commitment and disbursment values. - //Make year configurable - //+ "|" + report.getYear() return new ArrayList<>(ampDashboardFunding.stream() .collect(Collectors.toMap( report -> report.getDonorAgency() @@ -91,7 +88,7 @@ private List getFundingByCurrency(String currencyCode) { + "|" + report.getLocation() + "|" + report.getImplementationLevel() + "|" + report.getStatus() - //+ "|" + report.getYear() + + "|" + extractYear(report) + "|" + report.getReportingSystem() + "|" + report.getTypeOfAssistance() + "|" + report.getProcurementSystem() @@ -110,8 +107,7 @@ private List processReportData(GeneratedReport report, String logger.info("Number of leaf headers: " + report.leafHeaders.size()); // Find columns by name for robustness (works regardless of order) - // Note: Indices are based on the order in addColumnsToSpecification: - // Hierarchies: 0-10, then AMP_ID (11), ACTIVITY_COUNT (12), measures (13-14) + // Note: indices align with hierarchy columns first; non-hierarchy columns may be omitted from leaf headers ReportOutputColumn donorAgency = findColumnByName(report.leafHeaders, ColumnConstants.DONOR_AGENCY, 0); ReportOutputColumn implementingAgency = findColumnByName(report.leafHeaders, ColumnConstants.IMPLEMENTING_AGENCY, 1); ReportOutputColumn procurementSystemAgency = findColumnByName(report.leafHeaders, ColumnConstants.PROCUREMENT_SYSTEM, 2); @@ -121,10 +117,10 @@ private List processReportData(GeneratedReport report, String ReportOutputColumn impLocation = findColumnByName(report.leafHeaders, location_adm_level, 5); ReportOutputColumn status = findColumnByName(report.leafHeaders, ColumnConstants.STATUS, 6); ReportOutputColumn typeOfAssistance = findColumnByName(report.leafHeaders, ColumnConstants.TYPE_OF_ASSISTANCE, 7); - ReportOutputColumn reportingSystem = findColumnByName(report.leafHeaders, ColumnConstants.PRIMARY_SECTOR, 8); // Also called Forum + ReportOutputColumn reportingSystem = findColumnByName(report.leafHeaders, ColumnConstants.REPORTING_SYSTEM, 8); // Also called Forum ReportOutputColumn responsibleOrg = findColumnByName(report.leafHeaders, ColumnConstants.RESPONSIBLE_ORGANIZATION, 9); ReportOutputColumn secondarySector = findColumnByName(report.leafHeaders, ColumnConstants.SECONDARY_SECTOR, 10); - ReportOutputColumn ampId = findColumnByName(report.leafHeaders, ColumnConstants.AMP_ID, 11); // AMP_ID is now after hierarchies + ReportOutputColumn ampId = findOptionalColumnByName(report.leafHeaders, ColumnConstants.AMP_ID, 11); List ampDashboardFunding = new ArrayList<>(); @@ -171,41 +167,23 @@ private List processReportData(GeneratedReport report, String for (ReportArea secondarySectorData : responsibleOrgData.getChildren()) { TextCell secondarySectorCell = (TextCell) secondarySectorData.getContents().get(secondarySector); - Long activityCount = 0L; - // collect activity count - for (Map.Entry content : responsibleOrgData.getContents().entrySet()) { - ReportOutputColumn col = content.getKey(); - if (col.originalColumnName.equals(ColumnConstants.ACTIVITY_COUNT)) { - IntCell amount = (IntCell) content.getValue(); - if (amount != null && amount.value != null) { - activityCount = (Long) amount.value; - } - } + Long activityCount = extractActivityCount(secondarySectorData); + // In some outputs, Activity Count sits on the parent node. + if (activityCount == 0L) { + activityCount = extractActivityCount(responsibleOrgData); } - // gather AMP IDs from children (leaf nodes) - List ampIdsList = new ArrayList<>(); - if (responsibleOrgData.getChildren() != null) { - for (ReportArea ampIdData : responsibleOrgData.getChildren()) { - TextCell ampIdCell = (TextCell) ampIdData.getContents().get(ampId); - if (ampIdCell != null && ampIdCell.value != null) { - ampIdsList.add(ampIdCell.value.toString()); - } - } - } - // fallback if AMP ID directly on this node and no children + // Gather AMP IDs from the same area where measures are read. + List ampIdsList = extractAmpIds(secondarySectorData, ampId); if (ampIdsList.isEmpty()) { - TextCell directAmpIdCell = (TextCell) responsibleOrgData.getContents().get(ampId); - if (directAmpIdCell != null && directAmpIdCell.value != null) { - ampIdsList.add(directAmpIdCell.value.toString()); - } + ampIdsList = extractAmpIds(responsibleOrgData, ampId); } - String ampIdsJoined = String.join(",", ampIdsList); + String ampIdsJoined = ampIdsList.isEmpty() ? null : String.join(",", ampIdsList); // now process measures - for (Map.Entry content : responsibleOrgData.getContents().entrySet()) { + for (Map.Entry content : secondarySectorData.getContents().entrySet()) { ReportOutputColumn col = content.getKey(); if (col.originalColumnName.equals(MeasureConstants.ACTUAL_COMMITMENTS) || col.originalColumnName.equals(MeasureConstants.ACTUAL_DISBURSEMENTS)) { - if (col.parentColumn != null && col.parentColumn.originalColumnName.equals("Totals")) { + if (col.parentColumn != null && !col.parentColumn.originalColumnName.equals("Totals")) { ReportsDashboard fundingReport = new ReportsDashboard(); fundingReport.setDonorAgency(donorAgencyCell != null ? donorAgencyCell.value.toString() : null); fundingReport.setImplementingAgency(implementingAgencyCell != null ? implementingAgencyCell.value.toString() : null); @@ -218,6 +196,7 @@ private List processReportData(GeneratedReport report, String fundingReport.setProcurementSystem(procurementSystemAgencyCell != null ? procurementSystemAgencyCell.value.toString() : null); fundingReport.setResponsibleOrganization(responsibleOrgCell != null ? responsibleOrgCell.value.toString() : null); fundingReport.setSecondarySector(secondarySectorCell != null ? secondarySectorCell.value.toString() : null); + assignYear(fundingReport, col.parentColumn.originalColumnName); fundingReport.setActivityCount(activityCount); fundingReport.setCurrency(currencyCode); fundingReport.setActivityIds(ampIdsJoined); @@ -313,6 +292,54 @@ private ReportOutputColumn findColumnByName(List leafHeaders leafHeaders.stream().map(c -> c.originalColumnName != null ? c.originalColumnName : "null").collect(Collectors.joining(", "))); } + private Long extractActivityCount(ReportArea reportArea) { + Long activityCount = 0L; + for (Map.Entry content : reportArea.getContents().entrySet()) { + ReportOutputColumn col = content.getKey(); + if (col.originalColumnName.equals(ColumnConstants.ACTIVITY_COUNT)) { + IntCell amount = (IntCell) content.getValue(); + if (amount != null && amount.value != null) { + activityCount = (Long) amount.value; + } + break; + } + } + return activityCount; + } + + private List extractAmpIds(ReportArea reportArea, ReportOutputColumn ampIdColumn) { + List ampIdsList = new ArrayList<>(); + if (ampIdColumn == null) { + return ampIdsList; + } + + if (reportArea.getChildren() != null) { + for (ReportArea ampIdData : reportArea.getChildren()) { + TextCell ampIdCell = (TextCell) ampIdData.getContents().get(ampIdColumn); + if (ampIdCell != null && ampIdCell.value != null) { + ampIdsList.add(ampIdCell.value.toString()); + } + } + } + + if (ampIdsList.isEmpty()) { + TextCell directAmpIdCell = (TextCell) reportArea.getContents().get(ampIdColumn); + if (directAmpIdCell != null && directAmpIdCell.value != null) { + ampIdsList.add(directAmpIdCell.value.toString()); + } + } + return ampIdsList; + } + + private ReportOutputColumn findOptionalColumnByName(List leafHeaders, String columnName, int expectedIndex) { + try { + return findColumnByName(leafHeaders, columnName, expectedIndex); + } catch (RuntimeException ex) { + logger.warn("Optional column missing from report output: " + columnName + ". Continuing without it."); + return null; + } + } + private void addFilters(ReportSpecificationImpl spec) { if (spec.getFilters() == null) { spec.setFilters(new ReportFiltersImpl()); @@ -333,9 +360,8 @@ private GeneratedReport generateReport(String currencyCode) { ReportSpecificationImpl spec = new ReportSpecificationImpl("preview report", ArConstants.DONOR_TYPE); addColumnsToSpecification(spec); - spec.setSummaryReport(false); - //TODO broken by year configurable - spec.setGroupingCriteria(GroupingCriteria.GROUPING_TOTALS_ONLY); + spec.setSummaryReport(true); + spec.setGroupingCriteria(GroupingCriteria.GROUPING_YEARLY); spec.setShowOriginalCurrency(false); spec.setDisplayEmptyFundingRows(true); ReportSettingsImpl reportSettings = new ReportSettingsImpl(); @@ -357,17 +383,15 @@ private void addColumnsToSpecification(ReportSpecificationImpl spec) { spec.addColumn(new ReportColumn(ColumnConstants.STATUS)); spec.addColumn(new ReportColumn(ColumnConstants.TYPE_OF_ASSISTANCE)); //TODO for GGW this is reporting system, for others it is Sectors - spec.addColumn(new ReportColumn(ColumnConstants.PRIMARY_SECTOR)); + spec.addColumn(new ReportColumn(ColumnConstants.REPORTING_SYSTEM)); spec.addColumn(new ReportColumn(ColumnConstants.RESPONSIBLE_ORGANIZATION)); spec.addColumn(new ReportColumn(ColumnConstants.SECONDARY_SECTOR)); - // Ensure AMP ID is part of the hierarchy so commitments roll up under each activity - - // Set hierarchies - includes AMP_ID so commitments/disbursements roll up under each activity - // This is required for summary reports to include all columns in leaf headers + // Set hierarchy columns used to navigate the report output tree. Set hierarchyColumns = new LinkedHashSet<>(spec.getColumns()); spec.setHierarchies(hierarchyColumns); - // Add non-hierarchy columns after setHierarchies + // Add non-hierarchy columns after setHierarchies. + // AMP ID may be absent in some grouped outputs; parser handles it as optional. spec.addColumn(new ReportColumn(ColumnConstants.AMP_ID)); spec.addColumn(new ReportColumn(ColumnConstants.ACTIVITY_COUNT)); spec.addMeasure(new ReportMeasure(MeasureConstants.ACTUAL_COMMITMENTS)); @@ -376,6 +400,23 @@ private void addColumnsToSpecification(ReportSpecificationImpl spec) { logger.info("Report columns set for Donor Funding Report" + spec.getColumns().size()); } + private String extractYear(ReportsDashboard report) { + try { + Object year = ReportsDashboard.class.getMethod("getYear").invoke(report); + return year != null ? year.toString() : null; + } catch (ReflectiveOperationException ignored) { + return null; + } + } + + private void assignYear(ReportsDashboard report, String year) { + try { + ReportsDashboard.class.getMethod("setYear", String.class).invoke(report, year); + } catch (ReflectiveOperationException ignored) { + // Year accessor is optional in some builds. + } + } + /** * Gets translations for all field labels (returns once, not per record) */ @@ -406,9 +447,10 @@ private List> getTranslations() { fieldMappings.put("implementingAgency", ColumnConstants.IMPLEMENTING_AGENCY); fieldMappings.put("pillar", ColumnConstants.NATIONAL_PLANNING_OBJECTIVES_LEVEL_1); fieldMappings.put("location", locationAdmLevel != null ? locationAdmLevel : ColumnConstants.LOCATION); + fieldMappings.put("country", locationAdmLevel != null ? locationAdmLevel : ColumnConstants.LOCATION); fieldMappings.put("implementationLevel", ColumnConstants.IMPLEMENTATION_LEVEL); fieldMappings.put("status", ColumnConstants.STATUS); - fieldMappings.put("reportingSystem", ColumnConstants.PRIMARY_SECTOR); + fieldMappings.put("reportingSystem", ColumnConstants.REPORTING_SYSTEM); fieldMappings.put("typeOfAssistance", ColumnConstants.TYPE_OF_ASSISTANCE); fieldMappings.put("procurementSystem", ColumnConstants.PROCUREMENT_SYSTEM); fieldMappings.put("responsibleOrganization", ColumnConstants.RESPONSIBLE_ORGANIZATION);