mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-31 12:19:47 +02:00
Signed-off-by: Anton Alexeyev <alexeyev.anton@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
448 lines
15 KiB
TypeScript
448 lines
15 KiB
TypeScript
//
|
|
// 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<Tx[]> {
|
|
const result: Tx[] = []
|
|
for (const tx of txes) {
|
|
const actualTx = tx as TxMixin<Person, Employee>
|
|
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<Employee> })
|
|
)[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<Tx[]> {
|
|
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<Tx[]> {
|
|
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<Tx[]> {
|
|
const result: Tx[] = []
|
|
for (const tx of txes) {
|
|
const actualTx = tx as TxCUD<UserStatus>
|
|
if (actualTx.objectClass !== core.class.UserStatus) {
|
|
continue
|
|
}
|
|
if (actualTx._class === core.class.TxCreateDoc) {
|
|
const createTx = actualTx as TxCreateDoc<UserStatus>
|
|
const status = TxProcessor.createDoc2Doc(createTx)
|
|
result.push(...(await createUserInfo(status.user, control)))
|
|
} else if (actualTx._class === core.class.TxUpdateDoc) {
|
|
const updateTx = actualTx as TxUpdateDoc<UserStatus>
|
|
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<Tx[]> {
|
|
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<MeetingMinutes>()
|
|
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<Employee> })
|
|
)[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<Room>): Promise<string> {
|
|
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<Tx[]> {
|
|
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<Room>, meetingEnd: Timestamp, control: TriggerControl): Promise<Tx[]> {
|
|
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<Tx[]> {
|
|
const result: Tx[] = []
|
|
for (const tx of txes) {
|
|
const actualTx = tx as TxCUD<ParticipantInfo>
|
|
if (actualTx._class === core.class.TxCreateDoc) {
|
|
const info = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc<ParticipantInfo>)
|
|
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<ParticipantInfo>).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<string> {
|
|
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 `<a href="${link}">${meetingMinutes.title}</a>`
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export async function meetingMinutesTextPresenter (doc: Doc): Promise<string> {
|
|
const meetingMinutes = doc as MeetingMinutes
|
|
return meetingMinutes.title
|
|
}
|
|
|
|
async function isRoomEmpty (
|
|
room: Ref<Room>,
|
|
isOffice: boolean,
|
|
persons: Ref<Person>[],
|
|
control: TriggerControl
|
|
): Promise<boolean> {
|
|
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<Office> }))[0]
|
|
if (office === undefined) return true
|
|
return office.person != null && office.person === persons[0]
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
async function OnRoomInfo (txes: TxCUD<RoomInfo>[], control: TriggerControl): Promise<Tx[]> {
|
|
const result: Tx[] = []
|
|
const personsByRoom = new Map<Ref<RoomInfo>, Ref<Person>[]>()
|
|
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<RoomInfo>
|
|
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
|
|
}
|
|
})
|