From f7d99f38257b4f350f9601c0c09dcc99b92a4552 Mon Sep 17 00:00:00 2001 From: Joosa Kurvinen Date: Fri, 20 Dec 2024 16:15:16 +0200 Subject: [PATCH 1/2] child documents report --- frontend/src/employee-frontend/App.tsx | 9 + .../employee-frontend/components/Reports.tsx | 14 + .../reports/ChildDocumentsReport.tsx | 306 ++++++++++++++++++ .../components/reports/queries.ts | 19 ++ .../generated/api-clients/reports.ts | 37 +++ frontend/src/lib-common/generated/action.d.ts | 1 + .../lib-common/generated/api-types/reports.ts | 39 +++ .../defaults/employee/i18n/fi.tsx | 27 ++ .../src/main/kotlin/fi/espoo/evaka/Audit.kt | 2 + .../evaka/reports/ChildDocumentsReport.kt | 192 +++++++++++ .../espoo/evaka/reports/ReportPermissions.kt | 6 + .../fi/espoo/evaka/shared/security/Action.kt | 6 +- 12 files changed, 657 insertions(+), 1 deletion(-) create mode 100644 frontend/src/employee-frontend/components/reports/ChildDocumentsReport.tsx create mode 100644 service/src/main/kotlin/fi/espoo/evaka/reports/ChildDocumentsReport.kt diff --git a/frontend/src/employee-frontend/App.tsx b/frontend/src/employee-frontend/App.tsx index c27ed010d48..12fb8a372b0 100755 --- a/frontend/src/employee-frontend/App.tsx +++ b/frontend/src/employee-frontend/App.tsx @@ -78,6 +78,7 @@ import AttendanceReservation from './components/reports/AttendanceReservation' import AttendanceReservationByChild from './components/reports/AttendanceReservationByChild' import ReportChildAgeLanguage from './components/reports/ChildAgeLanguage' import ChildAttendanceReport from './components/reports/ChildAttendanceReport' +import ReportChildDocuments from './components/reports/ChildDocumentsReport' import ReportChildrenInDifferentAddress from './components/reports/ChildrenInDifferentAddress' import ReportCustomerFees from './components/reports/CustomerFees' import ReportDecisions from './components/reports/Decisions' @@ -615,6 +616,14 @@ export default createBrowserRouter( ) }, + { + path: '/reports/child-documents', + element: ( + + + + ) + }, { path: '/reports/assistance-needs-and-actions', element: ( diff --git a/frontend/src/employee-frontend/components/Reports.tsx b/frontend/src/employee-frontend/components/Reports.tsx index f13d7c374d5..ac7b0f26118 100755 --- a/frontend/src/employee-frontend/components/Reports.tsx +++ b/frontend/src/employee-frontend/components/Reports.tsx @@ -688,6 +688,20 @@ export default React.memo(function Reports() { /> ) } + : null, + reports.has('CHILD_DOCUMENTS') + ? { + name: i18n.reports.childDocuments.title, + item: ( + + ) + } : null ] diff --git a/frontend/src/employee-frontend/components/reports/ChildDocumentsReport.tsx b/frontend/src/employee-frontend/components/reports/ChildDocumentsReport.tsx new file mode 100644 index 00000000000..6d6bbeb03cf --- /dev/null +++ b/frontend/src/employee-frontend/components/reports/ChildDocumentsReport.tsx @@ -0,0 +1,306 @@ +// SPDX-FileCopyrightText: 2017-2024 City of Espoo +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +import orderBy from 'lodash/orderBy' +import React, { useMemo, useState } from 'react' +import styled from 'styled-components' + +import { combine } from 'lib-common/api' +import { useBoolean } from 'lib-common/form/hooks' +import { Daycare } from 'lib-common/generated/api-types/daycare' +import { + ChildDocumentsReportTemplate, + GroupRow +} from 'lib-common/generated/api-types/reports' +import { + DaycareId, + DocumentTemplateId +} from 'lib-common/generated/api-types/shared' +import { fromUuid } from 'lib-common/id-type' +import { useQueryResult } from 'lib-common/query' +import Title from 'lib-components/atoms/Title' +import { Button } from 'lib-components/atoms/buttons/Button' +import ReturnButton from 'lib-components/atoms/buttons/ReturnButton' +import TreeDropdown, { + TreeNode +} from 'lib-components/atoms/dropdowns/TreeDropdown' +import MultiSelect from 'lib-components/atoms/form/MultiSelect' +import Container, { ContentArea } from 'lib-components/layout/Container' +import { Table, Tbody, Td, Th, Thead, Tr } from 'lib-components/layout/Table' +import { P, Strong } from 'lib-components/typography' +import { defaultMargins } from 'lib-components/white-space' +import { faChevronDown, faChevronUp } from 'lib-icons' + +import { useTranslation } from '../../state/i18n' +import { renderResult } from '../async-rendering' +import { unitsQuery } from '../unit/queries' + +import { FilterLabel, FilterRow } from './common' +import { + childDocumentsReportQuery, + childDocumentsReportTemplateOptionsQuery +} from './queries' + +export default React.memo(function ChildDocumentsReport() { + const { i18n } = useTranslation() + + const units = useQueryResult(unitsQuery({ includeClosed: false })) + const unitOptions = useMemo( + () => units.map((res) => orderBy(res, (u) => u.name)), + [units] + ) + const templates = useQueryResult(childDocumentsReportTemplateOptionsQuery()) + + return ( + + + + {i18n.reports.childDocuments.title} +

{i18n.reports.childDocuments.description}

+

+ {i18n.reports.childDocuments.info} +
+ {i18n.reports.childDocuments.info2} +

+ {renderResult( + combine(unitOptions, templates), + ([unitOptions, templates]) => ( + + ) + )} +
+
+ ) +}) + +const FilterRowWide = styled(FilterRow)` + width: 750px; +` + +const FlexGrow = styled.div` + flex-grow: 1; +` + +const ChildDocumentsReportInner = React.memo( + function ChildDocumentsReportInner({ + units, + templates + }: { + units: Daycare[] + templates: ChildDocumentsReportTemplate[] + }) { + const { i18n } = useTranslation() + + const [selectedUnits, setSelectedUnits] = useState([]) + + const sortedTemplates = useMemo( + () => orderBy(templates, (t) => t.name), + [templates] + ) + const [templateTree, setTemplateTree] = useState( + [ + { + key: 'VASU', + text: i18n.reports.childDocuments.categories.VASU, + checked: false, + children: sortedTemplates + .filter((t) => ['VASU', 'MIGRATED_VASU'].includes(t.type)) + .map((t) => ({ + key: t.id, + text: t.name, + checked: false, + children: [] + })) + }, + { + key: 'LEOPS+HOJKS', + text: i18n.reports.childDocuments.categories.LEOPS_HOJKS, + checked: false, + children: sortedTemplates + .filter((t) => + ['LEOPS', 'MIGRATED_LEOPS', 'HOJKS'].includes(t.type) + ) + .map((t) => ({ + key: t.id, + text: t.name, + checked: false, + children: [] + })) + }, + { + key: 'OTHER', + text: i18n.reports.childDocuments.categories.OTHER, + checked: false, + children: sortedTemplates + .filter( + (t) => + ![ + 'VASU', + 'MIGRATED_VASU', + 'LEOPS', + 'MIGRATED_LEOPS', + 'HOJKS' + ].includes(t.type) + ) + .map((t) => ({ + key: t.id, + text: t.name, + checked: false, + children: [] + })) + } + ].filter((category) => category.children.length > 0) + ) + + const unitIds = useMemo( + () => selectedUnits.map((u) => u.id), + [selectedUnits] + ) + const templateIds = useMemo( + () => + templateTree.flatMap((category) => + category.children + .filter((t) => t.checked) + .map((t) => fromUuid(t.key)) + ), + [templateTree] + ) + + return ( + <> + + {i18n.reports.childDocuments.filters.units} + + o.name} + getOptionId={(o) => o.id} + placeholder={i18n.common.select} + /> + + + + + {i18n.reports.childDocuments.filters.templates} + + + + + + {unitIds.length > 0 && templateIds.length > 0 && ( + + )} + + ) + } +) + +const ChildDocumentsReportTable = React.memo( + function ChildDocumentsReportInner({ + unitIds, + templateIds + }: { + unitIds: DaycareId[] + templateIds: DocumentTemplateId[] + }) { + const { i18n } = useTranslation() + + const rowsResult = useQueryResult( + childDocumentsReportQuery({ unitIds, templateIds }) + ) + const orderedRows = useMemo( + () => rowsResult.map((res) => orderBy(res, (r) => r.unitName)), + [rowsResult] + ) + + return renderResult(orderedRows, (unitRows) => ( + + + + + + + + + + + + + {unitRows.map((row) => ( + + + + + + + + + + + + ))} + +
{i18n.reports.childDocuments.table.unitOrGroup}{i18n.reports.childDocuments.table.draft}{i18n.reports.childDocuments.table.prepared}{i18n.reports.childDocuments.table.completed}{i18n.reports.childDocuments.table.none}{i18n.reports.childDocuments.table.total}
+ {row.unitName} + {row.drafts}{row.prepared}{row.completed}{row.none}{row.total}
+ )) + } +) + +const TdIndented = styled(Td)` + padding-left: ${defaultMargins.L}; +` + +const GroupSection = React.memo(function GroupSection({ + groupRows +}: { + groupRows: GroupRow[] +}) { + const { i18n } = useTranslation() + const t = i18n.reports.childDocuments + + const [expanded, { toggle: toggleExpanded }] = useBoolean(false) + const orderedRows = useMemo( + () => orderBy(groupRows, (r) => r.groupName), + [groupRows] + ) + + return ( + <> + {expanded && + orderedRows.map((row) => ( + + {row.groupName} + {row.drafts} + {row.prepared} + {row.completed} + {row.none} + {row.total} + + ))} + + +