Skip to content

Commit

Permalink
Merge pull request #5888 from espoon-voltti/marking-multiple-children…
Browse files Browse the repository at this point in the history
…-arrived

Usean lapsen sisäänkirjaaminen mobiilissa yhtä aikaa
  • Loading branch information
Joosakur authored Nov 7, 2024
2 parents b4d467f + 255239a commit 7886836
Show file tree
Hide file tree
Showing 19 changed files with 882 additions and 510 deletions.
8 changes: 7 additions & 1 deletion frontend/src/e2e-test/pages/mobile/list-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { UUID } from 'lib-common/types'

import { Page, Element } from '../../utils/page'
import { Page, Element, Checkbox } from '../../utils/page'

export default class MobileListPage {
unreadMessagesIndicator: Element
Expand All @@ -14,6 +14,8 @@ export default class MobileListPage {
departedChildrenTab: Element
absentChildrenTab: Element
groupSelectorButton: Element
multiselectToggle: Checkbox
markMultipleArrivedButton: Element
constructor(private readonly page: Page) {
this.unreadMessagesIndicator = page.findByDataQa(
'unread-messages-indicator'
Expand All @@ -24,6 +26,10 @@ export default class MobileListPage {
this.departedChildrenTab = page.findByDataQa('departed-tab')
this.absentChildrenTab = page.findByDataQa('absent-tab')
this.groupSelectorButton = page.findByDataQa('group-selector-button')
this.multiselectToggle = new Checkbox(
page.findByDataQa('multiselect-toggle')
)
this.markMultipleArrivedButton = page.findByDataQa('mark-multiple-arrived')
}

childRow = (childId: UUID) => this.page.findByDataQa(`child-${childId}`)
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/e2e-test/specs/6_mobile/child-attendances.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,31 @@ describe('Child mobile attendance list', () => {
await listPage.assertChildExists(child1)
})

test('Multiple children can be marked as present', async () => {
await openPage()
await createPlacements(testChild.id)
await createPlacements(testChild2.id, group2.id)
await createPlacements(testChildRestricted.id)

const mobileSignupUrl = await pairMobileDevice(testDaycare.id)
await page.goto(mobileSignupUrl)
await assertAttendanceCounts(3, 0, 0, 0, 3)
await listPage.comingChildrenTab.click()

await listPage.multiselectToggle.check()
await listPage.selectChild(testChild.id)
await listPage.selectChild(testChild2.id)
await listPage.markMultipleArrivedButton.click()
await childAttendancePage.setTime('08:00')
await childAttendancePage.selectMarkPresent()

await assertAttendanceCounts(1, 2, 0, 0, 3)

await listPage.presentChildrenTab.click()
await listPage.assertChildExists(testChild.id)
await listPage.assertChildExists(testChild2.id)
})

test('Child can be marked as absent for the whole day', async () => {
await openPage()
const child = testChild2.id
Expand Down
14 changes: 7 additions & 7 deletions frontend/src/employee-mobile-frontend/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ function UnitRouter() {
path="/children/:childId/*"
element={<ChildRouter unitId={unitId} />}
/>
<Route path="/mark-present" element={<MarkPresent unitId={unitId} />} />
<Route index element={<Navigate replace to="groups/all" />} />
</Routes>
)
Expand Down Expand Up @@ -266,10 +267,6 @@ function ChildRouter({ unitId }: { unitId: UUID }) {
index
element={<AttendanceChildPage unitId={unitId} childId={childId} />}
/>
<Route
path="/mark-present"
element={<MarkPresent unitId={unitId} childId={childId} />}
/>
<Route
path="/mark-absent"
element={<MarkAbsent unitId={unitId} childId={childId} />}
Expand Down Expand Up @@ -402,6 +399,12 @@ export const routes = {
settings(unitId: UUID): Uri {
return uri`${this.unit(unitId)}/settings`
},
markPresent(unitId: UUID, childIds: UUID[], multiselect: boolean): Uri {
const params = new URLSearchParams()
params.set('children', childIds.join(','))
if (multiselect) params.set('multiselect', 'true')
return uri`${this.unit(unitId)}/mark-present`.appendQuery(params)
},
unitOrGroup(unitOrGroup: UnitOrGroup): Uri {
const id = unitOrGroup.type === 'unit' ? 'all' : unitOrGroup.id
return uri`${this.unit(unitOrGroup.unitId)}/groups/${id}`
Expand All @@ -427,9 +430,6 @@ export const routes = {
childMarkAbsentBeforehand(unitId: UUID, child: UUID): Uri {
return uri`${this.child(unitId, child)}/mark-absent-beforehand`
},
markPresent(unitId: UUID, child: UUID): Uri {
return uri`${this.child(unitId, child)}/mark-present`
},
markAbsent(unitId: UUID, child: UUID): Uri {
return uri`${this.child(unitId, child)}/mark-absent`
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
//
// SPDX-License-Identifier: LGPL-2.1-or-later

import React, { useCallback, useMemo } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'

import {
AttendanceChild,
AttendanceStatus
} from 'lib-common/generated/api-types/attendance'
import { UUID } from 'lib-common/types'
import { ContentArea } from 'lib-components/layout/Container'
import { TabLinks } from 'lib-components/molecules/Tabs'

Expand All @@ -33,6 +34,10 @@ export default React.memo(function AttendanceList({
}: Props) {
const { i18n } = useTranslation()

const [multiselectChildren, setMultiselectChildren] = useState<UUID[] | null>(
null
)

const groupChildren = useMemo(
() =>
unitOrGroup.type === 'unit'
Expand Down Expand Up @@ -118,6 +123,12 @@ export default React.memo(function AttendanceList({
[activeStatus, childrenWithStatus]
)

// this resetting should be done as a tab change side effect but
// that would require changing tabs from NavLinks to buttons
useEffect(() => {
setMultiselectChildren(null)
}, [activeStatus])

return (
<>
<TabLinks tabs={tabs} mobile sticky topOffset={64} />
Expand All @@ -130,6 +141,8 @@ export default React.memo(function AttendanceList({
unitOrGroup={unitOrGroup}
items={filteredChildren}
type={activeStatus}
multiselectChildren={multiselectChildren}
setMultiselectChildren={setMultiselectChildren}
/>
</ContentArea>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,12 @@ const ChildSearch = React.memo(function Search({
setShowSearch={toggleShow}
searchResults={searchResults}
/>
<ChildList unitOrGroup={unitOrGroup} items={searchResults} />
<ChildList
unitOrGroup={unitOrGroup}
items={searchResults}
multiselectChildren={null}
setMultiselectChildren={() => undefined}
/>
</ContentArea>
</SearchContainer>
)
Expand Down
149 changes: 126 additions & 23 deletions frontend/src/employee-mobile-frontend/child-attendance/ChildList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,27 @@
// SPDX-License-Identifier: LGPL-2.1-or-later

import React from 'react'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'

import {
AttendanceChild,
AttendanceStatus
} from 'lib-common/generated/api-types/attendance'
import { FixedSpaceColumn } from 'lib-components/layout/flex-helpers'
import { UUID } from 'lib-common/types'
import { Button } from 'lib-components/atoms/buttons/Button'
import Checkbox from 'lib-components/atoms/form/Checkbox'
import {
FixedSpaceColumn,
FixedSpaceRow
} from 'lib-components/layout/flex-helpers'
import {
defaultMargins,
isSpacingSize,
SpacingSize
} from 'lib-components/white-space'
import colors from 'lib-customizations/common'
import { faTimes } from 'lib-icons'

import { routes } from '../App'
import { useTranslation } from '../common/i18n'
Expand All @@ -31,6 +39,8 @@ interface Props {
unitOrGroup: UnitOrGroup
items: ListItem[]
type?: AttendanceStatus
multiselectChildren: UUID[] | null
setMultiselectChildren: (selected: UUID[] | null) => void
}

const NoChildrenOnList = styled.div`
Expand All @@ -41,33 +51,100 @@ const NoChildrenOnList = styled.div`
export default React.memo(function ChildList({
unitOrGroup,
items,
type
type,
multiselectChildren,
setMultiselectChildren
}: Props) {
const { i18n } = useTranslation()
const navigate = useNavigate()
const unitId = unitOrGroup.unitId

return (
<FixedSpaceColumn>
<OrderedList spacing="zero">
{items.length > 0 ? (
items.map((ac) => (
<Li key={ac.id}>
<ChildListItem
unitOrGroup={unitOrGroup}
type={type}
key={ac.id}
child={ac}
childAttendanceUrl={routes.child(unitId, ac.id).value}
/>
</Li>
))
) : (
<NoChildrenOnList data-qa="no-children-indicator">
{i18n.mobile.emptyList(type || 'ABSENT')}
</NoChildrenOnList>
)}
</OrderedList>
</FixedSpaceColumn>
<>
<FixedSpaceColumn>
<OrderedList spacing="zero">
{items.length > 0 ? (
<>
{type === 'COMING' && (
<Li>
<MultiselectToggleBox>
<Checkbox
checked={multiselectChildren !== null}
onChange={(checked) =>
checked
? setMultiselectChildren([])
: setMultiselectChildren(null)
}
label={i18n.attendances.actions.arrivalMultiselect.toggle}
data-qa="multiselect-toggle"
/>
</MultiselectToggleBox>
</Li>
)}
{items.map((ac) => (
<Li key={ac.id}>
<ChildListItem
unitOrGroup={unitOrGroup}
type={type}
key={ac.id}
child={ac}
childAttendanceUrl={routes.child(unitId, ac.id).value}
selected={
multiselectChildren
? multiselectChildren.includes(ac.id)
: null
}
onChangeSelected={(selected) => {
if (multiselectChildren) {
setMultiselectChildren(
selected
? [...multiselectChildren, ac.id]
: multiselectChildren.filter((id) => id !== ac.id)
)
}
}}
/>
</Li>
))}
</>
) : (
<NoChildrenOnList data-qa="no-children-indicator">
{i18n.mobile.emptyList(type || 'ABSENT')}
</NoChildrenOnList>
)}
</OrderedList>
</FixedSpaceColumn>
{multiselectChildren && (
<MultiselectActions>
<FixedSpaceRow
spacing="s"
alignItems="center"
justifyContent="space-evenly"
>
<Button
appearance="inline"
text={i18n.common.cancel}
icon={faTimes}
onClick={() => setMultiselectChildren(null)}
/>
<FloatingActionButton
appearance="button"
primary
text={i18n.attendances.actions.arrivalMultiselect.confirm(
multiselectChildren.length
)}
disabled={multiselectChildren.length === 0}
onClick={() =>
navigate(
routes.markPresent(unitId, multiselectChildren, true).value
)
}
data-qa="mark-multiple-arrived"
/>
</FixedSpaceRow>
</MultiselectActions>
)}
</>
)
})

Expand Down Expand Up @@ -102,3 +179,29 @@ const Li = styled.li`
left: ${defaultMargins.s};
}
`

const MultiselectToggleBox = styled.div`
align-items: center;
display: flex;
padding: ${defaultMargins.s} ${defaultMargins.m};
border-radius: 2px;
background-color: ${colors.grayscale.g0};
`

const MultiselectActions = styled.div`
position: sticky;
z-index: 10;
bottom: 0;
left: 0;
right: 0;
width: 100%;
padding: ${defaultMargins.xs} ${defaultMargins.s};
min-height: 30px;
background-color: rgba(255, 255, 255, 0.9);
`

const FloatingActionButton = styled(Button)`
border-radius: 40px;
max-width: 240px;
white-space: break-spaces;
`
Loading

0 comments on commit 7886836

Please sign in to comment.