Support of Review category (#1108)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-03-10 16:04:08 +07:00 committed by GitHub
parent 4597788ec3
commit fc6517b894
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1999 additions and 53 deletions

View File

@ -241,6 +241,10 @@ export function createModel (builder: Builder): void {
presenter: contact.component.PersonPresenter
})
builder.mixin(core.class.Account, core.class.Class, view.mixin.AttributePresenter, {
presenter: contact.component.EmployeeAccountPresenter
})
builder.mixin(contact.class.Organization, core.class.Class, view.mixin.AttributePresenter, {
presenter: contact.component.OrganizationPresenter
})

View File

@ -33,7 +33,8 @@ export default mergeIds(contactId, contact, {
CreatePersons: '' as AnyComponent,
CreateOrganizations: '' as AnyComponent,
OrganizationPresenter: '' as AnyComponent,
Contacts: '' as AnyComponent
Contacts: '' as AnyComponent,
EmployeeAccountPresenter: '' as AnyComponent
},
string: {
Persons: '' as IntlString,

View File

@ -13,26 +13,34 @@
// limitations under the License.
//
import type { Class, Client } from '@anticrm/core'
import core, { Doc, Ref, Space, TxOperations } from '@anticrm/core'
import type { Client } from '@anticrm/core'
import { createKanbanTemplate } from '@anticrm/model-task'
import task, { KanbanTemplate } from '@anticrm/task'
import recruit from './plugin'
import task from '@anticrm/task'
import type { KanbanTemplate } from '@anticrm/task'
export async function createDeps (client: Client): Promise<void> {
const tx = new TxOperations(client, core.account.System)
await tx.createDoc(
task.class.Sequence,
task.space.Sequence,
{
attachedTo: recruit.class.Applicant,
sequence: 0
}
)
await createSequence(tx, recruit.class.Applicant)
await createSequence(tx, recruit.class.Review)
await createSequence(tx, recruit.class.Opinion)
await createDefaultKanbanTemplate(tx)
await createReviewTemplates(tx)
}
export async function createSequence (tx: TxOperations, _class: Ref<Class<Doc>>): Promise<void> {
if (await tx.findOne(task.class.Sequence, { attachedTo: _class }) === undefined) {
await tx.createDoc(
task.class.Sequence,
task.space.Sequence,
{
attachedTo: _class,
sequence: 0
}
)
}
}
const defaultKanban = {
@ -59,3 +67,40 @@ export const createDefaultKanbanTemplate = async (client: TxOperations): Promise
states: defaultKanban.states,
doneStates: defaultKanban.doneStates
})
export async function createReviewTemplates (tx: TxOperations): Promise<void> {
if (await tx.findOne(core.class.TxCreateDoc, { objectId: recruit.template.Interview }) === undefined) {
await createKanbanTemplate(tx, {
kanbanId: recruit.template.Interview,
space: recruit.space.ReviewTemplates as Ref<Doc> as Ref<Space>,
title: 'Interview',
states: [
{ color: 9, title: 'Prepare' },
{ color: 10, title: 'Appointment' },
{ color: 1, title: 'Opinions' }
],
doneStates: [
{ isWon: true, title: 'Pass' },
{ isWon: false, title: 'Failed' }
]
})
}
if (await tx.findOne(core.class.TxCreateDoc, { objectId: recruit.template.Task }) === undefined) {
await createKanbanTemplate(tx, {
kanbanId: recruit.template.Task,
space: recruit.space.ReviewTemplates as Ref<Doc> as Ref<Space>,
title: 'Test task',
states: [
{ color: 9, title: 'Prepare' },
{ color: 10, title: 'Assigned' },
{ color: 1, title: 'Review' },
{ color: 4, title: 'Opinions' }
],
doneStates: [
{ isWon: true, title: 'Pass' },
{ isWon: false, title: 'Failed' }
]
})
}
}

View File

@ -39,7 +39,9 @@ import task, { TSpaceWithStates, TTask } from '@anticrm/model-task'
import view from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench'
import { Applicant, Candidate, Candidates, Vacancy } from '@anticrm/recruit'
import { TOpinion, TReview, TReviewCategory } from './review-model'
import recruit from './plugin'
import { createReviewModel } from './review'
@Model(recruit.class.Vacancy, task.class.SpaceWithStates)
@UX(recruit.string.Vacancy, recruit.icon.Vacancy)
@ -92,6 +94,9 @@ export class TCandidate extends TPerson implements Candidate {
@Prop(Collection(tags.class.TagReference, recruit.string.SkillLabel), recruit.string.SkillsLabel)
skills?: number
@Prop(Collection(recruit.class.Review, recruit.string.Review), recruit.string.Reviews)
reviews?: number
}
@Model(recruit.class.Applicant, task.class.Task)
@ -112,7 +117,7 @@ export class TApplicant extends TTask implements Applicant {
}
export function createModel (builder: Builder): void {
builder.createModel(TVacancy, TCandidates, TCandidate, TApplicant)
builder.createModel(TVacancy, TCandidates, TCandidate, TApplicant, TReviewCategory, TReview, TOpinion)
builder.mixin(recruit.class.Vacancy, core.class.Class, workbench.mixin.SpaceView, {
view: {
@ -138,6 +143,13 @@ export function createModel (builder: Builder): void {
hidden: false,
navigatorModel: {
spaces: [
{
label: recruit.string.ReviewCategory,
spaceClass: recruit.class.ReviewCategory,
addSpaceLabel: recruit.string.CreateReviewCategory,
createComponent: recruit.component.CreateReviewCategory,
component: recruit.component.EditReviewCategory
}
],
specials: [
{
@ -384,6 +396,7 @@ export function createModel (builder: Builder): void {
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.IgnoreActions, {
actions: [view.action.Delete]
})
createReviewModel(builder)
}
export { createDeps } from './creation'

View File

@ -22,8 +22,9 @@ import contact, { DOMAIN_CONTACT } from '@anticrm/model-contact'
import tags, { DOMAIN_TAGS, TagCategory, TagElement } from '@anticrm/model-tags'
import { DOMAIN_TASK } from '@anticrm/model-task'
import { Candidate } from '@anticrm/recruit'
import recruit from './plugin'
import { getCategories } from '@anticrm/skillset'
import { createReviewTemplates, createSequence } from './creation'
import recruit from './plugin'
function toCandidateData (c: Pick<Candidate, 'onsite'|'title'|'remote'|'source'> | undefined): MixinData<Person, Candidate> {
if (c === undefined) {
@ -199,6 +200,10 @@ export const recruitOperation: MigrateOperation = {
await tx.update(t, { category: category })
}
}
await createReviewTemplates(tx)
await createSequence(tx, recruit.class.Review)
await createSequence(tx, recruit.class.Opinion)
}
}
async function migrateUpdateCandidateToPersonAndMixin (client: MigrationClient): Promise<void> {

View File

@ -26,11 +26,15 @@ import { ObjectSearchFactory, ObjectSearchCategory } from '@anticrm/model-presen
export default mergeIds(recruitId, recruit, {
action: {
CreateApplication: '' as Ref<Action>,
EditVacancy: '' as Ref<Action>
EditVacancy: '' as Ref<Action>,
CreateReview: '' as Ref<Action>,
CreateOpinion: '' as Ref<Action>
},
actionImpl: {
CreateApplication: '' as Resource<(object: Doc) => Promise<void>>,
EditVacancy: '' as Resource<(object: Doc) => Promise<void>>
EditVacancy: '' as Resource<(object: Doc) => Promise<void>>,
CreateReview: '' as Resource<(object: Doc) => Promise<void>>,
CreateOpinion: '' as Resource<(object: Doc) => Promise<void>>
},
string: {
ApplicationShort: '' as IntlString,
@ -47,7 +51,8 @@ export default mergeIds(recruitId, recruit, {
EditVacancy: '' as IntlString
},
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>>,
ReviewValidator: '' as Resource<<T extends Doc>(doc: T, client: Client) => Promise<Status>>
},
component: {
CreateVacancy: '' as AnyComponent,
@ -62,10 +67,23 @@ export default mergeIds(recruitId, recruit, {
Candidates: '' as AnyComponent,
CreateCandidate: '' as AnyComponent,
SkillsView: '' as AnyComponent,
Vacancies: '' as AnyComponent
Vacancies: '' as AnyComponent,
CreateReviewCategory: '' as AnyComponent,
EditReviewCategory: '' as AnyComponent,
CreateReview: '' as AnyComponent,
Reviews: '' as AnyComponent,
KanbanReviewCard: '' as AnyComponent,
EditReview: '' as AnyComponent,
ReviewPresenter: '' as AnyComponent,
Opinions: '' as AnyComponent,
OpinionPresenter: '' as AnyComponent
},
template: {
DefaultVacancy: '' as Ref<KanbanTemplate>
DefaultVacancy: '' as Ref<KanbanTemplate>,
Interview: '' as Ref<KanbanTemplate>,
Task: '' as Ref<KanbanTemplate>
},
completion: {
ApplicationQuery: '' as Resource<ObjectSearchFactory>,

View File

@ -0,0 +1,82 @@
import { Employee } from '@anticrm/contact'
import { Domain, IndexKind, Ref, Timestamp } from '@anticrm/core'
import { Collection, Index, Model, Prop, TypeDate, TypeMarkup, TypeRef, TypeString, UX } from '@anticrm/model'
import attachment from '@anticrm/model-attachment'
import chunter from '@anticrm/model-chunter'
import contact from '@anticrm/model-contact'
import core, { TAttachedDoc } from '@anticrm/model-core'
import task, { TSpaceWithStates, TTask } from '@anticrm/model-task'
import { Candidate, Opinion, Review, ReviewCategory } from '@anticrm/recruit'
import recruit from './plugin'
@Model(recruit.class.ReviewCategory, task.class.SpaceWithStates)
@UX(recruit.string.ReviewCategory, recruit.icon.Review)
export class TReviewCategory extends TSpaceWithStates implements ReviewCategory {
@Prop(TypeString(), recruit.string.FullDescription)
fullDescription?: string
}
@Model(recruit.class.Review, task.class.Task)
@UX(recruit.string.Review, recruit.icon.Review, recruit.string.ReviewShortLabel, 'number')
export class TReview extends TTask implements Review {
// We need to declare, to provide property with label
@Prop(TypeRef(recruit.class.Applicant), recruit.string.Candidate)
declare attachedTo: Ref<Candidate>
@Prop(TypeRef(contact.class.Employee), recruit.string.AssignedRecruiter)
declare assignee: Ref<Employee> | null
@Prop(TypeMarkup(), recruit.string.Description)
@Index(IndexKind.FullText)
description!: string
@Index(IndexKind.FullText)
@Prop(TypeMarkup(), recruit.string.Verdict)
verdict!: string
@Index(IndexKind.FullText)
@Prop(TypeString(), recruit.string.Location, recruit.icon.Location)
location?: string
@Index(IndexKind.FullText)
@Prop(TypeString(), recruit.string.Company, contact.icon.Company)
company?: string
@Prop(TypeDate(), recruit.string.StartDate)
startDate!: Timestamp | null
@Prop(TypeDate(), recruit.string.DueDate)
dueDate!: Timestamp | null
@Prop(Collection(recruit.class.Opinion), recruit.string.Opinions)
opinions?: number
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments)
attachments?: number
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
comments?: number
}
@Model(recruit.class.Opinion, core.class.AttachedDoc, 'recruit' as Domain)
@UX(recruit.string.Opinion, recruit.icon.Opinion, recruit.string.OpinionShortLabel)
export class TOpinion extends TAttachedDoc implements Opinion {
@Prop(TypeString(), task.string.TaskNumber)
number!: number
// We need to declare, to provide property with label
@Prop(TypeRef(recruit.class.Review), recruit.string.Review)
declare attachedTo: Ref<Review>
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments)
attachments?: number
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
comments?: number
@Prop(TypeMarkup(), recruit.string.Description)
description!: string
@Prop(TypeString(), recruit.string.OpinionValue)
value!: string
}

View File

@ -0,0 +1,179 @@
import { Doc, FindOptions } from '@anticrm/core'
import { Builder } from '@anticrm/model'
import attachment from '@anticrm/model-attachment'
import chunter from '@anticrm/model-chunter'
import contact from '@anticrm/model-contact'
import core from '@anticrm/model-core'
import task from '@anticrm/model-task'
import view from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench'
import recruit from './plugin'
export function createReviewModel (builder: Builder): void {
builder.mixin(recruit.class.ReviewCategory, core.class.Class, workbench.mixin.SpaceView, {
view: {
class: recruit.class.Review,
createItemDialog: recruit.component.CreateReview
}
})
builder.mixin(recruit.class.Review, core.class.Class, view.mixin.AttributeEditor, {
editor: recruit.component.Reviews
})
createTableViewlet(builder)
createKanbanViewlet(builder)
createStatusTableViewlet(builder)
builder.mixin(recruit.class.Review, core.class.Class, task.mixin.KanbanCard, {
card: recruit.component.KanbanReviewCard
})
builder.mixin(recruit.class.Review, core.class.Class, view.mixin.ObjectEditor, {
editor: recruit.component.EditReview
})
builder.mixin(recruit.class.Review, core.class.Class, view.mixin.AttributePresenter, {
presenter: recruit.component.ReviewPresenter
})
builder.mixin(recruit.class.Review, core.class.Class, view.mixin.ObjectValidator, {
validator: recruit.validator.ReviewValidator
})
builder.mixin(recruit.class.Opinion, core.class.Class, view.mixin.AttributePresenter, {
presenter: recruit.component.OpinionPresenter
})
builder.mixin(recruit.class.Review, core.class.Class, view.mixin.ObjectEditor, {
editor: recruit.component.EditReview
})
builder.createDoc(
view.class.Action,
core.space.Model,
{
label: recruit.string.CreateReview,
icon: recruit.icon.Create,
action: recruit.actionImpl.CreateReview
},
recruit.action.CreateReview
)
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: recruit.class.Applicant,
action: recruit.action.CreateReview
})
builder.createDoc(
task.class.KanbanTemplateSpace,
core.space.Model,
{
name: recruit.string.ReviewCategory,
description: task.string.ManageStatusesWithin,
icon: recruit.component.TemplatesIcon
},
recruit.space.ReviewTemplates
)
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: recruit.class.ReviewCategory,
action: task.action.ArchiveSpace,
query: {
archived: false
}
})
}
function createStatusTableViewlet (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: recruit.class.Review,
descriptor: task.viewlet.StatusTable,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: {
attachedTo: recruit.mixin.Candidate,
state: task.class.State,
assignee: contact.class.Employee,
doneState: task.class.DoneState
}
} as FindOptions<Doc>,
config: [
'',
'$lookup.attachedTo',
// '$lookup.assignee',
// 'location',
'company',
'dueDate',
{ key: '', presenter: recruit.component.OpinionsPresenter, label: recruit.string.Opinions, sortingKey: 'opinions' },
'$lookup.state',
'$lookup.doneState',
{ presenter: attachment.component.AttachmentsPresenter, label: attachment.string.Files, sortingKey: 'attachments' },
{ presenter: chunter.component.CommentsPresenter, label: chunter.string.Comments, sortingKey: 'comments' },
'modifiedOn'
]
})
builder.createDoc(
view.class.Action,
core.space.Model,
{
label: recruit.string.CreateOpinion,
icon: recruit.icon.Create,
action: recruit.actionImpl.CreateOpinion
},
recruit.action.CreateOpinion
)
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: recruit.class.Review,
action: recruit.action.CreateOpinion
})
}
function createKanbanViewlet (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: recruit.class.Review,
descriptor: task.viewlet.Kanban,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: {
attachedTo: recruit.mixin.Candidate,
state: task.class.State
}
} as FindOptions<Doc>,
config: ['$lookup.attachedTo', '$lookup.state']
})
}
function createTableViewlet (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: recruit.class.Review,
descriptor: view.viewlet.Table,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: {
attachedTo: recruit.mixin.Candidate,
state: task.class.State,
assignee: contact.class.Employee,
doneState: task.class.DoneState
}
} as FindOptions<Doc>,
config: [
'',
'$lookup.attachedTo',
// '$lookup.assignee',
// 'location',
'company',
'dueDate',
{ key: '', presenter: recruit.component.OpinionsPresenter, label: recruit.string.Opinions, sortingKey: 'opinions' },
'$lookup.state',
'$lookup.doneState',
{ presenter: attachment.component.AttachmentsPresenter, label: attachment.string.Files, sortingKey: 'attachments' },
{ presenter: chunter.component.CommentsPresenter, label: chunter.string.Comments, sortingKey: 'comments' },
'modifiedOn'
]
})
builder.mixin(recruit.class.Opinion, core.class.Class, view.mixin.AttributeEditor, {
editor: recruit.component.Opinions
})
}

