UBER-615 HR Management: Add departments sidebar (#3522)

Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2023-07-25 17:49:23 +07:00 committed by GitHub
parent e7a388fb4e
commit 021dcafeb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 365 additions and 132 deletions

View File

@ -1,5 +1,5 @@
//
// Copyright © 2022 Hardcore Engineering Inc.
// Copyright © 2022, 2023 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
@ -185,25 +185,7 @@ export function createModel (builder: Builder): void {
icon: hr.icon.HR,
alias: hrId,
hidden: false,
navigatorModel: {
specials: [
{
id: 'structure',
component: hr.component.Structure,
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: []
}
component: hr.component.Schedule
},
hr.app.HR
)

View File

@ -1,5 +1,5 @@
//
// Copyright © 2022 Hardcore Engineering Inc.
// Copyright © 2022, 2023 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
@ -23,8 +23,6 @@ import { Action, ActionCategory, ViewAction } from '@hcengineering/view'
export default mergeIds(hrId, hr, {
string: {
HRApplication: '' as IntlString,
Departments: '' as IntlString,
Request: '' as IntlString,
Vacation: '' as IntlString,
Sick: '' as IntlString,

View File

@ -0,0 +1,22 @@
<!--
// Copyright © 2023 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: '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">
<polygon points="11.3,5.8 8,9.1 4.7,5.8 4,6.5 8,10.5 12,6.5 " />
</svg>

View File

@ -141,6 +141,7 @@ export { default as IconInfo } from './components/icons/Info.svelte'
export { default as IconBlueCheck } from './components/icons/BlueCheck.svelte'
export { default as IconCheck } from './components/icons/Check.svelte'
export { default as IconCheckAll } from './components/icons/CheckAll.svelte'
export { default as IconChevronDown } from './components/icons/ChevronDown.svelte'
export { default as IconArrowLeft } from './components/icons/ArrowLeft.svelte'
export { default as IconArrowRight } from './components/icons/ArrowRight.svelte'
export { default as IconNavPrev } from './components/icons/NavPrev.svelte'

View File

@ -15,6 +15,7 @@
"MoveStaff": "Employee transfer",
"MoveStaffDescr": "Do you want to transfer employee from {current} to {department}",
"Departments": "Departments",
"Positions": "Positions",
"ShowEmployees": "Show employees",
"AddEmployee": "Add employee",
"SelectEmployee": "Select employee",

View File

@ -15,6 +15,7 @@
"MoveStaff": "Перевод сотрудника",
"MoveStaffDescr": "Вы действительно хотите перевести сотрудника из {current} в {department}",
"Departments": "Департаменты",
"Positions": "Позиции",
"ShowEmployees": "Просмотреть сотрудников",
"AddEmployee": "Добавить сотрудника",
"SelectEmployee": "Выберите сотрудника",

View File

@ -45,6 +45,7 @@
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/attachment-resources": "^0.6.0",
"@hcengineering/workbench-resources": "^0.6.1",
"@hcengineering/text-editor": "^0.6.0",
"@hcengineering/setting": "^0.6.9",
"@hcengineering/attachment": "^0.6.8",

View File

@ -19,12 +19,17 @@
import { employeeByIdStore } from '@hcengineering/contact-resources'
import { DocumentQuery, getCurrentAccount, Ref } from '@hcengineering/core'
import { Department, Staff } from '@hcengineering/hr'
import { createQuery, SpaceSelector } from '@hcengineering/presentation'
import { createQuery } from '@hcengineering/presentation'
import type { TabItem } from '@hcengineering/ui'
import { Button, IconBack, IconForward, Label, SearchEdit, TabList } from '@hcengineering/ui'
import view from '@hcengineering/view'
import hr from '../plugin'
import ScheduleView from './ScheduleView.svelte'
import Sidebar from './sidebar/Sidebar.svelte'
export let visibileNav = true
const accountEmployee = $employeeByIdStore.get((getCurrentAccount() as EmployeeAccount).employee)
let accountStaff: Staff | undefined
@ -97,94 +102,104 @@
}
}
function departmentSelected (selected: Ref<Department>): void {
department = selected
}
const viewslist: TabItem[] = [
{ id: 'chart', icon: view.icon.Views },
{ id: 'stats', icon: view.icon.Table }
]
</script>
<div class="ac-header full divide caption-height">
<div class="ac-header__wrap-title mr-3">
<span class="ac-header__title"><Label label={hr.string.Schedule} /></span>
</div>
<div class="flex h-full">
{#if visibileNav}
<Sidebar
{department}
{descendants}
departmentById={departments}
on:selected={(e) => departmentSelected(e.detail)}
/>
{/if}
<div class="ac-header-full medium-gap mb-1">
{#if mode === CalendarMode.Month}
<TabList
items={viewslist}
multiselect={false}
selected={display}
on:select={(result) => {
if (result.detail !== undefined) display = result.detail.id
}}
/>
{/if}
<SpaceSelector
_class={hr.class.Department}
label={hr.string.Department}
bind:space={department}
size={'medium'}
kind={'regular'}
/>
</div>
</div>
<div class="ac-header full divide search-start">
<div class="ac-header-full small-gap">
<SearchEdit bind:value={search} on:change={() => updateResultQuery(search)} />
<!-- <ActionIcon icon={IconMoreH} size={'small'} /> -->
</div>
<div class="ac-header-full medium-gap">
<!-- <ViewletSettingButton bind:viewOptions {viewlet} /> -->
<!-- <ActionIcon icon={IconMoreH} size={'small'} /> -->
</div>
</div>
<div class="ac-header full divide">
<div class="ac-header-full small-gap">
<Button
icon={IconBack}
kind={'ghost'}
on:click={() => {
inc(-1)
}}
/>
<Button
label={calendar.string.Today}
kind={'ghost'}
on:click={() => {
currentDate = new Date()
}}
/>
<Button
icon={IconForward}
kind={'ghost'}
on:click={() => {
inc(1)
}}
/>
<div class="buttons-divider" />
<div class="fs-title flex-row-center flex-grow firstLetter">
{#if mode === CalendarMode.Month}
<span class="mr-2 overflow-label">{getMonthName(currentDate)}</span>
{/if}
{currentDate.getFullYear()}
<div class="antiPanel-component filled">
<div class="ac-header full divide caption-height">
<div class="ac-header__wrap-title mr-3">
<span class="ac-header__title"><Label label={hr.string.Schedule} /></span>
</div>
<div class="ac-header-full medium-gap mb-1">
{#if mode === CalendarMode.Month}
<TabList
items={viewslist}
multiselect={false}
selected={display}
on:select={(result) => {
if (result.detail !== undefined) display = result.detail.id
}}
/>
{/if}
</div>
</div>
<div class="ac-header full divide search-start">
<div class="ac-header-full small-gap">
<SearchEdit bind:value={search} on:change={() => updateResultQuery(search)} />
<!-- <ActionIcon icon={IconMoreH} size={'small'} /> -->
</div>
<div class="ac-header-full medium-gap">
<!-- <ViewletSettingButton bind:viewOptions {viewlet} /> -->
<!-- <ActionIcon icon={IconMoreH} size={'small'} /> -->
</div>
</div>
<div class="ac-header full divide">
<div class="ac-header-full small-gap">
<Button
icon={IconBack}
kind={'ghost'}
on:click={() => {
inc(-1)
}}
/>
<Button
label={calendar.string.Today}
kind={'ghost'}
on:click={() => {
currentDate = new Date()
}}
/>
<Button
icon={IconForward}
kind={'ghost'}
on:click={() => {
inc(1)
}}
/>
<div class="buttons-divider" />
<div class="fs-title flex-row-center flex-grow firstLetter">
{#if mode === CalendarMode.Month}
<span class="mr-2 overflow-label">{getMonthName(currentDate)}</span>
{/if}
{currentDate.getFullYear()}
</div>
</div>
<TabList
items={[
{ id: 'ModeMonth', labelIntl: calendar.string.ModeMonth },
{ id: 'ModeYear', labelIntl: calendar.string.ModeYear }
]}
multiselect={false}
on:select={handleSelect}
/>
</div>
</div>
<TabList
items={[
{ id: 'ModeMonth', labelIntl: calendar.string.ModeMonth },
{ id: 'ModeYear', labelIntl: calendar.string.ModeYear }
]}
multiselect={false}
on:select={handleSelect}
/>
</div>
<ScheduleView
{department}
{descendants}
departmentById={departments}
staffQuery={resultQuery}
{currentDate}
{mode}
{display}
/>
<ScheduleView
{department}
{descendants}
departmentById={departments}
staffQuery={resultQuery}
{currentDate}
{mode}
{display}
/>
</div>
</div>

View File

@ -0,0 +1,59 @@
<!--
// Copyright © 2023 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 { createEventDispatcher } from 'svelte'
import { Ref } from '@hcengineering/core'
import { Department } from '@hcengineering/hr'
import hr from '../../plugin'
import TreeElement from './TreeElement.svelte'
export let departments: Ref<Department>[]
export let descendants: Map<Ref<Department>, Department[]>
export let departmentById: Map<Ref<Department>, Department>
export let selected: Ref<Department> | undefined
export let level = 0
const dispatch = createEventDispatcher()
function getDescendants (department: Ref<Department>): Ref<Department>[] {
return (descendants.get(department) ?? []).map((p) => p._id)
}
function handleDepartmentSelected (department: Ref<Department>): void {
dispatch('selected', department)
}
</script>
{#each departments as dep}
{@const department = departmentById.get(dep)}
{@const desc = getDescendants(dep)}
{#if department}
<TreeElement
icon={hr.icon.Department}
title={department.name}
selected={selected === department._id}
node={desc.length > 0}
{level}
on:click={() => handleDepartmentSelected(department._id)}
>
{#if desc.length}
<svelte:self departments={desc} {descendants} {departmentById} {selected} level={level + 1} on:selected />
{/if}
</TreeElement>
{/if}
{/each}

View File

@ -0,0 +1,53 @@
<!--
// Copyright © 2023 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 } from '@hcengineering/core'
import { Department } from '@hcengineering/hr'
import { Scroller } from '@hcengineering/ui'
import { TreeNode } from '@hcengineering/view-resources'
import { NavFooter, NavHeader } from '@hcengineering/workbench-resources'
import hr from '../../plugin'
import DepartmentsHierarchy from './DepartmentsHierarchy.svelte'
export let department: Ref<Department>
export let descendants: Map<Ref<Department>, Department[]>
export let departmentById: Map<Ref<Department>, Department>
const departments = [hr.ids.Head]
</script>
<div class="antiPanel-navigator filledNav indent">
<NavHeader label={hr.string.HRApplication} />
<Scroller shrink>
<!-- TODO Specials -->
<div class="antiNav-divider short line" />
<TreeNode label={hr.string.Departments} parent>
<DepartmentsHierarchy {departments} {descendants} {departmentById} selected={department} on:selected />
</TreeNode>
<div class="antiNav-divider short line" />
<TreeNode label={hr.string.Positions} parent>
<!-- TODO Positions -->
</TreeNode>
</Scroller>
<NavFooter />
</div>

View File

@ -0,0 +1,70 @@
<!--
// Copyright © 2023 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 { Asset, IntlString } from '@hcengineering/platform'
import type { AnySvelteComponent } from '@hcengineering/ui'
import { Icon, IconChevronDown, Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let iconProps: Record<string, any> | undefined = undefined
export let label: IntlString | undefined = undefined
export let title: string | undefined = undefined
export let node = false
export let parent = false
export let collapsed = false
export let selected = false
export let level = 0
$: style = `padding-left: calc(${level} * 1.25rem);`
const dispatch = createEventDispatcher()
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="antiNav-element"
class:selected
class:parent
class:collapsed
class:child={!node}
{style}
on:click={() => {
if (selected) {
collapsed = !collapsed
}
dispatch('click')
}}
>
<span class="an-element__label" class:title={node}>
{#if icon && !parent}
<div class="an-element__icon">
<Icon {icon} {iconProps} size={'small'} />
</div>
{/if}
<span class="overflow-label">
{#if label}<Label {label} />{:else}{title}{/if}
</span>
{#if node}
<div class="an-element__icon-arrow" class:collapsed>
<IconChevronDown size={'small'} />
</div>
{/if}
</span>
</div>
{#if node && !collapsed}
<div class="antiNav-element__dropbox"><slot /></div>
{/if}

View File

@ -18,7 +18,9 @@ import { IntlString, mergeIds } from '@hcengineering/platform'
export default mergeIds(hrId, hr, {
string: {
HRApplication: '' as IntlString,
Department: '' as IntlString,
Departments: '' as IntlString,
ParentDepartmentLabel: '' as IntlString,
Structure: '' as IntlString,
CreateDepartment: '' as IntlString,
@ -55,6 +57,7 @@ export default mergeIds(hrId, hr, {
Managers: '' as IntlString,
Export: '' as IntlString,
Separator: '' as IntlString,
ChooseSeparator: '' as IntlString
ChooseSeparator: '' as IntlString,
Positions: '' as IntlString
}
})

View File

@ -1,5 +1,6 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
// Copyright © 2023 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
@ -16,7 +17,7 @@
import type { Doc, Ref } from '@hcengineering/core'
import type { Asset, IntlString } from '@hcengineering/platform'
import type { Action, AnySvelteComponent } from '@hcengineering/ui'
import { ActionIcon, Icon, IconMoreH, Label, Menu, showPopup } from '@hcengineering/ui'
import { ActionIcon, Icon, IconChevronDown, IconMoreH, Label, Menu, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
export let _id: Ref<Doc> | undefined = undefined
@ -75,9 +76,7 @@
{#if node}
<div class="an-element__icon-arrow" class:collapsed>
<svg class="svg-small" fill="currentColor" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<polygon points="11.3,5.8 8,9.1 4.7,5.8 4,6.5 8,10.5 12,6.5 " />
</svg>
<IconChevronDown size={'small'} />
</div>
{/if}
</span>

View File

@ -0,0 +1,35 @@
<!--
// Copyright © 2023 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 setting from '@hcengineering/setting'
import { Icon, Label, showPopup } from '@hcengineering/ui'
import workbench from '../plugin'
import HelpAndSupport from './HelpAndSupport.svelte'
</script>
<div class="antiNav-footer-line" />
<div class="antiNav-footer-grower" />
<div class="antiNav-footer">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="antiNav-element" style:flex-grow={1} on:click={() => showPopup(HelpAndSupport, {}, 'help-center')}>
<div class="an-element__icon">
<Icon icon={setting.icon.Support} size={'small'} />
</div>
<span class="an-element__label title dark">
<Label label={workbench.string.HelpAndSupport} />
</span>
</div>
<slot />
</div>

View File

@ -17,13 +17,10 @@
import { getResource } from '@hcengineering/platform'
import preference, { SpacePreference } from '@hcengineering/preference'
import { createQuery, getClient } from '@hcengineering/presentation'
import setting from '@hcengineering/setting'
import { Icon, Label, Scroller, showPopup } from '@hcengineering/ui'
import { Scroller } from '@hcengineering/ui'
import { NavLink } from '@hcengineering/view-resources'
import type { Application, NavigatorModel, SpecialNavModel } from '@hcengineering/workbench'
import workbench from '../plugin'
import { getSpecialSpaceClass } from '../utils'
import HelpAndSupport from './HelpAndSupport.svelte'
import SpacesNav from './navigator/SpacesNav.svelte'
import SpecialElement from './navigator/SpecialElement.svelte'
import StarredNav from './navigator/StarredNav.svelte'
@ -184,17 +181,3 @@
<div class="antiNav-space" />
</Scroller>
{/if}
<div class="antiNav-footer-line" />
<div class="antiNav-footer-grower" />
<div class="antiNav-footer">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="antiNav-element" style:flex-grow={1} on:click={() => showPopup(HelpAndSupport, {}, 'help-center')}>
<div class="an-element__icon">
<Icon icon={setting.icon.Support} size={'small'} />
</div>
<span class="an-element__label title dark">
<Label label={workbench.string.HelpAndSupport} />
</span>
</div>
<slot />
</div>

View File

@ -69,6 +69,7 @@
import AppSwitcher from './AppSwitcher.svelte'
import Applications from './Applications.svelte'
import Logo from './Logo.svelte'
import NavFooter from './NavFooter.svelte'
import NavHeader from './NavHeader.svelte'
import Navigator from './Navigator.svelte'
import SelectWorkspaceMenu from './SelectWorkspaceMenu.svelte'
@ -695,11 +696,18 @@
{/await}
{/if}
{/if}
<Navigator {currentSpace} {currentSpecial} model={navigatorModel} {currentApplication} on:open={checkOnHide}>
<Navigator
{currentSpace}
{currentSpecial}
model={navigatorModel}
{currentApplication}
on:open={checkOnHide}
/>
<NavFooter>
{#if currentApplication.navFooterComponent}
<Component is={currentApplication.navFooterComponent} props={{ currentSpace }} />
{/if}
</Navigator>
</NavFooter>
</div>
{/if}
<div

View File

@ -28,6 +28,8 @@ async function hasArchiveSpaces (spaces: Space[]): Promise<boolean> {
return spaces.find((sp) => sp.archived) !== undefined
}
export { default as SpaceBrowser } from './components/SpaceBrowser.svelte'
export { default as NavFooter } from './components/NavFooter.svelte'
export { default as NavHeader } from './components/NavHeader.svelte'
export { default as SpecialElement } from './components/navigator/SpecialElement.svelte'
export { default as TreeSeparator } from './components/navigator/TreeSeparator.svelte'
export { SpecialView }

View File

@ -20,7 +20,7 @@ test.describe('hr tests', () => {
await (await page.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished()
})
test('test-pto-after-department-change', async ({ page, context }) => {
test.skip('test-pto-after-department-change', async ({ page }) => {
await page.locator('[id="app-hr\\:string\\:HRApplication"]').click()
await page.click('text="Structure"')
const department1 = 'dep1'