Skip to content

Commit

Permalink
Merge pull request #5862 from espoon-voltti/discussion-time-removal-b…
Browse files Browse the repository at this point in the history
…y-child

Kasvattajalle mahdollisuus poistaa lapsen kaikki yhden kyselyn keskusteluaikavaraukset kerralla
  • Loading branch information
tmkrepo authored Nov 5, 2024
2 parents 5377010 + c07d4b5 commit af51756
Show file tree
Hide file tree
Showing 13 changed files with 507 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ export class DiscussionSurveyReadView {
)
}

async openReservationClearingConfirmationModal(childId: string) {
await this.page.findByDataQa(`clear-reservations-button-${childId}`).click()
return new Modal(
this.page.findByDataQa(`clear-reservations-confirmation-modal`)
)
}

async openSurveyEditor() {
await this.editSurveyButton.click()
return new DiscussionSurveyEditor(this.page)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,48 @@ describe('Discussion surveys', () => {
await surveyView.assertNoTimesExist(testDay)
})

test("Employee can clear all child's survey reservations", async () => {
const testDay = mockedToday.addDays(1)

await calendarPage.selectGroup(groupId)
await calendarPage.weekModeButton.click()

const surveyListPage =
await calendarPage.calendarEventsSection.openDiscussionSurveyPage()
const surveyView = await surveyListPage.openDiscussionSurvey(testSurveyId)

await surveyView.addEventTimeForDay(testDay, {
startTime: '09:00',
endTime: '09:30'
})
await surveyView.addEventTimeForDay(testDay, {
startTime: '10:00',
endTime: '10:30'
})
await surveyView.waitUntilLoaded()

let reservationModal = await surveyView.openReservationModal(0, testDay)
await reservationModal.reserveEventTimeForChild(
`${child1Fixture.lastName} ${child1Fixture.firstName}`
)
reservationModal = await surveyView.openReservationModal(1, testDay)
await reservationModal.reserveEventTimeForChild(
`${child1Fixture.lastName} ${child1Fixture.firstName}`
)
await surveyView.waitUntilLoaded()
await surveyView.assertReservedAttendeeExists(child1Fixture.id)

const confirmationModal =
await surveyView.openReservationClearingConfirmationModal(
child1Fixture.id
)
await confirmationModal.submit()

await surveyView.waitUntilLoaded()

await surveyView.assertUnreservedAttendeeExists(child1Fixture.id)
})

test('Employee can reserve a time', async () => {
const childData = {
lastName: 'Högfors',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import groupBy from 'lodash/groupBy'
import orderBy from 'lodash/orderBy'
import partition from 'lodash/partition'
import React, {
MutableRefObject,
useCallback,
Expand Down Expand Up @@ -421,22 +420,15 @@ export default React.memo(function DiscussionReservationSurveyView({
gp.overlapsWith(eventData.period.asDateRange())
)
)
const reservations = eventData.times.filter(
(t) => t.childId !== null
)
const [reserved, unreserved] = partition(
sortedPeriodInvitees,
(e) => reservations.some((r) => r.childId === e.child.id)
)

return (
<>
<FormSectionGroup>
<H3>{t.discussionReservation.surveyInviteeTitle}</H3>
<FormFieldGroup>
<InviteeSection
reserved={reserved}
unreserved={unreserved}
invitees={sortedPeriodInvitees}
event={eventData}
/>
</FormFieldGroup>
</FormSectionGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,30 @@
//
// SPDX-License-Identifier: LGPL-2.1-or-later

import React from 'react'
import { faX } from '@fortawesome/free-solid-svg-icons'
import orderBy from 'lodash/orderBy'
import partition from 'lodash/partition'
import React, { useMemo, useState } from 'react'
import { Link } from 'react-router-dom'
import styled from 'styled-components'

import { useTranslation } from 'employee-frontend/state/i18n'
import {
CalendarEvent,
CalendarEventTime
} from 'lib-common/generated/api-types/calendarevent'
import { ChildBasics } from 'lib-common/generated/api-types/placement'
import { UUID } from 'lib-common/types'
import { Button } from 'lib-components/atoms/buttons/Button'
import {
FixedSpaceColumn,
FixedSpaceRow
} from 'lib-components/layout/flex-helpers'
import { MutateFormModal } from 'lib-components/molecules/modals/FormModal'
import { fontWeights } from 'lib-components/typography'
import { defaultMargins } from 'lib-components/white-space'

import { clearChildCalendarEventTimeReservationsForSurveyMutation } from '../queries'

import { ChildGroupInfo } from './DiscussionSurveyView'

Expand All @@ -21,51 +34,140 @@ const ReservationCount = styled.span`
`

const getChildUrl = (c: { id: string }) => `/child-information/${c.id}`

type InviteeInfo = { child: ChildBasics; reservations: CalendarEventTime[] }
const ChildNameList = React.memo(function ChildNameList({
eventId,
childList,
'data-qa': dataQa
}: {
childList: ChildBasics[]
eventId: UUID
childList: InviteeInfo[]
'data-qa'?: string
}) {
const { i18n } = useTranslation()
const [selectedInvitee, setSelectedInvitee] = useState<InviteeInfo | null>(
null
)
return (
<p data-qa={dataQa}>
{childList.map((item, index) => (
<span key={item.id} data-qa={`attendee-${item.id}`}>
<Link to={getChildUrl(item)}>
{`${item.firstName} ${item.lastName}`}
</Link>
{index === childList.length - 1 ? '' : ', '}
</span>
<InviteeGrid data-qa={dataQa}>
{selectedInvitee && (
<MutateFormModal
resolveMutation={
clearChildCalendarEventTimeReservationsForSurveyMutation
}
title={
i18n.unit.calendar.events.discussionReservation
.reservationClearConfirmationTitle
}
resolveAction={() => ({
body: {
childId: selectedInvitee.child.id,
calendarEventId: eventId
},
eventId
})}
resolveLabel={i18n.common.remove}
onSuccess={() => {
setSelectedInvitee(null)
}}
rejectAction={() => setSelectedInvitee(null)}
rejectLabel={i18n.common.cancel}
data-qa="clear-reservations-confirmation-modal"
>
<CenteringDiv>
<h3 data-qa="child-name">{`${selectedInvitee.child.firstName} ${selectedInvitee.child.lastName}`}</h3>
{selectedInvitee.reservations.map((r, i) => (
<p
key={r.id}
data-qa={`reservation-datetime-${i}`}
>{`${r.date.format()}: ${r.startTime.format()} - ${r.endTime.format()}`}</p>
))}
</CenteringDiv>
</MutateFormModal>
)}
{childList.map((item) => (
<React.Fragment key={item.child.id}>
<div>
<Link
to={getChildUrl(item.child)}
data-qa={`attendee-${item.child.id}`}
>
{`${item.child.firstName.split(' ')[0]} ${item.child.lastName}`}
</Link>
</div>
{item.reservations.length > 0 ? (
<div>
<Button
appearance="inline"
icon={faX}
onClick={() => setSelectedInvitee(item)}
text={
i18n.unit.calendar.events.discussionReservation
.clearReservationButtonLabel
}
data-qa={`clear-reservations-button-${item.child.id}`}
/>
</div>
) : (
<div />
)}
</React.Fragment>
))}
</p>
</InviteeGrid>
)
})

const CenteringDiv = styled.div`
text-align: center;
`

const InviteeGrid = styled.div`
padding: ${defaultMargins.s};
display: grid;
grid-template-columns: [name] 60% [button] 40%;
gap: 4px;
`

export default React.memo(function InviteeSection({
reserved,
unreserved
invitees,
event
}: {
reserved: ChildGroupInfo[]
unreserved: ChildGroupInfo[]
invitees: ChildGroupInfo[]
event: CalendarEvent
}) {
const { i18n } = useTranslation()
const t = i18n.unit.calendar.events.discussionReservation

const [reserved, unreserved] = useMemo(
() =>
partition(
invitees.map((i) => ({
child: i.child,
reservations: orderBy(
event.times.filter((t) => t.childId === i.child.id),
[(t) => t.date, (t) => t.startTime, (t) => t.endTime]
)
})),
(c) => c.reservations.length > 0
),
[invitees, event.times]
)

return (
<FixedSpaceRow spacing="XL" fullWidth justifyContent="space-between">
<FixedSpaceColumn fullWidth>
<FixedSpaceColumn fullWidth spacing="xs">
<ReservationCount>{`${t.unreservedTitle} (${unreserved.length}/${unreserved.length + reserved.length})`}</ReservationCount>
<ChildNameList
childList={unreserved.map((u) => u.child)}
childList={unreserved}
eventId={event.id}
data-qa="unreserved-attendees"
/>
</FixedSpaceColumn>
<FixedSpaceColumn fullWidth>
<FixedSpaceColumn fullWidth spacing="xs">
<ReservationCount>{`${t.reservedTitle} (${reserved.length}/${unreserved.length + reserved.length})`}</ReservationCount>
<ChildNameList
childList={reserved.map((r) => r.child)}
childList={reserved}
eventId={event.id}
data-qa="reserved-attendees"
/>
</FixedSpaceColumn>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import {
addCalendarEventTime,
clearEventTimesInEventForChild,
createCalendarEvent,
deleteCalendarEvent,
deleteCalendarEventTime,
Expand Down Expand Up @@ -74,6 +75,15 @@ export const setCalendarEventTimeReservationMutation = mutation({
invalidateQueryKeys: ({ eventId }) => [queryKeys.discussionSurvey(eventId)]
})

export const clearChildCalendarEventTimeReservationsForSurveyMutation =
mutation({
api: (arg: Arg0<typeof clearEventTimesInEventForChild>) =>
clearEventTimesInEventForChild(arg),
invalidateQueryKeys: ({ body }) => [
queryKeys.discussionSurvey(body.calendarEventId)
]
})

export const addCalendarEventTimeMutation = mutation({
api: (arg: Arg0<typeof addCalendarEventTime>) => addCalendarEventTime(arg),
invalidateQueryKeys: ({ id }) => [queryKeys.discussionSurvey(id)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import LocalDate from 'lib-common/local-date'
import { CalendarEvent } from 'lib-common/generated/api-types/calendarevent'
import { CalendarEventForm } from 'lib-common/generated/api-types/calendarevent'
import { CalendarEventTimeClearingForm } from 'lib-common/generated/api-types/calendarevent'
import { CalendarEventTimeEmployeeReservationForm } from 'lib-common/generated/api-types/calendarevent'
import { CalendarEventTimeForm } from 'lib-common/generated/api-types/calendarevent'
import { CalendarEventUpdateForm } from 'lib-common/generated/api-types/calendarevent'
Expand Down Expand Up @@ -39,6 +40,23 @@ export async function addCalendarEventTime(
}


/**
* Generated from fi.espoo.evaka.calendarevent.CalendarEventController.clearEventTimesInEventForChild
*/
export async function clearEventTimesInEventForChild(
request: {
body: CalendarEventTimeClearingForm
}
): Promise<void> {
const { data: json } = await client.request<JsonOf<void>>({
url: uri`/employee/calendar-event/clear-survey-reservations-for-child`.toString(),
method: 'POST',
data: request.body satisfies JsonCompatible<CalendarEventTimeClearingForm>
})
return json
}


/**
* Generated from fi.espoo.evaka.calendarevent.CalendarEventController.createCalendarEvent
*/
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/lib-common/generated/api-types/calendarevent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ export interface CalendarEventTimeCitizenReservationForm {
childId: UUID
}

/**
* Generated from fi.espoo.evaka.calendarevent.CalendarEventTimeClearingForm
*/
export interface CalendarEventTimeClearingForm {
calendarEventId: UUID
childId: UUID
}

/**
* Generated from fi.espoo.evaka.calendarevent.CalendarEventTimeEmployeeReservationForm
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2409,7 +2409,10 @@ export const fi = {
eventTime: {
addError: 'Keskusteluajan lisääminen epäonnistui',
deleteError: 'Keskusteluajan poistaminen epäonnistui'
}
},
reservationClearConfirmationTitle:
'Poistetaanko seuraavat varaukset?',
clearReservationButtonLabel: 'Poista varaukset'
},
reservedTimesLabel: 'varattua',
freeTimesLabel: 'vapaata'
Expand Down
Loading

0 comments on commit af51756

Please sign in to comment.