From b043b7e3e8f6bde2b209d550ae129d3b78763c39 Mon Sep 17 00:00:00 2001 From: Joosa Kurvinen Date: Fri, 8 Nov 2024 11:38:50 +0200 Subject: [PATCH 1/5] child is shown absent when they are not having an operational date --- .../child-attendance/utils.ts | 22 +++++++++++++------ .../child-info/AttendanceChildPage.tsx | 18 +++++++-------- .../generated/api-types/attendance.ts | 2 ++ .../espoo/evaka/attendance/ChildAttendance.kt | 1 + .../attendance/ChildAttendanceController.kt | 7 ++++++ 5 files changed, 34 insertions(+), 16 deletions(-) diff --git a/frontend/src/employee-mobile-frontend/child-attendance/utils.ts b/frontend/src/employee-mobile-frontend/child-attendance/utils.ts index e7bcc8e3f20..597eb6520a0 100644 --- a/frontend/src/employee-mobile-frontend/child-attendance/utils.ts +++ b/frontend/src/employee-mobile-frontend/child-attendance/utils.ts @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2017-2022 City of Espoo +// SPDX-FileCopyrightText: 2017-2024 City of Espoo // // SPDX-License-Identifier: LGPL-2.1-or-later @@ -31,28 +31,36 @@ export type AttendanceStatuses = Record< ChildAttendanceStatusResponse | undefined > +export type ChildAttendanceStatus = ChildAttendanceStatusResponse & { + noServiceToday?: boolean +} + export function childAttendanceStatus( child: AttendanceChild, attendanceStatuses: Record -): ChildAttendanceStatusResponse { +): ChildAttendanceStatus { const status = attendanceStatuses[child.id] if (status) return status - if (child.scheduleType === 'TERM_BREAK') { - return defaultChildAttendanceStatusTermBreak + if ( + child.scheduleType === 'TERM_BREAK' || + !child.operationalDates.some((date) => date.isToday()) + ) { + return childAttendanceStatusNoService } return defaultChildAttendanceStatus } -const defaultChildAttendanceStatus: ChildAttendanceStatusResponse = { +const defaultChildAttendanceStatus: ChildAttendanceStatus = { status: 'COMING', attendances: [], absences: [] } -const defaultChildAttendanceStatusTermBreak: ChildAttendanceStatusResponse = { +const childAttendanceStatusNoService: ChildAttendanceStatus = { status: 'ABSENT', attendances: [], - absences: [] + absences: [], + noServiceToday: true } diff --git a/frontend/src/employee-mobile-frontend/child-info/AttendanceChildPage.tsx b/frontend/src/employee-mobile-frontend/child-info/AttendanceChildPage.tsx index 4a0bcc665a0..7cd5a51b08d 100644 --- a/frontend/src/employee-mobile-frontend/child-info/AttendanceChildPage.tsx +++ b/frontend/src/employee-mobile-frontend/child-info/AttendanceChildPage.tsx @@ -206,7 +206,14 @@ export default React.memo(function AttendanceChildPage({ - {child.scheduleType !== 'TERM_BREAK' ? ( + {childAttendance.noServiceToday ? ( + <> +
+ {i18n.attendances.termBreak} +
+ + + ) : ( <> - ) : ( - <> -
- {i18n.attendances.termBreak} -
- - )} {childAttendance.status === 'COMING' && ( @@ -246,7 +246,7 @@ export default React.memo(function AttendanceChildPage({ )} {childAttendance.status === 'ABSENT' && - child.scheduleType !== 'TERM_BREAK' && ( + !childAttendance.noServiceToday && ( ): A dailyNote: (json.dailyNote != null) ? deserializeJsonChildDailyNote(json.dailyNote) : null, dailyServiceTimes: (json.dailyServiceTimes != null) ? deserializeJsonDailyServiceTimesValue(json.dailyServiceTimes) : null, dateOfBirth: LocalDate.parseIso(json.dateOfBirth), + operationalDates: json.operationalDates.map(e => LocalDate.parseIso(e)), reservations: json.reservations.map(e => deserializeJsonReservationResponse(e)), stickyNotes: json.stickyNotes.map(e => deserializeJsonChildStickyNote(e)) } diff --git a/service/src/main/kotlin/fi/espoo/evaka/attendance/ChildAttendance.kt b/service/src/main/kotlin/fi/espoo/evaka/attendance/ChildAttendance.kt index 31377437651..7f67b7b5800 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/attendance/ChildAttendance.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/attendance/ChildAttendance.kt @@ -38,6 +38,7 @@ data class AttendanceChild( val dateOfBirth: LocalDate, val placementType: PlacementType, val scheduleType: ScheduleType, + val operationalDates: Set, val groupId: GroupId?, val backup: Boolean, val dailyServiceTimes: DailyServiceTimesValue?, diff --git a/service/src/main/kotlin/fi/espoo/evaka/attendance/ChildAttendanceController.kt b/service/src/main/kotlin/fi/espoo/evaka/attendance/ChildAttendanceController.kt index fce7a159936..65ff2d53e19 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/attendance/ChildAttendanceController.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/attendance/ChildAttendanceController.kt @@ -34,6 +34,7 @@ import fi.espoo.evaka.shared.domain.EvakaClock import fi.espoo.evaka.shared.domain.FiniteDateRange import fi.espoo.evaka.shared.domain.HelsinkiDateTime import fi.espoo.evaka.shared.domain.TimeInterval +import fi.espoo.evaka.shared.domain.getOperationalDatesForChildren import fi.espoo.evaka.shared.security.AccessControl import fi.espoo.evaka.shared.security.Action import fi.espoo.evaka.shared.utils.mapOfNotNullValues @@ -77,6 +78,11 @@ class ChildAttendanceController( val childrenBasics = tx.fetchChildrenBasics(unitId, now) val childIds = childrenBasics.asSequence().map { it.id }.toSet() + val operationalDatesByChild = + tx.getOperationalDatesForChildren( + range = FiniteDateRange(today, today.plusDays(7)), + children = childIds, + ) val dailyNotes = tx.getChildDailyNotesForChildren(childIds).associateBy { it.childId } @@ -94,6 +100,7 @@ class ChildAttendanceController( placementType = child.placementType, scheduleType = child.placementType.scheduleType(today, clubTerms, preschoolTerms), + operationalDates = operationalDatesByChild[child.id] ?: emptySet(), groupId = child.groupId, backup = child.backup, dailyServiceTimes = child.dailyServiceTimes?.times, From 87b76196df921be3670e8c550406ab61068e23f8 Mon Sep 17 00:00:00 2001 From: Joosa Kurvinen Date: Fri, 8 Nov 2024 13:13:02 +0200 Subject: [PATCH 2/5] fix e2e test --- frontend/src/e2e-test/specs/6_mobile/messages.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/e2e-test/specs/6_mobile/messages.spec.ts b/frontend/src/e2e-test/specs/6_mobile/messages.spec.ts index 2c3db72a3aa..56a80a33edd 100644 --- a/frontend/src/e2e-test/specs/6_mobile/messages.spec.ts +++ b/frontend/src/e2e-test/specs/6_mobile/messages.spec.ts @@ -75,7 +75,7 @@ const staff2Name = `${staff2LastName} ${staff2FirstName}` const pin = '2580' -const mockedDate = LocalDate.of(2022, 5, 21) +const mockedDate = LocalDate.of(2022, 5, 20) // Friday const mockedDateAt10 = HelsinkiDateTime.fromLocal( mockedDate, LocalTime.of(10, 2) From 602ae66771c3afdbdd9d93e2796ceaec9d1bc17e Mon Sep 17 00:00:00 2001 From: Joosa Kurvinen Date: Fri, 8 Nov 2024 13:26:03 +0200 Subject: [PATCH 3/5] add e2e tests --- .../specs/6_mobile/child-attendances.spec.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/frontend/src/e2e-test/specs/6_mobile/child-attendances.spec.ts b/frontend/src/e2e-test/specs/6_mobile/child-attendances.spec.ts index 49ee306f452..c6735526c6c 100644 --- a/frontend/src/e2e-test/specs/6_mobile/child-attendances.spec.ts +++ b/frontend/src/e2e-test/specs/6_mobile/child-attendances.spec.ts @@ -542,6 +542,53 @@ describe('Child mobile attendance list', () => { await listPage.selectChild(child) await childPage.termBreak.waitUntilVisible() }) + + test('Non operational day child is shown in absent list', async () => { + // change mocked now to be during weekend + now = HelsinkiDateTime.of(2024, 5, 18, 13, 0, 0) + today = now.toLocalDate() + await openPage({ mockedTime: now }) + + const child = testChild2.id + await createPlacements(child, testDaycareGroup.id, 'PRESCHOOL') + + const mobileSignupUrl = await pairMobileDevice(testDaycare.id) + await page.goto(mobileSignupUrl) + + await assertAttendanceCounts(0, 0, 0, 1, 1) + await listPage.absentChildrenTab.click() + await listPage.selectChild(child) + await childPage.termBreak.waitUntilVisible() + }) + + test('Child with shift care is shown in coming list even on weekend', async () => { + // change mocked now to be during weekend + now = HelsinkiDateTime.of(2024, 5, 18, 13, 0, 0) + today = now.toLocalDate() + await openPage({ mockedTime: now }) + + const child = testChild2.id + const placement = await createPlacements( + child, + testDaycareGroup.id, + 'PRESCHOOL' + ) + const employee = await Fixture.employee().save() + const serviceNeedOption = await Fixture.serviceNeedOption().save() + await Fixture.serviceNeed({ + placementId: placement.id, + startDate: placement.startDate, + endDate: placement.endDate, + shiftCare: 'FULL', + optionId: serviceNeedOption.id, + confirmedBy: employee.id + }).save() + + const mobileSignupUrl = await pairMobileDevice(testDaycare.id) + await page.goto(mobileSignupUrl) + + await assertAttendanceCounts(1, 0, 0, 0, 1) + }) }) describe('Notes on child departure page', () => { From 40346b1125bc7132caa773170f30c2796cc59fcb Mon Sep 17 00:00:00 2001 From: Joosa Kurvinen Date: Mon, 11 Nov 2024 10:12:19 +0200 Subject: [PATCH 4/5] e2e fixes --- .../specs/6_mobile/child-attendances.spec.ts | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/frontend/src/e2e-test/specs/6_mobile/child-attendances.spec.ts b/frontend/src/e2e-test/specs/6_mobile/child-attendances.spec.ts index c6735526c6c..c218ec8f5f1 100644 --- a/frontend/src/e2e-test/specs/6_mobile/child-attendances.spec.ts +++ b/frontend/src/e2e-test/specs/6_mobile/child-attendances.spec.ts @@ -40,8 +40,7 @@ let listPage: MobileListPage let childPage: MobileChildPage let childAttendancePage: ChildAttendancePage -let now = HelsinkiDateTime.of(2024, 5, 17, 13, 0, 0) -let today = now.toLocalDate() +const now = HelsinkiDateTime.of(2024, 5, 17, 13, 0, 0) const group2 = { id: uuidv4(), @@ -84,7 +83,8 @@ beforeEach(async () => { async function createPlacements( childId: string, groupId: string = testDaycareGroup.id, - placementType: PlacementType = 'DAYCARE' + placementType: PlacementType = 'DAYCARE', + today: LocalDate = now.toLocalDate() ) { const daycarePlacementFixture = await Fixture.placement({ childId, @@ -471,6 +471,7 @@ describe('Child mobile attendance list', () => { startDate: LocalDate.of(2022, 1, 1) }).save() + const today = now.toLocalDate() const placement1StartDate = today.subMonths(5) const placement1EndDate = today.subMonths(1) @@ -527,12 +528,16 @@ describe('Child mobile attendance list', () => { test('Term break child is shown in absent list', async () => { // change mocked now to be during term break - now = HelsinkiDateTime.of(2024, 1, 1, 13, 0, 0) - today = now.toLocalDate() - await openPage({ mockedTime: now }) + const testNow = HelsinkiDateTime.of(2024, 1, 1, 13, 0, 0) + await openPage({ mockedTime: testNow }) const child = testChild2.id - await createPlacements(child, testDaycareGroup.id, 'PRESCHOOL') + await createPlacements( + child, + testDaycareGroup.id, + 'PRESCHOOL', + testNow.toLocalDate() + ) const mobileSignupUrl = await pairMobileDevice(testDaycare.id) await page.goto(mobileSignupUrl) @@ -545,12 +550,16 @@ describe('Child mobile attendance list', () => { test('Non operational day child is shown in absent list', async () => { // change mocked now to be during weekend - now = HelsinkiDateTime.of(2024, 5, 18, 13, 0, 0) - today = now.toLocalDate() - await openPage({ mockedTime: now }) + const testNow = HelsinkiDateTime.of(2024, 5, 18, 13, 0, 0) + await openPage({ mockedTime: testNow }) const child = testChild2.id - await createPlacements(child, testDaycareGroup.id, 'PRESCHOOL') + await createPlacements( + child, + testDaycareGroup.id, + 'PRESCHOOL', + testNow.toLocalDate() + ) const mobileSignupUrl = await pairMobileDevice(testDaycare.id) await page.goto(mobileSignupUrl) @@ -563,15 +572,15 @@ describe('Child mobile attendance list', () => { test('Child with shift care is shown in coming list even on weekend', async () => { // change mocked now to be during weekend - now = HelsinkiDateTime.of(2024, 5, 18, 13, 0, 0) - today = now.toLocalDate() - await openPage({ mockedTime: now }) + const testNow = HelsinkiDateTime.of(2024, 5, 18, 13, 0, 0) + await openPage({ mockedTime: testNow }) const child = testChild2.id const placement = await createPlacements( child, testDaycareGroup.id, - 'PRESCHOOL' + 'PRESCHOOL', + testNow.toLocalDate() ) const employee = await Fixture.employee().save() const serviceNeedOption = await Fixture.serviceNeedOption().save() From 221166e010d2229da5eacf341e272f4a9f055e44 Mon Sep 17 00:00:00 2001 From: Joosa Kurvinen Date: Tue, 12 Nov 2024 11:48:43 +0200 Subject: [PATCH 5/5] no undefined or double negatives --- .../child-attendance/utils.ts | 9 +++++---- .../child-info/AttendanceChildPage.tsx | 18 +++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/frontend/src/employee-mobile-frontend/child-attendance/utils.ts b/frontend/src/employee-mobile-frontend/child-attendance/utils.ts index 597eb6520a0..1cd18c1bed9 100644 --- a/frontend/src/employee-mobile-frontend/child-attendance/utils.ts +++ b/frontend/src/employee-mobile-frontend/child-attendance/utils.ts @@ -32,7 +32,7 @@ export type AttendanceStatuses = Record< > export type ChildAttendanceStatus = ChildAttendanceStatusResponse & { - noServiceToday?: boolean + hasOperationalDay: boolean } export function childAttendanceStatus( @@ -40,7 +40,7 @@ export function childAttendanceStatus( attendanceStatuses: Record ): ChildAttendanceStatus { const status = attendanceStatuses[child.id] - if (status) return status + if (status) return { ...status, hasOperationalDay: true } if ( child.scheduleType === 'TERM_BREAK' || @@ -55,12 +55,13 @@ export function childAttendanceStatus( const defaultChildAttendanceStatus: ChildAttendanceStatus = { status: 'COMING', attendances: [], - absences: [] + absences: [], + hasOperationalDay: true } const childAttendanceStatusNoService: ChildAttendanceStatus = { status: 'ABSENT', attendances: [], absences: [], - noServiceToday: true + hasOperationalDay: false } diff --git a/frontend/src/employee-mobile-frontend/child-info/AttendanceChildPage.tsx b/frontend/src/employee-mobile-frontend/child-info/AttendanceChildPage.tsx index 7cd5a51b08d..15ba66114a8 100644 --- a/frontend/src/employee-mobile-frontend/child-info/AttendanceChildPage.tsx +++ b/frontend/src/employee-mobile-frontend/child-info/AttendanceChildPage.tsx @@ -206,14 +206,7 @@ export default React.memo(function AttendanceChildPage({ - {childAttendance.noServiceToday ? ( - <> -
- {i18n.attendances.termBreak} -
- - - ) : ( + {childAttendance.hasOperationalDay ? ( <> + ) : ( + <> +
+ {i18n.attendances.termBreak} +
+ + )} {childAttendance.status === 'COMING' && ( @@ -246,7 +246,7 @@ export default React.memo(function AttendanceChildPage({ )} {childAttendance.status === 'ABSENT' && - !childAttendance.noServiceToday && ( + childAttendance.hasOperationalDay && (