From 574045904023dfb7b17154ef80b54a2c3246cf33 Mon Sep 17 00:00:00 2001 From: Joonas Javanainen Date: Thu, 30 Jan 2025 14:43:06 +0200 Subject: [PATCH 01/10] Remove dead code --- .../kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt index 9464760f92..048f2a9416 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt @@ -79,8 +79,6 @@ data class EmployeeWithDaycareRoles( val active: Boolean, ) -data class EmployeeIdWithName(val id: EmployeeId, val name: String) - fun Database.Transaction.createEmployee(employee: NewEmployee): Employee = createUpdate { sql( @@ -571,19 +569,6 @@ fun Database.Transaction.deactivateEmployeeRemoveRolesAndPin(id: EmployeeId) { listPersonalDevices(id).forEach { deleteDevice(it.id) } } -fun Database.Read.getEmployeeNamesByIds(employeeIds: List) = - createQuery { - sql( - """ -SELECT id, concat(first_name, ' ', last_name) AS name -FROM employee -WHERE id = ANY(${bind(employeeIds)}) -""" - ) - } - .toList() - .associate { it.id to it.name } - fun Database.Transaction.setEmployeePreferredFirstName( employeeId: EmployeeId, preferredFirstName: String?, From 3e94c4afc382a69445ccedc8c70c6f87ed2a98bb Mon Sep 17 00:00:00 2001 From: Joonas Javanainen Date: Thu, 30 Jan 2025 14:51:31 +0200 Subject: [PATCH 02/10] Pass info about the existence of employee ssn to frontend --- .../src/lib-common/generated/api-types/pis.ts | 2 ++ .../EmployeeControllerIntegrationTest.kt | 2 ++ .../evaka/vtjclient/VtjClientServiceTest.kt | 1 + .../main/kotlin/fi/espoo/evaka/pis/Employee.kt | 1 + .../fi/espoo/evaka/pis/EmployeeQueries.kt | 17 ++++++++++------- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/frontend/src/lib-common/generated/api-types/pis.ts b/frontend/src/lib-common/generated/api-types/pis.ts index dc34cad035..17a935c3a1 100644 --- a/frontend/src/lib-common/generated/api-types/pis.ts +++ b/frontend/src/lib-common/generated/api-types/pis.ts @@ -199,6 +199,7 @@ export interface Employee { email: string | null externalId: string | null firstName: string + hasSsn: boolean id: EmployeeId lastName: string preferredFirstName: string | null @@ -247,6 +248,7 @@ export interface EmployeeWithDaycareRoles { externalId: string | null firstName: string globalRoles: UserRole[] + hasSsn: boolean id: EmployeeId lastLogin: HelsinkiDateTime | null lastName: string diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/pis/controller/EmployeeControllerIntegrationTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/pis/controller/EmployeeControllerIntegrationTest.kt index dd0cb354bf..40aa7d1a12 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/pis/controller/EmployeeControllerIntegrationTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/pis/controller/EmployeeControllerIntegrationTest.kt @@ -278,6 +278,7 @@ class EmployeeControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach id = EmployeeId(UUID.randomUUID()), temporaryInUnitId = null, active = true, + hasSsn = false, ) val employee2 = @@ -293,6 +294,7 @@ class EmployeeControllerIntegrationTest : FullApplicationTest(resetDbBeforeEach id = EmployeeId(UUID.randomUUID()), temporaryInUnitId = null, active = true, + hasSsn = false, ) private fun Database.Read.hasActiveMessagingAccount(employeeId: EmployeeId) = diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/vtjclient/VtjClientServiceTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/vtjclient/VtjClientServiceTest.kt index 160a2c2eeb..093eec9a23 100755 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/vtjclient/VtjClientServiceTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/vtjclient/VtjClientServiceTest.kt @@ -207,6 +207,7 @@ class VtjClientServiceTest : FullApplicationTest(resetDbBeforeEach = false) { updated = null, temporaryInUnitId = null, active = true, + hasSsn = false, ) private fun vtjRequestType(requestType: RequestType): RequestMatcher = diff --git a/service/src/main/kotlin/fi/espoo/evaka/pis/Employee.kt b/service/src/main/kotlin/fi/espoo/evaka/pis/Employee.kt index 66670e7e57..80e30d7fb1 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/pis/Employee.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/pis/Employee.kt @@ -22,6 +22,7 @@ data class Employee( val updated: HelsinkiDateTime?, val temporaryInUnitId: DaycareId?, val active: Boolean, + val hasSsn: Boolean, ) data class TemporaryEmployee( diff --git a/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt index 048f2a9416..0beb688cca 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt @@ -77,6 +77,7 @@ data class EmployeeWithDaycareRoles( @Json val personalMobileDevices: List = listOf(), val temporaryUnitName: String?, val active: Boolean, + val hasSsn: Boolean, ) fun Database.Transaction.createEmployee(employee: NewEmployee): Employee = @@ -85,7 +86,7 @@ fun Database.Transaction.createEmployee(employee: NewEmployee): Employee = """ INSERT INTO employee (first_name, last_name, email, external_id, employee_number, roles, temporary_in_unit_id, active) VALUES (${bind(employee.firstName)}, ${bind(employee.lastName)}, ${bind(employee.email)}, ${bind(employee.externalId)}, ${bind(employee.employeeNumber)}, ${bind(employee.roles)}::user_role[], ${bind(employee.temporaryInUnitId)}, ${bind(employee.active)}) -RETURNING id, first_name, last_name, email, external_id, created, updated, roles, temporary_in_unit_id, active +RETURNING id, first_name, last_name, email, external_id, created, updated, roles, temporary_in_unit_id, active, (social_security_number IS NOT NULL) AS has_ssn """ ) } @@ -116,7 +117,7 @@ VALUES (${bind(employee.firstName)}, ${bind(employee.lastName)}, ${bind(employee }, ${bind(employee.roles)}::user_role[], ${bind(employee.active)}, ${bind(now)}) ON CONFLICT (external_id) DO UPDATE SET last_login = ${bind(now)}, first_name = EXCLUDED.first_name, last_name = EXCLUDED.last_name, email = EXCLUDED.email, employee_number = EXCLUDED.employee_number, active = TRUE -RETURNING id, preferred_first_name, first_name, last_name, email, external_id, created, updated, roles, active +RETURNING id, preferred_first_name, first_name, last_name, email, external_id, created, updated, roles, active, (social_security_number IS NOT NULL) AS has_ssn """ ) } @@ -140,7 +141,7 @@ fun Database.Transaction.loginEmployeeWithSuomiFi( UPDATE employee SET last_login = ${bind(now)}, first_name = ${bind(request.firstName)}, last_name = ${bind(request.lastName)} WHERE social_security_number = ${bind(request.ssn.value)} -RETURNING id, preferred_first_name, first_name, last_name, email, external_id, created, updated, temporary_in_unit_id, active +RETURNING id, preferred_first_name, first_name, last_name, email, external_id, created, updated, temporary_in_unit_id, active, (social_security_number IS NOT NULL) AS has_ssn """ ) } @@ -183,7 +184,7 @@ private fun Database.Read.searchEmployees( createQuery { sql( """ -SELECT e.id, preferred_first_name, first_name, last_name, email, external_id, e.created, e.updated, roles, temporary_in_unit_id, e.active +SELECT e.id, preferred_first_name, first_name, last_name, email, external_id, e.created, e.updated, roles, temporary_in_unit_id, e.active, (social_security_number IS NOT NULL) AS has_ssn FROM employee e WHERE (${bind(id)}::uuid IS NULL OR e.id = ${bind(id)}) AND (${bind(externalId)}::text IS NULL OR e.external_id = ${bind(externalId)}) AND (${bind(temporaryInUnitId)} IS NULL OR e.temporary_in_unit_id = ${bind(temporaryInUnitId)}) @@ -196,7 +197,7 @@ private fun Database.Read.searchFinanceDecisionHandlers(id: EmployeeId? = null) createQuery { sql( """ -SELECT DISTINCT e.id, e.preferred_first_name, e.first_name, e.last_name, e.email, e.external_id, e.created, e.updated, e.roles, e.active +SELECT DISTINCT e.id, e.preferred_first_name, e.first_name, e.last_name, e.email, e.external_id, e.created, e.updated, e.roles, e.active, (e.social_security_number IS NOT NULL) AS has_ssn FROM employee e JOIN daycare ON daycare.finance_decision_handler = e.id WHERE (${bind(id)}::uuid IS NULL OR e.id = ${bind(id)}) @@ -264,6 +265,7 @@ SELECT employee.last_name, employee.email, employee.active, + (employee.social_security_number IS NOT NULL) AS has_ssn, temp_unit.name as temporary_unit_name, employee.roles AS global_roles, ( @@ -418,6 +420,7 @@ SELECT employee.last_name, employee.email, employee.active, + (employee.social_security_number IS NOT NULL) AS has_ssn, temp_unit.name as temporary_unit_name, employee.roles AS global_roles, ( @@ -587,7 +590,7 @@ fun Database.Read.getEmployeesByRoles(roles: Set, unitId: DaycareId?): createQuery { sql( """ -SELECT id, first_name, last_name, email, external_id, created, updated, active +SELECT id, first_name, last_name, email, external_id, created, updated, active, (social_security_number IS NOT NULL) AS has_ssn FROM employee WHERE roles && ${bind(globalRoles)}::user_role[] ORDER BY last_name, first_name @@ -599,7 +602,7 @@ ORDER BY last_name, first_name createQuery { sql( """ -SELECT id, first_name, last_name, email, external_id, created, updated, active +SELECT id, first_name, last_name, email, external_id, created, updated, active, (social_security_number IS NOT NULL) AS has_ssn FROM employee WHERE roles && ${bind(globalRoles)}::user_role[] OR id IN ( SELECT employee_id From 7396f777bae31c5631daaf7c06df63750ccad7f6 Mon Sep 17 00:00:00 2001 From: Joonas Javanainen Date: Thu, 30 Jan 2025 14:56:16 +0200 Subject: [PATCH 03/10] Show hasSsn info in employee list --- .../employee-frontend/components/employees/EmployeeList.tsx | 4 +++- frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/employee-frontend/components/employees/EmployeeList.tsx b/frontend/src/employee-frontend/components/employees/EmployeeList.tsx index c7bcf34625..74d4c537b4 100644 --- a/frontend/src/employee-frontend/components/employees/EmployeeList.tsx +++ b/frontend/src/employee-frontend/components/employees/EmployeeList.tsx @@ -62,7 +62,8 @@ export function EmployeeList({ employees }: Props) { externalId, employeeNumber, temporaryUnitName, - active + active, + hasSsn }) => ( navigate(`/employees/${id}`)}> @@ -71,6 +72,7 @@ export function EmployeeList({ employees }: Props) { {email} {!!externalId &&
{externalId}
} + {hasSsn &&
{i18n.employees.hasSsn}
} {!!employeeNumber && (
{i18n.employees.employeeNumber}: {employeeNumber} diff --git a/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx b/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx index 2da8aca530..174f3f138c 100755 --- a/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx +++ b/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx @@ -4711,7 +4711,8 @@ export const fi = { newSsnEmployeeModal: { title: 'Lisää uusi hetullinen käyttäjä', createButton: 'Luo tunnus' - } + }, + hasSsn: 'Hetullinen käyttäjä' }, financeBasics: { fees: { From ff4b5a09ba45e27b5743b4397d839845d1a080d3 Mon Sep 17 00:00:00 2001 From: Joonas Javanainen Date: Thu, 30 Jan 2025 14:56:31 +0200 Subject: [PATCH 04/10] Show ssn employees in ACL lists --- .../components/unit/tab-unit-information/UnitAccessControl.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/employee-frontend/components/unit/tab-unit-information/UnitAccessControl.tsx b/frontend/src/employee-frontend/components/unit/tab-unit-information/UnitAccessControl.tsx index be31f15599..b160953d3f 100755 --- a/frontend/src/employee-frontend/components/unit/tab-unit-information/UnitAccessControl.tsx +++ b/frontend/src/employee-frontend/components/unit/tab-unit-information/UnitAccessControl.tsx @@ -382,7 +382,7 @@ export default React.memo(function UnitAccessControl({ employees.filter( (employee) => employee.id !== user?.id && - employee.externalId !== null && + (employee.externalId !== null || employee.hasSsn) && employee.temporaryInUnitId === null && !daycareAclRows.some((row) => row.employee.id === employee.id) ), From 66230eb7197254cf300e6223146aa2b554a529c1 Mon Sep 17 00:00:00 2001 From: Joonas Javanainen Date: Thu, 30 Jan 2025 14:57:01 +0200 Subject: [PATCH 05/10] Add missing translation --- frontend/src/lib-customizations/defaults/citizen/i18n/sv.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/lib-customizations/defaults/citizen/i18n/sv.tsx b/frontend/src/lib-customizations/defaults/citizen/i18n/sv.tsx index fc64fdc333..470e9f9dad 100644 --- a/frontend/src/lib-customizations/defaults/citizen/i18n/sv.tsx +++ b/frontend/src/lib-customizations/defaults/citizen/i18n/sv.tsx @@ -2104,7 +2104,7 @@ const sv: Translations = { unverifiedEmailWarning: 'Inloggning med e-post är endast tillåten om du har bekräftat din e-postadress', updatePassword: 'Uppdatera lösenord', - activateCredentials: 'Salli sähköpostikirjautuminen', + activateCredentials: 'Tillåt inloggning med e-post', activationSuccess: 'E-postinloggning aktiverad', activationSuccessOk: 'Klart', confirmPassword: 'Bekräfta lösenordet', From 3ddd2724d4c42954859397af69280c52c2c91bd9 Mon Sep 17 00:00:00 2001 From: Joonas Javanainen Date: Thu, 30 Jan 2025 15:14:56 +0200 Subject: [PATCH 06/10] Show better error on ssn conflict --- .../components/employees/EmployeesPage.tsx | 27 +++++++++++++++---- .../defaults/employee/i18n/fi.tsx | 3 ++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/frontend/src/employee-frontend/components/employees/EmployeesPage.tsx b/frontend/src/employee-frontend/components/employees/EmployeesPage.tsx index b228f9397b..e6460ad422 100644 --- a/frontend/src/employee-frontend/components/employees/EmployeesPage.tsx +++ b/frontend/src/employee-frontend/components/employees/EmployeesPage.tsx @@ -2,10 +2,11 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -import React, { useState } from 'react' +import React, { useCallback, useState } from 'react' import { useNavigate } from 'react-router' import styled from 'styled-components' +import { Failure } from 'lib-common/api' import { globalRoles } from 'lib-common/api-types/employee-auth' import { string } from 'lib-common/form/fields' import { object, required, validated } from 'lib-common/form/form' @@ -27,10 +28,10 @@ import { FixedSpaceFlexWrap, FixedSpaceRow } from 'lib-components/layout/flex-helpers' +import { AlertBox } from 'lib-components/molecules/MessageBoxes' import { MutateFormModal } from 'lib-components/molecules/modals/FormModal' import { Label } from 'lib-components/typography' -import { faPlus } from 'lib-icons' -import { faSearch } from 'lib-icons' +import { faPlus, faSearch } from 'lib-icons' import { useTranslation } from '../../state/i18n' import { RequirePermittedGlobalAction } from '../../utils/roles' @@ -183,16 +184,30 @@ const CreateModal = React.memo(function CreateModal({ const { i18n } = useTranslation() const t = i18n.employees.newSsnEmployeeModal + const [ssnConflict, setSsnConflict] = useState(false) const form = useForm( createForm, () => ({ ssn: '', firstName: '', lastName: '', email: '' }), - i18n.validationErrors + i18n.validationErrors, + { + onUpdate: (prev, next) => { + if (prev.ssn !== next.ssn) { + setSsnConflict(false) + } + return next + } + } ) const ssn = useFormField(form, 'ssn') const firstName = useFormField(form, 'firstName') const lastName = useFormField(form, 'lastName') const email = useFormField(form, 'email') + const onFailure = useCallback( + (failure: Failure) => setSsnConflict(failure.statusCode === 409), + [setSsnConflict] + ) + return ( onSuccess(id)} + onFailure={onFailure} rejectAction={onClose} rejectLabel={i18n.common.cancel} > @@ -250,6 +266,7 @@ const CreateModal = React.memo(function CreateModal({ bind={email} /> + {ssnConflict && } ) diff --git a/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx b/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx index 174f3f138c..b0ede4eff1 100755 --- a/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx +++ b/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx @@ -4710,7 +4710,8 @@ export const fi = { createNewSsnEmployee: 'Luo uusi hetullinen käyttäjä', newSsnEmployeeModal: { title: 'Lisää uusi hetullinen käyttäjä', - createButton: 'Luo tunnus' + createButton: 'Luo tunnus', + ssnConflict: 'Hetu on jo käytössä' }, hasSsn: 'Hetullinen käyttäjä' }, From 974582a15e4672bf9bb7b1e866cc8c57f0a0c264 Mon Sep 17 00:00:00 2001 From: Joonas Javanainen Date: Thu, 30 Jan 2025 15:17:18 +0200 Subject: [PATCH 07/10] Improve person free text search docs --- .../fi/espoo/evaka/application/ApplicationQueries.kt | 5 +++-- .../kotlin/fi/espoo/evaka/invoicing/data/InvoiceQueries.kt | 4 ++-- service/src/main/kotlin/fi/espoo/evaka/shared/db/Search.kt | 7 ++++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/service/src/main/kotlin/fi/espoo/evaka/application/ApplicationQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/application/ApplicationQueries.kt index ed35362e4c..7bc3505b38 100755 --- a/service/src/main/kotlin/fi/espoo/evaka/application/ApplicationQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/application/ApplicationQueries.kt @@ -22,7 +22,7 @@ import fi.espoo.evaka.shared.* import fi.espoo.evaka.shared.db.Database import fi.espoo.evaka.shared.db.PredicateSql import fi.espoo.evaka.shared.db.Row -import fi.espoo.evaka.shared.db.freeTextSearchPredicate +import fi.espoo.evaka.shared.db.personFreeTextSearchPredicate import fi.espoo.evaka.shared.domain.EvakaClock import fi.espoo.evaka.shared.domain.HelsinkiDateTime import fi.espoo.evaka.shared.security.actionrule.AccessControlFilter @@ -367,7 +367,8 @@ fun Database.Read.fetchApplicationSummaries( ) } else null, - if (searchTerms.isNotBlank()) freeTextSearchPredicate(listOf("child"), searchTerms) + if (searchTerms.isNotBlank()) + personFreeTextSearchPredicate(listOf("child"), searchTerms) else null, when (transferApplications) { TransferApplicationFilter.TRANSFER_ONLY -> diff --git a/service/src/main/kotlin/fi/espoo/evaka/invoicing/data/InvoiceQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/invoicing/data/InvoiceQueries.kt index 2bbbcc0ee2..8327a01241 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/invoicing/data/InvoiceQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/invoicing/data/InvoiceQueries.kt @@ -22,7 +22,7 @@ import fi.espoo.evaka.shared.db.Database import fi.espoo.evaka.shared.db.Predicate import fi.espoo.evaka.shared.db.PredicateSql import fi.espoo.evaka.shared.db.QuerySql -import fi.espoo.evaka.shared.db.freeTextSearchPredicate +import fi.espoo.evaka.shared.db.personFreeTextSearchPredicate import fi.espoo.evaka.shared.domain.FiniteDateRange import fi.espoo.evaka.shared.domain.HelsinkiDateTime import fi.espoo.evaka.shared.mapToPaged @@ -292,7 +292,7 @@ fun Database.Read.paginatedSearch( } else null, if (searchTerms.isNotBlank()) - freeTextSearchPredicate(listOf("head", "child"), searchTerms) + personFreeTextSearchPredicate(listOf("head", "child"), searchTerms) else null, if (periodStart != null) PredicateSql { where("invoice_date >= ${bind(periodStart)}") } else null, diff --git a/service/src/main/kotlin/fi/espoo/evaka/shared/db/Search.kt b/service/src/main/kotlin/fi/espoo/evaka/shared/db/Search.kt index 9d2e255964..4220a919be 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/shared/db/Search.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/shared/db/Search.kt @@ -15,7 +15,12 @@ private const val freeTextParamName = "free_text" private val ssnParamName = { index: Int -> "ssn_$index" } private val dateParamName = { index: Int -> "date_$index" } -fun freeTextSearchPredicate(tables: Collection, searchText: String): PredicateSql { +/** + * Returns a predicate that does a free text search using the given text on the given table names. + * + * The tables *must have* the following columns: social_security_number, date_of_birth, freetext_vec + */ +fun personFreeTextSearchPredicate(tables: Collection, searchText: String): PredicateSql { val ssnPredicates = findSsnParams(searchText).map { ssn -> Predicate { where("lower($it.social_security_number) = lower(${bind(ssn)})") } From f125aeb7862c7e57e1f92b160f582ecda94fd3f2 Mon Sep 17 00:00:00 2001 From: Joonas Javanainen Date: Thu, 30 Jan 2025 15:31:21 +0200 Subject: [PATCH 08/10] Modernize employee search --- .../fi/espoo/evaka/pis/EmployeeQueries.kt | 49 ++++++------------- .../kotlin/fi/espoo/evaka/shared/db/Search.kt | 49 +++++-------------- 2 files changed, 28 insertions(+), 70 deletions(-) diff --git a/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt index 0beb688cca..72cb85ecba 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/pis/EmployeeQueries.kt @@ -15,10 +15,7 @@ import fi.espoo.evaka.shared.DaycareId import fi.espoo.evaka.shared.EmployeeId import fi.espoo.evaka.shared.GroupId import fi.espoo.evaka.shared.auth.UserRole -import fi.espoo.evaka.shared.db.Binding -import fi.espoo.evaka.shared.db.Database -import fi.espoo.evaka.shared.db.Predicate -import fi.espoo.evaka.shared.db.freeTextSearchQueryForColumns +import fi.espoo.evaka.shared.db.* import fi.espoo.evaka.shared.domain.BadRequest import fi.espoo.evaka.shared.domain.EvakaClock import fi.espoo.evaka.shared.domain.HelsinkiDateTime @@ -383,31 +380,18 @@ fun getEmployeesPaged( hideDeactivated: Boolean = false, globalRoles: Set?, ): PagedEmployeesWithDaycareRoles { - val (freeTextQuery, freeTextParams) = - freeTextSearchQueryForColumns( - listOf("employee"), - listOf("first_name", "last_name"), - searchTerm, - ) - - val params = - listOfNotNull( - Binding.of("offset", (page - 1) * pageSize), - Binding.of("pageSize", pageSize), - if (globalRoles != null) Binding.of("roles", globalRoles) else null, - ) - + val offset = (page - 1) * pageSize val conditions = - listOfNotNull( - if (searchTerm.isNotBlank()) freeTextQuery else null, - if (hideDeactivated) "employee.active = TRUE" else null, - if (globalRoles != null) "employee.roles && :roles" else null, + Predicate.allNotNull( + if (searchTerm.isNotBlank()) employeeFreeTextSearchPredicate(searchTerm) else null, + if (hideDeactivated) Predicate { where("$it.active = TRUE") } else null, + if (globalRoles != null) Predicate { where("$it.roles && ${bind(globalRoles)}") } + else null, ) - val whereClause = conditions.takeIf { it.isNotEmpty() }?.joinToString(" AND ") ?: "TRUE" - - val sql = - """ + return tx.createQuery { + sql( + """ SELECT employee.id, employee.external_id, @@ -444,15 +428,12 @@ SELECT count(*) OVER () AS count FROM employee LEFT JOIN daycare temp_unit ON temp_unit.id = employee.temporary_in_unit_id -WHERE $whereClause +WHERE ${predicate(conditions.forTable("employee"))} ORDER BY last_name, first_name DESC -LIMIT :pageSize OFFSET :offset - """ - .trimIndent() - @Suppress("DEPRECATION") - return tx.createQuery(sql) - .addBindings(params) - .addBindings(freeTextParams) +LIMIT ${bind(pageSize)} OFFSET ${bind(offset)} +""" + ) + } .mapToPaged(::PagedEmployeesWithDaycareRoles, pageSize) } diff --git a/service/src/main/kotlin/fi/espoo/evaka/shared/db/Search.kt b/service/src/main/kotlin/fi/espoo/evaka/shared/db/Search.kt index 4220a919be..e7d6971bb4 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/shared/db/Search.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/shared/db/Search.kt @@ -100,24 +100,20 @@ fun freeTextSearchQuery(tables: List, searchText: String): DBQuery { return DBQuery(wholeQuery, allParams) } -fun freeTextSearchQueryForColumns( - tables: List, - columns: List, - searchText: String, -): DBQuery { - val query = - listOfNotNull( - "true", - freeTextQuery(tables, freeTextParamName, columns).takeIf { searchText.isNotBlank() }, - ) - .joinToString(" AND ") - val params = - listOfNotNull( - (Binding.of(freeTextParamName, freeTextParamsToTsQuery(searchText))).takeIf { - searchText.isNotBlank() +fun employeeFreeTextSearchPredicate(searchText: String): Predicate { + val nameColumns = listOf("first_name", "last_name") + return searchText + .takeIf { it.isNotBlank() } + ?.let(::freeTextParamsToTsQuery) + ?.let { tsQuery -> + Predicate { + val tsVector = + nameColumns.joinToString(" || ") { column -> + "to_tsvector('simple', coalesce(unaccent($it.$column), ''))" + } + where("($tsVector) @@ to_tsquery('simple', ${bind(tsQuery)})") } - ) - return DBQuery(query, params) + } ?: Predicate.alwaysTrue() } fun disjointNumberQuery( @@ -136,25 +132,6 @@ fun disjointNumberQuery( return numberParamQuery to numberParams } -private val freeTextSearchColumns = - listOf("first_name", "last_name", "street_address", "postal_code") - -private fun freeTextQuery( - tables: List, - param: String, - columns: List = freeTextSearchColumns, -): String { - val tsVector = - tables - .flatMap { table -> columns.map { column -> "$table.$column" } } - .map { column -> "to_tsvector('simple', coalesce(unaccent($column), ''))" } - .joinToString("\n|| ", "(", ")") - - val tsQuery = "to_tsquery('simple', :$param)" - - return "($tsVector @@ $tsQuery)" -} - private fun freeTextComputedColumnQuery(tables: List, param: String): String { return "(" + tables.joinToString(" OR ") { table -> From 668df89571b06622aa887fed9f1fefe624afd2a8 Mon Sep 17 00:00:00 2001 From: Joonas Javanainen Date: Thu, 30 Jan 2025 15:34:51 +0200 Subject: [PATCH 09/10] Support searching by employee ssn --- .../kotlin/fi/espoo/evaka/shared/db/Search.kt | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/service/src/main/kotlin/fi/espoo/evaka/shared/db/Search.kt b/service/src/main/kotlin/fi/espoo/evaka/shared/db/Search.kt index e7d6971bb4..92351b1446 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/shared/db/Search.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/shared/db/Search.kt @@ -102,18 +102,27 @@ fun freeTextSearchQuery(tables: List, searchText: String): DBQuery { fun employeeFreeTextSearchPredicate(searchText: String): Predicate { val nameColumns = listOf("first_name", "last_name") - return searchText - .takeIf { it.isNotBlank() } - ?.let(::freeTextParamsToTsQuery) - ?.let { tsQuery -> - Predicate { - val tsVector = - nameColumns.joinToString(" || ") { column -> - "to_tsvector('simple', coalesce(unaccent($it.$column), ''))" - } - where("($tsVector) @@ to_tsquery('simple', ${bind(tsQuery)})") + val ssnPredicate = + Predicate.all( + findSsnParams(searchText).map { ssn -> + Predicate { where("lower($it.social_security_number) = lower(${bind(ssn)})") } } - } ?: Predicate.alwaysTrue() + ) + val freeTextPredicate = + searchText + .let(removeSsnParams) + .takeIf { it.isNotBlank() } + ?.let(::freeTextParamsToTsQuery) + ?.let { tsQuery -> + Predicate { + val tsVector = + nameColumns.joinToString(" || ") { column -> + "to_tsvector('simple', coalesce(unaccent($it.$column), ''))" + } + where("($tsVector) @@ to_tsquery('simple', ${bind(tsQuery)})") + } + } + return Predicate.allNotNull(ssnPredicate, freeTextPredicate) } fun disjointNumberQuery( From 88fd3ecf7b7e6f2c0e27b04466a207d4c7e41946 Mon Sep 17 00:00:00 2001 From: Joonas Javanainen Date: Thu, 30 Jan 2025 15:36:16 +0200 Subject: [PATCH 10/10] Add missing table column "Deaktivoi" buttons are missing their Th element --- .../src/employee-frontend/components/employees/EmployeeList.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/employee-frontend/components/employees/EmployeeList.tsx b/frontend/src/employee-frontend/components/employees/EmployeeList.tsx index 74d4c537b4..b966ed42ea 100644 --- a/frontend/src/employee-frontend/components/employees/EmployeeList.tsx +++ b/frontend/src/employee-frontend/components/employees/EmployeeList.tsx @@ -157,6 +157,7 @@ export function EmployeeList({ employees }: Props) { {i18n.employees.name} {i18n.employees.rights} {i18n.employees.lastLogin} + {rows}