// // Copyright © 2022 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, { Contact, Employee, formatName, getName, Person, PersonAccount } from '@hcengineering/contact' import core, { Doc, Ref, SortingOrder, toIdMap, Tx, TxCreateDoc, TxFactory, TxMixin, TxProcessor, TxRemoveDoc, TxUpdateDoc } from '@hcengineering/core' import gmail from '@hcengineering/gmail' import hr, { Department, DepartmentMember, fromTzDate, PublicHoliday, Request, Staff, tzDateEqual } from '@hcengineering/hr' import notification, { NotificationType } from '@hcengineering/notification' import { translate } from '@hcengineering/platform' import { TriggerControl } from '@hcengineering/server-core' import { sendEmailNotification } from '@hcengineering/server-gmail-resources' import { getEmployee, getPersonAccountById } from '@hcengineering/server-notification' import { getContentByTemplate, getNotificationProviderControl, isAllowed } from '@hcengineering/server-notification-resources' async function getOldDepartment ( currentTx: TxMixin | TxUpdateDoc, control: TriggerControl ): Promise | undefined> { const txes = await control.findAll>( core.class.TxMixin, { objectId: currentTx.objectId }, { sort: { modifiedOn: SortingOrder.Ascending } } ) let lastDepartment: Ref | undefined for (const tx of txes) { if (tx._id === currentTx._id) continue if (tx.attributes?.department !== undefined) { lastDepartment = tx.attributes.department } } return lastDepartment } async function buildHierarchy (_id: Ref, control: TriggerControl): Promise { const res: Department[] = [] const ancestors = new Map, Ref>() const departments = await control.findAll(hr.class.Department, {}) for (const department of departments) { if (department._id === hr.ids.Head || department.parent === undefined) continue ancestors.set(department._id, department.parent) } const departmentsMap = toIdMap(departments) while (true) { const department = departmentsMap.get(_id) if (department === undefined) return res res.push(department) const next = ancestors.get(department._id) if (next === undefined) return res _id = next } } function exlude (first: Ref[], second: Ref[]): Ref[] { const set = new Set(first) const res: Ref[] = [] for (const department of second) { if (!set.has(department)) { res.push(department) } } return res } function getTxes ( factory: TxFactory, account: Ref[], added: Ref[], removed?: Ref[] ): Tx[] { const pushTxes = added .map((dep) => account.map((it) => factory.createTxUpdateDoc(hr.class.Department, core.space.Workspace, dep, { $push: { members: it } }) ) ) .flat() if (removed === undefined) return pushTxes const pullTxes = removed .map((dep) => account.map((it) => factory.createTxUpdateDoc(hr.class.Department, core.space.Workspace, dep, { $pull: { members: it } }) ) ) .flat() return [...pullTxes, ...pushTxes] } /** * @public */ export async function OnDepartmentStaff (tx: Tx, control: TriggerControl): Promise { const ctx = TxProcessor.extractTx(tx) as TxMixin const targetAccount = control.modelDb.getAccountByPersonId(ctx.objectId) as PersonAccount[] if (targetAccount.length === 0) return [] if (ctx.attributes.department !== undefined) { const lastDepartment = await getOldDepartment(ctx, control) const departmentId = ctx.attributes.department if (departmentId === null) { if (lastDepartment !== undefined) { const removed = await buildHierarchy(lastDepartment, control) return getTxes( control.txFactory, targetAccount.map((it) => it._id), [], removed.map((p) => p._id) ) } } const push = (await buildHierarchy(departmentId, control)).map((p) => p._id) if (lastDepartment === undefined) { return getTxes( control.txFactory, targetAccount.map((it) => it._id), push ) } let removed = (await buildHierarchy(lastDepartment, control)).map((p) => p._id) const added = exlude(removed, push) removed = exlude(push, removed) return getTxes( control.txFactory, targetAccount.map((it) => it._id), added, removed ) } return [] } /** * @public */ export async function OnDepartmentRemove (tx: Tx, control: TriggerControl): Promise { const ctx = TxProcessor.extractTx(tx) as TxRemoveDoc const department = control.removedMap.get(ctx.objectId) as Department if (department === undefined) return [] const res: Tx[] = [] const nested = await control.findAll(hr.class.Department, { parent: department._id }) for (const dep of nested) { res.push(control.txFactory.createTxRemoveDoc(dep._class, dep.space, dep._id)) } const targetAccounts = await control.modelDb.findAll(contact.class.PersonAccount, { _id: { $in: department.members } }) const employeeIds = targetAccounts.map((acc) => acc.person as Ref) const employee = await control.findAll(contact.mixin.Employee, { _id: { $in: employeeIds } }) const removed = await buildHierarchy(department._id, control) employee.forEach((em) => { res.push(control.txFactory.createTxMixin(em._id, em._class, em.space, hr.mixin.Staff, { department: undefined })) }) res.push( ...getTxes( control.txFactory, targetAccounts.map((it) => it._id), [], removed.map((p) => p._id) ) ) return res } /** * @public */ export async function OnEmployee (tx: Tx, control: TriggerControl): Promise { const ctx = TxProcessor.extractTx(tx) as TxMixin const person = (await control.findAll(contact.class.Person, { _id: ctx.objectId }))[0] if (person === undefined) { return [] } const employee = control.hierarchy.as(person, ctx.mixin) if (control.hierarchy.hasMixin(person, hr.mixin.Staff) || !employee.active) { return [] } return [ control.txFactory.createTxMixin(ctx.objectId, ctx.objectClass, ctx.objectSpace, hr.mixin.Staff, { department: hr.ids.Head }) ] } /** * @public */ export async function OnEmployeeDeactivate (tx: Tx, control: TriggerControl): Promise { const actualTx = TxProcessor.extractTx(tx) if (core.class.TxUpdateDoc !== actualTx._class) { return [] } const ctx = actualTx as TxUpdateDoc if (ctx.objectClass !== contact.mixin.Employee || ctx.operations.active !== false) { return [] } const targetAccount = control.modelDb.getAccountByPersonId(ctx.objectId) as PersonAccount[] if (targetAccount.length === 0) return [] const lastDepartment = await getOldDepartment(ctx, control) if (lastDepartment === undefined) return [] const removed = await buildHierarchy(lastDepartment, control) return getTxes( control.txFactory, targetAccount.map((it) => it._id), [], removed.map((p) => p._id) ) } // TODO: why we need specific email notifications instead of using general flow? async function sendEmailNotifications ( control: TriggerControl, sender: PersonAccount, doc: Request | PublicHoliday, space: Ref, typeId: Ref ): Promise { const contacts = new Set>() const departments = await buildHierarchy(space, control) for (const department of departments) { if (department.subscribers === undefined) continue for (const subscriber of department.subscribers) { contacts.add(subscriber) } } // should respect employee settings const type = await control.modelDb.findOne(notification.class.NotificationType, { _id: typeId }) if (type === undefined) return const provider = await control.modelDb.findOne(notification.class.NotificationProvider, { _id: gmail.providers.EmailNotificationProvider }) if (provider === undefined) return const notificationControl = await getNotificationProviderControl(control.ctx, control) for (const accountId of contacts.values()) { const accounts = control.modelDb.getAccountByPersonId(accountId) as PersonAccount[] for (const account of accounts) { const allowed = isAllowed(control, account._id, type, provider, notificationControl) if (!allowed) { contacts.delete(account.person) } } } const channels = await control.findAll(contact.class.Channel, { provider: contact.channelProvider.Email, attachedTo: { $in: Array.from(contacts) } }) const senderPerson = (await control.findAll(contact.class.Person, { _id: sender.person }))[0] const senderName = senderPerson !== undefined ? formatName(senderPerson.name, control.branding?.lastNameFirst) : '' const content = await getContentByTemplate(doc, senderName, type._id, control, '') if (content === undefined) return for (const channel of channels) { await sendEmailNotification(control.ctx, content.text, content.html, content.subject, channel.value) } } /** * @public */ export async function OnRequestCreate (tx: Tx, control: TriggerControl): Promise { const ctx = TxProcessor.extractTx(tx) as TxCreateDoc const sender = getPersonAccountById(ctx.modifiedBy, control) if (sender === undefined) return [] const request = TxProcessor.createDoc2Doc(ctx) await sendEmailNotifications(control, sender, request, request.department, hr.ids.CreateRequestNotification) return [] } /** * @public */ export async function OnRequestUpdate (tx: Tx, control: TriggerControl): Promise { const ctx = TxProcessor.extractTx(tx) as TxUpdateDoc const sender = getPersonAccountById(ctx.modifiedBy, control) if (sender === undefined) return [] const request = (await control.findAll(hr.class.Request, { _id: ctx.objectId }))[0] as Request if (request === undefined) return [] await sendEmailNotifications(control, sender, request, request.department, hr.ids.UpdateRequestNotification) return [] } /** * @public */ export async function OnRequestRemove (tx: Tx, control: TriggerControl): Promise { const ctx = TxProcessor.extractTx(tx) as TxCreateDoc const sender = getPersonAccountById(ctx.modifiedBy, control) if (sender === undefined) return [] const request = control.removedMap.get(ctx.objectId) as Request if (request === undefined) return [] await sendEmailNotifications(control, sender, request, request.department, hr.ids.RemoveRequestNotification) return [] } /** * @public */ export async function RequestHTMLPresenter (doc: Doc, control: TriggerControl): Promise { const request = doc as Request const employee = (await control.findAll(contact.mixin.Employee, { _id: request.attachedTo }))[0] const who = getName(control.hierarchy, employee, control.branding?.lastNameFirst) const type = await translate(control.modelDb.getObject(request.type).label, {}) const date = tzDateEqual(request.tzDate, request.tzDueDate) ? `on ${new Date(fromTzDate(request.tzDate)).toLocaleDateString()}` : `from ${new Date(fromTzDate(request.tzDate)).toLocaleDateString()} to ${new Date( fromTzDate(request.tzDueDate) ).toLocaleDateString()}` return `${who} - ${type.toLowerCase()} ${date}` } /** * @public */ export async function RequestTextPresenter (doc: Doc, control: TriggerControl): Promise { const request = doc as Request const employee = (await control.findAll(contact.mixin.Employee, { _id: request.attachedTo }))[0] const who = getName(control.hierarchy, employee, control.branding?.lastNameFirst) const type = await translate(control.modelDb.getObject(request.type).label, {}) const date = tzDateEqual(request.tzDate, request.tzDueDate) ? `on ${new Date(fromTzDate(request.tzDate)).toLocaleDateString()}` : `from ${new Date(fromTzDate(request.tzDate)).toLocaleDateString()} to ${new Date( fromTzDate(request.tzDueDate) ).toLocaleDateString()}` return `${who} - ${type.toLowerCase()} ${date}` } /** * @public */ export async function OnPublicHolidayCreate (tx: Tx, control: TriggerControl): Promise { const ctx = TxProcessor.extractTx(tx) as TxCreateDoc const sender = getPersonAccountById(ctx.modifiedBy, control) if (sender === undefined) return [] const employee = await getEmployee(sender.person as Ref, control) if (employee === undefined) return [] const publicHoliday = TxProcessor.createDoc2Doc(ctx) await sendEmailNotifications( control, sender, publicHoliday, publicHoliday.department, hr.ids.CreatePublicHolidayNotification ) return [] } /** * @public */ export async function PublicHolidayHTMLPresenter (doc: Doc, control: TriggerControl): Promise { const holiday = doc as PublicHoliday const sender = getPersonAccountById(holiday.modifiedBy, control) if (sender === undefined) return '' const employee = await getEmployee(sender.person as Ref, control) if (employee === undefined) return '' const who = formatName(employee.name, control.branding?.lastNameFirst) const date = `on ${new Date(fromTzDate(holiday.date)).toLocaleDateString()}` return `${holiday.title} ${date}
${holiday.description}
Set by ${who}` } /** * @public */ export async function PublicHolidayTextPresenter (doc: Doc, control: TriggerControl): Promise { const holiday = doc as PublicHoliday const sender = getPersonAccountById(holiday.modifiedBy, control) if (sender === undefined) return '' const employee = await getEmployee(sender.person as Ref, control) if (employee === undefined) return '' const who = formatName(employee.name, control.branding?.lastNameFirst) const date = `on ${new Date(fromTzDate(holiday.date)).toLocaleDateString()}` return `${holiday.title} ${date}. ${holiday.description}. Set by ${who}` } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ trigger: { OnEmployee, OnRequestCreate, OnRequestUpdate, OnRequestRemove, OnDepartmentStaff, OnDepartmentRemove, OnEmployeeDeactivate, OnPublicHolidayCreate }, function: { RequestHTMLPresenter, RequestTextPresenter, PublicHolidayHTMLPresenter, PublicHolidayTextPresenter } })