// // Copyright © 2024 Hardcore Engineering Inc. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // // See the License for the specific language governing permissions and // limitations under the License. // import contact, { Employee, getName, Person } from '@hcengineering/contact' import core, { type AccountUuid, combineAttributes, concatLink, Doc, generateId, Ref, Timestamp, Tx, TxCreateDoc, TxCUD, TxMixin, TxProcessor, TxUpdateDoc, UserStatus } from '@hcengineering/core' import love, { isOffice, loveId, MeetingMinutes, MeetingStatus, Office, ParticipantInfo, Room, RoomAccess, RoomInfo } from '@hcengineering/love' import { getMetadata } from '@hcengineering/platform' import serverCore, { TriggerControl } from '@hcengineering/server-core' import view from '@hcengineering/view' import { workbenchId } from '@hcengineering/workbench' export async function OnEmployee (txes: Tx[], control: TriggerControl): Promise { const result: Tx[] = [] for (const tx of txes) { const actualTx = tx as TxMixin if (actualTx._class !== core.class.TxMixin) { continue } if (actualTx.mixin !== contact.mixin.Employee) { continue } const val = actualTx.attributes.active if (val === undefined) { continue } const user = ( await control.findAll(control.ctx, contact.mixin.Employee, { _id: actualTx.objectId as Ref }) )[0] if (user === undefined) { continue } if (user.role === 'GUEST') { continue } if (val) { const freeRoom = (await control.findAll(control.ctx, love.class.Office, { person: null }))[0] if (freeRoom !== undefined) { return [ control.txFactory.createTxUpdateDoc(freeRoom._class, freeRoom.space, freeRoom._id, { person: actualTx.objectId }) ] } } else { const room = (await control.findAll(control.ctx, love.class.Office, { person: actualTx.objectId }))[0] if (room !== undefined) { result.push( control.txFactory.createTxUpdateDoc(room._class, room.space, room._id, { person: null }) ) } } } return result } async function createUserInfo (user: AccountUuid, control: TriggerControl): Promise { const person = (await control.findAll(control.ctx, contact.class.Person, { personUuid: user }))[0] if (person === undefined) return [] // we already have participantInfo for this person const infos = await control.findAll(control.ctx, love.class.ParticipantInfo, { person: person._id }) if (infos.length > 0) return [] const room = (await control.findAll(control.ctx, love.class.Office, { person: person._id }))[0] const tx = control.txFactory.createTxCreateDoc(love.class.ParticipantInfo, core.space.Workspace, { person: person._id, name: person !== undefined ? getName(control.hierarchy, person, control.branding?.lastNameFirst) : 'User', room: room?._id ?? love.ids.Reception, x: 0, y: 0, account: user, sessionId: null }) const ptx = control.txFactory.createTxApplyIf( core.space.Workspace, user, [], [ { _class: love.class.ParticipantInfo, query: { person: person._id } } ], [tx], 'createUserInfo' ) return [ptx] } async function removeUserInfo (user: AccountUuid, control: TriggerControl): Promise { const person = (await control.findAll(control.ctx, contact.class.Person, { personUuid: user }))[0] if (person === undefined) return [] // recheck that user is still offline const status = (await control.findAll(control.ctx, core.class.UserStatus, { user }))[0] if (status !== undefined && status.online) return [] const infos = await control.findAll(control.ctx, love.class.ParticipantInfo, { person: person._id }) const res: Tx[] = [] for (const info of infos) { res.push(control.txFactory.createTxRemoveDoc(info._class, info.space, info._id)) } return res } export async function OnUserStatus (txes: Tx[], control: TriggerControl): Promise { const result: Tx[] = [] for (const tx of txes) { const actualTx = tx as TxCUD if (actualTx.objectClass !== core.class.UserStatus) { continue } if (actualTx._class === core.class.TxCreateDoc) { const createTx = actualTx as TxCreateDoc const status = TxProcessor.createDoc2Doc(createTx) result.push(...(await createUserInfo(status.user, control))) } else if (actualTx._class === core.class.TxUpdateDoc) { const updateTx = actualTx as TxUpdateDoc const val = updateTx.operations.online if (val === undefined) return [] const status = (await control.findAll(control.ctx, core.class.UserStatus, { _id: updateTx.objectId }))[0] if (status !== undefined) { if (val) { result.push(...(await createUserInfo(status.user, control))) } else { result.push(...(await removeUserInfo(status.user, control))) } } } } return result } async function roomJoinHandler (info: ParticipantInfo, control: TriggerControl): Promise { const res: Tx[] = [] const roomInfos = await control.queryFind(control.ctx, love.class.RoomInfo, {}) const roomInfo = roomInfos.find((ri) => ri.room === info.room) if (roomInfo !== undefined && !roomInfo.persons.includes(info.person)) { res.push( control.txFactory.createTxUpdateDoc(love.class.RoomInfo, core.space.Workspace, roomInfo._id, { $push: { persons: info.person } }) ) } else { const room = (await control.findAll(control.ctx, love.class.Room, { _id: info.room }))[0] if (room === undefined) return [] res.push( control.txFactory.createTxCreateDoc(love.class.RoomInfo, core.space.Workspace, { persons: [info.person], room: info.room, isOffice: isOffice(room) }) ) } if (info.account != null) { const meetingMinutes = ( await control.findAll(control.ctx, love.class.MeetingMinutes, { attachedTo: info.room, status: MeetingStatus.Active }) )[0] if (meetingMinutes !== undefined) { const colab = await control.findAll(control.ctx, core.class.Collaborator, { attachedTo: meetingMinutes._id, collaborator: info.account }) if (colab.length === 0) { res.push( control.txFactory.createTxCreateDoc(core.class.Collaborator, core.space.Workspace, { attachedTo: meetingMinutes._id, attachedToClass: meetingMinutes._class, collection: 'collaborators', collaborator: info.account }) ) } } else { const room = (await control.findAll(control.ctx, love.class.Room, { _id: info.room }))[0] if (room === undefined) return res if (isOffice(room) && room.person === info.person) return res const _id = generateId() const date = new Date() .toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }) .replace(',', ' at') const tx = control.txFactory.createTxCreateDoc( love.class.MeetingMinutes, core.space.Workspace, { description: null, attachedTo: info.room, status: MeetingStatus.Active, title: `${await getRoomName(control, info.room)} ${date}`, attachedToClass: love.class.Room, collection: 'meetings' }, _id ) tx.space = core.space.Tx res.push(tx) res.push( control.txFactory.createTxCreateDoc(core.class.Collaborator, core.space.Workspace, { attachedTo: _id, attachedToClass: love.class.MeetingMinutes, collection: 'collaborators', collaborator: info.account }) ) if (isOffice(room) && room.person !== info.person && room.person !== null) { const person = ( await control.findAll(control.ctx, contact.mixin.Employee, { _id: room.person as Ref }) )[0] if (person?.personUuid !== undefined) { res.push( control.txFactory.createTxCreateDoc(core.class.Collaborator, core.space.Workspace, { attachedTo: _id, attachedToClass: love.class.MeetingMinutes, collection: 'collaborators', collaborator: person.personUuid }) ) } } } } return res } async function getRoomName (control: TriggerControl, roomId: Ref): Promise { const room = (await control.findAll(control.ctx, love.class.Room, { _id: roomId }))[0] if (room === undefined) return '' if (isOffice(room) && room.person !== null && room.name === '') { const employee = (await control.findAll(control.ctx, contact.class.Person, { _id: room.person }))[0] if (employee != null) { return getName(control.hierarchy, employee) } } return room.name } async function setDefaultRoomAccess (info: ParticipantInfo, control: TriggerControl): Promise { const res: Tx[] = [] const roomInfos = await control.queryFind(control.ctx, love.class.RoomInfo, {}) const oldRoomInfo = roomInfos.find((ri) => ri.persons.includes(info.person)) if (oldRoomInfo !== undefined) { if (oldRoomInfo.persons.length === 1 && oldRoomInfo.persons[0] === info.person) { res.push(control.txFactory.createTxRemoveDoc(oldRoomInfo._class, oldRoomInfo.space, oldRoomInfo._id)) const resetAccessTx = control.txFactory.createTxUpdateDoc( oldRoomInfo.isOffice ? love.class.Office : love.class.Room, core.space.Workspace, oldRoomInfo.room, { access: oldRoomInfo.isOffice ? RoomAccess.Knock : RoomAccess.Open } ) res.push(resetAccessTx) } else { res.push( control.txFactory.createTxUpdateDoc(love.class.RoomInfo, core.space.Workspace, oldRoomInfo._id, { $pull: { persons: info.person } }) ) } } return res } async function finishRoomMeetings (room: Ref, meetingEnd: Timestamp, control: TriggerControl): Promise { const res: Tx[] = [] const meetingMinutes = await control.findAll(control.ctx, love.class.MeetingMinutes, { attachedTo: room, status: MeetingStatus.Active }) for (const meeting of meetingMinutes) { res.push( control.txFactory.createTxUpdateDoc(meeting._class, meeting.space, meeting._id, { status: MeetingStatus.Finished, meetingEnd }) ) } return res } export async function OnParticipantInfo (txes: Tx[], control: TriggerControl): Promise { const result: Tx[] = [] for (const tx of txes) { const actualTx = tx as TxCUD if (actualTx._class === core.class.TxCreateDoc) { const info = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc) result.push(...(await roomJoinHandler(info, control))) } if (actualTx._class === core.class.TxRemoveDoc) { const removedInfo = control.removedMap.get(actualTx.objectId) as ParticipantInfo if (removedInfo === undefined) { continue } result.push(...(await setDefaultRoomAccess(removedInfo, control))) continue } if (actualTx._class === core.class.TxUpdateDoc) { const newRoom = (actualTx as TxUpdateDoc).operations.room if (newRoom === undefined) { continue } const info = ( await control.findAll(control.ctx, love.class.ParticipantInfo, { _id: actualTx.objectId }, { limit: 1 }) )[0] if (info === undefined) { continue } result.push(...(await setDefaultRoomAccess(info, control))) result.push(...(await roomJoinHandler(info, control))) } } return result } export async function meetingMinutesHTMLPresenter (doc: Doc, control: TriggerControl): Promise { const meetingMinutes = doc as MeetingMinutes const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? '' const panelProps = [view.component.EditDoc, meetingMinutes._id, meetingMinutes._class] const fragment = encodeURIComponent(panelProps.join('|')) const path = `${workbenchId}/${control.workspace.url}/${loveId}#${fragment}` const link = concatLink(front, path) return `${meetingMinutes.title}` } /** * @public */ export async function meetingMinutesTextPresenter (doc: Doc): Promise { const meetingMinutes = doc as MeetingMinutes return meetingMinutes.title } async function isRoomEmpty ( room: Ref, isOffice: boolean, persons: Ref[], control: TriggerControl ): Promise { if (persons.length === 0) return true if (isOffice && persons.length === 1) { const office = (await control.findAll(control.ctx, love.class.Office, { _id: room as Ref }))[0] if (office === undefined) return true return office.person != null && office.person === persons[0] } return false } async function OnRoomInfo (txes: TxCUD[], control: TriggerControl): Promise { const result: Tx[] = [] const personsByRoom = new Map, Ref[]>() for (const tx of txes) { if (tx._class === core.class.TxRemoveDoc) { const roomInfo = control.removedMap.get(tx.objectId) as RoomInfo if (roomInfo === undefined) continue if (roomInfo.room === love.ids.Reception) continue personsByRoom.delete(tx.objectId) result.push(...(await finishRoomMeetings(roomInfo.room, tx.modifiedOn, control))) continue } if (tx._class === core.class.TxUpdateDoc) { const updateTx = tx as TxUpdateDoc const pulled = combineAttributes([updateTx.operations], 'persons', '$pull', '$in') const pushed = combineAttributes([updateTx.operations], 'persons', '$push', '$each') if (pulled.length === 0 && pushed.length === 0) continue const roomInfos = await control.queryFind(control.ctx, love.class.RoomInfo, {}) const roomInfo = roomInfos.find((r) => r._id === tx.objectId) if (roomInfo === undefined) continue if (roomInfo.room === love.ids.Reception) continue const currentPersons = personsByRoom.get(tx.objectId) ?? roomInfo.persons const newPersons = currentPersons.filter((p) => !pulled.includes(p)).concat(pushed) personsByRoom.set(tx.objectId, newPersons) if (await isRoomEmpty(roomInfo.room, roomInfo.isOffice, newPersons, control)) { result.push(...(await finishRoomMeetings(roomInfo.room, tx.modifiedOn, control))) } } } return result } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ function: { MeetingMinutesHTMLPresenter: meetingMinutesHTMLPresenter, MeetingMinutesTextPresenter: meetingMinutesTextPresenter }, trigger: { OnEmployee, OnUserStatus, OnParticipantInfo, OnRoomInfo } })