mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-31 20:57:31 +00:00
Support of Review category (#1108)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
4597788ec3
commit
fc6517b894
@ -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
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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' }
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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> {
|
||||
|
@ -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>,
|
||||
|
82
models/recruit/src/review-model.ts
Normal file
82
models/recruit/src/review-model.ts
Normal 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
|
||||
}
|
179
models/recruit/src/review.ts
Normal file
179
models/recruit/src/review.ts
Normal 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
|
||||
})
|
||||
}
|
@ -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'
|
||||
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
@ -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),
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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": "Пожалуйста выберети кадегорию"
|
||||
}
|
||||
}
|
@ -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`))
|
||||
|
@ -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}
|
@ -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">
|
||||
|
25
plugins/recruit-resources/src/components/icons/Review.svelte
Normal file
25
plugins/recruit-resources/src/components/icons/Review.svelte
Normal 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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
{#if value && shortLabel}
|
||||
{shortLabel}-{value.number}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
@ -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>
|
@ -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 }}
|
||||
/>
|
@ -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> {value.opinions}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
@ -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>
|
@ -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> {shortLabel}-{value.number}
|
||||
</div>
|
||||
{/if}
|
@ -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>
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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>
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user