mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 19:11:33 +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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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,
|
||||
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,
|
||||
@ -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 = {}
|
||||
@ -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()
|
||||
|
||||
@ -42,7 +45,8 @@
|
||||
stateField,
|
||||
vacancyField,
|
||||
defaultTemplate,
|
||||
copyTalentFields
|
||||
copyTalentFields,
|
||||
stateMapping
|
||||
}
|
||||
})
|
||||
} else {
|
||||
@ -54,7 +58,8 @@
|
||||
stateField,
|
||||
vacancyField,
|
||||
defaultTemplate,
|
||||
copyTalentFields
|
||||
copyTalentFields,
|
||||
stateMapping
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
<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>
|
||||
|
||||
{#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="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>
|
||||
@ -132,4 +244,7 @@
|
||||
color: var(--caption-color);
|
||||
}
|
||||
}
|
||||
.scroll {
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
@ -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 (
|
||||
@ -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}`)
|
||||
@ -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()
|
||||
})
|
||||
}
|
||||
|
@ -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'
|
||||
@ -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.
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
@ -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.
|
||||
|
@ -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
|
||||
*/
|
||||
@ -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[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -24,6 +24,7 @@ import bitrix, {
|
||||
BitrixEntityMapping,
|
||||
BitrixEntityType,
|
||||
BitrixFieldMapping,
|
||||
BitrixStateMapping,
|
||||
BitrixSyncDoc,
|
||||
CopyValueOperation,
|
||||
CreateChannelOperation,
|
||||
@ -404,8 +405,7 @@ export async function convert (
|
||||
|
||||
const getCreateAttachedValue = async (attr: AnyAttribute, operation: CreateHRApplication): Promise<void> => {
|
||||
const vacancyName = extractValue(operation.vacancyField)
|
||||
const statusName = extractValue(operation.stateField)
|
||||
|
||||
const sourceStatusName = extractValue(operation.stateField)
|
||||
postOperations.push(async (doc, extraDocs, existingDoc) => {
|
||||
let vacancyId: Ref<Vacancy> | undefined
|
||||
|
||||
@ -440,42 +440,77 @@ export async function convert (
|
||||
vacancies.push(vacancy)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if candidate already have vacancy
|
||||
const existing = applications.find(
|
||||
(it) =>
|
||||
it.attachedTo === ((existingDoc?._id ?? doc.document._id) as unknown as Ref<Candidate>) &&
|
||||
it.space === vacancyId
|
||||
)
|
||||
if (sourceStatusName != null && sourceStatusName !== '') {
|
||||
// Check if candidate already have vacancy
|
||||
const existing = applications.find(
|
||||
(it) =>
|
||||
it.attachedTo === ((existingDoc?._id ?? doc.document._id) as unknown as Ref<Candidate>) &&
|
||||
it.space === vacancyId
|
||||
)
|
||||
|
||||
const candidate = doc.mixins[recruit.mixin.Candidate] as Data<Candidate>
|
||||
const ops = new TxOperations(client, document.modifiedBy)
|
||||
const candidate = doc.mixins[recruit.mixin.Candidate] as Data<Candidate>
|
||||
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
|
||||
const states = await client.findAll(task.class.State, { space: vacancyId })
|
||||
|
||||
const update: DocumentUpdate<Applicant> = {}
|
||||
for (const k of operation.copyTalentFields ?? []) {
|
||||
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
|
||||
}
|
||||
}
|
||||
const state = states.find((it) => it.name.toLowerCase().trim() === statusName.toLowerCase().trim())
|
||||
if (state !== undefined) {
|
||||
if (existing !== undefined) {
|
||||
if (existing.state !== state?._id) {
|
||||
update.state = state._id
|
||||
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())
|
||||
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 (Object.keys(update).length > 0) {
|
||||
await ops.update(existing, update)
|
||||
|
||||
if (existing !== undefined) {
|
||||
if (existing.state !== state?._id) {
|
||||
update.state = state._id
|
||||
}
|
||||
if (Object.keys(update).length > 0) {
|
||||
await ops.update(existing, update)
|
||||
}
|
||||
} else {
|
||||
await createApplication(ops, state, vacancyId, document, update as Data<Applicant>)
|
||||
}
|
||||
} else {
|
||||
await createApplication(ops, state, vacancyId, { ...document, ...update })
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -604,3 +639,21 @@ export async function convert (
|
||||
export function toClassRef (val: any): 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 { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
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 {
|
||||
getViewOptions,
|
||||
@ -37,7 +37,6 @@
|
||||
getRequests,
|
||||
getStartDate,
|
||||
getTotal,
|
||||
tableToCSV,
|
||||
weekDays
|
||||
} from '../../utils'
|
||||
import StatPresenter from './StatPresenter.svelte'
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Employee, getName } from '@hcengineering/contact'
|
||||
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 { 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'
|
||||
|
||||
const todayDate = new Date()
|
||||
@ -209,23 +209,6 @@ export function isHoliday (holidays: Date[] | undefined, day: Date): boolean {
|
||||
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 (
|
||||
departmentMap: Map<Ref<Staff>, Department[]>,
|
||||
employee: Ref<Staff>,
|
||||
|
@ -109,7 +109,8 @@
|
||||
"TemplateReplace": "You you replace selected template?",
|
||||
"TemplateReplaceConfirm": "All field changes will be override by template values",
|
||||
|
||||
"OpenVacancyList": "Open list"
|
||||
"OpenVacancyList": "Open list",
|
||||
"Export": "Export"
|
||||
},
|
||||
"status": {
|
||||
"TalentRequired": "Please select talent",
|
||||
|
@ -109,7 +109,8 @@
|
||||
"Organizations": "Компании",
|
||||
"TemplateReplace": "Вы хотите заменить выбранный шаблон?",
|
||||
"TemplateReplaceConfirm": "Все внесенные изменения в будут заменены значениями из шаблоном",
|
||||
"OpenVacancyList": "Открыть список"
|
||||
"OpenVacancyList": "Открыть список",
|
||||
"Export": "Экспорт"
|
||||
},
|
||||
"status": {
|
||||
"TalentRequired": "Пожалуйста выберите таланта",
|
||||
|
@ -16,20 +16,29 @@
|
||||
import core, { Doc, DocumentQuery, Ref } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
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 {
|
||||
FilterBar,
|
||||
FilterButton,
|
||||
TableBrowser,
|
||||
ViewletSettingButton,
|
||||
getViewOptions,
|
||||
setActiveViewletId,
|
||||
TableBrowser,
|
||||
viewOptionStore,
|
||||
ViewletSettingButton
|
||||
viewOptionStore
|
||||
} from '@hcengineering/view-resources'
|
||||
import recruit from '../plugin'
|
||||
import CreateVacancy from './CreateVacancy.svelte'
|
||||
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
|
||||
export let archived = false
|
||||
|
||||
@ -176,6 +185,25 @@
|
||||
on:click={showCreateDialog}
|
||||
/>
|
||||
<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>
|
||||
|
||||
@ -194,6 +222,7 @@
|
||||
_class={recruit.class.Vacancy}
|
||||
config={createConfig(descr, preference, applications)}
|
||||
options={descr.options}
|
||||
tableId={'vacanciesData'}
|
||||
query={{
|
||||
...resultQuery,
|
||||
archived
|
||||
|
@ -127,7 +127,8 @@ export default mergeIds(recruitId, recruit, {
|
||||
|
||||
TemplateReplace: '' as IntlString,
|
||||
TemplateReplaceConfirm: '' as IntlString,
|
||||
OpenVacancyList: '' as IntlString
|
||||
OpenVacancyList: '' as IntlString,
|
||||
Export: '' as IntlString
|
||||
},
|
||||
space: {
|
||||
CandidatesPublic: '' as Ref<Space>
|
||||
|
@ -32,6 +32,7 @@
|
||||
export let config: (BuildModelKey | string)[]
|
||||
export let showFilterBar = 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.
|
||||
export let loadingProps: LoadingProps | undefined = undefined
|
||||
@ -97,6 +98,7 @@
|
||||
showFooter
|
||||
checked={$selectionStore ?? []}
|
||||
{prefferedSorting}
|
||||
{tableId}
|
||||
selection={listProvider.current($focusStore)}
|
||||
on:row-focus={(evt) => {
|
||||
listProvider.updateFocus(evt.detail)
|
||||
|
Loading…
Reference in New Issue
Block a user