diff --git a/frontend/src/e2e-test/dev-api/fixtures.ts b/frontend/src/e2e-test/dev-api/fixtures.ts index ffcc90f72e5..27fa6c189c5 100644 --- a/frontend/src/e2e-test/dev-api/fixtures.ts +++ b/frontend/src/e2e-test/dev-api/fixtures.ts @@ -761,6 +761,8 @@ export class Fixture { date: LocalDate.todayInHelsinkiTz(), arrived: LocalTime.nowInHelsinkiTz(), departed: LocalTime.nowInHelsinkiTz(), + modifiedAt: HelsinkiDateTime.now(), + modifiedBy: systemInternalUser.id, ...initial }) } diff --git a/frontend/src/e2e-test/generated/api-types.ts b/frontend/src/e2e-test/generated/api-types.ts index b4911ac487d..25bb339137b 100644 --- a/frontend/src/e2e-test/generated/api-types.ts +++ b/frontend/src/e2e-test/generated/api-types.ts @@ -405,6 +405,8 @@ export interface DevChildAttendance { childId: PersonId date: LocalDate departed: LocalTime | null + modifiedAt: HelsinkiDateTime + modifiedBy: EvakaUserId unitId: DaycareId } @@ -1271,7 +1273,8 @@ export function deserializeJsonDevChildAttendance(json: JsonOf { unitId: testDaycare.id, date, arrived: LocalTime.of(8, 15), - departed: LocalTime.of(16, 30) + departed: LocalTime.of(16, 30), + modifiedAt: HelsinkiDateTime.now(), + modifiedBy: systemInternalUser.id })) ) }) diff --git a/frontend/src/employee-frontend/components/unit/unit-reservations/ChildDateModal.tsx b/frontend/src/employee-frontend/components/unit/unit-reservations/ChildDateModal.tsx index 6cc9b0191be..ced47c8e0c1 100644 --- a/frontend/src/employee-frontend/components/unit/unit-reservations/ChildDateModal.tsx +++ b/frontend/src/employee-frontend/components/unit/unit-reservations/ChildDateModal.tsx @@ -197,8 +197,8 @@ export default React.memo(function ChildDateModal({ attendances: editingFuture ? [] : childDayRecord.attendances.map((a) => ({ - startTime: a.formatStart(), - endTime: a.formatEnd() + startTime: a.interval.formatStart(), + endTime: a.interval.formatEnd() })), billableAbsence: childDayRecord.possibleAbsenceCategories.includes( 'BILLABLE' diff --git a/frontend/src/employee-frontend/components/unit/unit-reservations/ChildDayAttendance.tsx b/frontend/src/employee-frontend/components/unit/unit-reservations/ChildDayAttendance.tsx index ed93c9fff5a..d9c790b6278 100644 --- a/frontend/src/employee-frontend/components/unit/unit-reservations/ChildDayAttendance.tsx +++ b/frontend/src/employee-frontend/components/unit/unit-reservations/ChildDayAttendance.tsx @@ -16,14 +16,15 @@ import { ChildServiceNeedInfo } from 'lib-common/generated/api-types/absence' import { DailyServiceTimesValue } from 'lib-common/generated/api-types/dailyservicetimes' import { ScheduleType } from 'lib-common/generated/api-types/placement' import { + AttendanceTimesForDate, Reservation, UnitDateInfo } from 'lib-common/generated/api-types/reservations' import LocalDate from 'lib-common/local-date' import { reservationHasTimes } from 'lib-common/reservations' -import TimeInterval from 'lib-common/time-interval' import TimeRange from 'lib-common/time-range' import TimeRangeEndpoint from 'lib-common/time-range-endpoint' +import Tooltip from 'lib-components/atoms/Tooltip' import { IconOnlyButton } from 'lib-components/atoms/buttons/IconOnlyButton' import { fontWeights } from 'lib-components/typography' import { colors } from 'lib-customizations/common' @@ -38,7 +39,7 @@ interface Props { date: LocalDate attendanceIndex: number dateInfo: UnitDateInfo - attendance: TimeInterval | undefined + attendance: AttendanceTimesForDate | undefined reservations: Reservation[] dailyServiceTimes: DailyServiceTimesValue | null inOtherUnit: boolean @@ -111,50 +112,71 @@ export default React.memo(function ChildDayAttendance({ return ( - {!inOtherUnit && !isInBackupGroup ? ( - - {requiresBackupCare ? ( - - {i18n.unit.attendanceReservations.requiresBackupCare}{' '} - - - ) : ( - <> - - {attendance?.formatStart() ?? '-'} - - - {attendance?.end ? attendance.formatEnd() : '-'} - - - )} - - ) : null} - {!inOtherUnit && !isInBackupGroup && scheduleType !== 'TERM_BREAK' && ( - - - - )} + + {!inOtherUnit && !isInBackupGroup ? ( + + {requiresBackupCare ? ( + + {i18n.unit.attendanceReservations.requiresBackupCare}{' '} + + + ) : ( + <> + + {attendance?.interval?.formatStart() ?? '-'} + + + {attendance?.interval?.end + ? attendance.interval.formatEnd() + : '-'} + + + )} + + ) : null} + {!inOtherUnit && !isInBackupGroup && scheduleType !== 'TERM_BREAK' && ( + + + + )} + ) }) diff --git a/frontend/src/employee-frontend/components/unit/unit-reservations/ChildDayReservation.tsx b/frontend/src/employee-frontend/components/unit/unit-reservations/ChildDayReservation.tsx index 3e071f3d341..60e2bf76c78 100644 --- a/frontend/src/employee-frontend/components/unit/unit-reservations/ChildDayReservation.tsx +++ b/frontend/src/employee-frontend/components/unit/unit-reservations/ChildDayReservation.tsx @@ -119,8 +119,14 @@ export default React.memo(function ChildDayReservation({ ) : reservation && reservation.type === 'TIMES' ? ( )} - {reservation.staffCreated && '*'} + {reservation?.staffCreated && '*'} ) : reservationIndex === 0 ? ( diff --git a/frontend/src/lib-common/generated/api-types/attendance.ts b/frontend/src/lib-common/generated/api-types/attendance.ts index c4b08031a3e..35849b1a3c1 100644 --- a/frontend/src/lib-common/generated/api-types/attendance.ts +++ b/frontend/src/lib-common/generated/api-types/attendance.ts @@ -15,6 +15,7 @@ import { ChildStickyNote } from './note' import { DailyServiceTimesValue } from './dailyservicetimes' import { DaycareId } from './shared' import { EmployeeId } from './shared' +import { EvakaUser } from './user' import { GroupId } from './shared' import { HelsinkiDateTimeRange } from './shared' import { JsonOf } from '../../json' @@ -96,6 +97,8 @@ export type AttendanceStatus = export interface AttendanceTimes { arrived: HelsinkiDateTime departed: HelsinkiDateTime | null + modifiedAt: HelsinkiDateTime + modifiedBy: EvakaUser } /** @@ -485,7 +488,8 @@ export function deserializeJsonAttendanceTimes(json: JsonOf): A return { ...json, arrived: HelsinkiDateTime.parseIso(json.arrived), - departed: (json.departed != null) ? HelsinkiDateTime.parseIso(json.departed) : null + departed: (json.departed != null) ? HelsinkiDateTime.parseIso(json.departed) : null, + modifiedAt: HelsinkiDateTime.parseIso(json.modifiedAt) } } diff --git a/frontend/src/lib-common/generated/api-types/reservations.ts b/frontend/src/lib-common/generated/api-types/reservations.ts index 8a841c5b6e5..66006c481ce 100644 --- a/frontend/src/lib-common/generated/api-types/reservations.ts +++ b/frontend/src/lib-common/generated/api-types/reservations.ts @@ -5,6 +5,7 @@ // GENERATED FILE: no manual modifications import FiniteDateRange from '../../finite-date-range' +import HelsinkiDateTime from '../../helsinki-date-time' import LocalDate from '../../local-date' import TimeInterval from '../../time-interval' import TimeRange from '../../time-range' @@ -14,6 +15,7 @@ import { ChildImageId } from './shared' import { ChildServiceNeedInfo } from './absence' import { DailyServiceTimesValue } from './dailyservicetimes' import { DaycareId } from './shared' +import { EvakaUser } from './user' import { GroupId } from './shared' import { HolidayPeriodEffect } from './holidayperiod' import { JsonOf } from '../../json' @@ -49,6 +51,17 @@ export interface AbsenceTypeResponse { staffCreated: boolean } +/** +* Generated from fi.espoo.evaka.reservations.AttendanceTimesForDate +*/ +export interface AttendanceTimesForDate { + date: LocalDate + interval: TimeInterval + modifiedAt: HelsinkiDateTime + modifiedBy: EvakaUser + staffModified: boolean +} + /** * Generated from fi.espoo.evaka.reservations.AttendanceReservationController.BackupPlacementType */ @@ -87,7 +100,7 @@ export interface ChildDatePresence { export interface ChildRecordOfDay { absenceBillable: AbsenceTypeResponse | null absenceNonbillable: AbsenceTypeResponse | null - attendances: TimeInterval[] + attendances: AttendanceTimesForDate[] backupGroupId: GroupId | null childId: PersonId dailyServiceTimes: DailyServiceTimesValue | null @@ -338,6 +351,8 @@ export namespace ReservationResponse { */ export interface NoTimes { type: 'NO_TIMES' + modifiedAt: HelsinkiDateTime + modifiedBy: EvakaUser staffCreated: boolean } @@ -346,6 +361,8 @@ export namespace ReservationResponse { */ export interface Times { type: 'TIMES' + modifiedAt: HelsinkiDateTime + modifiedBy: EvakaUser range: TimeRange staffCreated: boolean } @@ -429,6 +446,16 @@ export function deserializeJsonAbsenceRequest(json: JsonOf): Abs } +export function deserializeJsonAttendanceTimesForDate(json: JsonOf): AttendanceTimesForDate { + return { + ...json, + date: LocalDate.parseIso(json.date), + interval: TimeInterval.parseJson(json.interval), + modifiedAt: HelsinkiDateTime.parseIso(json.modifiedAt) + } +} + + export function deserializeJsonChild(json: JsonOf): Child { return { ...json, @@ -451,7 +478,7 @@ export function deserializeJsonChildDatePresence(json: JsonOf export function deserializeJsonChildRecordOfDay(json: JsonOf): ChildRecordOfDay { return { ...json, - attendances: json.attendances.map(e => TimeInterval.parseJson(e)), + attendances: json.attendances.map(e => deserializeJsonAttendanceTimesForDate(e)), dailyServiceTimes: (json.dailyServiceTimes != null) ? deserializeJsonDailyServiceTimesValue(json.dailyServiceTimes) : null, reservations: json.reservations.map(e => deserializeJsonReservationResponse(e)) } @@ -620,14 +647,23 @@ export function deserializeJsonReservationChildInfo(json: JsonOf): ReservationResponse.NoTimes { + return { + ...json, + modifiedAt: HelsinkiDateTime.parseIso(json.modifiedAt) + } +} + export function deserializeJsonReservationResponseTimes(json: JsonOf): ReservationResponse.Times { return { ...json, + modifiedAt: HelsinkiDateTime.parseIso(json.modifiedAt), range: TimeRange.parseJson(json.range) } } export function deserializeJsonReservationResponse(json: JsonOf): ReservationResponse { switch (json.type) { + case 'NO_TIMES': return deserializeJsonReservationResponseNoTimes(json) case 'TIMES': return deserializeJsonReservationResponseTimes(json) default: return json } diff --git a/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx b/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx index cbf0e711dce..9081acdc320 100755 --- a/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx +++ b/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx @@ -2540,6 +2540,16 @@ export const fi = { requiresBackupCare: 'Tee varasijoitus', openReservationModal: 'Tee toistuva varaus', childCount: 'Lapsia läsnä', + lastModifiedStaff: (date: string, name: string) => ( +
+

*Henkilökunnan tekemä merkintä

+

+ Viimeksi muokattu {date}; muokkaaja: {name} +

+
+ ), + lastModifiedOther: (date: string, name: string) => + `Viimeksi muokattu ${date}; muokkaaja: ${name}`, reservationModal: { title: 'Tee varaus', selectedChildren: 'Lapset, joille varaus tehdään', diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/TestFixtures.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/TestFixtures.kt index 619051f505d..2db90e99e34 100755 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/TestFixtures.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/TestFixtures.kt @@ -38,6 +38,7 @@ import fi.espoo.evaka.shared.dev.DevCareArea import fi.espoo.evaka.shared.dev.DevDaycare import fi.espoo.evaka.shared.dev.DevDaycareGroup import fi.espoo.evaka.shared.dev.DevEmployee +import fi.espoo.evaka.shared.dev.DevMobileDevice import fi.espoo.evaka.shared.dev.DevPerson import fi.espoo.evaka.shared.dev.DevPreschoolTerm import fi.espoo.evaka.shared.dev.insert @@ -749,3 +750,13 @@ fun DevEmployee.toEvakaUser() = name = this.lastName + " " + this.firstName, type = EvakaUserType.EMPLOYEE, ) + +fun DevPerson.toEvakaUser(type: EvakaUserType) = + EvakaUser( + id = EvakaUserId(this.id.raw), + name = this.lastName + " " + this.firstName, + type = type, + ) + +fun DevMobileDevice.toEvakaUser() = + EvakaUser(id = EvakaUserId(this.id.raw), name = this.name, type = EvakaUserType.EMPLOYEE) diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/absence/AbsenceServiceIntegrationTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/absence/AbsenceServiceIntegrationTest.kt index 2d9eb69a850..4cc54793cea 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/absence/AbsenceServiceIntegrationTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/absence/AbsenceServiceIntegrationTest.kt @@ -555,7 +555,7 @@ class AbsenceServiceIntegrationTest : FullApplicationTest(resetDbBeforeEach = tr date = holidayPeriod.end.minusDays(2), startTime = null, endTime = null, - created = HelsinkiDateTime.of(placementStart, LocalTime.of(8, 0)), + createdAt = HelsinkiDateTime.of(placementStart, LocalTime.of(8, 0)), createdBy = EvakaUserId(employeeId.raw), ) ) @@ -565,7 +565,7 @@ class AbsenceServiceIntegrationTest : FullApplicationTest(resetDbBeforeEach = tr date = holidayPeriod.end.minusDays(1), startTime = LocalTime.of(8, 0), endTime = LocalTime.of(16, 0), - created = HelsinkiDateTime.of(placementStart, LocalTime.of(9, 0)), + createdAt = HelsinkiDateTime.of(placementStart, LocalTime.of(9, 0)), createdBy = EvakaUserId(employeeId.raw), ) ) diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/attendance/AttendanceQueriesTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/attendance/AttendanceQueriesTest.kt index a042076c009..f9e1b6d9877 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/attendance/AttendanceQueriesTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/attendance/AttendanceQueriesTest.kt @@ -5,6 +5,7 @@ package fi.espoo.evaka.attendance import fi.espoo.evaka.PureJdbiTest +import fi.espoo.evaka.shared.dev.DevEmployee import fi.espoo.evaka.shared.dev.DevPersonType import fi.espoo.evaka.shared.dev.insert import fi.espoo.evaka.shared.dev.insertTestChildAttendance @@ -13,6 +14,7 @@ import fi.espoo.evaka.testArea import fi.espoo.evaka.testChild_1 import fi.espoo.evaka.testChild_2 import fi.espoo.evaka.testDaycare +import fi.espoo.evaka.toEvakaUser import java.time.LocalDate import java.time.LocalTime import kotlin.test.assertEquals @@ -22,10 +24,12 @@ import org.junit.jupiter.api.Test class AttendanceQueriesTest : PureJdbiTest(resetDbBeforeEach = true) { private val now = HelsinkiDateTime.of(LocalDate.of(2021, 1, 1), LocalTime.of(12, 0, 0)) + private val employee = DevEmployee() @BeforeEach fun beforeEach() { db.transaction { tx -> + tx.insert(employee) tx.insert(testArea) tx.insert(testDaycare) listOf(testChild_1, testChild_2).forEach { tx.insert(it, DevPersonType.CHILD) } @@ -46,11 +50,23 @@ class AttendanceQueriesTest : PureJdbiTest(resetDbBeforeEach = true) { childId = testChild_1.id, arrived = now, departed = null, + modifiedAt = now, + modifiedBy = employee.evakaUserId, ) } val attendances = db.read { it.getUnitChildAttendances(testDaycare.id, now) } assertEquals( - mapOf(testChild_1.id to listOf(AttendanceTimes(arrived = now, departed = null))), + mapOf( + testChild_1.id to + listOf( + AttendanceTimes( + arrived = now, + departed = null, + modifiedAt = now, + modifiedBy = employee.toEvakaUser(), + ) + ) + ), attendances, ) } @@ -63,13 +79,22 @@ class AttendanceQueriesTest : PureJdbiTest(resetDbBeforeEach = true) { childId = testChild_1.id, arrived = now.minusHours(1), departed = now, + modifiedAt = now, + modifiedBy = employee.evakaUserId, ) } val attendances = db.read { it.getUnitChildAttendances(testDaycare.id, now) } assertEquals( mapOf( testChild_1.id to - listOf(AttendanceTimes(arrived = now.minusHours(1), departed = now)) + listOf( + AttendanceTimes( + arrived = now.minusHours(1), + departed = now, + modifiedAt = now, + modifiedBy = employee.toEvakaUser(), + ) + ) ), attendances, ) @@ -83,21 +108,39 @@ class AttendanceQueriesTest : PureJdbiTest(resetDbBeforeEach = true) { childId = testChild_1.id, arrived = now.minusHours(1), departed = now, + modifiedAt = now, + modifiedBy = employee.evakaUserId, ) tx.insertTestChildAttendance( unitId = testDaycare.id, childId = testChild_2.id, arrived = now.minusHours(2), departed = null, + modifiedAt = now, + modifiedBy = employee.evakaUserId, ) } val attendances = db.read { it.getUnitChildAttendances(testDaycare.id, now) } assertEquals( mapOf( testChild_1.id to - listOf(AttendanceTimes(arrived = now.minusHours(1), departed = now)), + listOf( + AttendanceTimes( + arrived = now.minusHours(1), + departed = now, + modifiedAt = now, + modifiedBy = employee.toEvakaUser(), + ) + ), testChild_2.id to - listOf(AttendanceTimes(arrived = now.minusHours(2), departed = null)), + listOf( + AttendanceTimes( + arrived = now.minusHours(2), + departed = null, + modifiedAt = now, + modifiedBy = employee.toEvakaUser(), + ) + ), ), attendances, ) @@ -111,24 +154,32 @@ class AttendanceQueriesTest : PureJdbiTest(resetDbBeforeEach = true) { childId = testChild_1.id, arrived = now.minusHours(3), departed = now.minusHours(2), + modifiedAt = now, + modifiedBy = employee.evakaUserId, ) tx.insertTestChildAttendance( unitId = testDaycare.id, childId = testChild_1.id, arrived = now.minusHours(1), departed = now, + modifiedAt = now, + modifiedBy = employee.evakaUserId, ) tx.insertTestChildAttendance( unitId = testDaycare.id, childId = testChild_2.id, arrived = now.minusHours(4), departed = now.minusHours(3), + modifiedAt = now, + modifiedBy = employee.evakaUserId, ) tx.insertTestChildAttendance( unitId = testDaycare.id, childId = testChild_2.id, arrived = now.minusHours(2), departed = null, + modifiedAt = now, + modifiedBy = employee.evakaUserId, ) } val attendances = db.read { it.getUnitChildAttendances(testDaycare.id, now) } @@ -137,13 +188,33 @@ class AttendanceQueriesTest : PureJdbiTest(resetDbBeforeEach = true) { // Newest attendance is first testChild_1.id to listOf( - AttendanceTimes(arrived = now.minusHours(1), departed = now), - AttendanceTimes(arrived = now.minusHours(3), departed = now.minusHours(2)), + AttendanceTimes( + arrived = now.minusHours(1), + departed = now, + modifiedAt = now, + modifiedBy = employee.toEvakaUser(), + ), + AttendanceTimes( + arrived = now.minusHours(3), + departed = now.minusHours(2), + modifiedAt = now, + modifiedBy = employee.toEvakaUser(), + ), ), testChild_2.id to listOf( - AttendanceTimes(arrived = now.minusHours(2), departed = null), - AttendanceTimes(arrived = now.minusHours(4), departed = now.minusHours(3)), + AttendanceTimes( + arrived = now.minusHours(2), + departed = null, + modifiedAt = now, + modifiedBy = employee.toEvakaUser(), + ), + AttendanceTimes( + arrived = now.minusHours(4), + departed = now.minusHours(3), + modifiedAt = now, + modifiedBy = employee.toEvakaUser(), + ), ), ), attendances, @@ -158,19 +229,30 @@ class AttendanceQueriesTest : PureJdbiTest(resetDbBeforeEach = true) { childId = testChild_1.id, arrived = now.minusDays(1), departed = now.minusDays(1).atEndOfDay(), + modifiedAt = now, + modifiedBy = employee.evakaUserId, ) tx.insertTestChildAttendance( unitId = testDaycare.id, childId = testChild_1.id, arrived = now.atStartOfDay(), departed = null, + modifiedAt = now, + modifiedBy = employee.evakaUserId, ) } val attendances = db.read { it.getUnitChildAttendances(testDaycare.id, now) } assertEquals( mapOf( testChild_1.id to - listOf(AttendanceTimes(arrived = now.minusDays(1), departed = null)) + listOf( + AttendanceTimes( + arrived = now.minusDays(1), + departed = null, + modifiedAt = now, + modifiedBy = employee.toEvakaUser(), + ) + ) ), attendances, ) @@ -184,12 +266,16 @@ class AttendanceQueriesTest : PureJdbiTest(resetDbBeforeEach = true) { childId = testChild_1.id, arrived = now.minusDays(1), departed = now.minusDays(1).atEndOfDay(), + modifiedAt = now, + modifiedBy = employee.evakaUserId, ) tx.insertTestChildAttendance( unitId = testDaycare.id, childId = testChild_1.id, arrived = now.atStartOfDay(), departed = now.minusMinutes(45), + modifiedAt = now, + modifiedBy = employee.evakaUserId, ) } val attendances = db.read { it.getUnitChildAttendances(testDaycare.id, now) } @@ -197,7 +283,12 @@ class AttendanceQueriesTest : PureJdbiTest(resetDbBeforeEach = true) { mapOf( testChild_1.id to listOf( - AttendanceTimes(arrived = now.minusDays(1), departed = now.minusMinutes(45)) + AttendanceTimes( + arrived = now.minusDays(1), + departed = now.minusMinutes(45), + modifiedAt = now, + modifiedBy = employee.toEvakaUser(), + ) ) ), attendances, diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/attendance/GetAttendancesIntegrationTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/attendance/GetAttendancesIntegrationTest.kt index 5e31bba7501..afef25ba9fd 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/attendance/GetAttendancesIntegrationTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/attendance/GetAttendancesIntegrationTest.kt @@ -13,6 +13,7 @@ import fi.espoo.evaka.placement.PlacementType import fi.espoo.evaka.reservations.ReservationResponse import fi.espoo.evaka.shared.ChildId import fi.espoo.evaka.shared.DaycareId +import fi.espoo.evaka.shared.EmployeeId import fi.espoo.evaka.shared.GroupId import fi.espoo.evaka.shared.MobileDeviceId import fi.espoo.evaka.shared.PlacementId @@ -21,6 +22,8 @@ import fi.espoo.evaka.shared.dev.DevAbsence import fi.espoo.evaka.shared.dev.DevBackupCare import fi.espoo.evaka.shared.dev.DevDaycareGroup import fi.espoo.evaka.shared.dev.DevDaycareGroupPlacement +import fi.espoo.evaka.shared.dev.DevEmployee +import fi.espoo.evaka.shared.dev.DevMobileDevice import fi.espoo.evaka.shared.dev.DevPersonType import fi.espoo.evaka.shared.dev.DevPlacement import fi.espoo.evaka.shared.dev.DevReservation @@ -36,6 +39,7 @@ import fi.espoo.evaka.testArea import fi.espoo.evaka.testChild_1 import fi.espoo.evaka.testDaycare import fi.espoo.evaka.testDaycare2 +import fi.espoo.evaka.toEvakaUser import java.time.LocalDate import java.time.LocalTime import java.util.UUID @@ -51,8 +55,10 @@ import org.springframework.beans.factory.annotation.Autowired class GetAttendancesIntegrationTest : FullApplicationTest(resetDbBeforeEach = true) { @Autowired private lateinit var childAttendanceController: ChildAttendanceController + // TODO Re-write this a bit to use DevMobileDevice instead of what it actually does. + private val mobileUser = AuthenticatedUser.MobileDevice(MobileDeviceId(UUID.randomUUID())) - private val mobileUser2 = AuthenticatedUser.MobileDevice(MobileDeviceId(UUID.randomUUID())) + private val mobileUser2 = DevMobileDevice(unitId = testDaycare2.id, name = "Laite ") private val groupId = GroupId(UUID.randomUUID()) private val groupId2 = GroupId(UUID.randomUUID()) private val groupName = "Testaajat" @@ -61,6 +67,10 @@ class GetAttendancesIntegrationTest : FullApplicationTest(resetDbBeforeEach = tr private val placementStart = now.toLocalDate().minusDays(30) private val placementEnd = now.toLocalDate().plusDays(30) + private val user2 = mobileUser2.toEvakaUser() + private val employee2 = + DevEmployee(id = EmployeeId(user2.id.raw), lastName = "Laite", firstName = "") + @BeforeEach fun beforeEach() { db.transaction { tx -> @@ -89,7 +99,8 @@ class GetAttendancesIntegrationTest : FullApplicationTest(resetDbBeforeEach = tr ) ) tx.createMobileDeviceToUnit(mobileUser.id, testDaycare.id) - tx.createMobileDeviceToUnit(mobileUser2.id, testDaycare2.id) + tx.insert(employee2) + tx.insert(mobileUser2) } } @@ -361,7 +372,7 @@ class GetAttendancesIntegrationTest : FullApplicationTest(resetDbBeforeEach = tr departed = null, ) } - val childInBackup = expectOneChildAttendance(backupUnitId, mobileUser2) + val childInBackup = expectOneChildAttendance(backupUnitId, mobileUser2.user) assertEquals(AttendanceStatus.PRESENT, childInBackup.status) assertNotNull(childInBackup.attendances) assertNull(childInBackup.attendances[0].departed) @@ -393,7 +404,7 @@ class GetAttendancesIntegrationTest : FullApplicationTest(resetDbBeforeEach = tr departed = null, ) } - val childInBackup = expectOneChildAttendance(backupUnitId, mobileUser2) + val childInBackup = expectOneChildAttendance(backupUnitId, mobileUser2.user) assertEquals(AttendanceStatus.PRESENT, childInBackup.status) assertNotNull(childInBackup.attendances) assertNull(childInBackup.attendances[0].departed) @@ -413,13 +424,21 @@ class GetAttendancesIntegrationTest : FullApplicationTest(resetDbBeforeEach = tr date = now.toLocalDate(), startTime = reservationStart, endTime = reservationEnd, - createdBy = mobileUser2.evakaUserId, + createdAt = now, + createdBy = mobileUser2.toEvakaUser().id, ) ) } val child = expectOneChild() assertEquals( - listOf(ReservationResponse.Times(TimeRange(reservationStart, reservationEnd), true)), + listOf( + ReservationResponse.Times( + TimeRange(reservationStart, reservationEnd), + true, + now, + mobileUser2.toEvakaUser(), + ) + ), child.reservations, ) } @@ -433,12 +452,16 @@ class GetAttendancesIntegrationTest : FullApplicationTest(resetDbBeforeEach = tr date = now.toLocalDate(), startTime = null, endTime = null, - createdBy = mobileUser2.evakaUserId, + createdAt = now, + createdBy = mobileUser2.toEvakaUser().id, ) ) } val child = expectOneChild() - assertEquals(listOf(ReservationResponse.NoTimes(true)), child.reservations) + assertEquals( + listOf(ReservationResponse.NoTimes(true, now, mobileUser2.toEvakaUser())), + child.reservations, + ) } @Test @@ -461,13 +484,21 @@ class GetAttendancesIntegrationTest : FullApplicationTest(resetDbBeforeEach = tr date = now.toLocalDate(), startTime = reservationStart, endTime = reservationEnd, - createdBy = mobileUser2.evakaUserId, + createdAt = now, + createdBy = mobileUser2.toEvakaUser().id, ) ) } - val childInBackup = expectOneChild(backupUnitId, mobileUser2) + val childInBackup = expectOneChild(backupUnitId, mobileUser2.user) assertEquals( - listOf(ReservationResponse.Times(TimeRange(reservationStart, reservationEnd), true)), + listOf( + ReservationResponse.Times( + TimeRange(reservationStart, reservationEnd), + true, + now, + mobileUser2.toEvakaUser(), + ) + ), childInBackup.reservations, ) } diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/attendance/MobileUnitControllerIntegrationTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/attendance/MobileUnitControllerIntegrationTest.kt index 68fb0b15321..f8892a9be9a 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/attendance/MobileUnitControllerIntegrationTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/attendance/MobileUnitControllerIntegrationTest.kt @@ -141,36 +141,48 @@ class MobileUnitControllerIntegrationTest : FullApplicationTest(resetDbBeforeEac testDaycare.id, today, TimeInterval(LocalTime.of(8, 30), null), + now, + mobileUser.evakaUserId, ) tx.insertAttendance( testChild_2.id, testDaycare.id, today, TimeInterval(LocalTime.of(9, 0), null), + now, + mobileUser.evakaUserId, ) tx.insertAttendance( testChild_3.id, testDaycare.id, today, TimeInterval(LocalTime.of(9, 30), null), + now, + mobileUser.evakaUserId, ) tx.insertAttendance( testChild_4.id, testDaycare.id, today, TimeInterval(LocalTime.of(10, 0), null), + now, + mobileUser.evakaUserId, ) tx.insertAttendance( testChild_5.id, testDaycare.id, today, TimeInterval(LocalTime.of(10, 15), null), + now, + mobileUser.evakaUserId, ) tx.insertAttendance( testChild_6.id, testDaycare.id, today, TimeInterval(LocalTime.of(10, 30), null), + now, + mobileUser.evakaUserId, ) val employee1 = DevEmployee(firstName = "One", lastName = "in group 1") @@ -234,6 +246,8 @@ class MobileUnitControllerIntegrationTest : FullApplicationTest(resetDbBeforeEac testDaycare.id, today, TimeInterval(LocalTime.of(6, 0), null), + now, + mobileUser.evakaUserId, ) } val unitInfo = fetchUnitInfo(testDaycare.id) diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/reservations/AttendanceReservationsControllerIntegrationTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/reservations/AttendanceReservationsControllerIntegrationTest.kt index 5b99311b7e2..1797a897bd0 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/reservations/AttendanceReservationsControllerIntegrationTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/reservations/AttendanceReservationsControllerIntegrationTest.kt @@ -68,6 +68,7 @@ import fi.espoo.evaka.testChild_5 import fi.espoo.evaka.testChild_6 import fi.espoo.evaka.testDaycare import fi.espoo.evaka.testDaycare2 +import fi.espoo.evaka.toEvakaUser import fi.espoo.evaka.withHolidays import java.math.BigDecimal import java.time.LocalDate @@ -105,6 +106,9 @@ class AttendanceReservationsControllerIntegrationTest : private val fullDay = TimeRange(LocalTime.parse("00:00"), LocalTime.parse("23:59")) + private val employee = DevEmployee(employeeId) + private val employee2 = DevEmployee(employeeId2) + @BeforeEach fun beforeEach() { db.transaction { tx -> @@ -120,9 +124,9 @@ class AttendanceReservationsControllerIntegrationTest : tx.insert(testGroup2) tx.insert(testGroupInDaycare2) - tx.insert(DevEmployee(employeeId)) + tx.insert(employee) tx.insertDaycareAclRow(testDaycare.id, employeeId, UserRole.STAFF) - tx.insert(DevEmployee(employeeId2)) + tx.insert(employee2) tx.insertDaycareAclRow(testDaycare.id, employeeId2, UserRole.STAFF) } } @@ -181,6 +185,7 @@ class AttendanceReservationsControllerIntegrationTest : date = mon, startTime = LocalTime.of(8, 0), endTime = LocalTime.of(16, 0), + createdAt = now, createdBy = EvakaUserId(employeeId.raw), ) ) @@ -189,12 +194,15 @@ class AttendanceReservationsControllerIntegrationTest : unitId = testDaycare.id, arrived = HelsinkiDateTime.of(mon, LocalTime.of(8, 15)), departed = HelsinkiDateTime.of(mon, LocalTime.of(16, 5)), + modifiedAt = now, + modifiedBy = EvakaUserId(employeeId.raw), ) it.insert( DevAbsence( childId = testChild_1.id, date = tue, absenceType = AbsenceType.OTHER_ABSENCE, + modifiedAt = now, modifiedBy = EvakaUserId(employeeId.raw), absenceCategory = AbsenceCategory.BILLABLE, ) @@ -206,6 +214,7 @@ class AttendanceReservationsControllerIntegrationTest : date = wed, startTime = null, endTime = null, + createdAt = now, createdBy = EvakaUserId(employeeId.raw), ) ) @@ -310,6 +319,7 @@ class AttendanceReservationsControllerIntegrationTest : date = thu, startTime = LocalTime.of(9, 0), endTime = LocalTime.of(15, 0), + createdAt = now, createdBy = EvakaUserId(employeeId.raw), ) ) @@ -320,6 +330,7 @@ class AttendanceReservationsControllerIntegrationTest : date = fri, startTime = LocalTime.of(7, 0), endTime = LocalTime.of(17, 0), + createdAt = now, createdBy = EvakaUserId(employeeId.raw), ) ) @@ -420,13 +431,22 @@ class AttendanceReservationsControllerIntegrationTest : ReservationResponse.Times( TimeRange(LocalTime.of(8, 0), LocalTime.of(16, 0)), true, + now, + employee.toEvakaUser(), ) ), attendances = listOf( - TimeInterval( - start = LocalTime.of(8, 15), - end = LocalTime.of(16, 5), + AttendanceTimesForDate( + date = mon, // TODO not sure + interval = + TimeInterval( + start = LocalTime.of(8, 15), + end = LocalTime.of(16, 5), + ), + modifiedAt = now, + modifiedBy = employee.toEvakaUser(), + staffModified = true, ) ), absenceBillable = null, @@ -475,7 +495,8 @@ class AttendanceReservationsControllerIntegrationTest : assertEquals( UnitAttendanceReservations.ChildRecordOfDay( childId = testChild_1.id, - reservations = listOf(ReservationResponse.NoTimes(true)), + reservations = + listOf(ReservationResponse.NoTimes(true, now, employee.toEvakaUser())), attendances = emptyList(), absenceBillable = null, absenceNonbillable = null, @@ -567,6 +588,8 @@ class AttendanceReservationsControllerIntegrationTest : ReservationResponse.Times( TimeRange(LocalTime.of(9, 0), LocalTime.of(15, 0)), true, + now, + employee.toEvakaUser(), ) ), attendances = emptyList(), @@ -660,6 +683,7 @@ class AttendanceReservationsControllerIntegrationTest : date = mon, startTime = LocalTime.of(19, 0), endTime = LocalTime.of(23, 59), + createdAt = now, createdBy = EvakaUserId(employeeId.raw), ), DevReservation( @@ -667,6 +691,7 @@ class AttendanceReservationsControllerIntegrationTest : date = tue, startTime = LocalTime.of(0, 0), endTime = LocalTime.of(8, 0), + createdAt = now, createdBy = EvakaUserId(employeeId.raw), ), DevReservation( @@ -674,6 +699,7 @@ class AttendanceReservationsControllerIntegrationTest : date = tue, startTime = LocalTime.of(17, 30), endTime = LocalTime.of(23, 59), + createdAt = now, createdBy = EvakaUserId(employeeId.raw), ), DevReservation( @@ -681,6 +707,7 @@ class AttendanceReservationsControllerIntegrationTest : date = wed, startTime = LocalTime.of(0, 0), endTime = LocalTime.of(9, 30), + createdAt = now, createdBy = EvakaUserId(employeeId.raw), ), ) @@ -703,6 +730,8 @@ class AttendanceReservationsControllerIntegrationTest : unitId = testDaycare.id, arrived = it.first, departed = it.second, + modifiedAt = now, + modifiedBy = EvakaUserId(employeeId.raw), ) } } @@ -718,9 +747,20 @@ class AttendanceReservationsControllerIntegrationTest : ReservationResponse.Times( TimeRange(LocalTime.of(19, 0), LocalTime.of(23, 59)), true, + now, + employee.toEvakaUser(), + ) + ), + attendances = + listOf( + AttendanceTimesForDate( + mon, + TimeInterval(LocalTime.of(19, 10), LocalTime.of(23, 59)), + now, + employee.toEvakaUser(), + true, ) ), - attendances = listOf(TimeInterval(LocalTime.of(19, 10), LocalTime.of(23, 59))), absenceBillable = null, absenceNonbillable = null, possibleAbsenceCategories = setOf(AbsenceCategory.BILLABLE), @@ -743,16 +783,32 @@ class AttendanceReservationsControllerIntegrationTest : ReservationResponse.Times( TimeRange(LocalTime.of(0, 0), LocalTime.of(8, 0)), true, + now, + employee.toEvakaUser(), ), ReservationResponse.Times( TimeRange(LocalTime.of(17, 30), LocalTime.of(23, 59)), true, + now, + employee.toEvakaUser(), ), ), attendances = listOf( - TimeInterval(LocalTime.of(0, 0), LocalTime.of(10, 30)), - TimeInterval(LocalTime.of(17, 0), null), + AttendanceTimesForDate( + tue, + TimeInterval(LocalTime.of(0, 0), LocalTime.of(10, 30)), + now, + employee.toEvakaUser(), + true, + ), + AttendanceTimesForDate( + tue, + TimeInterval(LocalTime.of(17, 0), null), + now, + employee.toEvakaUser(), + true, + ), ), absenceBillable = null, absenceNonbillable = null, @@ -776,6 +832,8 @@ class AttendanceReservationsControllerIntegrationTest : ReservationResponse.Times( TimeRange(LocalTime.of(0, 0), LocalTime.of(9, 30)), true, + now, + employee.toEvakaUser(), ) ), attendances = emptyList(), @@ -872,6 +930,7 @@ class AttendanceReservationsControllerIntegrationTest : startTime = LocalTime.of(9, 0), endTime = LocalTime.of(11, 0), createdBy = EvakaUserId(employeeId.raw), + createdAt = clock.now(), ) ) tx.insert( @@ -900,6 +959,8 @@ class AttendanceReservationsControllerIntegrationTest : ReservationResponse.Times( TimeRange(LocalTime.of(9, 0), LocalTime.of(11, 0)), true, + modifiedBy = employee.toEvakaUser(), + modifiedAt = clock.now(), ) ), absenceType = null, @@ -975,13 +1036,26 @@ class AttendanceReservationsControllerIntegrationTest : ReservationResponse.Times( TimeRange(LocalTime.of(9, 0), LocalTime.of(17, 0)), true, + testNow, + employee.toEvakaUser(), ), ReservationResponse.Times( TimeRange(LocalTime.of(22, 0), LocalTime.of(23, 59)), true, + testNow, + employee.toEvakaUser(), ), ), - attendances = listOf(TimeInterval(start = LocalTime.of(12, 30), end = null)), + attendances = + listOf( + AttendanceTimesForDate( + wed, + TimeInterval(start = LocalTime.of(12, 30), end = null), + testNow, + employee.toEvakaUser(), + true, + ) + ), absenceBillable = AbsenceTypeResponse(AbsenceType.OTHER_ABSENCE, true), absenceNonbillable = AbsenceTypeResponse(AbsenceType.OTHER_ABSENCE, true), possibleAbsenceCategories = @@ -1025,14 +1099,26 @@ class AttendanceReservationsControllerIntegrationTest : ReservationResponse.Times( TimeRange(LocalTime.of(9, 0), LocalTime.of(17, 0)), true, + testNow, + employee2.toEvakaUser(), ), ReservationResponse.Times( TimeRange(LocalTime.of(21, 30), LocalTime.of(23, 59)), true, + testNow, + employee2.toEvakaUser(), ), ), attendances = - listOf(TimeInterval(start = LocalTime.of(12, 30), end = LocalTime.of(17, 0))), + listOf( + AttendanceTimesForDate( + wed, + TimeInterval(start = LocalTime.of(12, 30), end = LocalTime.of(17, 0)), + testNow, + employee2.toEvakaUser(), + true, + ) + ), absenceBillable = AbsenceTypeResponse(AbsenceType.FORCE_MAJEURE, true), absenceNonbillable = AbsenceTypeResponse(AbsenceType.OTHER_ABSENCE, true), possibleAbsenceCategories = @@ -1340,10 +1426,14 @@ class AttendanceReservationsControllerIntegrationTest : ReservationResponse.Times( TimeRange(LocalTime.of(8, 0), LocalTime.of(12, 0)), true, + now, + employee.toEvakaUser(), ), ReservationResponse.Times( TimeRange(LocalTime.of(13, 0), LocalTime.of(16, 0)), true, + now, + employee.toEvakaUser(), ), ), groupId = testGroup1.id, @@ -1649,6 +1739,7 @@ class AttendanceReservationsControllerIntegrationTest : date = mon, startTime = LocalTime.of(8, 0), endTime = LocalTime.of(12, 0), + createdAt = now, createdBy = EvakaUserId(employeeId.raw), ) ) @@ -1658,6 +1749,7 @@ class AttendanceReservationsControllerIntegrationTest : date = mon, startTime = LocalTime.of(13, 0), endTime = LocalTime.of(16, 0), + createdAt = now, createdBy = EvakaUserId(employeeId.raw), ) ) @@ -1666,6 +1758,7 @@ class AttendanceReservationsControllerIntegrationTest : childId = testChild_1.id, date = tue, absenceType = AbsenceType.OTHER_ABSENCE, + modifiedAt = now, modifiedBy = EvakaUserId(employeeId.raw), absenceCategory = AbsenceCategory.BILLABLE, ) @@ -1675,6 +1768,7 @@ class AttendanceReservationsControllerIntegrationTest : childId = testChild_1.id, date = tue, absenceType = AbsenceType.OTHER_ABSENCE, + modifiedAt = now, modifiedBy = EvakaUserId(employeeId.raw), absenceCategory = AbsenceCategory.NONBILLABLE, ) @@ -1684,6 +1778,7 @@ class AttendanceReservationsControllerIntegrationTest : childId = testChild_1.id, date = fri, absenceType = AbsenceType.OTHER_ABSENCE, + modifiedAt = now, modifiedBy = EvakaUserId(employeeId.raw), absenceCategory = AbsenceCategory.NONBILLABLE, ) @@ -1693,6 +1788,7 @@ class AttendanceReservationsControllerIntegrationTest : childId = testChild_2.id, date = tue, absenceType = AbsenceType.OTHER_ABSENCE, + modifiedAt = now, modifiedBy = EvakaUserId(employeeId.raw), absenceCategory = AbsenceCategory.BILLABLE, ) diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/reservations/ReservationControllerCitizenIntegrationTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/reservations/ReservationControllerCitizenIntegrationTest.kt index e2894aae5b9..51389ebb542 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/reservations/ReservationControllerCitizenIntegrationTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/reservations/ReservationControllerCitizenIntegrationTest.kt @@ -59,6 +59,8 @@ import fi.espoo.evaka.snDaycareContractDays10 import fi.espoo.evaka.snDaycareFullDay35 import fi.espoo.evaka.snDaycareHours120 import fi.espoo.evaka.snPreschoolDaycareContractDays13 +import fi.espoo.evaka.toEvakaUser +import fi.espoo.evaka.user.EvakaUserType import fi.espoo.evaka.withHolidays import java.time.LocalDate import java.time.LocalTime @@ -96,6 +98,9 @@ class ReservationControllerCitizenIntegrationTest : FullApplicationTest(resetDbB private val startTime = LocalTime.of(9, 0) private val endTime = LocalTime.of(17, 0) + private val now = HelsinkiDateTime.of(mockToday, LocalTime.of(13, 0)) + private val employee = DevEmployee() + @BeforeEach fun before() { reservationControllerCitizen = @@ -118,7 +123,6 @@ class ReservationControllerCitizenIntegrationTest : FullApplicationTest(resetDbB val area = DevCareArea() val daycare = DevDaycare(areaId = area.id, enabledPilotFeatures = setOf(PilotFeature.RESERVATIONS)) - val employee = DevEmployee() val adult = DevPerson() val child1 = DevPerson(dateOfBirth = LocalDate.of(2015, 1, 1)) @@ -553,6 +557,7 @@ class ReservationControllerCitizenIntegrationTest : FullApplicationTest(resetDbB date = monday, startTime = LocalTime.of(9, 0), endTime = LocalTime.of(16, 0), + createdAt = now, createdBy = adult.evakaUserId(), ), DevReservation( @@ -560,6 +565,7 @@ class ReservationControllerCitizenIntegrationTest : FullApplicationTest(resetDbB date = tuesday, startTime = LocalTime.of(9, 0), endTime = LocalTime.of(16, 0), + createdAt = now, createdBy = adult.evakaUserId(), ), // No reservation on Wednesday @@ -613,6 +619,8 @@ class ReservationControllerCitizenIntegrationTest : FullApplicationTest(resetDbB ReservationResponse.Times( TimeRange(LocalTime.of(9, 0), LocalTime.of(16, 0)), false, + now, + adult.toEvakaUser(EvakaUserType.CITIZEN), ) ), attendances = @@ -641,6 +649,8 @@ class ReservationControllerCitizenIntegrationTest : FullApplicationTest(resetDbB ReservationResponse.Times( TimeRange(LocalTime.of(9, 0), LocalTime.of(16, 0)), false, + now, + adult.toEvakaUser(EvakaUserType.CITIZEN), ) ), attendances = @@ -1118,6 +1128,7 @@ class ReservationControllerCitizenIntegrationTest : FullApplicationTest(resetDbB ), ) }, + now, ) val res = @@ -1159,14 +1170,24 @@ class ReservationControllerCitizenIntegrationTest : FullApplicationTest(resetDbB child1.id, reservations = listOf( - ReservationResponse.Times(TimeRange(startTime, endTime), false) + ReservationResponse.Times( + TimeRange(startTime, endTime), + false, + now, + adult.toEvakaUser(EvakaUserType.CITIZEN), + ) ), ), dayChild( child2.id, reservations = listOf( - ReservationResponse.Times(TimeRange(startTime, endTime), false) + ReservationResponse.Times( + TimeRange(startTime, endTime), + false, + now, + adult.toEvakaUser(EvakaUserType.CITIZEN), + ) ), ), ) @@ -1183,14 +1204,24 @@ class ReservationControllerCitizenIntegrationTest : FullApplicationTest(resetDbB child1.id, reservations = listOf( - ReservationResponse.Times(TimeRange(startTime, endTime), false) + ReservationResponse.Times( + TimeRange(startTime, endTime), + false, + now, + adult.toEvakaUser(EvakaUserType.CITIZEN), + ) ), ), dayChild( child2.id, reservations = listOf( - ReservationResponse.Times(TimeRange(startTime, endTime), false) + ReservationResponse.Times( + TimeRange(startTime, endTime), + false, + now, + adult.toEvakaUser(EvakaUserType.CITIZEN), + ) ), ), ) @@ -1210,7 +1241,7 @@ class ReservationControllerCitizenIntegrationTest : FullApplicationTest(resetDbB val area = DevCareArea() val daycare = DevDaycare(areaId = area.id, enabledPilotFeatures = setOf(PilotFeature.RESERVATIONS)) - val employee = DevEmployee() + // val employee = DevEmployee() val adult = DevPerson() val child = DevPerson() @@ -1218,7 +1249,7 @@ class ReservationControllerCitizenIntegrationTest : FullApplicationTest(resetDbB db.transaction { tx -> tx.insert(area) tx.insert(daycare) - tx.insert(employee) + // tx.insert(employee) tx.insert(adult, DevPersonType.ADULT) tx.insert(child, DevPersonType.CHILD) @@ -1245,6 +1276,7 @@ class ReservationControllerCitizenIntegrationTest : FullApplicationTest(resetDbB ), DailyReservationRequest.Absent(child.id, tuesday), ), + now, ) val res = @@ -1259,7 +1291,14 @@ class ReservationControllerCitizenIntegrationTest : FullApplicationTest(resetDbB dayChild( child.id, reservations = - listOf(ReservationResponse.Times(TimeRange(startTime, endTime), false)), + listOf( + ReservationResponse.Times( + TimeRange(startTime, endTime), + false, + now, + adult.toEvakaUser(EvakaUserType.CITIZEN), + ) + ), ) ), day.children, diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/shared/db/SchemaConventionsTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/shared/db/SchemaConventionsTest.kt index aa82affdf49..886e85ce8f6 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/shared/db/SchemaConventionsTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/shared/db/SchemaConventionsTest.kt @@ -43,7 +43,6 @@ class SchemaConventionsTest : PureJdbiTest(resetDbBeforeEach = false) { "assistance_need_preschool_decision_guardian", "assistance_need_voucher_coefficient", "attachment", - "attendance_reservation", "backup_care", "backup_curriculum_content", "backup_curriculum_document", @@ -51,7 +50,6 @@ class SchemaConventionsTest : PureJdbiTest(resetDbBeforeEach = false) { "backup_curriculum_template", "backup_messaging_blocklist", "care_area", - "child_attendance", "child_daily_note", "child_document", "child_images", @@ -136,7 +134,6 @@ class SchemaConventionsTest : PureJdbiTest(resetDbBeforeEach = false) { "assistance_need_preschool_decision", "assistance_need_voucher_coefficient", "attachment", - "attendance_reservation", "backup_care", "backup_curriculum_content", "backup_curriculum_document", @@ -144,7 +141,6 @@ class SchemaConventionsTest : PureJdbiTest(resetDbBeforeEach = false) { "backup_curriculum_template", "backup_messaging_blocklist", "care_area", - "child_attendance", "child_daily_note", "child_document", "child_images", diff --git a/service/src/main/kotlin/fi/espoo/evaka/absence/AbsenceQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/absence/AbsenceQueries.kt index 5244ad5f442..5526384cab8 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/absence/AbsenceQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/absence/AbsenceQueries.kt @@ -556,7 +556,7 @@ fun Database.Read.getGroupReservations( WITH all_placements AS ( ${subquery(placementsQuery(dateRange, groupId))} ) -SELECT r.child_id, r.date, r.start_time, r.end_time, e.type AS created_by_evaka_user_type, r.created AS created_date +SELECT r.child_id, r.date, r.start_time, r.end_time, e.type AS created_by_evaka_user_type, r.created_at AS created_date FROM attendance_reservation r JOIN evaka_user e ON r.created_by = e.id WHERE between_start_and_end(${bind(dateRange)}, r.date) 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 7f67b7b5800..53571195e55 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/attendance/ChildAttendance.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/attendance/ChildAttendance.kt @@ -17,8 +17,10 @@ import fi.espoo.evaka.shared.DaycareId import fi.espoo.evaka.shared.GroupId import fi.espoo.evaka.shared.domain.HelsinkiDateTime import fi.espoo.evaka.shared.domain.TimeInterval +import fi.espoo.evaka.user.EvakaUser import java.time.LocalDate import java.time.LocalTime +import org.jdbi.v3.core.mapper.Nested data class ContactInfo( val id: String, @@ -61,10 +63,17 @@ data class ChildAttendanceRow( val date: LocalDate, val startTime: LocalTime, val endTime: LocalTime?, + val modifiedAt: HelsinkiDateTime, + @Nested("modified_by") val modifiedBy: EvakaUser, ) { fun asTimeInterval(): TimeInterval = TimeInterval(startTime, endTime) } -data class AttendanceTimes(val arrived: HelsinkiDateTime, val departed: HelsinkiDateTime?) +data class AttendanceTimes( + val arrived: HelsinkiDateTime, + val departed: HelsinkiDateTime?, + val modifiedAt: HelsinkiDateTime, + @Nested("modified_by") val modifiedBy: EvakaUser, +) data class ChildAbsence(val category: AbsenceCategory, val type: AbsenceType) 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 89eef344612..0928bea0095 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/attendance/ChildAttendanceController.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/attendance/ChildAttendanceController.kt @@ -217,6 +217,8 @@ class ChildAttendanceController( unitId = unitId, date = today, range = TimeInterval(body.arrived, null), + now = clock.now(), + createdById = user.evakaUserId, ) } catch (e: Exception) { throw mapPSQLException(e) @@ -408,6 +410,8 @@ class ChildAttendanceController( unitId, date, TimeInterval(startTime, endTime), + clock.now(), + user.evakaUserId, ) } } diff --git a/service/src/main/kotlin/fi/espoo/evaka/attendance/ChildAttendanceQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/attendance/ChildAttendanceQueries.kt index 3711e437b98..ca1dde75546 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/attendance/ChildAttendanceQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/attendance/ChildAttendanceQueries.kt @@ -12,6 +12,7 @@ import fi.espoo.evaka.shared.AbsenceId import fi.espoo.evaka.shared.ChildAttendanceId import fi.espoo.evaka.shared.ChildId import fi.espoo.evaka.shared.DaycareId +import fi.espoo.evaka.shared.EvakaUserId import fi.espoo.evaka.shared.GroupId import fi.espoo.evaka.shared.PersonId import fi.espoo.evaka.shared.db.Database @@ -20,6 +21,7 @@ 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.TimeRange +import fi.espoo.evaka.user.EvakaUser import java.time.LocalDate import java.time.LocalTime import org.jdbi.v3.core.mapper.Nested @@ -29,12 +31,14 @@ fun Database.Transaction.insertAttendance( unitId: DaycareId, date: LocalDate, range: TimeInterval, + now: HelsinkiDateTime, + createdById: EvakaUserId, ): ChildAttendanceId { return createUpdate { sql( """ - INSERT INTO child_attendance (child_id, unit_id, date, start_time, end_time) - VALUES (${bind(childId)}, ${bind(unitId)}, ${bind(date)}, ${bind(range.start)}, ${bind(range.end)}) + INSERT INTO child_attendance (child_id, unit_id, date, start_time, end_time, modified_at, modified_by) + VALUES (${bind(childId)}, ${bind(unitId)}, ${bind(date)}, ${bind(range.start)}, ${bind(range.end)}, ${bind(now)}, ${bind(createdById)}) RETURNING id """ ) @@ -285,6 +289,8 @@ private data class UnitChildAttendancesRow( val childId: ChildId, val arrived: HelsinkiDateTime, var departed: HelsinkiDateTime?, + val modifiedAt: HelsinkiDateTime, + @Nested("modified_by") val modifiedBy: EvakaUser, ) fun Database.Read.getUnitChildAttendances( @@ -298,8 +304,9 @@ fun Database.Read.getUnitChildAttendances( return createQuery { sql( """ -SELECT child_id, ca.arrived, ca.departed +SELECT child_id, ca.arrived, ca.departed, ca.modified_at, e.id AS modified_by_id, e.name AS modified_by_name, e.type AS modified_by_type FROM child_attendance ca +JOIN evaka_user e ON ca.modified_by = e.id WHERE ca.unit_id = ${bind(unitId)} AND tstzrange(arrived, departed) && ${bind(range)} """ ) @@ -307,7 +314,9 @@ WHERE ca.unit_id = ${bind(unitId)} AND tstzrange(arrived, departed) && ${bind(ra .toList() .groupBy( keySelector = { it.childId }, - valueTransform = { AttendanceTimes(it.arrived, it.departed) }, + valueTransform = { + AttendanceTimes(it.arrived, it.departed, it.modifiedAt, it.modifiedBy) + }, ) .mapValues { mergeOverNightRanges(it.value) @@ -330,7 +339,23 @@ private fun mergeOverNightRanges(attendances: List): List attendance.modifiedAt) { + acc.dropLast(1) + + AttendanceTimes( + previous.arrived, + attendance.departed, + previous.modifiedAt, + previous.modifiedBy, + ) + } else { + acc.dropLast(1) + + AttendanceTimes( + previous.arrived, + attendance.departed, + attendance.modifiedAt, + attendance.modifiedBy, + ) + } } else { acc + attendance } @@ -457,8 +482,18 @@ fun Database.Read.getChildAttendances(where: Predicate): List ReservationInsert( @@ -545,6 +549,8 @@ class AttendanceReservationController( startTime = it.start, endTime = it.end, date = examinationDate, + createdAt = it.createdAt, + createdBy = it.createdBy, staffCreated = it.staffCreated, ) .toReservationTimes() @@ -719,7 +725,7 @@ data class UnitAttendanceReservations( data class ChildRecordOfDay( val childId: ChildId, val reservations: List, - val attendances: List, + val attendances: List, val absenceBillable: AbsenceTypeResponse?, val absenceNonbillable: AbsenceTypeResponse?, val possibleAbsenceCategories: Set, @@ -937,7 +943,7 @@ WHERE (p.unit_id = ${bind(unitId)} OR bc.unit_id = ${bind(unitId)}) AND daterang data class ChildData( val child: UnitAttendanceReservations.Child, val reservations: Map>, - val attendances: Map>, + val attendances: Map>, val absences: Map>, val operationalDays: Set, ) @@ -957,22 +963,31 @@ private data class ReservationTimesForDate( val date: LocalDate, val startTime: LocalTime?, val endTime: LocalTime?, + val createdAt: HelsinkiDateTime, + val createdBy: EvakaUser, // TODO should these be modified_by instead? val staffCreated: Boolean, ) { fun toReservationTimes() = when { - startTime == null || endTime == null -> ReservationResponse.NoTimes(staffCreated) - else -> ReservationResponse.Times(TimeRange(startTime, endTime), staffCreated) + startTime == null || endTime == null -> + ReservationResponse.NoTimes(staffCreated, createdAt, createdBy) + else -> + ReservationResponse.Times( + TimeRange(startTime, endTime), + staffCreated, + createdAt, + createdBy, + ) } } -private data class AttendanceTimesForDate( +data class AttendanceTimesForDate( val date: LocalDate, - val startTime: LocalTime, - val endTime: LocalTime?, -) { - fun toTimeInterval() = TimeInterval(startTime, endTime) -} + val interval: TimeInterval, + val modifiedAt: HelsinkiDateTime, + val modifiedBy: EvakaUser, + val staffModified: Boolean, +) private data class AbsenceForDate( val date: LocalDate, @@ -1002,6 +1017,12 @@ SELECT 'date', ar.date, 'startTime', ar.start_time, 'endTime', ar.end_time, + 'createdAt', ar.created_at, + 'createdBy', jsonb_build_object( + 'id', eu.id, + 'name', eu.name, + 'type', eu.type + ), 'staffCreated', eu.type <> 'CITIZEN' ) ORDER BY ar.date, ar.start_time) FROM attendance_reservation ar @@ -1011,10 +1032,21 @@ SELECT coalesce(( SELECT jsonb_agg(jsonb_build_object( 'date', att.date, - 'startTime', att.start_time, - 'endTime', att.end_time + 'interval', jsonb_build_object( + 'start', att.start_time, + 'end', att.end_time + ), + 'modifiedAt', att.modified_at, + 'modifiedBy', jsonb_build_object( + 'id', eu.id, + 'name', eu.name, + 'type', eu.type + ), + 'staffModified', eu.type <> 'CITIZEN' ) ORDER BY att.date, att.start_time) - FROM child_attendance att WHERE att.child_id = p.id AND between_start_and_end(${bind(dateRange)}, att.date) + FROM child_attendance att + JOIN evaka_user eu ON att.modified_by = eu.id + WHERE att.child_id = p.id AND between_start_and_end(${bind(dateRange)}, att.date) ), '[]'::jsonb) AS attendances, coalesce(( SELECT jsonb_agg(jsonb_build_object( @@ -1054,11 +1086,7 @@ WHERE p.id = ANY(${bind(childIds)}) keySelector = { it.date }, valueTransform = { it.toReservationTimes() }, ), - attendances = - row.attendances.groupBy( - keySelector = { it.date }, - valueTransform = { it.toTimeInterval() }, - ), + attendances = row.attendances.groupBy(keySelector = { it.date }), absences = row.absences .groupBy( diff --git a/service/src/main/kotlin/fi/espoo/evaka/reservations/ReservationQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/reservations/ReservationQueries.kt index 37019e28908..ff223a62e82 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/reservations/ReservationQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/reservations/ReservationQueries.kt @@ -29,6 +29,7 @@ import fi.espoo.evaka.shared.domain.HelsinkiDateTime import fi.espoo.evaka.shared.domain.TimeRange import fi.espoo.evaka.shared.domain.getHolidays import fi.espoo.evaka.shared.domain.isHoliday +import fi.espoo.evaka.user.EvakaUser import java.math.BigDecimal import java.time.LocalDate import java.time.LocalTime @@ -121,14 +122,15 @@ data class ReservationInsert(val childId: ChildId, val date: LocalDate, val rang fun Database.Transaction.insertValidReservations( userId: EvakaUserId, + createdAt: HelsinkiDateTime, reservations: List, ): List { return reservations.mapNotNull { createQuery { sql( """ -INSERT INTO attendance_reservation (child_id, date, start_time, end_time, created_by) -SELECT ${bind(it.childId)}, ${bind(it.date)}, ${bind(it.range?.start)}, ${bind(it.range?.end)}, ${bind(userId)} +INSERT INTO attendance_reservation (child_id, date, start_time, end_time, created_at, created_by) +SELECT ${bind(it.childId)}, ${bind(it.date)}, ${bind(it.range?.start)}, ${bind(it.range?.end)}, ${bind(createdAt)}, ${bind(userId)} FROM realized_placement_all(${bind(it.date)}) rp JOIN daycare d ON d.id = rp.unit_id AND 'RESERVATIONS' = ANY(d.enabled_pilot_features) LEFT JOIN service_need sn ON sn.placement_id = rp.placement_id AND daterange(sn.start_date, sn.end_date, '[]') @> ${bind(it.date)} @@ -167,7 +169,11 @@ SELECT ar.child_id, ar.start_time, ar.end_time, - eu.type <> 'CITIZEN' as staff_created + eu.type <> 'CITIZEN' as staff_created, + ar.created_at, + eu.id AS created_by_id, + eu.name AS created_by_name, + eu.type AS created_by_type FROM attendance_reservation ar JOIN evaka_user eu ON ar.created_by = eu.id WHERE ${predicate(where.forTable("ar"))} @@ -180,6 +186,12 @@ WHERE ${predicate(where.forTable("ar"))} column("child_id"), Reservation.of(column("start_time"), column("end_time")), column("staff_created"), + column("created_at"), + EvakaUser( + column("created_by_id"), + column("created_by_name"), + column("created_by_type"), + ), ) } @@ -494,6 +506,8 @@ data class ConfirmedDayAbsenceInfo(val category: AbsenceCategory) data class ConfirmedDayReservationInfo( val start: LocalTime?, val end: LocalTime?, + val createdAt: HelsinkiDateTime, + val createdBy: EvakaUser, val staffCreated: Boolean, ) @@ -519,8 +533,20 @@ SELECT pcd.child_id, (SELECT coalesce(jsonb_agg(jsonb_build_object( 'start', s.start_time, 'end', s.end_time, + 'createdAt', s.created_at, + 'createdBy', jsonb_build_object( + 'id', s.created_by_id, + 'name', s.created_by_name, + 'type', s.created_by_type + ), 'staffCreated', s.staff_created)), '[]'::jsonb) - FROM (select ar.start_time, ar.end_time, eu.type <> 'CITIZEN' as staff_created + FROM (select ar.start_time, + ar.end_time, + ar.created_at, + eu.id AS created_by_id, + eu.name AS created_by_name, + eu.type AS created_by_type, + eu.type <> 'CITIZEN' AS staff_created FROM attendance_reservation ar JOIN evaka_user eu ON ar.created_by = eu.id WHERE ar.child_id = pcd.child_id diff --git a/service/src/main/kotlin/fi/espoo/evaka/reservations/Reservations.kt b/service/src/main/kotlin/fi/espoo/evaka/reservations/Reservations.kt index e4f4dd466a3..3f0bf0a2704 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/reservations/Reservations.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/reservations/Reservations.kt @@ -41,6 +41,7 @@ import fi.espoo.evaka.shared.domain.HelsinkiDateTime import fi.espoo.evaka.shared.domain.TimeInterval import fi.espoo.evaka.shared.domain.TimeRange import fi.espoo.evaka.shared.utils.mapOfNotNullValues +import fi.espoo.evaka.user.EvakaUser import java.time.LocalDate import java.time.LocalTime import kotlin.math.roundToLong @@ -115,10 +116,20 @@ data class AbsenceTypeResponse(val absenceType: AbsenceType, val staffCreated: B @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") sealed class ReservationResponse : Comparable { - @JsonTypeName("NO_TIMES") data class NoTimes(val staffCreated: Boolean) : ReservationResponse() + @JsonTypeName("NO_TIMES") + data class NoTimes( + val staffCreated: Boolean, + val modifiedAt: HelsinkiDateTime, + val modifiedBy: EvakaUser, + ) : ReservationResponse() @JsonTypeName("TIMES") - data class Times(val range: TimeRange, val staffCreated: Boolean) : ReservationResponse() + data class Times( + val range: TimeRange, + val staffCreated: Boolean, + val modifiedAt: HelsinkiDateTime, + val modifiedBy: EvakaUser, + ) : ReservationResponse() override fun compareTo(other: ReservationResponse): Int { return when { @@ -140,9 +151,19 @@ sealed class ReservationResponse : Comparable { companion object { fun from(reservationRow: ReservationRow) = when (reservationRow.reservation) { - is Reservation.NoTimes -> NoTimes(reservationRow.staffCreated) + is Reservation.NoTimes -> + NoTimes( + reservationRow.staffCreated, + reservationRow.modifiedAt, + reservationRow.modifiedBy, + ) is Reservation.Times -> - Times(reservationRow.reservation.range, reservationRow.staffCreated) + Times( + reservationRow.reservation.range, + reservationRow.staffCreated, + reservationRow.modifiedAt, + reservationRow.modifiedBy, + ) } } } @@ -152,6 +173,8 @@ data class ReservationRow( val childId: ChildId, val reservation: Reservation, val staffCreated: Boolean, + val modifiedAt: HelsinkiDateTime, + val modifiedBy: EvakaUser, ) data class CreateReservationsResult( @@ -319,6 +342,7 @@ fun createReservationsAndAbsences( val upsertedReservations = tx.insertValidReservations( user.evakaUserId, + now, validated.filterIsInstance().flatMap { res -> listOfNotNull(res.reservation, res.secondReservation).map { ReservationInsert(res.childId, res.date, it) @@ -454,7 +478,7 @@ fun upsertChildDatePresence( val insertedReservations = reservations .filter { it.second == null } - .map { tx.insertReservation(userId, input.date, input.childId, it.first) } + .map { tx.insertReservation(userId, now, input.date, input.childId, it.first) } val deletedAttendances = tx.deleteAttendancesByDate(childId = input.childId, date = input.date) val insertedAttendances = @@ -464,6 +488,8 @@ fun upsertChildDatePresence( input.unitId, input.date, TimeInterval(attendance.start, attendance.end), + now, + userId, ) } @@ -545,6 +571,7 @@ private fun Database.Transaction.deleteReservations( private fun Database.Transaction.insertReservation( userId: EvakaUserId, + now: HelsinkiDateTime, date: LocalDate, childId: ChildId, reservation: Reservation, @@ -552,8 +579,8 @@ private fun Database.Transaction.insertReservation( return createQuery { sql( """ - INSERT INTO attendance_reservation (child_id, created_by, date, start_time, end_time) - VALUES (${bind(childId)}, ${bind(userId)}, ${bind(date)}, ${bind(reservation.asTimeRange()?.start)}, ${bind(reservation.asTimeRange()?.end)}) + INSERT INTO attendance_reservation (child_id, created_at, created_by, date, start_time, end_time) + VALUES (${bind(childId)}, ${bind(now)}, ${bind(userId)}, ${bind(date)}, ${bind(reservation.asTimeRange()?.start)}, ${bind(reservation.asTimeRange()?.end)}) RETURNING id """ ) diff --git a/service/src/main/kotlin/fi/espoo/evaka/shared/dev/DataInitializers.kt b/service/src/main/kotlin/fi/espoo/evaka/shared/dev/DataInitializers.kt index 4bc0127820d..296e1e60560 100755 --- a/service/src/main/kotlin/fi/espoo/evaka/shared/dev/DataInitializers.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/shared/dev/DataInitializers.kt @@ -880,6 +880,8 @@ fun Database.Transaction.insertTestChildAttendance( unitId: DaycareId, arrived: HelsinkiDateTime, departed: HelsinkiDateTime?, + modifiedAt: HelsinkiDateTime = HelsinkiDateTime.now(), + modifiedBy: EvakaUserId = AuthenticatedUser.SystemInternalUser.evakaUserId, ) { val attendances: List> = if (departed == null) { @@ -902,8 +904,8 @@ fun Database.Transaction.insertTestChildAttendance( executeBatch(attendances) { sql( """ -INSERT INTO child_attendance (child_id, unit_id, date, start_time, end_time) -VALUES (${bind(childId)}, ${bind(unitId)}, ${bind { (date, _, _) -> date }}, ${bind { (_, startTime, _) -> startTime.withSecond(0).withNano(0) }}, ${bind { (_, _, endTime) -> endTime?.withSecond(0)?.withNano(0) }}) +INSERT INTO child_attendance (child_id, unit_id, date, start_time, end_time, modified_at, modified_by) +VALUES (${bind(childId)}, ${bind(unitId)}, ${bind { (date, _, _) -> date }}, ${bind { (_, startTime, _) -> startTime.withSecond(0).withNano(0) }}, ${bind { (_, _, endTime) -> endTime?.withSecond(0)?.withNano(0) }}, ${bind(modifiedAt)}, ${bind(modifiedBy)}) """ ) } @@ -1186,7 +1188,7 @@ data class DevReservation( val date: LocalDate, val startTime: LocalTime?, val endTime: LocalTime?, - val created: HelsinkiDateTime = HelsinkiDateTime.now(), + val createdAt: HelsinkiDateTime = HelsinkiDateTime.now(), val createdBy: EvakaUserId, ) @@ -1194,8 +1196,8 @@ fun Database.Transaction.insert(row: DevReservation): AttendanceReservationId = createUpdate { sql( """ -INSERT INTO attendance_reservation (id, child_id, date, start_time, end_time, created, created_by) -VALUES (${bind(row.id)}, ${bind(row.childId)}, ${bind(row.date)}, ${bind(row.startTime)}, ${bind(row.endTime)}, ${bind(row.created)}, ${bind(row.createdBy)}) +INSERT INTO attendance_reservation (id, child_id, date, start_time, end_time, created_at, created_by) +VALUES (${bind(row.id)}, ${bind(row.childId)}, ${bind(row.date)}, ${bind(row.startTime)}, ${bind(row.endTime)}, ${bind(row.createdAt)}, ${bind(row.createdBy)}) RETURNING id """ ) @@ -1521,8 +1523,8 @@ fun Database.Transaction.insert(row: DevChildAttendance): ChildAttendanceId = createUpdate { sql( """ -INSERT INTO child_attendance (child_id, unit_id, date, start_time, end_time) -VALUES (${bind(row.childId)}, ${bind(row.unitId)}, ${bind(row.date)}, ${bind(row.arrived)}, ${bind(row.departed)}) +INSERT INTO child_attendance (child_id, unit_id, date, start_time, end_time, modified_at, modified_by) +VALUES (${bind(row.childId)}, ${bind(row.unitId)}, ${bind(row.date)}, ${bind(row.arrived)}, ${bind(row.departed)}, ${bind(row.modifiedAt)}, ${bind(row.modifiedBy)}) """ ) } diff --git a/service/src/main/kotlin/fi/espoo/evaka/shared/dev/DevApi.kt b/service/src/main/kotlin/fi/espoo/evaka/shared/dev/DevApi.kt index 3b014e14382..9e48b318e69 100755 --- a/service/src/main/kotlin/fi/espoo/evaka/shared/dev/DevApi.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/shared/dev/DevApi.kt @@ -996,7 +996,7 @@ UPDATE placement SET end_date = ${bind(req.endDate)}, termination_requested_date db.connect { dbc -> dbc.transaction { tx -> tx.ensureFakeAdminExists() - tx.insertValidReservations(fakeAdmin.evakaUserId, body) + tx.insertValidReservations(fakeAdmin.evakaUserId, clock.now(), body) } } } @@ -1932,6 +1932,8 @@ data class DevChildAttendance( val date: LocalDate, val arrived: LocalTime, val departed: LocalTime?, + val modifiedAt: HelsinkiDateTime = HelsinkiDateTime.now(), + val modifiedBy: EvakaUserId = AuthenticatedUser.SystemInternalUser.evakaUserId, ) data class DevAssistanceAction( @@ -2097,7 +2099,10 @@ data class DevMobileDevice( val name: String = "Laite", val longTermToken: UUID? = null, val pushNotificationCategories: Set = emptySet(), -) +) { + val user: AuthenticatedUser.MobileDevice + @JsonIgnore get() = AuthenticatedUser.MobileDevice(id) +} data class DevPersonalMobileDevice( val id: MobileDeviceId = MobileDeviceId(UUID.randomUUID()), diff --git a/service/src/main/resources/db/migration/V475__attendance_reservation_modification_metadata.sql b/service/src/main/resources/db/migration/V475__attendance_reservation_modification_metadata.sql new file mode 100644 index 00000000000..86f63355d07 --- /dev/null +++ b/service/src/main/resources/db/migration/V475__attendance_reservation_modification_metadata.sql @@ -0,0 +1,27 @@ +ALTER TABLE child_attendance + ADD COLUMN modified_at TIMESTAMP WITH TIME ZONE, + ADD COLUMN modified_by UUID REFERENCES evaka_user; + +UPDATE child_attendance SET + modified_at = COALESCE(created, updated), + modified_by = COALESCE(modified_by, '00000000-0000-0000-0000-000000000000'::UUID); + +ALTER TABLE child_attendance + ALTER COLUMN modified_at SET NOT NULL, + ALTER COLUMN modified_by SET NOT NULL; + +CREATE INDEX fk$child_attendance_modified_by ON child_attendance(modified_by); + + +DROP TRIGGER set_timestamp ON child_attendance; +ALTER TABLE child_attendance RENAME COLUMN updated TO updated_at; +CREATE TRIGGER set_timestamp BEFORE UPDATE ON child_attendance FOR EACH ROW EXECUTE PROCEDURE trigger_refresh_updated_at(); + +ALTER TABLE child_attendance RENAME COLUMN created TO created_at; + + +DROP TRIGGER set_timestamp ON attendance_reservation; +ALTER TABLE attendance_reservation RENAME COLUMN updated TO updated_at; +CREATE TRIGGER set_timestamp BEFORE UPDATE ON attendance_reservation FOR EACH ROW EXECUTE PROCEDURE trigger_refresh_updated_at(); + +ALTER TABLE attendance_reservation RENAME COLUMN created TO created_at; diff --git a/service/src/main/resources/migrations.txt b/service/src/main/resources/migrations.txt index ba86afa7e1c..dc401bfdb20 100644 --- a/service/src/main/resources/migrations.txt +++ b/service/src/main/resources/migrations.txt @@ -470,3 +470,4 @@ V471__application_confidentiality.sql V472__employee_ssn.sql V473__person_municipality_of_residence.sql V474__holiday_questionnaire_open_ranges.sql +V475__attendance_reservation_modification_metadata.sql diff --git a/service/src/test/kotlin/fi/espoo/evaka/reports/MealReportTests.kt b/service/src/test/kotlin/fi/espoo/evaka/reports/MealReportTests.kt index 1aff5c73070..1e86fec071f 100644 --- a/service/src/test/kotlin/fi/espoo/evaka/reports/MealReportTests.kt +++ b/service/src/test/kotlin/fi/espoo/evaka/reports/MealReportTests.kt @@ -16,11 +16,15 @@ import fi.espoo.evaka.reservations.ReservationResponse import fi.espoo.evaka.reservations.UnitAttendanceReservations import fi.espoo.evaka.serviceneed.ShiftCareType import fi.espoo.evaka.shared.ChildId +import fi.espoo.evaka.shared.EvakaUserId import fi.espoo.evaka.shared.PreschoolTermId import fi.espoo.evaka.shared.data.DateSet import fi.espoo.evaka.shared.domain.FiniteDateRange +import fi.espoo.evaka.shared.domain.MockEvakaClock import fi.espoo.evaka.shared.domain.TimeRange import fi.espoo.evaka.specialdiet.SpecialDiet +import fi.espoo.evaka.user.EvakaUser +import fi.espoo.evaka.user.EvakaUserType import java.time.LocalDate import java.time.LocalTime import java.util.* @@ -30,6 +34,8 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class MealReportTests { + val clock = MockEvakaClock(2024, 1, 3, 3, 7) + @Test fun `mealReportData should return no meals for absent child`() { val testDate = LocalDate.of(2023, 4, 15) @@ -736,6 +742,8 @@ class MealReportTests { val lunchTime = TimeRange(LocalTime.of(11, 0), LocalTime.of(11, 20)) val snackTime = TimeRange(LocalTime.of(14, 0), LocalTime.of(14, 20)) + val user = EvakaUser(EvakaUserId(UUID.randomUUID()), "Test Name", EvakaUserType.EMPLOYEE) + val childData = mapOf( childId1 to @@ -757,6 +765,8 @@ class MealReportTests { ReservationResponse.Times( TimeRange(LocalTime.of(8, 0), LocalTime.of(14, 20)), false, + clock.now(), + user, ) ) ), @@ -785,6 +795,8 @@ class MealReportTests { ReservationResponse.Times( TimeRange(LocalTime.of(10, 0), LocalTime.of(14, 20)), false, + clock.now(), + user, ) ) ),