TSK-1015: Bitrix Create Vacancy/Application (#2913)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2023-04-06 17:52:32 +07:00
committed by GitHub
parent ef836ac2f0
commit 921291ba24
16 changed files with 381 additions and 30 deletions
+72
View File
@@ -0,0 +1,72 @@
import { Organization } from '@hcengineering/contact'
import core, { Account, Client, Doc, Ref, SortingOrder, TxOperations } from '@hcengineering/core'
import recruit, { Vacancy } from '@hcengineering/recruit'
import task, { KanbanTemplate, State, calcRank, createKanban } from '@hcengineering/task'
export async function createVacancy (
rawClient: Client,
name: string,
templateId: Ref<KanbanTemplate>,
account: Ref<Account>,
company?: Ref<Organization>
): Promise<Ref<Vacancy>> {
const client = new TxOperations(rawClient, account)
const template = await client.findOne(task.class.KanbanTemplate, { _id: templateId })
if (template === undefined) {
throw Error(`Failed to find target kanban template: ${templateId}`)
}
const sequence = await client.findOne(task.class.Sequence, { attachedTo: recruit.class.Vacancy })
if (sequence === undefined) {
throw new Error('sequence object not found')
}
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
const id = await client.createDoc(recruit.class.Vacancy, core.space.Space, {
name,
description: template.shortDescription ?? '',
fullDescription: template.description,
private: false,
archived: false,
company,
number: (incResult as any).object.sequence,
members: []
})
await createKanban(client, id, templateId)
return id
}
export async function createApplication (
client: TxOperations,
selectedState: State,
_space: Ref<Vacancy>,
doc: Doc
): Promise<void> {
if (selectedState === undefined) {
throw new Error(`Please select initial state:${_space}`)
}
const state = await client.findOne(task.class.State, { space: _space, _id: selectedState?._id })
if (state === undefined) {
throw new Error(`create application: state not found space:${_space}`)
}
const sequence = await client.findOne(task.class.Sequence, { attachedTo: recruit.class.Applicant })
if (sequence === undefined) {
throw new Error('sequence object not found')
}
const lastOne = await client.findOne(recruit.class.Applicant, {}, { sort: { rank: SortingOrder.Descending } })
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
await client.addCollection(recruit.class.Applicant, _space, doc._id, recruit.mixin.Candidate, 'applications', {
state: state._id,
doneState: null,
number: (incResult as any).object.sequence,
assignee: null,
rank: calcRank(lastOne, undefined),
startDate: null,
dueDate: null,
createOn: Date.now()
})
}
+8 -9
View File
@@ -123,20 +123,19 @@ export async function syncDocument (
// Just create supplier documents, like TagElements.
for (const ed of resultDoc.extraDocs) {
await applyOp.createDoc(
ed._class,
ed.space,
ed,
ed._id,
resultDoc.document.modifiedOn,
resultDoc.document.modifiedBy
)
const { _class, space, _id, ...data } = ed
await applyOp.createDoc(_class, space, data, _id, resultDoc.document.modifiedOn, resultDoc.document.modifiedBy)
}
for (const op of resultDoc.postOperations) {
await op(resultDoc.document, existing)
}
const idMapping = new Map<Ref<Doc>, Ref<Doc>>()
// Find all attachment documents to existing.
const byClass = new Map<Ref<Class<Doc>>, (AttachedDoc & BitrixSyncDoc)[]>()
const idMapping = new Map<Ref<Doc>, Ref<Doc>>()
for (const d of resultDoc.extraSync) {
byClass.set(d._class, [...(byClass.get(d._class) ?? []), d])
}
+33 -1
View File
@@ -1,6 +1,7 @@
import { ChannelProvider } from '@hcengineering/contact'
import { AttachedDoc, Class, Doc, Mixin, Ref } from '@hcengineering/core'
import { ExpertKnowledge, InitialKnowledge, MeaningfullKnowledge } from '@hcengineering/tags'
import { KanbanTemplate } from '@hcengineering/task'
/**
* @public
@@ -174,7 +175,8 @@ export enum MappingOperation {
CreateTag, // Create tag
CreateChannel, // Create channel
DownloadAttachment,
FindReference
FindReference,
CreateHRApplication
}
/**
* @public
@@ -252,6 +254,35 @@ export interface FindReferenceOperation {
referenceClass: Ref<Class<Doc>>
}
/**
* @public
*/
export interface CreateAttachedField {
match: boolean // We should match type and pass if exists.
// Original document field to use value from.
sourceField: string
valueField: string // final value should go into valueField, field name to match, like `space`
// If reference is defined, we should find for some existing document by matching field with sourceField value.
// Document we should match value against.
referenceClass: Ref<Class<Doc>>
// Field to check for matched value against.
referenceField?: string
}
/**
* @public
*/
export interface CreateHRApplication {
kind: MappingOperation.CreateHRApplication
vacancyField: string // Name of vacancy in bitrix.
stateField: string // Name of status in bitrix.
defaultTemplate: Ref<KanbanTemplate>
}
/**
* @public
*/
@@ -265,6 +296,7 @@ export interface BitrixFieldMapping extends AttachedDoc {
| CreateChannelOperation
| DownloadAttachmentOperation
| FindReferenceOperation
| CreateHRApplication
}
/**
+82 -4
View File
@@ -1,5 +1,5 @@
import attachment, { Attachment } from '@hcengineering/attachment'
import contact, { Channel, EmployeeAccount } from '@hcengineering/contact'
import contact, { Channel, EmployeeAccount, Organization } from '@hcengineering/contact'
import core, {
AnyAttribute,
AttachedDoc,
@@ -7,14 +7,18 @@ import core, {
Client,
Data,
Doc,
generateId,
Mixin,
Ref,
RefTo,
Space,
WithLookup
TxOperations,
WithLookup,
generateId
} from '@hcengineering/core'
import { Message } from '@hcengineering/gmail'
import recruit, { Candidate, Vacancy } from '@hcengineering/recruit'
import tags, { TagCategory, TagElement, TagReference } from '@hcengineering/tags'
import task from '@hcengineering/task'
import bitrix, {
BitrixEntityMapping,
BitrixEntityType,
@@ -22,11 +26,13 @@ import bitrix, {
BitrixSyncDoc,
CopyValueOperation,
CreateChannelOperation,
CreateHRApplication,
CreateTagOperation,
DownloadAttachmentOperation,
FindReferenceOperation,
MappingOperation
} from '.'
import { createApplication, createVacancy } from './hr'
/**
* @public
@@ -60,6 +66,11 @@ export interface BitrixSyncRequest {
update: (doc: Ref<Doc>) => void
}
/**
* @public
*/
export type PostOperation = (doc: BitrixSyncDoc, existing?: Doc) => Promise<void>
/**
* @public
*/
@@ -71,6 +82,7 @@ export interface ConvertResult {
gmailDocuments: (Message & BitrixSyncDoc)[]
blobs: [Attachment & BitrixSyncDoc, () => Promise<File | undefined>, (file: File, attach: Attachment) => void][]
syncRequests: BitrixSyncRequest[]
postOperations: PostOperation[]
}
/**
@@ -113,6 +125,8 @@ export async function convert (
][] = []
const mixins: Record<Ref<Mixin<Doc>>, Data<Doc>> = {}
const postOperations: PostOperation[] = []
// Fill required mixins.
for (const m of entity.mixins ?? []) {
mixins[m] = {}
@@ -383,6 +397,65 @@ export async function convert (
}
}
const getCreateAttachedValue = async (attr: AnyAttribute, operation: CreateHRApplication): Promise<void> => {
const vacancyName = extractValue(operation.vacancyField)
const statusName = extractValue(operation.stateField)
postOperations.push(async (doc, existingDoc) => {
const vacancies = await client.findAll(recruit.class.Vacancy, {})
let vacancyId: Ref<Vacancy> | undefined
if (vacancyName !== undefined) {
const tName = vacancyName.trim().toLowerCase()
const vacancy = vacancies.find((it) => it.name.toLowerCase().trim() === tName)
let refOrgField: Ref<Organization> | undefined
const allAttrs = hierarchy.getAllAttributes(recruit.mixin.Candidate)
for (const a of allAttrs.values()) {
if (a.type._class === core.class.RefTo && (a.type as RefTo<Doc>).to === contact.class.Organization) {
refOrgField = (mixins as any)[recruit.mixin.Candidate][a.name] as Ref<Organization>
}
}
if (vacancy !== undefined) {
vacancyId = vacancy?._id
} else {
vacancyId = await createVacancy(
client,
vacancyName.trim(),
operation.defaultTemplate,
document.modifiedBy,
refOrgField
)
}
} else {
return
}
// Check if candidate already have vacancy
const existing = await client.findOne(recruit.class.Applicant, {
attachedTo: (existingDoc?._id ?? doc._id) as unknown as Ref<Candidate>,
space: vacancyId
})
if (statusName != null && statusName !== '') {
// Find status for vacancy
const states = await client.findAll(task.class.State, { space: vacancyId })
const state = states.find((it) => it.name.toLowerCase().trim() === statusName.toLowerCase().trim())
const ops = new TxOperations(client, document.modifiedBy)
if (state !== undefined) {
if (existing !== undefined && existing.state !== state?._id) {
await ops.update(existing, { state: state._id })
} else {
await createApplication(ops, state, vacancyId, document)
}
}
}
})
}
const setValue = (value: any, attr: AnyAttribute): void => {
if (value !== undefined) {
if (hierarchy.isMixin(attr.attributeOf)) {
@@ -479,6 +552,10 @@ export async function convert (
}
break
}
case MappingOperation.CreateHRApplication: {
await getCreateAttachedValue(attr, f.operation)
break
}
}
setValue(value, attr)
}
@@ -490,7 +567,8 @@ export async function convert (
extraDocs: newExtraDocs,
blobs,
syncRequests,
gmailDocuments: []
gmailDocuments: [],
postOperations
}
}