Skip to content

Commit

Permalink
Merge pull request #6383 from espoon-voltti/scheduled-acl-ux-improvem…
Browse files Browse the repository at this point in the history
…ents

Ajastettujen luvitusten UX-parannuksia
  • Loading branch information
Joosakur authored Feb 19, 2025
2 parents 5cfcccb + bfc4c6d commit 8cdf5c0
Show file tree
Hide file tree
Showing 11 changed files with 357 additions and 108 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
//
// SPDX-License-Identifier: LGPL-2.1-or-later

import React from 'react'
import sortBy from 'lodash/sortBy'
import React, { useMemo } from 'react'

import { scopedRoles } from 'lib-common/api-types/employee-auth'
import { boolean, localDate, string } from 'lib-common/form/fields'
Expand All @@ -18,11 +19,13 @@ import {
import { useForm, useFormFields } from 'lib-common/form/hooks'
import { Form, ValidationError, ValidationSuccess } from 'lib-common/form/types'
import { Daycare } from 'lib-common/generated/api-types/daycare'
import { UpsertEmployeeDaycareRolesRequest } from 'lib-common/generated/api-types/pis'
import {
EmployeeWithDaycareRoles,
UpsertEmployeeDaycareRolesRequest
} from 'lib-common/generated/api-types/pis'
import {
AreaId,
DaycareId,
EmployeeId,
UserRole
} from 'lib-common/generated/api-types/shared'
import LocalDate from 'lib-common/local-date'
Expand All @@ -32,6 +35,7 @@ import TreeDropdown, {
TreeNode
} from 'lib-components/atoms/dropdowns/TreeDropdown'
import { FixedSpaceColumn } from 'lib-components/layout/flex-helpers'
import { AlertBox } from 'lib-components/molecules/MessageBoxes'
import { DatePickerF } from 'lib-components/molecules/date-picker/DatePicker'
import { MutateFormModal } from 'lib-components/molecules/modals/FormModal'
import { Label } from 'lib-components/typography'
Expand All @@ -53,6 +57,12 @@ const treeNode = (): Form<DaycareTreeNode, never, DaycareTreeNode, unknown> =>
children: array(recursive(treeNode))
})

const unitsFromTree = (tree: DaycareTreeNode[]): DaycareId[] =>
tree
.flatMap((careArea) => careArea.children)
.filter((u) => u.checked)
.map((u) => u.key as DaycareId)

