Leaves schedule (#2135)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2022-06-24 10:21:13 +06:00 committed by GitHub
parent dbca41afee
commit 3b8690248b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 960 additions and 40 deletions

View File

@ -11,6 +11,10 @@ Chunter:
- Reactions on messages
HR:
- Leaves schedule
## 0.6.28
Core:

View File

@ -51,7 +51,6 @@ export default mergeIds(contactId, contact, {
Location: '' as IntlString,
Channel: '' as IntlString,
ChannelProvider: '' as IntlString,
Employee: '' as IntlString,
Value: '' as IntlString,
Phone: '' as IntlString,
PhonePlaceholder: '' as IntlString,

View File

@ -35,6 +35,7 @@
"@anticrm/model-workbench": "~0.6.1",
"@anticrm/model-contact": "~0.6.1",
"@anticrm/model-chunter": "~0.6.0",
"@anticrm/model-calendar": "~0.6.0",
"@anticrm/model-attachment": "~0.6.0",
"@anticrm/hr": "~0.6.0",
"@anticrm/hr-resources": "~0.6.0",

View File

@ -14,16 +14,35 @@
//
import { Employee } from '@anticrm/contact'
import contact, { TEmployee, TEmployeeAccount } from '@anticrm/model-contact'
import { Arr, IndexKind, Ref } from '@anticrm/core'
import type { Department, DepartmentMember, Staff } from '@anticrm/hr'
import { Builder, Index, Mixin, Model, Prop, TypeRef, Collection, TypeString, UX, ArrOf } from '@anticrm/model'
import core, { TSpace } from '@anticrm/model-core'
import workbench from '@anticrm/model-workbench'
import hr from './plugin'
import view, { createAction } from '@anticrm/model-view'
import { Arr, Class, Domain, DOMAIN_MODEL, IndexKind, Markup, Ref, Timestamp } from '@anticrm/core'
import type { Department, DepartmentMember, Request, RequestType, Staff } from '@anticrm/hr'
import {
ArrOf,
Builder,
Collection,
Hidden,
Index,
Mixin,
Model,
Prop,
TypeDate,
TypeIntlString,
TypeMarkup,
TypeRef,
TypeString,
UX
} from '@anticrm/model'
import attachment from '@anticrm/model-attachment'
import calendar from '@anticrm/model-calendar'
import chunter from '@anticrm/model-chunter'
import contact, { TEmployee, TEmployeeAccount } from '@anticrm/model-contact'
import core, { TAttachedDoc, TDoc, TSpace } from '@anticrm/model-core'
import view, { createAction } from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench'
import { Asset, IntlString } from '@anticrm/platform'
import hr from './plugin'
export const DOMAIN_HR = 'hr' as Domain
@Model(hr.class.Department, core.class.Space)
@UX(hr.string.Department, hr.icon.Department)
@ -64,8 +83,51 @@ export class TStaff extends TEmployee implements Staff {
department!: Ref<Department>
}
@Model(hr.class.RequestType, core.class.Doc, DOMAIN_MODEL)
@UX(hr.string.RequestType)
export class TRequestType extends TDoc implements RequestType {
@Prop(TypeIntlString(), core.string.Name)
label!: IntlString
icon!: Asset
value!: number
color!: number
}
@Model(hr.class.Request, core.class.AttachedDoc, DOMAIN_HR)
@UX(hr.string.Request, hr.icon.PTO)
export class TRequest extends TAttachedDoc implements Request {
@Prop(TypeRef(hr.mixin.Staff), contact.string.Employee)
declare attachedTo: Ref<Staff>
declare attachedToClass: Ref<Class<Staff>>
@Prop(TypeRef(hr.class.Department), hr.string.Department)
declare space: Ref<Department>
@Prop(TypeRef(hr.class.RequestType), hr.string.RequestType)
@Hidden()
type!: Ref<RequestType>
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
comments?: number
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, undefined, attachment.string.Files)
attachments?: number
@Prop(TypeMarkup(), core.string.Description)
@Index(IndexKind.FullText)
description!: Markup
@Prop(TypeDate(false), calendar.string.Date)
date!: Timestamp
@Prop(TypeDate(false), calendar.string.DueTo)
dueDate!: Timestamp
}
export function createModel (builder: Builder): void {
builder.createModel(TDepartment, TDepartmentMember, TStaff)
builder.createModel(TDepartment, TDepartmentMember, TRequest, TRequestType, TStaff)
builder.createDoc(
workbench.class.Application,
@ -82,6 +144,13 @@ export function createModel (builder: Builder): void {
icon: hr.icon.Structure,
label: hr.string.Structure,
position: 'top'
},
{
id: 'schedule',
component: hr.component.Schedule,
icon: calendar.icon.Calendar,
label: hr.string.Schedule,
position: 'top'
}
],
spaces: []
@ -98,17 +167,103 @@ export function createModel (builder: Builder): void {
editor: hr.component.EditDepartment
})
builder.mixin(hr.class.Request, core.class.Class, view.mixin.ObjectEditor, {
editor: hr.component.EditRequest
})
builder.mixin(hr.class.DepartmentMember, core.class.Class, view.mixin.ArrayEditor, {
editor: hr.component.DepartmentStaff
})
builder.createDoc(
hr.class.RequestType,
core.space.Model,
{
label: hr.string.Vacation,
icon: hr.icon.Vacation,
color: 2,
value: 0
},
hr.ids.Vacation
)
builder.createDoc(
hr.class.RequestType,
core.space.Model,
{
label: hr.string.Sick,
icon: hr.icon.Sick,
color: 11,
value: -1
},
hr.ids.Sick
)
builder.createDoc(
hr.class.RequestType,
core.space.Model,
{
label: hr.string.PTO,
icon: hr.icon.PTO,
color: 9,
value: -1
},
hr.ids.PTO
)
builder.createDoc(
hr.class.RequestType,
core.space.Model,
{
label: hr.string.PTO2,
icon: hr.icon.PTO,
color: 9,
value: -0.5
},
hr.ids.PTO2
)
builder.createDoc(
hr.class.RequestType,
core.space.Model,
{
label: hr.string.Overtime,
icon: hr.icon.Overtime,
color: 5,
value: 1
},
hr.ids.Overtime
)
builder.createDoc(
hr.class.RequestType,
core.space.Model,
{
label: hr.string.Overtime2,
icon: hr.icon.Overtime,
color: 5,
value: 0.5
},
hr.ids.Overtime2
)
builder.createDoc(
hr.class.RequestType,
core.space.Model,
{
label: hr.string.Remote,
icon: hr.icon.Remote,
color: 4,
value: 0
},
hr.ids.Remote
)
createAction(
builder,
{
action: view.actionImpl.ShowPanel,
actionProps: {
component: hr.component.EditDepartment
},
actionProps: {},
label: view.string.Open,
icon: view.icon.Open,
keyBinding: ['e'],
@ -138,6 +293,22 @@ export function createModel (builder: Builder): void {
},
hr.action.DeleteDepartment
)
createAction(
builder,
{
action: view.actionImpl.ShowPanel,
actionProps: {},
label: view.string.Open,
icon: view.icon.Open,
keyBinding: ['e'],
input: 'any',
category: hr.category.HR,
target: hr.class.Request,
context: { mode: 'context', application: hr.app.HR, group: 'top' }
},
hr.action.EditRequest
)
}
export { hrOperation } from './migration'

View File

@ -23,19 +23,31 @@ import { Action, ActionCategory } from '@anticrm/view'
export default mergeIds(hrId, hr, {
string: {
HRApplication: '' as IntlString,
Departments: '' as IntlString
Departments: '' as IntlString,
Request: '' as IntlString,
Vacation: '' as IntlString,
Sick: '' as IntlString,
PTO: '' as IntlString,
PTO2: '' as IntlString,
Remote: '' as IntlString,
Overtime: '' as IntlString,
Overtime2: '' as IntlString
},
component: {
Structure: '' as AnyComponent,
EditDepartment: '' as AnyComponent,
DepartmentStaff: '' as AnyComponent,
DepartmentEditor: '' as AnyComponent
DepartmentEditor: '' as AnyComponent,
Schedule: '' as AnyComponent,
EditRequest: '' as AnyComponent
},
category: {
HR: '' as Ref<ActionCategory>
},
action: {
EditDepartment: '' as Ref<Action>,
DeleteDepartment: '' as Ref<Action>
DeleteDepartment: '' as Ref<Action>,
EditRequest: '' as Ref<Action>,
DeleteRequest: '' as Ref<Action>
}
})

View File

@ -133,7 +133,7 @@ export default plugin(coreId, {
Array: '' as IntlString,
Bag: '' as IntlString,
Name: '' as IntlString,
Description: '' as IntlString,
Enum: '' as IntlString
Enum: '' as IntlString,
Description: '' as IntlString
}
})

View File

@ -213,6 +213,7 @@ const contactPlugin = plugin(contactId, {
string: {
PersonAlreadyExists: '' as IntlString,
Person: '' as IntlString,
Employee: '' as IntlString,
CreateOrganization: '' as IntlString
},
viewlet: {

View File

@ -10,4 +10,23 @@
<path d="M12,10.8c2.6,0,4.8-2.1,4.8-4.8S14.6,1.2,12,1.2C9.4,1.2,7.2,3.4,7.2,6S9.4,10.8,12,10.8z M12,2.8 c1.8,0,3.2,1.5,3.2,3.2S13.8,9.2,12,9.2S8.8,7.8,8.8,6S10.2,2.8,12,2.8z" />
<path d="M12,12.2c-5.4,0-9.8,4.4-9.8,9.8c0,0.4,0.3,0.8,0.8,0.8s0.8-0.3,0.8-0.8c0-4.2,3.1-7.7,7.2-8.2l-1.7,5.5 c-0.1,0.2,0,0.5,0.1,0.7l2,2.5c0.1,0.2,0.4,0.3,0.6,0.3s0.4-0.1,0.6-0.3l2-2.5c0.2-0.2,0.2-0.5,0.1-0.7L13,13.8 c4.1,0.5,7.2,4,7.2,8.2c0,0.4,0.3,0.8,0.8,0.8s0.8-0.3,0.8-0.8C21.8,16.6,17.4,12.2,12,12.2z M12,20.8l-1.2-1.5l1.2-3.8l1.2,3.8 L12,20.8z" />
</symbol>
<symbol id="vacation" viewBox="0 0 34 34">
<path d="M20.431,30.09c0.555-2.625,1.982-11.16-1.273-17.413c0,0,6.072,0.623,5.762,8.408c0,0,5.295-10.588-3.581-12.612 c0,0,2.803-6.696,9.653-4.049c0,0-5.513-6.188-13.417,1.082C16.751,3.23,17.289,0,17.289,0c-3.264,1.591-3.846,4.54-3.817,6.557 c-2.88-0.541-7.92,0.586-9.382,5.806c0,0,5.5-3.833,9.833-1.5c0,0-7.689,2.126-8.001,9.443c0,0,3.205-3.939,6.51-4.904 c-0.268,0.256-0.437,0.613-0.437,1.013c0,0.774,0.628,1.401,1.401,1.401c0.281,0,0.542-0.084,0.761-0.227 c-0.182,0.236-0.294,0.527-0.294,0.85c0,0.773,0.628,1.402,1.401,1.402c0.674,0,1.235-0.478,1.37-1.109 c0.408,2.896,0.452,7.51-2.268,11.199c-1.687-0.976-2.462-2.771-2.796-3.969c0.16,0.223,0.46,0.312,0.722,0.196 c0.302-0.131,0.438-0.481,0.307-0.783c-0.055-0.125-0.147-0.221-0.259-0.28c0.109,0.02,0.226,0.006,0.335-0.041 c0.302-0.133,0.438-0.482,0.307-0.785c-0.067-0.154-0.194-0.267-0.342-0.318c1.451-0.188,3.37,0.801,3.37,0.801 c-1.367-2.797-4.725-2.314-4.725-2.314c1.29-1.646,4.085-1.09,4.085-1.09c-1.459-1.785-3.613-1.365-4.643-0.664 c-0.333-0.789-1.062-1.838-2.604-1.901c0,0,0.76,1.166,0.826,2.192c-4.317-1.483-5.409,1.863-5.409,1.863 c2.218-2.197,4.449-0.066,4.449-0.066c-3.111,2.3,0.755,5.521,0.755,5.521c-1.448-2.979,0.811-4.256,0.811-4.256 c-0.163,2.396,1.092,4.842,1.94,6.2c-4.803,0.354-8.146,1.136-8.146,2.046c0,1.241,6.231,2.25,13.917,2.25 c7.687,0,13.917-1.009,13.917-2.25C31.182,31.213,26.589,30.324,20.431,30.09z M14.083,15.2c0.182,0.013,0.362,0.042,0.541,0.082 c-0.053,0.085-0.096,0.177-0.13,0.272C14.381,15.411,14.243,15.29,14.083,15.2z M14.503,17.264c0.087-0.114,0.157-0.24,0.206-0.379 c0.059,0.075,0.124,0.143,0.196,0.204C14.761,17.127,14.625,17.185,14.503,17.264z M16.166,17.375 c0.075-0.02,0.148-0.043,0.218-0.074c0.028,0.136,0.058,0.283,0.086,0.437C16.39,17.6,16.287,17.478,16.166,17.375z M12.003,24.915 c0.042,0.045,0.091,0.082,0.145,0.11c-0.062-0.01-0.124-0.008-0.187,0.003C11.979,24.994,11.993,24.954,12.003,24.915z M11.959,24.154c-0.046,0.062-0.08,0.133-0.1,0.207c-0.029-0.031-0.062-0.061-0.097-0.084 C11.826,24.23,11.891,24.189,11.959,24.154z M11.462,25.549c-0.015-0.066-0.028-0.127-0.041-0.187c0.032,0,0.065-0.005,0.098-0.009 C11.49,25.415,11.47,25.48,11.462,25.549z"/>
</symbol>
<symbol id="sick" viewBox="0 0 24 24">
<path d="M12,10.8c2.6,0,4.8-2.1,4.8-4.8S14.6,1.2,12,1.2C9.4,1.2,7.2,3.4,7.2,6S9.4,10.8,12,10.8z M12,2.8 c1.8,0,3.2,1.5,3.2,3.2S13.8,9.2,12,9.2S8.8,7.8,8.8,6S10.2,2.8,12,2.8z" />
<path d="M12,12.2c-5.4,0-9.8,4.4-9.8,9.8c0,0.4,0.3,0.8,0.8,0.8s0.8-0.3,0.8-0.8c0-4.2,3.1-7.7,7.2-8.2l-1.7,5.5 c-0.1,0.2,0,0.5,0.1,0.7l2,2.5c0.1,0.2,0.4,0.3,0.6,0.3s0.4-0.1,0.6-0.3l2-2.5c0.2-0.2,0.2-0.5,0.1-0.7L13,13.8 c4.1,0.5,7.2,4,7.2,8.2c0,0.4,0.3,0.8,0.8,0.8s0.8-0.3,0.8-0.8C21.8,16.6,17.4,12.2,12,12.2z M12,20.8l-1.2-1.5l1.2-3.8l1.2,3.8 L12,20.8z" />
</symbol>
<symbol id="pto" viewBox="0 0 24 24">
<path d="M12,10.8c2.6,0,4.8-2.1,4.8-4.8S14.6,1.2,12,1.2C9.4,1.2,7.2,3.4,7.2,6S9.4,10.8,12,10.8z M12,2.8 c1.8,0,3.2,1.5,3.2,3.2S13.8,9.2,12,9.2S8.8,7.8,8.8,6S10.2,2.8,12,2.8z" />
<path d="M12,12.2c-5.4,0-9.8,4.4-9.8,9.8c0,0.4,0.3,0.8,0.8,0.8s0.8-0.3,0.8-0.8c0-4.2,3.1-7.7,7.2-8.2l-1.7,5.5 c-0.1,0.2,0,0.5,0.1,0.7l2,2.5c0.1,0.2,0.4,0.3,0.6,0.3s0.4-0.1,0.6-0.3l2-2.5c0.2-0.2,0.2-0.5,0.1-0.7L13,13.8 c4.1,0.5,7.2,4,7.2,8.2c0,0.4,0.3,0.8,0.8,0.8s0.8-0.3,0.8-0.8C21.8,16.6,17.4,12.2,12,12.2z M12,20.8l-1.2-1.5l1.2-3.8l1.2,3.8 L12,20.8z" />
</symbol>
<symbol id="remote" viewBox="0 0 24 24">
<path d="M12,10.8c2.6,0,4.8-2.1,4.8-4.8S14.6,1.2,12,1.2C9.4,1.2,7.2,3.4,7.2,6S9.4,10.8,12,10.8z M12,2.8 c1.8,0,3.2,1.5,3.2,3.2S13.8,9.2,12,9.2S8.8,7.8,8.8,6S10.2,2.8,12,2.8z" />
<path d="M12,12.2c-5.4,0-9.8,4.4-9.8,9.8c0,0.4,0.3,0.8,0.8,0.8s0.8-0.3,0.8-0.8c0-4.2,3.1-7.7,7.2-8.2l-1.7,5.5 c-0.1,0.2,0,0.5,0.1,0.7l2,2.5c0.1,0.2,0.4,0.3,0.6,0.3s0.4-0.1,0.6-0.3l2-2.5c0.2-0.2,0.2-0.5,0.1-0.7L13,13.8 c4.1,0.5,7.2,4,7.2,8.2c0,0.4,0.3,0.8,0.8,0.8s0.8-0.3,0.8-0.8C21.8,16.6,17.4,12.2,12,12.2z M12,20.8l-1.2-1.5l1.2-3.8l1.2,3.8 L12,20.8z" />
</symbol>
<symbol id="overtime" viewBox="0 0 24 24">
<path d="M12,10.8c2.6,0,4.8-2.1,4.8-4.8S14.6,1.2,12,1.2C9.4,1.2,7.2,3.4,7.2,6S9.4,10.8,12,10.8z M12,2.8 c1.8,0,3.2,1.5,3.2,3.2S13.8,9.2,12,9.2S8.8,7.8,8.8,6S10.2,2.8,12,2.8z" />
<path d="M12,12.2c-5.4,0-9.8,4.4-9.8,9.8c0,0.4,0.3,0.8,0.8,0.8s0.8-0.3,0.8-0.8c0-4.2,3.1-7.7,7.2-8.2l-1.7,5.5 c-0.1,0.2,0,0.5,0.1,0.7l2,2.5c0.1,0.2,0.4,0.3,0.6,0.3s0.4-0.1,0.6-0.3l2-2.5c0.2-0.2,0.2-0.5,0.1-0.7L13,13.8 c4.1,0.5,7.2,4,7.2,8.2c0,0.4,0.3,0.8,0.8,0.8s0.8-0.3,0.8-0.8C21.8,16.6,17.4,12.2,12,12.2z M12,20.8l-1.2-1.5l1.2-3.8l1.2,3.8 L12,20.8z" />
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -15,6 +15,21 @@
"MoveStaff": "Employee transfer",
"MoveStaffDescr": "Do you want to transfer employee from {current} to {department}",
"Departments": "Departments",
"AddEmployee": "Add employee"
"ShowEmployees": "Show employees",
"AddEmployee": "Add employee",
"Schedule": "Schedule",
"RequestType": "Type",
"CreateRequest": "Create {type}",
"Today": "Today",
"NoEmployeesInDepartment": "There are no employees in the selected department",
"Vacation": "Vacation",
"Sick": "Sick",
"PTO": "PTO",
"Remote": "Remote",
"Overtime": "Overtime",
"PTO2": "PTO/2",
"Overtime2": "Overtime/2",
"EditRequest": "Edit {type}",
"Request": "Request"
}
}

View File

@ -15,6 +15,21 @@
"MoveStaff": "Перевод сотрудника",
"MoveStaffDescr": "Вы действительно хотите перевести сотрудника из {current} в {department}",
"Departments": "Департаменты",
"AddEmployee": "Добавить сотрудника"
"ShowEmployees": "Просмотреть сотрудников",
"AddEmployee": "Добавить сотрудника",
"Schedule": "График",
"RequestType": "Тип",
"CreateRequest": "Создать {type}",
"Today": "Сегодня",
"NoEmployeesInDepartment": "Нет сотрудников в выбранном департаменте",
"Vacation": "Отпуск",
"Sick": "Больничный",
"PTO": "PTO",
"Remote": "Удаленно",
"Overtime": "Переработка",
"PTO2": "PTO/2",
"Overtime2": "Переработка/2",
"EditRequest": "Редактировать {type}",
"Request": "Запрос"
}
}