View File

@ -20,7 +20,7 @@ import { readable } from 'svelte/store'
import Root from './components/internal/Root.svelte'
export type { AnyComponent, AnySvelteComponent, Action, LabelAndProps, TooltipAligment } from './types'
export type { AnyComponent, AnySvelteComponent, Action, LabelAndProps, TooltipAligment, AnySvelteComponentWithProps } from './types'
// export { applicationShortcutKey } from './utils'
export { getCurrentLocation, locationToUrl, navigate, location } from './location'

View File

@ -30,6 +30,15 @@ export type AnySvelteComponent = typeof SvelteComponent
export type Component<C extends AnySvelteComponent> = Resource<C>
export type AnyComponent = Resource<AnySvelteComponent>
/**
* Allow to pass component with some predefined properties.
* @public
*/
export interface AnySvelteComponentWithProps {
component: AnySvelteComponent
props?: Record<string, any>
}
export interface Action {
label: IntlString
icon: Asset | AnySvelteComponent

View File

@ -22,21 +22,21 @@
{#if i === 0}
<div class="mr-1"><IconAdd size={'small'} /></div>
{/if}
{#if typeof viewlet?.component === 'string'}
<Component is={viewlet?.component} props={getProps(getDTxProps(ctx), edit)} on:close={onCancelEdit} />
{:else}
<svelte:component this={viewlet?.component} {...getProps(getDTxProps(ctx), edit)} on:close={onCancelEdit} />
{/if}
{#if typeof viewlet?.component === 'string'}
<Component is={viewlet?.component} props={getProps(getDTxProps(ctx), edit)} on:close={onCancelEdit} />
{:else}
<svelte:component this={viewlet?.component} {...getProps(getDTxProps(ctx), edit)} on:close={onCancelEdit} />
{/if}
{/each}
{#each filterTx([tx, ...tx.txes], core.class.TxRemoveDoc) as ctx, i}
{#if i === 0}
<div class="mr-1"><IconDelete size={'small'} /></div>
{/if}
{#if typeof viewlet?.component === 'string'}
<Component is={viewlet?.component} props={getProps(getDTxProps(ctx), edit)} on:close={onCancelEdit} />
{:else}
<svelte:component this={viewlet?.component} {...getProps(getDTxProps(ctx), edit)} on:close={onCancelEdit} />
{/if}
{#if typeof viewlet?.component === 'string'}
<Component is={viewlet?.component} props={getProps(getDTxProps(ctx), edit)} on:close={onCancelEdit} />
{:else}
<svelte:component this={viewlet?.component} {...getProps(getDTxProps(ctx), edit)} on:close={onCancelEdit} />
{/if}
{/each}
</div>

View File

@ -0,0 +1,69 @@
<!--
// 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 { Employee, EmployeeAccount, formatName } from '@anticrm/contact'
import { Account } from '@anticrm/core'
import { Avatar, getClient } from '@anticrm/presentation'
import { showPopup } from '@anticrm/ui'
import { EditDoc } from '@anticrm/view-resources'
import contact from '../plugin'
export let value: Account
let employee: Employee | undefined
async function onClick () {
if (employee !== undefined) {
showPopup(EditDoc, { _id: employee._id, _class: employee._class }, 'full')
}
}
const client = getClient()
$: if (value._class === contact.class.EmployeeAccount) {
client.findOne(contact.class.Employee, { _id: (value as EmployeeAccount).employee }).then(r => {
employee = r
})
}
</script>
{#if value}
<div class="flex-row-center" class:user-container={employee !== undefined} on:click={onClick}>
{#if employee}
<Avatar size={'x-small'} avatar={employee.avatar} />
<div class="overflow-label user">{formatName(employee.name)}</div>
{:else}
{JSON.stringify(value)}
<div class="overflow-label user">{value.email}</div>
{/if}
</div>
{/if}
<style lang="scss">
.user-container {
cursor: pointer;
.user {
margin-left: 0.5rem;
font-weight: 500;
text-align: left;
color: var(--theme-content-accent-color);
}
&:hover .user {
text-decoration: underline;
color: var(--theme-caption-color);
}
}
</style>

View File

@ -34,6 +34,7 @@ import OrganizationPresenter from './components/OrganizationPresenter.svelte'
import PersonPresenter from './components/PersonPresenter.svelte'
import SocialEditor from './components/SocialEditor.svelte'
import contact from './plugin'
import EmployeeAccountPresenter from './components/EmployeeAccountPresenter.svelte'
export { Channels, ChannelsEditor, ContactPresenter, ChannelsView }
@ -61,7 +62,8 @@ export default async (): Promise<Resources> => ({
CreatePersons,
CreateOrganizations,
SocialEditor,
Contacts
Contacts,
EmployeeAccountPresenter
},
completion: {
EmployeeQuery: async (client: Client, query: string) => await queryContact(contact.class.Employee, client, query),

View File

@ -51,10 +51,36 @@
"PersonLocationPlaceholder": "Location",
"ManageVacancyStatuses": "Manage vacancy statuses",
"EditVacancy": "Edit",
"FullDescription": "Full description"
"FullDescription": "Full description",
"CreateReviewCategory": "Create review category",
"ReviewCategoryName": "Review category title*",
"ReviewCategoryPlaceholder": "Interview",
"ReviewCategory": "Reviews",
"ReviewCategoryDescription": "Description",
"ThisReviewCategoryIsPrivate": "This category is private",
"CreateReview": "Create review",
"SelectReviewCategory": "select category",
"Reviews": "Reviews",
"Review": "Review",
"Opinions": "Opinions",
"Opinion": "Opinion",
"OpinionValue": "Rating",
"OpinionShortLabel": "OPE",
"ReviewShortLabel": "RVE",
"StartDate": "Start date",
"DueDate": "Due date",
"ReviewCategoryTitle":"Category",
"Verdict": "Verdict",
"OpinionSave": "Save",
"CandidateReviews": "All Candidate Reviews",
"NoReviewForCandidate": "No reviews",
"CreateAnReview": "Create review",
"CreateOpinion": "Create opinion",
"OpinionValuePlaceholder": "10/10"
},
"status": {
"CandidateRequired": "Please select candidate",
"VacancyRequired": "Please select vacancy"
"VacancyRequired": "Please select vacancy",
"ReviewCategoryRequired": "Please select review category"
}
}

View File

@ -51,10 +51,37 @@
"PersonLocationPlaceholder": "Местоположение",
"ManageVacancyStatuses": "Управление статусами вакансии",
"EditVacancy": "Редактировать",
"FullDescription": "Детальное описание"
"FullDescription": "Детальное описание",
"CreateReviewCategory": "Создать категорию оценки",
"ReviewCategoryName": "Имя категории*",
"ReviewCategoryPlaceholder": "Интервью",
"ReviewCategory": "Оценки",
"ReviewCategoryDescription": "Описание",
"ThisReviewCategoryIsPrivate": "Эта категория личная",
"CreateReview": "Запланировать оценку",
"SelectReviewCategory": "выбрать категорию",
"Reviews": "Оценки",
"Review": "Оценка",
"Opinions": "Мнения",
"Opinion": "Мнение",
"OpinionValue": "Оценка",
"OpinionShortLabel": "OPE",
"ReviewShortLabel": "RVE",
"StartDate": "Дата начала",
"DueDate": "Дата конца",
"ReviewCategoryTitle":"Категория",
"Verdict": "Вердикт",
"OpinionSave": "Сохранить",
"CandidateReviews": "Оценки кандидата",
"NoReviewForCandidate": "Нет оценок",
"CreateAnReview": "Добавить оценку",
"CreateOpinion": "Добавить мнение",
"OpinionValuePlaceholder": "10/10"
},
"status": {
"CandidateRequired": "Пожалуйста выберите кандидата",
"VacancyRequired": "Пожалуйста выберите вакансию"
"VacancyRequired": "Пожалуйста выберите вакансию",
"ReviewCategoryRequired": "Пожалуйста выберети кадегорию"
}
}

View File

@ -23,7 +23,9 @@ loadMetadata(recruit.icon, {
Location: `${icons}#location`,
Calendar: `${icons}#calendar`,
Create: `${icons}#create`,
Application: `${icons}#application`
Application: `${icons}#application`,
Review: `${icons}#application`,
Opinion: `${icons}#application`
})
addStringsLoader(recruitId, async (lang: string) => await import(`../lang/${lang}.json`))

View File

@ -0,0 +1,28 @@
<!--
// 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 { getClient } from '@anticrm/presentation'
import type { Applicant } from '@anticrm/recruit'
export let value: Applicant
const client = getClient()
const shortLabel = client.getHierarchy().getClass(value._class).shortLabel
</script>
{#if value && shortLabel}
{shortLabel}-{value.number}
{/if}

View File

@ -23,6 +23,7 @@
import recruit from '../plugin'
import { Ref } from '@anticrm/core'
import Reviews from './review/Reviews.svelte'
export let object: Applicant
let candidate: Candidate
@ -55,6 +56,9 @@
<div class="arrows"><ExpandRightDouble /></div>
<div class="card"><VacancyCard {vacancy} /></div>
</div>
<div class='mt-6'>
<Reviews objectId={candidate._id} reviews={candidate.reviews ?? 0} label={recruit.string.CandidateReviews} />
</div>
{/if}
<style lang="scss">

View File

@ -0,0 +1,25 @@
<!--
// 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">
export let size: 'x-small' | 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M11.7,3.6h-1.1C10.4,2.7,9.7,2,8.8,2H7.2C6.3,2,5.6,2.7,5.4,3.6H4.3C3,3.6,2,4.6,2,5.9V8c0,0.2,0.1,0.3,0.2,0.4 C3.8,9.3,5.9,9.9,8,9.9c2.1,0,4.2-0.5,5.8-1.5C13.9,8.3,14,8.2,14,8V5.9C14,4.6,13,3.6,11.7,3.6z M7.2,3h1.5c0.4,0,0.6,0.2,0.8,0.6 H6.5C6.6,3.2,6.9,3,7.2,3z M13,7.7c-1.4,0.8-3.2,1.2-5,1.2c-1.8,0-3.6-0.4-5-1.2V5.9c0-0.7,0.6-1.3,1.3-1.3h7.4 c0.7,0,1.3,0.6,1.3,1.3V7.7z"/>
<path d="M13.5,9.7c-0.3,0-0.5,0.2-0.5,0.5l-0.1,1.5c-0.1,0.8-0.7,1.3-1.4,1.3H4.5c-0.7,0-1.4-0.6-1.4-1.3L3,10.1 c0-0.3-0.3-0.5-0.5-0.5C2.2,9.7,2,9.9,2,10.2l0.1,1.5C2.2,13,3.3,14,4.5,14h6.9c1.3,0,2.3-1,2.4-2.3l0.1-1.5 C14,9.9,13.8,9.7,13.5,9.7z"/>
</svg>

View File

@ -0,0 +1,107 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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 type { Employee } from '@anticrm/contact'
import { Account, generateId, Ref } from '@anticrm/core'
import { OK, Status } from '@anticrm/platform'
import { Card, getClient } from '@anticrm/presentation'
import type { Opinion, Review } from '@anticrm/recruit'
import task, { SpaceWithStates } from '@anticrm/task'
import { StyledTextEditor } from '@anticrm/text-editor'
import { EditBox, Grid, Label, Status as StatusControl } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import recruit from '../../plugin'
export let space: Ref<SpaceWithStates>
export let review: Ref<Review>
export let assignee: Ref<Employee>
const status: Status = OK
const doc: Opinion = {
number: 0,
attachedTo: review,
attachedToClass: recruit.class.Review,
_class: recruit.class.Opinion,
space: space,
_id: generateId(),
collection: 'reviews',
modifiedOn: Date.now(),
modifiedBy: '' as Ref<Account>,
description: '',
value: ''
}
const dispatch = createEventDispatcher()
const client = getClient()
export function canClose (): boolean {
return review === undefined && assignee === undefined
}
async function createOpinion () {
const sequence = await client.findOne(task.class.Sequence, { attachedTo: recruit.class.Review })
if (sequence === undefined) {
throw new Error('sequence object not found')
}
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
const reviewInstance = await client.findOne(recruit.class.Review, { _id: doc.attachedTo as Ref<Review> })
if (reviewInstance === undefined) {
throw new Error('contact not found')
}
await client.addCollection(
recruit.class.Opinion,
reviewInstance.space, doc.attachedTo, doc.attachedToClass, 'opinions',
{
number: (incResult as any).object.sequence,
description: doc.description,
value: doc.value
}
)
}
</script>
<Card
label={recruit.string.CreateOpinion}
okAction={createOpinion}
canSave={(doc.value ?? '').trim().length > 0}
bind:space={doc.space}
on:close={() => {
dispatch('close')
}}
>
<StatusControl slot="error" {status} />
<Grid column={1} rowGap={1.75}>
<EditBox bind:value={doc.value} label={recruit.string.OpinionValue} placeholder={recruit.string.OpinionValuePlaceholder} focus maxWidth={'10rem'}/>
<div class='mt-1 mb-1'>
<Label label={recruit.string.Description}/>:
</div>
<div class='description flex'>
<StyledTextEditor bind:content={doc.description}/>
</div>
</Grid>
</Card>
<style lang="scss">
.description{
height: 10rem;
padding: 0.5rem;
border: 1px solid var(--theme-menu-divider);
border-radius: 8px;
}
</style>

View File

@ -0,0 +1,161 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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 type { Contact, Employee, Person } from '@anticrm/contact'
import contact from '@anticrm/contact'
import { Account, Class, Client, Doc, generateId, Ref, SortingOrder } from '@anticrm/core'
import { getResource, OK, Resource, Severity, Status } from '@anticrm/platform'
import { Card, getClient, UserBox } from '@anticrm/presentation'
import type { Candidate, Review } from '@anticrm/recruit'
import task, { calcRank, SpaceWithStates, State } from '@anticrm/task'
import { Grid, Status as StatusControl } from '@anticrm/ui'
import {DatePicker} from '@anticrm/ui'
import view from '@anticrm/view'
import { createEventDispatcher } from 'svelte'
import recruit from '../../plugin'
export let space: Ref<SpaceWithStates>
export let candidate: Ref<Person>
export let assignee: Ref<Employee>
export let preserveCandidate = false
let status: Status = OK
let startDate: Date = new Date()
let dueDate: Date = new Date()
const doc: Review = {
state: '' as Ref<State>,
doneState: null,
number: 0,
assignee: assignee,
rank: '',
attachedTo: candidate,
attachedToClass: recruit.mixin.Candidate,
_class: recruit.class.Review,
space: space,
_id: generateId(),
collection: 'reviews',
modifiedOn: Date.now(),
modifiedBy: '' as Ref<Account>,
startDate: null,
dueDate: null,
description: '',
verdict: ''
}
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
export function canClose (): boolean {
return candidate === undefined && assignee === undefined
}
async function createReview () {
const state = await client.findOne(task.class.State, { space: doc.space })
if (state === undefined) {
throw new Error('create application: state not found')
}
const sequence = await client.findOne(task.class.Sequence, { attachedTo: recruit.class.Review })
if (sequence === undefined) {
throw new Error('sequence object not found')
}
const lastOne = await client.findOne(
recruit.class.Review,
{ state: state._id },
{ sort: { rank: SortingOrder.Descending } }
)
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
const candidateInstance = await client.findOne(contact.class.Person, { _id: doc.attachedTo as Ref<Person> })
if (candidateInstance === undefined) {
throw new Error('contact not found')
}
if (!client.getHierarchy().hasMixin(candidateInstance, recruit.mixin.Candidate)) {
await client.createMixin<Contact, Candidate>(candidateInstance._id, candidateInstance._class, candidateInstance.space, recruit.mixin.Candidate, {})
}
await client.addCollection(
recruit.class.Review,
doc.space, doc.attachedTo, doc.attachedToClass, 'reviews',
{
state: state._id,
doneState: null,
number: (incResult as any).object.sequence,
assignee: doc.assignee,
rank: calcRank(lastOne, undefined),
startDate: startDate?.getTime() ?? null,
dueDate: dueDate?.getTime() ?? null,
description: '',
verdict: ''
}
)
}
async function invokeValidate (
action: Resource<<T extends Doc>(doc: T, client: Client) => Promise<Status>>
): Promise<Status> {
const impl = await getResource(action)
return await impl(doc, client)
}
async function validate (doc: Review, _class: Ref<Class<Doc>>): Promise<void> {
const clazz = hierarchy.getClass(_class)
const validatorMixin = hierarchy.as(clazz, view.mixin.ObjectValidator)
if (validatorMixin?.validator != null) {
status = await invokeValidate(validatorMixin.validator)
} else if (clazz.extends != null) {
await validate(doc, clazz.extends)
} else {
status = OK
}
}
$: validate(doc, doc._class)
</script>
<Card
label={recruit.string.CreateReview}
okAction={createReview}
canSave={status.severity === Severity.OK}
spaceClass={recruit.class.ReviewCategory}
spaceQuery={{ archived: false }}
spaceLabel={recruit.string.ReviewCategory}
spacePlaceholder={recruit.string.SelectReviewCategory}
bind:space={doc.space}
on:close={() => {
dispatch('close')
}}
>
<StatusControl slot="error" {status} />
<Grid column={1} rowGap={1.75}>
{#if !preserveCandidate}
<UserBox _class={contact.class.Person} title={recruit.string.Candidate} caption={recruit.string.Candidates} bind:value={doc.attachedTo} />
{/if}
<UserBox
_class={contact.class.Employee}
title={recruit.string.AssignRecruiter}
caption={recruit.string.Recruiters}
bind:value={doc.assignee}
allowDeselect
titleDeselect={recruit.string.UnAssignRecruiter}
/>
<DatePicker title={recruit.string.StartDate} bind:selected={startDate}/>
<DatePicker title={recruit.string.DueDate} bind:selected={dueDate}/>
</Grid>
</Card>

View File

@ -0,0 +1,72 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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 core, { Ref } from '@anticrm/core'
import { getClient, SpaceCreateCard } from '@anticrm/presentation'
import task, { createKanban, KanbanTemplate } from '@anticrm/task'
import { Component, Dropdown, EditBox, Grid } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import recruit from '../../plugin'
import Company from '../icons/Company.svelte'
import Review from '../icons/Review.svelte'
const dispatch = createEventDispatcher()
let name: string = ''
const description: string = ''
let templateId: Ref<KanbanTemplate> | undefined
export function canClose (): boolean {
return name === '' && templateId !== undefined
}
const client = getClient()
async function createReviewCategory () {
if (templateId !== undefined && await client.findOne(task.class.KanbanTemplate, { _id: templateId }) === undefined) {
throw Error(`Failed to find target kanban template: ${templateId}`)
}
const id = await client.createDoc(recruit.class.ReviewCategory, core.space.Model, {
name,
description,
private: false,
archived: false,
members: []
})
await createKanban(client, id, templateId)
}
</script>
<SpaceCreateCard
label={recruit.string.CreateReviewCategory}
okAction={createReviewCategory}
canSave={!!name}
on:close={() => { dispatch('close') }}
>
<Grid column={1} rowGap={1.5}>
<EditBox label={recruit.string.ReviewCategoryName} bind:value={name} icon={Review} placeholder={recruit.string.ReviewCategoryPlaceholder} maxWidth={'16rem'} focus/>
<Dropdown icon={Company} label={recruit.string.Company} placeholder={'Company'} />
<Component is={task.component.KanbanTemplateSelector} props={{
folders: [recruit.space.ReviewTemplates],
template: templateId
}} on:change={(evt) => {
templateId = evt.detail
}}/>
</Grid>
</SpaceCreateCard>

View File

@ -0,0 +1,95 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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 type { DocumentUpdate, Ref } from '@anticrm/core'
import { Card, getClient } from '@anticrm/presentation'
import { Opinion } from '@anticrm/recruit'
import { StyledTextEditor } from '@anticrm/text-editor'
import { EditBox, Grid, Label } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import recruit from '../../plugin'
export let item: Opinion
let value: string = ''
let description: string = ''
let _itemId: Ref<Opinion>
$: if (_itemId !== item._id) {
_itemId = item._id
value = item.value
description = item.description
}
const dispatch = createEventDispatcher()
const client = getClient()
export function canClose (): boolean {
return true
}
async function editOpinion () {
const ops: DocumentUpdate<Opinion> = {}
if (item.value !== value) {
ops.value = value
}
if (item.description !== description) {
ops.description = description
}
if (Object.keys(ops).length === 0) {
return
}
await client.update(item, ops)
}
</script>
<Card
label={recruit.string.Opinion}
okAction={editOpinion}
canSave={value.length > 0}
space={item.space}
on:close={() => {
dispatch('close')
}}
okLabel={recruit.string.OpinionSave}>
<Grid column={1} rowGap={1.75}>
<EditBox
label={recruit.string.OpinionValue}
bind:value={value}
icon={recruit.icon.Application}
placeholder={recruit.string.OpinionValue}
maxWidth="39rem"
focus
/>
<div class='mt-1 mb-1'>
<Label label={recruit.string.Description}/>:
</div>
<div class='description flex'>
<StyledTextEditor bind:content={description} placeholder={recruit.string.Description}/>
</div>
</Grid>
</Card>
<style lang="scss">
.description{
height: 10rem;
padding: 0.5rem;
border: 1px solid var(--theme-menu-divider);
border-radius: 8px;
}
</style>

View File

@ -0,0 +1,126 @@
<!--
// 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 contact from '@anticrm/contact'
import { createQuery, getClient } from '@anticrm/presentation'
import type { Candidate, Review, ReviewCategory } from '@anticrm/recruit'
import { StyledTextBox } from '@anticrm/text-editor'
import { EditBox, Grid, Label } from '@anticrm/ui'
import { createEventDispatcher, onMount } from 'svelte'
import recruit from '../../plugin'
import CandidateCard from '../CandidateCard.svelte'
import ExpandRightDouble from '../icons/ExpandRightDouble.svelte'
import ReviewCategoryCard from './ReviewCategoryCard.svelte'
export let object: Review
let candidate: Candidate
let reviewCategory: ReviewCategory
const candidateQuery = createQuery()
$: if (object !== undefined) {
candidateQuery.query(recruit.mixin.Candidate, { _id: object.attachedTo }, (result) => {
candidate = result[0]
})
}
const reviewCategoryQuery = createQuery()
$: if (candidate !== undefined) {
reviewCategoryQuery.query(recruit.class.ReviewCategory, { _id: object.space }, (result) => {
reviewCategory = result[0]
})
}
const dispatch = createEventDispatcher()
const client = getClient()
onMount(() => {
dispatch('open', { ignoreKeys: ['location', 'company', 'number', 'comments', 'startDate', 'description'] })
})
</script>
{#if object !== undefined && candidate !== undefined}
<div class="flex-between">
<div class="card"><CandidateCard {candidate} /></div>
<div class="arrows"><ExpandRightDouble /></div>
<div class="card"><ReviewCategoryCard category={reviewCategory} /></div>
</div>
<div class="mt-4 mb-1">
<Grid column={1}>
<EditBox
label={recruit.string.Company}
bind:value={object.company}
icon={contact.icon.Company}
placeholder={recruit.string.Company}
maxWidth="39rem"
focus
on:change={() => client.update(object, { company: object.company })}
/>
<EditBox
label={recruit.string.Location}
bind:value={object.location}
icon={recruit.icon.Location}
placeholder={recruit.string.Location}
maxWidth="39rem"
focus
on:change={() => client.update(object, { location: object.location })}
/>
</Grid>
</div>
<div class="mt-4 mb-1">
<Label label={recruit.string.Description} />
</div>
<div class="description flex">
<StyledTextBox
content={object.description}
on:value={(evt) => {
console.log(evt.detail)
client.update(object, { description: evt.detail })
}}
/>
</div>
<div class="mt-4 mb-1">
<Label label={recruit.string.Verdict} />
</div>
<div class="description flex">
<StyledTextBox
content={object.verdict}
on:value={(evt) => {
client.update(object, { verdict: evt.detail })
}}
/>
</div>
{/if}
<style lang="scss">
.card {
align-self: stretch;
width: calc(50% - 3.5rem);
}
.arrows {
width: 4rem;
}
.description {
height: 10rem;
padding: 1rem;
border: 1px solid var(--theme-menu-divider);
border-radius: 8px;
}
</style>

View File

@ -0,0 +1,217 @@
<!--
// 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 activity from '@anticrm/activity'
import { Attachments } from '@anticrm/attachment-resources'
import type { Ref } from '@anticrm/core'
import type { IntlString } from '@anticrm/platform'
import { AttributesBar, createQuery, getClient } from '@anticrm/presentation'
import { ReviewCategory } from '@anticrm/recruit'
import { TextEditor } from '@anticrm/text-editor'
import { Component, EditBox, Grid, Icon, IconClose, Label, ToggleWithLabel } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import recruit from '../../plugin'
export let _id: Ref<ReviewCategory>
let object: ReviewCategory
const dispatch = createEventDispatcher()
const client = getClient()
const query = createQuery()
const clazz = client.getHierarchy().getClass(recruit.class.ReviewCategory)
$: query.query(recruit.class.ReviewCategory, { _id }, result => { object = result[0] })
const tabs: IntlString[] = ['General' as IntlString, 'Members' as IntlString, 'Activity' as IntlString]
let selected = 0
let textEditor: TextEditor
function onChange (key:string, value: any): void {
client.updateDoc(object._class, object.space, object._id, { [key]: value })
}
</script>
<div class="overlay" on:click={() => { dispatch('close') }}/>
<div class="dialog-container">
{#if object}
<div class="flex-row-center header">
<div class="flex-grow">
<div class="flex">
<div class="svg-medium flex-no-shrink">
{#if clazz.icon}<Icon icon={clazz.icon} size={'medium'} />{/if}
</div>
<div class="flex-grow fs-title ml-2">
{object.name}
</div>
</div>
<div class="small-text">{object.description}</div>
</div>
<div class="tool" on:click={() => { dispatch('close') }}><IconClose size={'small'} /></div>
</div>
<div class="flex-row-center subtitle">
<AttributesBar {object} keys={[]} />
</div>
<div class="flex-stretch tab-container">
{#each tabs as tab, i}
<div class="flex-row-center tab" class:selected={i === selected}
on:click={() => { selected = i }}>
<Label label={tab}/>
</div>
{/each}
<div class="grow"/>
</div>
<div class="scroll">
<div class="flex-col box">
{#if selected === 0}
<Grid column={1} rowGap={1.5}>
<EditBox label={recruit.string.ReviewCategoryName} bind:value={object.name} placeholder={recruit.string.ReviewCategoryPlaceholder} maxWidth="39rem" focus on:change={() => { onChange('name', object.name) }}/>
<EditBox label={recruit.string.Description} bind:value={object.description} placeholder={recruit.string.ReviewCategoryDescription} maxWidth="39rem" focus on:change={() => { onChange('description', object.description) }}/>
</Grid>
<div class="mt-10">
<span class="title">Description</span>
<div class="description-container">
<TextEditor bind:this={textEditor} bind:content={object.fullDescription} on:blur={textEditor.submit} on:content={() => { onChange('fullDescription', object.fullDescription) }} />
</div>
</div>
<div class="mt-14">
<Attachments objectId={object._id} _class={object._class} space={object.space} />
</div>
{:else if selected === 1}
<ToggleWithLabel label={recruit.string.ThisReviewCategoryIsPrivate} description={recruit.string.MakePrivateDescription}/>
{:else if selected === 2}
<Component is={activity.component.Activity} props={{ object, transparent: true }} />
{/if}
</div>
</div>
{/if}
</div>
<style lang="scss">
.dialog-container {
overflow: hidden;
position: fixed;
top: 32px;
bottom: 1.25rem;
left: 50%;
right: 1rem;
display: flex;
flex-direction: column;
height: calc(100% - 32px - 1.25rem);
background: var(--theme-bg-color);
border-radius: 1.25rem;
.header {
flex-shrink: 0;
padding: 0 2rem 0 2.5rem;
height: 4.5rem;
border-bottom: 1px solid var(--theme-dialog-divider);
.tool {
margin-left: .75rem;
color: var(--theme-content-accent-color);
cursor: pointer;
&:hover { color: var(--theme-caption-color); }
}
}
.subtitle {
flex-shrink: 0;
padding: 0 2.5rem;
height: 3.5rem;
border-bottom: 1px solid var(--theme-dialog-divider);
}
}
.tab-container {
flex-shrink: 0;
flex-wrap: nowrap;
margin: 0 2.5rem;
height: 4.5rem;
border-bottom: 1px solid var(--theme-menu-divider);
.tab {
height: 4.5rem;
font-weight: 500;
color: var(--theme-content-trans-color);
cursor: pointer;
user-select: none;
&.selected {
border-top: .125rem solid transparent;
border-bottom: .125rem solid var(--theme-caption-color);
color: var(--theme-caption-color);
cursor: default;
}
}
.tab + .tab {
margin-left: 2.5rem;
}
.grow {
min-width: 2.5rem;
flex-grow: 1;
}
}
.scroll {
flex-grow: 1;
overflow-x: hidden;
overflow-y: auto;
margin: 1rem 2rem;
padding: 1.5rem .5rem;
height: 100%;
.box {
margin-right: 1px;
height: 100%;
}
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000;
opacity: .5;
}
.title {
margin-right: .75rem;
font-weight: 500;
font-size: 1.25rem;
color: var(--theme-caption-color);
}
.description-container {
display: flex;
justify-content: space-between;
overflow-y: auto;
height: 100px;
padding: 0px 16px;
background-color: var(--theme-bg-accent-color);
border: 1px solid var(--theme-bg-accent-color);
border-top: 20px solid transparent;
border-bottom: 20px solid transparent;
border-radius: .75rem;
margin-top: 1.5rem;
}
</style>

View File

@ -0,0 +1,79 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
//
// 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 { AttachmentsPresenter } from '@anticrm/attachment-resources'
import { CommentsPresenter } from '@anticrm/chunter-resources'
import { formatName } from '@anticrm/contact'
import type { WithLookup } from '@anticrm/core'
import { Avatar } from '@anticrm/presentation'
import type { Review } from '@anticrm/recruit'
import { ActionIcon, IconMoreH, showPanel } from '@anticrm/ui'
import view from '@anticrm/view'
import ReviewPresenter from './ReviewPresenter.svelte'
export let object: WithLookup<Review>
export let draggable: boolean
function showCandidate () {
showPanel(view.component.EditDoc, object.attachedTo, object.attachedToClass, 'full')
}
</script>
<div class="card-container" {draggable} class:draggable on:dragstart on:dragend>
<div class="flex-between mb-3">
<Avatar avatar={object.$lookup?.attachedTo?.avatar} size={'medium'} />
<div class="flex-grow flex-col min-w-0 ml-2">
<div class="fs-title over-underline lines-limit-2" on:click={showCandidate}>
{#if object.$lookup?.attachedTo}
{formatName(object.$lookup?.attachedTo?.name)}
{/if}
</div>
<div class="small-text lines-limit-2">{object.$lookup?.attachedTo?.title ?? ''}</div>
</div>
<div class="tool"><ActionIcon label={undefined} icon={IconMoreH} size={'small'} /></div>
</div>
<div class="flex-between">
<div class="flex-row-center">
<div class="sm-tool-icon step-lr75">
<ReviewPresenter value={object} />
</div>
{#if (object.attachments ?? 0) > 0}
<div class="step-lr75"><AttachmentsPresenter value={object} /></div>
{/if}
{#if (object.comments ?? 0) > 0}
<div class="step-lr75"><CommentsPresenter value={object} /></div>
{/if}
</div>
<Avatar size={'x-small'} />
</div>
</div>
<style lang="scss">
.card-container {
display: flex;
flex-direction: column;
padding: 1rem 1.25rem;
background-color: rgba(222, 222, 240, 0.06);
border-radius: 0.75rem;
user-select: none;
&.draggable {
cursor: grab;
}
}
.tool {
align-self: start;
}
</style>

View File

@ -0,0 +1,55 @@
<!--
// 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 { translate } from '@anticrm/platform'
import { getClient } from '@anticrm/presentation'
import type { Opinion } from '@anticrm/recruit'
import recruit from '@anticrm/recruit'
import { closeTooltip, Icon, showPopup } from '@anticrm/ui'
import EditOpinion from './EditOpinion.svelte'
export let value: Opinion
function show () {
closeTooltip()
showPopup(EditOpinion, { item: value }, element)
}
const client = getClient()
let shortLabel = ''
const label = client.getHierarchy().getClass(value._class).shortLabel
if (label !== undefined) {
translate(label, {}).then(r => {
shortLabel = r
})
}
let element:HTMLElement
</script>
{#if value}
<div class="sm-tool-icon" on:click={show} bind:this={element}>
<span class="icon">
<Icon icon={recruit.icon.Review} size={'small'} />
</span>&nbsp;
{#if value && shortLabel}
{shortLabel}-{value.number}
{/if}
</div>
{/if}

View File

@ -0,0 +1,86 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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 type { Doc, Ref } from '@anticrm/core'
import core from '@anticrm/core'
import { CircleButton, IconAdd, Label, showPopup } from '@anticrm/ui'
import { Table } from '@anticrm/view-resources'
import recruit from '../../plugin'
import FileDuo from '../icons/FileDuo.svelte'
import CreateOpinion from './CreateOpinion.svelte'
export let objectId: Ref<Doc>
export let opinions: number
const createApp = (ev: MouseEvent): void => {
showPopup(CreateOpinion, { review: objectId }, ev.target as HTMLElement)
}
</script>
<div class="applications-container">
<div class="flex-row-center">
<div class="title"><Label label={recruit.string.Opinions} /></div>
<CircleButton icon={IconAdd} size={'small'} selected on:click={createApp} />
</div>
{#if opinions > 0}
<Table
_class={recruit.class.Opinion}
config={['', 'value', 'description', '$lookup.modifiedBy']}
options={{
lookup: {
modifiedBy: core.class.Account
}
}}
query={ { attachedTo: objectId } }
loadingProps={ { length: opinions } }
/>
{:else}
<div class="flex-col-center mt-5 createapp-container">
<FileDuo size={'large'} />
<div class="small-text content-dark-color mt-2">
<Label label={recruit.string.NoReviewForCandidate} />
</div>
<div class="text-sm">
<div class='over-underline' on:click={createApp}>
<Label label={recruit.string.CreateAnReview} />
</div>
</div>
</div>
{/if}
</div>
<style lang="scss">
.applications-container {
display: flex;
flex-direction: column;
.title {
margin-right: .75rem;
font-weight: 500;
font-size: 1.25rem;
color: var(--theme-caption-color);
}
}
.createapp-container {
padding: 1rem;
color: var(--theme-caption-color);
background: var(--theme-bg-accent-color);
border: 1px solid var(--theme-bg-accent-color);
border-radius: .75rem;
}
</style>

View File

@ -0,0 +1,36 @@
<!--
// 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 type { Review } from '@anticrm/recruit'
import recruit from '@anticrm/recruit'
import { Table } from '@anticrm/view-resources'
export let value: Review
</script>
<Table
_class={recruit.class.Opinion}
config={['', 'value', 'description']}
options={
{
}
}
query={ { attachedTo: value._id } }
loadingProps={{ length: value.opinions ?? 0 }}
/>

View File

@ -0,0 +1,32 @@
<!--
// 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 type { Review } from '@anticrm/recruit'
import { Icon, Tooltip } from '@anticrm/ui'
import OpinionsPopup from './OpinionsPopup.svelte'
import recruit from '../../plugin'
export let value: Review
</script>
{#if value.opinions && value.opinions > 0}
<Tooltip label={recruit.string.Opinions} component={OpinionsPopup} props={{ value }}>
<div class="sm-tool-icon">
<span class="icon"><Icon icon={recruit.icon.Opinion} size={'small'} /></span>&nbsp;{value.opinions}
</div>
</Tooltip>
{/if}

View File

@ -0,0 +1,74 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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 Company from '../icons/Company.svelte'
import type { ReviewCategory } from '@anticrm/recruit'
import { closePanel, closePopup, closeTooltip, getCurrentLocation, navigate } from '@anticrm/ui'
export let category: ReviewCategory
</script>
<div class="flex-col h-full card-container">
<div class="label">Review category</div>
<div class="flex-center logo">
<Company size={'large'} />
</div>
{#if category}
<div class="name over-underline" on:click={() => {
closeTooltip()
closePopup()
closePanel()
const loc = getCurrentLocation()
loc.path[2] = category._id
loc.path.length = 3
navigate(loc)
}}>{category.name}</div>
<div class="description">{category.description ?? ''}</div>
{/if}
</div>
<style lang="scss">
.card-container {
padding: 1rem 1.5rem 1.25rem;
background-color: var(--theme-button-bg-enabled);
border: 1px solid var(--theme-bg-accent-color);
border-radius: .75rem;
.logo {
width: 5rem;
height: 5rem;
color: var(--primary-button-color);
background-color: var(--primary-button-enabled);
border-radius: 50%;
}
.label {
margin-bottom: 1.75rem;
font-weight: 500;
font-size: .625rem;
color: var(--theme-content-dark-color);
}
.name {
margin: 1rem 0 .25rem;
font-weight: 500;
font-size: 1rem;
color: var(--theme-caption-color);
}
.description {
font-size: .75rem;
color: var(--theme-content-dark-color);
}
}
</style>

View File

@ -0,0 +1,47 @@
<!--
// 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 { translate } from '@anticrm/platform'
import { getClient } from '@anticrm/presentation'
import type { Review } from '@anticrm/recruit'
import recruit from '@anticrm/recruit'
import { closeTooltip, Icon, showPanel } from '@anticrm/ui'
import view from '@anticrm/view'
export let value: Review
const client = getClient()
let shortLabel = ''
const label = client.getHierarchy().getClass(value._class).shortLabel
if (label !== undefined) {
translate(label, {}).then(r => {
shortLabel = r
})
}
function show () {
closeTooltip()
showPanel(view.component.EditDoc, value._id, value._class, 'full')
}
</script>
{#if value && shortLabel}
<div class="sm-tool-icon" on:click={show}>
<span class="icon"><Icon icon={recruit.icon.Application} size={'small'} /></span>&nbsp;{shortLabel}-{value.number}
</div>
{/if}

View File

@ -0,0 +1,99 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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 type { Doc, Ref } from '@anticrm/core'
import core from '@anticrm/core'
import { IntlString } from '@anticrm/platform'
import task from '@anticrm/task'
import { CircleButton, IconAdd, Label, showPopup } from '@anticrm/ui'
import { Table } from '@anticrm/view-resources'
import recruit from '../../plugin'
import FileDuo from '../icons/FileDuo.svelte'
import CreateReview from './CreateReview.svelte'
export let objectId: Ref<Doc>
export let reviews: number
export let label: IntlString = recruit.string.Reviews
const createApp = (ev: MouseEvent): void => {
showPopup(CreateReview, { candidate: objectId, preserveCandidate: true }, ev.target as HTMLElement)
}
</script>
<div class="applications-container">
<div class="flex-row-center">
<div class="title"><Label {label} /></div>
<CircleButton icon={IconAdd} size={'small'} selected on:click={createApp} />
</div>
{#if reviews > 0}
<Table
_class={recruit.class.Review}
config={[
'',
{ key: '$lookup.space.name', label: recruit.string.ReviewCategoryTitle },
'dueDate',
{
key: '',
presenter: recruit.component.OpinionsPresenter,
label: recruit.string.Opinions,
sortingKey: 'opinions'
},
'$lookup.state',
'$lookup.doneState'
]}
options={{
lookup: {
state: task.class.State,
space: core.class.Space,
doneState: task.class.DoneState
}
}}
query={{ attachedTo: objectId }}
loadingProps={{ length: reviews }}
/>
{:else}
<div class="flex-col-center mt-5 createapp-container">
<FileDuo size={'large'} />
<div class="small-text content-dark-color mt-2">
<Label label={recruit.string.NoReviewForCandidate} />
</div>
<div class="text-sm">
<div class='over-underline' on:click={createApp}><Label label={recruit.string.CreateAnReview} /></div>
</div>
</div>
{/if}
</div>
<style lang="scss">
.applications-container {
display: flex;
flex-direction: column;
.title {
margin-right: 0.75rem;
font-weight: 500;
font-size: 1.25rem;
color: var(--theme-caption-color);
}
}
.createapp-container {
padding: 1rem;
color: var(--theme-caption-color);
background: var(--theme-bg-accent-color);
border: 1px solid var(--theme-bg-accent-color);
border-radius: 0.75rem;
}
</style>

View File

@ -16,7 +16,7 @@
import type { Client, Doc } from '@anticrm/core'
import { IntlString, OK, Resources, Severity, Status, translate } from '@anticrm/platform'
import { ObjectSearchResult } from '@anticrm/presentation'
import { Applicant } from '@anticrm/recruit'
import { Applicant, Review } from '@anticrm/recruit'
import task from '@anticrm/task'
import { showPanel, showPopup } from '@anticrm/ui'
import ApplicationItem from './components/ApplicationItem.svelte'
@ -30,6 +30,17 @@ import CreateVacancy from './components/CreateVacancy.svelte'
import EditApplication from './components/EditApplication.svelte'
import EditVacancy from './components/EditVacancy.svelte'
import KanbanCard from './components/KanbanCard.svelte'
import CreateReview from './components/review/CreateReview.svelte'
import CreateOpinion from './components/review/CreateOpinion.svelte'
import CreateReviewCategory from './components/review/CreateReviewCategory.svelte'
import EditReview from './components/review/EditReview.svelte'
import EditReviewCategory from './components/review/EditReviewCategory.svelte'
import KanbanReviewCard from './components/review/KanbanReviewCard.svelte'
import OpinionPresenter from './components/review/OpinionPresenter.svelte'
import OpinionsPresenter from './components/review/OpinionsPresenter.svelte'
import Opinions from './components/review/Opinions.svelte'
import ReviewPresenter from './components/review/ReviewPresenter.svelte'
import Reviews from './components/review/Reviews.svelte'
import SkillsView from './components/SkillsView.svelte'
import TemplatesIcon from './components/TemplatesIcon.svelte'
import Vacancies from './components/Vacancies.svelte'
@ -46,6 +57,14 @@ async function editVacancy (object: Doc): Promise<void> {
showPanel(recruit.component.EditVacancy, object._id, object._class, 'right')
}
async function createOpinion (object: Doc): Promise<void> {
showPopup(CreateOpinion, { space: object.space, review: object._id })
}
async function createReview (object: Doc): Promise<void> {
showPopup(CreateReview, { application: object._id, preserveApplication: true })
}
export async function applicantValidator (applicant: Applicant, client: Client): Promise<Status> {
if (applicant.attachedTo === undefined) {
return new Status(Severity.INFO, recruit.status.CandidateRequired, {})
@ -63,6 +82,16 @@ export async function applicantValidator (applicant: Applicant, client: Client):
return OK
}
export async function reviewValidator (review: Review, client: Client): Promise<Status> {
if (review.attachedTo === undefined) {
return new Status(Severity.INFO, recruit.status.CandidateRequired, {})
}
if (review.space === undefined) {
return new Status(Severity.INFO, recruit.status.ReviewCategoryRequired, {})
}
return OK
}
export async function queryApplication (client: Client, search: string): Promise<ObjectSearchResult[]> {
const _class = recruit.class.Applicant
const cl = client.getHierarchy().getClass(_class)
@ -72,7 +101,7 @@ export async function queryApplication (client: Client, search: string): Promise
const sequence = (await client.findOne(task.class.Sequence, { attachedTo: _class }))?.sequence ?? 0
const named = new Map((await client.findAll(_class, { $search: search }, { limit: 200 })).map(e => [e._id, e]))
const named = new Map((await client.findAll(_class, { $search: search }, { limit: 200, lookup: { attachedTo: recruit.mixin.Candidate } })).map(e => [e._id, e]))
const nids: number[] = []
if (sequence > 0) {
for (let n = 0; n < sequence; n++) {
@ -81,7 +110,7 @@ export async function queryApplication (client: Client, search: string): Promise
nids.push(n)
}
}
const numbered = await client.findAll<Applicant>(_class, { number: { $in: nids } }, { limit: 200 })
const numbered = await client.findAll<Applicant>(_class, { number: { $in: nids } }, { limit: 200, lookup: { attachedTo: recruit.mixin.Candidate } })
for (const d of numbered) {
if (!named.has(d._id)) {
named.set(d._id, d)
@ -92,7 +121,7 @@ export async function queryApplication (client: Client, search: string): Promise
return Array.from(named.values()).map(e => ({
doc: e,
title: `${shortLabel}-${e.number}`,
icon: task.icon.Task,
icon: recruit.icon.Application,
component: ApplicationItem
}))
}
@ -100,10 +129,13 @@ export async function queryApplication (client: Client, search: string): Promise
export default async (): Promise<Resources> => ({
actionImpl: {
CreateApplication: createApplication,
EditVacancy: editVacancy
EditVacancy: editVacancy,
CreateReview: createReview,
CreateOpinion: createOpinion
},
validator: {
ApplicantValidator: applicantValidator
ApplicantValidator: applicantValidator,
ReviewValidator: reviewValidator
},
component: {
CreateVacancy,
@ -121,7 +153,18 @@ export default async (): Promise<Resources> => ({
SkillsView,
Vacancies,
VacancyItemPresenter,
VacancyCountPresenter
VacancyCountPresenter,
CreateReviewCategory,
EditReviewCategory,
CreateReview,
ReviewPresenter,
EditReview,
KanbanReviewCard,
Reviews,
Opinions,
OpinionPresenter,
OpinionsPresenter
},
completion: {
ApplicationQuery: async (client: Client, query: string) => await queryApplication(client, query)

View File

@ -24,7 +24,8 @@ export default mergeIds(recruitId, recruit, {
status: {
ApplicationExists: '' as StatusCode,
CandidateRequired: '' as StatusCode,
VacancyRequired: '' as StatusCode
VacancyRequired: '' as StatusCode,
ReviewCategoryRequired: '' as StatusCode
},
string: {
CreateVacancy: '' as IntlString,
@ -51,6 +52,7 @@ export default mergeIds(recruitId, recruit, {
Applications: '' as IntlString,
ThisVacancyIsPrivate: '' as IntlString,
Description: '' as IntlString,
Verdict: '' as IntlString,
Company: '' as IntlString,
Edit: '' as IntlString,
Delete: '' as IntlString,
@ -68,7 +70,32 @@ export default mergeIds(recruitId, recruit, {
PersonLastNamePlaceholder: '' as IntlString,
Location: '' as IntlString,
Title: '' as IntlString,
Vacancies: '' as IntlString
Vacancies: '' as IntlString,
Review: '' as IntlString,
ReviewCategory: '' as IntlString,
CreateReviewCategory: '' as IntlString,
ReviewCategoryName: '' as IntlString,
ReviewCategoryTitle: '' as IntlString,
ReviewCategoryPlaceholder: '' as IntlString,
ReviewCategoryDescription: '' as IntlString,
ThisReviewCategoryIsPrivate: '' as IntlString,
CreateReview: '' as IntlString,
SelectReviewCategory: '' as IntlString,
Reviews: '' as IntlString,
NoReviewForCandidate: '' as IntlString,
CreateAnReview: '' as IntlString,
CreateOpinion: '' as IntlString,
Opinion: '' as IntlString,
OpinionValue: '' as IntlString,
OpinionValuePlaceholder: '' as IntlString,
OpinionSave: '' as IntlString,
Opinions: '' as IntlString,
OpinionShortLabel: '' as IntlString,
ReviewShortLabel: '' as IntlString,
StartDate: '' as IntlString,
DueDate: '' as IntlString,
CandidateReviews: '' as IntlString
},
space: {
CandidatesPublic: '' as Ref<Space>
@ -79,6 +106,7 @@ export default mergeIds(recruitId, recruit, {
},
component: {
VacancyItemPresenter: '' as AnyComponent,
VacancyCountPresenter: '' as AnyComponent
VacancyCountPresenter: '' as AnyComponent,
OpinionsPresenter: '' as AnyComponent
}
})

View File

@ -14,7 +14,7 @@
//
import type { Person } from '@anticrm/contact'
import type { Class, Doc, Mixin, Ref, Space, Timestamp } from '@anticrm/core'
import type { AttachedDoc, Class, Doc, Mixin, Ref, Space, Timestamp } from '@anticrm/core'
import type { Asset, Plugin } from '@anticrm/platform'
import { plugin } from '@anticrm/platform'
import type { KanbanTemplateSpace, SpaceWithStates, Task } from '@anticrm/task'
@ -31,6 +31,14 @@ export interface Vacancy extends SpaceWithStates {
company?: string
}
/**
* @public
*/
export interface ReviewCategory extends SpaceWithStates {
fullDescription?: string
attachments?: number
}
/**
* @public
*/
@ -46,16 +54,49 @@ export interface Candidate extends Person {
remote?: boolean
source?: string
skills?: number
reviews?: number
}
/**
* @public
*/
export interface Applicant extends Task {
attachedTo: Ref<Candidate>
attachments?: number
comments?: number
}
/**
* @public
*/
export interface Review extends Task {
attachedTo: Ref<Candidate>
attachments?: number
comments?: number
description: string
verdict: string
location?: string
company?: string
startDate: Timestamp | null
dueDate: Timestamp | null
opinions?: number
}
/**
* @public
*/
export interface Opinion extends AttachedDoc {
number: number
attachedTo: Ref<Review>
comments?: number
attachments?: number
description: string
value: string
}
/**
* @public
*/
@ -71,7 +112,10 @@ const recruit = plugin(recruitId, {
class: {
Applicant: '' as Ref<Class<Applicant>>,
Candidates: '' as Ref<Class<Candidates>>,
Vacancy: '' as Ref<Class<Vacancy>>
Vacancy: '' as Ref<Class<Vacancy>>,
ReviewCategory: '' as Ref<Class<ReviewCategory>>,
Review: '' as Ref<Class<Review>>,
Opinion: '' as Ref<Class<Opinion>>
},
mixin: {
Candidate: '' as Ref<Mixin<Candidate>>
@ -85,10 +129,13 @@ const recruit = plugin(recruitId, {
Location: '' as Asset,
Calendar: '' as Asset,
Create: '' as Asset,
Application: '' as Asset
Application: '' as Asset,
Review: '' as Asset,
Opinion: '' as Asset
},
space: {
VacancyTemplates: '' as Ref<KanbanTemplateSpace>
VacancyTemplates: '' as Ref<KanbanTemplateSpace>,
ReviewTemplates: '' as Ref<KanbanTemplateSpace>
}
})

View File

@ -60,7 +60,7 @@
<StyledTextBox
bind:content={object.description}
placeholder={plugin.string.TaskDescriptionPlaceholder}
on:blur={() => change('description', object.description)}
on:value={() => change('description', object.description)}
/>
</div>
</Grid>

View File

@ -20,4 +20,7 @@ import { MessageViewer } from '@anticrm/presentation'
export let value: string
</script>
<MessageViewer message={value}/>
<span class="lines-limit-2">
<MessageViewer message={value}/>
</span>