Skip to content

Commit 4c0ed94

Browse files
authored
Merge pull request #6339 from espoon-voltti/scheduled-acl
Pääkäyttäjä voi ajastaa tulevia luvituksia etukäteen
2 parents c27b7df + a718a3f commit 4c0ed94

File tree

20 files changed

+572
-23
lines changed

20 files changed

+572
-23
lines changed

frontend/src/employee-frontend/components/employees/DaycareRolesModal.tsx

+13-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const form = transformed(
5757
object({
5858
daycareTree: array(treeNode()),
5959
role: required(oneOf<UserRole>()),
60+
startDate: required(localDate()),
6061
endDate: localDate()
6162
}),
6263
(res) => {
@@ -67,9 +68,13 @@ const form = transformed(
6768

6869
if (daycareIds.length === 0) return ValidationError.of('required')
6970

71+
if (res.endDate && res.endDate.isBefore(res.startDate))
72+
return ValidationError.field('endDate', 'dateTooEarly')
73+
7074
return ValidationSuccess.of<UpsertEmployeeDaycareRolesRequest>({
7175
daycareIds,
7276
role: res.role,
77+
startDate: res.startDate,
7378
endDate: res.endDate ?? null
7479
})
7580
}
@@ -119,14 +124,17 @@ export default React.memo(function DaycareRolesModal({
119124
label: i18n.roles.adRoles[r]
120125
}))
121126
},
127+
startDate: localDate.fromDate(LocalDate.todayInHelsinkiTz(), {
128+
minDate: LocalDate.todayInHelsinkiTz()
129+
}),
122130
endDate: localDate.fromDate(null, {
123131
minDate: LocalDate.todayInHelsinkiTz()
124132
})
125133
}),
126134
i18n.validationErrors
127135
)
128136

129-
const { daycareTree, role, endDate } = useFormFields(boundForm)
137+
const { daycareTree, role, startDate, endDate } = useFormFields(boundForm)
130138

