Recruit fixes (#2660)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-02-20 12:10:15 +07:00 committed by GitHub
parent 1a57aad78e
commit 0e6c458bfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 274 additions and 94 deletions

View File

@ -1033,24 +1033,6 @@ export function createModel (builder: Builder): void {
}, },
recruit.action.MoveApplicant recruit.action.MoveApplicant
) )
createAction(
builder,
{
label: recruit.string.RecognizeAttachment,
action: recruit.actionImpl.MoveApplicant,
icon: view.icon.Move,
input: 'any',
category: view.category.General,
target: recruit.class.Applicant,
context: {
mode: ['context', 'browser'],
group: 'tools'
},
override: [task.action.Move]
},
recruit.action.MoveApplicant
)
} }
export { recruitOperation } from './migration' export { recruitOperation } from './migration'

View File

@ -14,7 +14,6 @@
// //
import type { Client, Doc, Ref } from '@hcengineering/core' import type { Client, Doc, Ref } from '@hcengineering/core'
import { ObjectSearchCategory, ObjectSearchFactory } from '@hcengineering/model-presentation'
import type { IntlString, Resource, Status } from '@hcengineering/platform' import type { IntlString, Resource, Status } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform' import { mergeIds } from '@hcengineering/platform'
import { recruitId } from '@hcengineering/recruit' import { recruitId } from '@hcengineering/recruit'
@ -51,7 +50,6 @@ export default mergeIds(recruitId, recruit, {
TalentPools: '' as IntlString, TalentPools: '' as IntlString,
SearchApplication: '' as IntlString, SearchApplication: '' as IntlString,
SearchVacancy: '' as IntlString, SearchVacancy: '' as IntlString,
Application: '' as IntlString,
AssignedRecruiter: '' as IntlString, AssignedRecruiter: '' as IntlString,
Due: '' as IntlString, Due: '' as IntlString,
Source: '' as IntlString, Source: '' as IntlString,
@ -63,8 +61,7 @@ export default mergeIds(recruitId, recruit, {
GotoAssigned: '' as IntlString, GotoAssigned: '' as IntlString,
GotoApplicants: '' as IntlString, GotoApplicants: '' as IntlString,
GotoRecruitApplication: '' as IntlString, GotoRecruitApplication: '' as IntlString,
VacancyList: '' as IntlString, VacancyList: '' as IntlString
RecognizeAttachment: '' as IntlString
}, },
validator: { validator: {
ApplicantValidator: '' as Resource<<T extends Doc>(doc: T, client: Client) => Promise<Status>> ApplicantValidator: '' as Resource<<T extends Doc>(doc: T, client: Client) => Promise<Status>>
@ -100,12 +97,6 @@ export default mergeIds(recruitId, recruit, {
DefaultVacancy: '' as Ref<KanbanTemplate>, DefaultVacancy: '' as Ref<KanbanTemplate>,
Task: '' as Ref<KanbanTemplate> Task: '' as Ref<KanbanTemplate>
}, },
completion: {
ApplicationQuery: '' as Resource<ObjectSearchFactory>,
ApplicationCategory: '' as Ref<ObjectSearchCategory>,
VacancyCategory: '' as Ref<ObjectSearchCategory>,
VacancyQuery: '' as Resource<ObjectSearchFactory>
},
viewlet: { viewlet: {
TableCandidate: '' as Ref<Viewlet>, TableCandidate: '' as Ref<Viewlet>,
TableVacancy: '' as Ref<Viewlet>, TableVacancy: '' as Ref<Viewlet>,

View File

@ -7,7 +7,7 @@ import chunter from '@hcengineering/model-chunter'
import contact from '@hcengineering/model-contact' import contact from '@hcengineering/model-contact'
import core, { TAttachedDoc } from '@hcengineering/model-core' import core, { TAttachedDoc } from '@hcengineering/model-core'
import task from '@hcengineering/model-task' import task from '@hcengineering/model-task'
import { Candidate, Opinion, Review } from '@hcengineering/recruit' import { Applicant, Candidate, Opinion, Review } from '@hcengineering/recruit'
import recruit from './plugin' import recruit from './plugin'
@Model(recruit.class.Review, calendar.class.Event) @Model(recruit.class.Review, calendar.class.Event)
@ -24,6 +24,9 @@ export class TReview extends TEvent implements Review {
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
verdict!: string verdict!: string
@Prop(TypeRef(recruit.class.Applicant), recruit.string.Application, { icon: recruit.icon.Application })
application?: Ref<Applicant>
@Prop(TypeRef(contact.class.Organization), recruit.string.Company, { icon: contact.icon.Company }) @Prop(TypeRef(contact.class.Organization), recruit.string.Company, { icon: contact.icon.Company })
company?: Ref<Organization> company?: Ref<Organization>

View File

@ -105,7 +105,8 @@
"Score": "Score", "Score": "Score",
"Match": "Match", "Match": "Match",
"PerformMatch": "Match", "PerformMatch": "Match",
"MoveApplication": "Move to another vacancy" "MoveApplication": "Move to another vacancy",
"SearchVacancy": "Search vacancy..."
}, },
"status": { "status": {
"TalentRequired": "Please select talent", "TalentRequired": "Please select talent",

View File

@ -107,7 +107,8 @@
"Score": "Оценка", "Score": "Оценка",
"Match": "Совпадение", "Match": "Совпадение",
"PerformMatch": "Сопоставить", "PerformMatch": "Сопоставить",
"MoveApplication": "Поменять Вакансию" "MoveApplication": "Поменять Вакансию",
"SearchVacancy": "Найти вакансию..."
}, },
"status": { "status": {
"TalentRequired": "Пожалуйста выберите таланта", "TalentRequired": "Пожалуйста выберите таланта",

View File

@ -23,7 +23,7 @@
<Table <Table
_class={recruit.class.Applicant} _class={recruit.class.Applicant}
config={['', '$lookup.space.name', 'state', 'doneState']} config={['', '$lookup.space.name', '$lookup.space.company', 'state', 'doneState']}
query={{ attachedTo: value._id }} query={{ attachedTo: value._id }}
loadingProps={{ length: value.applications ?? 0 }} loadingProps={{ length: value.applications ?? 0 }}
/> />

View File

@ -60,7 +60,13 @@
</div> </div>
</Scroller> </Scroller>
<div class="mt-6"> <div class="mt-6">
<Reviews objectId={candidate._id} reviews={candidate.reviews ?? 0} label={recruit.string.TalentReviews} /> <Reviews
objectId={candidate._id}
reviews={candidate.reviews ?? 0}
label={recruit.string.TalentReviews}
application={object?._id}
company={vacancy?.company}
/>
</div> </div>
{/if} {/if}

View File

@ -16,14 +16,15 @@
import calendar from '@hcengineering/calendar' import calendar from '@hcengineering/calendar'
import type { Contact, EmployeeAccount, Organization, Person } from '@hcengineering/contact' import type { Contact, EmployeeAccount, Organization, Person } from '@hcengineering/contact'
import contact from '@hcengineering/contact' import contact from '@hcengineering/contact'
import { Account, Class, Client, Doc, generateId, getCurrentAccount, Ref, DateRangeMode } from '@hcengineering/core' import { Account, Class, Client, DateRangeMode, Doc, generateId, getCurrentAccount, Ref } from '@hcengineering/core'
import { getResource, OK, Resource, Severity, Status } from '@hcengineering/platform' import { getResource, OK, Resource, Severity, Status } from '@hcengineering/platform'
import { Card, getClient, UserBox, UserBoxList } from '@hcengineering/presentation' import { Card, getClient, UserBox, UserBoxList } from '@hcengineering/presentation'
import type { Candidate, Review } from '@hcengineering/recruit' import type { Applicant, Candidate, Review } from '@hcengineering/recruit'
import task from '@hcengineering/task' import task from '@hcengineering/task'
import { StyledTextArea } from '@hcengineering/text-editor' import { StyledTextArea } from '@hcengineering/text-editor'
import { DateRangePresenter, EditBox, Status as StatusControl } from '@hcengineering/ui' import { DateRangePresenter, EditBox, Status as StatusControl } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { ObjectSearchBox } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import recruit from '../../plugin' import recruit from '../../plugin'
@ -46,7 +47,9 @@
let startDate: number = initDate.getTime() let startDate: number = initDate.getTime()
let dueDate: number = initDate.getTime() + 30 * 60 * 1000 let dueDate: number = initDate.getTime() + 30 * 60 * 1000
let location: string = '' let location: string = ''
let company: Ref<Organization> | undefined = undefined
export let company: Ref<Organization> | undefined = undefined
export let application: Ref<Applicant> | undefined = undefined
const doc: Review = { const doc: Review = {
number: 0, number: 0,
@ -61,6 +64,7 @@
date: 0, date: 0,
dueDate: undefined, dueDate: undefined,
description, description,
application,
company, company,
verdict: '', verdict: '',
title, title,
@ -106,6 +110,7 @@
title, title,
participants: doc.participants, participants: doc.participants,
company, company,
application,
location location
}) })
} }
@ -169,6 +174,19 @@
label={recruit.string.Company} label={recruit.string.Company}
kind={'no-border'} kind={'no-border'}
size={'small'} size={'small'}
showNavigate={false}
create={{ component: contact.component.CreateOrganization, label: contact.string.CreateOrganization }}
/>
<ObjectSearchBox
_class={recruit.class.Applicant}
bind:value={application}
label={recruit.string.Application}
placeholder={recruit.string.ApplicationCreateLabel}
kind={'no-border'}
searchField={'number'}
size={'small'}
showNavigate={false}
allowCategory={[recruit.completion.ApplicationCategory]}
/> />
<DateRangePresenter <DateRangePresenter
bind:value={startDate} bind:value={startDate}

View File

@ -22,6 +22,7 @@
import { FullDescriptionBox } from '@hcengineering/text-editor' import { FullDescriptionBox } from '@hcengineering/text-editor'
import { EditBox, Grid, showPanel } from '@hcengineering/ui' import { EditBox, Grid, showPanel } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { ObjectPresenter } from '@hcengineering/view-resources'
import { createEventDispatcher, onMount } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
import recruit from '../../plugin' import recruit from '../../plugin'
@ -67,14 +68,16 @@
if (rawTitle !== object.title) client.update(object, { title: rawTitle }) if (rawTitle !== object.title) client.update(object, { title: rawTitle })
}} }}
/> />
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div <div
class="clear-mins" class="clear-mins flex-row-center"
on:click={() => { on:click={() => {
if (candidate !== undefined) { if (candidate !== undefined) {
showPanel(view.component.EditDoc, candidate._id, Hierarchy.mixinOrClass(candidate), 'content') showPanel(view.component.EditDoc, candidate._id, Hierarchy.mixinOrClass(candidate), 'content')
} }
}} }}
> >
<ObjectPresenter _class={recruit.class.Applicant} bind:objectId={object.application} />
<UserBox <UserBox
readonly readonly
_class={contact.class.Person} _class={contact.class.Person}
@ -85,6 +88,7 @@
size={'x-large'} size={'x-large'}
justify={'left'} justify={'left'}
width={'100%'} width={'100%'}
showNavigate={false}
/> />
</div> </div>
</Grid> </Grid>

