Skip to content

Commit a271897

Browse files
Merge pull request #1638 from bcgov/feature/EDX-3172
youthPRPHeadcountPerSchool report/headcount
2 parents eb7f259 + 159c600 commit a271897

File tree

13 files changed

+1688
-3
lines changed

13 files changed

+1688
-3
lines changed

api/src/main/java/ca/bc/gov/educ/studentdatacollection/api/constants/v1/DistrictReportTypeCode.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public enum DistrictReportTypeCode {
3333
ALL_STUDENT_ELL_DIS_CSV("ALL_STUDENT_ELL_DIS_CSV"),
3434
ALL_STUDENT_REFUGEE_DIS_CSV("ALL_STUDENT_REFUGEE_DIS_CSV"),
3535
DIS_ZERO_FTE_SUMMARY("DIS_ZERO_FTE_SUMMARY"),
36+
DIS_PRP_OR_YOUTH_SUMMARY("DIS_PRP_OR_YOUTH_SUMMARY"),
3637
DIS_BAND_RESIDENCE_HEADCOUNT("DIS_BAND_RESIDENCE_HEADCOUNT"),
3738
DIS_BAND_RESIDENCE_HEADCOUNT_PER_SCHOOL("DIS_BAND_RESIDENCE_HEADCOUNT_PER_SCHOOL"),
3839
;

api/src/main/java/ca/bc/gov/educ/studentdatacollection/api/constants/v1/HeadcountReportTypeCodes.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ public enum HeadcountReportTypeCodes {
2424
ELL_PER_SCHOOL("ell-per-school"),
2525
REFUGEE_PER_SCHOOL("refugee-per-school"),
2626
ZERO_FTE_SUMMARY("zero-fte-summary"),
27-
INCLUSIVE_EDUCATION_VARIANCE("INCLUSIVE_EDUCATION_VARIANCE");
27+
INCLUSIVE_EDUCATION_VARIANCE("INCLUSIVE_EDUCATION_VARIANCE"),
28+
PRP_OR_YOUTH_SUMMARY("prp-or-youth-summary"),
29+
;
2830

2931
private final String code;
3032
HeadcountReportTypeCodes(String code) { this.code = code; }

api/src/main/java/ca/bc/gov/educ/studentdatacollection/api/controller/v1/ReportGenerationController.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public class ReportGenerationController implements ReportGenerationEndpoint {
5555
private final SdcSchoolCollectionStudentSearchService sdcSchoolCollectionStudentSearchService;
5656
private final SdcSchoolCollectionHistoryService sdcSchoolCollectionHistoryService;
5757
private final ZeroFTEHeadCountReportService zeroFTEHeadCountReportService;
58+
private final PRPorYouthHeadcountReportService prpOrYouthHeadcountReportService;
5859
private static final SdcSchoolCollectionStudentMapper sdcSchoolCollectionStudentMapper = SdcSchoolCollectionStudentMapper.mapper;
5960

6061
@Override
@@ -119,6 +120,7 @@ public DownloadableReportResponse generateSDCDistrictReport(UUID sdcDistrictColl
119120
case ALL_STUDENT_ELL_DIS_CSV -> allStudentLightCollectionGenerateCsvService.generateEllFromSdcDistrictCollectionID(sdcDistrictCollectionID);
120121
case ALL_STUDENT_REFUGEE_DIS_CSV -> allStudentLightCollectionGenerateCsvService.generateRefugeeFromSdcDistrictCollectionID(sdcDistrictCollectionID);
121122
case DIS_ZERO_FTE_SUMMARY -> zeroFTEHeadCountReportService.generateZeroFTEHeadcountReport(sdcDistrictCollectionID);
123+
case DIS_PRP_OR_YOUTH_SUMMARY -> prpOrYouthHeadcountReportService.generatePerSchoolReport(sdcDistrictCollectionID);
122124
case DIS_BAND_RESIDENCE_HEADCOUNT -> bandOfResidenceHeadcountReportService.generateDistrictBandOfResidenceReport(sdcDistrictCollectionID);
123125
case DIS_BAND_RESIDENCE_HEADCOUNT_PER_SCHOOL -> bandOfResidenceHeadcountPerSchoolReportService.generateBandOfResidenceHeadcountPerSchoolReport(sdcDistrictCollectionID);
124126
default -> new DownloadableReportResponse();

api/src/main/java/ca/bc/gov/educ/studentdatacollection/api/controller/v1/SdcDistrictCollectionHeadcountReportsController.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ public SdcSchoolCollectionStudentHeadcounts getSdcSchoolCollectionStudentHeadcou
5858
return sdcDistrictCollectionHeadcountService.getRefugeePerSchoolHeadcounts(sdcDistrictCollectionEntity, compare);
5959
} else if (HeadcountReportTypeCodes.ZERO_FTE_SUMMARY.getCode().equals(type)) {
6060
return sdcDistrictCollectionHeadcountService.getZeroFTESummaryHeadcounts(sdcDistrictCollectionEntity, compare);
61-
} else if (HeadcountReportTypeCodes.INCLUSIVE_EDUCATION_VARIANCE .getCode().equals(type)) {
61+
} else if (HeadcountReportTypeCodes.INCLUSIVE_EDUCATION_VARIANCE.getCode().equals(type)) {
6262
return sdcDistrictCollectionHeadcountService.getSpecialEdVarianceHeadcounts(sdcDistrictCollectionEntity);
63+
} else if (HeadcountReportTypeCodes.PRP_OR_YOUTH_SUMMARY.getCode().equals(type)) {
64+
return sdcDistrictCollectionHeadcountService.getYouthPRPHeadcountsPerSchool(sdcDistrictCollectionEntity, compare);
6365
}
6466
return null;
6567
}
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
package ca.bc.gov.educ.studentdatacollection.api.helpers;
2+
3+
import ca.bc.gov.educ.studentdatacollection.api.constants.v1.FacilityTypeCodes;
4+
import ca.bc.gov.educ.studentdatacollection.api.constants.v1.SchoolGradeCodes;
5+
import ca.bc.gov.educ.studentdatacollection.api.exception.EntityNotFoundException;
6+
import ca.bc.gov.educ.studentdatacollection.api.exception.StudentDataCollectionAPIRuntimeException;
7+
import ca.bc.gov.educ.studentdatacollection.api.model.v1.SdcDistrictCollectionEntity;
8+
import ca.bc.gov.educ.studentdatacollection.api.model.v1.SdcSchoolCollectionEntity;
9+
import ca.bc.gov.educ.studentdatacollection.api.repository.v1.SdcDistrictCollectionRepository;
10+
import ca.bc.gov.educ.studentdatacollection.api.repository.v1.SdcSchoolCollectionRepository;
11+
import ca.bc.gov.educ.studentdatacollection.api.repository.v1.SdcSchoolCollectionStudentRepository;
12+
import ca.bc.gov.educ.studentdatacollection.api.rest.RestUtils;
13+
import ca.bc.gov.educ.studentdatacollection.api.struct.external.institute.v1.SchoolTombstone;
14+
import ca.bc.gov.educ.studentdatacollection.api.struct.v1.SdcSchoolCollection;
15+
import ca.bc.gov.educ.studentdatacollection.api.struct.v1.SdcSchoolCollectionStudent;
16+
import ca.bc.gov.educ.studentdatacollection.api.struct.v1.headcounts.*;
17+
import lombok.EqualsAndHashCode;
18+
import lombok.extern.slf4j.Slf4j;
19+
import org.springframework.stereotype.Component;
20+
21+
import java.util.*;
22+
import java.util.function.Function;
23+
24+
@Component
25+
@Slf4j
26+
@EqualsAndHashCode(callSuper = true)
27+
public class PRPorYouthHeadcountHelper extends HeadcountHelper<PRPorYouthHeadcountResult> {
28+
private static final String YOUTH_TITLE = "Youth Custody";
29+
private static final String SHORT_PRP_TITLE = "Short PRP";
30+
private static final String LONG_PRP_TITLE = "Long PRP";
31+
private static final String ALL_TITLE = "Total";
32+
private static final String REPORTED_TITLE = "Reported";
33+
private static final String ALL_SCHOOLS = "All Schools";
34+
private static final String SECTION = "section";
35+
private static final String TITLE = "title";
36+
private static final String TOTAL = "Total";
37+
38+
private final RestUtils restUtils;
39+
40+
public PRPorYouthHeadcountHelper(SdcSchoolCollectionRepository sdcSchoolCollectionRepository, SdcSchoolCollectionStudentRepository sdcSchoolCollectionStudentRepository, SdcDistrictCollectionRepository sdcDistrictCollectionRepository, RestUtils restUtils) {
41+
super(sdcSchoolCollectionRepository, sdcSchoolCollectionStudentRepository, sdcDistrictCollectionRepository, restUtils);
42+
this.restUtils = restUtils;
43+
headcountMethods = getHeadcountMethods();
44+
}
45+
46+
public void setGradeCodesForDistricts() {
47+
gradeCodes = SchoolGradeCodes.getNonIndependentKtoGAGrades();
48+
}
49+
50+
public void setComparisonValuesForDistrictBySchool(SdcDistrictCollectionEntity sdcDistrictCollectionEntity, List<HeadcountHeader> headcountHeaderList, HeadcountResultsTable collectionData) {
51+
UUID previousCollectionID = getPreviousCollectionIDByDistrictCollectionID(sdcDistrictCollectionEntity);
52+
Map<String, List<UUID>> youthPRPSchoolUUIDs = getPRPAndYouthSchoolUUIDs(previousCollectionID);
53+
List<UUID> youthPRPSchoolIDs = youthPRPSchoolUUIDs.get("ALLPRPORYOUTH");
54+
List<UUID> youthSchoolIDs = youthPRPSchoolUUIDs.get("YOUTH");
55+
List<UUID> shortPRPSchoolIDs = youthPRPSchoolUUIDs.get("SHORT_PRP");
56+
List<UUID> longPRPSchoolIDs = youthPRPSchoolUUIDs.get("LONG_PRP");
57+
58+
List<PRPorYouthHeadcountResult> collectionRawDataForHeadcount = sdcSchoolCollectionStudentRepository.getYouthPRPHeadcountsBySdcDistrictCollectionIdGroupBySchoolId(previousCollectionID, youthPRPSchoolIDs,
59+
youthSchoolIDs, shortPRPSchoolIDs, longPRPSchoolIDs);
60+
61+
HeadcountResultsTable previousCollectionData = convertHeadcountResultsToSchoolGradeTable(sdcDistrictCollectionEntity.getSdcDistrictCollectionID(), collectionRawDataForHeadcount);
62+
List<HeadcountHeader> previousHeadcountHeaderList = this.getHeaders(previousCollectionID, youthPRPSchoolIDs, youthSchoolIDs, shortPRPSchoolIDs, longPRPSchoolIDs);
63+
setComparisonValues(headcountHeaderList, previousHeadcountHeaderList);
64+
setResultsTableComparisonValuesDynamicNested(collectionData, previousCollectionData);
65+
}
66+
67+
public HeadcountResultsTable convertHeadcountResultsToSchoolGradeTable(UUID sdcDistrictCollectionID, List<PRPorYouthHeadcountResult> results) throws EntityNotFoundException {
68+
HeadcountResultsTable table = new HeadcountResultsTable();
69+
List<String> headers = new ArrayList<>();
70+
Set<String> grades = new HashSet<>(gradeCodes);
71+
Map<String, Map<String, Integer>> schoolGradeCounts = new HashMap<>();
72+
Map<String, Integer> totalCounts = new HashMap<>();
73+
Map<String, String> schoolDetails = new HashMap<>();
74+
75+
List<SchoolTombstone> allSchools = getAllSchoolTombstones(sdcDistrictCollectionID);
76+
77+
// Collect all grades and initialize school-grade map
78+
for (PRPorYouthHeadcountResult result : results) {
79+
if (grades.contains(result.getEnrolledGradeCode())) {
80+
schoolGradeCounts.computeIfAbsent(result.getSchoolID(), k -> new HashMap<>());
81+
schoolDetails.putIfAbsent(result.getSchoolID(),
82+
restUtils.getSchoolBySchoolID(result.getSchoolID())
83+
.map(school -> school.getMincode() + " - " + school.getDisplayName())
84+
.orElseThrow(() -> new EntityNotFoundException(SdcSchoolCollectionStudent.class, "SchoolID", result.getSchoolID())));
85+
}
86+
}
87+
88+
for (SchoolTombstone school : allSchools) {
89+
schoolGradeCounts.computeIfAbsent(school.getSchoolId(), k -> new HashMap<>());
90+
schoolDetails.putIfAbsent(school.getSchoolId(), school.getMincode() + " - " + school.getDisplayName());
91+
}
92+
93+
// Initialize totals for each grade
94+
for (String grade : gradeCodes) {
95+
totalCounts.put(grade, 0);
96+
schoolGradeCounts.values().forEach(school -> school.putIfAbsent(grade, 0));
97+
}
98+
99+
// Sort grades and add to headers
100+
headers.add(TITLE);
101+
headers.addAll(gradeCodes);
102+
headers.add(TOTAL);
103+
table.setHeaders(headers);
104+
105+
// Populate counts for each school and grade
106+
Map<String, Integer> schoolTotals = new HashMap<>();
107+
for (PRPorYouthHeadcountResult result : results) {
108+
if (gradeCodes.contains(result.getEnrolledGradeCode())) {
109+
Map<String, Integer> gradeCounts = schoolGradeCounts.get(result.getSchoolID());
110+
String grade = result.getEnrolledGradeCode();
111+
int count = getCountFromResult(result);
112+
gradeCounts.merge(grade, count, Integer::sum);
113+
totalCounts.merge(grade, count, Integer::sum);
114+
schoolTotals.merge(result.getSchoolID(), count, Integer::sum);
115+
}
116+
}
117+
118+
// Add all schools row at the start
119+
List<Map<String, HeadcountHeaderColumn>> rows = new ArrayList<>();
120+
Map<String, HeadcountHeaderColumn> totalRow = new LinkedHashMap<>();
121+
totalRow.put(TITLE, HeadcountHeaderColumn.builder().currentValue(ALL_SCHOOLS).build());
122+
totalRow.put(SECTION, HeadcountHeaderColumn.builder().currentValue(ALL_SCHOOLS).build());
123+
totalCounts.forEach((grade, count) -> totalRow.put(grade, HeadcountHeaderColumn.builder().currentValue(String.valueOf(count)).build()));
124+
totalRow.put(TOTAL, HeadcountHeaderColumn.builder().currentValue(String.valueOf(schoolTotals.values().stream().mapToInt(Integer::intValue).sum())).build());
125+
rows.add(totalRow);
126+
127+
// Create rows for the table, including school names
128+
schoolGradeCounts.forEach((schoolID, gradesCount) -> {
129+
Map<String, HeadcountHeaderColumn> rowData = new LinkedHashMap<>();
130+
rowData.put(TITLE, HeadcountHeaderColumn.builder().currentValue(schoolDetails.get(schoolID)).build());
131+
rowData.put(SECTION, HeadcountHeaderColumn.builder().currentValue(ALL_SCHOOLS).build());
132+
gradesCount.forEach((grade, count) -> rowData.put(grade, HeadcountHeaderColumn.builder().currentValue(String.valueOf(count)).build()));
133+
rowData.put(TOTAL, HeadcountHeaderColumn.builder().currentValue(String.valueOf(schoolTotals.getOrDefault(schoolID, 0))).build());
134+
rows.add(rowData);
135+
});
136+
137+
table.setRows(rows);
138+
return table;
139+
}
140+
141+
private int getCountFromResult(PRPorYouthHeadcountResult result) {
142+
try {
143+
return Integer.parseInt(result.getYouthPRPTotals());
144+
} catch (NumberFormatException e) {
145+
log.error("Error parsing count from result for SchoolID {}: {}", result.getSchoolID(), e.getMessage());
146+
return 0;
147+
}
148+
}
149+
150+
public List<HeadcountHeader> getHeaders(UUID sdcDistrictCollectionID, List<UUID> youthPRPSchoolIDs, List<UUID> youthSchoolIDs, List<UUID> shortPRPSchoolIDs, List<UUID> longPRPSchoolIDs) {
151+
PRPorYouthHeadcountResult result = sdcSchoolCollectionStudentRepository.getPRPYouthHeadersByDistrictId(sdcDistrictCollectionID, youthPRPSchoolIDs, youthSchoolIDs, shortPRPSchoolIDs, longPRPSchoolIDs);
152+
List<HeadcountHeader> headcountHeaderList = new ArrayList<>();
153+
Arrays.asList(YOUTH_TITLE, SHORT_PRP_TITLE, LONG_PRP_TITLE, ALL_TITLE).forEach(headerTitle -> {
154+
HeadcountHeader headcountHeader = new HeadcountHeader();
155+
headcountHeader.setColumns(new HashMap<>());
156+
headcountHeader.setTitle(headerTitle);
157+
headcountHeader.setOrderedColumnTitles(List.of(REPORTED_TITLE));
158+
switch (headerTitle) {
159+
case YOUTH_TITLE -> {
160+
headcountHeader.getColumns().put(REPORTED_TITLE, HeadcountHeaderColumn.builder().currentValue(result.getYouthTotals()).build());
161+
}
162+
case SHORT_PRP_TITLE -> {
163+
headcountHeader.getColumns().put(REPORTED_TITLE, HeadcountHeaderColumn.builder().currentValue(result.getShortPRPTotals()).build());
164+
}
165+
case LONG_PRP_TITLE -> {
166+
headcountHeader.getColumns().put(REPORTED_TITLE, HeadcountHeaderColumn.builder().currentValue(result.getLongPRPTotals()).build());
167+
}
168+
case ALL_TITLE -> {
169+
headcountHeader.getColumns().put(REPORTED_TITLE, HeadcountHeaderColumn.builder().currentValue(result.getYouthPRPTotals()).build());
170+
}
171+
default -> {
172+
log.error("Unexpected header title. This cannot happen::" + headerTitle);
173+
throw new StudentDataCollectionAPIRuntimeException("Unexpected header title. This cannot happen::" + headerTitle);
174+
}
175+
}
176+
headcountHeaderList.add(headcountHeader);
177+
});
178+
return headcountHeaderList;
179+
}
180+
181+
private Map<String, Function<PRPorYouthHeadcountResult, String>> getHeadcountMethods() {
182+
Map<String, Function<PRPorYouthHeadcountResult, String>> headcountMethods = new HashMap<>();
183+
184+
headcountMethods.put(YOUTH_TITLE, PRPorYouthHeadcountResult::getYouthTotals);
185+
headcountMethods.put(SHORT_PRP_TITLE, PRPorYouthHeadcountResult::getShortPRPTotals);
186+
headcountMethods.put(LONG_PRP_TITLE, PRPorYouthHeadcountResult::getLongPRPTotals);
187+
headcountMethods.put(ALL_TITLE, PRPorYouthHeadcountResult::getYouthPRPTotals);
188+
return headcountMethods;
189+
}
190+
191+
public Map<String, List<SchoolTombstone>> getAllPRPAndYouthSchoolTombstones(UUID sdcDistrictCollectionID) {
192+
List<SdcSchoolCollectionEntity> allSchoolCollections = sdcSchoolCollectionRepository.findAllBySdcDistrictCollectionID(sdcDistrictCollectionID);
193+
194+
Map<String, String> facilityTypeCodes = Map.of(
195+
"YOUTH", FacilityTypeCodes.YOUTH.getCode(),
196+
"SHORT_PRP", FacilityTypeCodes.SHORT_PRP.getCode(),
197+
"LONG_PRP", FacilityTypeCodes.LONG_PRP.getCode()
198+
);
199+
200+
Map<String, List<SchoolTombstone>> result = new HashMap<>();
201+
// Get all schools
202+
List<SchoolTombstone> allSchools = allSchoolCollections.stream()
203+
.map(schoolCollection -> restUtils.getSchoolBySchoolID(schoolCollection.getSchoolID().toString())
204+
.orElseThrow(() -> new EntityNotFoundException(SdcSchoolCollection.class, "SchoolID", schoolCollection.getSchoolID().toString())))
205+
.filter(school -> facilityTypeCodes.containsValue(school.getFacilityTypeCode()))
206+
.toList();
207+
result.put("ALLPRPORYOUTH", allSchools);
208+
209+
// Get by type
210+
facilityTypeCodes.forEach((key, code) -> {
211+
List<SchoolTombstone> schools = allSchools.stream()
212+
.filter(school -> code.equals(school.getFacilityTypeCode()))
213+
.toList();
214+
result.put(key, schools);
215+
});
216+
217+
return result;
218+
}
219+
220+
public Map<String, List<UUID>> getPRPAndYouthSchoolUUIDs(UUID sdcDistrictCollectionID) {
221+
Map<String, List<SchoolTombstone>> schoolTombstoneMap = getAllPRPAndYouthSchoolTombstones(sdcDistrictCollectionID);
222+
Map<String, List<UUID>> uuidMap = new HashMap<>();
223+
schoolTombstoneMap.forEach((key, value) ->
224+
uuidMap.put(key, value.stream().map(SchoolTombstone::getSchoolId).map(UUID::fromString).toList())
225+
);
226+
return uuidMap;
227+
}
228+
229+
}

api/src/main/java/ca/bc/gov/educ/studentdatacollection/api/reports/BaseReportGenerationService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package ca.bc.gov.educ.studentdatacollection.api.reports;
22

3+
import ca.bc.gov.educ.studentdatacollection.api.constants.v1.FacilityTypeCodes;
34
import ca.bc.gov.educ.studentdatacollection.api.constants.v1.SchoolCategoryCodes;
45
import ca.bc.gov.educ.studentdatacollection.api.exception.EntityNotFoundException;
56
import ca.bc.gov.educ.studentdatacollection.api.exception.StudentDataCollectionAPIRuntimeException;
@@ -164,7 +165,7 @@ public List<SchoolTombstone> getAllSchoolTombstones(UUID sdcDistrictCollectionID
164165

165166
return allSchoolCollections.stream()
166167
.map(schoolCollection -> restUtils.getSchoolBySchoolID(schoolCollection.getSchoolID().toString())
167-
.orElseThrow(() -> new EntityNotFoundException(SdcSchoolCollection.class, "SchoolID", schoolCollection.getSchoolID().toString())))
168+
.orElseThrow(() -> new EntityNotFoundException(SdcSchoolCollection.class, "SchoolID", schoolCollection.getSchoolID().toString())))
168169
.toList();
169170
}
170171
}

0 commit comments

Comments
 (0)