UBER-615 Redesign hr schedule month view (#3538)

Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2023-08-01 17:39:26 +07:00 committed by GitHub
parent 746d8a1c41
commit 424c546089
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 637 additions and 430 deletions

View File

@ -59,6 +59,7 @@
--primary-color-skyblue: #93CAF3;
--primary-color-pink: #FA8DA1;
--highlight-blue-01: #0084FF;
--highlight-red: #F96E50;
--highlight-red-hover: #ff967e;
--highlight-red-press: #f96f50bd;
@ -349,9 +350,13 @@
--theme-calendar-today-color: #000;
--theme-calendar-holiday-color: #eb5757;
--theme-calendar-weekend-color: rgba(242, 153, 74, 1);
--theme-calendar-today-bgcolor: rgba(43, 81, 144, .1);
--theme-calendar-today-bgcolor: rgba(51, 157, 255, .1);
--theme-calendar-holiday-bgcolor: rgba(235, 87, 87, .1);
--theme-calendar-weekend-bgcolor: rgba(242, 153, 74, .1);
--theme-calendar-event-available-color: rgba(55, 122, 230, 0.20);
--theme-calendar-event-available-bgcolor: #f6f9fe;
--theme-calendar-event-unavailable-color: rgba(244, 119, 88, 0.20);
--theme-calendar-event-unavailable-bgcolor: #fdece7;
--theme-tooltip-color: #FFF;
--theme-tooltip-bg: #444248;

View File

@ -98,6 +98,7 @@
<span class="text-sm content-dark-color">
<Label label={hr.string.NoMembers} />
</span>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span class="text-sm content-color over-underline" on:click={add}>
<Label label={hr.string.AddMember} />
</span>

View File

@ -1,90 +0,0 @@
<!--
// 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, { Department, Request, RequestType, Staff } from '@hcengineering/hr'
import { getClient } from '@hcengineering/presentation'
import { closeTooltip, getPlatformColor, Icon, isWeekend, showPopup, themeStore } from '@hcengineering/ui'
import { ContextMenu } from '@hcengineering/view-resources'
import { getHolidayDatesForEmployee, isHoliday } from '../utils'
import { Ref } from '@hcengineering/core'
export let requests: Request[]
export let date: Date
export let editable: boolean = false
export let holidays: Map<Ref<Department>, Date[]>
export let employee: Staff
export let departments: Ref<Department>[]
const client = getClient()
export let noWeekendHolidayType: Ref<RequestType>[]
async function getType (request: Request): Promise<RequestType | undefined> {
return await client.findOne(hr.class.RequestType, {
_id: request.type
})
}
function getStyle (type: RequestType, request: Request): string {
let res = `background-color: ${
(isWeekend(date) || isHoliday(getHolidayDatesForEmployee(staffDepartmentMap, employee._id, holidays), date)) &&
noWeekendHolidayType.includes(type._id)
? getPlatformColor(16, $themeStore.dark)
: getPlatformColor(type.color, $themeStore.dark)
};`
if (Math.abs(type.value % 1) === 0.5) {
res += ' height: 50%;'
}
if (!departments.includes(request.space)) res += ' opacity: 0.5;'
return res
}
function click (e: MouseEvent, request: Request) {
if (!editable) return
e.stopPropagation()
e.preventDefault()
closeTooltip()
showPopup(ContextMenu, { object: request }, e.target as HTMLElement)
}
export let staffDepartmentMap: Map<Ref<Staff>, Department[]>
</script>
<div class="w-full h-full relative p-1 flex">
{#each requests as request}
{#await getType(request) then type}
{#if type}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="request flex-center"
class:cursor-pointer={editable}
style={getStyle(type, request)}
on:click={(e) => {
click(e, request)
}}
>
<Icon icon={type.icon} size={Math.abs(type.value % 1) !== 0.5 ? 'large' : 'small'} />
</div>
{/if}
{/await}
{/each}
</div>
<style lang="scss">
.request {
border-radius: 3px;
flex-grow: 1;
// height: 100%;
width: 30px;
}
</style>

View File

@ -282,40 +282,43 @@
})
</script>
{#if staffDepartmentMap.size > 0}
{#if mode === CalendarMode.Year}
<YearView {departmentStaff} {employeeRequests} {types} {currentDate} {holidays} {staffDepartmentMap} />
{:else if mode === CalendarMode.Month}
{#if display === 'chart'}
<MonthView
{departmentStaff}
{employeeRequests}
{types}
{startDate}
{endDate}
{editableList}
{currentDate}
{timeReports}
{holidays}
{department}
{departments}
{staffDepartmentMap}
/>
{:else if display === 'stats'}
<MonthTableView
{departmentStaff}
{employeeRequests}
{types}
{currentDate}
{timeReports}
{holidays}
{staffDepartmentMap}
{getHolidays}
/>
<div class="w-full h-full background-comp-header-color">
{#if staffDepartmentMap.size > 0}
{#if mode === CalendarMode.Year}
<YearView {departmentStaff} {employeeRequests} {types} {currentDate} {holidays} {staffDepartmentMap} />
{:else if mode === CalendarMode.Month}
{#if display === 'chart'}
<MonthView
{departmentStaff}
{employeeRequests}
{types}
{startDate}
{endDate}
{editableList}
{currentDate}
{timeReports}
{holidays}
{department}
{departments}
{departmentById}
{staffDepartmentMap}
/>
{:else if display === 'stats'}
<MonthTableView
{departmentStaff}
{employeeRequests}
{types}
{currentDate}
{timeReports}
{holidays}
{staffDepartmentMap}
{getHolidays}
/>
{/if}
{/if}
{:else}
<div class="flex-center h-full w-full flex-grow fs-title">
<Label label={hr.string.NoEmployeesInDepartment} />
</div>
{/if}
{:else}
<div class="flex-center h-full w-full flex-grow fs-title">
<Label label={hr.string.NoEmployeesInDepartment} />
</div>
{/if}
</div>

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
@ -14,16 +14,14 @@
-->
<script lang="ts">
import { Employee } from '@hcengineering/contact'
import { EmployeePresenter } from '@hcengineering/contact-resources'
import contact from '@hcengineering/contact-resources/src/plugin'
import contact from '@hcengineering/contact'
import { AccountRole, getCurrentAccount, Ref } from '@hcengineering/core'
import type { Department, Request, RequestType, Staff } from '@hcengineering/hr'
import { tzDateCompare, type Department, type Request, type RequestType, type Staff } from '@hcengineering/hr'
import {
areDatesEqual,
day as getDay,
daysInMonth,
eventToHTMLElement,
floorFractionDigits,
getWeekDayName,
isWeekend,
Label,
@ -34,12 +32,35 @@
deviceOptionsStore as deviceInfo
} from '@hcengineering/ui'
import hr from '../../plugin'
import { EmployeeReports, getHolidayDatesForEmployee, getRequests, getTotal, isHoliday } from '../../utils'
import { getHolidayDatesForEmployee, getRequests, isHoliday } from '../../utils'
import CreateRequest from '../CreateRequest.svelte'
import RequestsPopup from '../RequestsPopup.svelte'
import ScheduleRequests from '../ScheduleRequests.svelte'
import ReportsPopup from './ReportsPopup.svelte'
import CreatePublicHoliday from './CreatePublicHoliday.svelte'
import ScheduleRequest from './ScheduleRequest.svelte'
import StaffPresenter from './StaffPresenter.svelte'
const headerHeightRem = 4.375
const eventHeightRem = 1.5
const eventMarginRem = 0.5
const minColWidthRem = 2.5
const minRowHeightRem = 4.375
interface TimelineElement {
request: Request
date: number
dueDate: number
length: number
}
interface TimelineRowTrack {
elements: TimelineElement[]
}
interface TimelineRow {
employee: Staff
requests: Request[]
tracks: TimelineRowTrack[]
}
export let currentDate: Date = new Date()
@ -48,16 +69,92 @@
export let departmentStaff: Staff[]
export let department: Ref<Department>
export let departments: Ref<Department>[]
export let departmentById: Map<Ref<Department>, Department>
export let employeeRequests: Map<Ref<Staff>, Request[]>
export let editableList: Ref<Employee>[]
export let types: Map<Ref<RequestType>, RequestType>
export let timeReports: Map<Ref<Employee>, EmployeeReports>
export let staffDepartmentMap: Map<Ref<Staff>, Department[]>
export let holidays: Map<Ref<Department>, Date[]>
const todayDate = new Date()
function createRequest (e: MouseEvent, date: Date, staff: Staff): void {
const noWeekendHolidayType: Ref<RequestType>[] = [hr.ids.PTO, hr.ids.PTO2, hr.ids.Vacation]
function checkConflict(request1: Request, request2: Request): boolean {
return (
tzDateCompare(request1.tzDate, request2.tzDueDate) <= 0 && tzDateCompare(request1.tzDueDate, request2.tzDate) >= 0
)
}
function getMonthDate(request: Request): number {
const year = currentDate.getFullYear()
const month = currentDate.getMonth()
return request.tzDate.year === year && request.tzDate.month === month ? request.tzDate.day : startDate.getDate()
}
function getMonthDueDate(request: Request): number {
const year = currentDate.getFullYear()
const month = currentDate.getMonth()
return request.tzDueDate.year === year && request.tzDueDate.month === month
? request.tzDueDate.day
: endDate.getDate()
}
function getOrderedEmployeeRequests(employee: Staff): Request[] {
const requests = getRequests(employeeRequests, startDate, endDate, employee._id)
requests.sort((a, b) => {
const res = tzDateCompare(a.tzDate, b.tzDate)
if (res === 0) {
return tzDateCompare(a.tzDueDate, b.tzDueDate)
}
return res
})
return requests
}
function buildTimelineRows(): TimelineRow[] {
const res: TimelineRow[] = []
for (const employee of departmentStaff) {
const tracks: TimelineRowTrack[] = []
const requests = getOrderedEmployeeRequests(employee)
for (const request of requests) {
let found: TimelineRowTrack | undefined = undefined
for (const track of tracks) {
const conflict = track.elements.some((it) => checkConflict(request, it.request))
if (!conflict) {
found = track
break
}
}
if (found === undefined) {
found = { elements: [] }
tracks.push(found)
}
const date = getMonthDate(request)
const dueDate = getMonthDueDate(request)
found.elements.push({
request,
date,
dueDate,
length: dueDate - date + 1
})
}
res.push({ employee, requests, tracks })
}
return res
}
function createRequest(e: MouseEvent, date: Date, staff: Staff): void {
if (!isEditable(staff)) return
const readonly = editableList.length === 1
@ -76,7 +173,7 @@
)
}
function isFutureDate () {
function isFutureDate() {
const today = new Date(Date.now())
return (
currentDate >= today ||
@ -84,20 +181,16 @@
)
}
function isEditable (employee: Staff): boolean {
function isEditable(employee: Staff): boolean {
return editableList.includes(employee._id) && (isFutureDate() || getCurrentAccount().role === AccountRole.Owner)
}
const noWeekendHolidayType: Ref<RequestType>[] = [hr.ids.PTO, hr.ids.PTO2, hr.ids.Vacation]
function getTooltip (requests: Request[], day: Date, staff: Staff): LabelAndProps | undefined {
function getTooltip(requests: Request[], day: Date, staff: Staff): LabelAndProps | undefined {
if (requests.length === 0) return
if (
day &&
(isWeekend(day) ||
(holidays?.size > 0 && isHoliday(getHolidayDatesForEmployee(staffDepartmentMap, staff._id, holidays), day))) &&
requests.some((req) => noWeekendHolidayType.includes(req.type))
) {
const weekend = isWeekend(day)
const holiday =
holidays?.size > 0 && isHoliday(getHolidayDatesForEmployee(staffDepartmentMap, staff._id, holidays), day)
if (day && (weekend || holiday) && requests.some((req) => noWeekendHolidayType.includes(req.type))) {
return
}
return {
@ -106,203 +199,161 @@
}
}
$: values = [...Array(daysInMonth(currentDate)).keys()]
let hoveredIndex: number = -1
let hoveredColumn: number = -1
function findReports (employee: Employee, date: Date, timeReports: Map<Ref<Employee>, EmployeeReports>): number {
const wday = date.getDate()
return floorFractionDigits(
(timeReports.get(employee._id)?.reports ?? [])
.filter((it) => new Date(it.date ?? 0).getDate() === wday)
.reduce((a, b) => a + b.value, 0),
3
)
}
function showReportInfo (employee: Staff, rTime: EmployeeReports | undefined): void {
if (rTime === undefined) {
return
}
showPopup(ReportsPopup, { employee, reports: rTime.reports }, 'top')
}
function setPublicHoliday (date: Date) {
function setPublicHoliday(date: Date) {
showPopup(CreatePublicHoliday, { date, department })
}
export let staffDepartmentMap: Map<Ref<Staff>, Department[]>
export let holidays: Map<Ref<Department>, Date[]>
function getRowHeight(row: TimelineRow): number {
const height = row.tracks.length * (eventHeightRem + eventMarginRem) - eventMarginRem + 2
return Math.max(height, minRowHeightRem)
}
let colWidth: number
$: colWidthRem = colWidth / $deviceInfo.fontSize
function getColumnWidth(gridWidth: number, currentDate: Date): number {
const width = gridWidth / daysInMonth(currentDate)
return Math.max(width, minColWidthRem)
}
export function getCellStyle(): string {
return `width: ${columnWidthRem}rem;`
}
export function getRowStyle(row: TimelineRow): string {
const height = getRowHeight(row)
return `height: ${height}rem;`
}
export function getElementStyle(element: TimelineElement, trackIndex: number): string {
const left = (element.date - 1) * columnWidthRem
const top = trackIndex * (eventHeightRem + eventMarginRem)
const width = columnWidthRem * element.length
return `
left: ${left}rem;
top: ${top}rem;
width: ${width}rem;
`
}
let headerWidth: number
$: headerWidthRem = headerWidth / $deviceInfo.fontSize
let containerWidth: number
$: containerWidthRem = containerWidth / $deviceInfo.fontSize
$: values = [...Array(daysInMonth(currentDate)).keys()]
$: columnWidthRem = getColumnWidth(containerWidthRem - headerWidthRem, currentDate)
let rows: TimelineRow[]
$: departmentStaff, employeeRequests, (rows = buildTimelineRows())
</script>
{#if departmentStaff.length}
<Scroller fade={{ multipler: { top: 3, bottom: 3, left: colWidthRem } }} horizontal>
<table class="scroller-first-column">
<thead class="scroller-thead">
<tr class="scroller-thead__tr">
<th>
<div class="fullfill center">
<Label label={contact.string.Employee} />
</div>
</th>
<th>#</th>
<th>##</th>
{#each values as value, i}
{@const day = getDay(startDate, value)}
<th
class:today={areDatesEqual(todayDate, day)}
class:holiday={isHoliday([...holidays.values()].flat(), day)}
class:weekend={isWeekend(day)}
class:hoveredCell={hoveredColumn === i}
on:mousemove={() => {
hoveredIndex = i
}}
on:mouseleave={() => {
hoveredIndex = -1
}}
on:click={() => isFutureDate() && setPublicHoliday(day)}
>
{getWeekDayName(day, 'short')}
<span>{day.getDate()}</span>
</th>
{/each}
</tr>
</thead>
<tbody>
{#each departmentStaff as employee, row}
{@const requests = employeeRequests.get(employee._id) ?? []}
{@const rTime = timeReports.get(employee._id)}
<tr>
<td bind:clientWidth={colWidth}>
<div class="fullfill">
<EmployeePresenter value={employee} />
{#if rows.length}
{@const dep = departmentById.get(department)}
<Scroller fade={{ multipler: { top: headerHeightRem, bottom: 0, left: headerWidthRem } }} horizontal>
<div bind:clientWidth={containerWidth} class="timeline">
{#key [containerWidthRem, columnWidthRem, headerWidthRem]}
<!-- Resource Header -->
<div bind:clientWidth={headerWidth} class="timeline-header timeline-resource-header">
<div class="timeline-row">
<div class="timeline-resource-cell">
<div class="timeline-resource-header__title">
{dep?.name}
</div>
</td>
<td
class="flex-center p-1 whitespace-nowrap text-center"
class:firstLine={row === 0}
class:lastLine={row === departmentStaff.length - 1}
>
{getTotal(
requests,
startDate,
endDate,
types,
getHolidayDatesForEmployee(staffDepartmentMap, employee._id, holidays)
)}
</td>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<td
class="p-1 text-center whitespace-nowrap cursor-pointer"
on:click={() => showReportInfo(employee, rTime)}
>
{#if rTime !== undefined}
{floorFractionDigits(rTime.value, 3)}
({rTime.tasks.size})
{:else}
0
{/if}
</td>
{#each values as value, i}
<div class="timeline-resource-header__subtitle">
<Label label={contact.string.NumberMembers} params={{ count: rows.length }} />
</div>
</div>
</div>
</div>
<!-- Resource Content -->
<div bind:clientWidth={headerWidth} class="timeline-resource-content">
{#each rows as row}
<div class="timeline-row" style={getRowStyle(row)}>
<div class="timeline-resource-cell">
<StaffPresenter value={row.employee} />
</div>
</div>
{/each}
</div>
<!-- Grid Header -->
<div class="timeline-header timeline-grid-header">
<div class="timeline-row flex">
{#each values as value}
{@const day = getDay(startDate, value)}
{@const requests = getRequests(employeeRequests, day, day, employee._id)}
{@const editable = isEditable(employee)}
{@const tooltipValue = getTooltip(requests, day, employee)}
{@const ww = findReports(employee, day, timeReports)}
{#key [tooltipValue, editable]}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<td
class="w-9 max-w-9 min-w-9"
class:today={areDatesEqual(todayDate, day)}
class:holiday={isHoliday(getHolidayDatesForEmployee(staffDepartmentMap, employee._id, holidays), day)}
class:weekend={isWeekend(day) ||
isHoliday(getHolidayDatesForEmployee(staffDepartmentMap, employee._id, holidays), day)}
class:cursor-pointer={editable}
class:hovered={i === hoveredIndex}
class:firstLine={row === 0}
class:lastLine={row === departmentStaff.length - 1}
use:tooltip={tooltipValue}
on:click={(e) => createRequest(e, day, employee)}
on:mousemove={() => {
hoveredColumn = i
}}
on:mouseleave={() => {
hoveredColumn = -1
}}
{@const today = areDatesEqual(todayDate, day)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="timeline-cell timeline-day-header flex-col-center justify-center"
style={getCellStyle()}
on:click={() => isFutureDate() && setPublicHoliday(day)}
>
<div
class="timeline-day-header__day flex-col-center justify-center"
class:timeline-day-header__day--today={today}
>
<div class:worked={ww > 0} class="h-full w-full">
{#if requests.length}
<ScheduleRequests
{employeeRequests}
{departments}
{requests}
{editable}
date={day}
{noWeekendHolidayType}
{holidays}
{employee}
{staffDepartmentMap}
/>
{day.getDate()}
</div>
<div class="timeline-day-header__weekday">{getWeekDayName(day, 'short')}</div>
</div>
{/each}
</div>
</div>
<!-- Grid Content -->
<div class="timeline-grid-content timeline-grid-bg">
{#each rows as row}
{@const employee = row.employee}
{@const tracks = row.tracks}
{@const editable = isEditable(employee)}
<div class="timeline-row flex" style={getRowStyle(row)}>
<div class="timeline-events">
{#each tracks as track, trackIndex}
{#each track.elements as element}
{@const request = element.request}
<div class="timeline-event-wrapper" style={getElementStyle(element, trackIndex)}>
<ScheduleRequest {request} {editable} shouldShowDescription={element.length > 1} />
</div>
{/each}
{/each}
</div>
{#each values as value, i}
{@const day = getDay(startDate, value)}
{@const today = areDatesEqual(todayDate, day)}
{@const weekend = isWeekend(day)}
{@const holiday = isHoliday(
getHolidayDatesForEmployee(staffDepartmentMap, employee._id, holidays),
day
)}
{@const requests = getRequests(employeeRequests, day, day, employee._id)}
{@const tooltipValue = getTooltip(requests, day, employee)}
{#key [tooltipValue, editable]}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="timeline-cell"
class:timeline-cell--today={today}
class:timeline-cell--weekend={weekend}
class:timeline-cell--holiday={holiday}
style={getCellStyle()}
use:tooltip={tooltipValue}
on:click={(e) => createRequest(e, day, employee)}
>
{#if today}
<div class="timeline-cell-today-marker" />
{/if}
</div>
</td>
{/key}
{/each}
</tr>
{/each}
</tbody>
<tfoot class="scroller-tfoot">
<tr>
<td class="summary">
<div class="fullfill">
<Label label={hr.string.Summary} />
{/key}
{/each}
</div>
</td>
<td class="flex-center p-1 whitespace-nowrap text-center summary">
{getTotal(
Array.from(employeeRequests.values()).flat(),
startDate,
endDate,
types,
[...holidays.values()].flat()
)}
</td>
<td class="p-1 text-center summary">
{floorFractionDigits(
Array.from(timeReports.values())
.flat()
.reduce((a, b) => a + b.value, 0),
3
)}
</td>
{#each values as value, i}
{@const day = getDay(startDate, value)}
<td
class="p-1 text-center summary"
class:hovered={i === hoveredIndex}
class:holiday={isHoliday(
getHolidayDatesForEmployee(staffDepartmentMap, departmentStaff[0]._id, holidays),
day
)}
class:weekend={isWeekend(day)}
class:today={areDatesEqual(todayDate, day)}
on:mousemove={() => {
hoveredColumn = i
}}
on:mouseleave={() => {
hoveredColumn = -1
}}
>
{getTotal([...employeeRequests.values()].flat(), day, day, types, [...holidays.values()].flat())}
</td>
{/each}
</tr>
</tfoot>
</table>
</div>
{/key}
</div>
</Scroller>
{:else}
<div class="flex-center h-full w-full flex-grow fs-title">
@ -311,107 +362,162 @@
{/if}
<style lang="scss">
table {
position: relative;
$timeline-row-height: 4.375rem;
$timeline-header-height: 4.5rem;
$timeline-column-width: 2rem;
$timeline-bg-color: var(--theme-comp-header-color);
$timeline-border-color: var(--theme-bg-divider-color);
$timeline-border: 1px solid $timeline-border-color;
.timeline {
width: 100%;
display: grid;
grid-auto-flow: column;
grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr;
}
td,
th {
width: 2rem;
min-width: 1.5rem;
border: none;
&:first-child {
width: 15rem;
}
}
th {
flex-shrink: 0;
padding: 0;
height: 3rem;
min-height: 3rem;
max-height: 3rem;
text-transform: uppercase;
font-weight: 500;
font-size: 0.75rem;
line-height: 105%;
color: var(--theme-halfcontent-color);
box-shadow: inset 0 -1px 0 0 var(--theme-divider-color);
user-select: none;
cursor: pointer;
.timeline-header {
height: $timeline-row-height;
background-color: $timeline-bg-color;
span {
display: block;
font-weight: 600;
font-size: 1rem;
}
&.today {
color: var(--theme-calendar-today-color);
}
&.holiday:not(.today) {
color: var(--theme-calendar-holiday-color);
}
&.weekend:not(.today) {
color: var(--theme-calendar-weekend-color);
}
&.hoveredCell {
background-color: var(--theme-button-pressed);
}
}
td {
height: 3.5rem;
border: none;
color: var(--caption-color);
&.today {
background-color: var(--theme-calendar-today-bgcolor);
}
&.summary {
font-weight: 600;
}
&.weekend:not(.today) {
background-color: var(--theme-calendar-weekend-bgcolor);
}
&.holiday:not(.today) {
background-color: var(--theme-calendar-holiday-bgcolor);
}
}
td:not(:last-child) {
border-right: 1px solid var(--theme-bg-divider-color);
}
tbody tr:not(:last-child),
thead th:first-child .fullfill,
tfoot tr {
border-bottom: 1px solid var(--theme-bg-divider-color);
}
tfoot tr,
tfoot tr td:first-child .fullfill {
box-shadow: inset 0 1px 0 0 var(--theme-divider-color);
}
tfoot tr,
tfoot tr td {
height: 3rem;
}
tr.scroller-thead__tr:not(:last-child) {
border-right: 1px solid var(--theme-bg-divider-color);
}
.hovered {
position: relative;
&::after {
position: absolute;
content: '';
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--theme-caption-color);
opacity: 0.15;
}
.timeline-row {
height: $timeline-row-height;
min-height: $timeline-row-height;
}
}
.worked {
background-color: var(--highlight-select);
.timeline-header {
position: sticky;
top: 0;
z-index: 1;
&.timeline-resource-header {
left: 0;
z-index: 2;
}
}
.timeline-resource-header__title {
white-space: nowrap;
font-size: 0.875rem;
font-weight: 500;
}
.timeline-resource-header__subtitle {
white-space: nowrap;
font-size: 0.6875rem;
font-weight: 400;
line-height: 1.25rem;
opacity: 0.4;
}
.timeline-resource-content {
background-color: $timeline-bg-color;
position: sticky;
left: 0;
z-index: 1;
}
.timeline-day-header {
cursor: pointer;
.timeline-day-header__day {
width: 1.3125rem;
height: 1.3125rem;
font-size: 0.8125rem;
font-weight: 500;
&.timeline-day-header__day--today {
color: white;
background-color: #3871e0;
border-radius: 0.375rem;
}
}
.timeline-day-header__weekday {
font-size: 0.6875rem;
font-weight: 400;
line-height: 1.25rem;
opacity: 0.4;
}
}
.timeline-grid-bg {
background-image: linear-gradient(
135deg,
$timeline-border-color 8.33%,
$timeline-bg-color 8.33%,
$timeline-bg-color 50%,
$timeline-border-color 50%,
$timeline-border-color 58.33%,
$timeline-bg-color 58.33%,
$timeline-bg-color 100%
);
background-size: 6px 6px;
}
.timeline-row {
position: relative;
height: $timeline-row-height;
border-bottom: $timeline-border;
}
.timeline-events {
position: absolute;
width: 100%;
top: 1em;
bottom: 1em;
pointer-events: none;
}
.timeline-cell {
border-right: $timeline-border;
width: $timeline-column-width;
height: 100%;
&:not(.timeline-cell--weekend, .timeline-cell--holiday) {
background-color: $timeline-bg-color;
}
&.timeline-cell--holiday {
background-color: transparent;
}
&.timeline-cell--weekend {
background-color: transparent;
}
&.timeline-cell--today {
background-color: $timeline-bg-color;
}
.timeline-cell-today-marker {
width: 100%;
height: 100%;
background-color: var(--theme-calendar-today-bgcolor);
pointer-events: none;
}
}
.timeline-resource-cell {
border-right: $timeline-border;
width: 100%;
height: 100%;
padding: 1rem 2rem;
}
.timeline-event-wrapper {
position: absolute;
height: 1.5rem;
padding-left: 0.125rem;
padding-right: 0.125rem;
pointer-events: all;
}
</style>

View File

@ -0,0 +1,99 @@
<!--
// 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 hr, { Request, RequestType } from '@hcengineering/hr'
import { getClient } from '@hcengineering/presentation'
import { closeTooltip, Icon, Label, showPopup } from '@hcengineering/ui'
import { ContextMenu, HTMLPresenter } from '@hcengineering/view-resources'
export let request: Request
export let editable: boolean = false
export let shouldShowDescription: boolean = true
const client = getClient()
async function getType (request: Request): Promise<RequestType | undefined> {
return await client.findOne(hr.class.RequestType, {
_id: request.type
})
}
function isAvailable (type: RequestType, request: Request): boolean {
// TODO Move availability to the Request model
const available = type.value >= 0
return available
}
function click (e: MouseEvent, request: Request) {
if (!editable) return
e.stopPropagation()
e.preventDefault()
closeTooltip()
showPopup(ContextMenu, { object: request }, e.target as HTMLElement)
}
</script>
{#await getType(request) then type}
{#if type}
{@const available = isAvailable(type, request)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="request flex-row-center flex-gap-2"
class:flex-center={!shouldShowDescription}
class:request--available={available}
class:request--unavailable={!available}
class:cursor-pointer={editable}
on:click={(e) => click(e, request)}
>
<Icon
icon={type.icon}
size={'small'}
fill={available ? 'var(--highlight-blue-01)' : 'var(--primary-color-orange-02)'}
/>
{#if shouldShowDescription}
<span class="overflow-label">
{#if request.description !== ''}
<HTMLPresenter value={request.description} />
{:else if type}
<Label label={type.label} />
{/if}
</span>
{/if}
</div>
{/if}
{/await}
<style lang="scss">
.request {
border-radius: 0.25rem;
height: 100%;
width: 100%;
padding: 0rem 0.5rem;
overflow: hidden;
}
.request--available {
border: 1px solid var(--theme-calendar-event-available-color);
background-color: var(--theme-calendar-event-available-bgcolor);
}
.request--unavailable {
border: 1px solid var(--theme-calendar-event-unavailable-color);
background-color: var(--theme-calendar-event-unavailable-bgcolor);
}
</style>

View File

@ -0,0 +1,68 @@
<!--
// 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 { getName } from '@hcengineering/contact'
import { Avatar } from '@hcengineering/contact-resources'
import hr, { Department, Staff } from '@hcengineering/hr'
import { getClient } from '@hcengineering/presentation'
import { DocNavLink } from '@hcengineering/view-resources'
export let value: Staff
const client = getClient()
async function getDepartment (staff: Staff): Promise<Department | undefined> {
return await client.findOne(hr.class.Department, {
_id: staff.department
})
}
</script>
{#if value}
<DocNavLink object={value}>
<div class="flex-row-center">
<div class="member-icon mr-2">
<Avatar size={'medium'} avatar={value.avatar} />
</div>
<div class="flex-col">
<div class="member-title fs-title">
{getName(value)}
</div>
{#await getDepartment(value) then department}
{#if department}
<div class="member-department text-md">
{department.name}
</div>
{/if}
{/await}
</div>
</div>
</DocNavLink>
{/if}
<style lang="scss">
.member-icon {
color: var(--theme-dark-color);
}
.member-title {
color: var(--theme-caption-color);
overflow: hidden;
}
.member-department {
color: var(--theme-caption-color);
opacity: 0.6;
overflow: hidden;
}
</style>

View File

@ -39,3 +39,18 @@ export function fromTzDate (tzDate: TzDate): number {
export function tzDateEqual (tzDate: TzDate, tzDate2: TzDate): boolean {
return tzDate.year === tzDate2.year && tzDate.month === tzDate2.month && tzDate.day === tzDate2.day
}
/**
* @public
*/
export function tzDateCompare (tzDate1: TzDate, tzDate2: TzDate): number {
if (tzDate1.year === tzDate2.year) {
if (tzDate1.month === tzDate2.month) {
return tzDate1.day - tzDate2.day
} else {
return tzDate1.month - tzDate2.month
}
} else {
return tzDate1.year - tzDate2.year
}
}