fix: better public holidays display (#10901)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov
2026-06-05 10:41:42 +07:00
committed by GitHub
parent 3551e8425d
commit ce3e346617
7 changed files with 142 additions and 47 deletions
@@ -74,6 +74,7 @@
const query = createQuery()
let ancestors: Map<Ref<Department>, Ref<Department>[]> = new Map<Ref<Department>, Ref<Department>[]>()
let descendants: Map<Ref<Department>, Department[]> = new Map<Ref<Department>, Department[]>()
let departments: Map<Ref<Department>, Department> = new Map<Ref<Department>, Department>()
@@ -83,6 +84,9 @@
query.query(hr.class.Department, {}, (res) => {
departments.clear()
descendants.clear()
ancestors.clear()
// build descendants and departments
for (const doc of res) {
if (doc.parent !== undefined && doc._id !== hr.ids.Head) {
const current = descendants.get(doc.parent) ?? []
@@ -91,8 +95,28 @@
}
departments.set(doc._id, doc)
}
// build ancestors: for each department, walk up to root
const byId = new Map<Ref<Department>, Ref<Department>>()
for (const doc of res) {
byId.set(doc._id, doc.parent ?? hr.ids.Head)
}
for (const doc of res) {
const list: Ref<Department>[] = []
let parent: Ref<Department> | undefined = doc._id
while (parent !== undefined && parent !== hr.ids.Head) {
parent = byId.get(parent)
if (parent !== undefined) {
list.push(parent)
}
}
ancestors.set(doc._id, list)
}
departments = departments
descendants = descendants
ancestors = ancestors
})
function inc (val: number): void {
@@ -299,6 +323,7 @@
<ScheduleView
{department}
{descendants}
{ancestors}
departmentById={departments}
staffQuery={resultQuery}
{currentDate}
@@ -16,7 +16,7 @@
import { CalendarMode } from '@hcengineering/calendar-resources'
import { Employee, getCurrentEmployee } from '@hcengineering/contact'
import { DocumentQuery, Ref } from '@hcengineering/core'
import { Department, Request, RequestType, Staff, fromTzDate } from '@hcengineering/hr'
import { Department, PublicHoliday, Request, RequestType, Staff, fromTzDate } from '@hcengineering/hr'
import { createQuery, getClient } from '@hcengineering/presentation'
import tracker, { Issue } from '@hcengineering/tracker'
import { Label } from '@hcengineering/ui'
@@ -30,6 +30,7 @@
export let department: Ref<Department>
export let descendants: Map<Ref<Department>, Department[]>
export let ancestors: Map<Ref<Department>, Ref<Department>[]>
export let departmentById: Map<Ref<Department>, Department>
export let currentDate: Date = new Date()
export let mode: CalendarMode
@@ -232,7 +233,7 @@
}
}
)
let holidays = new Map<Ref<Department>, Date[]>()
let holidaysMap = new Map<Ref<Department>, Date[]>()
const holidaysQuery = createQuery()
$: holidaysQuery.query(
hr.class.PublicHoliday,
@@ -241,31 +242,33 @@
'date.year': currentDate.getFullYear()
},
(res) => {
const group = groupBy(res, 'department')
holidays = new Map()
for (const groupKey in group) {
holidays.set(
groupKey as Ref<Department>,
group[groupKey].map((holiday) => new Date(fromTzDate(holiday.date)))
)
}
holidaysMap = toHolidaysMap(res)
}
)
function toHolidaysMap (holidays: PublicHoliday[]): Map<Ref<Department>, Date[]> {
const group = groupBy(holidays, 'department')
const result = new Map()
for (const groupKey in group) {
// ensure unique holiday dates
const dates = new Set<number>()
for (const holiday of group[groupKey]) {
dates.add(fromTzDate(holiday.date))
}
result.set(
groupKey as Ref<Department>,
Array.from(dates).map((date) => new Date(date))
)
}
return result
}
async function getHolidays (month: Date): Promise<Map<Ref<Department>, Date[]>> {
const result = await client.findAll(hr.class.PublicHoliday, {
'date.month': month.getMonth(),
'date.year': month.getFullYear()
})
const group = groupBy(result, 'department')
const rMap = new Map()
for (const groupKey in group) {
rMap.set(
groupKey,
group[groupKey].map((holiday) => new Date(fromTzDate(holiday.date)))
)
}
return rMap
return toHolidaysMap(result)
}
const client = getClient()
@@ -285,15 +288,34 @@
return map
}
let staffDepartmentMap = new Map()
$: getDepartmentsForEmployee(departmentStaff).then((res) => {
$: void getDepartmentsForEmployee(departmentStaff).then((res) => {
staffDepartmentMap = res
})
function getDepartmentHolidays (department: Ref<Department>): Date[] {
const parents = ancestors.get(department) ?? []
const result = []
// get own holidays
const holidays = holidaysMap.get(department) ?? []
result.push(...holidays)
// get ancestor holidays
for (const parent of parents) {
const parentHolidays = holidaysMap.get(parent) ?? []
result.push(...parentHolidays)
}
return result
}
</script>
{#if staffDepartmentMap.size > 0}
{#if mode === CalendarMode.Year}
<YearView {departmentStaff} {employeeRequests} {types} {currentDate} {holidays} {staffDepartmentMap} />
<YearView {departmentStaff} {employeeRequests} {types} {currentDate} {holidaysMap} {staffDepartmentMap} />
{:else if mode === CalendarMode.Month}
{@const holidays = getDepartmentHolidays(department)}
{#if display === 'chart'}
<MonthView
{departmentStaff}
@@ -303,6 +325,7 @@
{editableList}
{currentDate}
{holidays}
{holidaysMap}
{department}
{departmentById}
{staffDepartmentMap}
@@ -314,7 +337,7 @@
{types}
{currentDate}
{timeReports}
{holidays}
{holidaysMap}
{staffDepartmentMap}
{getHolidays}
{preference}
@@ -21,16 +21,46 @@
import hr from '../../plugin'
import DepartmentEditor from '../DepartmentEditor.svelte'
let description: string
let title: string
export let date: Timestamp
export let department: Ref<Department>
const client = getClient()
let existingHoliday: PublicHoliday | undefined = undefined
const dispatch = createEventDispatcher()
async function findHoliday () {
existingHoliday = await client.findOne(hr.class.PublicHoliday, { date: timeToTzDate(date) })
let description: string
let title: string
let existingHoliday: PublicHoliday | undefined = undefined
async function getAncestors (department: Ref<Department>): Promise<Ref<Department>[]> {
const departments = await client.findAll(hr.class.Department, {})
const byId = new Map<Ref<Department>, Ref<Department>>()
for (const doc of departments) {
byId.set(doc._id, doc.parent ?? hr.ids.Head)
}
const ancestors: Ref<Department>[] = []
let parent: Ref<Department> | undefined = department
while (parent !== undefined && parent !== hr.ids.Head) {
parent = byId.get(parent)
if (parent !== undefined) {
ancestors.push(parent)
}
}
return ancestors
}
async function findHoliday (): Promise<void> {
const holidays = await client.findAll(hr.class.PublicHoliday, { date: timeToTzDate(date) })
// look into current department first
let holiday = holidays.find((p) => p.department === department)
if (holiday === undefined) {
// if not found look at parent departments
const ancestors = await getAncestors(department)
holiday = holidays.find((p) => ancestors.includes(p.department))
}
existingHoliday = holiday
if (existingHoliday !== undefined) {
title = existingHoliday.title
description = existingHoliday.description
@@ -38,7 +68,7 @@
}
}
async function saveHoliday () {
async function saveHoliday (): Promise<void> {
if (existingHoliday !== undefined) {
await client.updateDoc(hr.class.PublicHoliday, core.space.Workspace, existingHoliday._id, {
title,
@@ -54,10 +84,16 @@
await client.createDoc(hr.class.PublicHoliday, core.space.Workspace, holiday)
}
}
findHoliday()
function deleteHoliday () {
existingHoliday && client.remove(existingHoliday)
let loading = true
void findHoliday().then(() => {
loading = false
})
function deleteHoliday (): void {
if (existingHoliday !== undefined) {
void client.remove(existingHoliday)
}
dispatch('close')
}
</script>
@@ -67,9 +103,9 @@
on:close
okLabel={existingHoliday ? presentation.string.Save : presentation.string.Ok}
okAction={() => {
saveHoliday()
void saveHoliday()
}}
canSave={true}
canSave={!loading}
on:changeContent
>
<div class="flex-grow mt-4">
@@ -17,10 +17,9 @@
import { Doc, Ref } from '@hcengineering/core'
import type { Request, RequestType, Staff } from '@hcengineering/hr'
import { Department } from '@hcengineering/hr'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Button, DropdownIntlItem, Label, Loading, showPopup, tableToCSV } from '@hcengineering/ui'
import { Label, Loading } from '@hcengineering/ui'
import { BuildModelKey, Viewlet, ViewletPreference } from '@hcengineering/view'
import { TableBrowser, ViewletSelector, ViewletSettingButton } from '@hcengineering/view-resources'
import { TableBrowser } from '@hcengineering/view-resources'
import hr from '../../plugin'
import {
EmployeeReports,
@@ -47,7 +46,7 @@
export let employeeRequests: Map<Ref<Staff>, Request[]>
export let timeReports: Map<Ref<Employee>, EmployeeReports>
export let holidays: Map<Ref<Department>, Date[]> = new Map<Ref<Department>, Date[]>()
export let holidaysMap: Map<Ref<Department>, Date[]> = new Map<Ref<Department>, Date[]>()
export let getHolidays: (month: Date) => Promise<Map<Ref<Department>, Date[]>>
$: month = getStartDate(currentDate.getFullYear(), currentDate.getMonth()) // getMonth(currentDate, currentDate.getMonth())
$: wDays = weekDays(month.getFullYear(), month.getMonth())
@@ -60,7 +59,7 @@
types,
month.getFullYear(),
month.getMonth(),
getHolidayDatesForEmployee(staffDepartmentMap, staff._id, holidays)
getHolidayDatesForEmployee(staffDepartmentMap, staff._id, holidaysMap)
)
return ds.join(' ')
}
@@ -76,7 +76,9 @@
export let editableList: Ref<Employee>[]
export let staffDepartmentMap: Map<Ref<Staff>, Department[]>
export let holidays: Map<Ref<Department>, Date[]>
export let holidays: Date[]
export let holidaysMap: Map<Ref<Department>, Date[]>
const todayDate = new Date()
@@ -191,7 +193,7 @@
if (requests.length === 0) return
const weekend = isWeekend(day)
const holiday =
holidays?.size > 0 && isHoliday(getHolidayDatesForEmployee(staffDepartmentMap, staff._id, holidays), day)
holidaysMap?.size > 0 && isHoliday(getHolidayDatesForEmployee(staffDepartmentMap, staff._id, holidaysMap), day)
if (day && (weekend || holiday) && requests.some((req) => noWeekendHolidayType.includes(req.type))) {
return
}
@@ -307,6 +309,7 @@
{#each values as value}
{@const day = getDay(startDate, value)}
{@const today = areDatesEqual(todayDate, day)}
{@const holiday = isHoliday(holidays, day)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
@@ -321,6 +324,7 @@
<div
class="timeline-day-header__day flex-col-center justify-center"
class:timeline-day-header__day--today={today}
class:timeline-day-header__day--holiday={holiday && !today}
>
{day.getDate()}
</div>
@@ -361,7 +365,7 @@
{@const today = areDatesEqual(todayDate, day)}
{@const weekend = isWeekend(day)}
{@const holiday = isHoliday(
getHolidayDatesForEmployee(staffDepartmentMap, employee._id, holidays),
getHolidayDatesForEmployee(staffDepartmentMap, employee._id, holidaysMap),
day
)}
{@const requests = getRequests(employeeRequests, day, day, employee._id)}
@@ -474,6 +478,12 @@
background-color: #3871e0;
border-radius: 0.375rem;
}
&.timeline-day-header__day--holiday {
color: white;
background-color: #d32f2f;
border-radius: 0.375rem;
}
}
.timeline-day-header__weekday {
@@ -38,7 +38,7 @@
export let employeeRequests: Map<Ref<Staff>, Request[]>
export let holidays: Map<Ref<Department>, Date[]>
export let holidaysMap: Map<Ref<Department>, Date[]>
export let staffDepartmentMap: Map<Ref<Staff>, Department[]>
function getTooltip (requests: Request[]): LabelAndProps | undefined {
@@ -117,7 +117,7 @@
startDate,
endDate,
types,
getHolidayDatesForEmployee(staffDepartmentMap, employee._id, holidays)
getHolidayDatesForEmployee(staffDepartmentMap, employee._id, holidaysMap)
)}
</div>
</td>
@@ -139,7 +139,7 @@
startDate,
endDate,
types,
[...holidays.values()].flat()
[...holidaysMap.values()].flat()
)}
</div>
</td>
+5 -3
View File
@@ -218,14 +218,16 @@ export function getHolidayDatesForEmployee (
const deps = departmentMap.get(employee)
if (deps === undefined) return []
if (holidays.size === 0) return []
const dates = []
const dates = new Map<string, Date>()
for (const dep of deps) {
const depDates = holidays?.get(dep._id)
if (depDates !== undefined) {
dates.push(...depDates)
for (const date of depDates) {
dates.set(`${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`, date)
}
}
}
return dates
return [...dates.values()]
}
export interface EmployeeReports {