mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-13 13:17:45 +02:00
TSK-1015: Bitrix Create Vacancy/Application (#2913)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
@@ -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()
|
||||
})
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user