diff --git a/models/contact/src/index.ts b/models/contact/src/index.ts index f2ca32cb68..bff3f3c010 100644 --- a/models/contact/src/index.ts +++ b/models/contact/src/index.ts @@ -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 }) diff --git a/models/contact/src/plugin.ts b/models/contact/src/plugin.ts index 21ab4a373a..6a2448378f 100644 --- a/models/contact/src/plugin.ts +++ b/models/contact/src/plugin.ts @@ -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, diff --git a/models/recruit/src/creation.ts b/models/recruit/src/creation.ts index 1c2497a962..e5ec9e4d00 100644 --- a/models/recruit/src/creation.ts +++ b/models/recruit/src/creation.ts @@ -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 { 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>): Promise { + 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 { + 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 as Ref, + 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 as Ref, + 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' } + ] + }) + } +} diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index a26418f384..f8cbb24099 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -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' diff --git a/models/recruit/src/migration.ts b/models/recruit/src/migration.ts index d680ddae1e..cd9218c290 100644 --- a/models/recruit/src/migration.ts +++ b/models/recruit/src/migration.ts @@ -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 | undefined): MixinData { 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 { diff --git a/models/recruit/src/plugin.ts b/models/recruit/src/plugin.ts index ee09e47ffc..43cb7e16b3 100644 --- a/models/recruit/src/plugin.ts +++ b/models/recruit/src/plugin.ts @@ -26,11 +26,15 @@ import { ObjectSearchFactory, ObjectSearchCategory } from '@anticrm/model-presen export default mergeIds(recruitId, recruit, { action: { CreateApplication: '' as Ref, - EditVacancy: '' as Ref + EditVacancy: '' as Ref, + CreateReview: '' as Ref, + CreateOpinion: '' as Ref }, actionImpl: { CreateApplication: '' as Resource<(object: Doc) => Promise>, - EditVacancy: '' as Resource<(object: Doc) => Promise> + EditVacancy: '' as Resource<(object: Doc) => Promise>, + CreateReview: '' as Resource<(object: Doc) => Promise>, + CreateOpinion: '' as Resource<(object: Doc) => Promise> }, string: { ApplicationShort: '' as IntlString, @@ -47,7 +51,8 @@ export default mergeIds(recruitId, recruit, { EditVacancy: '' as IntlString }, validator: { - ApplicantValidator: '' as Resource<(doc: T, client: Client) => Promise> + ApplicantValidator: '' as Resource<(doc: T, client: Client) => Promise>, + ReviewValidator: '' as Resource<(doc: T, client: Client) => Promise> }, 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 + DefaultVacancy: '' as Ref, + + Interview: '' as Ref, + Task: '' as Ref }, completion: { ApplicationQuery: '' as Resource, diff --git a/models/recruit/src/review-model.ts b/models/recruit/src/review-model.ts new file mode 100644 index 0000000000..b80a6e8823 --- /dev/null +++ b/models/recruit/src/review-model.ts @@ -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 + + @Prop(TypeRef(contact.class.Employee), recruit.string.AssignedRecruiter) + declare assignee: Ref | 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 + + @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 +} diff --git a/models/recruit/src/review.ts b/models/recruit/src/review.ts new file mode 100644 index 0000000000..6681eff9ee --- /dev/null +++ b/models/recruit/src/review.ts @@ -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, + 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, + 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, + 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 + }) +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index e3402e4e5a..03578dec8b 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -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' diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index 5983defed2..13d838963b 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -30,6 +30,15 @@ export type AnySvelteComponent = typeof SvelteComponent export type Component = Resource export type AnyComponent = Resource +/** + * Allow to pass component with some predefined properties. + * @public + */ +export interface AnySvelteComponentWithProps { + component: AnySvelteComponent + props?: Record +} + export interface Action { label: IntlString icon: Asset | AnySvelteComponent diff --git a/plugins/activity-resources/src/components/TxViewTx.svelte b/plugins/activity-resources/src/components/TxViewTx.svelte index cb4d26076e..f608b118e3 100644 --- a/plugins/activity-resources/src/components/TxViewTx.svelte +++ b/plugins/activity-resources/src/components/TxViewTx.svelte @@ -22,21 +22,21 @@ {#if i === 0}
{/if} - {#if typeof viewlet?.component === 'string'} - - {:else} - - {/if} + {#if typeof viewlet?.component === 'string'} + + {:else} + + {/if} {/each} {#each filterTx([tx, ...tx.txes], core.class.TxRemoveDoc) as ctx, i} {#if i === 0}
{/if} - {#if typeof viewlet?.component === 'string'} - - {:else} - - {/if} + {#if typeof viewlet?.component === 'string'} + + {:else} + + {/if} {/each} diff --git a/plugins/contact-resources/src/components/EmployeeAccountPresenter.svelte b/plugins/contact-resources/src/components/EmployeeAccountPresenter.svelte new file mode 100644 index 0000000000..c69a48d9cf --- /dev/null +++ b/plugins/contact-resources/src/components/EmployeeAccountPresenter.svelte @@ -0,0 +1,69 @@ + + + +{#if value} +
+ {#if employee} + +
{formatName(employee.name)}
+ {:else} + {JSON.stringify(value)} +
{value.email}
+ {/if} +
+{/if} + + diff --git a/plugins/contact-resources/src/index.ts b/plugins/contact-resources/src/index.ts index e887df8e89..5d19c2c90b 100644 --- a/plugins/contact-resources/src/index.ts +++ b/plugins/contact-resources/src/index.ts @@ -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 => ({ CreatePersons, CreateOrganizations, SocialEditor, - Contacts + Contacts, + EmployeeAccountPresenter }, completion: { EmployeeQuery: async (client: Client, query: string) => await queryContact(contact.class.Employee, client, query), diff --git a/plugins/recruit-assets/lang/en.json b/plugins/recruit-assets/lang/en.json index 49ebda211d..46351fbbf3 100644 --- a/plugins/recruit-assets/lang/en.json +++ b/plugins/recruit-assets/lang/en.json @@ -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" } } \ No newline at end of file diff --git a/plugins/recruit-assets/lang/ru.json b/plugins/recruit-assets/lang/ru.json index 5452a3f603..8c0aeded45 100644 --- a/plugins/recruit-assets/lang/ru.json +++ b/plugins/recruit-assets/lang/ru.json @@ -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": "Пожалуйста выберети кадегорию" } } \ No newline at end of file diff --git a/plugins/recruit-assets/src/index.ts b/plugins/recruit-assets/src/index.ts index dabcbb0f91..3582d13618 100644 --- a/plugins/recruit-assets/src/index.ts +++ b/plugins/recruit-assets/src/index.ts @@ -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`)) diff --git a/plugins/recruit-resources/src/components/ApplicationTitle.svelte b/plugins/recruit-resources/src/components/ApplicationTitle.svelte new file mode 100644 index 0000000000..0c45a4f5ba --- /dev/null +++ b/plugins/recruit-resources/src/components/ApplicationTitle.svelte @@ -0,0 +1,28 @@ + + + +{#if value && shortLabel} + {shortLabel}-{value.number} +{/if} diff --git a/plugins/recruit-resources/src/components/EditApplication.svelte b/plugins/recruit-resources/src/components/EditApplication.svelte index e12fdf56f1..9c5a67815b 100644 --- a/plugins/recruit-resources/src/components/EditApplication.svelte +++ b/plugins/recruit-resources/src/components/EditApplication.svelte @@ -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 @@
+
+ +
{/if} diff --git a/plugins/recruit-resources/src/components/review/CreateReview.svelte b/plugins/recruit-resources/src/components/review/CreateReview.svelte new file mode 100644 index 0000000000..0df60080ea --- /dev/null +++ b/plugins/recruit-resources/src/components/review/CreateReview.svelte @@ -0,0 +1,161 @@ + + + + { + dispatch('close') + }} +> + + + {#if !preserveCandidate} + + {/if} + + + + + diff --git a/plugins/recruit-resources/src/components/review/CreateReviewCategory.svelte b/plugins/recruit-resources/src/components/review/CreateReviewCategory.svelte new file mode 100644 index 0000000000..3db785d210 --- /dev/null +++ b/plugins/recruit-resources/src/components/review/CreateReviewCategory.svelte @@ -0,0 +1,72 @@ + + + + + { dispatch('close') }} +> + + + + + { + templateId = evt.detail + }}/> + + diff --git a/plugins/recruit-resources/src/components/review/EditOpinion.svelte b/plugins/recruit-resources/src/components/review/EditOpinion.svelte new file mode 100644 index 0000000000..9261564af4 --- /dev/null +++ b/plugins/recruit-resources/src/components/review/EditOpinion.svelte @@ -0,0 +1,95 @@ + + + + 0} + space={item.space} + on:close={() => { + dispatch('close') + }} + okLabel={recruit.string.OpinionSave}> + + +
+
+
+ +
+
+
+ + \ No newline at end of file diff --git a/plugins/recruit-resources/src/components/review/EditReview.svelte b/plugins/recruit-resources/src/components/review/EditReview.svelte new file mode 100644 index 0000000000..437259b867 --- /dev/null +++ b/plugins/recruit-resources/src/components/review/EditReview.svelte @@ -0,0 +1,126 @@ + + + +{#if object !== undefined && candidate !== undefined} +
+
+
+
+
+ +
+ + client.update(object, { company: object.company })} + /> + client.update(object, { location: object.location })} + /> + +
+ +
+
+
+ { + console.log(evt.detail) + client.update(object, { description: evt.detail }) + }} + /> +
+ +
+
+
+ { + client.update(object, { verdict: evt.detail }) + }} + /> +
+{/if} + + diff --git a/plugins/recruit-resources/src/components/review/EditReviewCategory.svelte b/plugins/recruit-resources/src/components/review/EditReviewCategory.svelte new file mode 100644 index 0000000000..99d8e27782 --- /dev/null +++ b/plugins/recruit-resources/src/components/review/EditReviewCategory.svelte @@ -0,0 +1,217 @@ + + + + +
{ dispatch('close') }}/> +
+ {#if object} +
+
+
+
+ {#if clazz.icon}{/if} +
+
+ {object.name} +
+
+
{object.description}
+
+
{ dispatch('close') }}>
+
+
+ +
+
+ {#each tabs as tab, i} +
{ selected = i }}> +
+ {/each} +
+
+
+
+ {#if selected === 0} + + { onChange('name', object.name) }}/> + { onChange('description', object.description) }}/> + +
+ Description +
+ { onChange('fullDescription', object.fullDescription) }} /> +
+
+
+ +
+ {:else if selected === 1} + + {:else if selected === 2} + + {/if} +
+
+ {/if} +
+ + \ No newline at end of file diff --git a/plugins/recruit-resources/src/components/review/KanbanReviewCard.svelte b/plugins/recruit-resources/src/components/review/KanbanReviewCard.svelte new file mode 100644 index 0000000000..19222fe89a --- /dev/null +++ b/plugins/recruit-resources/src/components/review/KanbanReviewCard.svelte @@ -0,0 +1,79 @@ + + + +
+
+ +
+
+ {#if object.$lookup?.attachedTo} + {formatName(object.$lookup?.attachedTo?.name)} + {/if} +
+
{object.$lookup?.attachedTo?.title ?? ''}
+
+
+
+
+
+
+ +
+ {#if (object.attachments ?? 0) > 0} +
+ {/if} + {#if (object.comments ?? 0) > 0} +
+ {/if} +
+ +
+
+ + diff --git a/plugins/recruit-resources/src/components/review/OpinionPresenter.svelte b/plugins/recruit-resources/src/components/review/OpinionPresenter.svelte new file mode 100644 index 0000000000..15002934e5 --- /dev/null +++ b/plugins/recruit-resources/src/components/review/OpinionPresenter.svelte @@ -0,0 +1,55 @@ + + + +{#if value} +
+ + +   + {#if value && shortLabel} + {shortLabel}-{value.number} + {/if} +
+{/if} diff --git a/plugins/recruit-resources/src/components/review/Opinions.svelte b/plugins/recruit-resources/src/components/review/Opinions.svelte new file mode 100644 index 0000000000..960830b4d7 --- /dev/null +++ b/plugins/recruit-resources/src/components/review/Opinions.svelte @@ -0,0 +1,86 @@ + + + + +
+
+
+ +
+ {#if opinions > 0} + + {:else} +
+ +
+
+
+
+
+
+
+ {/if} + + + diff --git a/plugins/recruit-resources/src/components/review/OpinionsPopup.svelte b/plugins/recruit-resources/src/components/review/OpinionsPopup.svelte new file mode 100644 index 0000000000..dd0d0cff2d --- /dev/null +++ b/plugins/recruit-resources/src/components/review/OpinionsPopup.svelte @@ -0,0 +1,36 @@ + + + + +
diff --git a/plugins/recruit-resources/src/components/review/OpinionsPresenter.svelte b/plugins/recruit-resources/src/components/review/OpinionsPresenter.svelte new file mode 100644 index 0000000000..8f7f828423 --- /dev/null +++ b/plugins/recruit-resources/src/components/review/OpinionsPresenter.svelte @@ -0,0 +1,32 @@ + + + + +{#if value.opinions && value.opinions > 0} + +
+  {value.opinions} +
+
+{/if} diff --git a/plugins/recruit-resources/src/components/review/ReviewCategoryCard.svelte b/plugins/recruit-resources/src/components/review/ReviewCategoryCard.svelte new file mode 100644 index 0000000000..e75c6f7f90 --- /dev/null +++ b/plugins/recruit-resources/src/components/review/ReviewCategoryCard.svelte @@ -0,0 +1,74 @@ + + + + +
+
Review category
+ + {#if category} +
{ + closeTooltip() + closePopup() + closePanel() + const loc = getCurrentLocation() + loc.path[2] = category._id + loc.path.length = 3 + navigate(loc) + }}>{category.name}
+
{category.description ?? ''}
+ {/if} +
+ + \ No newline at end of file diff --git a/plugins/recruit-resources/src/components/review/ReviewPresenter.svelte b/plugins/recruit-resources/src/components/review/ReviewPresenter.svelte new file mode 100644 index 0000000000..9b52102c9f --- /dev/null +++ b/plugins/recruit-resources/src/components/review/ReviewPresenter.svelte @@ -0,0 +1,47 @@ + + + +{#if value && shortLabel} +
+  {shortLabel}-{value.number} +
+{/if} diff --git a/plugins/recruit-resources/src/components/review/Reviews.svelte b/plugins/recruit-resources/src/components/review/Reviews.svelte new file mode 100644 index 0000000000..31907f8f17 --- /dev/null +++ b/plugins/recruit-resources/src/components/review/Reviews.svelte @@ -0,0 +1,99 @@ + + + +
+
+
+ +
+ {#if reviews > 0} +
+ {:else} +
+ +
+
+
+
+
+
+ {/if} + + + diff --git a/plugins/recruit-resources/src/index.ts b/plugins/recruit-resources/src/index.ts index 091bc6eaa1..1c1a5a011d 100644 --- a/plugins/recruit-resources/src/index.ts +++ b/plugins/recruit-resources/src/index.ts @@ -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 { showPanel(recruit.component.EditVacancy, object._id, object._class, 'right') } +async function createOpinion (object: Doc): Promise { + showPopup(CreateOpinion, { space: object.space, review: object._id }) +} + +async function createReview (object: Doc): Promise { + showPopup(CreateReview, { application: object._id, preserveApplication: true }) +} + export async function applicantValidator (applicant: Applicant, client: Client): Promise { 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 { + 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 { 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(_class, { number: { $in: nids } }, { limit: 200 }) + const numbered = await client.findAll(_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 => ({ 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 => ({ 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) diff --git a/plugins/recruit-resources/src/plugin.ts b/plugins/recruit-resources/src/plugin.ts index ab1492f8b1..1bff88a1a8 100644 --- a/plugins/recruit-resources/src/plugin.ts +++ b/plugins/recruit-resources/src/plugin.ts @@ -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 @@ -79,6 +106,7 @@ export default mergeIds(recruitId, recruit, { }, component: { VacancyItemPresenter: '' as AnyComponent, - VacancyCountPresenter: '' as AnyComponent + VacancyCountPresenter: '' as AnyComponent, + OpinionsPresenter: '' as AnyComponent } }) diff --git a/plugins/recruit/src/index.ts b/plugins/recruit/src/index.ts index 0740c563c4..eb23df33cd 100644 --- a/plugins/recruit/src/index.ts +++ b/plugins/recruit/src/index.ts @@ -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 attachments?: number comments?: number } +/** + * @public + */ +export interface Review extends Task { + attachedTo: Ref + 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 + comments?: number + attachments?: number + description: string + value: string +} + /** * @public */ @@ -71,7 +112,10 @@ const recruit = plugin(recruitId, { class: { Applicant: '' as Ref>, Candidates: '' as Ref>, - Vacancy: '' as Ref> + Vacancy: '' as Ref>, + ReviewCategory: '' as Ref>, + Review: '' as Ref>, + Opinion: '' as Ref> }, mixin: { Candidate: '' as Ref> @@ -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 + VacancyTemplates: '' as Ref, + ReviewTemplates: '' as Ref } }) diff --git a/plugins/task-resources/src/components/EditIssue.svelte b/plugins/task-resources/src/components/EditIssue.svelte index 70f10ba1d3..9f2515b862 100644 --- a/plugins/task-resources/src/components/EditIssue.svelte +++ b/plugins/task-resources/src/components/EditIssue.svelte @@ -60,7 +60,7 @@ change('description', object.description)} + on:value={() => change('description', object.description)} /> diff --git a/plugins/view-resources/src/components/HTMLPresenter.svelte b/plugins/view-resources/src/components/HTMLPresenter.svelte index c5b0ece5ef..1d552a4201 100644 --- a/plugins/view-resources/src/components/HTMLPresenter.svelte +++ b/plugins/view-resources/src/components/HTMLPresenter.svelte @@ -20,4 +20,7 @@ import { MessageViewer } from '@anticrm/presentation' export let value: string - + + + +