From b87b37f7c5ab228834fa62673923a07115a91bc2 Mon Sep 17 00:00:00 2001 From: Tero Laakso Date: Fri, 24 Jan 2025 16:59:08 +0200 Subject: [PATCH 01/11] Allow starting childs as possible recipients --- .../components/messages/MessageEditor.tsx | 12 +++- .../generated/api-clients/messaging.ts | 3 +- .../generated/api-clients/messaging.ts | 3 +- .../messages/MessageEditor.tsx | 7 ++- .../src/lib-common/api-types/messaging.ts | 60 +++++++++++++++++-- .../generated/api-types/messaging.ts | 9 +++ .../lib-components/messages/SelectorNode.ts | 45 +++++++++----- .../employee-mobile-frontend/i18n/fi.ts | 1 + .../defaults/employee/i18n/fi.tsx | 3 +- .../main/kotlin/evaka/codegen/api/Config.kt | 6 +- .../MessageReceiversIntegrationTest.kt | 8 +++ .../fi/espoo/evaka/messaging/Message.kt | 10 +++- .../espoo/evaka/messaging/MessageQueries.kt | 40 +++++++++++-- 13 files changed, 175 insertions(+), 32 deletions(-) diff --git a/frontend/src/employee-frontend/components/messages/MessageEditor.tsx b/frontend/src/employee-frontend/components/messages/MessageEditor.tsx index 69464ab75e2..ee54bddcd50 100644 --- a/frontend/src/employee-frontend/components/messages/MessageEditor.tsx +++ b/frontend/src/employee-frontend/components/messages/MessageEditor.tsx @@ -216,6 +216,7 @@ export default React.memo(function MessageEditor({ receiversAsSelectorNode( defaultSender.value, availableReceivers, + i18n.messages.receiverSelection.starters, draftContent?.recipientIds ) ) @@ -312,13 +313,20 @@ export default React.memo(function MessageEditor({ const accountReceivers = receiversAsSelectorNode( sender.value, - availableReceivers + availableReceivers, + i18n.messages.receiverSelection.starters ) if (accountReceivers) { setReceiverTree(accountReceivers) } }, - [availableReceivers, message.type, selectedReceivers, updateMessage] + [ + availableReceivers, + i18n.messages.receiverSelection.starters, + message.type, + selectedReceivers, + updateMessage + ] ) const handleRecipientChange = useCallback( (recipients: SelectorNode[]) => { diff --git a/frontend/src/employee-frontend/generated/api-clients/messaging.ts b/frontend/src/employee-frontend/generated/api-clients/messaging.ts index 0e08236ce5b..344319a1c67 100644 --- a/frontend/src/employee-frontend/generated/api-clients/messaging.ts +++ b/frontend/src/employee-frontend/generated/api-clients/messaging.ts @@ -30,6 +30,7 @@ import { UpdatableDraftContent } from 'lib-common/generated/api-types/messaging' import { client } from '../../api/client' import { createUrlSearchParams } from 'lib-common/api' import { deserializeJsonDraftContent } from 'lib-common/generated/api-types/messaging' +import { deserializeJsonMessageReceiversResponse } from 'lib-common/generated/api-types/messaging' import { deserializeJsonMessageThread } from 'lib-common/generated/api-types/messaging' import { deserializeJsonPagedMessageCopies } from 'lib-common/generated/api-types/messaging' import { deserializeJsonPagedMessageThreads } from 'lib-common/generated/api-types/messaging' @@ -208,7 +209,7 @@ export async function getReceiversForNewMessage(): Promise deserializeJsonMessageReceiversResponse(e)) } diff --git a/frontend/src/employee-mobile-frontend/generated/api-clients/messaging.ts b/frontend/src/employee-mobile-frontend/generated/api-clients/messaging.ts index a186d412e1b..bf8a14bbe7c 100644 --- a/frontend/src/employee-mobile-frontend/generated/api-clients/messaging.ts +++ b/frontend/src/employee-mobile-frontend/generated/api-clients/messaging.ts @@ -28,6 +28,7 @@ import { UpdatableDraftContent } from 'lib-common/generated/api-types/messaging' import { client } from '../../client' import { createUrlSearchParams } from 'lib-common/api' import { deserializeJsonDraftContent } from 'lib-common/generated/api-types/messaging' +import { deserializeJsonMessageReceiversResponse } from 'lib-common/generated/api-types/messaging' import { deserializeJsonMessageThread } from 'lib-common/generated/api-types/messaging' import { deserializeJsonPagedMessageThreads } from 'lib-common/generated/api-types/messaging' import { deserializeJsonPagedSentMessages } from 'lib-common/generated/api-types/messaging' @@ -149,7 +150,7 @@ export async function getReceiversForNewMessage(): Promise deserializeJsonMessageReceiversResponse(e)) } diff --git a/frontend/src/employee-mobile-frontend/messages/MessageEditor.tsx b/frontend/src/employee-mobile-frontend/messages/MessageEditor.tsx index 8d370fd9a0a..d308b2c1834 100644 --- a/frontend/src/employee-mobile-frontend/messages/MessageEditor.tsx +++ b/frontend/src/employee-mobile-frontend/messages/MessageEditor.tsx @@ -149,6 +149,7 @@ export default React.memo(function MessageEditor({ recipients: receiversAsSelectorNode( accountId, availableRecipients, + i18n.messages.messageEditor.starters, draft.recipientIds ), urgent: draft.urgent, @@ -156,7 +157,11 @@ export default React.memo(function MessageEditor({ content: draft.content } : { - recipients: receiversAsSelectorNode(accountId, availableRecipients), + recipients: receiversAsSelectorNode( + accountId, + availableRecipients, + i18n.messages.messageEditor.starters + ), urgent: false, title: '', content: '' diff --git a/frontend/src/lib-common/api-types/messaging.ts b/frontend/src/lib-common/api-types/messaging.ts index 88c970bf50f..909ef625020 100644 --- a/frontend/src/lib-common/api-types/messaging.ts +++ b/frontend/src/lib-common/api-types/messaging.ts @@ -2,12 +2,15 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later +import { JsonOf } from 'lib-common/json' +import LocalDate from 'lib-common/local-date' import { UUID } from 'lib-common/types' import { MessageRecipientType } from '../generated/api-types/messaging' export type MessageReceiver = - | MessageReceiverBase // unit in area, or child, or citizen + | MessageReceiverBase // unit in area, or citizen + | MessageReceiverChild | MessageReceiverArea | MessageReceiverUnit | MessageReceiverGroup @@ -18,6 +21,10 @@ interface MessageReceiverBase { type: MessageRecipientType } +interface MessageReceiverChild extends MessageReceiverBase { + startDate: LocalDate | null +} + interface MessageReceiverArea extends MessageReceiverBase { receivers: MessageReceiverUnitInArea[] } @@ -26,10 +33,12 @@ type MessageReceiverUnitInArea = MessageReceiverBase interface MessageReceiverUnit extends MessageReceiverBase { receivers: MessageReceiverGroup[] + hasStarters: boolean } interface MessageReceiverGroup extends MessageReceiverBase { receivers: MessageReceiverBase[] + hasStarters: boolean } export const sortReceivers = ( @@ -47,6 +56,49 @@ export const sortReceivers = ( } : receiver ) - .sort((a, b) => - a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase()) - ) + .sort((a, b) => { + const aStarter = messageReceiverIsStarter(a) ? 1 : 0 + const bStarter = messageReceiverIsStarter(b) ? 1 : 0 + + if (aStarter !== bStarter) { + return aStarter - bStarter + } + const aDate = messageReceiverStartDate(a) + const bDate = messageReceiverStartDate(b) + + if (aDate && bDate) { + return aDate.compareTo(bDate) + } + return a.name + .toLocaleLowerCase() + .localeCompare(b.name.toLocaleLowerCase()) + }) + +export function messageReceiverStartDate( + receiver: MessageReceiver +): LocalDate | null { + return 'startDate' in receiver ? receiver.startDate : null +} + +export function messageReceiverIsStarter(receiver: MessageReceiver): boolean { + return ( + ('hasStarters' in receiver && receiver.hasStarters) || + messageReceiverStartDate(receiver) !== null + ) +} + +export function deserializeMessageReceiver( + json: JsonOf +): MessageReceiver { + const result: MessageReceiver = { + ...json, + ...('startDate' in json && { + startDate: + json.startDate !== null ? LocalDate.parseIso(json.startDate) : null + }), + ...('receivers' in json && { + receivers: json.receivers.map(deserializeMessageReceiver) + }) + } + return result +} diff --git a/frontend/src/lib-common/generated/api-types/messaging.ts b/frontend/src/lib-common/generated/api-types/messaging.ts index 66a5eaa233a..0411abf87eb 100644 --- a/frontend/src/lib-common/generated/api-types/messaging.ts +++ b/frontend/src/lib-common/generated/api-types/messaging.ts @@ -20,6 +20,7 @@ import { MessageId } from './shared' import { MessageReceiver } from '../../api-types/messaging' import { MessageThreadId } from './shared' import { PersonId } from './shared' +import { deserializeMessageReceiver } from '../../api-types/messaging' /** * Generated from fi.espoo.evaka.messaging.AccountType @@ -471,6 +472,14 @@ export function deserializeJsonMessageCopy(json: JsonOf): MessageCo } +export function deserializeJsonMessageReceiversResponse(json: JsonOf): MessageReceiversResponse { + return { + ...json, + receivers: json.receivers.map(e => deserializeMessageReceiver(e)) + } +} + + export function deserializeJsonMessageThread(json: JsonOf): MessageThread { return { ...json, diff --git a/frontend/src/lib-components/messages/SelectorNode.ts b/frontend/src/lib-components/messages/SelectorNode.ts index 734a615abd9..81de068b90a 100644 --- a/frontend/src/lib-components/messages/SelectorNode.ts +++ b/frontend/src/lib-components/messages/SelectorNode.ts @@ -2,7 +2,11 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -import { MessageReceiver } from 'lib-common/api-types/messaging' +import { + MessageReceiver, + messageReceiverIsStarter, + messageReceiverStartDate +} from 'lib-common/api-types/messaging' import { MessageReceiversResponse, MessageRecipient @@ -27,6 +31,7 @@ export interface SelectorNode extends TreeNode { export const receiversAsSelectorNode = ( accountId: UUID, receivers: MessageReceiversResponse[], + starterTranslation: string, checkedIds: UUID[] = [] ): SelectorNode[] => { const accountReceivers = receivers.find( @@ -36,9 +41,9 @@ export const receiversAsSelectorNode = ( if (!accountReceivers) { return [] } - - const selectorNodes = accountReceivers.map(receiverAsSelectorNode) - + const selectorNodes = accountReceivers.map((r) => + receiverAsSelectorNode(r, starterTranslation) + ) if (selectorNodes.length === 1 && selectorNodes[0].children.length === 0) { return selectorNodes.map((node) => ({ ...node, checked: true })) } @@ -67,16 +72,28 @@ function checkAll(node: SelectorNode): SelectorNode { return { ...node, checked: true, children: node.children.map(checkAll) } } -const receiverAsSelectorNode = (receiver: MessageReceiver): SelectorNode => ({ - key: receiver.id, - checked: false, - text: receiver.name, - messageRecipient: { type: receiver.type, id: receiver.id }, - children: - 'receivers' in receiver - ? receiver.receivers.map(receiverAsSelectorNode) - : [] -}) +function receiverAsSelectorNode( + receiver: MessageReceiver, + starterTranslation: string +): SelectorNode { + const startDate = messageReceiverStartDate(receiver) + const isStarter = messageReceiverIsStarter(receiver) + const nameWithStarterIndication = isStarter + ? `${receiver.name} (${startDate?.format() ?? starterTranslation})` + : receiver.name + return { + key: `${receiver.id}-${isStarter}`, + checked: false, + text: nameWithStarterIndication, + messageRecipient: { type: receiver.type, id: receiver.id }, + children: + 'receivers' in receiver + ? receiver.receivers.map((r) => + receiverAsSelectorNode(r, starterTranslation) + ) + : [] + } +} export type SelectedNode = { key: UUID diff --git a/frontend/src/lib-customizations/defaults/employee-mobile-frontend/i18n/fi.ts b/frontend/src/lib-customizations/defaults/employee-mobile-frontend/i18n/fi.ts index cf2a8a4fdce..2fc35f3949f 100644 --- a/frontend/src/lib-customizations/defaults/employee-mobile-frontend/i18n/fi.ts +++ b/frontend/src/lib-customizations/defaults/employee-mobile-frontend/i18n/fi.ts @@ -445,6 +445,7 @@ export const fi = { sender: 'Lähettäjä', receivers: 'Vastaanottajat', recipientsPlaceholder: 'Valitse...', + starters: 'aloittavat', subject: { heading: 'Otsikko', placeholder: 'Kirjoita...' diff --git a/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx b/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx index 75783e9e492..e8c11a6ffed 100755 --- a/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx +++ b/frontend/src/lib-customizations/defaults/employee/i18n/fi.tsx @@ -4607,7 +4607,8 @@ export const fi = { childName: 'Nimi', childDob: 'Syntymäaika', receivers: 'Vastaanottajat', - confirmText: 'Lähetä viesti valituille' + confirmText: 'Lähetä viesti valituille', + starters: 'aloittavat' }, noTitle: 'Ei otsikkoa', notSent: 'Ei lähetetty', diff --git a/service/codegen/src/main/kotlin/evaka/codegen/api/Config.kt b/service/codegen/src/main/kotlin/evaka/codegen/api/Config.kt index 0d17e419752..3de48484ed4 100644 --- a/service/codegen/src/main/kotlin/evaka/codegen/api/Config.kt +++ b/service/codegen/src/main/kotlin/evaka/codegen/api/Config.kt @@ -61,6 +61,8 @@ object Imports { val vasuQuestion = TsImport.Named(LibCommon / "api-types/vasu.ts", "VasuQuestion") val mapVasuQuestion = TsImport.Named(LibCommon / "api-types/vasu.ts", "mapVasuQuestion") val messageReceiver = TsImport.Named(LibCommon / "api-types/messaging.ts", "MessageReceiver") + val deserializeMessageReceiver = + TsImport.Named(LibCommon / "api-types/messaging.ts", "deserializeMessageReceiver") val uuid = TsImport.Named(LibCommon / "types.d.ts", "UUID") val action = TsImport.Named(LibCommon / "generated/action.ts", "Action") val jsonOf = TsImport.Named(LibCommon / "json.d.ts", "JsonOf") @@ -184,7 +186,9 @@ val defaultMetadata = TsExternalTypeRef( "MessageReceiver", keyRepresentation = null, - deserializeJson = null, + deserializeJson = { json -> + TsCode { "${ref(Imports.deserializeMessageReceiver)}(${inline(json)})" } + }, serializePathVariable = null, serializeRequestParam = null, Imports.messageReceiver, diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/messaging/MessageReceiversIntegrationTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/messaging/MessageReceiversIntegrationTest.kt index e371876cb06..b35c45e04f1 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/messaging/MessageReceiversIntegrationTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/messaging/MessageReceiversIntegrationTest.kt @@ -128,17 +128,20 @@ class MessageReceiversIntegrationTest : FullApplicationTest(resetDbBeforeEach = MessageReceiver.Unit( id = daycare1.id, name = daycare1.name, + hasStarters = false, receivers = listOf( MessageReceiver.Group( id = group1.id, name = group1.name, + hasStarters = false, receivers = listOf( MessageReceiver.Child( id = child1.id, name = "${child1.lastName} ${child2.firstName}", + startDate = null, ) ), ) @@ -153,11 +156,13 @@ class MessageReceiversIntegrationTest : FullApplicationTest(resetDbBeforeEach = MessageReceiver.Group( id = group1.id, name = group1.name, + hasStarters = false, receivers = listOf( MessageReceiver.Child( id = child1.id, name = "${child1.lastName} ${child1.firstName}", + startDate = null, ) ), ) @@ -182,17 +187,20 @@ class MessageReceiversIntegrationTest : FullApplicationTest(resetDbBeforeEach = MessageReceiver.Unit( id = daycare2.id, name = daycare2.name, + hasStarters = false, receivers = listOf( MessageReceiver.Group( id = group2.id, name = group2.name, + hasStarters = false, receivers = listOf( MessageReceiver.Child( id = child2.id, name = "${child2.lastName} ${child2.firstName}", + startDate = null, ) ), ) diff --git a/service/src/main/kotlin/fi/espoo/evaka/messaging/Message.kt b/service/src/main/kotlin/fi/espoo/evaka/messaging/Message.kt index 4950013de89..7b3080c9d2d 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/messaging/Message.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/messaging/Message.kt @@ -22,6 +22,7 @@ import fi.espoo.evaka.shared.config.SealedSubclassSimpleName import fi.espoo.evaka.shared.db.DatabaseEnum import fi.espoo.evaka.shared.domain.FiniteDateRange import fi.espoo.evaka.shared.domain.HelsinkiDateTime +import java.time.LocalDate import org.jdbi.v3.core.mapper.Nested import org.jdbi.v3.core.mapper.PropagateNull import org.jdbi.v3.json.Json @@ -156,16 +157,21 @@ sealed class MessageReceiver(val type: MessageRecipientType) { override val id: DaycareId, override val name: String, val receivers: List, + val hasStarters: Boolean, ) : MessageReceiver(MessageRecipientType.UNIT) data class Group( override val id: GroupId, override val name: String, val receivers: List, + val hasStarters: Boolean, ) : MessageReceiver(MessageRecipientType.GROUP) - data class Child(override val id: ChildId, override val name: String) : - MessageReceiver(MessageRecipientType.CHILD) + data class Child( + override val id: ChildId, + override val name: String, + val startDate: LocalDate?, + ) : MessageReceiver(MessageRecipientType.CHILD) data class Citizen(override val id: PersonId, override val name: String) : MessageReceiver(MessageRecipientType.CITIZEN) diff --git a/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt index 214bdfc71c4..1698309cbbc 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt @@ -1108,6 +1108,7 @@ data class UnitMessageReceiversResult( val childId: ChildId, val firstName: String, val lastName: String, + val startDate: LocalDate?, ) data class MunicipalMessageReceiversResult( @@ -1131,8 +1132,13 @@ fun Database.Read.getReceiversForNewMessage( WHERE ${predicate(idFilter.forTable("message_account"))} AND type = ANY('{PERSONAL,GROUP}'::message_account_type[]) + ), starting_children as( + SELECT p.child_id, p.unit_id, dgp.daycare_group_id AS group_id, COALESCE(dgp.start_date, p.start_date) AS start_date + FROM placement p + LEFT JOIN daycare_group_placement dgp ON p.id = dgp.daycare_placement_id + WHERE p.start_date > ${bind(today)} OR dgp.start_date > ${bind(today)} ), children AS ( - SELECT a.id AS account_id, p.child_id, NULL AS unit_id, NULL AS unit_name, p.group_id, g.name AS group_name + SELECT a.id AS account_id, p.child_id, NULL AS unit_id, NULL AS unit_name, p.group_id, g.name AS group_name, NULL::date AS start_date FROM accounts a JOIN realized_placement_all(${bind(today)}) p ON a.daycare_group_id = p.group_id JOIN daycare d ON p.unit_id = d.id @@ -1141,13 +1147,32 @@ fun Database.Read.getReceiversForNewMessage( UNION ALL - SELECT a.id AS account_id, p.child_id, p.unit_id, d.name AS unit_name, p.group_id, g.name AS group_name + SELECT a.id AS account_id, p.child_id, p.unit_id, d.name AS unit_name, p.group_id, g.name AS group_name, NULL::date AS start_date FROM accounts a JOIN daycare_acl_view acl ON a.employee_id = acl.employee_id JOIN daycare d ON acl.daycare_id = d.id JOIN daycare_group g ON d.id = g.daycare_id JOIN realized_placement_all(${bind(today)}) p ON g.id = p.group_id WHERE 'MESSAGING' = ANY(d.enabled_pilot_features) + + UNION ALL + + SELECT a.id AS account_id, p.child_id, NULL AS unit_id, NULL AS unit_name, p.group_id, g.name AS group_name, p.start_date + FROM accounts a + JOIN starting_children p ON a.daycare_group_id = p.group_id + JOIN daycare d ON p.unit_id = d.id + JOIN daycare_group g ON p.group_id = g.id + WHERE 'MESSAGING' = ANY(d.enabled_pilot_features) + + UNION ALL + + SELECT a.id AS account_id, p.child_id, p.unit_id, d.name AS unit_name, p.group_id, g.name AS group_name, p.start_date + FROM accounts a + JOIN daycare_acl_view acl ON a.employee_id = acl.employee_id + JOIN daycare d ON acl.daycare_id = d.id + JOIN starting_children p ON d.id = p.unit_id + LEFT JOIN daycare_group g ON p.group_id = g.id + WHERE 'MESSAGING' = ANY(d.enabled_pilot_features) ) SELECT DISTINCT c.account_id, @@ -1156,6 +1181,7 @@ fun Database.Read.getReceiversForNewMessage( c.group_id, c.group_name, c.child_id, + c.start_date, p.first_name, p.last_name FROM children c @@ -1177,8 +1203,9 @@ fun Database.Read.getReceiversForNewMessage( } .toList() .groupBy { it.accountId } - .map { (accountId, receivers) -> - val units = receivers.groupBy { it.unitId to it.unitName } + .map { (groupKey, receivers) -> + val units = + receivers.groupBy { Triple(it.unitId, it.unitName, it.startDate != null) } val accountReceivers = units.flatMap { (unit, groups) -> val (unitId, unitName) = unit @@ -1188,11 +1215,12 @@ fun Database.Read.getReceiversForNewMessage( MessageReceiver.Unit( id = unitId, name = unitName, + hasStarters = groups.any { it.startDate != null }, receivers = getReceiverGroups(groups), ) ) } - MessageReceiversResponse(accountId = accountId, receivers = accountReceivers) + MessageReceiversResponse(accountId = groupKey, receivers = accountReceivers) } val municipalReceivers = @@ -1248,11 +1276,13 @@ private fun getReceiverGroups( MessageReceiver.Group( id = groupId, name = groupName, + hasStarters = children.any { it.startDate != null }, receivers = children.map { MessageReceiver.Child( id = it.childId, name = formatName(it.firstName, it.lastName, true), + startDate = it.startDate, ) }, ) From 1fae22a3423fc2efebcadd523d08ed164c2490bb Mon Sep 17 00:00:00 2001 From: Tero Laakso Date: Tue, 28 Jan 2025 12:22:57 +0200 Subject: [PATCH 02/11] Use recipient.id as receiver message account id --- .../components/messages/MessageEditor.tsx | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/frontend/src/employee-frontend/components/messages/MessageEditor.tsx b/frontend/src/employee-frontend/components/messages/MessageEditor.tsx index ee54bddcd50..7efd3699748 100644 --- a/frontend/src/employee-frontend/components/messages/MessageEditor.tsx +++ b/frontend/src/employee-frontend/components/messages/MessageEditor.tsx @@ -90,12 +90,12 @@ type Message = Omit< const messageToUpdatableDraftWithAccount = ( m: Message, - recipients: { key: string; text: string }[] + recipients: { id: string; text: string }[] ): Draft => ({ content: m.content, urgent: m.urgent, sensitive: false, - recipientIds: recipients.map(({ key }) => key), + recipientIds: recipients.map(({ id }) => id), recipientNames: recipients.map(({ text }) => text), title: m.title, type: m.type, @@ -252,7 +252,13 @@ export default React.memo(function MessageEditor({ setMessage(updatedMessage) const selectedReceivers = getSelected(receiverTree) setDraft( - messageToUpdatableDraftWithAccount(updatedMessage, selectedReceivers) + messageToUpdatableDraftWithAccount( + updatedMessage, + selectedReceivers.map((r) => ({ + id: r.messageRecipient.id, + text: r.text + })) + ) ) }, [message, receiverTree, setDraft] @@ -341,12 +347,17 @@ export default React.memo(function MessageEditor({ const updatedMessage = { ...message, - recipientIds: selected.map((s) => s.key), + recipientIds: selected.map((s) => s.messageRecipient.id), recipientNames: selected.map((s) => s.text), sensitive: shouldResetSensitivity ? false : message.sensitive } setMessage(updatedMessage) - setDraft(messageToUpdatableDraftWithAccount(updatedMessage, selected)) + setDraft( + messageToUpdatableDraftWithAccount( + updatedMessage, + selected.map((r) => ({ id: r.messageRecipient.id, text: r.text })) + ) + ) }, [message, senderAccountType, setDraft] ) From 8488a0625229ad35ef74f113c72128c8e3b9075c Mon Sep 17 00:00:00 2001 From: Tero Laakso Date: Tue, 28 Jan 2025 12:54:02 +0200 Subject: [PATCH 03/11] Fix receiver selection in tests --- .../pages/employee/messages/messages-page.ts | 6 +++--- .../src/e2e-test/specs/6_mobile/messages.spec.ts | 16 ++++++++++------ .../specs/7_messaging/messaging-by-staff.spec.ts | 14 +++++++------- .../e2e-test/specs/7_messaging/messaging.spec.ts | 2 +- .../7_messaging/municipal-messaging.spec.ts | 6 +++--- 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/frontend/src/e2e-test/pages/employee/messages/messages-page.ts b/frontend/src/e2e-test/pages/employee/messages/messages-page.ts index 7d5fae58ebe..6df12e639a5 100644 --- a/frontend/src/e2e-test/pages/employee/messages/messages-page.ts +++ b/frontend/src/e2e-test/pages/employee/messages/messages-page.ts @@ -219,7 +219,7 @@ export class MessageEditor extends Element { sensitive?: boolean attachmentCount?: number sender?: string - receivers?: string[] + receiverKeys?: string[] confirmManyRecipients?: boolean yearsOfBirth?: number[] shiftcare?: boolean @@ -234,10 +234,10 @@ export class MessageEditor extends Element { await this.senderSelection.fillAndSelectFirst(message.sender) } - if (message.receivers) { + if (message.receiverKeys) { await this.receiverSelection.open() await this.receiverSelection.expandAll() - for (const receiver of message.receivers) { + for (const receiver of message.receiverKeys) { await this.receiverSelection.option(receiver).check() } await this.receiverSelection.close() diff --git a/frontend/src/e2e-test/specs/6_mobile/messages.spec.ts b/frontend/src/e2e-test/specs/6_mobile/messages.spec.ts index 18008e36b21..371bd5cec53 100644 --- a/frontend/src/e2e-test/specs/6_mobile/messages.spec.ts +++ b/frontend/src/e2e-test/specs/6_mobile/messages.spec.ts @@ -382,9 +382,13 @@ describe('Messages page', () => { // The user has access to all groups, but only the one whose messages are viewed should be available in the // message editor await messageEditor.recipients.open() - await messageEditor.recipients.option(daycareGroup.id).click() - await messageEditor.recipients.option(daycareGroup2.id).waitUntilHidden() - await messageEditor.recipients.option(daycareGroup3.id).waitUntilHidden() + await messageEditor.recipients.option(`${daycareGroup.id}-false`).click() + await messageEditor.recipients + .option(`${daycareGroup2.id}-false`) + .waitUntilHidden() + await messageEditor.recipients + .option(`${daycareGroup3.id}-false`) + .waitUntilHidden() const message = { title: 'Otsikko', content: 'Testiviestin sisältö' } await messageEditor.fillMessage(message) @@ -401,7 +405,7 @@ describe('Messages page', () => { test('Employee sees sent messages', async () => { await staffStartsNewMessage() await messageEditor.recipients.open() - await messageEditor.recipients.option(daycareGroup.id).click() + await messageEditor.recipients.option(`${daycareGroup.id}-false`).click() const message = { title: 'Otsikko', content: 'Testiviestin sisältö' } await messageEditor.fillMessage(message) await messageEditor.send.click() @@ -419,7 +423,7 @@ describe('Messages page', () => { test('Employee sees a draft message and can send it', async () => { let messageEditor = await staffStartsNewMessage() await messageEditor.recipients.open() - await messageEditor.recipients.option(daycareGroup.id).click() + await messageEditor.recipients.option(`${daycareGroup.id}-false`).click() const message = { title: 'Otsikko', content: 'Testiviestin sisältö' } await messageEditor.fillMessage(message) await messageEditor.close.click() @@ -442,7 +446,7 @@ describe('Messages page', () => { test('Employee can discard a draft message', async () => { let messageEditor = await staffStartsNewMessage() await messageEditor.recipients.open() - await messageEditor.recipients.option(daycareGroup.id).click() + await messageEditor.recipients.option(`${daycareGroup.id}-false`).click() const message = { title: 'Otsikko', content: 'Testiviestin sisältö' } await messageEditor.fillMessage(message) await messageEditor.close.click() diff --git a/frontend/src/e2e-test/specs/7_messaging/messaging-by-staff.spec.ts b/frontend/src/e2e-test/specs/7_messaging/messaging-by-staff.spec.ts index 26e5f23f148..c4aae0efbd6 100644 --- a/frontend/src/e2e-test/specs/7_messaging/messaging-by-staff.spec.ts +++ b/frontend/src/e2e-test/specs/7_messaging/messaging-by-staff.spec.ts @@ -241,7 +241,7 @@ describe('Sending and receiving sensitive messages', () => { const sensitiveMessage = { ...defaultMessage, sensitive: true, - receivers: [testChild2.id] + receiverKeys: [`${testChild2.id}-false`] } await initStaffPage(mockedDateAt10) @@ -271,7 +271,7 @@ describe('Staff copies', () => { const message = { title: 'Ilmoitus', content: 'Ilmoituksen sisältö', - receivers: [testDaycare.id], + receiverKeys: [`${testDaycare.id}-false`], type: 'BULLETIN' as const } const messageEditor = await new MessagesPage( @@ -294,12 +294,12 @@ describe('Staff copies', () => { const message = { title: 'Ilmoitus', content: 'Ilmoituksen sisältö', - receivers: [testChild2.id] + receiverKeys: [`${testChild2.id}-false`] } const bulletin = { title: 'Ilmoitus', content: 'Ilmoituksen sisältö', - receivers: [testChild2.id], + receiverKeys: [`${testChild2.id}-false`], type: 'BULLETIN' as const } const messagesPage = new MessagesPage(unitSupervisorPage) @@ -321,7 +321,7 @@ describe('Staff copies', () => { title: 'Ilmoitus', content: 'Ilmoituksen sisältö', sender: `${testDaycare.name} - ${testDaycareGroup.name}`, - receivers: [testDaycareGroup.id] + receiverKeys: [`${testDaycareGroup.id}-false`] } const messageEditor = await new MessagesPage( unitSupervisorPage @@ -373,7 +373,7 @@ describe('Additional filters', () => { const message = { title: 'Ilmoitus rajatulle joukolle', content: 'Ilmoituksen sisältö rajatulle joukolle', - receivers: [testDaycare.id], + receiverKeys: [`${testDaycare.id}-false`], yearsOfBirth: [2014] } const messageEditor = await new MessagesPage( @@ -406,7 +406,7 @@ describe('Additional filters', () => { const message = { title: 'Ilmoitus rajatulle joukolle', content: 'Ilmoituksen sisältö rajatulle joukolle', - receivers: [testDaycare.id] + receiverKeys: [`${testDaycare.id}-false`] } let messageEditor = await new MessagesPage( unitSupervisorPage diff --git a/frontend/src/e2e-test/specs/7_messaging/messaging.spec.ts b/frontend/src/e2e-test/specs/7_messaging/messaging.spec.ts index 443e151024e..86af741cc9b 100644 --- a/frontend/src/e2e-test/specs/7_messaging/messaging.spec.ts +++ b/frontend/src/e2e-test/specs/7_messaging/messaging.spec.ts @@ -249,7 +249,7 @@ describe('Sending and receiving messages', () => { const messageEditor = await messagesPage.openMessageEditor() await messageEditor.sendNewMessage({ ...defaultMessage, - receivers: [testChild2.id] + receiverKeys: [`${testChild2.id}-false`] }) await runPendingAsyncJobs(mockedDateAt10.addMinutes(1)) diff --git a/frontend/src/e2e-test/specs/7_messaging/municipal-messaging.spec.ts b/frontend/src/e2e-test/specs/7_messaging/municipal-messaging.spec.ts index de0cb55cf59..54f662cfcd7 100644 --- a/frontend/src/e2e-test/specs/7_messaging/municipal-messaging.spec.ts +++ b/frontend/src/e2e-test/specs/7_messaging/municipal-messaging.spec.ts @@ -153,7 +153,7 @@ describe('Municipal messaging -', () => { const messageEditor = await messagesPage.openMessageEditor() await messageEditor.sendNewMessage({ ...defaultMessage, - receivers: [testCareArea.id, testCareArea2.id], + receiverKeys: [`${testCareArea.id}-false`, `${testCareArea2.id}-false`], confirmManyRecipients: true }) await runPendingAsyncJobs(messageSendTime.addMinutes(1)) @@ -171,7 +171,7 @@ describe('Municipal messaging -', () => { const messageEditor = await messagesPage.openMessageEditor() await messageEditor.sendNewMessage({ ...defaultMessage, - receivers: [testCareArea.id] + receiverKeys: [`${testCareArea.id}-false`] }) const sentMessagesPage = await messagesPage.openSentMessages() @@ -188,7 +188,7 @@ describe('Municipal messaging -', () => { const messageEditor = await messagesPage.openMessageEditor() await messageEditor.sendNewMessage({ ...defaultMessage, - receivers: [testCareArea.id] + receiverKeys: [`${testCareArea.id}-false`] }) await runPendingAsyncJobs(messageSendTime.addMinutes(1)) From 78d243dbb0421796024cd25f3493c776776a946a Mon Sep 17 00:00:00 2001 From: Tero Laakso Date: Wed, 29 Jan 2025 17:05:42 +0200 Subject: [PATCH 04/11] Save starter info for message draft recipients --- .../e2e-test/specs/6_mobile/messages.spec.ts | 12 ++--- .../7_messaging/messaging-by-staff.spec.ts | 14 +++--- .../specs/7_messaging/messaging.spec.ts | 2 +- .../7_messaging/municipal-messaging.spec.ts | 6 +-- .../components/messages/MessageEditor.tsx | 40 +++++++---------- .../messages/MessageEditor.tsx | 8 +++- .../generated/api-types/messaging.ts | 12 ++++- .../lib-components/messages/SelectorNode.ts | 45 +++++++++++++++---- .../espoo/evaka/messaging/DraftQueriesTest.kt | 9 ++-- .../evaka/messaging/MessageIntegrationTest.kt | 6 +-- .../fi/espoo/evaka/messaging/DraftContent.kt | 6 ++- .../fi/espoo/evaka/messaging/DraftQueries.kt | 2 +- .../V495__draft_message_recipients.sql | 9 ++++ service/src/main/resources/migrations.txt | 1 + 14 files changed, 110 insertions(+), 62 deletions(-) create mode 100644 service/src/main/resources/db/migration/V495__draft_message_recipients.sql diff --git a/frontend/src/e2e-test/specs/6_mobile/messages.spec.ts b/frontend/src/e2e-test/specs/6_mobile/messages.spec.ts index 371bd5cec53..1f021d8072a 100644 --- a/frontend/src/e2e-test/specs/6_mobile/messages.spec.ts +++ b/frontend/src/e2e-test/specs/6_mobile/messages.spec.ts @@ -382,12 +382,12 @@ describe('Messages page', () => { // The user has access to all groups, but only the one whose messages are viewed should be available in the // message editor await messageEditor.recipients.open() - await messageEditor.recipients.option(`${daycareGroup.id}-false`).click() + await messageEditor.recipients.option(`${daycareGroup.id}+false`).click() await messageEditor.recipients - .option(`${daycareGroup2.id}-false`) + .option(`${daycareGroup2.id}+false`) .waitUntilHidden() await messageEditor.recipients - .option(`${daycareGroup3.id}-false`) + .option(`${daycareGroup3.id}+false`) .waitUntilHidden() const message = { title: 'Otsikko', content: 'Testiviestin sisältö' } @@ -405,7 +405,7 @@ describe('Messages page', () => { test('Employee sees sent messages', async () => { await staffStartsNewMessage() await messageEditor.recipients.open() - await messageEditor.recipients.option(`${daycareGroup.id}-false`).click() + await messageEditor.recipients.option(`${daycareGroup.id}+false`).click() const message = { title: 'Otsikko', content: 'Testiviestin sisältö' } await messageEditor.fillMessage(message) await messageEditor.send.click() @@ -423,7 +423,7 @@ describe('Messages page', () => { test('Employee sees a draft message and can send it', async () => { let messageEditor = await staffStartsNewMessage() await messageEditor.recipients.open() - await messageEditor.recipients.option(`${daycareGroup.id}-false`).click() + await messageEditor.recipients.option(`${daycareGroup.id}+false`).click() const message = { title: 'Otsikko', content: 'Testiviestin sisältö' } await messageEditor.fillMessage(message) await messageEditor.close.click() @@ -446,7 +446,7 @@ describe('Messages page', () => { test('Employee can discard a draft message', async () => { let messageEditor = await staffStartsNewMessage() await messageEditor.recipients.open() - await messageEditor.recipients.option(`${daycareGroup.id}-false`).click() + await messageEditor.recipients.option(`${daycareGroup.id}+false`).click() const message = { title: 'Otsikko', content: 'Testiviestin sisältö' } await messageEditor.fillMessage(message) await messageEditor.close.click() diff --git a/frontend/src/e2e-test/specs/7_messaging/messaging-by-staff.spec.ts b/frontend/src/e2e-test/specs/7_messaging/messaging-by-staff.spec.ts index c4aae0efbd6..397b5ebfa70 100644 --- a/frontend/src/e2e-test/specs/7_messaging/messaging-by-staff.spec.ts +++ b/frontend/src/e2e-test/specs/7_messaging/messaging-by-staff.spec.ts @@ -241,7 +241,7 @@ describe('Sending and receiving sensitive messages', () => { const sensitiveMessage = { ...defaultMessage, sensitive: true, - receiverKeys: [`${testChild2.id}-false`] + receiverKeys: [`${testChild2.id}+false`] } await initStaffPage(mockedDateAt10) @@ -271,7 +271,7 @@ describe('Staff copies', () => { const message = { title: 'Ilmoitus', content: 'Ilmoituksen sisältö', - receiverKeys: [`${testDaycare.id}-false`], + receiverKeys: [`${testDaycare.id}+false`], type: 'BULLETIN' as const } const messageEditor = await new MessagesPage( @@ -294,12 +294,12 @@ describe('Staff copies', () => { const message = { title: 'Ilmoitus', content: 'Ilmoituksen sisältö', - receiverKeys: [`${testChild2.id}-false`] + receiverKeys: [`${testChild2.id}+false`] } const bulletin = { title: 'Ilmoitus', content: 'Ilmoituksen sisältö', - receiverKeys: [`${testChild2.id}-false`], + receiverKeys: [`${testChild2.id}+false`], type: 'BULLETIN' as const } const messagesPage = new MessagesPage(unitSupervisorPage) @@ -321,7 +321,7 @@ describe('Staff copies', () => { title: 'Ilmoitus', content: 'Ilmoituksen sisältö', sender: `${testDaycare.name} - ${testDaycareGroup.name}`, - receiverKeys: [`${testDaycareGroup.id}-false`] + receiverKeys: [`${testDaycareGroup.id}+false`] } const messageEditor = await new MessagesPage( unitSupervisorPage @@ -373,7 +373,7 @@ describe('Additional filters', () => { const message = { title: 'Ilmoitus rajatulle joukolle', content: 'Ilmoituksen sisältö rajatulle joukolle', - receiverKeys: [`${testDaycare.id}-false`], + receiverKeys: [`${testDaycare.id}+false`], yearsOfBirth: [2014] } const messageEditor = await new MessagesPage( @@ -406,7 +406,7 @@ describe('Additional filters', () => { const message = { title: 'Ilmoitus rajatulle joukolle', content: 'Ilmoituksen sisältö rajatulle joukolle', - receiverKeys: [`${testDaycare.id}-false`] + receiverKeys: [`${testDaycare.id}+false`] } let messageEditor = await new MessagesPage( unitSupervisorPage diff --git a/frontend/src/e2e-test/specs/7_messaging/messaging.spec.ts b/frontend/src/e2e-test/specs/7_messaging/messaging.spec.ts index 86af741cc9b..6bbbd1a1cd8 100644 --- a/frontend/src/e2e-test/specs/7_messaging/messaging.spec.ts +++ b/frontend/src/e2e-test/specs/7_messaging/messaging.spec.ts @@ -249,7 +249,7 @@ describe('Sending and receiving messages', () => { const messageEditor = await messagesPage.openMessageEditor() await messageEditor.sendNewMessage({ ...defaultMessage, - receiverKeys: [`${testChild2.id}-false`] + receiverKeys: [`${testChild2.id}+false`] }) await runPendingAsyncJobs(mockedDateAt10.addMinutes(1)) diff --git a/frontend/src/e2e-test/specs/7_messaging/municipal-messaging.spec.ts b/frontend/src/e2e-test/specs/7_messaging/municipal-messaging.spec.ts index 54f662cfcd7..ba05334097a 100644 --- a/frontend/src/e2e-test/specs/7_messaging/municipal-messaging.spec.ts +++ b/frontend/src/e2e-test/specs/7_messaging/municipal-messaging.spec.ts @@ -153,7 +153,7 @@ describe('Municipal messaging -', () => { const messageEditor = await messagesPage.openMessageEditor() await messageEditor.sendNewMessage({ ...defaultMessage, - receiverKeys: [`${testCareArea.id}-false`, `${testCareArea2.id}-false`], + receiverKeys: [`${testCareArea.id}+false`, `${testCareArea2.id}+false`], confirmManyRecipients: true }) await runPendingAsyncJobs(messageSendTime.addMinutes(1)) @@ -171,7 +171,7 @@ describe('Municipal messaging -', () => { const messageEditor = await messagesPage.openMessageEditor() await messageEditor.sendNewMessage({ ...defaultMessage, - receiverKeys: [`${testCareArea.id}-false`] + receiverKeys: [`${testCareArea.id}+false`] }) const sentMessagesPage = await messagesPage.openSentMessages() @@ -188,7 +188,7 @@ describe('Municipal messaging -', () => { const messageEditor = await messagesPage.openMessageEditor() await messageEditor.sendNewMessage({ ...defaultMessage, - receiverKeys: [`${testCareArea.id}-false`] + receiverKeys: [`${testCareArea.id}+false`] }) await runPendingAsyncJobs(messageSendTime.addMinutes(1)) diff --git a/frontend/src/employee-frontend/components/messages/MessageEditor.tsx b/frontend/src/employee-frontend/components/messages/MessageEditor.tsx index 7efd3699748..3ff1ebd3e43 100644 --- a/frontend/src/employee-frontend/components/messages/MessageEditor.tsx +++ b/frontend/src/employee-frontend/components/messages/MessageEditor.tsx @@ -47,7 +47,8 @@ import { getSelected, receiversAsSelectorNode, SelectedNode, - SelectorNode + SelectorNode, + selectedNodeToReceiver } from 'lib-components/messages/SelectorNode' import { SaveDraftParams } from 'lib-components/messages/types' import { Draft, useDraft } from 'lib-components/messages/useDraft' @@ -80,22 +81,22 @@ import { useTranslation } from '../../state/i18n' import { createMessagePreflightCheckQuery } from './queries' -type Message = Omit< - UpdatableDraftContent, - 'recipientIds' | 'recipientNames' -> & { +type Message = Omit & { sender: SelectOption attachments: Attachment[] } const messageToUpdatableDraftWithAccount = ( m: Message, - recipients: { id: string; text: string }[] + recipients: { id: string; isStarter: boolean; text: string }[] ): Draft => ({ content: m.content, urgent: m.urgent, sensitive: false, - recipientIds: recipients.map(({ id }) => id), + recipients: recipients.map((r) => ({ + accountId: r.id, + starter: r.isStarter + })), recipientNames: recipients.map(({ text }) => text), title: m.title, type: m.type, @@ -217,7 +218,7 @@ export default React.memo(function MessageEditor({ defaultSender.value, availableReceivers, i18n.messages.receiverSelection.starters, - draftContent?.recipientIds + draftContent?.recipients ) ) const [filtersVisible, useFiltersVisible] = useBoolean(false) @@ -250,15 +251,11 @@ export default React.memo(function MessageEditor({ (changes) => { const updatedMessage = { ...message, ...changes } setMessage(updatedMessage) - const selectedReceivers = getSelected(receiverTree) + const selectedReceivers = getSelected(receiverTree).map( + selectedNodeToReceiver + ) setDraft( - messageToUpdatableDraftWithAccount( - updatedMessage, - selectedReceivers.map((r) => ({ - id: r.messageRecipient.id, - text: r.text - })) - ) + messageToUpdatableDraftWithAccount(updatedMessage, selectedReceivers) ) }, [message, receiverTree, setDraft] @@ -338,25 +335,20 @@ export default React.memo(function MessageEditor({ (recipients: SelectorNode[]) => { setReceiverTree(recipients) const selected = getSelected(recipients) - + const selectedAsReceivers = selected.map(selectedNodeToReceiver) const shouldResetSensitivity = !shouldSensitiveCheckboxBeEnabled( selected, message.type, senderAccountType ) - const updatedMessage = { + const updatedMessage: Message = { ...message, - recipientIds: selected.map((s) => s.messageRecipient.id), - recipientNames: selected.map((s) => s.text), sensitive: shouldResetSensitivity ? false : message.sensitive } setMessage(updatedMessage) setDraft( - messageToUpdatableDraftWithAccount( - updatedMessage, - selected.map((r) => ({ id: r.messageRecipient.id, text: r.text })) - ) + messageToUpdatableDraftWithAccount(updatedMessage, selectedAsReceivers) ) }, [message, senderAccountType, setDraft] diff --git a/frontend/src/employee-mobile-frontend/messages/MessageEditor.tsx b/frontend/src/employee-mobile-frontend/messages/MessageEditor.tsx index d308b2c1834..a0993b5f533 100644 --- a/frontend/src/employee-mobile-frontend/messages/MessageEditor.tsx +++ b/frontend/src/employee-mobile-frontend/messages/MessageEditor.tsx @@ -34,6 +34,7 @@ import { ContentArea } from 'lib-components/layout/Container' import { getSelected, receiversAsSelectorNode, + selectedNodeToReceiver, SelectorNode } from 'lib-components/messages/SelectorNode' import { ConfirmedMutation } from 'lib-components/molecules/ConfirmedMutation' @@ -101,7 +102,10 @@ const messageForm = mapped( }, draftContent: (): UpdatableDraftContent => ({ ...commonContent, - recipientIds: selectedRecipients.map((r) => r.messageRecipient.id), + recipients: selectedRecipients.map(selectedNodeToReceiver).map((r) => ({ + accountId: r.id, + starter: r.isStarter + })), recipientNames: selectedRecipients.map((r) => r.text) }) } @@ -150,7 +154,7 @@ export default React.memo(function MessageEditor({ accountId, availableRecipients, i18n.messages.messageEditor.starters, - draft.recipientIds + draft.recipients ), urgent: draft.urgent, title: draft.title, diff --git a/frontend/src/lib-common/generated/api-types/messaging.ts b/frontend/src/lib-common/generated/api-types/messaging.ts index 0411abf87eb..5a83cd54f46 100644 --- a/frontend/src/lib-common/generated/api-types/messaging.ts +++ b/frontend/src/lib-common/generated/api-types/messaging.ts @@ -111,8 +111,8 @@ export interface DraftContent { content: string createdAt: HelsinkiDateTime id: MessageDraftId - recipientIds: string[] recipientNames: string[] + recipients: SelectableRecipient[] sensitive: boolean title: string type: MessageType @@ -344,6 +344,14 @@ export interface ReplyToMessageBody { recipientAccountIds: MessageAccountId[] } +/** +* Generated from fi.espoo.evaka.messaging.SelectableRecipient +*/ +export interface SelectableRecipient { + accountId: string + starter: boolean +} + /** * Generated from fi.espoo.evaka.messaging.SentMessage */ @@ -398,8 +406,8 @@ export interface UnreadCountByAccountAndGroup { */ export interface UpdatableDraftContent { content: string - recipientIds: string[] recipientNames: string[] + recipients: SelectableRecipient[] sensitive: boolean title: string type: MessageType diff --git a/frontend/src/lib-components/messages/SelectorNode.ts b/frontend/src/lib-components/messages/SelectorNode.ts index 81de068b90a..e777874441f 100644 --- a/frontend/src/lib-components/messages/SelectorNode.ts +++ b/frontend/src/lib-components/messages/SelectorNode.ts @@ -9,7 +9,8 @@ import { } from 'lib-common/api-types/messaging' import { MessageReceiversResponse, - MessageRecipient + MessageRecipient, + SelectableRecipient } from 'lib-common/generated/api-types/messaging' import { UUID } from 'lib-common/types' import { TreeNode } from 'lib-components/atoms/dropdowns/TreeDropdown' @@ -32,7 +33,7 @@ export const receiversAsSelectorNode = ( accountId: UUID, receivers: MessageReceiversResponse[], starterTranslation: string, - checkedIds: UUID[] = [] + checkedRecipients: SelectableRecipient[] = [] ): SelectorNode[] => { const accountReceivers = receivers.find( (receiver) => receiver.accountId === accountId @@ -47,18 +48,25 @@ export const receiversAsSelectorNode = ( if (selectorNodes.length === 1 && selectorNodes[0].children.length === 0) { return selectorNodes.map((node) => ({ ...node, checked: true })) } - if (checkedIds.length > 0) { - return selectorNodes.map((node) => checkSelected(checkedIds, node)) + if (checkedRecipients.length > 0) { + return selectorNodes.map((node) => checkSelected(checkedRecipients, node)) } return selectorNodes } -function checkSelected(selectedIds: UUID[], node: SelectorNode): SelectorNode { - if (selectedIds.includes(node.key)) { +function checkSelected( + selectedRecipients: SelectableRecipient[], + node: SelectorNode +): SelectorNode { + if ( + selectedRecipients + .map((r) => receiverToKey(r.accountId, r.starter)) + .includes(node.key) + ) { return checkAll(node) } else { const children = node.children.map((child) => - checkSelected(selectedIds, child) + checkSelected(selectedRecipients, child) ) return { ...node, @@ -72,6 +80,27 @@ function checkAll(node: SelectorNode): SelectorNode { return { ...node, checked: true, children: node.children.map(checkAll) } } +function receiverToKey(accountId: UUID, isStarter: boolean): string { + return `${accountId}+${isStarter}` +} + +function keyToReceiver(key: string): { + accountId: UUID + isStarter: boolean +} { + const [accountId, isStarter] = key.split('+') + return { accountId, isStarter: isStarter === 'true' } +} + +export function selectedNodeToReceiver(node: SelectedNode) { + const r = keyToReceiver(node.key) + return { + id: r.accountId, + isStarter: r.isStarter, + text: node.text + } +} + function receiverAsSelectorNode( receiver: MessageReceiver, starterTranslation: string @@ -82,7 +111,7 @@ function receiverAsSelectorNode( ? `${receiver.name} (${startDate?.format() ?? starterTranslation})` : receiver.name return { - key: `${receiver.id}-${isStarter}`, + key: receiverToKey(receiver.id, isStarter), checked: false, text: nameWithStarterIndication, messageRecipient: { type: receiver.type, id: receiver.id }, diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/messaging/DraftQueriesTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/messaging/DraftQueriesTest.kt index d25edea52b0..abcf43eeae4 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/messaging/DraftQueriesTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/messaging/DraftQueriesTest.kt @@ -38,7 +38,10 @@ class DraftQueriesTest : PureJdbiTest(resetDbBeforeEach = true) { val title = "Hello" val type = MessageType.MESSAGE val recipients = - setOf(MessageAccountId(UUID.randomUUID()), MessageAccountId(UUID.randomUUID())) + setOf( + SelectableRecipient(MessageAccountId(UUID.randomUUID()), false), + SelectableRecipient(MessageAccountId(UUID.randomUUID()), false), + ) val recipientNames = listOf("Auringonkukat", "Hippiäiset") val id = db.transaction { it.initDraft(accountId) } @@ -48,7 +51,7 @@ class DraftQueriesTest : PureJdbiTest(resetDbBeforeEach = true) { type = type, title = title, content = content, - recipientIds = recipients, + recipients = recipients, recipientNames = recipientNames, urgent = false, sensitive = false, @@ -71,7 +74,7 @@ class DraftQueriesTest : PureJdbiTest(resetDbBeforeEach = true) { assertEquals(expected.title, actual.title) assertEquals(expected.type, actual.type) assertEquals(expected.content, actual.content) - assertEquals(expected.recipientIds, actual.recipientIds) + assertEquals(expected.recipients, actual.recipients) assertEquals(expected.recipientNames, actual.recipientNames) } } diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/messaging/MessageIntegrationTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/messaging/MessageIntegrationTest.kt index d456bae31cc..8d4fe43e5b6 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/messaging/MessageIntegrationTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/messaging/MessageIntegrationTest.kt @@ -1490,7 +1490,7 @@ class MessageIntegrationTest : FullApplicationTest(resetDbBeforeEach = true) { group1Account, "title", "content", - setOf(person2Account), + setOf(SelectableRecipient(person2Account, false)), emptyList(), clock.now().minusDays(updatedDaysAgo.toLong()), ) @@ -1872,7 +1872,7 @@ class MessageIntegrationTest : FullApplicationTest(resetDbBeforeEach = true) { accountId: MessageAccountId, title: String, content: String, - recipientIds: Set, + recipients: Set, recipientNames: List, now: HelsinkiDateTime, ) { @@ -1895,7 +1895,7 @@ class MessageIntegrationTest : FullApplicationTest(resetDbBeforeEach = true) { content, urgent = false, sensitive = false, - recipientIds = recipientIds, + recipients = recipients, recipientNames = recipientNames, ), ) diff --git a/service/src/main/kotlin/fi/espoo/evaka/messaging/DraftContent.kt b/service/src/main/kotlin/fi/espoo/evaka/messaging/DraftContent.kt index 02467d06a83..d667d3043fb 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/messaging/DraftContent.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/messaging/DraftContent.kt @@ -10,6 +10,8 @@ import fi.espoo.evaka.shared.MessageDraftId import fi.espoo.evaka.shared.domain.HelsinkiDateTime import org.jdbi.v3.json.Json +data class SelectableRecipient(val accountId: Id<*>, val starter: Boolean) + data class DraftContent( val id: MessageDraftId, val createdAt: HelsinkiDateTime, @@ -18,7 +20,7 @@ data class DraftContent( val content: String, val urgent: Boolean, val sensitive: Boolean, - val recipientIds: Set>, + @Json val recipients: Set, val recipientNames: List, @Json val attachments: List, ) @@ -29,6 +31,6 @@ data class UpdatableDraftContent( val content: String, val urgent: Boolean, val sensitive: Boolean, - val recipientIds: Set>, + val recipients: Set, val recipientNames: List, ) diff --git a/service/src/main/kotlin/fi/espoo/evaka/messaging/DraftQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/messaging/DraftQueries.kt index eb07410e882..91468b92c82 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/messaging/DraftQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/messaging/DraftQueries.kt @@ -77,7 +77,7 @@ SET urgent = ${bind(draft.urgent)}, sensitive = ${bind(draft.sensitive)}, type = ${bind(draft.type)}, - recipient_ids = ${bind(draft.recipientIds)}, + recipients = ${bindJson(draft.recipients)}, recipient_names = ${bind(draft.recipientNames)}, modified_at = ${bind(now)} WHERE id = ${bind(id)} diff --git a/service/src/main/resources/db/migration/V495__draft_message_recipients.sql b/service/src/main/resources/db/migration/V495__draft_message_recipients.sql new file mode 100644 index 00000000000..4679d78eac1 --- /dev/null +++ b/service/src/main/resources/db/migration/V495__draft_message_recipients.sql @@ -0,0 +1,9 @@ +ALTER TABLE message_draft + ADD COLUMN recipients jsonb DEFAULT '[]'::jsonb; + +UPDATE message_draft +SET recipients = (SELECT jsonb_agg(jsonb_build_object('accountId', id, 'starter', false)) + FROM (SELECT unnest(recipient_ids) AS id)); + +ALTER TABLE message_draft + DROP COLUMN recipient_ids; diff --git a/service/src/main/resources/migrations.txt b/service/src/main/resources/migrations.txt index bdb09b33adb..a1de03b000f 100644 --- a/service/src/main/resources/migrations.txt +++ b/service/src/main/resources/migrations.txt @@ -490,3 +490,4 @@ V491__password_blacklist.sql V492__finance_note.sql V493__acl_end_date.sql V494__daycare_acl_schedule.sql +V495__draft_message_recipients.sql From 630000e41780051e29262421f6dff34d3316cbec Mon Sep 17 00:00:00 2001 From: Tero Laakso Date: Fri, 31 Jan 2025 13:09:03 +0200 Subject: [PATCH 05/11] Choose recipient message accounts for starters --- .../generated/api-types/messaging.ts | 1 + .../lib-components/messages/SelectorNode.ts | 6 +- .../fi/espoo/evaka/messaging/Message.kt | 6 +- .../espoo/evaka/messaging/MessageQueries.kt | 78 +++++++++++++++---- 4 files changed, 74 insertions(+), 17 deletions(-) diff --git a/frontend/src/lib-common/generated/api-types/messaging.ts b/frontend/src/lib-common/generated/api-types/messaging.ts index 5a83cd54f46..8dc1b4302d5 100644 --- a/frontend/src/lib-common/generated/api-types/messaging.ts +++ b/frontend/src/lib-common/generated/api-types/messaging.ts @@ -215,6 +215,7 @@ export interface MessageReceiversResponse { */ export interface MessageRecipient { id: string + starter: boolean type: MessageRecipientType } diff --git a/frontend/src/lib-components/messages/SelectorNode.ts b/frontend/src/lib-components/messages/SelectorNode.ts index e777874441f..c83a434f5bf 100644 --- a/frontend/src/lib-components/messages/SelectorNode.ts +++ b/frontend/src/lib-components/messages/SelectorNode.ts @@ -114,7 +114,11 @@ function receiverAsSelectorNode( key: receiverToKey(receiver.id, isStarter), checked: false, text: nameWithStarterIndication, - messageRecipient: { type: receiver.type, id: receiver.id }, + messageRecipient: { + type: receiver.type, + id: receiver.id, + starter: isStarter + }, children: 'receivers' in receiver ? receiver.receivers.map((r) => diff --git a/service/src/main/kotlin/fi/espoo/evaka/messaging/Message.kt b/service/src/main/kotlin/fi/espoo/evaka/messaging/Message.kt index 7b3080c9d2d..e8f40f6743a 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/messaging/Message.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/messaging/Message.kt @@ -223,7 +223,11 @@ enum class MessageRecipientType { CITIZEN, } -data class MessageRecipient(val type: MessageRecipientType, val id: Id<*>) { +data class MessageRecipient( + val type: MessageRecipientType, + val id: Id<*>, + val starter: Boolean = false, +) { fun toAreaId(): AreaId? = if (type == MessageRecipientType.AREA) AreaId(id.raw) else null fun toUnitId(): DaycareId? = if (type == MessageRecipientType.UNIT) DaycareId(id.raw) else null diff --git a/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt index 1698309cbbc..6b91679f5a7 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt @@ -1103,8 +1103,8 @@ data class UnitMessageReceiversResult( val accountId: MessageAccountId, val unitId: DaycareId?, val unitName: String?, - val groupId: GroupId, - val groupName: String, + val groupId: GroupId?, + val groupName: String?, val childId: ChildId, val firstName: String, val lastName: String, @@ -1132,7 +1132,7 @@ fun Database.Read.getReceiversForNewMessage( WHERE ${predicate(idFilter.forTable("message_account"))} AND type = ANY('{PERSONAL,GROUP}'::message_account_type[]) - ), starting_children as( + ), starting_children AS ( SELECT p.child_id, p.unit_id, dgp.daycare_group_id AS group_id, COALESCE(dgp.start_date, p.start_date) AS start_date FROM placement p LEFT JOIN daycare_group_placement dgp ON p.id = dgp.daycare_placement_id @@ -1202,6 +1202,7 @@ fun Database.Read.getReceiversForNewMessage( ) } .toList() + .filter { it.groupId != null } .groupBy { it.accountId } .map { (groupKey, receivers) -> val units = @@ -1274,8 +1275,9 @@ private fun getReceiverGroups( .map { (group, children) -> val (groupId, groupName) = group MessageReceiver.Group( - id = groupId, - name = groupName, + id = groupId!!, // TODO: Groupless are filtered out. Fix this when children without + // group need to be available. + name = groupName!!, hasStarters = children.any { it.startDate != null }, receivers = children.map { @@ -1294,13 +1296,21 @@ fun Database.Read.getMessageAccountsForRecipients( filters: MessageController.PostMessageFilters?, date: LocalDate, ): List> { - val groupedRecipients = recipients.groupBy { it.type } - val areaRecipients = groupedRecipients[MessageRecipientType.AREA]?.map { it.id } ?: listOf() - val unitRecipients = groupedRecipients[MessageRecipientType.UNIT]?.map { it.id } ?: listOf() - val groupRecipients = groupedRecipients[MessageRecipientType.GROUP]?.map { it.id } ?: listOf() - val childRecipients = groupedRecipients[MessageRecipientType.CHILD]?.map { it.id } ?: listOf() - val citizenRecipients = - groupedRecipients[MessageRecipientType.CITIZEN]?.map { it.id } ?: listOf() + + fun getRecipientIdsByType( + recipients: List + ): Map>> { + val recipientMap = + MessageRecipientType.values().associateWith { emptyList>() }.toMutableMap() + recipients + .groupBy { it.type } + .forEach { (type, recipients) -> recipientMap[type] = recipients.map { it.id } } + return recipientMap.toMap() + } + + val recipientsByStartingStatus = recipients.groupBy { it.starter } + val starterRecipients = getRecipientIdsByType(recipientsByStartingStatus[true] ?: emptyList()) + val currentRecipients = getRecipientIdsByType(recipientsByStartingStatus[false] ?: emptyList()) val filterPredicates = PredicateSql.allNotNull( @@ -1328,13 +1338,16 @@ fun Database.Read.getMessageAccountsForRecipients( """ WITH sender AS ( SELECT type, daycare_group_id, employee_id FROM message_account WHERE id = ${bind(accountId)} -), children AS ( +), current_children AS ( SELECT DISTINCT pl.child_id FROM realized_placement_all(${bind(date)}) pl JOIN daycare d ON pl.unit_id = d.id LEFT JOIN person p ON p.id = pl.child_id LEFT JOIN service_need sn ON sn.placement_id = pl.placement_id AND daterange(sn.start_date, sn.end_date, '[]') @> ${bind(date)} - WHERE (d.care_area_id = ANY(${bind(areaRecipients)}) OR pl.unit_id = ANY(${bind(unitRecipients)}) OR pl.group_id = ANY(${bind(groupRecipients)}) OR pl.child_id = ANY(${bind(childRecipients)})) + WHERE (d.care_area_id = ANY(${bind(currentRecipients[MessageRecipientType.AREA])}) + OR pl.unit_id = ANY(${bind(currentRecipients[MessageRecipientType.UNIT])}) + OR pl.group_id = ANY(${bind(currentRecipients[MessageRecipientType.GROUP])}) + OR pl.child_id = ANY(${bind(currentRecipients[MessageRecipientType.CHILD])})) AND ${predicate(filterPredicates)} AND EXISTS ( SELECT 1 @@ -1363,6 +1376,41 @@ WITH sender AS ( WHERE type = 'MUNICIPAL' ) AND 'MESSAGING' = ANY(d.enabled_pilot_features) +), starting_children AS ( + SELECT DISTINCT pl.child_id + FROM placement pl + JOIN daycare d ON pl.unit_id = d.id + LEFT JOIN daycare_group_placement dgp ON pl.id = dgp.daycare_placement_id + LEFT JOIN person p ON p.id = pl.child_id + LEFT JOIN service_need sn ON false + WHERE (pl.start_date > ${bind(date)} OR dgp.start_date > ${bind(date)}) + AND (d.care_area_id = ANY(${bind(starterRecipients[MessageRecipientType.AREA])}) + OR pl.unit_id = ANY(${bind(starterRecipients[MessageRecipientType.UNIT])}) + OR dgp.daycare_group_id = ANY(${bind(starterRecipients[MessageRecipientType.GROUP])}) + OR pl.child_id = ANY(${bind(starterRecipients[MessageRecipientType.CHILD])})) + AND ${predicate(filterPredicates)} + AND EXISTS ( + SELECT 1 + FROM daycare_acl_view acl + WHERE acl.daycare_id = pl.unit_id AND acl.employee_id = (SELECT sender.employee_id FROM sender) + + UNION ALL + + SELECT 1 + FROM sender + WHERE dgp.daycare_group_id = sender.daycare_group_id + + UNION ALL + + SELECT 1 + FROM sender + WHERE type = 'MUNICIPAL' + ) + AND 'MESSAGING' = ANY(d.enabled_pilot_features) +), children AS ( + SELECT child_id FROM current_children + UNION + SELECT child_id FROM starting_children ) SELECT acc.id AS account_id, c.child_id FROM children c @@ -1381,7 +1429,7 @@ UNION SELECT acc.id AS account_id, NULL as child_id FROM person p JOIN message_account acc ON p.id = acc.person_id -WHERE p.id = ANY(${bind(citizenRecipients)}) +WHERE p.id = ANY(${bind(currentRecipients[MessageRecipientType.CITIZEN])}) """ ) } From f9369bd42cc85ab68e6d18ce1cc9ad4dcef38934 Mon Sep 17 00:00:00 2001 From: Tero Laakso Date: Fri, 31 Jan 2025 13:09:10 +0200 Subject: [PATCH 06/11] Add e2e test --- .../7_messaging/messaging-by-staff.spec.ts | 66 ++++++++++++++++++- frontend/src/e2e-test/utils/page.ts | 5 ++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/frontend/src/e2e-test/specs/7_messaging/messaging-by-staff.spec.ts b/frontend/src/e2e-test/specs/7_messaging/messaging-by-staff.spec.ts index 397b5ebfa70..b80f2f9b127 100644 --- a/frontend/src/e2e-test/specs/7_messaging/messaging-by-staff.spec.ts +++ b/frontend/src/e2e-test/specs/7_messaging/messaging-by-staff.spec.ts @@ -17,7 +17,8 @@ import { testChild, testChild2, testDaycare, - testDaycareGroup + testDaycareGroup, + testPreschool } from '../../dev-api/fixtures' import { createDaycareGroups, @@ -61,6 +62,7 @@ beforeEach(async () => { await resetServiceState() await Fixture.careArea(testCareArea).save() await Fixture.daycare(testDaycare).save() + await Fixture.daycare(testPreschool).save() await Fixture.family({ guardian: testAdult, children: [testChild, testChild2] @@ -78,7 +80,8 @@ beforeEach(async () => { .save() unitSupervisor = await Fixture.employee() - .unitSupervisor(testDaycare.id) + .withDaycareAcl(testDaycare.id, 'UNIT_SUPERVISOR') + .withDaycareAcl(testPreschool.id, 'UNIT_SUPERVISOR') .save() const unitId = testDaycare.id @@ -227,6 +230,65 @@ describe('Sending and receiving messages', () => { }) } ) + + test('Staff can select starters as receivers', async () => { + const daycarePlacementFixture1 = await Fixture.placement({ + childId, + unitId: testPreschool.id, + startDate: mockedDate.addYears(1).addDays(1), + endDate: mockedDate.addYears(2) + }).save() + const preschoolGroup = await Fixture.daycareGroup({ + daycareId: testPreschool.id, + name: 'Esiopetusryhmä' + }).save() + await Fixture.groupPlacement({ + daycarePlacementId: daycarePlacementFixture1.id, + daycareGroupId: preschoolGroup.id, + startDate: mockedDate.addYears(1).addDays(1), + endDate: mockedDate.addYears(2) + }).save() + + // Verify that available recipients contain current placements and starters + await initUnitSupervisorPage(mockedDateAt10) + await unitSupervisorPage.goto(`${config.employeeUrl}/messages`) + const messagesPage = new MessagesPage(unitSupervisorPage) + const messageEditor = await messagesPage.openMessageEditor() + const receiverSelector = messageEditor.receiverSelection + await receiverSelector.open() + await receiverSelector.expandAll() // open first level + await receiverSelector.expandAll() // open second level + const labels = await receiverSelector.labels.allTexts() + const starterChildLabel = `${testChild.lastName} ${testChild.firstName} (${mockedDate.addYears(1).addDays(1).format()})` + const expectedReceiverNames = [ + testDaycare.name, + testDaycareGroup.name, + `${testChild.lastName} ${testChild.firstName}`, + `${testChild2.lastName} ${testChild2.firstName}`, + `${testPreschool.name} (aloittavat)`, + `${preschoolGroup.name} (aloittavat)`, + starterChildLabel + ] + expect(labels.sort()).toEqual(expectedReceiverNames.sort()) + + // Send a message to a starter child + await receiverSelector.optionByLabel(starterChildLabel).click() + await messageEditor.inputTitle.fill('Aloittavalle otsikko') + await messageEditor.inputContent.fill('Sisältö') + await messageEditor.sendButton.click() + await messageEditor.waitUntilHidden() + + await runPendingAsyncJobs(mockedDateAt10.addMinutes(1)) + + // Verify that the message is received by the starter child + await initCitizenPage(mockedDateAt11) + await citizenPage.goto(config.enduserMessagesUrl) + const citizenMessagesPage = new CitizenMessagesPage(citizenPage) + await citizenMessagesPage.assertThreadContent({ + title: 'Aloittavalle otsikko', + content: 'Sisältö' + }) + }) }) describe('Sending and receiving sensitive messages', () => { diff --git a/frontend/src/e2e-test/utils/page.ts b/frontend/src/e2e-test/utils/page.ts index dadacd8e21c..f3b42af3c64 100644 --- a/frontend/src/e2e-test/utils/page.ts +++ b/frontend/src/e2e-test/utils/page.ts @@ -594,6 +594,7 @@ export class Modal extends Element { export class TreeDropdown extends Element { values = this.findByDataQa('selected-values').findAllByDataQa('value') + labels = this.findByDataQa('select-receiver-tree').findAll('label') private async expanded(): Promise { return ( @@ -623,6 +624,10 @@ export class TreeDropdown extends Element { return new Checkbox(this.findAll(`[data-qa*="tree-checkbox-"]`).nth(0)) } + optionByLabel(label: string): Element { + return this.labels.find(`text=${label}`) + } + async expandOption(key: string): Promise { await this.findByDataQa(`tree-toggle-${key}`).click() } From 70bedaac46eaeb2ebff46fce3c7f6bce4ff597d7 Mon Sep 17 00:00:00 2001 From: Tero Laakso Date: Wed, 5 Feb 2025 15:29:27 +0200 Subject: [PATCH 07/11] Allow citizen to reply whenever there's a message regarding future or current placements Sending new messages is allowed from 2 weeks before placement --- .../evaka/messaging/MessageQueriesTest.kt | 8 +++-- .../espoo/evaka/messaging/MessageQueries.kt | 36 ++++++++++++------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/service/src/integrationTest/kotlin/fi/espoo/evaka/messaging/MessageQueriesTest.kt b/service/src/integrationTest/kotlin/fi/espoo/evaka/messaging/MessageQueriesTest.kt index 5c4691fecfd..60cbff3e5c5 100644 --- a/service/src/integrationTest/kotlin/fi/espoo/evaka/messaging/MessageQueriesTest.kt +++ b/service/src/integrationTest/kotlin/fi/espoo/evaka/messaging/MessageQueriesTest.kt @@ -391,6 +391,7 @@ class MessageQueriesTest : PureJdbiTest(resetDbBeforeEach = true) { @Test fun `query citizen receivers for group change over 2 weeks into the future`() { lateinit var group1Account: MessageAccount + lateinit var group2Account: MessageAccount val today = LocalDate.now() val startDate = today.minusDays(30) @@ -399,9 +400,10 @@ class MessageQueriesTest : PureJdbiTest(resetDbBeforeEach = true) { val endDate = today.plusDays(30) db.transaction { tx -> - val (childId, daycareId, group1Id, tempGroup1Account, group2Id) = + val (childId, daycareId, group1Id, tempGroup1Account, group2Id, tempGroup2Account) = prepareDataForReceiversTest(tx) group1Account = tempGroup1Account + group2Account = tempGroup2Account val placementId = tx.insert( @@ -434,13 +436,15 @@ class MessageQueriesTest : PureJdbiTest(resetDbBeforeEach = true) { val receivers = db.read { it.getCitizenReceivers(today, accounts.person1.id).values } val expectedForNewMessage = setOf(group1Account, accounts.employee1) + val expectedForReply = + setOf(group1Account, accounts.employee1, accounts.employee2, group2Account) assertEquals( expectedForNewMessage, receivers.flatMap { r -> r.newMessage.map { a -> a.account } }.toSet(), ) assertEquals( - expectedForNewMessage + accounts.employee2, + expectedForReply, receivers.flatMap { r -> r.reply.map { a -> a.account } }.toSet(), ) } diff --git a/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt index 6b91679f5a7..4cc4afb209f 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt @@ -764,6 +764,8 @@ fun Database.Read.getCitizenReceivers( val oooPeriod: FiniteDateRange?, ) + val sendNewMessageWeeksBefore = 2L + return createQuery { sql( """ @@ -780,34 +782,44 @@ WITH user_account AS ( FROM user_account acc JOIN foster_parent fp ON acc.person_id = fp.parent_id AND valid_during @> ${bind(today)} ), backup_care_placements AS ( - SELECT p.id, p.unit_id, p.child_id, p.group_id + SELECT p.id, p.unit_id, p.child_id, p.group_id, p.start_date FROM children c - JOIN backup_care p ON p.child_id = c.child_id AND daterange((p.start_date - INTERVAL '2 weeks')::date, p.end_date, '[]') @> ${bind(today)} + JOIN backup_care p ON p.child_id = c.child_id AND p.end_date >= ${bind(today)} WHERE EXISTS ( SELECT 1 FROM daycare u WHERE p.unit_id = u.id AND 'MESSAGING' = ANY(u.enabled_pilot_features) ) ), placements AS ( - SELECT p.id, p.unit_id, p.child_id + SELECT p.id, p.unit_id, p.child_id, p.start_date FROM children c - JOIN placement p ON p.child_id = c.child_id AND daterange((p.start_date - INTERVAL '2 weeks')::date, p.end_date, '[]') @> ${bind(today)} + JOIN placement p ON p.child_id = c.child_id AND p.end_date >= ${bind(today)} AND EXISTS ( SELECT 1 FROM daycare u WHERE p.unit_id = u.id AND 'MESSAGING' = ANY(u.enabled_pilot_features) ) ), relevant_placements AS ( - SELECT p.id, p.unit_id, p.child_id + SELECT p.id, p.unit_id, p.child_id, p.start_date FROM placements p UNION - SELECT bc.id, bc.unit_id, bc.child_id + SELECT bc.id, bc.unit_id, bc.child_id, bc.start_date FROM backup_care_placements bc ), personal_accounts AS ( - SELECT acc.id, acc_name.name, 'PERSONAL' AS type, p.child_id, acl.role != 'UNIT_SUPERVISOR' AS reply_only, ooo.period AS ooo_period - FROM (SELECT DISTINCT unit_id, child_id FROM relevant_placements) p + SELECT + acc.id, + acc_name.name, + 'PERSONAL' AS type, + p.child_id, + (acl.role != 'UNIT_SUPERVISOR' OR ${bind(today.plusWeeks(sendNewMessageWeeksBefore))} < p.start_date) AS reply_only, + ooo.period AS ooo_period + FROM ( + SELECT unit_id, child_id, min(start_date) AS start_date + FROM relevant_placements + GROUP BY unit_id, child_id + ) p JOIN daycare_acl acl ON acl.daycare_id = p.unit_id JOIN message_account acc ON acc.employee_id = acl.employee_id JOIN message_account_view acc_name ON acc_name.id = acc.id @@ -820,15 +832,15 @@ personal_accounts AS ( WHERE active IS TRUE ), group_accounts AS ( - SELECT acc.id, g.name, 'GROUP' AS type, p.child_id + SELECT acc.id, g.name, 'GROUP' AS type, p.child_id, ${bind(today.plusWeeks(sendNewMessageWeeksBefore))} < dgp.start_date AS reply_only FROM placements p - JOIN daycare_group_placement dgp ON dgp.daycare_placement_id = p.id AND ${bind(today)} BETWEEN (dgp.start_date - INTERVAL '2 weeks')::date AND dgp.end_date + JOIN daycare_group_placement dgp ON dgp.daycare_placement_id = p.id AND dgp.end_date >= ${bind(today)} JOIN daycare_group g ON g.id = dgp.daycare_group_id JOIN message_account acc on g.id = acc.daycare_group_id UNION ALL - SELECT acc.id, g.name, 'GROUP' AS type, p.child_id + SELECT acc.id, g.name, 'GROUP' AS type, p.child_id, ${bind(today.plusWeeks(sendNewMessageWeeksBefore))} < p.start_date AS reply_only FROM backup_care_placements p JOIN daycare_group g ON g.id = p.group_id JOIN message_account acc on g.id = acc.daycare_group_id @@ -853,7 +865,7 @@ citizen_accounts AS ( mixed_accounts AS ( SELECT id, name, type, child_id, reply_only, ooo_period FROM personal_accounts UNION ALL - SELECT id, name, type, child_id, FALSE AS reply_only, null as ooo_period FROM group_accounts + SELECT id, name, type, child_id, reply_only, null as ooo_period FROM group_accounts UNION ALL SELECT id, name, type, child_id, FALSE AS reply_only, null as ooo_period FROM citizen_accounts ) From cdae940d1a81faef35216b6c6e979ef8d167c8be Mon Sep 17 00:00:00 2001 From: Tero Laakso Date: Wed, 5 Feb 2025 16:12:21 +0200 Subject: [PATCH 08/11] Add test for staff messaging a starter child --- .../citizen-frontend/messages/ThreadView.tsx | 1 + .../pages/citizen/citizen-messages.ts | 13 ++- .../7_messaging/messaging-by-staff.spec.ts | 95 ++++++++++++++++++- .../specs/7_messaging/messaging.spec.ts | 4 +- .../specs/7_messaging/out-of-office.spec.ts | 2 +- frontend/src/e2e-test/utils/page.ts | 7 ++ 6 files changed, 112 insertions(+), 10 deletions(-) diff --git a/frontend/src/citizen-frontend/messages/ThreadView.tsx b/frontend/src/citizen-frontend/messages/ThreadView.tsx index 878cd2d0b06..42838273f6b 100644 --- a/frontend/src/citizen-frontend/messages/ThreadView.tsx +++ b/frontend/src/citizen-frontend/messages/ThreadView.tsx @@ -321,6 +321,7 @@ export default React.memo( {children.map((child) => ( { } ) - test('Staff can select starters as receivers', async () => { + test('Unit supervisor can send a message to a starter', async () => { const daycarePlacementFixture1 = await Fixture.placement({ childId, unitId: testPreschool.id, @@ -271,8 +272,9 @@ describe('Sending and receiving messages', () => { ] expect(labels.sort()).toEqual(expectedReceiverNames.sort()) - // Send a message to a starter child + // Send a message to a starter child -> selects the whole unit await receiverSelector.optionByLabel(starterChildLabel).click() + await receiverSelector.close() await messageEditor.inputTitle.fill('Aloittavalle otsikko') await messageEditor.inputContent.fill('Sisältö') await messageEditor.sendButton.click() @@ -289,6 +291,95 @@ describe('Sending and receiving messages', () => { content: 'Sisältö' }) }) + + test('Staff can send a message to a starter', async () => { + const secondGroup = await Fixture.daycareGroup({ + daycareId: testDaycare.id, + name: 'Toinen ryhmä' + }).save() + const futureStaff = await Fixture.employee() + .staff(testDaycare.id) + .withGroupAcl(secondGroup.id, mockedDateAt10, mockedDateAt10) + .save() + const daycarePlacementFixture1 = await Fixture.placement({ + childId, + unitId: testDaycare.id, + startDate: mockedDate.addYears(1).addDays(1), + endDate: mockedDate.addYears(2) + }).save() + const daycarePlacementFixture2 = await Fixture.placement({ + childId: testChild2.id, + unitId: testDaycare.id, + startDate: mockedDate.addYears(1).addDays(1), + endDate: mockedDate.addYears(2) + }).save() + await Fixture.groupPlacement({ + daycarePlacementId: daycarePlacementFixture1.id, + daycareGroupId: secondGroup.id, + startDate: mockedDate.addYears(1).addDays(1), + endDate: mockedDate.addYears(2) + }).save() + await Fixture.groupPlacement({ + daycarePlacementId: daycarePlacementFixture2.id, + daycareGroupId: secondGroup.id, + startDate: mockedDate.addYears(1).addDays(1), + endDate: mockedDate.addYears(2) + }).save() + await createMessageAccounts() + + // Verify that available recipients contain current placements and starters + staffPage = await Page.open({ mockedTime: mockedDateAt10 }) + await employeeLogin(staffPage, futureStaff) + await staffPage.goto(`${config.employeeUrl}/messages`) + const messagesPage = new MessagesPage(staffPage) + const messageEditor = await messagesPage.openMessageEditor() + const receiverSelector = messageEditor.receiverSelection + await receiverSelector.open() + await receiverSelector.expandAll() + const labels = await receiverSelector.labels.allTexts() + const expectedReceiverNames = [ + `${secondGroup.name} (aloittavat)`, + `${testChild.lastName} ${testChild.firstName} (${mockedDate.addYears(1).addDays(1).format()})`, + `${testChild2.lastName} ${testChild2.firstName} (${mockedDate.addYears(1).addDays(1).format()})` + ] + expect(labels.sort()).toEqual(expectedReceiverNames.sort()) + + // Send a message to a starter group (contains 2 children) + await receiverSelector + .optionByLabel(`${secondGroup.name} (aloittavat)`) + .click() + await receiverSelector.close() + await messageEditor.inputTitle.fill('Aloittavalle otsikko') + await messageEditor.inputContent.fill('Sisältö') + await messageEditor.sendButton.click() + await messageEditor.waitUntilHidden() + + await runPendingAsyncJobs(mockedDateAt10.addMinutes(1)) + + // Verify that the message is received by the starter + await initCitizenPage(mockedDateAt11) + await citizenPage.goto(config.enduserMessagesUrl) + const citizenMessagesPage = new CitizenMessagesPage(citizenPage) + await citizenMessagesPage.assertThreadContent({ + title: 'Aloittavalle otsikko', + content: 'Sisältö', + childNames: [formatFirstName(testChild), formatFirstName(testChild2)] + }) + + // Reply to the message + await citizenMessagesPage.replyToFirstThread('Vastaukseni') + + await runPendingAsyncJobs(mockedDateAt11.addMinutes(1)) + + // Verify that the message is received by the staff + staffPage = await Page.open({ mockedTime: mockedDateAt12 }) + await employeeLogin(staffPage, futureStaff) + await staffPage.goto(`${config.employeeUrl}/messages`) + const staffMessagesPage = new MessagesPage(staffPage) + await waitUntilEqual(() => staffMessagesPage.getReceivedMessageCount(), 1) + await staffMessagesPage.receivedMessage.click() + await staffMessagesPage.assertMessageContent(1, 'Vastaukseni') + }) }) describe('Sending and receiving sensitive messages', () => { diff --git a/frontend/src/e2e-test/specs/7_messaging/messaging.spec.ts b/frontend/src/e2e-test/specs/7_messaging/messaging.spec.ts index 6bbbd1a1cd8..58fb2d4423a 100644 --- a/frontend/src/e2e-test/specs/7_messaging/messaging.spec.ts +++ b/frontend/src/e2e-test/specs/7_messaging/messaging.spec.ts @@ -763,12 +763,12 @@ describe('Sending and receiving messages', () => { await openCitizen(mockedDateAt11) await citizenPage.goto(config.enduserMessagesUrl) const citizenMessagesPage = new CitizenMessagesPage(citizenPage) - await citizenMessagesPage.openFirstThreadReplyEditor() + await citizenMessagesPage.startReplyToFirstThread() await citizenMessagesPage.discardMessageButton.waitUntilVisible() await citizenMessagesPage.messageReplyContent.fill(defaultContent) await citizenMessagesPage.discardReplyEditor() await citizenMessagesPage.discardMessageButton.waitUntilHidden() - await citizenMessagesPage.openFirstThreadReplyEditor() + await citizenMessagesPage.startReplyToFirstThread() await citizenMessagesPage.messageReplyContent.assertTextEquals('') }) diff --git a/frontend/src/e2e-test/specs/7_messaging/out-of-office.spec.ts b/frontend/src/e2e-test/specs/7_messaging/out-of-office.spec.ts index bac8b43b474..cf634efac0a 100644 --- a/frontend/src/e2e-test/specs/7_messaging/out-of-office.spec.ts +++ b/frontend/src/e2e-test/specs/7_messaging/out-of-office.spec.ts @@ -96,7 +96,7 @@ describe('Out of Office', () => { await editor2.sendMessage() // Reply editor shows the out of office period - await messagesPage2.openFirstThreadReplyEditor() + await messagesPage2.startReplyToFirstThread() await messagesPage2.assertThreadOutOfOffice({ name: getSupervisorName(), period: FiniteDateRange.tryCreate(newStartDate, endDate)! diff --git a/frontend/src/e2e-test/utils/page.ts b/frontend/src/e2e-test/utils/page.ts index f3b42af3c64..804300b7aca 100644 --- a/frontend/src/e2e-test/utils/page.ts +++ b/frontend/src/e2e-test/utils/page.ts @@ -170,6 +170,13 @@ export class ElementCollection { async assertTextsEqual(values: string[]) { await waitUntilEqual(() => this.allTexts(), values) } + + async assertTextsEqualAnyOrder(values: string[]) { + await waitUntilEqual(async () => { + const texts = await this.allTexts() + return texts.sort() + }, values.slice().sort()) + } } export class Element { From 97115aa17db97c680267799cbe0e0dc566492104 Mon Sep 17 00:00:00 2001 From: Tero Laakso Date: Thu, 6 Feb 2025 09:49:24 +0200 Subject: [PATCH 09/11] Remove code for handling starting children without group --- .../fi/espoo/evaka/messaging/MessageQueries.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt index 4cc4afb209f..febd4d5e208 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt @@ -1115,8 +1115,8 @@ data class UnitMessageReceiversResult( val accountId: MessageAccountId, val unitId: DaycareId?, val unitName: String?, - val groupId: GroupId?, - val groupName: String?, + val groupId: GroupId, + val groupName: String, val childId: ChildId, val firstName: String, val lastName: String, @@ -1145,9 +1145,9 @@ fun Database.Read.getReceiversForNewMessage( ${predicate(idFilter.forTable("message_account"))} AND type = ANY('{PERSONAL,GROUP}'::message_account_type[]) ), starting_children AS ( - SELECT p.child_id, p.unit_id, dgp.daycare_group_id AS group_id, COALESCE(dgp.start_date, p.start_date) AS start_date + SELECT p.child_id, p.unit_id, dgp.daycare_group_id AS group_id, dgp.start_date FROM placement p - LEFT JOIN daycare_group_placement dgp ON p.id = dgp.daycare_placement_id + JOIN daycare_group_placement dgp ON p.id = dgp.daycare_placement_id WHERE p.start_date > ${bind(today)} OR dgp.start_date > ${bind(today)} ), children AS ( SELECT a.id AS account_id, p.child_id, NULL AS unit_id, NULL AS unit_name, p.group_id, g.name AS group_name, NULL::date AS start_date @@ -1214,7 +1214,6 @@ fun Database.Read.getReceiversForNewMessage( ) } .toList() - .filter { it.groupId != null } .groupBy { it.accountId } .map { (groupKey, receivers) -> val units = @@ -1287,9 +1286,8 @@ private fun getReceiverGroups( .map { (group, children) -> val (groupId, groupName) = group MessageReceiver.Group( - id = groupId!!, // TODO: Groupless are filtered out. Fix this when children without - // group need to be available. - name = groupName!!, + id = groupId, + name = groupName, hasStarters = children.any { it.startDate != null }, receivers = children.map { From 62072ecf78e9bda1905e65a5551941f4c15d12fc Mon Sep 17 00:00:00 2001 From: Tero Laakso Date: Fri, 7 Feb 2025 09:33:27 +0200 Subject: [PATCH 10/11] Update receiver SQL queries --- .../espoo/evaka/messaging/MessageQueries.kt | 149 +++++++++--------- 1 file changed, 72 insertions(+), 77 deletions(-) diff --git a/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt b/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt index febd4d5e208..5280f447ecb 100644 --- a/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt +++ b/service/src/main/kotlin/fi/espoo/evaka/messaging/MessageQueries.kt @@ -1150,41 +1150,44 @@ fun Database.Read.getReceiversForNewMessage( JOIN daycare_group_placement dgp ON p.id = dgp.daycare_placement_id WHERE p.start_date > ${bind(today)} OR dgp.start_date > ${bind(today)} ), children AS ( - SELECT a.id AS account_id, p.child_id, NULL AS unit_id, NULL AS unit_name, p.group_id, g.name AS group_name, NULL::date AS start_date - FROM accounts a - JOIN realized_placement_all(${bind(today)}) p ON a.daycare_group_id = p.group_id - JOIN daycare d ON p.unit_id = d.id - JOIN daycare_group g ON p.group_id = g.id - WHERE 'MESSAGING' = ANY(d.enabled_pilot_features) - + WITH current_group_receivers AS ( + SELECT a.id AS account_id, p.child_id, NULL::uuid AS unit_id, NULL::text AS unit_name, p.group_id, g.name AS group_name, NULL::date AS start_date + FROM accounts a + JOIN realized_placement_all(${bind(today)}) p ON a.daycare_group_id = p.group_id + JOIN daycare d ON p.unit_id = d.id + JOIN daycare_group g ON p.group_id = g.id + WHERE 'MESSAGING' = ANY(d.enabled_pilot_features) + ), current_unit_receivers AS ( + SELECT a.id AS account_id, p.child_id, p.unit_id, d.name AS unit_name, p.group_id, g.name AS group_name, NULL::date AS start_date + FROM accounts a + JOIN daycare_acl_view acl ON a.employee_id = acl.employee_id + JOIN daycare d ON acl.daycare_id = d.id + JOIN daycare_group g ON d.id = g.daycare_id + JOIN realized_placement_all(${bind(today)}) p ON g.id = p.group_id + WHERE 'MESSAGING' = ANY(d.enabled_pilot_features) + ), starting_group_receivers AS ( + SELECT a.id AS account_id, p.child_id, NULL::uuid AS unit_id, NULL::text AS unit_name, p.group_id, g.name AS group_name, p.start_date + FROM accounts a + JOIN starting_children p ON a.daycare_group_id = p.group_id + JOIN daycare d ON p.unit_id = d.id + JOIN daycare_group g ON p.group_id = g.id + WHERE 'MESSAGING' = ANY(d.enabled_pilot_features) + ), starting_unit_receivers AS ( + SELECT a.id AS account_id, p.child_id, p.unit_id, d.name AS unit_name, p.group_id, g.name AS group_name, p.start_date + FROM accounts a + JOIN daycare_acl_view acl ON a.employee_id = acl.employee_id + JOIN daycare d ON acl.daycare_id = d.id + JOIN starting_children p ON d.id = p.unit_id + LEFT JOIN daycare_group g ON p.group_id = g.id + WHERE 'MESSAGING' = ANY(d.enabled_pilot_features) + ) + SELECT * FROM current_group_receivers UNION ALL - - SELECT a.id AS account_id, p.child_id, p.unit_id, d.name AS unit_name, p.group_id, g.name AS group_name, NULL::date AS start_date - FROM accounts a - JOIN daycare_acl_view acl ON a.employee_id = acl.employee_id - JOIN daycare d ON acl.daycare_id = d.id - JOIN daycare_group g ON d.id = g.daycare_id - JOIN realized_placement_all(${bind(today)}) p ON g.id = p.group_id - WHERE 'MESSAGING' = ANY(d.enabled_pilot_features) - + SELECT * FROM current_unit_receivers UNION ALL - - SELECT a.id AS account_id, p.child_id, NULL AS unit_id, NULL AS unit_name, p.group_id, g.name AS group_name, p.start_date - FROM accounts a - JOIN starting_children p ON a.daycare_group_id = p.group_id - JOIN daycare d ON p.unit_id = d.id - JOIN daycare_group g ON p.group_id = g.id - WHERE 'MESSAGING' = ANY(d.enabled_pilot_features) - + SELECT * FROM starting_group_receivers UNION ALL - - SELECT a.id AS account_id, p.child_id, p.unit_id, d.name AS unit_name, p.group_id, g.name AS group_name, p.start_date - FROM accounts a - JOIN daycare_acl_view acl ON a.employee_id = acl.employee_id - JOIN daycare d ON acl.daycare_id = d.id - JOIN starting_children p ON d.id = p.unit_id - LEFT JOIN daycare_group g ON p.group_id = g.id - WHERE 'MESSAGING' = ANY(d.enabled_pilot_features) + SELECT * FROM starting_unit_receivers ) SELECT DISTINCT c.account_id, @@ -1201,10 +1204,8 @@ fun Database.Read.getReceiversForNewMessage( WHERE EXISTS ( SELECT 1 FROM guardian g - WHERE g.child_id = c.child_id - - UNION ALL - + WHERE g.child_id = c.child_id) + OR EXISTS ( SELECT 1 FROM foster_parent fp WHERE fp.child_id = c.child_id AND fp.valid_during @> ${bind(today)} @@ -1359,31 +1360,27 @@ WITH sender AS ( OR pl.group_id = ANY(${bind(currentRecipients[MessageRecipientType.GROUP])}) OR pl.child_id = ANY(${bind(currentRecipients[MessageRecipientType.CHILD])})) AND ${predicate(filterPredicates)} - AND EXISTS ( - SELECT 1 - FROM child_daycare_acl(${bind(date)}) - JOIN mobile_device_daycare_acl_view USING (daycare_id) - WHERE mobile_device_id = (SELECT sender.employee_id FROM sender) - AND child_id = pl.child_id - - UNION ALL - - SELECT 1 - FROM employee_child_daycare_acl(${bind(date)}) - WHERE employee_id = (SELECT sender.employee_id FROM sender) - AND child_id = pl.child_id - - UNION ALL - - SELECT 1 - FROM sender - WHERE pl.group_id = sender.daycare_group_id - - UNION ALL - - SELECT 1 - FROM sender - WHERE type = 'MUNICIPAL' + AND ( + EXISTS ( + SELECT 1 + FROM child_daycare_acl(${bind(date)}) + JOIN mobile_device_daycare_acl_view USING (daycare_id) + WHERE mobile_device_id = (SELECT sender.employee_id FROM sender) + AND child_id = pl.child_id + ) OR EXISTS ( + SELECT 1 + FROM employee_child_daycare_acl(${bind(date)}) + WHERE employee_id = (SELECT sender.employee_id FROM sender) + AND child_id = pl.child_id + ) OR EXISTS ( + SELECT 1 + FROM sender + WHERE pl.group_id = sender.daycare_group_id + ) OR EXISTS ( + SELECT 1 + FROM sender + WHERE type = 'MUNICIPAL' + ) ) AND 'MESSAGING' = ANY(d.enabled_pilot_features) ), starting_children AS ( @@ -1399,22 +1396,20 @@ WITH sender AS ( OR dgp.daycare_group_id = ANY(${bind(starterRecipients[MessageRecipientType.GROUP])}) OR pl.child_id = ANY(${bind(starterRecipients[MessageRecipientType.CHILD])})) AND ${predicate(filterPredicates)} - AND EXISTS ( - SELECT 1 - FROM daycare_acl_view acl - WHERE acl.daycare_id = pl.unit_id AND acl.employee_id = (SELECT sender.employee_id FROM sender) - - UNION ALL - - SELECT 1 - FROM sender - WHERE dgp.daycare_group_id = sender.daycare_group_id - - UNION ALL - - SELECT 1 - FROM sender - WHERE type = 'MUNICIPAL' + AND ( + EXISTS ( + SELECT 1 + FROM daycare_acl_view acl + WHERE acl.daycare_id = pl.unit_id AND acl.employee_id = (SELECT sender.employee_id FROM sender) + ) OR EXISTS ( + SELECT 1 + FROM sender + WHERE dgp.daycare_group_id = sender.daycare_group_id + ) OR EXISTS ( + SELECT 1 + FROM sender + WHERE type = 'MUNICIPAL' + ) ) AND 'MESSAGING' = ANY(d.enabled_pilot_features) ), children AS ( From 64725235dd4c323b35fb440afb674065428606a8 Mon Sep 17 00:00:00 2001 From: Tero Laakso Date: Fri, 7 Feb 2025 11:03:19 +0200 Subject: [PATCH 11/11] Fix migration for drafts without recipients --- .../db/migration/V495__draft_message_recipients.sql | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/service/src/main/resources/db/migration/V495__draft_message_recipients.sql b/service/src/main/resources/db/migration/V495__draft_message_recipients.sql index 4679d78eac1..31c4e5f8757 100644 --- a/service/src/main/resources/db/migration/V495__draft_message_recipients.sql +++ b/service/src/main/resources/db/migration/V495__draft_message_recipients.sql @@ -1,9 +1,11 @@ ALTER TABLE message_draft - ADD COLUMN recipients jsonb DEFAULT '[]'::jsonb; + ADD COLUMN recipients jsonb DEFAULT '[]'::jsonb NOT NULL; UPDATE message_draft -SET recipients = (SELECT jsonb_agg(jsonb_build_object('accountId', id, 'starter', false)) - FROM (SELECT unnest(recipient_ids) AS id)); +SET recipients = COALESCE( + (SELECT jsonb_agg(jsonb_build_object('accountId', id, 'starter', false)) + FROM (SELECT unnest(recipient_ids) AS id)) + , '[]'::jsonb); ALTER TABLE message_draft DROP COLUMN recipient_ids;