Skip to content

Commit 9728da0

Browse files
committed
scheduled acl
1 parent 75ff298 commit 9728da0

File tree

20 files changed

+567
-23
lines changed

20 files changed

+567
-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

+69-9
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
DaycareAclRow,
1818
DaycareId,
1919
EmployeeId,
20+
ScheduledDaycareAclRow,
2021
UserRole
2122
} from 'lib-common/generated/api-types/shared'
2223
import {
@@ -57,7 +58,8 @@ import {
5758
deleteUnitSupervisorMutation,
5859
reactivateTemporaryEmployeeMutation,
5960
temporaryEmployeesQuery,
60-
unitAclQuery
61+
unitAclQuery,
62+
unitScheduledAclQuery
6163
} from '../queries'
6264

6365
import AddAclModal from './acl-modals/AddAclModal'
@@ -73,6 +75,17 @@ export type DaycareAclRole = Extract<
7375
| 'EARLY_CHILDHOOD_EDUCATION_SECRETARY'
7476
>
7577

78+
const roleOrder = (role: UserRole) =>
79+
role === 'UNIT_SUPERVISOR'
80+
? 0
81+
: role === 'SPECIAL_EDUCATION_TEACHER'
82+
? 1
83+
: role === 'EARLY_CHILDHOOD_EDUCATION_SECRETARY'
84+
? 2
85+
: role === 'STAFF'
86+
? 3
87+
: 999 // not expected
88+
7689
function GroupListing({
7790
unitGroups,
7891
groupIds
@@ -238,14 +251,7 @@ function AclTable({
238251
orderBy(
239252
rows.filter((row) => !row.employee.temporary),
240253
[
241-
(row) =>
242-
row.role === 'UNIT_SUPERVISOR'
243-
? 0
244-
: row.role === 'SPECIAL_EDUCATION_TEACHER'
245-
? 1
246-
: row.role === 'EARLY_CHILDHOOD_EDUCATION_SECRETARY'
247-
? 2
248-
: 3,
254+
(row) => roleOrder(row.role),
249255
(row) => row.employee.firstName,
250256
(row) => row.employee.lastName
251257
]
@@ -282,6 +288,52 @@ function AclTable({
282288
)
283289
}
284290

291+
function ScheduledAclTable({ rows }: { rows: ScheduledDaycareAclRow[] }) {
292+
const { i18n } = useTranslation()
293+
294+
const orderedRows = useMemo(
295+
() =>
296+
orderBy(rows, [
297+
(row) => roleOrder(row.role),
298+
(row) => row.firstName,
299+
(row) => row.lastName
300+
]),
301+
[rows]
302+
)
303+
304+
return (
305+
<Table data-qa="scheduled-acl-table">
306+
<Thead>
307+
<Tr>
308+
<Th>{i18n.unit.accessControl.role}</Th>
309+
<Th>{i18n.common.form.name}</Th>
310+
<Th>{i18n.unit.accessControl.aclStartDate}</Th>
311+
<Th>{i18n.unit.accessControl.aclEndDate}</Th>
312+
</Tr>
313+
</Thead>
314+
<Tbody>
315+
{orderedRows.map((row) => (
316+
<Tr key={row.id} data-qa={`scheduled-acl-row-${row.id}`}>
317+
<Td>
318+
<span data-qa="role">{i18n.roles.adRoles[row.role]}</span>
319+
</Td>
320+
<Td>
321+
<FixedSpaceColumn spacing="zero">
322+
<span data-qa="name">
323+
{formatName(row.firstName, row.lastName, i18n)}
324+
</span>
325+
<EmailSpan data-qa="email">{row.email}</EmailSpan>
326+
</FixedSpaceColumn>
327+
</Td>
328+
<Td>{row.startDate.format()}</Td>
329+
<Td>{row.endDate?.format()}</Td>
330+
</Tr>
331+
))}
332+
</Tbody>
333+
</Table>
334+
)
335+
}
336+
285337
function TemporaryEmployeesTable({
286338
unitId,
287339
unitGroups,
@@ -490,6 +542,9 @@ export default React.memo(function UnitAccessControl({
490542

491543
const employees = useQueryResult(getEmployeesQuery())
492544
const daycareAclRows = useQueryResult(unitAclQuery({ unitId }))
545+
const scheduledDaycareAclRows = useQueryResult(
546+
unitScheduledAclQuery({ unitId })
547+
)
493548
const temporaryEmployees = useQueryResult(
494549
permittedActions.includes('READ_TEMPORARY_EMPLOYEE')
495550
? temporaryEmployeesQuery({ unitId })
@@ -627,6 +682,11 @@ export default React.memo(function UnitAccessControl({
627682

628683
<Gap size="XL" />
629684

685+
<H3 noMargin>{i18n.unit.accessControl.scheduledAclRoles}</H3>
686+
{renderResult(scheduledDaycareAclRows, (scheduledDaycareAclRows) => (
687+
<ScheduledAclTable rows={scheduledDaycareAclRows} />
688+
))}
689+
630690
<FixedSpaceRow justifyContent="space-between" alignItems="center">
631691
<H3 noMargin>{i18n.unit.accessControl.temporaryEmployees.title}</H3>
632692
{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)