HR requests notifications (#2710)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-03-05 23:36:12 +06:00 committed by GitHub
parent ec2235e26b
commit 5696f216ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 533 additions and 78 deletions

View File

@ -127,6 +127,7 @@ export function createModel (builder: Builder): void {
notification.class.NotificationType,
core.space.Model,
{
hidden: false,
label: calendar.string.Reminder,
textTemplate: 'Reminder: {doc}',
htmlTemplate: 'Reminder: {doc}',

View File

@ -305,6 +305,10 @@ export function createModel (builder: Builder): void {
inlineEditor: contact.component.EmployeeArrayEditor
})
builder.mixin(contact.class.Contact, core.class.Class, view.mixin.ArrayEditor, {
inlineEditor: contact.component.ContactArrayEditor
})
builder.mixin(contact.class.Member, core.class.Class, view.mixin.ObjectPresenter, {
presenter: contact.component.MemberPresenter
})

View File

@ -43,6 +43,7 @@ export default mergeIds(contactId, contact, {
MemberPresenter: '' as AnyComponent,
EditMember: '' as AnyComponent,
EmployeeArrayEditor: '' as AnyComponent,
ContactArrayEditor: '' as AnyComponent,
EmployeeEditor: '' as AnyComponent,
CreateEmployee: '' as AnyComponent,
AccountArrayEditor: '' as AnyComponent,

View File

@ -31,6 +31,7 @@
"@hcengineering/contact": "^0.6.11",
"@hcengineering/platform": "^0.6.8",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/notification": "^0.6.5",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/model-workbench": "^0.6.1",
"@hcengineering/model-contact": "^0.6.1",

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import { Employee } from '@hcengineering/contact'
import { Contact, Employee } from '@hcengineering/contact'
import { Arr, Class, Domain, DOMAIN_MODEL, IndexKind, Markup, Ref, Type } from '@hcengineering/core'
import { Department, DepartmentMember, hrId, Request, RequestType, Staff, TzDate } from '@hcengineering/hr'
import {
@ -40,6 +40,7 @@ import view, { classPresenter, createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import { Asset, IntlString } from '@hcengineering/platform'
import hr from './plugin'
import notification from '@hcengineering/notification'
export const DOMAIN_HR = 'hr' as Domain
@ -70,6 +71,9 @@ export class TDepartment extends TSpace implements Department {
@Prop(ArrOf(TypeRef(hr.class.DepartmentMember)), contact.string.Members)
declare members: Arr<Ref<DepartmentMember>>
@Prop(ArrOf(TypeRef(contact.class.Contact)), hr.string.Subscribers)
subscribers?: Arr<Ref<Contact>>
}
@Model(hr.class.DepartmentMember, contact.class.EmployeeAccount)
@ -405,6 +409,45 @@ export function createModel (builder: Builder): void {
builder.mixin(hr.class.Request, core.class.Class, view.mixin.ObjectPresenter, {
presenter: hr.component.RequestPresenter
})
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
{
hidden: true,
label: hr.string.Request,
textTemplate: 'New request: {doc}',
htmlTemplate: 'New request: {doc}',
subjectTemplate: 'New request'
},
hr.ids.CreateRequestNotifcation
)
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
{
hidden: true,
label: hr.string.Request,
textTemplate: 'Request updated: {doc}',
htmlTemplate: 'Request updated: {doc}',
subjectTemplate: 'Request updated'
},
hr.ids.UpdateRequestNotifcation
)
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
{
hidden: true,
label: hr.string.Request,
textTemplate: 'Request removed: {doc}',
htmlTemplate: 'Request removed: {doc}',
subjectTemplate: 'Request removed'
},
hr.ids.RemoveRequestNotifcation
)
}
export { hrOperation } from './migration'

View File

@ -31,7 +31,8 @@ export default mergeIds(hrId, hr, {
PTO2: '' as IntlString,
Remote: '' as IntlString,
Overtime: '' as IntlString,
Overtime2: '' as IntlString
Overtime2: '' as IntlString,
Subscribers: '' as IntlString
},
component: {
Structure: '' as AnyComponent,

View File

@ -84,6 +84,7 @@ export class TNotificationType extends TDoc implements NotificationType {
textTemplate!: string
htmlTemplate!: string
subjectTemplate!: string
hidden!: boolean
}
@Model(notification.class.NotificationProvider, core.class.Doc, DOMAIN_MODEL)
@ -130,6 +131,7 @@ export function createModel (builder: Builder): void {
core.space.Model,
{
label: notification.string.MentionNotification,
hidden: false,
textTemplate: '{sender} mentioned you in {doc} {data}',
htmlTemplate: '<p><b>{sender}</b> mentioned you in {doc}</p> {data}',
subjectTemplate: 'You was mentioned in {doc}'
@ -141,7 +143,8 @@ export function createModel (builder: Builder): void {
notification.class.NotificationType,
core.space.Model,
{
label: notification.string.DMNotification,
label: notification.string.DM,
hidden: false,
textTemplate: '{sender} has send you a message: {doc} {data}',
htmlTemplate: '<p><b>{sender}</b> has send you a message {doc}</p> {data}',
subjectTemplate: 'You have new DM message in {doc}'

View File

@ -21,6 +21,7 @@ import { AnyComponent } from '@hcengineering/ui'
export default mergeIds(notificationId, notification, {
string: {
LastView: '' as IntlString,
DM: '' as IntlString,
DMNotification: '' as IntlString,
MentionNotification: '' as IntlString,
PlatformNotification: '' as IntlString,

View File

@ -28,6 +28,8 @@
"@hcengineering/core": "^0.6.21",
"@hcengineering/model": "^0.6.0",
"@hcengineering/platform": "^0.6.8",
"@hcengineering/hr": "^0.6.0",
"@hcengineering/server-notification": "^0.6.0",
"@hcengineering/server-hr": "^0.6.0",
"@hcengineering/server-core": "^0.6.1"
}

View File

@ -18,9 +18,31 @@ import { Builder } from '@hcengineering/model'
import serverCore from '@hcengineering/server-core'
import core from '@hcengineering/core'
import serverHr from '@hcengineering/server-hr'
import serverNotification from '@hcengineering/server-notification'
import hr from '@hcengineering/hr'
export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverHr.trigger.OnDepartmentStaff
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverHr.trigger.OnRequestCreate
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverHr.trigger.OnRequestUpdate
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverHr.trigger.OnRequestRemove
})
builder.mixin(hr.class.Request, core.class.Class, serverNotification.mixin.HTMLPresenter, {
presenter: serverHr.function.RequestHTMLPresenter
})
builder.mixin(hr.class.Request, core.class.Class, serverNotification.mixin.TextPresenter, {
presenter: serverHr.function.RequestTextPresenter
})
}

View File

@ -572,6 +572,7 @@ export function createModel (builder: Builder): void {
core.space.Model,
{
label: task.string.Assigned,
hidden: false,
textTemplate: '{doc} was assigned to you by {sender}',
htmlTemplate: '<p>{doc} was assigned to you by {sender}</p>',
subjectTemplate: '{doc} was assigned to you'

View File

@ -15,18 +15,18 @@
<script lang="ts">
import type { Class, Doc, Ref } from '@hcengineering/core'
import { createQuery } from '../utils'
import { Person } from '@hcengineering/contact'
import { Person as Contact } from '@hcengineering/contact'
import Avatar from './Avatar.svelte'
import { IconSize } from '@hcengineering/ui'
export let _class: Ref<Class<Doc>>
export let items: Ref<Person>[] = []
export let items: Ref<Contact>[] = []
export let size: IconSize
export let limit: number = 3
let persons: Person[] = []
let persons: Contact[] = []
const query = createQuery()
$: query.query<Person>(
$: query.query<Contact>(
_class,
{ _id: { $in: items } },
(result) => {

View File

@ -368,11 +368,11 @@ export async function getAttributeEditor (
}
if (editorMixin.inlineEditor === undefined) {
if (presenterClass.category === 'array') {
// NOTE: Don't show error for array attributes for compatibility with previous implementation
} else {
console.error(getAttributeEditorNotFoundError(_class, key))
}
// if (presenterClass.category === 'array') {
// // NOTE: Don't show error for array attributes for compatibility with previous implementation
// } else {
console.error(getAttributeEditorNotFoundError(_class, key))
// }
return
}

View File

@ -0,0 +1,31 @@
<script lang="ts">
import { Contact } from '@hcengineering/contact'
import { Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import ContactList from './ContactList.svelte'
export let label: IntlString
export let value: Ref<Contact>[]
export let onChange: (refs: Ref<Contact>[]) => void
export let readonly = false
let timer: any
function onUpdate (evt: CustomEvent<Ref<Contact>[]>): void {
clearTimeout(timer)
timer = setTimeout(() => {
onChange(evt.detail)
}, 500)
}
</script>
<ContactList
items={value}
{label}
on:update={onUpdate}
kind={'link'}
size={'medium'}
justify={'left'}
width={'100%'}
{readonly}
/>

View File

@ -0,0 +1,99 @@
<!--
// Copyright © 2023 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.
-->
<script lang="ts">
import { Contact } from '@hcengineering/contact'
import type { Class, DocumentQuery, Ref } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform'
import presentation, { CombineAvatars, createQuery, IconMembers, UsersPopup } from '@hcengineering/presentation'
import { Button, ButtonKind, ButtonSize, Label, showPopup, TooltipAlignment } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { ContactPresenter } from '..'
import contact from '../plugin'
export let items: Ref<Contact>[] = []
export let _class: Ref<Class<Contact>> = contact.class.Contact
export let label: IntlString
export let docQuery: DocumentQuery<Contact> | undefined = {
active: true
}
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = undefined
export let labelDirection: TooltipAlignment | undefined = undefined
export let emptyLabel = contact.string.Contacts
export let readonly: boolean = false
let contacts: Contact[] = []
const query = createQuery()
$: query.query<Contact>(_class, { _id: { $in: items } }, (result) => {
contacts = result
})
const dispatch = createEventDispatcher()
async function addPerson (evt: Event): Promise<void> {
showPopup(
UsersPopup,
{
_class,
label,
docQuery,
multiSelect: true,
allowDeselect: false,
selectedUsers: items,
readonly
},
evt.target as HTMLElement,
undefined,
(result) => {
if (result != null) {
items = result
dispatch('update', items)
}
}
)
}
</script>
<Button
icon={contacts.length === 0 ? IconMembers : undefined}
label={contacts.length === 0 ? emptyLabel : undefined}
notSelected={contacts.length === 0}
width={width ?? 'min-content'}
{kind}
{size}
{justify}
showTooltip={{ label, direction: labelDirection }}
on:click={addPerson}
>
<svelte:fragment slot="content">
{#if contacts.length > 0}
<div class="flex-row-center flex-nowrap pointer-events-none">
{#if contacts.length === 1}
<ContactPresenter value={contacts[0]} isInteractive={false} />
{:else}
<CombineAvatars {_class} bind:items size={'inline'} />
<span class="overflow-label ml-1-5">
<Label label={presentation.string.NumberMembers} params={{ count: contacts.length }} />
</span>
{/if}
</div>
{/if}
</svelte:fragment>
</Button>

View File

@ -55,6 +55,7 @@ import EmployeeRefPresenter from './components/EmployeeRefPresenter.svelte'
import ChannelFilter from './components/ChannelFilter.svelte'
import AccountBox from './components/AccountBox.svelte'
import MergeEmployee from './components/MergeEmployee.svelte'
import ContactArrayEditor from './components/ContactArrayEditor.svelte'
import contact from './plugin'
import {
employeeSort,
@ -154,6 +155,7 @@ export default async (): Promise<Resources> => ({
OpenChannel: openChannelURL
},
component: {
ContactArrayEditor,
PersonEditor,
OrganizationEditor,
ContactPresenter,

View File

@ -40,6 +40,7 @@
"Member": "Member",
"Members": "Members",
"NoMembers": "No members added",
"AddMember": "Add member"
"AddMember": "Add member",
"Subscribers": "Subscribers"
}
}

View File

@ -40,6 +40,7 @@
"Member": "Сотрудник",
"Members": "Сотрудники",
"NoMembers": "Нет добавленных сотрудников",
"AddMember": "Добавить сотрудника"
"AddMember": "Добавить сотрудника",
"Subscribers": "Подписчики"
}
}

View File

@ -17,13 +17,12 @@
import calendar from '@hcengineering/calendar'
import { Employee } from '@hcengineering/contact'
import core, { DocumentQuery, generateId, Ref } from '@hcengineering/core'
import { Request, RequestType, Staff } from '@hcengineering/hr'
import { Request, RequestType, Staff, toTzDate } from '@hcengineering/hr'
import { translate } from '@hcengineering/platform'
import { Card, createQuery, EmployeeBox, getClient } from '@hcengineering/presentation'
import ui, { Button, DateRangePresenter, DropdownLabelsIntl, IconAttachment } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import hr from '../plugin'
import { toTzDate } from '../utils'
export let staff: Staff
export let date: Date

View File

@ -14,10 +14,9 @@
// limitations under the License.
-->
<script lang="ts">
import { Request } from '@hcengineering/hr'
import { fromTzDate, Request, tzDateEqual } from '@hcengineering/hr'
import { getClient } from '@hcengineering/presentation'
import { DateRangePresenter, Label } from '@hcengineering/ui'
import { fromTzDate, tzDateEqual } from '../utils'
export let value: Request | null | undefined
export let noShift: boolean = false

View File

@ -16,10 +16,12 @@
import { CalendarMode } from '@hcengineering/calendar-resources'
import { Employee, EmployeeAccount } from '@hcengineering/contact'
import { DocumentQuery, getCurrentAccount, Ref } from '@hcengineering/core'
import type { Department, Request, RequestType, Staff } from '@hcengineering/hr'
import { Department, fromTzDate, Request, RequestType, Staff } from '@hcengineering/hr'
import { createQuery } from '@hcengineering/presentation'
import tracker, { Issue } from '@hcengineering/tracker'
import { Label } from '@hcengineering/ui'
import hr from '../plugin'
import { EmployeeReports, getEndDate, getStartDate } from '../utils'
import MonthTableView from './schedule/MonthTableView.svelte'
import MonthView from './schedule/MonthView.svelte'
import YearView from './schedule/YearView.svelte'
@ -149,9 +151,6 @@
const reportQuery = createQuery()
import tracker, { Issue } from '@hcengineering/tracker'
import { EmployeeReports, fromTzDate, getEndDate, getStartDate } from '../utils'
let timeReports: Map<Ref<Employee>, EmployeeReports> = new Map()
$: reportQuery.query(

View File

@ -13,11 +13,10 @@
// limitations under the License.
-->
<script lang="ts">
import { TzDate } from '@hcengineering/hr'
import { fromTzDate, toTzDate, TzDate } from '@hcengineering/hr'
// import { IntlString } from '@hcengineering/platform'
import { DateRangePresenter } from '@hcengineering/ui'
import { fromTzDate, toTzDate } from '../utils'
export let value: TzDate | null | undefined
export let onChange: (value: TzDate | null | undefined) => void

View File

@ -1,6 +1,6 @@
import { Employee, formatName } from '@hcengineering/contact'
import { Ref, TxOperations } from '@hcengineering/core'
import { Department, Request, RequestType, Staff, TzDate } from '@hcengineering/hr'
import { Department, fromTzDate, Request, RequestType, Staff } from '@hcengineering/hr'
import { MessageBox } from '@hcengineering/presentation'
import { Issue, TimeSpendReport } from '@hcengineering/tracker'
import { isWeekend, MILLISECONDS_IN_DAY, showPopup } from '@hcengineering/ui'
@ -56,32 +56,6 @@ export async function addMember (client: TxOperations, employee?: Employee, valu
}
}
/**
* @public
*/
export function toTzDate (date: Date): TzDate {
return {
year: date.getFullYear(),
month: date.getMonth(),
day: date.getDate(),
offset: date.getTimezoneOffset()
}
}
/**
* @public
*/
export function fromTzDate (tzDate: TzDate): number {
return new Date().setFullYear(tzDate?.year ?? 0, tzDate.month, tzDate.day)
}
/**
* @public
*/
export function tzDateEqual (tzDate: TzDate, tzDate2: TzDate): boolean {
return tzDate.year === tzDate2.year && tzDate.month === tzDate2.month && tzDate.day === tzDate2.day
}
/**
* @public
*/

View File

@ -29,7 +29,8 @@
"@hcengineering/contact": "^0.6.11",
"@hcengineering/core": "^0.6.21",
"@hcengineering/platform": "^0.6.8",
"@hcengineering/view": "^0.6.2"
"@hcengineering/view": "^0.6.2",
"@hcengineering/notification": "^0.6.5"
},
"repository": "https://github.com/hcenginneing/anticrm",
"publishConfig": {

View File

@ -13,11 +13,12 @@
// limitations under the License.
//
import type { Employee, EmployeeAccount } from '@hcengineering/contact'
import type { Contact, Employee, EmployeeAccount } from '@hcengineering/contact'
import type { Arr, AttachedDoc, Class, Doc, Markup, Mixin, Ref, Space, Type } from '@hcengineering/core'
import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import { Viewlet } from '@hcengineering/view'
import { NotificationType } from '@hcengineering/notification'
/**
* @public
@ -30,6 +31,7 @@ export interface Department extends Space {
comments?: number
channels?: number
members: Arr<Ref<DepartmentMember>>
subscribers?: Arr<Ref<Contact>>
}
/**
@ -126,7 +128,10 @@ const hr = plugin(hrId, {
PTO2: '' as Ref<RequestType>,
Remote: '' as Ref<RequestType>,
Overtime: '' as Ref<RequestType>,
Overtime2: '' as Ref<RequestType>
Overtime2: '' as Ref<RequestType>,
CreateRequestNotifcation: '' as Ref<NotificationType>,
UpdateRequestNotifcation: '' as Ref<NotificationType>,
RemoveRequestNotifcation: '' as Ref<NotificationType>
},
viewlet: {
TableMember: '' as Ref<Viewlet>,
@ -134,6 +139,8 @@ const hr = plugin(hrId, {
}
})
export * from './utils'
/**
* @public
*/

41
plugins/hr/src/utils.ts Normal file
View File

@ -0,0 +1,41 @@
//
// Copyright © 2023 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 { TzDate } from '.'
/**
* @public
*/
export function toTzDate (date: Date): TzDate {
return {
year: date.getFullYear(),
month: date.getMonth(),
day: date.getDate(),
offset: date.getTimezoneOffset()
}
}
/**
* @public
*/
export function fromTzDate (tzDate: TzDate): number {
return new Date().setFullYear(tzDate?.year ?? 0, tzDate.month, tzDate.day)
}
/**
* @public
*/
export function tzDateEqual (tzDate: TzDate, tzDate2: TzDate): boolean {
return tzDate.year === tzDate2.year && tzDate.month === tzDate2.month && tzDate.day === tzDate2.day
}

View File

@ -5,7 +5,8 @@
"Notifications": "Notifications",
"NoNotifications": "No notifications",
"MentionNotification": "Mentioned",
"DMNotification": "Sent you a message",
"DM": "Direct message",
"DMNotification": "Sent you as message",
"EmailNotification": "by email",
"PlatformNotification": "in platform",
"Track": "Track",

View File

@ -5,6 +5,7 @@
"Notifications": "Уведомления",
"NoNotifications": "Нет уведомлений",
"MentionNotification": "Упомянул",
"DM": "Личное сообщение",
"DMNotification": "Отправил сообщение",
"EmailNotification": "по email",
"PlatformNotification": "в системе",

View File

@ -39,7 +39,7 @@
Map<Ref<NotificationProvider>, NotificationSetting>
>()
typeQuery.query(notification.class.NotificationType, {}, (res) => (types = res))
typeQuery.query(notification.class.NotificationType, { hidden: false }, (res) => (types = res))
providersQuery.query(notification.class.NotificationProvider, {}, (res) => (providers = res))
settingsQuery.query(notification.class.NotificationSetting, { space }, (res) => {
settings = convertToMap(res)

View File

@ -76,6 +76,7 @@ export enum NotificationStatus {
* @public
*/
export interface NotificationType extends Doc {
hidden: boolean
label: IntlString
textTemplate: string
htmlTemplate: string

View File

@ -30,6 +30,9 @@
"@hcengineering/platform": "^0.6.8",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/contact": "^0.6.11",
"@hcengineering/server-notification": "^0.6.0",
"@hcengineering/server-notification-resources": "^0.6.0",
"@hcengineering/notification": "^0.6.5",
"@hcengineering/hr": "^0.6.0"
}
}

View File

@ -13,10 +13,25 @@
// limitations under the License.
//
import contact, { Employee } from '@hcengineering/contact'
import core, { Ref, SortingOrder, Tx, TxFactory, TxMixin, TxProcessor, TxUpdateDoc } from '@hcengineering/core'
import hr, { Department, DepartmentMember, Staff } from '@hcengineering/hr'
import contact, { Contact, Employee, EmployeeAccount, formatName } from '@hcengineering/contact'
import core, {
Doc,
Ref,
SortingOrder,
toIdMap,
Tx,
TxCreateDoc,
TxFactory,
TxMixin,
TxProcessor,
TxUpdateDoc
} from '@hcengineering/core'
import hr, { Department, DepartmentMember, fromTzDate, Request, Staff, tzDateEqual } from '@hcengineering/hr'
import notification, { NotificationType } from '@hcengineering/notification'
import { translate } from '@hcengineering/platform'
import { TriggerControl } from '@hcengineering/server-core'
import { getEmployeeAccountById } from '@hcengineering/server-notification'
import { getContent } from '@hcengineering/server-notification-resources'
async function getOldDepartment (
currentTx: TxMixin<Employee, Staff> | TxUpdateDoc<Employee>,
@ -39,19 +54,23 @@ async function getOldDepartment (
return lastDepartment
}
async function buildHierarchy (_id: Ref<Department>, control: TriggerControl): Promise<Ref<Department>[]> {
const res: Ref<Department>[] = []
if (_id === hr.ids.Head) return [hr.ids.Head]
const department = (
await control.findAll(hr.class.Department, {
_id
})
)[0]
if (department !== undefined) {
const ancestors = await buildHierarchy(department.space, control)
return [department._id, ...ancestors]
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
}
return res
}
function exlude (first: Ref<Department>[], second: Ref<Department>[]): Ref<Department>[] {
@ -112,16 +131,21 @@ export async function OnDepartmentStaff (tx: Tx, control: TriggerControl): Promi
if (departmentId === null) {
if (lastDepartment !== undefined) {
const removed = await buildHierarchy(lastDepartment, control)
return getTxes(control.txFactory, targetAccount._id, [], removed)
return getTxes(
control.txFactory,
targetAccount._id,
[],
removed.map((p) => p._id)
)
}
}
const push = await buildHierarchy(departmentId, control)
const push = (await buildHierarchy(departmentId, control)).map((p) => p._id)
if (lastDepartment === undefined) {
return getTxes(control.txFactory, targetAccount._id, push)
}
let removed = await buildHierarchy(lastDepartment, control)
let removed = (await buildHierarchy(lastDepartment, control)).map((p) => p._id)
const added = exlude(removed, push)
removed = exlude(push, removed)
return getTxes(control.txFactory, targetAccount._id, added, removed)
@ -153,13 +177,188 @@ export async function OnEmployeeDeactivate (tx: Tx, control: TriggerControl): Pr
if (lastDepartment === undefined) return []
const removed = await buildHierarchy(lastDepartment, control)
return getTxes(control.txFactory, targetAccount._id, [], removed)
return getTxes(
control.txFactory,
targetAccount._id,
[],
removed.map((p) => p._id)
)
}
async function getEmailNotification (
control: TriggerControl,
sender: EmployeeAccount,
doc: Request,
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)
}
}
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[]> {
const actualTx = TxProcessor.extractTx(tx)
if (core.class.TxCreateDoc !== actualTx._class) {
return []
}
const ctx = actualTx as TxCreateDoc<Request>
if (ctx.objectClass !== hr.class.Request) {
return []
}
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>,
hr.ids.CreateRequestNotifcation
)
}
/**
* @public
*/
export async function OnRequestUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx)
if (core.class.TxUpdateDoc !== actualTx._class) {
return []
}
const ctx = actualTx as TxUpdateDoc<Request>
if (ctx.objectClass !== hr.class.Request) {
return []
}
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>,
hr.ids.UpdateRequestNotifcation
)
}
/**
* @public
*/
export async function OnRequestRemove (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx)
if (core.class.TxCreateDoc !== actualTx._class) {
return []
}
const ctx = actualTx as TxCreateDoc<Request>
if (ctx.objectClass !== hr.class.Request) {
return []
}
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>,
hr.ids.RemoveRequestNotifcation
)
}
/**
* @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]
const who = formatName(employee.name)
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]
const who = formatName(employee.name)
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}`
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
trigger: {
OnRequestCreate,
OnRequestUpdate,
OnRequestRemove,
OnDepartmentStaff,
OnEmployeeDeactivate
},
function: {
RequestHTMLPresenter,
RequestTextPresenter
}
})

View File

@ -29,6 +29,7 @@
"dependencies": {
"@hcengineering/core": "^0.6.21",
"@hcengineering/platform": "^0.6.8",
"@hcengineering/server-core": "^0.6.1"
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-notification": "^0.6.0"
}
}

View File

@ -16,6 +16,7 @@
import type { Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { TriggerFunc } from '@hcengineering/server-core'
import { Presenter } from '@hcengineering/server-notification'
/**
* @public
@ -27,6 +28,13 @@ export const serverHrId = 'server-hr' as Plugin
*/
export default plugin(serverHrId, {
trigger: {
OnDepartmentStaff: '' as Resource<TriggerFunc>
OnDepartmentStaff: '' as Resource<TriggerFunc>,
OnRequestCreate: '' as Resource<TriggerFunc>,
OnRequestUpdate: '' as Resource<TriggerFunc>,
OnRequestRemove: '' as Resource<TriggerFunc>
},
function: {
RequestHTMLPresenter: '' as Resource<Presenter>,
RequestTextPresenter: '' as Resource<Presenter>
}
})

View File

@ -168,7 +168,10 @@ function fillTemplate (template: string, sender: string, doc: string, data: stri
return res
}
async function getContent (
/**
* @public
*/
export async function getContent (
doc: Doc | undefined,
sender: string,
type: Ref<NotificationType>,
@ -414,6 +417,8 @@ async function getBacklinkDoc (backlink: Backlink, control: TriggerControl): Pro
)[0]
}
export * from './types'
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
trigger: {

View File

@ -1,3 +1,6 @@
/**
* @public
*/
export interface Content {
text: string
html: string