From 24aff5f6eea893b56474baa6d964c4911bab2444 Mon Sep 17 00:00:00 2001 From: Petri Lehtinen Date: Fri, 13 Dec 2024 13:26:56 +0200 Subject: [PATCH 1/5] Disallow personal mobile device from non-municipal unit supervisors --- .../main/kotlin/fi/espoo/evaka/shared/security/Action.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/service/src/main/kotlin/fi/espoo/evaka/shared/security/Action.kt b/service/src/main/kotlin/fi/espoo/evaka/shared/security/Action.kt index d4573cfec5d..690fb9f722c 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/shared/security/Action.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/shared/security/Action.kt @@ -146,7 +146,9 @@ sealed interface Action { ), PERSONAL_MOBILE_DEVICE_PAGE( HasGlobalRole(ADMIN), - HasUnitRole(UNIT_SUPERVISOR).inAnyUnit(), + HasUnitRole(UNIT_SUPERVISOR) + .withUnitProviderTypes(ProviderType.MUNICIPAL, ProviderType.MUNICIPAL_SCHOOL) + .inAnyUnit(), IsEmployee.ownerOfAnyMobileDevice(), ), PIN_CODE_PAGE( @@ -201,7 +203,9 @@ sealed interface Action { READ_PERSONAL_MOBILE_DEVICES(IsEmployee.any()), CREATE_PERSONAL_MOBILE_DEVICE_PAIRING( HasGlobalRole(ADMIN), - HasUnitRole(UNIT_SUPERVISOR).inAnyUnit(), + HasUnitRole(UNIT_SUPERVISOR) + .withUnitProviderTypes(ProviderType.MUNICIPAL, ProviderType.MUNICIPAL_SCHOOL) + .inAnyUnit(), ), SEARCH_INVOICES(HasGlobalRole(ADMIN, FINANCE_ADMIN, FINANCE_STAFF)), CREATE_DRAFT_INVOICES(HasGlobalRole(ADMIN, FINANCE_ADMIN)), From 7d0d2d29c44be413623076798922041ca09cdf8a Mon Sep 17 00:00:00 2001 From: Petri Lehtinen Date: Fri, 13 Dec 2024 14:48:40 +0200 Subject: [PATCH 2/5] Delete disallowed personal mobile devices on ACL changes --- .../UnitAclControllerIntegrationTest.kt | 51 ++++++++++++++ .../kotlin/fi/espoo/evaka/daycare/UnitAcl.kt | 14 +--- .../daycare/controllers/UnitAclController.kt | 67 +++++++++++++++++-- .../evaka/pairing/MobileDeviceQueries.kt | 4 ++ .../fi/espoo/evaka/shared/async/AsyncJob.kt | 5 ++ .../evaka/shared/auth/AuthenticatedUser.kt | 6 ++ 6 files changed, 129 insertions(+), 18 deletions(-) diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/daycare/controllers/UnitAclControllerIntegrationTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/daycare/controllers/UnitAclControllerIntegrationTest.kt index 78300687ecd..5a574ec4245 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/daycare/controllers/UnitAclControllerIntegrationTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/daycare/controllers/UnitAclControllerIntegrationTest.kt @@ -9,16 +9,20 @@ import com.github.kittinunf.fuel.core.isSuccessful import com.github.kittinunf.fuel.jackson.responseObject import fi.espoo.evaka.FullApplicationTest import fi.espoo.evaka.attendance.getOccupancyCoefficientsByUnit +import fi.espoo.evaka.pairing.listPersonalDevices import fi.espoo.evaka.pis.TemporaryEmployee import fi.espoo.evaka.pis.controllers.PinCode import fi.espoo.evaka.pis.deactivateInactiveEmployees import fi.espoo.evaka.shared.DaycareId import fi.espoo.evaka.shared.EmployeeId import fi.espoo.evaka.shared.GroupId +import fi.espoo.evaka.shared.async.AsyncJob +import fi.espoo.evaka.shared.async.AsyncJobRunner import fi.espoo.evaka.shared.auth.* import fi.espoo.evaka.shared.dev.DevDaycare import fi.espoo.evaka.shared.dev.DevDaycareGroup import fi.espoo.evaka.shared.dev.DevEmployee +import fi.espoo.evaka.shared.dev.DevPersonalMobileDevice import fi.espoo.evaka.shared.dev.insert import fi.espoo.evaka.shared.domain.EvakaClock import fi.espoo.evaka.shared.domain.HelsinkiDateTime @@ -43,6 +47,7 @@ import org.springframework.beans.factory.annotation.Autowired class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = true) { @Autowired private lateinit var unitAclController: UnitAclController + @Autowired private lateinit var asyncJobRunner: AsyncJobRunner private val employee = DaycareAclRowEmployee( id = EmployeeId(UUID.randomUUID()), @@ -271,6 +276,52 @@ class UnitAclControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach = assertEquals(MessageAccountState.INACTIVE_ACCOUNT, employeeMessageAccountState()) } + @Test + fun `personal mobile device is deleted in async job after supervisor role is removed`() { + val supervisor1 = DevEmployee() + val device1 = DevPersonalMobileDevice(employeeId = supervisor1.id) + val supervisor2 = DevEmployee() + val device2 = DevPersonalMobileDevice(employeeId = supervisor2.id) + db.transaction { tx -> + tx.insert(supervisor1, unitRoles = mapOf(testDaycare.id to UserRole.UNIT_SUPERVISOR)) + tx.insert(device1) + tx.insert( + supervisor2, + unitRoles = + mapOf( + testDaycare.id to UserRole.UNIT_SUPERVISOR, + testDaycare2.id to UserRole.UNIT_SUPERVISOR, + ), + ) + tx.insert(device2) + } + + val now = HelsinkiDateTime.of(LocalDate.of(2024, 12, 13), LocalTime.of(12, 0)) + unitAclController.deleteUnitSupervisor( + dbInstance(), + admin, + MockEvakaClock(now), + testDaycare.id, + supervisor1.id, + ) + unitAclController.deleteUnitSupervisor( + dbInstance(), + admin, + MockEvakaClock(now), + testDaycare.id, + supervisor2.id, + ) + asyncJobRunner.runPendingJobsSync(MockEvakaClock(now.plusHours(1))) + + db.read { tx -> + assertEquals(emptyList(), tx.listPersonalDevices(employeeId = supervisor1.id)) + assertEquals( + listOf(device2.id), + tx.listPersonalDevices(employeeId = supervisor2.id).map { it.id }, + ) + } + } + @Test fun temporaryEmployeeCrud() { val dateTime = HelsinkiDateTime.of(LocalDate.of(2023, 3, 29), LocalTime.of(8, 37)) diff --git a/service/src/main/kotlin/fi/espoo/evaka/daycare/UnitAcl.kt b/service/src/main/kotlin/fi/espoo/evaka/daycare/UnitAcl.kt index 9c1d98820ef..6ee021c4dc9 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/daycare/UnitAcl.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/daycare/UnitAcl.kt @@ -5,22 +5,10 @@ package fi.espoo.evaka.daycare import fi.espoo.evaka.messaging.deactivateEmployeeMessageAccount -import fi.espoo.evaka.shared.DaycareId import fi.espoo.evaka.shared.EmployeeId -import fi.espoo.evaka.shared.auth.* +import fi.espoo.evaka.shared.auth.hasAnyDaycareAclRow import fi.espoo.evaka.shared.db.Database -fun removeDaycareAclForRole( - tx: Database.Transaction, - daycareId: DaycareId, - employeeId: EmployeeId, - role: UserRole, -) { - tx.syncDaycareGroupAcl(daycareId, employeeId, emptyList()) - tx.deleteDaycareAclRow(daycareId, employeeId, role) - deactivatePersonalMessageAccountIfNeeded(tx, employeeId) -} - fun deactivatePersonalMessageAccountIfNeeded(tx: Database.Transaction, employeeId: EmployeeId) { if (!tx.hasAnyDaycareAclRow(employeeId)) { // Deactivate the message account when the employee is not in any unit anymore diff --git a/service/src/main/kotlin/fi/espoo/evaka/daycare/controllers/UnitAclController.kt b/service/src/main/kotlin/fi/espoo/evaka/daycare/controllers/UnitAclController.kt index c6172bc6e7f..d274484070d 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/daycare/controllers/UnitAclController.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/daycare/controllers/UnitAclController.kt @@ -10,14 +10,16 @@ import fi.espoo.evaka.absence.getDaycareIdByGroup import fi.espoo.evaka.attendance.OccupancyCoefficientUpsert import fi.espoo.evaka.attendance.getOccupancyCoefficientForEmployeeInUnit import fi.espoo.evaka.attendance.upsertOccupancyCoefficient -import fi.espoo.evaka.daycare.removeDaycareAclForRole +import fi.espoo.evaka.daycare.deactivatePersonalMessageAccountIfNeeded import fi.espoo.evaka.messaging.deactivateEmployeeMessageAccount import fi.espoo.evaka.messaging.upsertEmployeeMessageAccount +import fi.espoo.evaka.pairing.deletePersonalDevices import fi.espoo.evaka.pis.Employee import fi.espoo.evaka.pis.NewEmployee import fi.espoo.evaka.pis.TemporaryEmployee import fi.espoo.evaka.pis.createEmployee import fi.espoo.evaka.pis.getEmployee +import fi.espoo.evaka.pis.getEmployeeRoles import fi.espoo.evaka.pis.getPinCode import fi.espoo.evaka.pis.getTemporaryEmployees import fi.espoo.evaka.pis.updateEmployee @@ -26,6 +28,8 @@ import fi.espoo.evaka.pis.upsertPinCode import fi.espoo.evaka.shared.DaycareId import fi.espoo.evaka.shared.EmployeeId import fi.espoo.evaka.shared.GroupId +import fi.espoo.evaka.shared.async.AsyncJob +import fi.espoo.evaka.shared.async.AsyncJobRunner import fi.espoo.evaka.shared.auth.* import fi.espoo.evaka.shared.db.Database import fi.espoo.evaka.shared.domain.* @@ -41,8 +45,10 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RestController @RestController -class UnitAclController(private val accessControl: AccessControl) { - +class UnitAclController( + private val accessControl: AccessControl, + private val asyncJobRunner: AsyncJobRunner, +) { val coefficientPositiveValue = BigDecimal("7.00") val coefficientNegativeValue = BigDecimal("0.00") @@ -110,7 +116,13 @@ class UnitAclController(private val accessControl: AccessControl) { daycareId, ) validateIsPermanentEmployee(it, employeeId) - removeDaycareAclForRole(it, daycareId, employeeId, UserRole.UNIT_SUPERVISOR) + removeDaycareAclForRole( + it, + clock.now(), + daycareId, + employeeId, + UserRole.UNIT_SUPERVISOR, + ) } } Audit.UnitAclDelete.log(targetId = AuditId(daycareId), objectId = AuditId(employeeId)) @@ -136,6 +148,7 @@ class UnitAclController(private val accessControl: AccessControl) { validateIsPermanentEmployee(it, employeeId) removeDaycareAclForRole( it, + clock.now(), daycareId, employeeId, UserRole.SPECIAL_EDUCATION_TEACHER, @@ -165,6 +178,7 @@ class UnitAclController(private val accessControl: AccessControl) { validateIsPermanentEmployee(it, employeeId) removeDaycareAclForRole( it, + clock.now(), daycareId, employeeId, UserRole.EARLY_CHILDHOOD_EDUCATION_SECRETARY, @@ -192,7 +206,7 @@ class UnitAclController(private val accessControl: AccessControl) { daycareId, ) validateIsPermanentEmployee(it, employeeId) - removeDaycareAclForRole(it, daycareId, employeeId, UserRole.STAFF) + removeDaycareAclForRole(it, clock.now(), daycareId, employeeId, UserRole.STAFF) } } Audit.UnitAclDelete.log(targetId = AuditId(daycareId), objectId = AuditId(employeeId)) @@ -594,4 +608,47 @@ class UnitAclController(private val accessControl: AccessControl) { fun parseCoefficientValue(bool: Boolean) = if (bool) coefficientPositiveValue else coefficientNegativeValue + + init { + asyncJobRunner.registerHandler(::deletePersonalMobileDevicesIfNeeded) + } + + private fun deletePersonalMobileDevicesIfNeeded( + db: Database.Connection, + clock: EvakaClock, + job: AsyncJob.DeletePersonalDevicesIfNeeded, + ) { + db.transaction { tx -> + if ( + !accessControl.hasPermissionFor( + tx, + AuthenticatedUser.Employee(job.employeeId, tx.getEmployeeRoles(job.employeeId)), + clock, + Action.Global.CREATE_PERSONAL_MOBILE_DEVICE_PAIRING, + ) + ) { + tx.deletePersonalDevices(job.employeeId) + } + } + } + + fun removeDaycareAclForRole( + tx: Database.Transaction, + now: HelsinkiDateTime, + daycareId: DaycareId, + employeeId: EmployeeId, + role: UserRole, + ) { + tx.syncDaycareGroupAcl(daycareId, employeeId, emptyList()) + tx.deleteDaycareAclRow(daycareId, employeeId, role) + deactivatePersonalMessageAccountIfNeeded(tx, employeeId) + + // Delete personal mobile devices after a while, in case the employee is added back to this + // or some other unit + asyncJobRunner.plan( + tx, + listOf(AsyncJob.DeletePersonalDevicesIfNeeded(employeeId)), + runAt = now.plusHours(1), + ) + } } diff --git a/service/src/main/kotlin/fi/espoo/evaka/pairing/MobileDeviceQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/pairing/MobileDeviceQueries.kt index 28b0ff7d93c..6c6d4f755f9 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/pairing/MobileDeviceQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/pairing/MobileDeviceQueries.kt @@ -78,3 +78,7 @@ fun Database.Transaction.renameDevice(id: MobileDeviceId, name: String) { fun Database.Transaction.deleteDevice(id: MobileDeviceId) = createUpdate { sql("DELETE FROM mobile_device WHERE id = ${bind(id)}") }.execute() + +fun Database.Transaction.deletePersonalDevices(employee: EmployeeId) = execute { + sql("DELETE FROM mobile_device WHERE employee_id = ${bind(employee)}") +} diff --git a/service/src/main/kotlin/fi/espoo/evaka/shared/async/AsyncJob.kt b/service/src/main/kotlin/fi/espoo/evaka/shared/async/AsyncJob.kt index 642a7d86125..5b4b55f1dc0 100755 --- a/service/src/main/kotlin/fi/espoo/evaka/shared/async/AsyncJob.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/shared/async/AsyncJob.kt @@ -428,6 +428,10 @@ sealed interface AsyncJob : AsyncJobPayload { override val user: AuthenticatedUser? = null } + data class DeletePersonalDevicesIfNeeded(val employeeId: EmployeeId) : AsyncJob { + override val user: AuthenticatedUser? = null + } + companion object { val main = AsyncJobRunner.Pool( @@ -462,6 +466,7 @@ sealed interface AsyncJob : AsyncJobPayload { PlacementTool::class, PlacementToolFromSSN::class, InvoiceCorrectionMigration::class, + DeletePersonalDevicesIfNeeded::class, ), ) val email = diff --git a/service/src/main/kotlin/fi/espoo/evaka/shared/auth/AuthenticatedUser.kt b/service/src/main/kotlin/fi/espoo/evaka/shared/auth/AuthenticatedUser.kt index b9da0907f69..029e9026117 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/shared/auth/AuthenticatedUser.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/shared/auth/AuthenticatedUser.kt @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonSerialize import com.google.common.hash.HashCode import com.google.common.hash.Hashing +import fi.espoo.evaka.pis.EmployeeRoles import fi.espoo.evaka.pis.EmployeeUser import fi.espoo.evaka.shared.EmployeeId import fi.espoo.evaka.shared.EvakaUserId @@ -45,6 +46,11 @@ sealed class AuthenticatedUser { val globalRoles: Set, val allScopedRoles: Set, ) : AuthenticatedUser() { + constructor( + id: EmployeeId, + roles: EmployeeRoles, + ) : this(id, roles.globalRoles, roles.allScopedRoles) + constructor( id: EmployeeId, roles: Set, From c30e19499363d7f81a3a0a76af18fcd12aada517 Mon Sep 17 00:00:00 2001 From: Petri Lehtinen Date: Fri, 13 Dec 2024 13:39:43 +0200 Subject: [PATCH 3/5] Delete disallowed mobile devices from database --- ...te_disallowed_personal_mobile_pairings.sql | 25 +++++++++++++++++++ service/src/main/resources/migrations.txt | 1 + 2 files changed, 26 insertions(+) create mode 100644 service/src/main/resources/db/migration/V478__delete_disallowed_personal_mobile_pairings.sql diff --git a/service/src/main/resources/db/migration/V478__delete_disallowed_personal_mobile_pairings.sql b/service/src/main/resources/db/migration/V478__delete_disallowed_personal_mobile_pairings.sql new file mode 100644 index 00000000000..f8900b21e94 --- /dev/null +++ b/service/src/main/resources/db/migration/V478__delete_disallowed_personal_mobile_pairings.sql @@ -0,0 +1,25 @@ +WITH disallowed_devices AS ( + SELECT id + FROM mobile_device + WHERE + employee_id IS NOT NULL AND + NOT EXISTS ( + SELECT FROM daycare_acl a + JOIN daycare d ON d.id = a.daycare_id + WHERE + (d.provider_type = 'MUNICIPAL' OR d.provider_type = 'MUNICIPAL_SCHOOL') AND + a.employee_id = mobile_device.employee_id AND + a.role = 'UNIT_SUPERVISOR' + ) +), push_subscriptions AS ( + DELETE FROM mobile_device_push_subscription + WHERE device IN (SELECT id FROM disallowed_devices) +), push_groups AS ( + DELETE FROM mobile_device_push_group + WHERE device IN (SELECT id FROM disallowed_devices) +), pairings AS ( + DELETE FROM pairing + WHERE mobile_device_id IN (SELECT id FROM disallowed_devices) +) +DELETE FROM mobile_device +WHERE id IN (SELECT id FROM disallowed_devices); diff --git a/service/src/main/resources/migrations.txt b/service/src/main/resources/migrations.txt index 9891bb0de7f..e2e857537bb 100644 --- a/service/src/main/resources/migrations.txt +++ b/service/src/main/resources/migrations.txt @@ -473,3 +473,4 @@ V474__holiday_questionnaire_open_ranges.sql V475__application_modified_metadata.sql V476__finance_metadata_process.sql V477__titania_errors_id.sql +V478__delete_disallowed_personal_mobile_pairings.sql From bce24f85364901556c53049aa8e641109b4757fc Mon Sep 17 00:00:00 2001 From: Petri Lehtinen Date: Mon, 16 Dec 2024 09:27:42 +0200 Subject: [PATCH 4/5] Delete non-realtime staff attendance support from employee-mobile --- .../src/e2e-test/pages/mobile/staff-page.ts | 85 --------- .../src/e2e-test/specs/6_mobile/staff.spec.ts | 126 ------------ frontend/src/employee-mobile-frontend/App.tsx | 16 -- .../common/BottomNavbar.tsx | 35 ++-- .../generated/api-clients/daycare.ts | 49 ----- .../generated/api-clients/occupancy.ts | 67 ------- .../staff/PlusMinus.tsx | 69 ------- .../staff/StaffAttendanceEditor.tsx | 179 ------------------ .../staff/StaffPage.tsx | 120 ------------ .../src/employee-mobile-frontend/staff/api.ts | 46 ----- .../employee-mobile-frontend/staff/utils.ts | 42 ---- .../lib-common/generated/api-types/daycare.ts | 21 -- .../generated/api-types/occupancy.ts | 16 -- .../controllers/StaffAttendanceController.kt | 41 ---- .../evaka/occupancy/OccupancyController.kt | 102 ---------- 15 files changed, 16 insertions(+), 998 deletions(-) delete mode 100644 frontend/src/e2e-test/specs/6_mobile/staff.spec.ts delete mode 100644 frontend/src/employee-mobile-frontend/generated/api-clients/daycare.ts delete mode 100644 frontend/src/employee-mobile-frontend/generated/api-clients/occupancy.ts delete mode 100644 frontend/src/employee-mobile-frontend/staff/PlusMinus.tsx delete mode 100644 frontend/src/employee-mobile-frontend/staff/StaffAttendanceEditor.tsx delete mode 100644 frontend/src/employee-mobile-frontend/staff/StaffPage.tsx delete mode 100644 frontend/src/employee-mobile-frontend/staff/api.ts delete mode 100644 frontend/src/employee-mobile-frontend/staff/utils.ts diff --git a/frontend/src/e2e-test/pages/mobile/staff-page.ts b/frontend/src/e2e-test/pages/mobile/staff-page.ts index 8f12350864c..27c8ae050f7 100644 --- a/frontend/src/e2e-test/pages/mobile/staff-page.ts +++ b/frontend/src/e2e-test/pages/mobile/staff-page.ts @@ -20,91 +20,6 @@ import { TextInput } from '../../utils/page' -export default class StaffPage { - #staffCount: Element - #staffOtherCount: Element - #cancelButton: Element - #confirmButton: Element - #occupancyRealized: Element - #updated: Element - constructor(readonly page: Page) { - this.#staffCount = page.findByDataQa('staff-count') - this.#staffOtherCount = page.findByDataQa('staff-other-count') - this.#cancelButton = page.findByDataQa('cancel-button') - this.#confirmButton = page.findByDataQa('confirm-button') - this.#occupancyRealized = page.findByDataQa('realized-occupancy') - this.#updated = page.findByDataQa('updated') - } - - private countButton(parent: Element, which: 'plus' | 'minus') { - return parent.find(`[data-qa="${which}-button"]`) - } - - get staffCount() { - return this.#staffCount.find('[data-qa="value"]').text - } - - get staffOtherCount() { - return this.#staffOtherCount.find('[data-qa="value"]').text - } - - async incDecButtonsVisible(): Promise { - return Promise.all( - [this.#staffCount, this.#staffOtherCount] - .map((parent) => - (['plus', 'minus'] as const).map((which) => - this.countButton(parent, which) - ) - ) - .flat() - .map((el) => el.visible) - ) - } - - async incStaffCount() { - return this.countButton(this.#staffCount, 'plus').click() - } - - async decStaffCount() { - return this.countButton(this.#staffCount, 'minus').click() - } - - async incStaffOtherCount() { - return this.countButton(this.#staffOtherCount, 'plus').click() - } - - async decStaffOtherCount() { - return this.countButton(this.#staffOtherCount, 'minus').click() - } - - async cancel() { - return this.#cancelButton.click() - } - - async confirm() { - return this.#confirmButton.click() - } - - get buttonsDisabled() { - return Promise.all([ - this.#cancelButton.disabled, - this.#confirmButton.disabled - ]).then(([cancel, confirm]) => cancel && confirm) - } - - get buttonsEnabled() { - return this.buttonsDisabled.then((disabled) => !disabled) - } - - get updated() { - return this.#updated.text - } - - get occupancy() { - return this.#occupancyRealized.text - } -} - export class StaffAttendancePage { editButton: Element previousAttendancesButton: Element diff --git a/frontend/src/e2e-test/specs/6_mobile/staff.spec.ts b/frontend/src/e2e-test/specs/6_mobile/staff.spec.ts deleted file mode 100644 index 18a4bb583aa..00000000000 --- a/frontend/src/e2e-test/specs/6_mobile/staff.spec.ts +++ /dev/null @@ -1,126 +0,0 @@ -// SPDX-FileCopyrightText: 2017-2022 City of Espoo -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -import HelsinkiDateTime from 'lib-common/helsinki-date-time' - -import { - testDaycareGroup, - Fixture, - familyWithTwoGuardians, - testDaycare, - testCareArea -} from '../../dev-api/fixtures' -import { - createDefaultServiceNeedOptions, - resetServiceState -} from '../../generated/api-clients' -import MobileNav from '../../pages/mobile/mobile-nav' -import StaffPage from '../../pages/mobile/staff-page' -import { waitUntilEqual, waitUntilTrue } from '../../utils' -import { pairMobileDevice } from '../../utils/mobile' -import { Page } from '../../utils/page' - -let page: Page -let nav: MobileNav -let staffPage: StaffPage -let mobileSignupUrl: string - -const now = HelsinkiDateTime.of(2023, 3, 15, 12, 0) -const today = now.toLocalDate() - -beforeEach(async () => { - await resetServiceState() - await Fixture.careArea(testCareArea).save() - await Fixture.daycare(testDaycare).save() - await Fixture.family(familyWithTwoGuardians).save() - await createDefaultServiceNeedOptions() - - await Fixture.daycareGroup(testDaycareGroup).save() - const daycarePlacementFixture = await Fixture.placement({ - childId: familyWithTwoGuardians.children[0].id, - unitId: testDaycare.id, - startDate: today, - endDate: today.addYears(1) - }).save() - await Fixture.groupPlacement({ - daycarePlacementId: daycarePlacementFixture.id, - daycareGroupId: testDaycareGroup.id, - startDate: today, - endDate: today.addYears(1) - }).save() - - page = await Page.open({ mockedTime: now }) - nav = new MobileNav(page) - - mobileSignupUrl = await pairMobileDevice(testDaycare.id) - await page.goto(mobileSignupUrl) - await nav.staff.click() - staffPage = new StaffPage(page) -}) - -describe('Staff page', () => { - test('Staff for all groups is read-only', async () => { - await waitUntilEqual(() => staffPage.staffCount, '0') - await waitUntilEqual(() => staffPage.staffOtherCount, '0') - await waitUntilEqual(() => staffPage.updated, 'Tietoja ei ole päivitetty') - - await waitUntilEqual( - () => staffPage.incDecButtonsVisible(), - [false, false, false, false] - ) - }) - - test('Set group staff', async () => { - await nav.selectGroup(testDaycareGroup.id) - await waitUntilEqual(() => staffPage.staffCount, '0') - await waitUntilEqual(() => staffPage.staffOtherCount, '0') - await waitUntilEqual(() => staffPage.updated, 'Tietoja ei ole päivitetty') - - await staffPage.incStaffCount() - await staffPage.incStaffCount() - await staffPage.incStaffCount() - await waitUntilEqual(() => staffPage.staffCount, '1,5') - await staffPage.incStaffOtherCount() - await waitUntilEqual(() => staffPage.staffOtherCount, '0,5') - await staffPage.confirm() - - await waitUntilEqual( - () => staffPage.occupancy, - 'Ryhmän käyttöaste tänään 9,5 %' - ) - await waitUntilTrue(async () => - (await staffPage.updated).startsWith('Tiedot päivitetty tänään ') - ) - }) - - test('Cancel resets the form', async () => { - await nav.selectGroup(testDaycareGroup.id) - await staffPage.incStaffCount() - await staffPage.confirm() - - await staffPage.incStaffCount() - await staffPage.incStaffOtherCount() - await staffPage.cancel() - - await waitUntilEqual(() => staffPage.staffCount, '0,5') - await waitUntilEqual(() => staffPage.staffOtherCount, '0') - }) - - test('Button state', async () => { - await nav.selectGroup(testDaycareGroup.id) - await waitUntilTrue(() => staffPage.buttonsDisabled) - - await staffPage.incStaffCount() - await waitUntilTrue(() => staffPage.buttonsEnabled) - - await staffPage.decStaffCount() - await waitUntilTrue(() => staffPage.buttonsDisabled) - - await staffPage.incStaffOtherCount() - await waitUntilTrue(() => staffPage.buttonsEnabled) - - await staffPage.decStaffOtherCount() - await waitUntilTrue(() => staffPage.buttonsDisabled) - }) -}) diff --git a/frontend/src/employee-mobile-frontend/App.tsx b/frontend/src/employee-mobile-frontend/App.tsx index 28a678720b0..9677c51d685 100755 --- a/frontend/src/employee-mobile-frontend/App.tsx +++ b/frontend/src/employee-mobile-frontend/App.tsx @@ -61,7 +61,6 @@ import PairingWizard from './pairing/PairingWizard' import { queryClient, QueryClientProvider } from './query' import { RememberContext, RememberContextProvider } from './remember' import { SettingsPage } from './settings/SettingsPage' -import StaffPage from './staff/StaffPage' import ExternalStaffMemberPage from './staff-attendance/ExternalStaffMemberPage' import MarkExternalStaffMemberArrivalPage from './staff-attendance/MarkExternalStaffMemberArrivalPage' import StaffAttendanceEditPage from './staff-attendance/StaffAttendanceEditPage' @@ -212,10 +211,6 @@ function GroupRouter({ unitId }: { unitId: DaycareId }) { path="child-attendance/*" element={} /> - } - /> } @@ -305,14 +300,6 @@ function ChildRouter({ unitId }: { unitId: DaycareId }) { ) } -function StaffRouter({ unitOrGroup }: { unitOrGroup: UnitOrGroup }) { - return ( - - } /> - - ) -} - function StaffAttendanceRouter({ unitOrGroup }: { unitOrGroup: UnitOrGroup }) { return ( @@ -449,9 +436,6 @@ export const routes = { childSensitiveInfo(unitId: UUID, child: UUID): Uri { return uri`${this.child(unitId, child)}/info` }, - staff(unitOrGroup: UnitOrGroup): Uri { - return uri`${this.unitOrGroup(unitOrGroup)}/staff` - }, staffAttendanceRoot(unitOrGroup: UnitOrGroup): Uri { return uri`${this.unitOrGroup(unitOrGroup)}/staff-attendance` }, diff --git a/frontend/src/employee-mobile-frontend/common/BottomNavbar.tsx b/frontend/src/employee-mobile-frontend/common/BottomNavbar.tsx index 9cf952a1938..508e4894689 100644 --- a/frontend/src/employee-mobile-frontend/common/BottomNavbar.tsx +++ b/frontend/src/employee-mobile-frontend/common/BottomNavbar.tsx @@ -140,26 +140,23 @@ export default function BottomNavbar({ /> - + onClick={() => + selected !== 'staff' && + navigate(routes.staffAttendances(unitOrGroup, 'absent').value) + } + > + + + + ) : null} {unit.features.includes('MOBILE_MESSAGING') ? (