feat: hide inactive employees (#10821)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov
2026-05-05 18:06:13 +07:00
committed by GitHub
parent 2d73be14f8
commit 136a4e6b93
23 changed files with 106 additions and 38 deletions
+14
View File
@@ -621,6 +621,20 @@ export function createModel (builder: Builder): void {
configOptions: {
hiddenKeys: ['name'],
sortable: true
},
viewOptions: {
groupBy: [],
orderBy: [],
other: [
{
key: 'hideInactive',
type: 'toggle',
defaultValue: true,
actionTarget: 'query',
action: contact.function.HideInactive,
label: contact.string.HideInactive
}
]
}
},
contact.viewlet.TableEmployee
+3 -2
View File
@@ -16,7 +16,7 @@
import { contactId } from '@hcengineering/contact'
import contact from '@hcengineering/contact-resources/src/plugin'
import type { Client, Doc, DocManager, Ref } from '@hcengineering/core'
import type { Client, Doc, DocManager, DocumentQuery, Ref } from '@hcengineering/core'
import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineering/model-presentation'
import { type NotificationGroup } from '@hcengineering/notification'
import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform'
@@ -144,7 +144,8 @@ export default mergeIds(contactId, contact, {
ChannelTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
ChannelIdentifierProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
SetPersonStore: '' as Resource<(manager: DocManager<any>) => void>,
PersonFilterFunction: '' as Resource<(doc: Doc, target: Doc) => boolean>
PersonFilterFunction: '' as Resource<(doc: Doc, target: Doc) => boolean>,
HideInactive: '' as Resource<(value: any, query: DocumentQuery<Doc>) => DocumentQuery<Doc>>
},
resolver: {
LocationData: '' as Resource<(loc: Location) => Promise<LocationData>>
+15 -1
View File
@@ -422,7 +422,21 @@ export function createModel (builder: Builder): void {
{
attachTo: hr.mixin.Staff,
descriptor: view.viewlet.Table,
config: ['']
config: [''],
viewOptions: {
groupBy: [],
orderBy: [],
other: [
{
key: 'hideInactive',
type: 'toggle',
defaultValue: true,
actionTarget: 'query',
action: contact.function.HideInactive,
label: contact.string.HideInactive
}
]
}
},
hr.viewlet.StaffStats
)
+2 -1
View File
@@ -149,6 +149,7 @@
"WorkspaceStatusIn1Hour": "Za 1 hodinu",
"WorkspaceStatusIn4Hours": "Za 4 hodiny",
"WorkspaceStatusEndOfDay": "Do konce dne",
"WorkspaceStatusPickDate": "Vyberte datum"
"WorkspaceStatusPickDate": "Vyberte datum",
"HideInactive": "Skrýt neaktivní"
}
}
+2 -1
View File
@@ -149,6 +149,7 @@
"WorkspaceStatusIn1Hour": "In 1 Stunde",
"WorkspaceStatusIn4Hours": "In 4 Stunden",
"WorkspaceStatusEndOfDay": "Ende des Tages",
"WorkspaceStatusPickDate": "Datum wählen"
"WorkspaceStatusPickDate": "Datum wählen",
"HideInactive": "Inaktive ausblenden"
}
}
+2 -1
View File
@@ -149,6 +149,7 @@
"WorkspaceStatusIn1Hour": "In 1 hour",
"WorkspaceStatusIn4Hours": "In 4 hours",
"WorkspaceStatusEndOfDay": "End of day",
"WorkspaceStatusPickDate": "Choose date"
"WorkspaceStatusPickDate": "Choose date",
"HideInactive": "Hide inactive"
}
}
+2 -1
View File
@@ -149,6 +149,7 @@
"WorkspaceStatusIn1Hour": "En 1 hora",
"WorkspaceStatusIn4Hours": "En 4 horas",
"WorkspaceStatusEndOfDay": "Fin del día",
"WorkspaceStatusPickDate": "Elegir fecha"
"WorkspaceStatusPickDate": "Elegir fecha",
"HideInactive": "Ocultar inactivos"
}
}
+2 -1
View File
@@ -149,6 +149,7 @@
"WorkspaceStatusIn1Hour": "Dans 1 heure",
"WorkspaceStatusIn4Hours": "Dans 4 heures",
"WorkspaceStatusEndOfDay": "Fin de journée",
"WorkspaceStatusPickDate": "Choisir une date"
"WorkspaceStatusPickDate": "Choisir une date",
"HideInactive": "Masquer les inactifs"
}
}
+2 -1
View File
@@ -149,6 +149,7 @@
"WorkspaceStatusIn1Hour": "Tra 1 ora",
"WorkspaceStatusIn4Hours": "Tra 4 ore",
"WorkspaceStatusEndOfDay": "Fine giornata",
"WorkspaceStatusPickDate": "Scegli data"
"WorkspaceStatusPickDate": "Scegli data",
"HideInactive": "Nascondi inattivi"
}
}
+2 -1
View File
@@ -149,6 +149,7 @@
"WorkspaceStatusIn1Hour": "1時間後",
"WorkspaceStatusIn4Hours": "4時間後",
"WorkspaceStatusEndOfDay": "今日の終わりまで",
"WorkspaceStatusPickDate": "日付を選択"
"WorkspaceStatusPickDate": "日付を選択",
"HideInactive": "非アクティブを非表示"
}
}
+2 -1
View File
@@ -149,6 +149,7 @@
"WorkspaceStatusIn1Hour": "Em 1 hora",
"WorkspaceStatusIn4Hours": "Em 4 horas",
"WorkspaceStatusEndOfDay": "Fim do dia",
"WorkspaceStatusPickDate": "Escolher data"
"WorkspaceStatusPickDate": "Escolher data",
"HideInactive": "Ocultar inativos"
}
}
+2 -1
View File
@@ -149,6 +149,7 @@
"WorkspaceStatusIn1Hour": "Em 1 hora",
"WorkspaceStatusIn4Hours": "Em 4 horas",
"WorkspaceStatusEndOfDay": "Fim do dia",
"WorkspaceStatusPickDate": "Escolher data"
"WorkspaceStatusPickDate": "Escolher data",
"HideInactive": "Ocultar inativos"
}
}
+2 -1
View File
@@ -149,6 +149,7 @@
"WorkspaceStatusIn1Hour": "Через 1 час",
"WorkspaceStatusIn4Hours": "Через 4 часа",
"WorkspaceStatusEndOfDay": "До конца дня",
"WorkspaceStatusPickDate": "Выбрать дату"
"WorkspaceStatusPickDate": "Выбрать дату",
"HideInactive": "Скрыть неактивных"
}
}
+2 -1
View File
@@ -149,6 +149,7 @@
"WorkspaceStatusIn1Hour": "1 saat içinde",
"WorkspaceStatusIn4Hours": "4 saat içinde",
"WorkspaceStatusEndOfDay": "Gün sonu",
"WorkspaceStatusPickDate": "Tarih seç"
"WorkspaceStatusPickDate": "Tarih seç",
"HideInactive": "Etkin olmayanları gizle"
}
}
+2 -1
View File
@@ -149,6 +149,7 @@
"WorkspaceStatusIn1Hour": "1 小时后",
"WorkspaceStatusIn4Hours": "4 小时后",
"WorkspaceStatusEndOfDay": "今天结束",
"WorkspaceStatusPickDate": "选择日期"
"WorkspaceStatusPickDate": "选择日期",
"HideInactive": "隐藏非活跃"
}
}
+3 -1
View File
@@ -150,6 +150,7 @@ import {
getCurrentEmployeePosition,
getPersonTooltip,
grouppingPersonManager,
hideInactive,
permissionsStore,
resolveLocation,
resolveLocationData
@@ -469,7 +470,8 @@ export default async (): Promise<Resources> => ({
ChannelTitleProvider: channelTitleProvider,
ChannelIdentifierProvider: channelIdentifierProvider,
CanResendInvitation: canResendInvitation,
CanMergePersons: canMergePersons
CanMergePersons: canMergePersons,
HideInactive: hideInactive
},
resolver: {
Location: resolveLocation,
+1
View File
@@ -28,6 +28,7 @@ import {
export default mergeIds(contactId, contact, {
string: {
HideInactive: '' as IntlString,
Apply: '' as IntlString,
CreatePerson: '' as IntlString,
OrganizationNamePlaceholder: '' as IntlString,
+4
View File
@@ -886,3 +886,7 @@ export function getPersonByPersonRefStore (
): Readable<Map<Ref<Person>, Readonly<Person>>> {
return contactCacheStoreManager.getPersonByPersonRefStore(personRefs)
}
export function hideInactive (value: any, query: DocumentQuery<Doc>): DocumentQuery<Doc> {
return value === true ? { ...query, active: true } : query
}
@@ -37,7 +37,7 @@
tableToCSV,
showPopup
} from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
import view, { Viewlet, ViewletPreference, type ViewOptions } from '@hcengineering/view'
import { ViewletSelector, ViewletSettingButton } from '@hcengineering/view-resources'
import { getCurrentEmployee } from '@hcengineering/contact'
@@ -65,10 +65,13 @@
let search = ''
let resultQuery: DocumentQuery<Staff> = {}
function updateResultQuery (search: string): void {
resultQuery = search === '' ? {} : { name: { $like: '%' + search + '%' } }
function updateResultQuery (search: string, viewOptions: ViewOptions | undefined): void {
const q: DocumentQuery<Staff> = search === '' ? {} : { name: { $like: '%' + search + '%' } }
resultQuery = viewOptions?.hideInactive !== false ? { ...q, active: true } : q
}
$: updateResultQuery(search, viewOptions)
const query = createQuery()
let descendants: Map<Ref<Department>, Department[]> = new Map<Ref<Department>, Department[]>()
@@ -131,6 +134,7 @@
]
let viewlet: Viewlet | undefined
let viewOptions: ViewOptions | undefined
let preference: ViewletPreference | undefined
let loading = false
@@ -220,29 +224,21 @@
if (result.detail !== undefined) display = result.detail.id
}}
/>
{#if display === 'stats'}
<ViewletSelector
hidden
bind:viewlet
bind:preference
bind:loading
viewletQuery={{ _id: hr.viewlet.StaffStats }}
/>
<ViewletSettingButton bind:viewlet />
{/if}
<ViewletSelector
hidden
bind:viewlet
bind:preference
bind:loading
viewletQuery={{ _id: hr.viewlet.StaffStats }}
/>
<ViewletSettingButton bind:viewOptions bind:viewlet />
{/if}
</svelte:fragment>
<Breadcrumb icon={hr.icon.HR} label={hr.string.Schedule} size={'large'} isCurrent />
<svelte:fragment slot="search">
<SearchInput
bind:value={search}
collapsed
on:change={() => {
updateResultQuery(search)
}}
/>
<SearchInput bind:value={search} collapsed />
</svelte:fragment>
<svelte:fragment slot="actions">
{#if mode === CalendarMode.Month && display === 'stats'}
@@ -14,9 +14,10 @@
-->
<script lang="ts">
import { getName } from '@hcengineering/contact'
import { Avatar } from '@hcengineering/contact-resources'
import contact, { Avatar } from '@hcengineering/contact-resources'
import hr, { Department, Staff } from '@hcengineering/hr'
import { getClient } from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
export let value: Staff
@@ -39,6 +40,9 @@
<div class="flex-col">
<div class="member-title fs-title">
{getName(client.getHierarchy(), value)}
{#if !value.active}
<span class="member-inactive"><Label label={contact.string.Inactive} /></span>
{/if}
</div>
{#await getDepartment(value) then department}
{#if department}
@@ -60,6 +64,12 @@
color: var(--theme-caption-color);
overflow: hidden;
}
.member-inactive {
color: var(--theme-warning-color);
opacity: 0.8;
font-size: 0.75rem;
margin-left: 0.25rem;
}
.member-department {
color: var(--theme-caption-color);
opacity: 0.6;
@@ -250,8 +250,15 @@ export class DocumentContentPage extends DocumentCommonPage {
await this.confirmKickEmployee.click()
}
async toggleHideInactive (): Promise<void> {
await this.page.locator('[data-id="btn-viewOptions"]').click()
await this.page.locator('.antiCard-menu__item', { hasText: 'Hide inactive' }).click()
await this.page.keyboard.press('Escape')
}
async checkIfEmployeeIsKicked (employee: string): Promise<void> {
await this.page.getByRole('link', { name: 'Employee' }).getByRole('button').first().click()
await this.toggleHideInactive()
await expect(this.page.getByText(employee + ' Inactive')).toBeVisible()
}
+2 -1
View File
@@ -84,7 +84,8 @@ test.describe('contact tests', () => {
await contractPage.clickCreateButton()
await contractPage.waitForFormAntiCardDetached()
await contractPage.kickEmployee(first, last)
// In non refactored code, the last assert just checks if the employee does exist, not if it has inactive status
// After kick, employee is inactive and hidden by default. Toggle off to see it.
await contractPage.toggleHideInactive()
await contractPage.expectKickEmployeeShowsInactiveStatus(first, last)
})
@@ -209,6 +209,12 @@ export class ContractPage {
}
}
async toggleHideInactive (): Promise<void> {
await this.page.locator('[data-id="btn-viewOptions"]').click()
await this.page.locator('.antiCard-menu__item', { hasText: 'Hide inactive' }).click()
await this.page.keyboard.press('Escape')
}
// ASSERTIONS
async expectKickEmployeeShowsInactiveStatus (first: string, last: string): Promise<void> {