const form = transformed(
object({
daycareTree: array(treeNode()),
Expand All @@ -61,11 +71,7 @@ const form = transformed(
endDate: localDate()
}),
(res) => {
const daycareIds = res.daycareTree
.flatMap((careArea) => careArea.children)
.filter((u) => u.checked)
.map((u) => u.key as DaycareId)

const daycareIds = unitsFromTree(res.daycareTree)
if (daycareIds.length === 0) return ValidationError.of('required')

if (res.endDate && res.endDate.isBefore(res.startDate))
Expand All @@ -81,11 +87,11 @@ const form = transformed(
)

export default React.memo(function DaycareRolesModal({
employeeId,
employee: { id: employeeId, daycareRoles, scheduledDaycareRoles },
units,
onClose
}: {
employeeId: EmployeeId
employee: EmployeeWithDaycareRoles
units: Daycare[]
onClose: () => void
}) {
Expand Down Expand Up @@ -136,6 +142,67 @@ export default React.memo(function DaycareRolesModal({

const { daycareTree, role, startDate, endDate } = useFormFields(boundForm)

const selectedUnits = useMemo(
() => (daycareTree.isValid() ? unitsFromTree(daycareTree.value()) : []),
[daycareTree]
)

const currentAclWarning = useMemo(() => {
if (!startDate.isValid()) return null
const isScheduling = startDate
.value()
.isAfter(LocalDate.todayInHelsinkiTz())
const overlapping = sortBy(
daycareRoles.filter(
(r) =>
selectedUnits.includes(r.daycareId) &&
(r.endDate === null || r.endDate.isEqualOrAfter(startDate.value()))
),
(r) => r.daycareName
)
if (overlapping.length === 0) return null
return (
<div>
<span>{i18n.employees.editor.unitRoles.warnings.hasCurrent}:</span>
<ul>
{overlapping.map((r) => (
<li key={r.daycareId}>
{i18n.roles.adRoles[r.role]}: {r.daycareName}
</li>
))}
</ul>
<span>
{isScheduling
? i18n.employees.editor.unitRoles.warnings.currentEnding(
startDate.value()
)
: i18n.employees.editor.unitRoles.warnings.currentRemoved}
</span>
</div>
)
}, [i18n, daycareRoles, startDate, selectedUnits])

const scheduledAclWarning = useMemo(() => {
const removed = scheduledDaycareRoles.filter((r) =>
selectedUnits.includes(r.daycareId)
)
if (removed.length === 0) return null
return (
<div>
<span>{i18n.employees.editor.unitRoles.warnings.hasScheduled}:</span>
<ul>
{removed.map((r) => (
<li key={r.daycareId}>
{i18n.roles.adRoles[r.role]}: {r.daycareName} (
{r.startDate.format()} -)
</li>
))}
</ul>
<span>{i18n.employees.editor.unitRoles.warnings.scheduledRemoved}</span>
</div>
)
}, [i18n, scheduledDaycareRoles, selectedUnits])

return (
<MutateFormModal
title={i18n.employees.editor.unitRoles.addRolesModalTitle}
Expand Down Expand Up @@ -168,6 +235,8 @@ export default React.memo(function DaycareRolesModal({
<Label>{i18n.employees.editor.unitRoles.endDate}</Label>
<DatePickerF bind={endDate} locale={lang} />
</FixedSpaceColumn>
{currentAclWarning && <AlertBox message={currentAclWarning} />}
{scheduledAclWarning && <AlertBox message={scheduledAclWarning} />}
</FixedSpaceColumn>
</MutateFormModal>
)
Expand Down
125 changes: 76 additions & 49 deletions frontend/src/employee-frontend/components/employees/EmployeePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ const EmployeePage = React.memo(function EmployeePage({
<div>
{rolesModalOpen && (
<DaycareRolesModal
employeeId={employee.id}
employee={employee}
units={units}
onClose={() => setRolesModalOpen(false)}
/>
Expand Down Expand Up @@ -192,59 +192,27 @@ const EmployeePage = React.memo(function EmployeePage({
</Tr>
</Thead>
<Tbody>
{sortedRoles.map(({ daycareId, daycareName, role, endDate }) => (
<Tr key={`${daycareId}/${role}`}>
<Td>
<Link to={`/units/${daycareId}`}>{daycareName}</Link>
</Td>
<Td>{i18n.roles.adRoles[role]}</Td>
<Td>{endDate?.format() ?? '-'}</Td>
<Td>
<ConfirmedMutation
buttonStyle="ICON"
icon={faTrash}
buttonAltText={i18n.common.remove}
confirmationTitle={
i18n.employees.editor.unitRoles.deleteConfirm
}
mutation={deleteEmployeeDaycareRolesMutation}
onClick={() => ({
id: employee.id,
daycareId: daycareId
})}
disabled={editingGlobalRoles}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>

<Gap />
{sortedRoles.map(({ daycareId, daycareName, role, endDate }) => {
const scheduledRow = sortedScheduledRoles.find(
(r) => r.daycareId === daycareId
)
const roleChangeDate =
scheduledRow &&
(endDate === null || scheduledRow.startDate <= endDate.addDays(1))
? scheduledRow.startDate.subDays(1)
: null

<Title size={3}>
{i18n.employees.editor.unitRoles.scheduledRolesTitle}
</Title>
<Table>
<Thead>
<Tr>
<Th>{i18n.employees.editor.unitRoles.unit}</Th>
<Th>{i18n.employees.editor.unitRoles.role}</Th>
<Th>{i18n.employees.editor.unitRoles.startDate}</Th>
<Th>{i18n.employees.editor.unitRoles.endDate}</Th>
<Th />
</Tr>
</Thead>
<Tbody>
{sortedScheduledRoles.map(
({ daycareId, daycareName, role, startDate, endDate }) => (
return (
<Tr key={`${daycareId}/${role}`}>
<Td>
<Link to={`/units/${daycareId}`}>{daycareName}</Link>
</Td>
<Td>{i18n.roles.adRoles[role]}</Td>
<Td>{startDate.format()}</Td>
<Td>{endDate?.format() ?? '-'}</Td>
<Td>
{roleChangeDate
? `${roleChangeDate.format()}*`
: endDate?.format()}
</Td>
<Td>
<ConfirmedMutation
buttonStyle="ICON"
Expand All @@ -253,7 +221,7 @@ const EmployeePage = React.memo(function EmployeePage({
confirmationTitle={
i18n.employees.editor.unitRoles.deleteConfirm
}
mutation={deleteEmployeeScheduledDaycareRoleMutation}
mutation={deleteEmployeeDaycareRolesMutation}
onClick={() => ({
id: employee.id,
daycareId: daycareId
Expand All @@ -263,6 +231,65 @@ const EmployeePage = React.memo(function EmployeePage({
</Td>
</Tr>
)
})}
</Tbody>
</Table>
<Gap size="xs" />
<span>*{i18n.unit.accessControl.roleChange}</span>

<Gap />

<Title size={3}>
{i18n.employees.editor.unitRoles.scheduledRolesTitle}
</Title>
<Table>
<Thead>
<Tr>
<Th>{i18n.employees.editor.unitRoles.unit}</Th>
<Th>{i18n.employees.editor.unitRoles.role}</Th>
<Th>{i18n.employees.editor.unitRoles.startDate}</Th>
<Th>{i18n.employees.editor.unitRoles.endDate}</Th>
<Th />
</Tr>
</Thead>
<Tbody>
{sortedScheduledRoles.map(
({ daycareId, daycareName, role, startDate, endDate }) => {
const isRoleChange = sortedRoles.some(
(r) =>
r.daycareId === daycareId &&
(r.endDate === null || r.endDate >= startDate.subDays(1))
)
return (
<Tr key={`${daycareId}/${role}`}>
<Td>
<Link to={`/units/${daycareId}`}>{daycareName}</Link>
</Td>
<Td>{i18n.roles.adRoles[role]}</Td>
<Td>
{startDate.format()}
{isRoleChange && '*'}
</Td>
<Td>{endDate?.format() ?? '-'}</Td>
<Td>
<ConfirmedMutation
buttonStyle="ICON"
icon={faTrash}
buttonAltText={i18n.common.remove}
confirmationTitle={
i18n.employees.editor.unitRoles.deleteConfirm
}
mutation={deleteEmployeeScheduledDaycareRoleMutation}
onClick={() => ({
id: employee.id,
daycareId: daycareId
})}
disabled={editingGlobalRoles}
/>
</Td>
</Tr>
)
}
)}
</Tbody>
</Table>
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/employee-frontend/components/unit/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
createTemporaryEmployee,
deleteEarlyChildhoodEducationSecretary,
deleteGroup,
deleteScheduledAcl,
deleteSpecialEducationTeacher,
deleteStaff,
deleteTemporaryEmployee,
Expand Down Expand Up @@ -136,6 +137,9 @@ export const deleteEarlyChildhoodEducationSecretaryMutation = q.mutation(
export const deleteStaffMutation = q.mutation(deleteStaff, [
({ unitId }) => unitAclQuery({ unitId })
])
export const deleteScheduledAclMutation = q.mutation(deleteScheduledAcl, [
({ unitId }) => unitScheduledAclQuery({ unitId })
])
export const updateGroupAclWithOccupancyCoefficientMutation = q.mutation(
updateGroupAclWithOccupancyCoefficient,
[({ unitId }) => unitAclQuery({ unitId })]
Expand Down
Loading

0 comments on commit 8cdf5c0

Please sign in to comment.