View File

@ -20,7 +20,12 @@ const icons = require('../assets/icons.svg') as string // eslint-disable-line
loadMetadata(hr.icon, {
HR: `${icons}#hr`,
Department: `${icons}#department`,
Structure: `${icons}#structure`
Structure: `${icons}#structure`,
Vacation: `${icons}#vacation`,
Sick: `${icons}#sick`,
PTO: `${icons}#pto`,
Overtime: `${icons}#overtime`,
Remote: `${icons}#remote`
})
addStringsLoader(hrId, async (lang: string) => await import(`../lang/${lang}.json`))

View File

@ -32,6 +32,7 @@
"dependencies": {
"@anticrm/platform": "~0.6.6",
"svelte": "^3.47",
"@anticrm/calendar": "~0.6.0",
"@anticrm/hr": "~0.6.0",
"@anticrm/ui": "~0.6.0",
"@anticrm/presentation": "~0.6.2",
@ -41,6 +42,8 @@
"@anticrm/view": "~0.6.0",
"@anticrm/view-resources": "~0.6.0",
"@anticrm/contact-resources": "~0.6.0",
"@anticrm/attachment-resources": "~0.6.0",
"@anticrm/text-editor": "~0.6.0",
"@anticrm/setting": "~0.6.1",
"@anticrm/attachment": "~0.6.1"
}

