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-skyblue: #93CAF3;
--primary-color-pink: #FA8DA1; --primary-color-pink: #FA8DA1;
--highlight-blue-01: #0084FF;
--highlight-red: #F96E50; --highlight-red: #F96E50;
--highlight-red-hover: #ff967e; --highlight-red-hover: #ff967e;
--highlight-red-press: #f96f50bd; --highlight-red-press: #f96f50bd;
@ -349,9 +350,13 @@
--theme-calendar-today-color: #000; --theme-calendar-today-color: #000;
--theme-calendar-holiday-color: #eb5757; --theme-calendar-holiday-color: #eb5757;
--theme-calendar-weekend-color: rgba(242, 153, 74, 1); --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-holiday-bgcolor: rgba(235, 87, 87, .1);
--theme-calendar-weekend-bgcolor: rgba(242, 153, 74, .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-color: #FFF;
--theme-tooltip-bg: #444248; --theme-tooltip-bg: #444248;

View File

@ -98,6 +98,7 @@
<span class="text-sm content-dark-color"> <span class="text-sm content-dark-color">
<Label label={hr.string.NoMembers} /> <Label label={hr.string.NoMembers} />
</span> </span>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span class="text-sm content-color over-underline" on:click={add}> <span class="text-sm content-color over-underline" on:click={add}>
<Label label={hr.string.AddMember} /> <Label label={hr.string.AddMember} />
</span> </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> </script>
{#if staffDepartmentMap.size > 0} <div class="w-full h-full background-comp-header-color">
{#if mode === CalendarMode.Year} {#if staffDepartmentMap.size > 0}
<YearView {departmentStaff} {employeeRequests} {types} {currentDate} {holidays} {staffDepartmentMap} /> {#if mode === CalendarMode.Year}
{:else if mode === CalendarMode.Month} <YearView {departmentStaff} {employeeRequests} {types} {currentDate} {holidays} {staffDepartmentMap} />
{#if display === 'chart'} {:else if mode === CalendarMode.Month}
<MonthView {#if display === 'chart'}
{departmentStaff} <MonthView
{employeeRequests} {departmentStaff}
{types} {employeeRequests}
{startDate} {types}
{endDate} {startDate}
{editableList} {endDate}
{currentDate} {editableList}
{timeReports} {currentDate}
{holidays} {timeReports}
{department} {holidays}
{departments} {department}
{staffDepartmentMap} {departments}
/> {departmentById}
{:else if display === 'stats'} {staffDepartmentMap}
<MonthTableView />
{departmentStaff} {:else if display === 'stats'}
{employeeRequests} <MonthTableView
{types} {departmentStaff}
{currentDate} {employeeRequests}
{timeReports} {types}
{holidays} {currentDate}
{staffDepartmentMap} {timeReports}
{getHolidays} {holidays}
/> {staffDepartmentMap}
{getHolidays}
/>
{/if}
{/if} {/if}
{:else}
<div class="flex-center h-full w-full flex-grow fs-title">
<Label label={hr.string.NoEmployeesInDepartment} />
</div>
{/if} {/if}
{:else} </div>
<div class="flex-center h-full w-full flex-grow fs-title">
<Label label={hr.string.NoEmployeesInDepartment} />
</div>
{/if}

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"); // 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 // you may not use this file except in compliance with the License. You may
@ -14,16 +14,14 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Employee } from '@hcengineering/contact' import { Employee } from '@hcengineering/contact'
import { EmployeePresenter } from '@hcengineering/contact-resources' import contact from '@hcengineering/contact'
import contact from '@hcengineering/contact-resources/src/plugin'
import { AccountRole, getCurrentAccount, Ref } from '@hcengineering/core' 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 { import {
areDatesEqual, areDatesEqual,
day as getDay, day as getDay,
daysInMonth, daysInMonth,
eventToHTMLElement, eventToHTMLElement,
floorFractionDigits,
getWeekDayName, getWeekDayName,
isWeekend, isWeekend,
Label, Label,
@ -34,12 +32,35 @@
deviceOptionsStore as deviceInfo deviceOptionsStore as deviceInfo
} from '@hcengineering/ui' } from '@hcengineering/ui'
import hr from '../../plugin' import hr from '../../plugin'
import { EmployeeReports, getHolidayDatesForEmployee, getRequests, getTotal, isHoliday } from '../../utils' import { getHolidayDatesForEmployee, getRequests, isHoliday } from '../../utils'
import CreateRequest from '../CreateRequest.svelte' import CreateRequest from '../CreateRequest.svelte'
import RequestsPopup from '../RequestsPopup.svelte' import RequestsPopup from '../RequestsPopup.svelte'
import ScheduleRequests from '../ScheduleRequests.svelte'
import ReportsPopup from './ReportsPopup.svelte'
import CreatePublicHoliday from './CreatePublicHoliday.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() export let currentDate: Date = new Date()
@ -48,16 +69,92 @@
export let departmentStaff: Staff[] export let departmentStaff: Staff[]
export let department: Ref<Department> 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 employeeRequests: Map<Ref<Staff>, Request[]>
export let editableList: Ref<Employee>[] 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() 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 if (!isEditable(staff)) return
const readonly = editableList.length === 1 const readonly = editableList.length === 1
@ -76,7 +173,7 @@
) )
} }
function isFutureDate () { function isFutureDate() {
const today = new Date(Date.now()) const today = new Date(Date.now())
return ( return (
currentDate >= today || 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) 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 (requests.length === 0) return
if ( const weekend = isWeekend(day)
day && const holiday =
(isWeekend(day) || holidays?.size > 0 && isHoliday(getHolidayDatesForEmployee(staffDepartmentMap, staff._id, holidays), day)
(holidays?.size > 0 && isHoliday(getHolidayDatesForEmployee(staffDepartmentMap, staff._id, holidays), day))) && if (day && (weekend || holiday) && requests.some((req) => noWeekendHolidayType.includes(req.type))) {
requests.some((req) => noWeekendHolidayType.includes(req.type))
) {
return return
} }
return { return {
@ -106,203 +199,161 @@
} }
} }
$: values = [...Array(daysInMonth(currentDate)).keys()] function setPublicHoliday(date: Date) {
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) {
showPopup(CreatePublicHoliday, { date, department }) showPopup(CreatePublicHoliday, { date, department })
} }
export let staffDepartmentMap: Map<Ref<Staff>, Department[]> function getRowHeight(row: TimelineRow): number {
export let holidays: Map<Ref<Department>, Date[]> const height = row.tracks.length * (eventHeightRem + eventMarginRem) - eventMarginRem + 2
return Math.max(height, minRowHeightRem)
}
let colWidth: number function getColumnWidth(gridWidth: number, currentDate: Date): number {
$: colWidthRem = colWidth / $deviceInfo.fontSize 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> </script>
{#if departmentStaff.length} {#if rows.length}
<Scroller fade={{ multipler: { top: 3, bottom: 3, left: colWidthRem } }} horizontal> {@const dep = departmentById.get(department)}
<table class="scroller-first-column">
<thead class="scroller-thead"> <Scroller fade={{ multipler: { top: headerHeightRem, bottom: 0, left: headerWidthRem } }} horizontal>
<tr class="scroller-thead__tr"> <div bind:clientWidth={containerWidth} class="timeline">
<th> {#key [containerWidthRem, columnWidthRem, headerWidthRem]}
<div class="fullfill center"> <!-- Resource Header -->
<Label label={contact.string.Employee} /> <div bind:clientWidth={headerWidth} class="timeline-header timeline-resource-header">
</div> <div class="timeline-row">
</th> <div class="timeline-resource-cell">
<th>#</th> <div class="timeline-resource-header__title">
<th>##</th> {dep?.name}
{#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} />
</div> </div>
</td> <div class="timeline-resource-header__subtitle">
<td <Label label={contact.string.NumberMembers} params={{ count: rows.length }} />
class="flex-center p-1 whitespace-nowrap text-center" </div>
class:firstLine={row === 0} </div>
class:lastLine={row === departmentStaff.length - 1} </div>
> </div>
{getTotal(
requests, <!-- Resource Content -->
startDate, <div bind:clientWidth={headerWidth} class="timeline-resource-content">
endDate, {#each rows as row}
types, <div class="timeline-row" style={getRowStyle(row)}>
getHolidayDatesForEmployee(staffDepartmentMap, employee._id, holidays) <div class="timeline-resource-cell">
)} <StaffPresenter value={row.employee} />
</td> </div>
<!-- svelte-ignore a11y-click-events-have-key-events --> </div>
<td {/each}
class="p-1 text-center whitespace-nowrap cursor-pointer" </div>
on:click={() => showReportInfo(employee, rTime)}
> <!-- Grid Header -->
{#if rTime !== undefined} <div class="timeline-header timeline-grid-header">
{floorFractionDigits(rTime.value, 3)} <div class="timeline-row flex">
({rTime.tasks.size}) {#each values as value}
{:else}
0
{/if}
</td>
{#each values as value, i}
{@const day = getDay(startDate, value)} {@const day = getDay(startDate, value)}
{@const requests = getRequests(employeeRequests, day, day, employee._id)} {@const today = areDatesEqual(todayDate, day)}
{@const editable = isEditable(employee)} <!-- svelte-ignore a11y-click-events-have-key-events -->
{@const tooltipValue = getTooltip(requests, day, employee)} <div
{@const ww = findReports(employee, day, timeReports)} class="timeline-cell timeline-day-header flex-col-center justify-center"
{#key [tooltipValue, editable]} style={getCellStyle()}
<!-- svelte-ignore a11y-click-events-have-key-events --> on:click={() => isFutureDate() && setPublicHoliday(day)}
<td >
class="w-9 max-w-9 min-w-9" <div
class:today={areDatesEqual(todayDate, day)} class="timeline-day-header__day flex-col-center justify-center"
class:holiday={isHoliday(getHolidayDatesForEmployee(staffDepartmentMap, employee._id, holidays), day)} class:timeline-day-header__day--today={today}
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
}}
> >
<div class:worked={ww > 0} class="h-full w-full"> {day.getDate()}
{#if requests.length} </div>
<ScheduleRequests <div class="timeline-day-header__weekday">{getWeekDayName(day, 'short')}</div>
{employeeRequests} </div>
{departments} {/each}
{requests} </div>
{editable} </div>
date={day}
{noWeekendHolidayType} <!-- Grid Content -->
{holidays} <div class="timeline-grid-content timeline-grid-bg">
{employee} {#each rows as row}
{staffDepartmentMap} {@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} {/if}
</div> </div>
</td> {/key}
{/key} {/each}
{/each}
</tr>
{/each}
</tbody>
<tfoot class="scroller-tfoot">
<tr>
<td class="summary">
<div class="fullfill">
<Label label={hr.string.Summary} />
</div> </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} {/each}
</tr> </div>
</tfoot> {/key}
</table> </div>
</Scroller> </Scroller>
{:else} {:else}
<div class="flex-center h-full w-full flex-grow fs-title"> <div class="flex-center h-full w-full flex-grow fs-title">
@ -311,107 +362,162 @@
{/if} {/if}
<style lang="scss"> <style lang="scss">
table { $timeline-row-height: 4.375rem;
position: relative; $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%; width: 100%;
display: grid;
grid-auto-flow: column;
grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr;
}
td, .timeline-header {
th { height: $timeline-row-height;
width: 2rem; background-color: $timeline-bg-color;
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;
span { .timeline-row {
display: block; height: $timeline-row-height;
font-weight: 600; min-height: $timeline-row-height;
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;
}
} }
} }
.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> </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 { export function tzDateEqual (tzDate: TzDate, tzDate2: TzDate): boolean {
return tzDate.year === tzDate2.year && tzDate.month === tzDate2.month && tzDate.day === tzDate2.day 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
}
}