Skip to content

Commit

Permalink
TSK-1154: Statuses table support
Browse files Browse the repository at this point in the history
+ allow to export list of Vacancies

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
  • Loading branch information
haiodo committed Apr 13, 2023
1 parent ee085dc commit 92f7ddb
Show file tree
Hide file tree
Showing 13 changed files with 332 additions and 107 deletions.
20 changes: 20 additions & 0 deletions packages/ui/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,23 @@ export function handler<T, EVT = MouseEvent> (target: T, op: (value: T, evt: 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')
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
BitrixFieldMapping,
CreateHRApplication,
Fields,
MappingOperation
MappingOperation,
getAllAttributes
} from '@hcengineering/bitrix'
import { AnyAttribute } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import task from '@hcengineering/task'
import { createQuery, getClient } from '@hcengineering/presentation'
import InlineAttributeBarEditor from '@hcengineering/presentation/src/components/InlineAttributeBarEditor.svelte'
import recruit from '@hcengineering/recruit'
import task, { DoneStateTemplate, StateTemplate } from '@hcengineering/task'
import {
Button,
DropdownIntlItem,
Expand All @@ -20,7 +23,6 @@
} from '@hcengineering/ui'
import { ObjectBox } from '@hcengineering/view-resources'
import bitrix from '../../plugin'
import recruit from '@hcengineering/recruit'
export let mapping: BitrixEntityMapping
export let fields: Fields = {}
Expand All @@ -31,6 +33,7 @@
let vacancyField = (field?.operation as CreateHRApplication)?.vacancyField
let defaultTemplate = (field?.operation as CreateHRApplication)?.defaultTemplate
let copyTalentFields = (field?.operation as CreateHRApplication)?.copyTalentFields ?? []
let stateMapping = (field?.operation as CreateHRApplication)?.stateMapping ?? []
const client = getClient()
Expand All @@ -42,7 +45,8 @@
stateField,
vacancyField,
defaultTemplate,
copyTalentFields
copyTalentFields,
stateMapping
}
})
} else {
Expand All @@ -54,7 +58,8 @@
stateField,
vacancyField,
defaultTemplate,
copyTalentFields
copyTalentFields,
stateMapping
}
})
}
Expand All @@ -68,49 +73,156 @@
}
$: 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))
$: applicantAllAttrs = Array.from(client.getHierarchy().getAllAttributes(recruit.class.Applicant).values())
$: 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>

<div class="flex-col flex-wrap">
<div class="flex-row-center gap-2">
<div class="flex-col w-120">
<DropdownLabels minW0={false} label={getEmbeddedLabel('Vacancy field')} {items} bind:selected={vacancyField} />
<DropdownLabels minW0={false} label={getEmbeddedLabel('State field')} {items} bind:selected={stateField} />
<ObjectBox
label={getEmbeddedLabel('Template')}
searchField={'title'}
_class={task.class.KanbanTemplate}
docQuery={{ space: recruit.space.VacancyTemplates }}
bind:value={defaultTemplate}
/>

{#each copyTalentFields as f, i}
<div class="flex-row-center pattern">
<DropdownLabelsIntl
minW0={false}
label={getEmbeddedLabel('Copy field')}
items={attrs}
bind:selected={f.candidate}
/> =>
<DropdownLabelsIntl
minW0={false}
label={getEmbeddedLabel('Copy field')}
items={applicantAttrs}
bind:selected={f.applicant}
/>
</div>
{/each}
<Button
icon={IconAdd}
size={'small'}
on:click={() => {
copyTalentFields = [...copyTalentFields, { candidate: allAttrs[0]._id, applicant: applicantAllAttrs[0]._id }]
}}
/>
<div class="flex-row-center p-1">
<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
width={'10rem'}
label={getEmbeddedLabel('Template')}
searchField={'title'}
_class={task.class.KanbanTemplate}
docQuery={{ space: recruit.space.VacancyTemplates }}
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}
<div class="flex-row-center pattern">
<DropdownLabelsIntl
width={'10rem'}
label={getEmbeddedLabel('Copy field')}
items={attrs}
bind:selected={f.candidate}
/> =>
<DropdownLabelsIntl
width={'10rem'}
label={getEmbeddedLabel('Copy field')}
items={applicantAttrs}
bind:selected={f.applicant}
/>
</div>
{/each}
</div>
<div class="mt-2 mb-1 flex-row-center p-1">
<span class="mr-2"> State mapping: </span>
<Button
icon={IconAdd}
size={'small'}
on:click={() => {
stateMapping = [...stateMapping, { sourceName: '', targetName: '', updateCandidate: [], doneState: '' }]
}}
/>
</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>
Expand All @@ -132,4 +244,7 @@
color: var(--caption-color);
}
}
.scroll {
overflow: auto;
}
</style>
12 changes: 5 additions & 7 deletions plugins/bitrix/src/hr.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Organization } from '@hcengineering/contact'
import core, { Account, Client, Doc, Ref, SortingOrder, TxOperations } from '@hcengineering/core'
import recruit, { Vacancy } from '@hcengineering/recruit'
import core, { Account, Client, Data, Doc, Ref, SortingOrder, TxOperations } from '@hcengineering/core'
import recruit, { Applicant, Vacancy } from '@hcengineering/recruit'
import task, { KanbanTemplate, State, calcRank, createKanban } from '@hcengineering/task'

export async function createVacancy (
Expand Down Expand Up @@ -42,7 +42,8 @@ export async function createApplication (
client: TxOperations,
selectedState: State,
_space: Ref<Vacancy>,
doc: Doc
doc: Doc,
data: Data<Applicant>
): Promise<void> {
if (selectedState === undefined) {
throw new Error(`Please select initial state:${_space}`)
Expand All @@ -60,13 +61,10 @@ export async function createApplication (
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)

await client.addCollection(recruit.class.Applicant, _space, doc._id, recruit.mixin.Candidate, 'applications', {
...data,
state: state._id,
doneState: null,
number: (incResult as any).object.sequence,
assignee: null,
rank: calcRank(lastOne, undefined),
startDate: null,
dueDate: null,
createOn: Date.now()
})
}
18 changes: 12 additions & 6 deletions plugins/bitrix/src/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import core, {
WithLookup
} from '@hcengineering/core'
import gmail, { Message } from '@hcengineering/gmail'
import recruit from '@hcengineering/recruit'
import tags, { TagElement } from '@hcengineering/tags'
import { deepEqual } from 'fast-equals'
import { BitrixClient } from './client'
Expand All @@ -40,7 +41,6 @@ import {
LoginInfo
} from './types'
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> {
// We need to update fields if they are different.
Expand Down Expand Up @@ -109,6 +109,17 @@ export async function syncDocument (

try {
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
existing = await updateMainDoc(applyOp)

Expand All @@ -130,9 +141,6 @@ export async function syncDocument (
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>>()

// Find all attachment documents to existing.
Expand Down Expand Up @@ -322,8 +330,6 @@ export async function syncDocument (

async function updateMainDoc (applyOp: ApplyOperations): Promise<BitrixSyncDoc> {
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.
return (await updateDoc(applyOp, existing, resultDoc.document, resultDoc.document.modifiedOn)) as BitrixSyncDoc
// Go over extra documents.
Expand Down
16 changes: 16 additions & 0 deletions plugins/bitrix/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,19 @@ export interface CreateAttachedField {
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
*/
Expand All @@ -283,6 +296,9 @@ export interface CreateHRApplication {
defaultTemplate: Ref<KanbanTemplate>

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[]
}

/**
Expand Down
Loading

0 comments on commit 92f7ddb

Please sign in to comment.