mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
fix: proper hr members update (#10977)
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
@@ -22,7 +22,8 @@ import {
|
||||
DOMAIN_MODEL_TX,
|
||||
type TxCUD
|
||||
} from '@hcengineering/core'
|
||||
import { type Department } from '@hcengineering/hr'
|
||||
import contact, { type Employee, type Person } from '@hcengineering/contact'
|
||||
import { type Department, type Staff } from '@hcengineering/hr'
|
||||
import {
|
||||
migrateSpace,
|
||||
tryMigrate,
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
type MigrationUpgradeClient
|
||||
} from '@hcengineering/model'
|
||||
import core, { DOMAIN_SPACE, getAccountsFromTxes } from '@hcengineering/model-core'
|
||||
import { DOMAIN_CONTACT } from '@hcengineering/model-contact'
|
||||
|
||||
import hr, { DOMAIN_HR, hrId } from './index'
|
||||
|
||||
@@ -103,6 +105,52 @@ async function migrateDepartmentMembersToEmployee (client: MigrationClient): Pro
|
||||
}
|
||||
}
|
||||
|
||||
async function rebuildDepartmentMembersFromStaff (client: MigrationClient): Promise<void> {
|
||||
const departments = await client.find<Department>(DOMAIN_HR, { _class: hr.class.Department })
|
||||
const departmentById = new Map(departments.map((department) => [department._id, department]))
|
||||
const membersByDepartment = new Map<Ref<Department>, Set<Ref<Employee>>>(
|
||||
departments.map((department) => [department._id, new Set<Ref<Employee>>()])
|
||||
)
|
||||
|
||||
const getHierarchy = (department: Ref<Department>): Ref<Department>[] => {
|
||||
const result: Ref<Department>[] = []
|
||||
let current: Ref<Department> | undefined = department
|
||||
|
||||
while (current !== undefined) {
|
||||
if (result.includes(current)) {
|
||||
break
|
||||
}
|
||||
|
||||
const currentDepartment = departmentById.get(current)
|
||||
if (currentDepartment === undefined) {
|
||||
break
|
||||
}
|
||||
|
||||
result.push(currentDepartment._id)
|
||||
current = currentDepartment.parent ?? (currentDepartment._id !== hr.ids.Head ? hr.ids.Head : undefined)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const persons = await client.find<Person>(DOMAIN_CONTACT, { _class: contact.class.Person })
|
||||
for (const person of persons) {
|
||||
const staff = client.hierarchy.asIf<Person, Staff>(person, hr.mixin.Staff)
|
||||
if (staff?.department === undefined || !staff.active) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const department of getHierarchy(staff.department)) {
|
||||
membersByDepartment.get(department)?.add(person._id as Ref<Employee>)
|
||||
}
|
||||
}
|
||||
|
||||
for (const department of departments) {
|
||||
const members = Array.from(membersByDepartment.get(department._id) ?? [])
|
||||
await client.update(DOMAIN_HR, { _id: department._id }, { members })
|
||||
}
|
||||
}
|
||||
|
||||
export const hrOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient, mode): Promise<void> {
|
||||
await tryMigrate(mode, client, hrId, [
|
||||
@@ -120,6 +168,10 @@ export const hrOperation: MigrateOperation = {
|
||||
{
|
||||
state: 'migrateDepartmentMembersToEmployee',
|
||||
func: migrateDepartmentMembersToEmployee
|
||||
},
|
||||
{
|
||||
state: 'rebuildDepartmentMembersFromStaff',
|
||||
func: rebuildDepartmentMembersFromStaff
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
@@ -41,6 +41,14 @@ export function createModel (builder: Builder): void {
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverHr.trigger.OnDepartmentUpdate,
|
||||
txMatch: {
|
||||
objectClass: hr.class.Department,
|
||||
_class: core.class.TxUpdateDoc
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverHr.trigger.OnRequestCreate,
|
||||
txMatch: {
|
||||
|
||||
@@ -77,7 +77,8 @@
|
||||
|
||||
$: dragPersonId = dragPerson?._id
|
||||
|
||||
$: values = allEmployees.filter((it) => it.department === value._id && it._id !== dragPersonId)
|
||||
$: members = new Set<Ref<Staff>>(value.members as Ref<Staff>[])
|
||||
$: values = allEmployees.filter((it) => members.has(it._id) && it._id !== dragPersonId)
|
||||
|
||||
$: dragging = value._id === dragOver?._id && dragPersonId !== undefined
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact from '@hcengineering/contact'
|
||||
import contact, { Employee } from '@hcengineering/contact'
|
||||
import { UsersPopup } from '@hcengineering/contact-resources'
|
||||
import { Ref, WithLookup } from '@hcengineering/core'
|
||||
import { Department, Staff } from '@hcengineering/hr'
|
||||
import { Department } from '@hcengineering/hr'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Button, IconAdd, Label, Scroller, Section, eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
||||
import { Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||
@@ -42,7 +42,7 @@
|
||||
}
|
||||
)
|
||||
|
||||
function add (e: MouseEvent) {
|
||||
function add (e: MouseEvent): void {
|
||||
showPopup(
|
||||
UsersPopup,
|
||||
{
|
||||
@@ -50,19 +50,15 @@
|
||||
docQuery: {
|
||||
active: true
|
||||
},
|
||||
ignoreUsers: memberItems.map((it) => it._id)
|
||||
ignoreUsers: members
|
||||
},
|
||||
eventToHTMLElement(e),
|
||||
(res) => addMember(client, res, value)
|
||||
)
|
||||
}
|
||||
|
||||
let memberItems: Staff[] = []
|
||||
|
||||
const membersQuery = createQuery()
|
||||
$: membersQuery.query(hr.mixin.Staff, { department: objectId }, (result) => {
|
||||
memberItems = result
|
||||
})
|
||||
let members: Array<Ref<Employee>> = []
|
||||
$: members = value?.members ?? []
|
||||
|
||||
let preference: ViewletPreference | undefined
|
||||
let loading = false
|
||||
@@ -85,14 +81,14 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
{#if (value?.members.length ?? 0) > 0}
|
||||
{#if members.length > 0}
|
||||
<Scroller>
|
||||
<Table
|
||||
_class={hr.mixin.Staff}
|
||||
config={preference?.config ?? viewlet?.config ?? []}
|
||||
options={viewlet?.options}
|
||||
query={{ department: objectId }}
|
||||
loadingProps={{ length: value?.members.length ?? 0 }}
|
||||
query={{ _id: { $in: members } }}
|
||||
loadingProps={{ length: members.length }}
|
||||
/>
|
||||
</Scroller>
|
||||
{:else}
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
? getEndDate(currentDate.getFullYear(), 11)
|
||||
: getEndDate(currentDate.getFullYear(), currentDate.getMonth())
|
||||
|
||||
$: departments = [department, ...getDescendants(department, descendants, new Set())]
|
||||
$: staffIdsForOpenedDepartments = staff.filter((p) => departments.includes(p.department)).map((p) => p._id)
|
||||
$: departmentMembers = new Set<Ref<Staff>>((departmentById.get(department)?.members ?? []) as Ref<Staff>[])
|
||||
$: staffIdsForOpenedDepartments = staff.filter((p) => departmentMembers.has(p._id)).map((p) => p._id)
|
||||
|
||||
const lq = createQuery()
|
||||
const typeQuery = createQuery()
|
||||
@@ -81,23 +81,7 @@
|
||||
|
||||
let employeeRequests = new Map<Ref<Staff>, Request[]>()
|
||||
|
||||
function getDescendants (
|
||||
department: Ref<Department>,
|
||||
descendants: Map<Ref<Department>, Department[]>,
|
||||
visited: Set<string>
|
||||
): Ref<Department>[] {
|
||||
const res = (descendants.get(department) ?? []).map((p) => p._id)
|
||||
for (const department of res) {
|
||||
const has = visited.has(department)
|
||||
if (!has) {
|
||||
visited.add(department)
|
||||
res.push(...getDescendants(department, descendants, visited))
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
let departmentStaff: Staff[]
|
||||
let departmentStaff: Staff[] = []
|
||||
let editableList: Ref<Employee>[] = []
|
||||
|
||||
function update (staffIdsForOpenedDepartments: Ref<Staff>[], startDate: Date, endDate: Date) {
|
||||
@@ -136,14 +120,11 @@
|
||||
function pushChilds (
|
||||
department: Ref<Department>,
|
||||
departmentStaff: Staff[],
|
||||
descendants: Map<Ref<Department>, Department[]>
|
||||
departmentById: Map<Ref<Department>, Department>
|
||||
): void {
|
||||
const staff = departmentStaff.filter((p) => p.department === department)
|
||||
const members = new Set<Ref<Staff>>((departmentById.get(department)?.members ?? []) as Ref<Staff>[])
|
||||
const staff = departmentStaff.filter((p) => members.has(p._id))
|
||||
editableList.push(...staff.filter((p) => !editableList.includes(p._id)).map((p) => p._id))
|
||||
const desc = descendants.get(department) ?? []
|
||||
for (const des of desc) {
|
||||
pushChilds(des._id, departmentStaff, descendants)
|
||||
}
|
||||
}
|
||||
|
||||
function isEditable (department: Department): boolean {
|
||||
@@ -155,17 +136,17 @@
|
||||
department: Ref<Department>,
|
||||
departmentStaff: Staff[],
|
||||
descendants: Map<Ref<Department>, Department[]>
|
||||
) {
|
||||
): void {
|
||||
const dep = departmentById.get(department)
|
||||
if (dep === undefined) return
|
||||
if (isEditable(dep)) {
|
||||
pushChilds(dep._id, departmentStaff, descendants)
|
||||
pushChilds(dep._id, departmentStaff, departmentById)
|
||||
} else {
|
||||
const descendantDepartments = descendants.get(dep._id)
|
||||
if (descendantDepartments !== undefined) {
|
||||
for (const department of descendantDepartments) {
|
||||
if (isEditable(department)) {
|
||||
pushChilds(department._id, departmentStaff, descendants)
|
||||
pushChilds(department._id, departmentStaff, departmentById)
|
||||
} else {
|
||||
checkDepartmentEditable(departmentById, department._id, departmentStaff, descendants)
|
||||
}
|
||||
@@ -178,7 +159,7 @@
|
||||
departmentById: Map<Ref<Department>, Department>,
|
||||
departmentStaff: Staff[],
|
||||
descendants: Map<Ref<Department>, Department[]>
|
||||
) {
|
||||
): void {
|
||||
editableList = [currentEmployee]
|
||||
checkDepartmentEditable(departmentById, hr.ids.Head, departmentStaff, descendants)
|
||||
editableList = editableList
|
||||
@@ -186,15 +167,16 @@
|
||||
|
||||
function updateStaff (
|
||||
staff: Staff[],
|
||||
departments: Ref<Department>[],
|
||||
staffIdsForOpenedDepartments: Ref<Staff>[],
|
||||
descendants: Map<Ref<Department>, Department[]>,
|
||||
departmentById: Map<Ref<Department>, Department>
|
||||
) {
|
||||
departmentStaff = staff.filter((p) => departments.includes(p.department))
|
||||
): void {
|
||||
const departmentMembers = new Set(staffIdsForOpenedDepartments)
|
||||
departmentStaff = staff.filter((p) => departmentMembers.has(p._id))
|
||||
updateEditableList(departmentById, departmentStaff, descendants)
|
||||
}
|
||||
|
||||
$: updateStaff(staff, departments, descendants, departmentById)
|
||||
$: updateStaff(staff, staffIdsForOpenedDepartments, descendants, departmentById)
|
||||
|
||||
const reportQuery = createQuery()
|
||||
|
||||
@@ -287,7 +269,7 @@
|
||||
}
|
||||
return map
|
||||
}
|
||||
let staffDepartmentMap = new Map()
|
||||
let staffDepartmentMap = new Map<Ref<Staff>, Department[]>()
|
||||
$: void getDepartmentsForEmployee(departmentStaff).then((res) => {
|
||||
staffDepartmentMap = res
|
||||
})
|
||||
@@ -295,18 +277,21 @@
|
||||
function getDepartmentHolidays (department: Ref<Department>): Date[] {
|
||||
const parents = ancestors.get(department) ?? []
|
||||
|
||||
const result = []
|
||||
const result = new Map<string, Date>()
|
||||
const addHoliday = (holiday: Date): void => {
|
||||
result.set(`${holiday.getFullYear()}-${holiday.getMonth()}-${holiday.getDate()}`, holiday)
|
||||
}
|
||||
|
||||
// get own holidays
|
||||
const holidays = holidaysMap.get(department) ?? []
|
||||
result.push(...holidays)
|
||||
holidays.forEach(addHoliday)
|
||||
|
||||
// get ancestor holidays
|
||||
for (const parent of parents) {
|
||||
const parentHolidays = holidaysMap.get(parent) ?? []
|
||||
result.push(...parentHolidays)
|
||||
parentHolidays.forEach(addHoliday)
|
||||
}
|
||||
return result
|
||||
return [...result.values()]
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getName } from '@hcengineering/contact'
|
||||
import contact, { Avatar } from '@hcengineering/contact-resources'
|
||||
import { Avatar } from '@hcengineering/contact-resources'
|
||||
import contact from '@hcengineering/contact-resources/src/plugin'
|
||||
import hr, { Department, Staff } from '@hcengineering/hr'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
|
||||
@@ -63,6 +63,44 @@ async function getOldDepartment (
|
||||
return lastDepartment
|
||||
}
|
||||
|
||||
async function getOldDepartmentParent (
|
||||
currentTx: TxUpdateDoc<Department>,
|
||||
control: TriggerControl
|
||||
): Promise<Ref<Department> | undefined> {
|
||||
const updateTxes = await control.findAll<TxUpdateDoc<Department>>(
|
||||
control.ctx,
|
||||
core.class.TxUpdateDoc,
|
||||
{
|
||||
objectId: currentTx.objectId,
|
||||
objectClass: currentTx.objectClass
|
||||
},
|
||||
{ sort: { modifiedOn: SortingOrder.Ascending } }
|
||||
)
|
||||
|
||||
let parent: Ref<Department> | undefined
|
||||
|
||||
for (const tx of updateTxes) {
|
||||
if (tx._id === currentTx._id) break
|
||||
if (tx.operations.parent !== undefined) {
|
||||
parent = tx.operations.parent
|
||||
}
|
||||
}
|
||||
|
||||
if (parent !== undefined) return parent
|
||||
|
||||
const createTxes = await control.findAll<TxCreateDoc<Department>>(
|
||||
control.ctx,
|
||||
core.class.TxCreateDoc,
|
||||
{
|
||||
objectId: currentTx.objectId,
|
||||
objectClass: currentTx.objectClass
|
||||
},
|
||||
{ sort: { modifiedOn: SortingOrder.Ascending } }
|
||||
)
|
||||
|
||||
return createTxes[0]?.attributes.parent
|
||||
}
|
||||
|
||||
async function buildHierarchy (_id: Ref<Department>, control: TriggerControl): Promise<Department[]> {
|
||||
const res: Department[] = []
|
||||
const ancestors = new Map<Ref<Department>, Ref<Department>>()
|
||||
@@ -82,7 +120,7 @@ async function buildHierarchy (_id: Ref<Department>, control: TriggerControl): P
|
||||
}
|
||||
}
|
||||
|
||||
function exlude (first: Ref<Department>[], second: Ref<Department>[]): Ref<Department>[] {
|
||||
function exclude (first: Ref<Department>[], second: Ref<Department>[]): Ref<Department>[] {
|
||||
const set = new Set(first)
|
||||
const res: Ref<Department>[] = []
|
||||
for (const department of second) {
|
||||
@@ -96,16 +134,18 @@ function exlude (first: Ref<Department>[], second: Ref<Department>[]): Ref<Depar
|
||||
function getTxes (
|
||||
factory: TxFactory,
|
||||
employees: Ref<Employee>[],
|
||||
added: Ref<Department>[],
|
||||
added: Department[],
|
||||
removed?: Ref<Department>[]
|
||||
): Tx[] {
|
||||
const pushTxes = added
|
||||
.map((dep) =>
|
||||
employees.map((emp) =>
|
||||
factory.createTxUpdateDoc(hr.class.Department, core.space.Workspace, dep, {
|
||||
$push: { members: emp }
|
||||
})
|
||||
)
|
||||
employees
|
||||
.filter((emp) => !dep.members.includes(emp))
|
||||
.map((emp) =>
|
||||
factory.createTxUpdateDoc(hr.class.Department, core.space.Workspace, dep._id, {
|
||||
$push: { members: emp }
|
||||
})
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
if (removed === undefined) return pushTxes
|
||||
@@ -146,22 +186,58 @@ export async function OnDepartmentStaff (txes: Tx[], control: TriggerControl): P
|
||||
)
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
const push = (await buildHierarchy(departmentId, control)).map((p) => p._id)
|
||||
const push = await buildHierarchy(departmentId, control)
|
||||
|
||||
if (lastDepartment === undefined) {
|
||||
result.push(...getTxes(control.txFactory, [employee], push))
|
||||
} else {
|
||||
let removed = (await buildHierarchy(lastDepartment, control)).map((p) => p._id)
|
||||
const added = exlude(removed, push)
|
||||
removed = exlude(push, removed)
|
||||
result.push(...getTxes(control.txFactory, [employee], added, removed))
|
||||
const removedDepartments = await buildHierarchy(lastDepartment, control)
|
||||
const removed = removedDepartments.map((p) => p._id)
|
||||
const pushIds = push.map((p) => p._id)
|
||||
const added = push.filter((p) => !removed.includes(p._id))
|
||||
const removedIds = exclude(pushIds, removed)
|
||||
result.push(...getTxes(control.txFactory, [employee], added, removedIds))
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function OnDepartmentUpdate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
|
||||
const result: Tx[] = []
|
||||
for (const tx of txes) {
|
||||
const ctx = tx as TxUpdateDoc<Department>
|
||||
if (ctx.operations.parent === undefined) continue
|
||||
|
||||
const department = await control.findAll(control.ctx, hr.class.Department, { _id: ctx.objectId })
|
||||
const currentDepartment = department[0]
|
||||
if (currentDepartment === undefined) continue
|
||||
|
||||
const members = currentDepartment.members
|
||||
if (members.length === 0) continue
|
||||
|
||||
const oldParent = await getOldDepartmentParent(ctx, control)
|
||||
if (oldParent === ctx.operations.parent) continue
|
||||
|
||||
const newParent = currentDepartment.parent
|
||||
const oldHierarchy = oldParent !== undefined ? await buildHierarchy(oldParent, control) : []
|
||||
const newHierarchy = newParent !== undefined ? await buildHierarchy(newParent, control) : []
|
||||
|
||||
const oldHierarchyIds = oldHierarchy.map((p) => p._id)
|
||||
const newHierarchyIds = newHierarchy.map((p) => p._id)
|
||||
const added = newHierarchy.filter((p) => !oldHierarchyIds.includes(p._id))
|
||||
const removed = oldHierarchy.filter((p) => !newHierarchyIds.includes(p._id)).map((p) => p._id)
|
||||
|
||||
result.push(...getTxes(control.txFactory, members, added, removed))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -438,6 +514,7 @@ export default async () => ({
|
||||
OnRequestUpdate,
|
||||
OnRequestRemove,
|
||||
OnDepartmentStaff,
|
||||
OnDepartmentUpdate,
|
||||
OnDepartmentRemove,
|
||||
OnEmployeeDeactivate,
|
||||
OnPublicHolidayCreate
|
||||
|
||||
@@ -31,6 +31,7 @@ export default plugin(serverHrId, {
|
||||
OnEmployee: '' as Resource<TriggerFunc>,
|
||||
OnEmployeeDeactivate: '' as Resource<TriggerFunc>,
|
||||
OnDepartmentStaff: '' as Resource<TriggerFunc>,
|
||||
OnDepartmentUpdate: '' as Resource<TriggerFunc>,
|
||||
OnDepartmentRemove: '' as Resource<TriggerFunc>,
|
||||
OnRequestCreate: '' as Resource<TriggerFunc>,
|
||||
OnRequestUpdate: '' as Resource<TriggerFunc>,
|
||||
|
||||
Reference in New Issue
Block a user