mirror of
https://github.com/hcengineering/platform.git
synced 2025-01-03 08:57:14 +03:00
TSK-1154: Statuses table support (#2974)
+ allow to export list of Vacancies Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
e4a824247a
commit
12974b5bb7
@ -79,3 +79,23 @@ export function handler<T, EVT = MouseEvent> (target: T, op: (value: T, evt: EVT
|
|||||||
op(target, evt)
|
op(target, evt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function tableToCSV (tableId: string, separator = ','): string {
|
||||||
|
const rows = document.querySelectorAll('table#' + tableId + ' tr')
|
||||||
|
// Construct csv
|
||||||
|
const csv: string[] = []
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
const row: string[] = []
|
||||||
|
const cols = rows[i].querySelectorAll('td, th')
|
||||||
|
for (let j = 0; j < cols.length; j++) {
|
||||||
|
let data = (cols[j] as HTMLElement).innerText.replace(/(\r\n|\n|\r)/gm, '').replace(/(\s\s)/gm, ' ')
|
||||||
|
data = data.replace(/"/g, '""')
|
||||||
|
row.push('"' + data + '"')
|
||||||
|
}
|
||||||
|
csv.push(row.join(separator))
|
||||||
|
}
|
||||||
|
return csv.join('\n')
|
||||||
|
}
|
||||||
|
@ -4,12 +4,15 @@
|
|||||||
BitrixFieldMapping,
|
BitrixFieldMapping,
|
||||||
CreateHRApplication,
|
CreateHRApplication,
|
||||||
Fields,
|
Fields,
|
||||||
MappingOperation
|
MappingOperation,
|
||||||
|
getAllAttributes
|
||||||
} from '@hcengineering/bitrix'
|
} from '@hcengineering/bitrix'
|
||||||
import { AnyAttribute } from '@hcengineering/core'
|
import { AnyAttribute } from '@hcengineering/core'
|
||||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||||
import task from '@hcengineering/task'
|
import InlineAttributeBarEditor from '@hcengineering/presentation/src/components/InlineAttributeBarEditor.svelte'
|
||||||
|
import recruit from '@hcengineering/recruit'
|
||||||
|
import task, { DoneStateTemplate, StateTemplate } from '@hcengineering/task'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DropdownIntlItem,
|
DropdownIntlItem,
|
||||||
@ -20,7 +23,6 @@
|
|||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import { ObjectBox } from '@hcengineering/view-resources'
|
import { ObjectBox } from '@hcengineering/view-resources'
|
||||||
import bitrix from '../../plugin'
|
import bitrix from '../../plugin'
|
||||||
import recruit from '@hcengineering/recruit'
|
|
||||||
|
|
||||||
export let mapping: BitrixEntityMapping
|
export let mapping: BitrixEntityMapping
|
||||||
export let fields: Fields = {}
|
export let fields: Fields = {}
|
||||||
@ -31,6 +33,7 @@
|
|||||||
let vacancyField = (field?.operation as CreateHRApplication)?.vacancyField
|
let vacancyField = (field?.operation as CreateHRApplication)?.vacancyField
|
||||||
let defaultTemplate = (field?.operation as CreateHRApplication)?.defaultTemplate
|
let defaultTemplate = (field?.operation as CreateHRApplication)?.defaultTemplate
|
||||||
let copyTalentFields = (field?.operation as CreateHRApplication)?.copyTalentFields ?? []
|
let copyTalentFields = (field?.operation as CreateHRApplication)?.copyTalentFields ?? []
|
||||||
|
let stateMapping = (field?.operation as CreateHRApplication)?.stateMapping ?? []
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
|
|
||||||
@ -42,7 +45,8 @@
|
|||||||
stateField,
|
stateField,
|
||||||
vacancyField,
|
vacancyField,
|
||||||
defaultTemplate,
|
defaultTemplate,
|
||||||
copyTalentFields
|
copyTalentFields,
|
||||||
|
stateMapping
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -54,7 +58,8 @@
|
|||||||
stateField,
|
stateField,
|
||||||
vacancyField,
|
vacancyField,
|
||||||
defaultTemplate,
|
defaultTemplate,
|
||||||
copyTalentFields
|
copyTalentFields,
|
||||||
|
stateMapping
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -68,50 +73,157 @@
|
|||||||
}
|
}
|
||||||
$: items = getItems(fields)
|
$: items = getItems(fields)
|
||||||
|
|
||||||
$: allAttrs = Array.from(client.getHierarchy().getAllAttributes(recruit.mixin.Candidate).values())
|
$: allAttrs = Array.from(getAllAttributes(client, recruit.mixin.Candidate).values())
|
||||||
$: attrs = allAttrs.map((it) => ({ id: it.name, label: it.label } as DropdownIntlItem))
|
$: attrs = allAttrs.map((it) => ({ id: it.name, label: it.label } as DropdownIntlItem))
|
||||||
|
|
||||||
$: applicantAllAttrs = Array.from(client.getHierarchy().getAllAttributes(recruit.class.Applicant).values())
|
$: applicantAllAttrs = Array.from(client.getHierarchy().getAllAttributes(recruit.class.Applicant).values())
|
||||||
$: applicantAttrs = applicantAllAttrs.map((it) => ({ id: it.name, label: it.label } as DropdownIntlItem))
|
$: applicantAttrs = applicantAllAttrs.map((it) => ({ id: it.name, label: it.label } as DropdownIntlItem))
|
||||||
|
|
||||||
|
$: sourceStates = Array.from(mapping.bitrixFields[stateField].items?.values() ?? []).map(
|
||||||
|
(it) => ({ id: it.VALUE, label: it.VALUE } as DropdownTextItem)
|
||||||
|
)
|
||||||
|
|
||||||
|
const statusQuery = createQuery()
|
||||||
|
const doneQuery = createQuery()
|
||||||
|
|
||||||
|
let stateTemplates: StateTemplate[] = []
|
||||||
|
let doneStateTemplates: DoneStateTemplate[] = []
|
||||||
|
|
||||||
|
$: statusQuery.query(task.class.StateTemplate, { attachedTo: defaultTemplate }, (res) => {
|
||||||
|
stateTemplates = res
|
||||||
|
})
|
||||||
|
|
||||||
|
$: doneQuery.query(task.class.DoneStateTemplate, { attachedTo: defaultTemplate }, (res) => {
|
||||||
|
doneStateTemplates = res
|
||||||
|
})
|
||||||
|
|
||||||
|
$: stateTitles = [{ id: '', label: 'None' }, ...stateTemplates.map((it) => ({ id: it.name, label: it.name }))]
|
||||||
|
$: doneStateTitles = [{ id: '', label: 'None' }, ...doneStateTemplates.map((it) => ({ id: it.name, label: it.name }))]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex-col flex-wrap">
|
<div class="flex-col flex-wrap">
|
||||||
<div class="flex-row-center gap-2">
|
<div class="flex-row-center gap-2">
|
||||||
<div class="flex-col w-120">
|
<div class="flex-col w-120">
|
||||||
<DropdownLabels minW0={false} label={getEmbeddedLabel('Vacancy field')} {items} bind:selected={vacancyField} />
|
<div class="flex-row-center p-1">
|
||||||
<DropdownLabels minW0={false} label={getEmbeddedLabel('State field')} {items} bind:selected={stateField} />
|
<span class="w-22"> Vacancy: </span>
|
||||||
|
<DropdownLabels
|
||||||
|
width={'10rem'}
|
||||||
|
label={getEmbeddedLabel('Vacancy field')}
|
||||||
|
{items}
|
||||||
|
bind:selected={vacancyField}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row-center p-1">
|
||||||
|
<span class="w-22"> State: </span>
|
||||||
|
<DropdownLabels width={'10rem'} label={getEmbeddedLabel('State field')} {items} bind:selected={stateField} />
|
||||||
|
</div>
|
||||||
|
<div class="flex-row-center p-1">
|
||||||
|
<span class="w-22"> Template: </span>
|
||||||
<ObjectBox
|
<ObjectBox
|
||||||
|
width={'10rem'}
|
||||||
label={getEmbeddedLabel('Template')}
|
label={getEmbeddedLabel('Template')}
|
||||||
searchField={'title'}
|
searchField={'title'}
|
||||||
_class={task.class.KanbanTemplate}
|
_class={task.class.KanbanTemplate}
|
||||||
docQuery={{ space: recruit.space.VacancyTemplates }}
|
docQuery={{ space: recruit.space.VacancyTemplates }}
|
||||||
bind:value={defaultTemplate}
|
bind:value={defaultTemplate}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 mb-1 flex-row-center p-1">
|
||||||
|
<span class="mr-2"> Copy following fields: </span>
|
||||||
|
<Button
|
||||||
|
icon={IconAdd}
|
||||||
|
size={'small'}
|
||||||
|
on:click={() => {
|
||||||
|
copyTalentFields = [
|
||||||
|
...copyTalentFields,
|
||||||
|
{ candidate: allAttrs[0]._id, applicant: applicantAllAttrs[0]._id }
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-col flex-wrap">
|
||||||
{#each copyTalentFields as f, i}
|
{#each copyTalentFields as f, i}
|
||||||
<div class="flex-row-center pattern">
|
<div class="flex-row-center pattern">
|
||||||
<DropdownLabelsIntl
|
<DropdownLabelsIntl
|
||||||
minW0={false}
|
width={'10rem'}
|
||||||
label={getEmbeddedLabel('Copy field')}
|
label={getEmbeddedLabel('Copy field')}
|
||||||
items={attrs}
|
items={attrs}
|
||||||
bind:selected={f.candidate}
|
bind:selected={f.candidate}
|
||||||
/> =>
|
/> =>
|
||||||
<DropdownLabelsIntl
|
<DropdownLabelsIntl
|
||||||
minW0={false}
|
width={'10rem'}
|
||||||
label={getEmbeddedLabel('Copy field')}
|
label={getEmbeddedLabel('Copy field')}
|
||||||
items={applicantAttrs}
|
items={applicantAttrs}
|
||||||
bind:selected={f.applicant}
|
bind:selected={f.applicant}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 mb-1 flex-row-center p-1">
|
||||||
|
<span class="mr-2"> State mapping: </span>
|
||||||
<Button
|
<Button
|
||||||
icon={IconAdd}
|
icon={IconAdd}
|
||||||
size={'small'}
|
size={'small'}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
copyTalentFields = [...copyTalentFields, { candidate: allAttrs[0]._id, applicant: applicantAllAttrs[0]._id }]
|
stateMapping = [...stateMapping, { sourceName: '', targetName: '', updateCandidate: [], doneState: '' }]
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-co">
|
||||||
|
{#each stateMapping as m}
|
||||||
|
<div class="flex-row-center pattern flex-between flex-wrap">
|
||||||
|
<DropdownLabels
|
||||||
|
width={'10rem'}
|
||||||
|
label={getEmbeddedLabel('Source state')}
|
||||||
|
items={sourceStates}
|
||||||
|
kind={m.sourceName !== '' ? 'primary' : 'secondary'}
|
||||||
|
bind:selected={m.sourceName}
|
||||||
|
/> =>
|
||||||
|
<DropdownLabels
|
||||||
|
width={'10rem'}
|
||||||
|
kind={m.targetName !== '' ? 'primary' : 'secondary'}
|
||||||
|
label={getEmbeddedLabel('Final state')}
|
||||||
|
items={stateTitles}
|
||||||
|
bind:selected={m.targetName}
|
||||||
|
/>
|
||||||
|
<span class="ml-4"> Done state: </span>
|
||||||
|
<DropdownLabels
|
||||||
|
width={'10rem'}
|
||||||
|
kind={m.doneState !== '' ? 'primary' : 'secondary'}
|
||||||
|
label={getEmbeddedLabel('Done state')}
|
||||||
|
items={doneStateTitles}
|
||||||
|
bind:selected={m.doneState}
|
||||||
|
/>
|
||||||
|
{#each m.updateCandidate as c}
|
||||||
|
{@const attribute = allAttrs.find((it) => it.name === c.attr)}
|
||||||
|
<DropdownLabelsIntl
|
||||||
|
width={'10rem'}
|
||||||
|
label={getEmbeddedLabel('Field to fill')}
|
||||||
|
items={attrs}
|
||||||
|
bind:selected={c.attr}
|
||||||
|
/>
|
||||||
|
{#if attribute}
|
||||||
|
=>
|
||||||
|
<InlineAttributeBarEditor
|
||||||
|
_class={recruit.mixin.Candidate}
|
||||||
|
key={{ key: 'value', attr: attribute }}
|
||||||
|
draft
|
||||||
|
object={c}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<Button
|
||||||
|
icon={IconAdd}
|
||||||
|
size={'small'}
|
||||||
|
on:click={() => {
|
||||||
|
m.updateCandidate = [...m.updateCandidate, { attr: allAttrs[0]._id, value: undefined }]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -132,4 +244,7 @@
|
|||||||
color: var(--caption-color);
|
color: var(--caption-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.scroll {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Organization } from '@hcengineering/contact'
|
import { Organization } from '@hcengineering/contact'
|
||||||
import core, { Account, Client, Doc, Ref, SortingOrder, TxOperations } from '@hcengineering/core'
|
import core, { Account, Client, Data, Doc, Ref, SortingOrder, TxOperations } from '@hcengineering/core'
|
||||||
import recruit, { Vacancy } from '@hcengineering/recruit'
|
import recruit, { Applicant, Vacancy } from '@hcengineering/recruit'
|
||||||
import task, { KanbanTemplate, State, calcRank, createKanban } from '@hcengineering/task'
|
import task, { KanbanTemplate, State, calcRank, createKanban } from '@hcengineering/task'
|
||||||
|
|
||||||
export async function createVacancy (
|
export async function createVacancy (
|
||||||
@ -42,7 +42,8 @@ export async function createApplication (
|
|||||||
client: TxOperations,
|
client: TxOperations,
|
||||||
selectedState: State,
|
selectedState: State,
|
||||||
_space: Ref<Vacancy>,
|
_space: Ref<Vacancy>,
|
||||||
doc: Doc
|
doc: Doc,
|
||||||
|
data: Data<Applicant>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (selectedState === undefined) {
|
if (selectedState === undefined) {
|
||||||
throw new Error(`Please select initial state:${_space}`)
|
throw new Error(`Please select initial state:${_space}`)
|
||||||
@ -60,13 +61,10 @@ export async function createApplication (
|
|||||||
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
|
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
|
||||||
|
|
||||||
await client.addCollection(recruit.class.Applicant, _space, doc._id, recruit.mixin.Candidate, 'applications', {
|
await client.addCollection(recruit.class.Applicant, _space, doc._id, recruit.mixin.Candidate, 'applications', {
|
||||||
|
...data,
|
||||||
state: state._id,
|
state: state._id,
|
||||||
doneState: null,
|
|
||||||
number: (incResult as any).object.sequence,
|
number: (incResult as any).object.sequence,
|
||||||
assignee: null,
|
|
||||||
rank: calcRank(lastOne, undefined),
|
rank: calcRank(lastOne, undefined),
|
||||||
startDate: null,
|
|
||||||
dueDate: null,
|
|
||||||
createOn: Date.now()
|
createOn: Date.now()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import core, {
|
|||||||
WithLookup
|
WithLookup
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import gmail, { Message } from '@hcengineering/gmail'
|
import gmail, { Message } from '@hcengineering/gmail'
|
||||||
|
import recruit from '@hcengineering/recruit'
|
||||||
import tags, { TagElement } from '@hcengineering/tags'
|
import tags, { TagElement } from '@hcengineering/tags'
|
||||||
import { deepEqual } from 'fast-equals'
|
import { deepEqual } from 'fast-equals'
|
||||||
import { BitrixClient } from './client'
|
import { BitrixClient } from './client'
|
||||||
@ -40,7 +41,6 @@ import {
|
|||||||
LoginInfo
|
LoginInfo
|
||||||
} from './types'
|
} from './types'
|
||||||
import { convert, ConvertResult } from './utils'
|
import { convert, ConvertResult } from './utils'
|
||||||
import recruit from '@hcengineering/recruit'
|
|
||||||
|
|
||||||
async function updateDoc (client: ApplyOperations, doc: Doc, raw: Doc | Data<Doc>, date: Timestamp): Promise<Doc> {
|
async function updateDoc (client: ApplyOperations, doc: Doc, raw: Doc | Data<Doc>, date: Timestamp): Promise<Doc> {
|
||||||
// We need to update fields if they are different.
|
// We need to update fields if they are different.
|
||||||
@ -109,6 +109,17 @@ export async function syncDocument (
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const applyOp = client.apply('bitrix')
|
const applyOp = client.apply('bitrix')
|
||||||
|
|
||||||
|
if (existing !== undefined) {
|
||||||
|
// We need update document id.
|
||||||
|
resultDoc.document._id = existing._id as Ref<BitrixSyncDoc>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Operations could add more change instructions
|
||||||
|
for (const op of resultDoc.postOperations) {
|
||||||
|
await op(resultDoc, extraDocs, existing)
|
||||||
|
}
|
||||||
|
|
||||||
// const newDoc = existing === undefined
|
// const newDoc = existing === undefined
|
||||||
existing = await updateMainDoc(applyOp)
|
existing = await updateMainDoc(applyOp)
|
||||||
|
|
||||||
@ -130,9 +141,6 @@ export async function syncDocument (
|
|||||||
await applyOp.createDoc(_class, space, data, _id, resultDoc.document.modifiedOn, resultDoc.document.modifiedBy)
|
await applyOp.createDoc(_class, space, data, _id, resultDoc.document.modifiedOn, resultDoc.document.modifiedBy)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const op of resultDoc.postOperations) {
|
|
||||||
await op(resultDoc, extraDocs, existing)
|
|
||||||
}
|
|
||||||
const idMapping = new Map<Ref<Doc>, Ref<Doc>>()
|
const idMapping = new Map<Ref<Doc>, Ref<Doc>>()
|
||||||
|
|
||||||
// Find all attachment documents to existing.
|
// Find all attachment documents to existing.
|
||||||
@ -322,8 +330,6 @@ export async function syncDocument (
|
|||||||
|
|
||||||
async function updateMainDoc (applyOp: ApplyOperations): Promise<BitrixSyncDoc> {
|
async function updateMainDoc (applyOp: ApplyOperations): Promise<BitrixSyncDoc> {
|
||||||
if (existing !== undefined) {
|
if (existing !== undefined) {
|
||||||
// We need update doucment id.
|
|
||||||
resultDoc.document._id = existing._id as Ref<BitrixSyncDoc>
|
|
||||||
// We need to update fields if they are different.
|
// We need to update fields if they are different.
|
||||||
return (await updateDoc(applyOp, existing, resultDoc.document, resultDoc.document.modifiedOn)) as BitrixSyncDoc
|
return (await updateDoc(applyOp, existing, resultDoc.document, resultDoc.document.modifiedOn)) as BitrixSyncDoc
|
||||||
// Go over extra documents.
|
// Go over extra documents.
|
||||||
|
@ -271,6 +271,19 @@ export interface CreateAttachedField {
|
|||||||
referenceField?: string
|
referenceField?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface BitrixStateMapping {
|
||||||
|
sourceName: string
|
||||||
|
targetName: string // if empty will not create application
|
||||||
|
|
||||||
|
doneState: string // Alternative is to set doneState to value
|
||||||
|
|
||||||
|
// Allow to put some values, in case of some statues
|
||||||
|
updateCandidate: { attr: string, value: any }[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -283,6 +296,9 @@ export interface CreateHRApplication {
|
|||||||
defaultTemplate: Ref<KanbanTemplate>
|
defaultTemplate: Ref<KanbanTemplate>
|
||||||
|
|
||||||
copyTalentFields?: { candidate: Ref<AnyAttribute>, applicant: Ref<AnyAttribute> }[]
|
copyTalentFields?: { candidate: Ref<AnyAttribute>, applicant: Ref<AnyAttribute> }[]
|
||||||
|
|
||||||
|
// We would like to map some of bitrix states to our states, name matching is used to hold values.
|
||||||
|
stateMapping?: BitrixStateMapping[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,6 +24,7 @@ import bitrix, {
|
|||||||
BitrixEntityMapping,
|
BitrixEntityMapping,
|
||||||
BitrixEntityType,
|
BitrixEntityType,
|
||||||
BitrixFieldMapping,
|
BitrixFieldMapping,
|
||||||
|
BitrixStateMapping,
|
||||||
BitrixSyncDoc,
|
BitrixSyncDoc,
|
||||||
CopyValueOperation,
|
CopyValueOperation,
|
||||||
CreateChannelOperation,
|
CreateChannelOperation,
|
||||||
@ -404,8 +405,7 @@ export async function convert (
|
|||||||
|
|
||||||
const getCreateAttachedValue = async (attr: AnyAttribute, operation: CreateHRApplication): Promise<void> => {
|
const getCreateAttachedValue = async (attr: AnyAttribute, operation: CreateHRApplication): Promise<void> => {
|
||||||
const vacancyName = extractValue(operation.vacancyField)
|
const vacancyName = extractValue(operation.vacancyField)
|
||||||
const statusName = extractValue(operation.stateField)
|
const sourceStatusName = extractValue(operation.stateField)
|
||||||
|
|
||||||
postOperations.push(async (doc, extraDocs, existingDoc) => {
|
postOperations.push(async (doc, extraDocs, existingDoc) => {
|
||||||
let vacancyId: Ref<Vacancy> | undefined
|
let vacancyId: Ref<Vacancy> | undefined
|
||||||
|
|
||||||
@ -440,10 +440,9 @@ export async function convert (
|
|||||||
vacancies.push(vacancy)
|
vacancies.push(vacancy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sourceStatusName != null && sourceStatusName !== '') {
|
||||||
// Check if candidate already have vacancy
|
// Check if candidate already have vacancy
|
||||||
const existing = applications.find(
|
const existing = applications.find(
|
||||||
(it) =>
|
(it) =>
|
||||||
@ -453,20 +452,55 @@ export async function convert (
|
|||||||
|
|
||||||
const candidate = doc.mixins[recruit.mixin.Candidate] as Data<Candidate>
|
const candidate = doc.mixins[recruit.mixin.Candidate] as Data<Candidate>
|
||||||
const ops = new TxOperations(client, document.modifiedBy)
|
const ops = new TxOperations(client, document.modifiedBy)
|
||||||
|
let statusName = sourceStatusName
|
||||||
|
let mapping: BitrixStateMapping | undefined
|
||||||
|
for (const t of operation.stateMapping ?? []) {
|
||||||
|
if (t.sourceName === sourceStatusName) {
|
||||||
|
statusName = t.targetName
|
||||||
|
mapping = t
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (statusName != null && statusName !== '') {
|
const attrs = getAllAttributes(client, recruit.mixin.Candidate)
|
||||||
|
|
||||||
|
// Update candidate operations
|
||||||
|
for (const u of mapping?.updateCandidate ?? []) {
|
||||||
|
const attribute = attrs.get(u.attr)
|
||||||
|
if (attribute === undefined) {
|
||||||
|
console.error('failed to fill attribute', u.attr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (client.getHierarchy().isMixin(attribute.attributeOf)) {
|
||||||
|
doc.mixins[attribute.attributeOf] = { ...(doc.mixins[attribute.attributeOf] ?? {}), [u.attr]: u.value }
|
||||||
|
} else {
|
||||||
|
;(doc.document as any)[u.attr] = u.value
|
||||||
|
}
|
||||||
|
}
|
||||||
// Find status for vacancy
|
// Find status for vacancy
|
||||||
const states = await client.findAll(task.class.State, { space: vacancyId })
|
|
||||||
|
|
||||||
const update: DocumentUpdate<Applicant> = {}
|
const update: DocumentUpdate<Applicant> = {}
|
||||||
for (const k of operation.copyTalentFields ?? []) {
|
for (const k of operation.copyTalentFields ?? []) {
|
||||||
const val = (candidate as any)[k.candidate]
|
const val = (candidate as any)[k.candidate]
|
||||||
if ((existing as any)[k.applicant] !== val) {
|
if ((existing as any)?.[k.applicant] !== val) {
|
||||||
;(update as any)[k.applicant] = val
|
;(update as any)[k.applicant] = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (vacancyId !== undefined) {
|
||||||
|
const states = await client.findAll(task.class.State, { space: vacancyId })
|
||||||
const state = states.find((it) => it.name.toLowerCase().trim() === statusName.toLowerCase().trim())
|
const state = states.find((it) => it.name.toLowerCase().trim() === statusName.toLowerCase().trim())
|
||||||
if (state !== undefined) {
|
if (state !== undefined) {
|
||||||
|
if (mapping?.doneState !== '') {
|
||||||
|
const doneStates = await client.findAll(task.class.DoneState, { space: vacancyId })
|
||||||
|
const doneState = doneStates.find(
|
||||||
|
(it) => it.name.toLowerCase().trim() === mapping?.doneState.toLowerCase().trim()
|
||||||
|
)
|
||||||
|
if (doneState !== undefined) {
|
||||||
|
if (doneState !== undefined && existing?.doneState !== doneState._id) {
|
||||||
|
update.doneState = doneState._id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (existing !== undefined) {
|
if (existing !== undefined) {
|
||||||
if (existing.state !== state?._id) {
|
if (existing.state !== state?._id) {
|
||||||
update.state = state._id
|
update.state = state._id
|
||||||
@ -475,7 +509,8 @@ export async function convert (
|
|||||||
await ops.update(existing, update)
|
await ops.update(existing, update)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await createApplication(ops, state, vacancyId, { ...document, ...update })
|
await createApplication(ops, state, vacancyId, document, update as Data<Applicant>)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -604,3 +639,21 @@ export async function convert (
|
|||||||
export function toClassRef (val: any): Ref<Class<Doc>> {
|
export function toClassRef (val: any): Ref<Class<Doc>> {
|
||||||
return val as Ref<Class<Doc>>
|
return val as Ref<Class<Doc>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function getAllAttributes (client: Client, _class: Ref<Class<Doc>>): Map<string, AnyAttribute> {
|
||||||
|
const h = client.getHierarchy()
|
||||||
|
const _classAttrs = h.getAllAttributes(recruit.mixin.Candidate)
|
||||||
|
|
||||||
|
const ancestors = h.getAncestors(_class)
|
||||||
|
for (const a of ancestors) {
|
||||||
|
for (const m of h.getDescendants(a).filter((it) => h.isMixin(it))) {
|
||||||
|
for (const [k, v] of h.getOwnAttributes(m)) {
|
||||||
|
_classAttrs.set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _classAttrs
|
||||||
|
}
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
import type { Request, RequestType, Staff } from '@hcengineering/hr'
|
import type { Request, RequestType, Staff } from '@hcengineering/hr'
|
||||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||||
import { Button, Label, Loading, Scroller, tableSP } from '@hcengineering/ui'
|
import { Button, Label, Loading, Scroller, tableSP, tableToCSV } from '@hcengineering/ui'
|
||||||
import view, { BuildModelKey, Viewlet, ViewletPreference } from '@hcengineering/view'
|
import view, { BuildModelKey, Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||||
import {
|
import {
|
||||||
getViewOptions,
|
getViewOptions,
|
||||||
@ -37,7 +37,6 @@
|
|||||||
getRequests,
|
getRequests,
|
||||||
getStartDate,
|
getStartDate,
|
||||||
getTotal,
|
getTotal,
|
||||||
tableToCSV,
|
|
||||||
weekDays
|
weekDays
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import StatPresenter from './StatPresenter.svelte'
|
import StatPresenter from './StatPresenter.svelte'
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Employee, getName } from '@hcengineering/contact'
|
import { Employee, getName } from '@hcengineering/contact'
|
||||||
import { Ref, TxOperations } from '@hcengineering/core'
|
import { Ref, TxOperations } from '@hcengineering/core'
|
||||||
import { Department, fromTzDate, Request, RequestType, Staff } from '@hcengineering/hr'
|
import { Department, Request, RequestType, Staff, fromTzDate } from '@hcengineering/hr'
|
||||||
import { MessageBox } from '@hcengineering/presentation'
|
import { MessageBox } from '@hcengineering/presentation'
|
||||||
import { Issue, TimeSpendReport } from '@hcengineering/tracker'
|
import { Issue, TimeSpendReport } from '@hcengineering/tracker'
|
||||||
import { areDatesEqual, isWeekend, MILLISECONDS_IN_DAY, showPopup } from '@hcengineering/ui'
|
import { MILLISECONDS_IN_DAY, areDatesEqual, isWeekend, showPopup } from '@hcengineering/ui'
|
||||||
import hr from './plugin'
|
import hr from './plugin'
|
||||||
|
|
||||||
const todayDate = new Date()
|
const todayDate = new Date()
|
||||||
@ -209,23 +209,6 @@ export function isHoliday (holidays: Date[] | undefined, day: Date): boolean {
|
|||||||
return holidays.some((date) => areDatesEqual(day, date))
|
return holidays.some((date) => areDatesEqual(day, date))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tableToCSV (tableId: string, separator = ','): string {
|
|
||||||
const rows = document.querySelectorAll('table#' + tableId + ' tr')
|
|
||||||
// Construct csv
|
|
||||||
const csv = []
|
|
||||||
for (let i = 0; i < rows.length; i++) {
|
|
||||||
const row = []
|
|
||||||
const cols = rows[i].querySelectorAll('td, th')
|
|
||||||
for (let j = 0; j < cols.length; j++) {
|
|
||||||
let data = (cols[j] as HTMLElement).innerText.replace(/(\r\n|\n|\r)/gm, '').replace(/(\s\s)/gm, ' ')
|
|
||||||
data = data.replace(/"/g, '""')
|
|
||||||
row.push('"' + data + '"')
|
|
||||||
}
|
|
||||||
csv.push(row.join(separator))
|
|
||||||
}
|
|
||||||
return csv.join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getHolidayDatesForEmployee (
|
export function getHolidayDatesForEmployee (
|
||||||
departmentMap: Map<Ref<Staff>, Department[]>,
|
departmentMap: Map<Ref<Staff>, Department[]>,
|
||||||
employee: Ref<Staff>,
|
employee: Ref<Staff>,
|
||||||
|
@ -109,7 +109,8 @@
|
|||||||
"TemplateReplace": "You you replace selected template?",
|
"TemplateReplace": "You you replace selected template?",
|
||||||
"TemplateReplaceConfirm": "All field changes will be override by template values",
|
"TemplateReplaceConfirm": "All field changes will be override by template values",
|
||||||
|
|
||||||
"OpenVacancyList": "Open list"
|
"OpenVacancyList": "Open list",
|
||||||
|
"Export": "Export"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"TalentRequired": "Please select talent",
|
"TalentRequired": "Please select talent",
|
||||||
|
@ -109,7 +109,8 @@
|
|||||||
"Organizations": "Компании",
|
"Organizations": "Компании",
|
||||||
"TemplateReplace": "Вы хотите заменить выбранный шаблон?",
|
"TemplateReplace": "Вы хотите заменить выбранный шаблон?",
|
||||||
"TemplateReplaceConfirm": "Все внесенные изменения в будут заменены значениями из шаблоном",
|
"TemplateReplaceConfirm": "Все внесенные изменения в будут заменены значениями из шаблоном",
|
||||||
"OpenVacancyList": "Открыть список"
|
"OpenVacancyList": "Открыть список",
|
||||||
|
"Export": "Экспорт"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"TalentRequired": "Пожалуйста выберите таланта",
|
"TalentRequired": "Пожалуйста выберите таланта",
|
||||||
|
@ -16,20 +16,29 @@
|
|||||||
import core, { Doc, DocumentQuery, Ref } from '@hcengineering/core'
|
import core, { Doc, DocumentQuery, Ref } from '@hcengineering/core'
|
||||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||||
import { Vacancy } from '@hcengineering/recruit'
|
import { Vacancy } from '@hcengineering/recruit'
|
||||||
import { Button, Icon, IconAdd, Label, Loading, SearchEdit, showPopup } from '@hcengineering/ui'
|
import {
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
IconAdd,
|
||||||
|
Label,
|
||||||
|
Loading,
|
||||||
|
SearchEdit,
|
||||||
|
deviceOptionsStore as deviceInfo,
|
||||||
|
showPopup,
|
||||||
|
tableToCSV
|
||||||
|
} from '@hcengineering/ui'
|
||||||
import view, { BuildModelKey, Viewlet, ViewletPreference } from '@hcengineering/view'
|
import view, { BuildModelKey, Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||||
import {
|
import {
|
||||||
FilterBar,
|
FilterBar,
|
||||||
FilterButton,
|
FilterButton,
|
||||||
|
TableBrowser,
|
||||||
|
ViewletSettingButton,
|
||||||
getViewOptions,
|
getViewOptions,
|
||||||
setActiveViewletId,
|
setActiveViewletId,
|
||||||
TableBrowser,
|
viewOptionStore
|
||||||
viewOptionStore,
|
|
||||||
ViewletSettingButton
|
|
||||||
} from '@hcengineering/view-resources'
|
} from '@hcengineering/view-resources'
|
||||||
import recruit from '../plugin'
|
import recruit from '../plugin'
|
||||||
import CreateVacancy from './CreateVacancy.svelte'
|
import CreateVacancy from './CreateVacancy.svelte'
|
||||||
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
|
||||||
|
|
||||||
export let archived = false
|
export let archived = false
|
||||||
|
|
||||||
@ -176,6 +185,25 @@
|
|||||||
on:click={showCreateDialog}
|
on:click={showCreateDialog}
|
||||||
/>
|
/>
|
||||||
<ViewletSettingButton bind:viewOptions viewlet={descr} />
|
<ViewletSettingButton bind:viewOptions viewlet={descr} />
|
||||||
|
<Button
|
||||||
|
label={recruit.string.Export}
|
||||||
|
size={'small'}
|
||||||
|
on:click={() => {
|
||||||
|
// Download it
|
||||||
|
const filename = 'vacancies' + new Date().toLocaleDateString() + '.csv'
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.style.display = 'none'
|
||||||
|
link.setAttribute('target', '_blank')
|
||||||
|
link.setAttribute(
|
||||||
|
'href',
|
||||||
|
'data:text/csv;charset=utf-8,%EF%BB%BF' + encodeURIComponent(tableToCSV('vacanciesData'))
|
||||||
|
)
|
||||||
|
link.setAttribute('download', filename)
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -194,6 +222,7 @@
|
|||||||
_class={recruit.class.Vacancy}
|
_class={recruit.class.Vacancy}
|
||||||
config={createConfig(descr, preference, applications)}
|
config={createConfig(descr, preference, applications)}
|
||||||
options={descr.options}
|
options={descr.options}
|
||||||
|
tableId={'vacanciesData'}
|
||||||
query={{
|
query={{
|
||||||
...resultQuery,
|
...resultQuery,
|
||||||
archived
|
archived
|
||||||
|
@ -127,7 +127,8 @@ export default mergeIds(recruitId, recruit, {
|
|||||||
|
|
||||||
TemplateReplace: '' as IntlString,
|
TemplateReplace: '' as IntlString,
|
||||||
TemplateReplaceConfirm: '' as IntlString,
|
TemplateReplaceConfirm: '' as IntlString,
|
||||||
OpenVacancyList: '' as IntlString
|
OpenVacancyList: '' as IntlString,
|
||||||
|
Export: '' as IntlString
|
||||||
},
|
},
|
||||||
space: {
|
space: {
|
||||||
CandidatesPublic: '' as Ref<Space>
|
CandidatesPublic: '' as Ref<Space>
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
export let config: (BuildModelKey | string)[]
|
export let config: (BuildModelKey | string)[]
|
||||||
export let showFilterBar = true
|
export let showFilterBar = true
|
||||||
export let enableChecking = true
|
export let enableChecking = true
|
||||||
|
export let tableId: string | undefined = undefined
|
||||||
|
|
||||||
// If defined, will show a number of dummy items before real data will appear.
|
// If defined, will show a number of dummy items before real data will appear.
|
||||||
export let loadingProps: LoadingProps | undefined = undefined
|
export let loadingProps: LoadingProps | undefined = undefined
|
||||||
@ -97,6 +98,7 @@
|
|||||||
showFooter
|
showFooter
|
||||||
checked={$selectionStore ?? []}
|
checked={$selectionStore ?? []}
|
||||||
{prefferedSorting}
|
{prefferedSorting}
|
||||||
|
{tableId}
|
||||||
selection={listProvider.current($focusStore)}
|
selection={listProvider.current($focusStore)}
|
||||||
on:row-focus={(evt) => {
|
on:row-focus={(evt) => {
|
||||||
listProvider.updateFocus(evt.detail)
|
listProvider.updateFocus(evt.detail)
|
||||||
|
Loading…
Reference in New Issue
Block a user