131139
return (
132140
<MutateFormModal
@@ -152,6 +160,10 @@ export default React.memo(function DaycareRolesModal({
152160
<Label>{i18n.employees.editor.unitRoles.role}</Label>
153161
<SelectF bind={role} />
154162
</FixedSpaceColumn>
163+
<FixedSpaceColumn spacing="xs">
164+
<Label>{i18n.employees.editor.unitRoles.startDate}</Label>
165+
<DatePickerF bind={startDate} locale={lang} />
166+
</FixedSpaceColumn>
155167
<FixedSpaceColumn spacing="xs">
156168
<Label>{i18n.employees.editor.unitRoles.endDate}</Label>
157169
<DatePickerF bind={endDate} locale={lang} />

frontend/src/employee-frontend/components/employees/EmployeePage.tsx

+55
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import DaycareRolesModal from './DaycareRolesModal'
4040
import {
4141
deleteEmployeeDaycareRolesMutation,
4242
deleteEmployeeMobileDeviceMutation,
43+
deleteEmployeeScheduledDaycareRoleMutation,
4344
employeeDetailsQuery,
4445
updateEmployeeGlobalRolesMutation
4546
} from './queries'
@@ -113,6 +114,12 @@ const EmployeePage = React.memo(function EmployeePage({
113114
[employee.daycareRoles]
114115
)
115116

117+
const sortedScheduledRoles = useMemo(
118+
() =>
119+
sortBy(employee.scheduledDaycareRoles, ({ daycareName }) => daycareName),
120+
[employee.scheduledDaycareRoles]
121+
)
122+
116123
return (
117124
<div>
118125
{rolesModalOpen && (
@@ -212,6 +219,54 @@ const EmployeePage = React.memo(function EmployeePage({
212219
))}
213220
</Tbody>
214221
</Table>
222+
223+
<Gap />
224+
225+
<Title size={3}>
226+
{i18n.employees.editor.unitRoles.scheduledRolesTitle}
227+
</Title>
228+
<Table>
229+
<Thead>
230+
<Tr>
231+
<Th>{i18n.employees.editor.unitRoles.unit}</Th>
232+
<Th>{i18n.employees.editor.unitRoles.role}</Th>
233+
<Th>{i18n.employees.editor.unitRoles.startDate}</Th>
234+
<Th>{i18n.employees.editor.unitRoles.endDate}</Th>
235+
<Th />
236+
</Tr>
237+
</Thead>
238+
<Tbody>
239+
{sortedScheduledRoles.map(
240+
({ daycareId, daycareName, role, startDate, endDate }) => (
241+
<Tr key={`${daycareId}/${role}`}>
242+
<Td>
243+
<Link to={`/units/${daycareId}`}>{daycareName}</Link>
244+
</Td>
245+
<Td>{i18n.roles.adRoles[role]}</Td>
246+
<Td>{startDate.format()}</Td>
247+
<Td>{endDate?.format() ?? '-'}</Td>
248+
<Td>
249+
<ConfirmedMutation
250+
buttonStyle="ICON"
251+
icon={faTrash}
252+
buttonAltText={i18n.common.remove}
253+
confirmationTitle={
254+
i18n.employees.editor.unitRoles.deleteConfirm
255+
}
256+
mutation={deleteEmployeeScheduledDaycareRoleMutation}
257+
onClick={() => ({
258+
id: employee.id,
259+
daycareId: daycareId
260+
})}
261+
disabled={editingGlobalRoles}
262+
/>
263+
</Td>
264+
</Tr>
265+
)
266+
)}
267+
</Tbody>
268+
</Table>
269+
215270
<Gap />
216271

217272
<Title size={3}>{i18n.employees.editor.mobile.title}</Title>

frontend/src/employee-frontend/components/employees/queries.ts

+6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
createSsnEmployee,
1212
deactivateEmployee,
1313
deleteEmployeeDaycareRoles,
14+
deleteEmployeeScheduledDaycareRole,
1415
getEmployeeDetails,
1516
searchEmployees,
1617
updateEmployeeGlobalRoles,
@@ -38,6 +39,11 @@ export const deleteEmployeeDaycareRolesMutation = q.mutation(
3839
[searchEmployeesQuery.prefix, ({ id }) => employeeDetailsQuery({ id })]
3940
)
4041

42+
export const deleteEmployeeScheduledDaycareRoleMutation = q.mutation(
43+
deleteEmployeeScheduledDaycareRole,
44+
[searchEmployeesQuery.prefix, ({ id }) => employeeDetailsQuery({ id })]
45+
)
46+
4147
export const deleteEmployeeMobileDeviceMutation = q.parametricMutation<{
4248
employeeId: EmployeeId
4349
}>()(deleteMobileDevice, [

frontend/src/employee-frontend/components/unit/queries.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
getDaycareAcl,
3333
getDaycares,
3434
getGroups,
35+
getScheduledDaycareAcl,
3536
getTemporaryEmployee,
3637
getTemporaryEmployees,
3738
getUnitGroupDetails,
@@ -77,12 +78,15 @@ export const unitQuery = q.query(getDaycare)
7778

7879
export const unitAclQuery = q.query(getDaycareAcl)
7980

81+
export const unitScheduledAclQuery = q.query(getScheduledDaycareAcl)
82+
8083
export const temporaryEmployeeQuery = q.query(getTemporaryEmployee)
8184

8285
export const temporaryEmployeesQuery = q.query(getTemporaryEmployees)
8386

8487
export const addFullAclForRoleMutation = q.mutation(addFullAclForRole, [
85-
({ unitId }) => unitAclQuery({ unitId })
88+
({ unitId }) => unitAclQuery({ unitId }),
89+
({ unitId }) => unitScheduledAclQuery({ unitId })
8690
])
8791
export const createTemporaryEmployeeMutation = q.mutation(
8892
createTemporaryEmployee,

frontend/src/employee-frontend/components/unit/tab-unit-information/UnitAccessControl.tsx

+73-9
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
DaycareAclRow,
1919
DaycareId,
2020
EmployeeId,
21+
ScheduledDaycareAclRow,
2122
UserRole
2223
} from 'lib-common/generated/api-types/shared'
2324
import {
@@ -58,7 +59,8 @@ import {
5859
deleteUnitSupervisorMutation,
5960
reactivateTemporaryEmployeeMutation,
6061
temporaryEmployeesQuery,
61-
unitAclQuery
62+
unitAclQuery,
63+
unitScheduledAclQuery
6264
} from '../queries'
6365

6466
import AddAclModal from './acl-modals/AddAclModal'
@@ -74,6 +76,21 @@ export type DaycareAclRole = Extract<
7476
| 'EARLY_CHILDHOOD_EDUCATION_SECRETARY'
7577
>
7678

79+
const roleOrder = (role: UserRole) => {
80+
switch (role) {
81+
case 'UNIT_SUPERVISOR':
82+
return 0
83+
case 'SPECIAL_EDUCATION_TEACHER':
84+
return 1
85+
case 'EARLY_CHILDHOOD_EDUCATION_SECRETARY':
86+
return 2
87+
case 'STAFF':
88+
return 3
89+
default:
90+
return 999 // not expected
91+
}
92+
}
93+
7794
function GroupListing({
7895
unitGroups,
7996
groupIds
@@ -235,14 +252,7 @@ function AclTable({
235252
orderBy(
236253
rows.filter((row) => !row.employee.temporary),
237254
[
238-
(row) =>
239-
row.role === 'UNIT_SUPERVISOR'
240-
? 0
241-
: row.role === 'SPECIAL_EDUCATION_TEACHER'
242-
? 1
243-
: row.role === 'EARLY_CHILDHOOD_EDUCATION_SECRETARY'
244-
? 2
245-
: 3,
255+
(row) => roleOrder(row.role),
246256
(row) => row.employee.firstName,
247257
(row) => row.employee.lastName
248258
]
@@ -313,6 +323,52 @@ function AclTable({
313323
)
314324
}
315325

326+
function ScheduledAclTable({ rows }: { rows: ScheduledDaycareAclRow[] }) {
327+
const { i18n } = useTranslation()
328+
329+
const orderedRows = useMemo(
330+
() =>
331+
orderBy(rows, [
332+
(row) => roleOrder(row.role),
333+
(row) => row.firstName,
334+
(row) => row.lastName
335+
]),
336+
[rows]
337+
)
338+
339+
return (
340+
<Table data-qa="scheduled-acl-table">
341+
<Thead>
342+
<Tr>
343+
<Th>{i18n.unit.accessControl.role}</Th>
344+
<Th>{i18n.common.form.name}</Th>
345+
<Th>{i18n.unit.accessControl.aclStartDate}</Th>
346+
<Th>{i18n.unit.accessControl.aclEndDate}</Th>
347+
</Tr>
348+
</Thead>
349+
<Tbody>
350+
{orderedRows.map((row) => (
351+
<Tr key={row.id} data-qa={`scheduled-acl-row-${row.id}`}>
352+
<Td>
353+
<span data-qa="role">{i18n.roles.adRoles[row.role]}</span>
354+
</Td>
355+
<Td>
356+
<FixedSpaceColumn spacing="zero">
357+
<span data-qa="name">
358+
{formatName(row.firstName, row.lastName, i18n)}
359+
</span>
360+
<EmailSpan data-qa="email">{row.email}</EmailSpan>
361+
</FixedSpaceColumn>
362+
</Td>
363+
<Td>{row.startDate.format()}</Td>
364+
<Td>{row.endDate?.format()}</Td>
365+
</Tr>
366+
))}
367+
</Tbody>
368+
</Table>
369+
)
370+
}
371+
316372
function TemporaryEmployeesTable({
317373
unitId,
318374
unitGroups,
@@ -521,6 +577,9 @@ export default React.memo(function UnitAccessControl({
521577

522578
const employees = useQueryResult(getEmployeesQuery())
523579
const daycareAclRows = useQueryResult(unitAclQuery({ unitId }))
580+
const scheduledDaycareAclRows = useQueryResult(
581+
unitScheduledAclQuery({ unitId })
582+
)
524583
const temporaryEmployees = useQueryResult(
525584
permittedActions.includes('READ_TEMPORARY_EMPLOYEE')
526585
? temporaryEmployeesQuery({ unitId })
@@ -649,6 +708,11 @@ export default React.memo(function UnitAccessControl({
649708

650709
<Gap size="XL" />
651710

711+
<H3 noMargin>{i18n.unit.accessControl.scheduledAclRoles}</H3>
712+
{renderResult(scheduledDaycareAclRows, (scheduledDaycareAclRows) => (
713+
<ScheduledAclTable rows={scheduledDaycareAclRows} />
714+
))}
715+
652716
<FixedSpaceRow justifyContent="space-between" alignItems="center">
653717
<H3 noMargin>{i18n.unit.accessControl.temporaryEmployees.title}</H3>
654718
{permittedActions.includes('CREATE_TEMPORARY_EMPLOYEE') && (

frontend/src/employee-frontend/generated/api-clients/daycare.ts

+18
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { PreschoolTerm } from 'lib-common/generated/api-types/daycare'
3838
import { PreschoolTermId } from 'lib-common/generated/api-types/shared'
3939
import { PreschoolTermRequest } from 'lib-common/generated/api-types/daycare'
4040
import { PublicUnit } from 'lib-common/generated/api-types/daycare'
41+
import { ScheduledDaycareAclRow } from 'lib-common/generated/api-types/shared'
4142
import { ServiceWorkerNote } from 'lib-common/generated/api-types/daycare'
4243
import { StaffAttendanceForDates } from 'lib-common/generated/api-types/daycare'
4344
import { StaffAttendanceUpdate } from 'lib-common/generated/api-types/daycare'
@@ -60,6 +61,7 @@ import { deserializeJsonDaycareResponse } from 'lib-common/generated/api-types/d
6061
import { deserializeJsonEmployee } from 'lib-common/generated/api-types/pis'
6162
import { deserializeJsonPreschoolTerm } from 'lib-common/generated/api-types/daycare'
6263
import { deserializeJsonPublicUnit } from 'lib-common/generated/api-types/daycare'
64+
import { deserializeJsonScheduledDaycareAclRow } from 'lib-common/generated/api-types/shared'
6365
import { deserializeJsonStaffAttendanceForDates } from 'lib-common/generated/api-types/daycare'
6466
import { deserializeJsonUnitGroupDetails } from 'lib-common/generated/api-types/daycare'
6567
import { uri } from 'lib-common/uri'
@@ -858,6 +860,22 @@ export async function getDaycareAcl(
858860
}
859861

860862

863+
/**
864+
* Generated from fi.espoo.evaka.daycare.controllers.UnitAclController.getScheduledDaycareAcl
865+
*/
866+
export async function getScheduledDaycareAcl(
867+
request: {
868+
unitId: DaycareId
869+
}
870+
): Promise<ScheduledDaycareAclRow[]> {
871+
const { data: json } = await client.request<JsonOf<ScheduledDaycareAclRow[]>>({
872+
url: uri`/employee/daycares/${request.unitId}/scheduled-acl`.toString(),
873+
method: 'GET'
874+
})
875+
return json.map(e => deserializeJsonScheduledDaycareAclRow(e))
876+
}
877+
878+
861879
/**
862880
* Generated from fi.espoo.evaka.daycare.controllers.UnitAclController.getTemporaryEmployee
863881
*/

frontend/src/employee-frontend/generated/api-clients/pis.ts

+21
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,27 @@ export async function deleteEmployeeDaycareRoles(
175175
}
176176

177177

178+
/**
179+
* Generated from fi.espoo.evaka.pis.controllers.EmployeeController.deleteEmployeeScheduledDaycareRole
180+
*/
181+
export async function deleteEmployeeScheduledDaycareRole(
182+
request: {
183+
id: EmployeeId,
184+
daycareId: DaycareId
185+
}
186+
): Promise<void> {
187+
const params = createUrlSearchParams(
188+
['daycareId', request.daycareId]
189+
)
190+
const { data: json } = await client.request<JsonOf<void>>({
191+
url: uri`/employee/employees/${request.id}/scheduled-daycare-role`.toString(),
192+
method: 'DELETE',
193+
params
194+
})
195+
return json
196+
}
197+
198+
178199
/**
179200
* Generated from fi.espoo.evaka.pis.controllers.EmployeeController.getEmployee
180201
*/

0 commit comments

Comments
 (0)