@ampath/esm-reports-app 1.0.0-next.8 → 1.0.0-next.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/0.css +1 -0
- package/dist/0.css.map +1 -0
- package/dist/0.js +1 -0
- package/dist/0.js.map +1 -0
- package/dist/197.js +1 -1
- package/dist/282.js +1 -1
- package/dist/282.js.map +1 -1
- package/dist/295.css +1 -0
- package/dist/295.css.map +1 -0
- package/dist/295.js +1 -0
- package/dist/295.js.map +1 -0
- package/dist/299.js +2 -0
- package/dist/299.js.LICENSE.txt +3 -0
- package/dist/299.js.map +1 -0
- package/dist/300.js +1 -1
- package/dist/311.js +1 -1
- package/dist/311.js.map +1 -1
- package/dist/335.js +1 -1
- package/dist/353.js +1 -1
- package/dist/353.js.map +1 -1
- package/dist/354.js +2 -0
- package/dist/354.js.LICENSE.txt +20 -0
- package/dist/354.js.map +1 -0
- package/dist/410.js +1 -0
- package/dist/410.js.map +1 -0
- package/dist/457.js +2 -0
- package/dist/457.js.LICENSE.txt +261 -0
- package/dist/457.js.map +1 -0
- package/dist/478.js +1 -1
- package/dist/478.js.map +1 -1
- package/dist/480.js +1 -0
- package/dist/540.js +1 -1
- package/dist/540.js.map +1 -1
- package/dist/55.js +1 -1
- package/dist/600.js +2 -0
- package/dist/600.js.LICENSE.txt +35 -0
- package/dist/600.js.map +1 -0
- package/dist/652.js +1 -1
- package/dist/757.js +1 -0
- package/dist/757.js.map +1 -0
- package/dist/781.js +1 -0
- package/dist/781.js.map +1 -0
- package/dist/961.js +1 -1
- package/dist/961.js.map +1 -1
- package/dist/99.js +1 -1
- package/dist/99.js.map +1 -0
- package/dist/ampath-esm-reports-app.js +1 -1
- package/dist/ampath-esm-reports-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +4 -2
- package/src/common/daily-summary-table.tsx +55 -0
- package/src/common/linelist-table/index.ts +3 -0
- package/src/common/linelist-table/linelist-columns.ts +48 -0
- package/src/common/linelist-table/linelist-table.component.tsx +442 -0
- package/src/common/linelist-table/linelist-table.scss +306 -0
- package/src/common/linelist-table/linelist-table.test.tsx +120 -0
- package/src/common/linelist-table/use-all-rows.ts +93 -0
- package/src/common/linelist-table/use-column-layout.ts +123 -0
- package/src/common/report-error/index.ts +1 -0
- package/src/common/report-error/show-report-error.ts +25 -0
- package/src/common/report-filters/date-range.ts +137 -0
- package/src/common/report-filters/index.ts +4 -0
- package/src/common/report-filters/report-filters.component.tsx +317 -99
- package/src/common/report-filters/report-filters.scss +264 -32
- package/src/common/report-filters/types.ts +79 -0
- package/src/common/report-filters/use-restore-period.ts +43 -0
- package/src/common/report-header/index.ts +1 -0
- package/src/common/report-header/use-location-details.ts +58 -0
- package/src/common/report-page/download-sheet-pdf.ts +225 -0
- package/src/common/report-page/index.ts +3 -0
- package/src/common/report-page/report-page.component.tsx +32 -0
- package/src/common/report-page/report-page.scss +45 -0
- package/src/common/report-page/use-page-scale.ts +60 -0
- package/src/common/report-placeholder/has-report-data.ts +15 -0
- package/src/common/report-placeholder/index.ts +3 -0
- package/src/common/report-placeholder/report-placeholder.component.tsx +81 -0
- package/src/common/report-placeholder/report-placeholder.scss +48 -0
- package/src/common/report-placeholder/use-generated-location.ts +62 -0
- package/src/common/report-register/index.ts +6 -0
- package/src/common/report-register/register-columns.ts +46 -0
- package/src/common/report-register/register-layout.component.tsx +265 -0
- package/src/common/report-register/register-layout.scss +150 -0
- package/src/common/report-register/register-table.scss +71 -0
- package/src/common/report-register/report-parents.ts +29 -0
- package/src/common/report-register/rows-to-csv.ts +89 -0
- package/src/common/report-register/use-location-name.ts +54 -0
- package/src/common/report-register/use-patient-list.ts +92 -0
- package/src/common/report-skeleton/index.ts +1 -0
- package/src/common/report-skeleton/report-skeleton.component.tsx +46 -0
- package/src/common/report-skeleton/report-skeleton.scss +29 -0
- package/src/config-schema.ts +9 -1
- package/src/dashboard/report-catalog.ts +191 -0
- package/src/dashboard/reports-dasboard.tsx +189 -18
- package/src/dashboard/reports-dashboard.module.scss +357 -14
- package/src/index.ts +13 -1
- package/src/registers/moh-240/moh-240-register.scss +41 -0
- package/src/registers/moh-240/moh-240.component.test.tsx +166 -0
- package/src/registers/moh-240/moh-240.component.tsx +164 -0
- package/src/registers/moh-267/moh-267-register.component.tsx +407 -0
- package/src/registers/moh-267/moh-267-register.scss +298 -0
- package/src/registers/moh-267/moh-267-summary.ts +102 -0
- package/src/registers/moh-267/moh-267.component.tsx +94 -0
- package/src/registers/moh-267/moh-267.types.ts +47 -0
- package/src/registers/moh-362/moh-362-lab-register.component.tsx +551 -0
- package/src/registers/moh-362/moh-362-lab-register.scss +412 -0
- package/src/registers/moh-362/moh-362-linkage-register.component.tsx +215 -0
- package/src/registers/moh-362/moh-362-linkage-register.scss +91 -0
- package/src/registers/moh-362/moh-362.component.tsx +117 -0
- package/src/registers/moh-362/moh-362.scss +11 -0
- package/src/registers/moh-362/moh-362.types.ts +87 -0
- package/src/registers/presumptive-tb/presumptive-tb-register.component.tsx +391 -0
- package/src/registers/presumptive-tb/presumptive-tb-register.scss +262 -0
- package/src/registers/presumptive-tb/presumptive-tb.component.tsx +92 -0
- package/src/registers/presumptive-tb/presumptive-tb.types.ts +88 -0
- package/src/registers/register-catalog.ts +202 -0
- package/src/registers/register-file-name.ts +18 -0
- package/src/registers/tb-lab-register/tb-lab-register-table.component.tsx +116 -0
- package/src/registers/tb-lab-register/tb-lab-register-table.scss +123 -0
- package/src/registers/tb-lab-register/tb-lab-register.columns.ts +164 -0
- package/src/registers/tb-lab-register/tb-lab-register.component.test.tsx +232 -0
- package/src/registers/tb-lab-register/tb-lab-register.component.tsx +153 -0
- package/src/registers/tb4/tb4-parts.tsx +164 -0
- package/src/registers/tb4/tb4-patient-sheet.component.tsx +418 -0
- package/src/registers/tb4/tb4-register.scss +282 -0
- package/src/registers/tb4/tb4-treatment-sheet.component.tsx +349 -0
- package/src/registers/tb4/tb4.component.tsx +120 -0
- package/src/registers/tb4/tb4.types.ts +129 -0
- package/src/reports/acf-summary/acf-summary-form.component.tsx +176 -0
- package/src/reports/acf-summary/acf-summary-linelist.component.tsx +86 -0
- package/src/reports/acf-summary/acf-summary.component.tsx +97 -0
- package/src/reports/acf-summary/acf-summary.counts.ts +37 -0
- package/src/reports/acf-summary/acf-summary.scss +162 -0
- package/src/reports/datatable-wrapper/datatable-wrapper.component.tsx +63 -48
- package/src/reports/moh-240/moh-240-header.component.tsx +62 -0
- package/src/reports/moh-240/moh-240.columns.ts +113 -0
- package/src/reports/moh-240/moh-240.component.tsx +72 -9
- package/src/reports/moh-240/moh-240.scss +40 -0
- package/src/reports/moh-240/sub-reports/moh-240-register.component.tsx +31 -37
- package/src/reports/moh-240/sub-reports/page-summary.component.tsx +143 -106
- package/src/reports/moh-505/moh-505-header.component.tsx +67 -0
- package/src/reports/moh-505/moh-505.component.tsx +441 -0
- package/src/reports/moh-505/moh-505.scss +117 -0
- package/src/reports/moh-705B/moh-204b-register.component.tsx +246 -0
- package/src/reports/moh-705B/moh-204b.columns.ts +39 -0
- package/src/reports/moh-705B/moh-705b.component.tsx +659 -0
- package/src/reports/moh-705B/moh-705b.scss +172 -0
- package/src/reports/moh-705a/moh-705a.component.tsx +661 -0
- package/src/reports/moh-705a/moh-705a.scss +172 -0
- package/src/reports/moh-705a/registers/moh-204a-register.component.tsx +331 -0
- package/src/reports/moh-705a/registers/moh-204a.columns.ts +57 -0
- package/src/reports/moh-705a/type.ts +6 -0
- package/src/reports/moh-706/moh-706-header.component.tsx +66 -0
- package/src/reports/moh-706/moh-706.component.tsx +150 -24
- package/src/reports/moh-706/moh-706.scss +119 -11
- package/src/reports/moh-706/sub-reports/bacteriology/bacteriology.component.tsx +35 -28
- package/src/reports/moh-706/sub-reports/blood-chemistry/blood-chemistry.component.tsx +229 -305
- package/src/reports/moh-706/sub-reports/haematology/haematology.component.tsx +26 -19
- package/src/reports/moh-706/sub-reports/histology-and-cytology/histology-and-cytology.component.tsx +23 -23
- package/src/reports/moh-706/sub-reports/parasitology/parasitology.component.tsx +25 -18
- package/src/reports/moh-706/sub-reports/serology/serology.component.tsx +21 -14
- package/src/reports/moh-706/sub-reports/specimen-referral-to-higher-levels/specimen-referral-to-higher-levels.component.tsx +2 -2
- package/src/reports/moh-706/sub-reports/urine-analysis/urine-analysis.component.tsx +27 -20
- package/src/reports/moh-710/moh-710.component.tsx +815 -632
- package/src/reports/moh-710/moh-710.scss +142 -21
- package/src/reports/moh-710/registers/moh-413/moh-413-columns.ts +22 -0
- package/src/reports/moh-710/registers/moh-413/moh-413.component.tsx +140 -0
- package/src/reports/moh-710/registers/moh-413/moh-413.scss +64 -0
- package/src/reports/moh-710/registers/moh-510/moh-510.columns.ts +40 -0
- package/src/reports/moh-710/registers/moh-510/moh-510.component.tsx +191 -0
- package/src/reports/moh-710/registers/moh-510/moh-510.scss +130 -0
- package/src/reports/moh-710/registers/moh-511-710-register.component.tsx +268 -0
- package/src/reports/moh-710/registers/moh-511-710.columns.ts +52 -0
- package/src/reports/moh-710/registers/type.ts +31 -0
- package/src/reports/moh-711/moh-711.component.tsx +153 -1269
- package/src/reports/moh-711/moh711.scss +135 -20
- package/src/reports/moh-711/registers/moh-333-register.component.tsx +680 -0
- package/src/reports/moh-711/registers/moh-333.columns.ts +119 -0
- package/src/reports/moh-711/registers/moh-405-register.component.tsx +566 -0
- package/src/reports/moh-711/registers/moh-405.columns.ts +120 -0
- package/src/reports/moh-711/registers/moh-406-register.component.tsx +581 -0
- package/src/reports/moh-711/registers/moh-406.columns.ts +133 -0
- package/src/reports/moh-711/registers/moh-407-register.component.tsx +157 -0
- package/src/reports/moh-711/registers/moh-407.columns.ts +24 -0
- package/src/reports/moh-711/registers/moh-407.scss +64 -0
- package/src/reports/moh-711/registers/moh-510-register.component.tsx +55 -0
- package/src/reports/moh-711/registers/moh-511-register.component.tsx +270 -0
- package/src/reports/moh-711/registers/moh-511.columns.ts +59 -0
- package/src/reports/moh-711/registers/moh-512-columns.ts +94 -0
- package/src/reports/moh-711/registers/moh-512.component.tsx +191 -0
- package/src/reports/moh-711/registers/moh-512.scss +64 -0
- package/src/reports/moh-711/registers/tb-register-columns.ts +17 -0
- package/src/reports/moh-711/registers/tb-register.component.tsx +96 -0
- package/src/reports/moh-711/registers/tb-register.scss +64 -0
- package/src/reports/moh-711/registers/type.ts +192 -0
- package/src/reports/moh-711/sections/anc.component.tsx +159 -0
- package/src/reports/moh-711/sections/cervical-cancer.component.tsx +148 -0
- package/src/reports/moh-711/sections/chanis.component.tsx +389 -0
- package/src/reports/moh-711/sections/family-planning.component.tsx +299 -0
- package/src/reports/moh-711/sections/gbv.component.tsx +115 -0
- package/src/reports/moh-711/sections/maternity.component.tsx +321 -0
- package/src/reports/moh-711/sections/medical-social-work.component.tsx +73 -0
- package/src/reports/moh-711/sections/other.component.tsx +47 -0
- package/src/reports/moh-711/sections/physiotherapy.component.tsx +61 -0
- package/src/reports/moh-711/sections/pnc.component.tsx +127 -0
- package/src/reports/moh-711/sections/post-abortion.component.tsx +42 -0
- package/src/reports/moh-711/sections/rehabilitation.component.tsx +57 -0
- package/src/reports/moh-711/sections/report-compiled-by.component.tsx +50 -0
- package/src/reports/moh-711/sections/tb-screening.component.tsx +94 -0
- package/src/reports/moh-717/moh-717.component.tsx +149 -985
- package/src/reports/moh-717/moh717.scss +217 -21
- package/src/reports/moh-717/registers/nutrition-register.component.tsx +48 -0
- package/src/reports/moh-717/sections/finance.component.tsx +42 -0
- package/src/reports/moh-717/sections/inpatient.component.tsx +374 -0
- package/src/reports/moh-717/sections/maternity.component.tsx +126 -0
- package/src/reports/moh-717/sections/medical-records.component.tsx +35 -0
- package/src/reports/moh-717/sections/mortuary.component.tsx +43 -0
- package/src/reports/moh-717/sections/operations.component.tsx +53 -0
- package/src/reports/moh-717/sections/orthopaedic-trauma.component.tsx +98 -0
- package/src/reports/moh-717/sections/outpatient.component.tsx +597 -0
- package/src/reports/moh-717/sections/pharmacy.component.tsx +43 -0
- package/src/reports/moh-717/sections/preparedby.component.tsx +52 -0
- package/src/reports/moh-717/sections/special-services.component.tsx +114 -0
- package/src/reports/moh-729b/moh-729b-form.component.tsx +385 -0
- package/src/reports/moh-729b/moh-729b-linelist.component.tsx +205 -0
- package/src/reports/moh-729b/moh-729b-summary.component.tsx +258 -0
- package/src/reports/moh-729b/moh-729b-summary.config.ts +229 -0
- package/src/reports/moh-729b/moh-729b.component.tsx +157 -0
- package/src/reports/moh-729b/moh-729b.config.ts +235 -0
- package/src/reports/moh-729b/moh-729b.scss +276 -0
- package/src/reports/moh-729b/moh-729b.types.ts +111 -0
- package/src/reports/moh-731/linelist/moh-731-indicators.ts +1122 -0
- package/src/reports/moh-731/linelist/moh-731-linelist-sections.ts +472 -0
- package/src/reports/moh-731/linelist/moh-731-linelist.component.tsx +102 -0
- package/src/reports/moh-731/linelist/use-open-moh-731-linelist.ts +50 -0
- package/src/reports/moh-731/moh-731.component.tsx +269 -0
- package/src/reports/moh-731/moh-731.scss +496 -0
- package/src/reports/moh-731/registers/moh-366.columns.ts +38 -0
- package/src/reports/moh-731/registers/moh-366.component.tsx +471 -0
- package/src/reports/moh-731/registers/prep-register.component.tsx +284 -0
- package/src/reports/moh-731/registers/prep.columns.ts +34 -0
- package/src/reports/moh-731/sections/hiv-and-tb-treatment.component.tsx +1206 -0
- package/src/reports/moh-731/sections/hiv-testing-and-pre-exposure.component.tsx +727 -0
- package/src/reports/moh-731/sections/pmtct.component.tsx +572 -0
- package/src/reports/moh-731/summary/moh-731-summary.component.tsx +313 -0
- package/src/reports/moh-731/summary/moh-731-summary.config.ts +527 -0
- package/src/reports/moh-731/summary/moh-731-summary.scss +247 -0
- package/src/reports/moh-740/moh-740.component.scss +202 -0
- package/src/reports/moh-740/moh-740.component.tsx +1332 -0
- package/src/reports/moh-740/moh-740.resource.ts +54 -0
- package/src/reports/moh-740/registers/moh-222-daily-register/moh-222-daily-register.tsx +149 -0
- package/src/reports/moh-740/registers/moh-270-permanent-register/moh-270-permanent-register.tsx +123 -0
- package/src/reports/moh-740/registers/moh-740-patient-list.scss +0 -0
- package/src/reports/moh-740/registers/moh-740-patient-list.tsx +60 -0
- package/src/reports/moh-740/registers/moh-740.columns.ts +54 -0
- package/src/reports/moh-740/shared/data-cell/data-cell.scss +6 -0
- package/src/reports/moh-740/shared/data-cell/data-cell.tsx +23 -0
- package/src/reports/moh-740/shared/utils/indicator-register-map.ts +159 -0
- package/src/reports/moh-740/types/index.ts +223 -0
- package/src/reports/moh-745/moh-745.component.tsx +2058 -0
- package/src/reports/moh-745/moh-745.scss +142 -0
- package/src/reports/moh-745/registers/moh-412-register.component.tsx +249 -0
- package/src/reports/moh-745/registers/moh-412.columns.ts +52 -0
- package/src/reports/moh-745/registers/type.ts +223 -0
- package/src/reports/table-wrapper/table-row-mapper.component.tsx +20 -5
- package/src/reports/table-wrapper/table-wrapper.component.tsx +23 -14
- package/src/reports/table-wrapper/table-wrapper.scss +1 -16
- package/src/resources/moh-267.resource.ts +27 -0
- package/src/resources/moh-362.resource.ts +27 -0
- package/src/resources/moh-505.resource.ts +69 -0
- package/src/resources/moh-705.resource.ts +118 -0
- package/src/resources/moh-706.resource.ts +80 -0
- package/src/resources/moh-710.resource.ts +60 -9
- package/src/resources/moh-711.resource.ts +206 -9
- package/src/resources/moh-717.resource.ts +26 -9
- package/src/resources/moh-729b.resource.ts +25 -0
- package/src/resources/moh-731.resource.ts +90 -0
- package/src/resources/moh-745.resource.ts +76 -0
- package/src/resources/patient-list-page.ts +66 -0
- package/src/resources/presumptive-tb.resource.ts +27 -0
- package/src/root.component.tsx +79 -8
- package/src/root.scss +36 -0
- package/src/routes.json +3 -1
- package/src/utils/format-indicator.ts +39 -0
- package/src/utils/get-base-url.ts +39 -7
- package/src/utils/utils.ts +30 -1
- package/tsconfig.json +1 -1
- package/webpack.config.js +22 -1
- package/dist/138.js +0 -2
- package/dist/138.js.LICENSE.txt +0 -20
- package/dist/138.js.map +0 -1
- package/dist/184.js +0 -2
- package/dist/184.js.LICENSE.txt +0 -14
- package/dist/184.js.map +0 -1
- package/dist/205.js +0 -1
- package/dist/205.js.map +0 -1
- package/dist/812.js +0 -1
- package/dist/812.js.map +0 -1
- package/dist/879.js +0 -1
- package/dist/879.js.map +0 -1
- package/dist/ampath-esm-reports-app.js.buildmanifest.json +0 -539
- package/dist/main.js +0 -1
- package/dist/main.js.map +0 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
|
|
3
|
+
/** The wire format every report resource expects for dates */
|
|
4
|
+
export const DATE_FORMAT = 'YYYY-MM-DD';
|
|
5
|
+
|
|
6
|
+
/** The format the month dropdown round-trips through */
|
|
7
|
+
export const MONTH_FORMAT = 'YYYY-MM';
|
|
8
|
+
|
|
9
|
+
export function formatDate(date: dayjs.ConfigType): string {
|
|
10
|
+
return dayjs(date).format(DATE_FORMAT);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatMonth(date: dayjs.ConfigType): string {
|
|
14
|
+
return dayjs(date).format(MONTH_FORMAT);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Expands `YYYY-MM` into the first and last day of that month.
|
|
19
|
+
*
|
|
20
|
+
* Parsed in local time on purpose: reporting periods are calendar months at the
|
|
21
|
+
* facility, so a UTC parse would shift the boundaries for facilities east of
|
|
22
|
+
* Greenwich and pull a day of data in from the neighbouring month.
|
|
23
|
+
*/
|
|
24
|
+
export function monthToDateRange(month: string): { startDate: string; endDate: string } {
|
|
25
|
+
const start = dayjs(`${month}-01`).startOf('month');
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
startDate: start.format(DATE_FORMAT),
|
|
29
|
+
endDate: start.endOf('month').format(DATE_FORMAT),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function describePeriod(startDate: string, endDate: string, month?: string): string {
|
|
34
|
+
if (month) {
|
|
35
|
+
return dayjs(`${month}-01`).format('MMMM YYYY');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return `${dayjs(startDate).format('DD MMM YYYY')} to ${dayjs(endDate).format('DD MMM YYYY')}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The raw field values the user has entered, before they resolve to a period */
|
|
42
|
+
export interface PeriodDraft {
|
|
43
|
+
/** `true` for the Custom range tab and for daily mode, `false` for Monthly */
|
|
44
|
+
usesDateRange: boolean;
|
|
45
|
+
/** `YYYY`, Monthly only */
|
|
46
|
+
year?: string;
|
|
47
|
+
/** `MM`, Monthly only */
|
|
48
|
+
month?: string;
|
|
49
|
+
/** `YYYY-MM-DD`, Custom range only */
|
|
50
|
+
startDate?: string;
|
|
51
|
+
/** `YYYY-MM-DD`, Custom range only */
|
|
52
|
+
endDate?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Messages keyed by the field they belong against, so each shows its own */
|
|
56
|
+
export interface PeriodErrors {
|
|
57
|
+
year?: string;
|
|
58
|
+
month?: string;
|
|
59
|
+
startDate?: string;
|
|
60
|
+
endDate?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Order the fields appear in, so the summary quotes the first thing to fix */
|
|
64
|
+
const FIELD_ORDER: Array<keyof PeriodErrors> = ['year', 'month', 'startDate', 'endDate'];
|
|
65
|
+
|
|
66
|
+
export function firstError(errors: PeriodErrors): string | undefined {
|
|
67
|
+
return FIELD_ORDER.map((field) => errors[field]).find(Boolean);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validates whichever period the active tab is collecting.
|
|
72
|
+
*
|
|
73
|
+
* Each tab has its own failure modes and each message is attached to the field
|
|
74
|
+
* that caused it, so nothing is reported against a control the user cannot see
|
|
75
|
+
* on the tab they are on.
|
|
76
|
+
*
|
|
77
|
+
* The Monthly tab needs the future check in particular: the date picker
|
|
78
|
+
* enforces `maxDate` itself, but Year and Month are independent dropdowns, so a
|
|
79
|
+
* later month in the current year yields a period that has not happened yet.
|
|
80
|
+
*/
|
|
81
|
+
export function validatePeriod({ usesDateRange, year, month, startDate, endDate }: PeriodDraft): PeriodErrors {
|
|
82
|
+
const errors: PeriodErrors = {};
|
|
83
|
+
const today = dayjs();
|
|
84
|
+
|
|
85
|
+
if (usesDateRange) {
|
|
86
|
+
const start = dayjs(startDate);
|
|
87
|
+
const end = dayjs(endDate);
|
|
88
|
+
const hasStart = Boolean(startDate) && start.isValid();
|
|
89
|
+
const hasEnd = Boolean(endDate) && end.isValid();
|
|
90
|
+
|
|
91
|
+
if (!hasStart) {
|
|
92
|
+
errors.startDate = 'Enter a start date as dd/mm/yyyy.';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!hasEnd) {
|
|
96
|
+
errors.endDate = 'Enter an end date as dd/mm/yyyy.';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (hasStart && hasEnd && start.isAfter(end, 'day')) {
|
|
100
|
+
errors.startDate = 'The start date must be on or before the end date.';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (hasStart && !errors.startDate && start.isAfter(today, 'day')) {
|
|
104
|
+
errors.startDate = 'The start date cannot be in the future.';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (hasEnd && end.isAfter(today, 'day')) {
|
|
108
|
+
errors.endDate = 'The end date cannot be in the future.';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return errors;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!year) {
|
|
115
|
+
errors.year = 'Choose a year.';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!month) {
|
|
119
|
+
errors.month = 'Choose a month.';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (year && month) {
|
|
123
|
+
// dayjs rolls an out-of-range month over into the next year rather than
|
|
124
|
+
// reporting it invalid, so `2026-13` would read as a future date instead of
|
|
125
|
+
// a malformed one. Check the parts before trusting the parse.
|
|
126
|
+
const monthNumber = Number(month);
|
|
127
|
+
const isWellFormed = /^\d{4}$/.test(year) && Number.isInteger(monthNumber) && monthNumber >= 1 && monthNumber <= 12;
|
|
128
|
+
|
|
129
|
+
if (!isWellFormed) {
|
|
130
|
+
errors.month = 'That is not a valid month.';
|
|
131
|
+
} else if (dayjs(`${year}-${month}-01`).isAfter(today, 'month')) {
|
|
132
|
+
errors.month = 'That month has not started yet.';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return errors;
|
|
137
|
+
}
|
|
@@ -1,124 +1,342 @@
|
|
|
1
|
-
import { ArrowLeft } from '@carbon/react/icons';
|
|
2
|
-
import { useNavigate } from 'react-router-dom';
|
|
3
1
|
import React from 'react';
|
|
4
|
-
import {
|
|
2
|
+
import { Link, useSearchParams } from 'react-router-dom';
|
|
3
|
+
import {
|
|
4
|
+
Breadcrumb,
|
|
5
|
+
BreadcrumbItem,
|
|
6
|
+
Button,
|
|
7
|
+
ContentSwitcher,
|
|
8
|
+
DatePicker,
|
|
9
|
+
DatePickerInput,
|
|
10
|
+
Layer,
|
|
11
|
+
Select,
|
|
12
|
+
SelectItem,
|
|
13
|
+
Switch,
|
|
14
|
+
} from '@carbon/react';
|
|
15
|
+
import { Calendar, ChartColumn, Download, WarningAlt } from '@carbon/react/icons';
|
|
16
|
+
import classNames from 'classnames';
|
|
17
|
+
import dayjs from 'dayjs';
|
|
5
18
|
|
|
19
|
+
import {
|
|
20
|
+
DATE_FORMAT,
|
|
21
|
+
describePeriod,
|
|
22
|
+
firstError,
|
|
23
|
+
formatDate,
|
|
24
|
+
formatMonth,
|
|
25
|
+
monthToDateRange,
|
|
26
|
+
validatePeriod,
|
|
27
|
+
} from './date-range';
|
|
28
|
+
import { showReportError } from '../report-error';
|
|
29
|
+
import { type ReportFiltersProps } from './types';
|
|
6
30
|
import styles from './report-filters.scss';
|
|
7
31
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
const ReportFiltersComponent: React.FC<ReportFiltersComponentProps> = ({
|
|
32
|
+
const months = Array.from({ length: 12 }, (_, index) => ({
|
|
33
|
+
value: String(index + 1).padStart(2, '0'),
|
|
34
|
+
label: dayjs().month(index).format('MMMM'),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const ReportFiltersComponent: React.FC<ReportFiltersProps> = ({
|
|
16
38
|
reportName,
|
|
17
|
-
mode = '
|
|
18
|
-
|
|
39
|
+
mode = 'both',
|
|
40
|
+
facilities,
|
|
41
|
+
yearsBack = 6,
|
|
42
|
+
isLoading = false,
|
|
43
|
+
isReportGenerated = false,
|
|
44
|
+
generatedPeriod,
|
|
19
45
|
onGenerate,
|
|
20
|
-
|
|
46
|
+
onPeriodChange,
|
|
47
|
+
onDownloadPdf,
|
|
48
|
+
backLink = { label: 'All reports', to: '/' },
|
|
49
|
+
defaultTab = 'monthly',
|
|
21
50
|
}) => {
|
|
22
|
-
|
|
51
|
+
/* A register's breadcrumb links back here carrying the period the report was
|
|
52
|
+
generated for. Seeding the controls from it matters as much as restoring
|
|
53
|
+
the figures: left on their defaults the bar would compare last month
|
|
54
|
+
against the restored period and warn that figures are stale on arrival. */
|
|
55
|
+
const [params] = useSearchParams();
|
|
56
|
+
const urlStart = params.get('startDate');
|
|
57
|
+
const urlEnd = params.get('endDate');
|
|
58
|
+
const urlMonth = params.get('month');
|
|
59
|
+
|
|
60
|
+
/* A period covering exactly one calendar month came from the Monthly tab,
|
|
61
|
+
however it is spelled in the URL. */
|
|
62
|
+
const urlWholeMonth =
|
|
63
|
+
urlMonth ??
|
|
64
|
+
(urlStart &&
|
|
65
|
+
urlEnd &&
|
|
66
|
+
dayjs(urlStart).isSame(dayjs(urlStart).startOf('month'), 'day') &&
|
|
67
|
+
dayjs(urlEnd).isSame(dayjs(urlStart).endOf('month'), 'day')
|
|
68
|
+
? dayjs(urlStart).format('YYYY-MM')
|
|
69
|
+
: null);
|
|
70
|
+
|
|
71
|
+
/* a period in the URL decides; otherwise the report's own default */
|
|
72
|
+
const [activeTab, setActiveTab] = React.useState<'monthly' | 'custom'>(
|
|
73
|
+
urlStart && urlEnd ? (urlWholeMonth ? 'monthly' : 'custom') : defaultTab,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const [startDateString, setStartDateString] = React.useState<string>(() => urlStart ?? formatDate(new Date()));
|
|
77
|
+
const [endDateString, setEndDateString] = React.useState<string>(() => urlEnd ?? formatDate(new Date()));
|
|
78
|
+
const [monthString, setMonthString] = React.useState<string>(
|
|
79
|
+
() => urlWholeMonth ?? formatMonth(dayjs().subtract(1, 'month')),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const [selectedFacility, setSelectedFacility] = React.useState<string>('');
|
|
83
|
+
|
|
84
|
+
const [selectedYear, selectedMonth] = monthString.split('-');
|
|
85
|
+
|
|
86
|
+
/** Recomputed per render so a session open across New Year still offers this year */
|
|
87
|
+
const years = React.useMemo(
|
|
88
|
+
() => Array.from({ length: yearsBack }, (_, index) => String(dayjs().year() - index)),
|
|
89
|
+
[yearsBack],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const showsSwitcher = mode === 'both';
|
|
93
|
+
const usesDateRange = mode === 'range' || (showsSwitcher && activeTab === 'custom');
|
|
94
|
+
const usesMonth = mode === 'monthly' || (showsSwitcher && activeTab === 'monthly');
|
|
95
|
+
|
|
96
|
+
const dateRange = React.useMemo(
|
|
97
|
+
() => [dayjs(startDateString).toDate(), dayjs(endDateString).toDate()],
|
|
98
|
+
[startDateString, endDateString],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const period = React.useMemo(() => {
|
|
102
|
+
const month = usesDateRange ? undefined : monthString;
|
|
103
|
+
const { startDate, endDate } = month
|
|
104
|
+
? monthToDateRange(month)
|
|
105
|
+
: { startDate: startDateString, endDate: endDateString };
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
startDate,
|
|
109
|
+
endDate,
|
|
110
|
+
month,
|
|
111
|
+
facility: facilities ? selectedFacility : undefined,
|
|
112
|
+
label: describePeriod(startDate, endDate, month),
|
|
113
|
+
};
|
|
114
|
+
}, [usesDateRange, monthString, startDateString, endDateString, facilities, selectedFacility]);
|
|
115
|
+
|
|
116
|
+
/* What the report currently on screen was generated for, so a change to the
|
|
117
|
+
filters can be called out rather than silently leaving stale figures up. */
|
|
118
|
+
const [generatedFor, setGeneratedFor] = React.useState<{ key: string; label: string } | null>(null);
|
|
119
|
+
|
|
120
|
+
const errors = React.useMemo(
|
|
121
|
+
() =>
|
|
122
|
+
validatePeriod({
|
|
123
|
+
usesDateRange,
|
|
124
|
+
year: selectedYear,
|
|
125
|
+
month: selectedMonth,
|
|
126
|
+
startDate: startDateString,
|
|
127
|
+
endDate: endDateString,
|
|
128
|
+
}),
|
|
129
|
+
[usesDateRange, selectedYear, selectedMonth, startDateString, endDateString],
|
|
130
|
+
);
|
|
131
|
+
const isValid = Object.keys(errors).length === 0;
|
|
23
132
|
|
|
24
|
-
|
|
25
|
-
|
|
133
|
+
/* Reported on change so a report can label its form with the chosen period
|
|
134
|
+
before any figures exist for it. */
|
|
135
|
+
React.useEffect(() => {
|
|
136
|
+
onPeriodChange?.(period);
|
|
137
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
138
|
+
}, [period]);
|
|
139
|
+
|
|
140
|
+
const periodKey = `${period.startDate}|${period.endDate}|${period.facility ?? ''}`;
|
|
141
|
+
|
|
142
|
+
/* Falls back to the period the caller says is on screen, which is what makes
|
|
143
|
+
the warning work after a reload rather than only after a generate. */
|
|
144
|
+
const shownPeriod =
|
|
145
|
+
generatedFor ??
|
|
146
|
+
(generatedPeriod?.startDate && generatedPeriod?.endDate
|
|
147
|
+
? {
|
|
148
|
+
key: `${generatedPeriod.startDate}|${generatedPeriod.endDate}|`,
|
|
149
|
+
label: describePeriod(generatedPeriod.startDate, generatedPeriod.endDate),
|
|
150
|
+
}
|
|
151
|
+
: null);
|
|
152
|
+
|
|
153
|
+
const [isPreparingPdf, setIsPreparingPdf] = React.useState(false);
|
|
154
|
+
const downloadPdf = async () => {
|
|
155
|
+
if (!onDownloadPdf) {
|
|
156
|
+
window.print();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
setIsPreparingPdf(true);
|
|
160
|
+
try {
|
|
161
|
+
await onDownloadPdf();
|
|
162
|
+
} catch (error) {
|
|
163
|
+
showReportError(`the ${reportName} PDF`, error);
|
|
164
|
+
} finally {
|
|
165
|
+
setIsPreparingPdf(false);
|
|
166
|
+
}
|
|
26
167
|
};
|
|
27
168
|
|
|
28
|
-
const
|
|
29
|
-
const date = new Date();
|
|
30
|
-
date.setMonth(date.getMonth() - 1);
|
|
169
|
+
const isStale = isReportGenerated && shownPeriod !== null && shownPeriod.key !== periodKey;
|
|
31
170
|
|
|
32
|
-
|
|
33
|
-
|
|
171
|
+
const handleGenerate = () => {
|
|
172
|
+
if (!isValid) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
34
175
|
|
|
35
|
-
|
|
176
|
+
setGeneratedFor({ key: periodKey, label: period.label });
|
|
177
|
+
onGenerate(period);
|
|
36
178
|
};
|
|
37
179
|
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (onGenerate) {
|
|
46
|
-
const filters: { startDate?: string; endDate?: string; month?: string } = {};
|
|
47
|
-
if (mode === 'daily') {
|
|
48
|
-
filters.startDate = startDateString;
|
|
49
|
-
filters.endDate = endDateString;
|
|
50
|
-
} else if (mode === 'monthly') {
|
|
51
|
-
filters.month = monthString;
|
|
52
|
-
}
|
|
53
|
-
onGenerate(filters);
|
|
180
|
+
/** Only commit a complete range, so re-rendering does not interrupt the second pick */
|
|
181
|
+
const handleDateRangeChange = (dates: Array<Date>) => {
|
|
182
|
+
const [start, end] = dates;
|
|
183
|
+
|
|
184
|
+
if (start && end) {
|
|
185
|
+
setStartDateString(dayjs(start).format(DATE_FORMAT));
|
|
186
|
+
setEndDateString(dayjs(end).format(DATE_FORMAT));
|
|
54
187
|
}
|
|
55
188
|
};
|
|
189
|
+
|
|
190
|
+
const dateRangePicker = (
|
|
191
|
+
<DatePicker
|
|
192
|
+
datePickerType="range"
|
|
193
|
+
dateFormat="d/m/Y"
|
|
194
|
+
maxDate={new Date()}
|
|
195
|
+
value={dateRange}
|
|
196
|
+
onChange={handleDateRangeChange}
|
|
197
|
+
className={styles.datePicker}
|
|
198
|
+
>
|
|
199
|
+
<DatePickerInput
|
|
200
|
+
id="report-start-date"
|
|
201
|
+
labelText="Start date"
|
|
202
|
+
placeholder="dd/mm/yyyy"
|
|
203
|
+
size="sm"
|
|
204
|
+
invalid={Boolean(errors.startDate)}
|
|
205
|
+
invalidText={errors.startDate}
|
|
206
|
+
/>
|
|
207
|
+
<DatePickerInput
|
|
208
|
+
id="report-end-date"
|
|
209
|
+
labelText="End date"
|
|
210
|
+
placeholder="dd/mm/yyyy"
|
|
211
|
+
size="sm"
|
|
212
|
+
invalid={Boolean(errors.endDate)}
|
|
213
|
+
invalidText={errors.endDate}
|
|
214
|
+
/>
|
|
215
|
+
</DatePicker>
|
|
216
|
+
);
|
|
217
|
+
|
|
56
218
|
return (
|
|
57
|
-
|
|
58
|
-
<
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
</div>
|
|
84
|
-
)}
|
|
85
|
-
{mode === 'monthly' && (
|
|
86
|
-
<div>
|
|
87
|
-
<div>
|
|
88
|
-
<div>
|
|
89
|
-
<label htmlFor="month">Month:</label>
|
|
90
|
-
<input
|
|
91
|
-
id="month"
|
|
92
|
-
type="month"
|
|
93
|
-
className={styles.input}
|
|
94
|
-
value={monthString}
|
|
95
|
-
onChange={(e) => setMonthString(e.target.value)}
|
|
96
|
-
/>
|
|
97
|
-
</div>
|
|
219
|
+
<div className={styles.filters}>
|
|
220
|
+
<Breadcrumb noTrailingSlash className={styles.breadcrumb}>
|
|
221
|
+
<BreadcrumbItem>
|
|
222
|
+
<Link to={backLink.to}>{backLink.label}</Link>
|
|
223
|
+
</BreadcrumbItem>
|
|
224
|
+
{/* The report name doubles as the page heading, so it stays an `h3`
|
|
225
|
+
rather than being repeated in a separate title row. */}
|
|
226
|
+
<BreadcrumbItem isCurrentPage>
|
|
227
|
+
<h3 className={styles.title}>{reportName}</h3>
|
|
228
|
+
</BreadcrumbItem>
|
|
229
|
+
</Breadcrumb>
|
|
230
|
+
|
|
231
|
+
<Layer className={styles.filtersContainer}>
|
|
232
|
+
<div className={styles.fields}>
|
|
233
|
+
{showsSwitcher && (
|
|
234
|
+
<div className={classNames(styles.field, styles.periodField)}>
|
|
235
|
+
<span className={styles.fieldLabel}>Period type</span>
|
|
236
|
+
<ContentSwitcher
|
|
237
|
+
selectedIndex={activeTab === 'monthly' ? 0 : 1}
|
|
238
|
+
onChange={({ index }) => setActiveTab(index === 0 ? 'monthly' : 'custom')}
|
|
239
|
+
size="sm"
|
|
240
|
+
className={styles.switcher}
|
|
241
|
+
>
|
|
242
|
+
<Switch name="monthly" text="Monthly" />
|
|
243
|
+
<Switch name="custom" text="Custom range" />
|
|
244
|
+
</ContentSwitcher>
|
|
98
245
|
</div>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
246
|
+
)}
|
|
247
|
+
|
|
248
|
+
{usesMonth && (
|
|
249
|
+
<>
|
|
250
|
+
<Select
|
|
251
|
+
id="report-year"
|
|
252
|
+
labelText="Year"
|
|
253
|
+
size="sm"
|
|
254
|
+
className={classNames(styles.field, styles.yearField)}
|
|
255
|
+
invalid={Boolean(errors.year)}
|
|
256
|
+
invalidText={errors.year}
|
|
257
|
+
value={selectedYear}
|
|
258
|
+
onChange={(event) => setMonthString(`${event.target.value}-${selectedMonth}`)}
|
|
259
|
+
>
|
|
260
|
+
{years.map((year) => (
|
|
261
|
+
<SelectItem key={year} value={year} text={year} />
|
|
262
|
+
))}
|
|
263
|
+
</Select>
|
|
264
|
+
<Select
|
|
265
|
+
id="report-month"
|
|
266
|
+
labelText="Month"
|
|
267
|
+
size="sm"
|
|
268
|
+
className={classNames(styles.field, styles.monthField)}
|
|
269
|
+
invalid={Boolean(errors.month)}
|
|
270
|
+
invalidText={errors.month}
|
|
271
|
+
value={selectedMonth}
|
|
272
|
+
onChange={(event) => setMonthString(`${selectedYear}-${event.target.value}`)}
|
|
273
|
+
>
|
|
274
|
+
{months.map((month) => (
|
|
275
|
+
<SelectItem key={month.value} value={month.value} text={month.label} />
|
|
276
|
+
))}
|
|
277
|
+
</Select>
|
|
278
|
+
</>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
{usesDateRange && <div className={classNames(styles.field, styles.rangeField)}>{dateRangePicker}</div>}
|
|
282
|
+
|
|
283
|
+
{facilities && (
|
|
284
|
+
<Select
|
|
285
|
+
id="report-facility"
|
|
286
|
+
labelText="Facility"
|
|
287
|
+
size="sm"
|
|
288
|
+
className={styles.field}
|
|
108
289
|
value={selectedFacility}
|
|
109
|
-
onChange={(
|
|
290
|
+
onChange={(event) => setSelectedFacility(event.target.value)}
|
|
110
291
|
>
|
|
111
|
-
<
|
|
112
|
-
|
|
292
|
+
<SelectItem value="" text="All facilities" />
|
|
293
|
+
{facilities.map((facility) => (
|
|
294
|
+
<SelectItem key={facility.value} value={facility.value} text={facility.label} />
|
|
295
|
+
))}
|
|
296
|
+
</Select>
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<div className={styles.actions}>
|
|
301
|
+
{/* Restating a period that cannot be generated reads as if it will be,
|
|
302
|
+
so the blocking reason takes its place. Each message is the icon
|
|
303
|
+
plus one span: left as loose text and <strong> in the flex row,
|
|
304
|
+
every fragment became a column of its own and wrapped apart. */}
|
|
305
|
+
{isValid && isStale ? (
|
|
306
|
+
<p className={classNames(styles.summary, styles.summaryStale)}>
|
|
307
|
+
<WarningAlt size={16} />
|
|
308
|
+
<span>
|
|
309
|
+
Filters changed. The report below is for <strong>{shownPeriod?.label}</strong> — select Generate
|
|
310
|
+
report to refresh it.
|
|
311
|
+
</span>
|
|
312
|
+
</p>
|
|
313
|
+
) : isValid ? (
|
|
314
|
+
<p className={styles.summary}>
|
|
315
|
+
<Calendar size={16} />
|
|
316
|
+
<span>
|
|
317
|
+
Reporting period: <strong>{period.label}</strong>
|
|
318
|
+
</span>
|
|
319
|
+
</p>
|
|
320
|
+
) : (
|
|
321
|
+
<p className={classNames(styles.summary, styles.summaryInvalid)}>
|
|
322
|
+
<WarningAlt size={16} />
|
|
323
|
+
<span>{firstError(errors)}</span>
|
|
324
|
+
</p>
|
|
325
|
+
)}
|
|
326
|
+
<div className={styles.actionButtons}>
|
|
327
|
+
{/* Only offered once there is something on screen to put on paper */}
|
|
328
|
+
{isReportGenerated && !isLoading && (
|
|
329
|
+
<Button kind="tertiary" size="sm" renderIcon={Download} onClick={downloadPdf} disabled={isPreparingPdf}>
|
|
330
|
+
{isPreparingPdf ? 'Preparing PDF…' : 'Download PDF'}
|
|
331
|
+
</Button>
|
|
332
|
+
)}
|
|
333
|
+
<Button size="sm" renderIcon={ChartColumn} onClick={handleGenerate} disabled={isLoading || !isValid}>
|
|
334
|
+
{isLoading ? 'Generating…' : 'Generate report'}
|
|
335
|
+
</Button>
|
|
113
336
|
</div>
|
|
114
|
-
)}
|
|
115
|
-
</div>
|
|
116
|
-
{!isLoding && (
|
|
117
|
-
<div className={styles.buttonContainer}>
|
|
118
|
-
<Button onClick={generateReport}>Generate Report</Button>
|
|
119
337
|
</div>
|
|
120
|
-
|
|
121
|
-
|
|
338
|
+
</Layer>
|
|
339
|
+
</div>
|
|
122
340
|
);
|
|
123
341
|
};
|
|
124
342
|
|