2022-06-20 04:24:08 +00:00
|
|
|
//
|
|
|
|
// 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.
|
|
|
|
//
|
|
|
|
|
2023-03-09 09:05:42 +00:00
|
|
|
import contact, { Contact, Employee, EmployeeAccount, formatName, getName } from '@hcengineering/contact'
|
2023-03-05 17:36:12 +00:00
|
|
|
import core, {
|
|
|
|
Doc,
|
|
|
|
Ref,
|
|
|
|
SortingOrder,
|
|
|
|
toIdMap,
|
|
|
|
Tx,
|
|
|
|
TxCreateDoc,
|
|
|
|
TxFactory,
|
|
|
|
TxMixin,
|
|
|
|
TxProcessor,
|
2023-05-04 12:35:07 +00:00
|
|
|
TxRemoveDoc,
|
2023-03-05 17:36:12 +00:00
|
|
|
TxUpdateDoc
|
|
|
|
} from '@hcengineering/core'
|
2023-03-09 11:19:05 +00:00
|
|
|
import hr, {
|
|
|
|
Department,
|
|
|
|
DepartmentMember,
|
|
|
|
fromTzDate,
|
|
|
|
PublicHoliday,
|
|
|
|
Request,
|
|
|
|
Staff,
|
|
|
|
tzDateEqual
|
|
|
|
} from '@hcengineering/hr'
|
2023-03-05 17:36:12 +00:00
|
|
|
import notification, { NotificationType } from '@hcengineering/notification'
|
|
|
|
import { translate } from '@hcengineering/platform'
|
2022-09-21 08:08:25 +00:00
|
|
|
import { TriggerControl } from '@hcengineering/server-core'
|
2023-03-09 11:19:05 +00:00
|
|
|
import { getEmployee, getEmployeeAccountById } from '@hcengineering/server-notification'
|
2023-04-25 08:34:38 +00:00
|
|
|
import { getContent, isAllowed } from '@hcengineering/server-notification-resources'
|
2022-06-20 04:24:08 +00:00
|
|
|
|
|
|
|
async function getOldDepartment (
|
2022-06-29 05:51:29 +00:00
|
|
|
currentTx: TxMixin<Employee, Staff> | TxUpdateDoc<Employee>,
|
2022-06-20 04:24:08 +00:00
|
|
|
control: TriggerControl
|
|
|
|
): Promise<Ref<Department> | undefined> {
|
|
|
|
const txes = await control.findAll<TxMixin<Employee, Staff>>(
|
|
|
|
core.class.TxMixin,
|
|
|
|
{
|
|
|
|
objectId: currentTx.objectId
|
|
|
|
},
|
|
|
|
{ sort: { modifiedOn: SortingOrder.Ascending } }
|
|
|
|
)
|
|
|
|
let lastDepartment: Ref<Department> | undefined
|
|
|
|
for (const tx of txes) {
|
|
|
|
if (tx._id === currentTx._id) continue
|
|
|
|
if (tx.attributes?.department !== undefined) {
|
|
|
|
lastDepartment = tx.attributes.department
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return lastDepartment
|
|
|
|
}
|
|
|
|
|
2023-03-05 17:36:12 +00:00
|
|
|
async function buildHierarchy (_id: Ref<Department>, control: TriggerControl): Promise<Department[]> {
|
|
|
|
const res: Department[] = []
|
|
|
|
const ancestors: Map<Ref<Department>, Ref<Department>> = new Map()
|
|
|
|
const departments = await control.findAll(hr.class.Department, {})
|
|
|
|
for (const department of departments) {
|
|
|
|
if (department._id === hr.ids.Head) continue
|
|
|
|
ancestors.set(department._id, department.space)
|
|
|
|
}
|
|
|
|
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
|
2022-06-20 04:24:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function exlude (first: Ref<Department>[], second: Ref<Department>[]): Ref<Department>[] {
|
|
|
|
const set = new Set(first)
|
|
|
|
const res: Ref<Department>[] = []
|
|
|
|
for (const department of second) {
|
|
|
|
if (!set.has(department)) {
|
|
|
|
res.push(department)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
function getTxes (
|
|
|
|
factory: TxFactory,
|
2022-06-20 15:59:56 +00:00
|
|
|
account: Ref<DepartmentMember>,
|
2022-06-20 04:24:08 +00:00
|
|
|
added: Ref<Department>[],
|
|
|
|
removed?: Ref<Department>[]
|
|
|
|
): Tx[] {
|
|
|
|
const pushTxes = added.map((dep) =>
|
|
|
|
factory.createTxUpdateDoc(hr.class.Department, core.space.Space, dep, {
|
|
|
|
$push: { members: account }
|
|
|
|
})
|
|
|
|
)
|
|
|
|
if (removed === undefined) return pushTxes
|
|
|
|
const pullTxes = removed.map((dep) =>
|
|
|
|
factory.createTxUpdateDoc(hr.class.Department, core.space.Space, dep, {
|
|
|
|
$pull: { members: account }
|
|
|
|
})
|
|
|
|
)
|
|
|
|
return [...pullTxes, ...pushTxes]
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
export async function OnDepartmentStaff (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
2023-05-03 14:03:36 +00:00
|
|
|
const ctx = TxProcessor.extractTx(tx) as TxMixin<Employee, Staff>
|
2022-06-20 04:24:08 +00:00
|
|
|
|
|
|
|
const targetAccount = (
|
|
|
|
await control.modelDb.findAll(contact.class.EmployeeAccount, {
|
|
|
|
employee: ctx.objectId
|
|
|
|
})
|
|
|
|
)[0]
|
|
|
|
if (targetAccount === undefined) return []
|
|
|
|
|
|
|
|
if (ctx.attributes.department !== undefined) {
|
|
|
|
const lastDepartment = await getOldDepartment(ctx, control)
|
|
|
|
|
|
|
|
const departmentId = ctx.attributes.department
|
2022-06-29 05:51:29 +00:00
|
|
|
if (departmentId === null) {
|
|
|
|
if (lastDepartment !== undefined) {
|
|
|
|
const removed = await buildHierarchy(lastDepartment, control)
|
2023-03-05 17:36:12 +00:00
|
|
|
return getTxes(
|
|
|
|
control.txFactory,
|
|
|
|
targetAccount._id,
|
|
|
|
[],
|
|
|
|
removed.map((p) => p._id)
|
|
|
|
)
|
2022-06-29 05:51:29 +00:00
|
|
|
}
|
|
|
|
}
|
2023-03-05 17:36:12 +00:00
|
|
|
const push = (await buildHierarchy(departmentId, control)).map((p) => p._id)
|
2022-06-20 04:24:08 +00:00
|
|
|
|
|
|
|
if (lastDepartment === undefined) {
|
|
|
|
return getTxes(control.txFactory, targetAccount._id, push)
|
|
|
|
}
|
|
|
|
|
2023-03-05 17:36:12 +00:00
|
|
|
let removed = (await buildHierarchy(lastDepartment, control)).map((p) => p._id)
|
2022-06-20 04:24:08 +00:00
|
|
|
const added = exlude(removed, push)
|
|
|
|
removed = exlude(push, removed)
|
|
|
|
return getTxes(control.txFactory, targetAccount._id, added, removed)
|
|
|
|
}
|
|
|
|
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
2023-05-04 12:35:07 +00:00
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
export async function OnDepartmentRemove (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
|
|
|
const ctx = TxProcessor.extractTx(tx) as TxRemoveDoc<Department>
|
|
|
|
|
|
|
|
const department = (await control.findAll(hr.class.Department, { _id: ctx.objectSpace as Ref<Department> }))[0]
|
|
|
|
|
|
|
|
const targetAccounts = await control.modelDb.findAll(contact.class.EmployeeAccount, {
|
|
|
|
_id: { $in: department.members }
|
|
|
|
})
|
|
|
|
const employeeIds = targetAccounts.map((acc) => acc.employee as Ref<Staff>)
|
|
|
|
|
|
|
|
const employee = await control.findAll(contact.class.Employee, {
|
|
|
|
_id: { $in: employeeIds }
|
|
|
|
})
|
|
|
|
const removed = await buildHierarchy(department._id, control)
|
|
|
|
const res: Tx[] = []
|
|
|
|
employee.forEach((em) => {
|
|
|
|
res.push(control.txFactory.createTxMixin(em._id, em._class, em.space, hr.mixin.Staff, { department: undefined }))
|
|
|
|
})
|
|
|
|
targetAccounts.forEach((acc) => {
|
|
|
|
res.push(
|
|
|
|
...getTxes(
|
|
|
|
control.txFactory,
|
|
|
|
acc._id,
|
|
|
|
[],
|
|
|
|
removed.map((p) => p._id)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
})
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
2022-06-29 05:51:29 +00:00
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
export async function OnEmployeeDeactivate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
2022-08-16 10:19:33 +00:00
|
|
|
const actualTx = TxProcessor.extractTx(tx)
|
2022-06-29 05:51:29 +00:00
|
|
|
if (core.class.TxUpdateDoc !== actualTx._class) {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
const ctx = actualTx as TxUpdateDoc<Employee>
|
|
|
|
if (ctx.objectClass !== contact.class.Employee || ctx.operations.active !== false) {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
|
|
|
const targetAccount = (
|
|
|
|
await control.modelDb.findAll(contact.class.EmployeeAccount, {
|
|
|
|
employee: ctx.objectId
|
|
|
|
})
|
|
|
|
)[0]
|
|
|
|
if (targetAccount === undefined) return []
|
|
|
|
const lastDepartment = await getOldDepartment(ctx, control)
|
|
|
|
if (lastDepartment === undefined) return []
|
|
|
|
|
|
|
|
const removed = await buildHierarchy(lastDepartment, control)
|
2023-03-05 17:36:12 +00:00
|
|
|
return getTxes(
|
|
|
|
control.txFactory,
|
|
|
|
targetAccount._id,
|
|
|
|
[],
|
|
|
|
removed.map((p) => p._id)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getEmailNotification (
|
|
|
|
control: TriggerControl,
|
|
|
|
sender: EmployeeAccount,
|
2023-03-09 11:19:05 +00:00
|
|
|
doc: Request | PublicHoliday,
|
2023-03-05 17:36:12 +00:00
|
|
|
space: Ref<Department>,
|
|
|
|
type: Ref<NotificationType>
|
|
|
|
): Promise<Tx[]> {
|
|
|
|
const contacts: Set<Ref<Contact>> = 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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-25 08:34:38 +00:00
|
|
|
// should respect employee settings
|
|
|
|
const accounts = await control.modelDb.findAll(contact.class.EmployeeAccount, {
|
|
|
|
employee: { $in: Array.from(contacts.values()) as Ref<Employee>[] }
|
|
|
|
})
|
|
|
|
for (const account of accounts) {
|
|
|
|
const allowed = await isAllowed(control, account._id, type, notification.providers.EmailNotification)
|
|
|
|
if (!allowed) {
|
|
|
|
contacts.delete(account.employee)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-05 17:36:12 +00:00
|
|
|
const channels = await control.findAll(contact.class.Channel, {
|
|
|
|
provider: contact.channelProvider.Email,
|
|
|
|
attachedTo: { $in: Array.from(contacts) }
|
|
|
|
})
|
|
|
|
|
|
|
|
const senderName = formatName(sender.name)
|
|
|
|
const content = await getContent(doc, senderName, type, control, '')
|
|
|
|
if (content === undefined) return []
|
|
|
|
|
|
|
|
const res: Tx[] = []
|
|
|
|
for (const channel of channels) {
|
|
|
|
const tx = control.txFactory.createTxCreateDoc(
|
|
|
|
notification.class.EmailNotification,
|
|
|
|
notification.space.Notifications,
|
|
|
|
{
|
|
|
|
status: 'new',
|
|
|
|
sender: senderName,
|
|
|
|
receivers: [channel.value],
|
|
|
|
subject: content.subject,
|
|
|
|
text: content.text,
|
|
|
|
html: content.html
|
|
|
|
}
|
|
|
|
)
|
|
|
|
res.push(tx)
|
|
|
|
}
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
export async function OnRequestCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
2023-05-03 14:03:36 +00:00
|
|
|
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<Request>
|
2023-03-05 17:36:12 +00:00
|
|
|
|
|
|
|
const sender = await getEmployeeAccountById(ctx.modifiedBy, control)
|
|
|
|
if (sender === undefined) return []
|
|
|
|
|
|
|
|
const request = TxProcessor.createDoc2Doc(ctx)
|
|
|
|
|
|
|
|
return await getEmailNotification(
|
|
|
|
control,
|
|
|
|
sender,
|
|
|
|
request,
|
|
|
|
ctx.objectSpace as Ref<Department>,
|
2023-03-09 11:19:05 +00:00
|
|
|
hr.ids.CreateRequestNotification
|
2023-03-05 17:36:12 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
export async function OnRequestUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
2023-05-03 14:03:36 +00:00
|
|
|
const ctx = TxProcessor.extractTx(tx) as TxUpdateDoc<Request>
|
2023-03-05 17:36:12 +00:00
|
|
|
|
|
|
|
const sender = await getEmployeeAccountById(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 []
|
|
|
|
|
|
|
|
return await getEmailNotification(
|
|
|
|
control,
|
|
|
|
sender,
|
|
|
|
request,
|
|
|
|
ctx.objectSpace as Ref<Department>,
|
2023-03-09 11:19:05 +00:00
|
|
|
hr.ids.UpdateRequestNotification
|
2023-03-05 17:36:12 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
export async function OnRequestRemove (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
2023-05-03 14:03:36 +00:00
|
|
|
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<Request>
|
2023-03-05 17:36:12 +00:00
|
|
|
|
|
|
|
const sender = await getEmployeeAccountById(ctx.modifiedBy, control)
|
|
|
|
if (sender === undefined) return []
|
|
|
|
|
|
|
|
const request = control.removedMap.get(ctx.objectId) as Request
|
|
|
|
if (request === undefined) return []
|
|
|
|
|
|
|
|
return await getEmailNotification(
|
|
|
|
control,
|
|
|
|
sender,
|
|
|
|
request,
|
|
|
|
ctx.objectSpace as Ref<Department>,
|
2023-03-09 11:19:05 +00:00
|
|
|
hr.ids.RemoveRequestNotification
|
2023-03-05 17:36:12 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
export async function RequestHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
|
|
|
|
const request = doc as Request
|
|
|
|
const employee = (await control.findAll(contact.class.Employee, { _id: request.attachedTo }))[0]
|
2023-03-09 09:05:42 +00:00
|
|
|
const who = getName(employee)
|
2023-03-05 17:36:12 +00:00
|
|
|
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<string> {
|
|
|
|
const request = doc as Request
|
|
|
|
const employee = (await control.findAll(contact.class.Employee, { _id: request.attachedTo }))[0]
|
2023-03-09 09:05:42 +00:00
|
|
|
const who = getName(employee)
|
2023-03-05 17:36:12 +00:00
|
|
|
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}`
|
2022-06-29 05:51:29 +00:00
|
|
|
}
|
|
|
|
|
2023-03-09 11:19:05 +00:00
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
export async function OnPublicHolidayCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
2023-05-03 14:03:36 +00:00
|
|
|
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<PublicHoliday>
|
2023-03-09 11:19:05 +00:00
|
|
|
|
|
|
|
const sender = await getEmployeeAccountById(ctx.modifiedBy, control)
|
|
|
|
if (sender === undefined) return []
|
|
|
|
const employee = await getEmployee(sender.employee, control)
|
|
|
|
if (employee === undefined) return []
|
|
|
|
|
|
|
|
const publicHoliday = TxProcessor.createDoc2Doc(ctx)
|
|
|
|
return await getEmailNotification(
|
|
|
|
control,
|
|
|
|
sender,
|
|
|
|
publicHoliday,
|
|
|
|
publicHoliday.department,
|
|
|
|
hr.ids.CreatePublicHolidayNotification
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
export async function PublicHolidayHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
|
|
|
|
const holiday = doc as PublicHoliday
|
|
|
|
const sender = await getEmployeeAccountById(holiday.modifiedBy, control)
|
|
|
|
if (sender === undefined) return ''
|
|
|
|
const employee = await getEmployee(sender.employee, control)
|
|
|
|
if (employee === undefined) return ''
|
|
|
|
const who = formatName(employee.name)
|
|
|
|
|
|
|
|
const date = `on ${new Date(fromTzDate(holiday.date)).toLocaleDateString()}`
|
|
|
|
|
|
|
|
return `${holiday.title} ${date}<br/>${holiday.description}<br/>Set by ${who}`
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
export async function PublicHolidayTextPresenter (doc: Doc, control: TriggerControl): Promise<string> {
|
|
|
|
const holiday = doc as PublicHoliday
|
|
|
|
const sender = await getEmployeeAccountById(holiday.modifiedBy, control)
|
|
|
|
if (sender === undefined) return ''
|
|
|
|
const employee = await getEmployee(sender.employee, control)
|
|
|
|
if (employee === undefined) return ''
|
|
|
|
const who = formatName(employee.name)
|
|
|
|
|
|
|
|
const date = `on ${new Date(fromTzDate(holiday.date)).toLocaleDateString()}`
|
|
|
|
|
|
|
|
return `${holiday.title} ${date}. ${holiday.description}. Set by ${who}`
|
|
|
|
}
|
|
|
|
|
2022-06-20 04:24:08 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
|
|
export default async () => ({
|
|
|
|
trigger: {
|
2023-03-05 17:36:12 +00:00
|
|
|
OnRequestCreate,
|
|
|
|
OnRequestUpdate,
|
|
|
|
OnRequestRemove,
|
2022-06-29 05:51:29 +00:00
|
|
|
OnDepartmentStaff,
|
2023-05-04 12:35:07 +00:00
|
|
|
OnDepartmentRemove,
|
2023-03-09 11:19:05 +00:00
|
|
|
OnEmployeeDeactivate,
|
|
|
|
OnPublicHolidayCreate
|
2023-03-05 17:36:12 +00:00
|
|
|
},
|
|
|
|
function: {
|
|
|
|
RequestHTMLPresenter,
|
2023-03-09 11:19:05 +00:00
|
|
|
RequestTextPresenter,
|
|
|
|
PublicHolidayHTMLPresenter,
|
|
|
|
PublicHolidayTextPresenter
|
2022-06-20 04:24:08 +00:00
|
|
|
}
|
|
|
|
})
|