View File

@ -0,0 +1,119 @@
<!--
// Copyright © 2022 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 { AttachmentStyledBox } from '@anticrm/attachment-resources'
import calendar from '@anticrm/calendar'
import core, { generateId, Ref } from '@anticrm/core'
import { Request, RequestType, Staff } from '@anticrm/hr'
import { translate } from '@anticrm/platform'
import { Card, createQuery, getClient } from '@anticrm/presentation'
import ui, { Button, DateRangePresenter, DropdownLabelsIntl, IconAttachment } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import hr from '../plugin'
export let staff: Staff
export let date: Date
let description: string = ''
const objectId: Ref<Request> = generateId()
let descriptionBox: AttachmentStyledBox
const dispatch = createEventDispatcher()
const client = getClient()
const typesQuery = createQuery()
let types: RequestType[] = []
let type: RequestType | undefined = undefined
let typeLabel = ''
$: type && translate(type.label, {}).then((p) => (typeLabel = p))
typesQuery.query(hr.class.RequestType, {}, (res) => {
types = res
if (type === undefined) {
type = types[0]
}
})
$: value = new Date(date).getTime()
$: dueDate = new Date(value).setDate(new Date(value).getDate() + 1)
export function canClose (): boolean {
return description.length === 0
}
async function saveRequest () {
let date: number | undefined
if (value != null) date = value
if (date === undefined) return
if (type === undefined) return
await client.createDoc(hr.class.Request, staff.department, {
attachedTo: staff._id,
attachedToClass: staff._class,
type: type._id,
date,
dueDate,
description,
collection: 'requests'
})
await descriptionBox.createAttachments()
}
function typeSelected (_id: Ref<RequestType>): void {
type = types.find((p) => p._id === _id)
}
</script>
<Card
label={hr.string.CreateRequest}
labelProps={{ type: typeLabel }}
okAction={saveRequest}
canSave={value !== undefined}
on:close={() => {
dispatch('close')
}}
>
<DropdownLabelsIntl
items={types.map((p) => {
return { id: p._id, label: p.label }
})}
placeholder={hr.string.RequestType}
label={hr.string.RequestType}
on:selected={(e) => typeSelected(e.detail)}
/>
<AttachmentStyledBox
bind:this={descriptionBox}
{objectId}
_class={hr.class.Request}
space={staff.department}
alwaysEdit
showButtons={false}
maxHeight={'card'}
bind:content={description}
placeholder={core.string.Description}
/>
<svelte:fragment slot="pool">
<DateRangePresenter bind:value editable labelNull={ui.string.SelectDate} />
<DateRangePresenter bind:value={dueDate} labelNull={calendar.string.DueTo} editable />
</svelte:fragment>
<svelte:fragment slot="footer">
<Button
icon={IconAttachment}
kind={'transparent'}
on:click={() => {
descriptionBox.attach()
}}
/>
</svelte:fragment>
</Card>