View File

@ -13,11 +13,12 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import type { Doc, Ref } from '@hcengineering/core'
import core from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import calendar from '@hcengineering/calendar' import calendar from '@hcengineering/calendar'
import { Button, IconAdd, Label, showPopup, resizeObserver, Scroller } from '@hcengineering/ui' import { Organization } from '@hcengineering/contact'
import { DateRangeMode, Doc, FindOptions, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { Applicant, Review } from '@hcengineering/recruit'
import { Button, DatePresenter, IconAdd, Label, resizeObserver, showPopup } from '@hcengineering/ui'
import { Table } from '@hcengineering/view-resources' import { Table } from '@hcengineering/view-resources'
import recruit from '../../plugin' import recruit from '../../plugin'
import FileDuo from '../icons/FileDuo.svelte' import FileDuo from '../icons/FileDuo.svelte'
@ -26,11 +27,27 @@
export let objectId: Ref<Doc> export let objectId: Ref<Doc>
export let reviews: number export let reviews: number
export let label: IntlString = recruit.string.Reviews export let label: IntlString = recruit.string.Reviews
export let application: Ref<Applicant> | undefined
export let company: Ref<Organization> | undefined
const createApp = (): void => { const createApp = (): void => {
showPopup(CreateReview, { candidate: objectId, preserveCandidate: true }, 'top') showPopup(
CreateReview,
{
candidate: objectId,
preserveCandidate: true,
application,
company
},
'top'
)
} }
let wSection: number let wSection: number
const options: FindOptions<Review> = {
lookup: {
application: recruit.class.Applicant
}
}
</script> </script>
<div class="antiSection" use:resizeObserver={(element) => (wSection = element.clientWidth)}> <div class="antiSection" use:resizeObserver={(element) => (wSection = element.clientWidth)}>
@ -41,12 +58,12 @@
<Button icon={IconAdd} kind={'transparent'} shape={'circle'} on:click={createApp} /> <Button icon={IconAdd} kind={'transparent'} shape={'circle'} on:click={createApp} />
</div> </div>
{#if reviews > 0} {#if reviews > 0}
{#if wSection < 640}
<Scroller horizontal>
<Table <Table
_class={recruit.class.Review} _class={recruit.class.Review}
config={[ config={[
'', '',
'$lookup.application',
'company',
'verdict', 'verdict',
{ {
key: '', key: '',
@ -55,44 +72,20 @@
sortingKey: 'opinions' sortingKey: 'opinions'
}, },
{ {
key: '', key: 'date',
presenter: calendar.component.DateTimePresenter, presenter: DatePresenter,
label: calendar.string.Date, label: calendar.string.Date,
sortingKey: 'date' sortingKey: 'date',
props: {
editable: false,
mode: DateRangeMode.DATE
}
} }
]} ]}
options={{ {options}
lookup: {
space: core.class.Space
}
}}
query={{ attachedTo: objectId }} query={{ attachedTo: objectId }}
loadingProps={{ length: reviews }} loadingProps={{ length: reviews }}
/> />
</Scroller>
{:else}
<Table
_class={recruit.class.Review}
config={[
'',
'verdict',
{
key: '',
presenter: recruit.component.OpinionsPresenter,
label: recruit.string.Opinions,
sortingKey: 'opinions'
},
{ key: '', presenter: calendar.component.DateTimePresenter, label: calendar.string.Date, sortingKey: 'date' }
]}
options={{
lookup: {
space: core.class.Space
}
}}
query={{ attachedTo: objectId }}
loadingProps={{ length: reviews }}
/>
{/if}
{:else} {:else}
<div class="antiSection-empty solid flex-col-center mt-3"> <div class="antiSection-empty solid flex-col-center mt-3">
<div class="caption-color"> <div class="caption-color">
@ -101,6 +94,7 @@
<span class="dark-color mt-2"> <span class="dark-color mt-2">
<Label label={recruit.string.NoReviewForCandidate} /> <Label label={recruit.string.NoReviewForCandidate} />
</span> </span>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span class="over-underline content-accent-color" on:click={createApp}> <span class="over-underline content-accent-color" on:click={createApp}>
<Label label={recruit.string.CreateAnReview} /> <Label label={recruit.string.CreateAnReview} />
</span> </span>

View File

@ -16,6 +16,7 @@
import { Client, Doc, Ref, Space } from '@hcengineering/core' import { Client, Doc, Ref, Space } from '@hcengineering/core'
import type { IntlString, Resource, StatusCode } from '@hcengineering/platform' import type { IntlString, Resource, StatusCode } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform' import { mergeIds } from '@hcengineering/platform'
import { ObjectSearchCategory, ObjectSearchFactory } from '@hcengineering/presentation'
import recruit, { recruitId } from '@hcengineering/recruit' import recruit, { recruitId } from '@hcengineering/recruit'
import { TagCategory } from '@hcengineering/tags' import { TagCategory } from '@hcengineering/tags'
import { AnyComponent } from '@hcengineering/ui' import { AnyComponent } from '@hcengineering/ui'
@ -118,7 +119,8 @@ export default mergeIds(recruitId, recruit, {
Score: '' as IntlString, Score: '' as IntlString,
Match: '' as IntlString, Match: '' as IntlString,
PerformMatch: '' as IntlString, PerformMatch: '' as IntlString,
MoveApplication: '' as IntlString MoveApplication: '' as IntlString,
Application: '' as IntlString
}, },
space: { space: {
CandidatesPublic: '' as Ref<Space> CandidatesPublic: '' as Ref<Space>
@ -127,6 +129,12 @@ export default mergeIds(recruitId, recruit, {
Other: '' as Ref<TagCategory>, Other: '' as Ref<TagCategory>,
Category: '' as Ref<TagCategory> Category: '' as Ref<TagCategory>
}, },
completion: {
ApplicationQuery: '' as Resource<ObjectSearchFactory>,
ApplicationCategory: '' as Ref<ObjectSearchCategory>,
VacancyCategory: '' as Ref<ObjectSearchCategory>,
VacancyQuery: '' as Resource<ObjectSearchFactory>
},
component: { component: {
VacancyItemPresenter: '' as AnyComponent, VacancyItemPresenter: '' as AnyComponent,
VacancyCountPresenter: '' as AnyComponent, VacancyCountPresenter: '' as AnyComponent,

View File

@ -111,6 +111,8 @@ export interface Review extends Event {
verdict: string verdict: string
application?: Ref<Applicant>
company?: Ref<Organization> company?: Ref<Organization>
opinions?: number opinions?: number

View File

@ -0,0 +1,169 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, DocumentQuery, FindOptions, Hierarchy, Ref } from '@hcengineering/core'
import { Asset, IntlString } from '@hcengineering/platform'
import presentation, {
getClient,
ObjectCreate,
ObjectSearchCategory,
ObjectSearchPopup,
ObjectSearchResult
} from '@hcengineering/presentation'
import {
ActionIcon,
AnySvelteComponent,
Button,
ButtonKind,
ButtonSize,
getEventPositionElement,
getFocusManager,
Icon,
IconOpen,
Label,
LabelAndProps,
showPanel,
showPopup
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import ObjectPresenter from './ObjectPresenter.svelte'
export let _class: Ref<Class<Doc>>
export let excluded: Ref<Doc>[] | undefined = undefined
export let options: FindOptions<Doc> | undefined = undefined
export let docQuery: DocumentQuery<Doc> | undefined = undefined
export let label: IntlString
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let placeholder: IntlString = presentation.string.Search
export let value: Ref<Doc> | null | undefined
export let allowDeselect = false
export let titleDeselect: IntlString | undefined = undefined
export let readonly = false
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = undefined
export let focusIndex = -1
export let showTooltip: LabelAndProps | undefined = undefined
export let showNavigate = true
export let id: string | undefined = undefined
export let searchField: string = 'name'
export let docProps: Record<string, any> = {}
export let allowCategory: Ref<ObjectSearchCategory>[] | undefined
export let create: ObjectCreate | undefined = undefined
const dispatch = createEventDispatcher()
let selected: Doc | undefined
let container: HTMLElement
const client = getClient()
async function updateSelected (value: Ref<Doc> | null | undefined) {
selected = value ? await client.findOne(_class, { _id: value }) : undefined
}
$: updateSelected(value)
const mgr = getFocusManager()
const _click = (ev: MouseEvent): void => {
if (!readonly) {
showPopup(
ObjectSearchPopup,
{
_class,
options,
docQuery,
ignoreObjects: excluded ?? [],
icon,
allowDeselect,
selected: value,
titleDeselect,
placeholder,
create,
searchField,
docProps,
allowCategory
},
!$$slots.content ? container : getEventPositionElement(ev),
(result: ObjectSearchResult) => {
if (result === null) {
value = null
selected = undefined
dispatch('change', null)
} else if (result !== undefined && result.doc._id !== value) {
value = result.doc._id
dispatch('change', value)
}
mgr?.setFocusPos(focusIndex)
}
)
}
}
$: hideIcon = size === 'x-large' || (size === 'large' && kind !== 'link')
</script>
<div {id} bind:this={container} class="min-w-0" class:w-full={width === '100%'} class:h-full={$$slots.content}>
{#if $$slots.content}
<div class="w-full h-full flex-streatch" on:click={_click}>
<slot name="content" />
</div>
{:else}
<Button {focusIndex} width={width ?? 'min-content'} {size} {kind} {justify} {showTooltip} on:click={_click}>
<span slot="content" class="overflow-label flex-grow" class:flex-between={showNavigate && selected}>
<div
class="disabled flex-row-center"
style:width={showNavigate && selected
? `calc(${width ?? 'min-content'} - 1.5rem)`
: `${width ?? 'min-content'}`}
>
{#if selected}
<ObjectPresenter
objectId={selected._id}
_class={selected._class}
value={selected}
props={{ ...docProps, isInteractive: false, inline: true, size: 'x-small' }}
/>
{:else}
<div class="flex-row-center">
{#if icon}
<Icon {icon} size={kind === 'link' ? 'small' : size} />
{/if}
<div class="ml-2">
<Label {label} />
</div>
</div>
{/if}
</div>
{#if selected && showNavigate}
<ActionIcon
icon={IconOpen}
size={'small'}
action={() => {
if (selected) {
showPanel(view.component.EditDoc, selected._id, Hierarchy.mixinOrClass(selected), 'content')
}
}}
/>
{/if}
</span>
</Button>
{/if}
</div>

View File

@ -94,6 +94,7 @@ export { default as LinkPresenter } from './components/LinkPresenter.svelte'
export { default as List } from './components/list/List.svelte' export { default as List } from './components/list/List.svelte'
export { default as ContextMenu } from './components/Menu.svelte' export { default as ContextMenu } from './components/Menu.svelte'
export { default as ObjectBox } from './components/ObjectBox.svelte' export { default as ObjectBox } from './components/ObjectBox.svelte'
export { default as ObjectSearchBox } from './components/ObjectSearchBox.svelte'
export { default as ObjectPresenter } from './components/ObjectPresenter.svelte' export { default as ObjectPresenter } from './components/ObjectPresenter.svelte'
export { default as TableBrowser } from './components/TableBrowser.svelte' export { default as TableBrowser } from './components/TableBrowser.svelte'
export { default as ValueSelector } from './components/ValueSelector.svelte' export { default as ValueSelector } from './components/ValueSelector.svelte'