Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attendance reservation metadata #6122

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions frontend/src/e2e-test/dev-api/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,8 @@ export class Fixture {
date: LocalDate.todayInHelsinkiTz(),
arrived: LocalTime.nowInHelsinkiTz(),
departed: LocalTime.nowInHelsinkiTz(),
modifiedAt: HelsinkiDateTime.now(),
modifiedBy: systemInternalUser.id,
...initial
})
}
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/e2e-test/generated/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,8 @@ export interface DevChildAttendance {
childId: PersonId
date: LocalDate
departed: LocalTime | null
modifiedAt: HelsinkiDateTime
modifiedBy: EvakaUserId
unitId: DaycareId
}

Expand Down Expand Up @@ -1271,7 +1273,8 @@ export function deserializeJsonDevChildAttendance(json: JsonOf<DevChildAttendanc
...json,
arrived: LocalTime.parseIso(json.arrived),
date: LocalDate.parseIso(json.date),
departed: (json.departed != null) ? LocalTime.parseIso(json.departed) : null
departed: (json.departed != null) ? LocalTime.parseIso(json.departed) : null,
modifiedAt: HelsinkiDateTime.parseIso(json.modifiedAt)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
// SPDX-License-Identifier: LGPL-2.1-or-later

import FiniteDateRange from 'lib-common/finite-date-range'
import HelsinkiDateTime from 'lib-common/helsinki-date-time'
import LocalDate from 'lib-common/local-date'
import LocalTime from 'lib-common/local-time'
import TimeRange from 'lib-common/time-range'

import {
systemInternalUser,
testCareArea,
testDaycare,
testDaycareGroup,
Expand Down Expand Up @@ -410,7 +412,9 @@ describe('Employee - Unit month calendar', () => {
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
}))
)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -111,50 +112,71 @@ export default React.memo(function ChildDayAttendance({

return (
<AttendanceDateCell>
{!inOtherUnit && !isInBackupGroup ? (
<TimesRow data-qa={`attendance-${date.formatIso()}-${attendanceIndex}`}>
{requiresBackupCare ? (
<TimeCell data-qa="backup-care-required-warning" warning>
{i18n.unit.attendanceReservations.requiresBackupCare}{' '}
<FontAwesomeIcon
icon={faExclamationTriangle}
color={colors.status.warning}
/>
</TimeCell>
) : (
<>
<AttendanceTime
data-qa="attendance-start"
warning={
attendance ? !isWithinExpectedTimes(attendance.start) : false
}
>
{attendance?.formatStart() ?? '-'}
</AttendanceTime>
<AttendanceTime
data-qa="attendance-end"
warning={
attendance?.end
? !isWithinExpectedTimes(attendance.end)
: false
}
>
{attendance?.end ? attendance.formatEnd() : '-'}
</AttendanceTime>
</>
)}
</TimesRow>
) : null}
{!inOtherUnit && !isInBackupGroup && scheduleType !== 'TERM_BREAK' && (
<DetailsToggle>
<IconOnlyButton
icon={faCircleEllipsis}
onClick={onStartEdit}
data-qa="open-details"
aria-label={i18n.common.open}
/>
</DetailsToggle>
)}
<Tooltip
tooltip={
attendance &&
(attendance?.staffModified
? i18n.unit.attendanceReservations.lastModifiedStaff(
attendance.modifiedAt.format(),
attendance.modifiedBy.name
)
: i18n.unit.attendanceReservations.lastModifiedOther(
attendance.modifiedAt.format(),
attendance.modifiedBy.name
))
}
>
{!inOtherUnit && !isInBackupGroup ? (
<TimesRow
data-qa={`attendance-${date.formatIso()}-${attendanceIndex}`}
>
{requiresBackupCare ? (
<TimeCell data-qa="backup-care-required-warning" warning>
{i18n.unit.attendanceReservations.requiresBackupCare}{' '}
<FontAwesomeIcon
icon={faExclamationTriangle}
color={colors.status.warning}
/>
</TimeCell>
) : (
<>
<AttendanceTime
data-qa="attendance-start"
warning={
attendance
? !isWithinExpectedTimes(attendance.interval.start)
: false
}
>
{attendance?.interval?.formatStart() ?? '-'}
</AttendanceTime>
<AttendanceTime
data-qa="attendance-end"
warning={
attendance?.interval?.end
? !isWithinExpectedTimes(attendance.interval.end)
: false
}
>
{attendance?.interval?.end
? attendance.interval.formatEnd()
: '-'}
</AttendanceTime>
</>
)}
</TimesRow>
) : null}
{!inOtherUnit && !isInBackupGroup && scheduleType !== 'TERM_BREAK' && (
<DetailsToggle>
<IconOnlyButton
icon={faCircleEllipsis}
onClick={onStartEdit}
data-qa="open-details"
aria-label={i18n.common.open}
/>
</DetailsToggle>
)}
</Tooltip>
</AttendanceDateCell>
)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,14 @@ export default React.memo(function ChildDayReservation({
) : reservation && reservation.type === 'TIMES' ? (
<ReservationTooltip
tooltip={
reservation.staffCreated &&
i18n.unit.attendanceReservations.createdByEmployee
reservation && (reservation.staffCreated ? i18n.unit.attendanceReservations.lastModifiedStaff(
reservation.modifiedAt.format(),
reservation.modifiedBy.name
)
: i18n.unit.attendanceReservations.lastModifiedOther(
reservation.modifiedAt.format(),
reservation.modifiedBy.name
))
}
>
<TimeCell
Expand All @@ -145,7 +151,7 @@ export default React.memo(function ChildDayReservation({
/>
</>
)}
{reservation.staffCreated && '*'}
{reservation?.staffCreated && '*'}
</TimeCell>
</ReservationTooltip>
) : reservationIndex === 0 ? (
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/lib-common/generated/api-types/attendance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -96,6 +97,8 @@ export type AttendanceStatus =
export interface AttendanceTimes {
arrived: HelsinkiDateTime
departed: HelsinkiDateTime | null
modifiedAt: HelsinkiDateTime
modifiedBy: EvakaUser
}

/**
Expand Down Expand Up @@ -485,7 +488,8 @@ export function deserializeJsonAttendanceTimes(json: JsonOf<AttendanceTimes>): 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)
}
}

Expand Down
40 changes: 38 additions & 2 deletions frontend/src/lib-common/generated/api-types/reservations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -338,6 +351,8 @@ export namespace ReservationResponse {
*/
export interface NoTimes {
type: 'NO_TIMES'
modifiedAt: HelsinkiDateTime
modifiedBy: EvakaUser
staffCreated: boolean
}

Expand All @@ -346,6 +361,8 @@ export namespace ReservationResponse {
*/
export interface Times {
type: 'TIMES'
modifiedAt: HelsinkiDateTime
modifiedBy: EvakaUser
range: TimeRange
staffCreated: boolean
}
Expand Down Expand Up @@ -429,6 +446,16 @@ export function deserializeJsonAbsenceRequest(json: JsonOf<AbsenceRequest>): Abs
}


export function deserializeJsonAttendanceTimesForDate(json: JsonOf<AttendanceTimesForDate>): AttendanceTimesForDate {
return {
...json,
date: LocalDate.parseIso(json.date),
interval: TimeInterval.parseJson(json.interval),
modifiedAt: HelsinkiDateTime.parseIso(json.modifiedAt)
}
}


export function deserializeJsonChild(json: JsonOf<Child>): Child {
return {
...json,
Expand All @@ -451,7 +478,7 @@ export function deserializeJsonChildDatePresence(json: JsonOf<ChildDatePresence>
export function deserializeJsonChildRecordOfDay(json: JsonOf<ChildRecordOfDay>): 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))
}
Expand Down Expand Up @@ -620,14 +647,23 @@ export function deserializeJsonReservationChildInfo(json: JsonOf<ReservationChil



export function deserializeJsonReservationResponseNoTimes(json: JsonOf<ReservationResponse.NoTimes>): ReservationResponse.NoTimes {
return {
...json,
modifiedAt: HelsinkiDateTime.parseIso(json.modifiedAt)
}
}

export function deserializeJsonReservationResponseTimes(json: JsonOf<ReservationResponse.Times>): ReservationResponse.Times {
return {
...json,
modifiedAt: HelsinkiDateTime.parseIso(json.modifiedAt),
range: TimeRange.parseJson(json.range)
}
}
export function deserializeJsonReservationResponse(json: JsonOf<ReservationResponse>): ReservationResponse {
switch (json.type) {
case 'NO_TIMES': return deserializeJsonReservationResponseNoTimes(json)
case 'TIMES': return deserializeJsonReservationResponseTimes(json)
default: return json
}
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2540,6 +2540,16 @@ export const fi = {
requiresBackupCare: 'Tee varasijoitus',
openReservationModal: 'Tee toistuva varaus',
childCount: 'Lapsia läsnä',
lastModifiedStaff: (date: string, name: string) => (
<div>
<p>*Henkilökunnan tekemä merkintä</p>
<p>
Viimeksi muokattu {date}; muokkaaja: {name}
</p>
</div>
),
lastModifiedOther: (date: string, name: string) =>
`Viimeksi muokattu ${date}; muokkaaja: ${name}`,
reservationModal: {
title: 'Tee varaus',
selectedChildren: 'Lapset, joille varaus tehdään',
Expand Down
Loading
Loading