View File

@ -0,0 +1,55 @@
<!--
// Copyright © 2022 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 core from '@anticrm/core'
import { Request } from '@anticrm/hr'
import { getClient } from '@anticrm/presentation'
import { StyledTextArea } from '@anticrm/text-editor'
import { createFocusManager, FocusHandler } from '@anticrm/ui'
import { createEventDispatcher, onMount } from 'svelte'
export let object: Request
const dispatch = createEventDispatcher()
const client = getClient()
async function onChangeDescription (): Promise<void> {
if (object === undefined) return
await client.update(object, {
description: object.description
})
}
const manager = createFocusManager()
onMount(() => {
dispatch('open', {
ignoreKeys: ['comments', 'description']
})
})
</script>
<FocusHandler {manager} />
{#if object !== undefined}
<div class="flex-row-stretch flex-grow">
<StyledTextArea
bind:content={object.description}
placeholder={core.string.Description}
showButtons={false}
on:value={onChangeDescription}
/>
</div>
{/if}

View File

@ -0,0 +1,35 @@
<!--
// Copyright © 2022 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 { Ref, SortingOrder } from '@anticrm/core'
import hr, { Staff } from '@anticrm/hr'
import { Table } from '@anticrm/view-resources'
export let date: Date
export let employee: Ref<Staff>
$: endDate = new Date(date).setDate(date.getDate() + 1)
</script>
<Table
_class={hr.class.Request}
query={{
attachedTo: employee,
dueDate: { $gte: date.getTime() },
date: { $lt: endDate }
}}
config={['$lookup.type.label', 'date', 'dueDate']}
options={{ sort: { date: SortingOrder.Ascending } }}
/>

View File

@ -0,0 +1,94 @@
<!--
// Copyright © 2022 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 calendar from '@anticrm/calendar'
import { Ref } from '@anticrm/core'
import { Department } from '@anticrm/hr'
import { createQuery, SpaceSelector } from '@anticrm/presentation'
import { Button, Icon, IconBack, IconForward, Label } from '@anticrm/ui'
import hr from '../plugin'
import ScheduleView from './ScheduleView.svelte'
let department = hr.ids.Head
let currentDate: Date = new Date()
const query = createQuery()
let descendants: Map<Ref<Department>, Department[]> = new Map<Ref<Department>, Department[]>()
let departments: Map<Ref<Department>, Department> = new Map<Ref<Department>, Department>()
query.query(hr.class.Department, {}, (res) => {
departments.clear()
descendants.clear()
for (const doc of res) {
const current = descendants.get(doc.space) ?? []
current.push(doc)
descendants.set(doc.space, current)
departments.set(doc._id, doc)
}
departments = departments
descendants = descendants
})
function inc (val: number): void {
currentDate.setMonth(currentDate.getMonth() + val)
currentDate = currentDate
}
function getMonthName (date: Date): string {
return new Intl.DateTimeFormat('default', {
month: 'long'
}).format(date)
}
</script>
<div class="ac-header full divide">
<div class="ac-header__wrap-title">
<div class="ac-header__icon"><Icon icon={calendar.icon.Calendar} size={'small'} /></div>
<span class="ac-header__title"><Label label={hr.string.Schedule} /></span>
<div class="flex ml-4 gap-2">
<Button
icon={IconBack}
size={'small'}
on:click={() => {
inc(-1)
}}
/>
<Button
size={'small'}
label={hr.string.Today}
on:click={() => {
currentDate = new Date()
}}
/>
<Button
icon={IconForward}
size={'small'}
on:click={() => {
inc(1)
}}
/>
</div>
<div class="fs-title ml-4 flex-row-center">
{getMonthName(currentDate)}
{currentDate.getFullYear()}
</div>
</div>
<SpaceSelector _class={hr.class.Department} label={hr.string.Department} bind:space={department} />
</div>
<div class="mr-6 h-full">
<ScheduleView {department} {descendants} departmentById={departments} {currentDate} />
</div>

View File

@ -0,0 +1,76 @@
<!--
// Copyright © 2022 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 hr, { Request, RequestType } from '@anticrm/hr'
import { getClient } from '@anticrm/presentation'
import { areDatesEqual, getPlatformColor, Icon, showPopup } from '@anticrm/ui'
import { ContextMenu } from '@anticrm/view-resources'
export let requests: Request[]
export let date: Date
export let editable: boolean = false
const client = getClient()
async function getType (request: Request): Promise<RequestType | undefined> {
return await client.findOne(hr.class.RequestType, {
_id: request.type
})
}
function getStyle (type: RequestType): string {
let res = `background-color: ${getPlatformColor(type.color)};`
if (Math.abs(type.value % 1) === 0.5) {
res += ' height: 50%;'
}
return res
}
function click (e: MouseEvent, request: Request) {
if (!editable) return
e.stopPropagation()
e.preventDefault()
showPopup(ContextMenu, { object: request }, e.target as HTMLElement)
}
</script>
<div class="w-full h-full relative">
{#each requests as request}
{#await getType(request) then type}
{#if type}
<div
class="request"
class:cursor-pointer={editable}
style={getStyle(type)}
on:click={(e) => {
click(e, request)
}}
>
{#if areDatesEqual(new Date(request.date), date) || date.getDate() === 1}
<Icon icon={type.icon} size="large" />
{/if}
</div>
{/if}
{/await}
{/each}
</div>
<style lang="scss">
.request {
position: absolute;
height: 100%;
width: 100%;
}
</style>

View File

@ -0,0 +1,244 @@
<!--
// Copyright © 2022 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 { getCurrentAccount, Ref, Timestamp } from '@anticrm/core'
import type { Department, Request, Staff } from '@anticrm/hr'
import { createQuery } from '@anticrm/presentation'
import {
Label,
daysInMonth,
isWeekend,
areDatesEqual,
day as getDay,
getWeekDayName,
showPopup,
eventToHTMLElement,
Scroller,
tooltip,
LabelAndProps
} from '@anticrm/ui'
import { EmployeePresenter } from '@anticrm/contact-resources'
import contact from '@anticrm/contact-resources/src/plugin'
import hr from '../plugin'
import { Employee, EmployeeAccount } from '@anticrm/contact'
import CreateRequest from './CreateRequest.svelte'
import ScheduleRequests from './ScheduleRequests.svelte'
import RequestsPopup from './RequestsPopup.svelte'
export let department: Ref<Department>
export let descendants: Map<Ref<Department>, Department[]>
export let departmentById: Map<Ref<Department>, Department>
export let currentDate: Date = new Date()
$: startDate = new Date(new Date(currentDate).setDate(1)).setHours(0, 0, 0, 0)
$: endDate = new Date(startDate).setMonth(new Date(startDate).getMonth() + 1)
$: departments = [department, ...getDescendants(department, descendants)]
const lq = createQuery()
const staffQuery = createQuery()
let staff: Staff[] = []
staffQuery.query(hr.mixin.Staff, {}, (res) => {
staff = res
})
let employeeRequests = new Map<Ref<Staff>, Request[]>()
function getDescendants (
department: Ref<Department>,
descendants: Map<Ref<Department>, Department[]>
): Ref<Department>[] {
const res = (descendants.get(department) ?? []).map((p) => p._id)
for (const department of res) {
res.push(...getDescendants(department, descendants))
}
return res
}
function update (departments: Ref<Department>[], startDate: Timestamp, endDate: Timestamp) {
lq.query(
hr.class.Request,
{
dueDate: { $gte: startDate },
date: { $lt: endDate },
space: { $in: departments }
},
(res) => {
employeeRequests.clear()
for (const request of res) {
const requests = employeeRequests.get(request.attachedTo) ?? []
requests.push(request)
employeeRequests.set(request.attachedTo, requests)
}
employeeRequests = employeeRequests
}
)
}
$: update(departments, startDate, endDate)
const todayDate = new Date()
function getRequests (employee: Ref<Staff>, date: Date): Request[] {
const requests = employeeRequests.get(employee)
if (requests === undefined) return []
const res: Request[] = []
const time = date.getTime()
const endTime = new Date(date).setDate(date.getDate() + 1)
for (const request of requests) {
if (request.date < endTime && request.dueDate > time) {
res.push(request)
}
}
return res
}
function createRequest (e: MouseEvent, date: Date, staff: Staff): void {
if (!isEditable(staff)) return
e.preventDefault()
e.stopPropagation()
showPopup(
CreateRequest,
{
staff,
date
},
eventToHTMLElement(e)
)
}
const currentEmployee = (getCurrentAccount() as EmployeeAccount).employee
function getTeamLead (_id: Ref<Department>): Ref<Employee> | undefined {
const department = departmentById.get(_id)
if (department === undefined) return
if (department.teamLead != null) return department.teamLead
return getTeamLead(department.space)
}
function isEditable (employee: Staff): boolean {
if (employee._id === currentEmployee) return true
const lead = getTeamLead(employee.department)
return lead === currentEmployee
}
function getTooltip (employee: Staff, date: Date): LabelAndProps | undefined {
const requests = getRequests(employee._id, date)
if (requests.length === 0) return
return {
component: RequestsPopup,
props: { date, employee: employee._id }
}
}
$: departmentStaff = staff.filter((p) => departments.includes(p.department) || employeeRequests.has(p._id))
</script>
{#if departmentStaff.length}
<Scroller>
<table>
<thead class="scroller-thead">
<tr class="scroller-thead__tr">
<th>
<Label label={contact.string.Employee} />
</th>
{#each [...Array(daysInMonth(currentDate)).keys()] as dayOfMonth}
{@const day = getDay(new Date(startDate), dayOfMonth)}
<th class:today={areDatesEqual(todayDate, day)} class:weekend={isWeekend(day)}>
<div class="cursor-pointer uppercase flex-col-center">
<div class="flex-center">{getWeekDayName(day, 'short')}</div>
<div class="flex-center">{day.getDate()}</div>
</div>
</th>
{/each}
</tr>
</thead>
<tbody>
{#each departmentStaff as employee}
<tr>
<td>
<EmployeePresenter value={employee} />
</td>
{#each [...Array(daysInMonth(currentDate)).keys()] as dayOfMonth}
{@const date = getDay(new Date(startDate), dayOfMonth)}
{@const requests = getRequests(employee._id, date)}
{@const editable = isEditable(employee)}
<td
class:today={areDatesEqual(todayDate, date)}
class:weekend={isWeekend(date)}
class:cursor-pointer={editable}
use:tooltip={getTooltip(employee, date)}
on:click={(e) => createRequest(e, date, employee)}
>
{#if requests.length}
<ScheduleRequests {requests} {date} {editable} />
{/if}
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</Scroller>
{:else}
<div class="flex-center h-full flex-grow fs-title">
<Label label={hr.string.NoEmployeesInDepartment} />
</div>
{/if}
<style lang="scss">
table {
border-collapse: collapse;
table-layout: fixed;
width: auto;
position: relative;
td,
th {
width: auto;
min-width: 1rem;
&:first-child {
width: 15rem;
padding: 0.5rem;
}
}
th {
padding: 0.5rem;
height: 2.5rem;
font-weight: 500;
font-size: 0.75rem;
color: var(--dark-color);
box-shadow: inset 0 -1px 0 0 var(--divider-color);
user-select: none;
&.today {
color: var(--caption-color);
}
&.weekend:not(.today) {
color: var(--warning-color);
}
}
td {
height: 3.5rem;
border: 1px solid var(--divider-color);
color: var(--caption-color);
&.today {
background-color: var(--theme-bg-accent-hover);
}
&.weekend:not(.today) {
background-color: var(--theme-bg-accent-color);
}
}
}
</style>

View File

@ -13,14 +13,14 @@
// limitations under the License.
-->
<script lang="ts">
import contact from '@anticrm/contact'
import { DocumentQuery, Ref } from '@anticrm/core'
import { Button, Icon, Label, Scroller, SearchEdit, showPopup, IconAdd, eventToHTMLElement } from '@anticrm/ui'
import type { Department } from '@anticrm/hr'
import { createQuery } from '@anticrm/presentation'
import { Button, eventToHTMLElement, Icon, IconAdd, Label, Scroller, SearchEdit, showPopup } from '@anticrm/ui'
import hr from '../plugin'
import CreateDepartment from './CreateDepartment.svelte'
import DepartmentCard from './DepartmentCard.svelte'
import { createQuery } from '@anticrm/presentation'
import contact from '@anticrm/contact'
let search = ''
let resultQuery: DocumentQuery<Department> = {}
@ -45,14 +45,10 @@
head = res.find((p) => p._id === hr.ids.Head)
descendants.clear()
for (const doc of res) {
const current = descendants.get(doc.space)
if (!current) {
descendants.set(doc.space, [doc])
} else {
const current = descendants.get(doc.space) ?? []
current.push(doc)
descendants.set(doc.space, current)
}
}
descendants = descendants
},
{

View File

@ -14,16 +14,20 @@
//
import { Resources } from '@anticrm/platform'
import Structure from './components/Structure.svelte'
import DepartmentEditor from './components/DepartmentEditor.svelte'
import DepartmentStaff from './components/DepartmentStaff.svelte'
import EditDepartment from './components/EditDepartment.svelte'
import DepartmentEditor from './components/DepartmentEditor.svelte'
import EditRequest from './components/EditRequest.svelte'
import Schedule from './components/Schedule.svelte'
import Structure from './components/Structure.svelte'
export default async (): Promise<Resources> => ({
component: {
Structure,
EditDepartment,
DepartmentStaff,
DepartmentEditor
DepartmentEditor,
Schedule,
EditRequest
}
})

View File

@ -31,6 +31,12 @@ export default mergeIds(hrId, hr, {
TeamLeadTooltip: '' as IntlString,
MoveStaff: '' as IntlString,
MoveStaffDescr: '' as IntlString,
AddEmployee: '' as IntlString
AddEmployee: '' as IntlString,
RequestType: '' as IntlString,
Schedule: '' as IntlString,
EditRequest: '' as IntlString,
CreateRequest: '' as IntlString,
Today: '' as IntlString,
NoEmployeesInDepartment: '' as IntlString
}
})

View File

@ -14,8 +14,8 @@
//
import type { Employee, EmployeeAccount } from '@anticrm/contact'
import type { Arr, Class, Doc, Mixin, Ref, Space } from '@anticrm/core'
import type { Asset, Plugin } from '@anticrm/platform'
import type { Arr, AttachedDoc, Class, Doc, Markup, Mixin, Ref, Space, Timestamp } from '@anticrm/core'
import type { Asset, IntlString, Plugin } from '@anticrm/platform'
import { plugin } from '@anticrm/platform'
/**
@ -43,6 +43,37 @@ export interface Staff extends Employee {
department: Ref<Department>
}
/**
* @public
*/
export interface RequestType extends Doc {
label: IntlString
icon: Asset
value: number
color: number
}
/**
* @public
*/
export interface Request extends AttachedDoc {
attachedTo: Ref<Staff>
attachedToClass: Ref<Class<Staff>>
space: Ref<Department>
type: Ref<RequestType>
description: Markup
comments?: number
attachments?: number
date: Timestamp
dueDate: Timestamp
}
/**
* @public
*/
@ -57,7 +88,9 @@ const hr = plugin(hrId, {
},
class: {
Department: '' as Ref<Class<Department>>,
DepartmentMember: '' as Ref<Class<DepartmentMember>>
DepartmentMember: '' as Ref<Class<DepartmentMember>>,
Request: '' as Ref<Class<Request>>,
RequestType: '' as Ref<Class<RequestType>>
},
mixin: {
Staff: '' as Ref<Mixin<Staff>>
@ -65,10 +98,23 @@ const hr = plugin(hrId, {
icon: {
HR: '' as Asset,
Department: '' as Asset,
Structure: '' as Asset
Structure: '' as Asset,
Vacation: '' as Asset,
Sick: '' as Asset,
PTO: '' as Asset,
Remote: '' as Asset,
Overtime: '' as Asset
},
ids: {
Head: '' as Ref<Department>
Head: '' as Ref<Department>,
Vacation: '' as Ref<RequestType>,
Leave: '' as Ref<RequestType>,
Sick: '' as Ref<RequestType>,
PTO: '' as Ref<RequestType>,
PTO2: '' as Ref<RequestType>,
Remote: '' as Ref<RequestType>,
Overtime: '' as Ref<RequestType>,
Overtime2: '' as Ref<RequestType>
}
})