Merge pull request #8003 from opf/feature/timelog_widgets_scaling

Feature/timelog widgets scaling
pull/8018/head
ulferts 5 years ago committed by GitHub
commit 30d5d90c80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      frontend/src/app/modules/calendar/te-calendar/te-calendar.component.sass
  2. 118
      frontend/src/app/modules/calendar/te-calendar/te-calendar.component.ts
  3. 7
      frontend/src/app/modules/calendar/te-calendar/te-calendar.template.html

@ -52,6 +52,8 @@
opacity: 0
background: none
border: none
display: flex
align-items: center
&:hover
opacity: 1
@ -59,13 +61,14 @@
background: #EAEAEA
.fc-content
position: sticky
color: black
width: 100%
top: 200px
font-weight: normal
font-size: 1.5rem
&.-prohibited
cursor: not-allowed
.te-calendar--time-entry
.fc-content
height: 100%
@ -76,7 +79,7 @@
height: 2em
z-index: 5
&.fc-short
&.-no-fadeout
.fc-fadeout
display: none

@ -44,6 +44,7 @@ interface CalendarMoveEvent {
const TIME_ENTRY_CLASS_NAME = 'te-calendar--time-entry';
const DAY_SUM_CLASS_NAME = 'te-calendar--day-sum';
const ADD_ENTRY_CLASS_NAME = 'te-calendar--add-entry';
const ADD_ENTRY_PROHIBITED_CLASS_NAME = '-prohibited';
@Component({
templateUrl: './te-calendar.template.html',
@ -63,6 +64,12 @@ export class TimeEntryCalendarComponent implements OnInit, OnDestroy, AfterViewI
@Input() static:boolean = false;
@Output() entries = new EventEmitter<CollectionResource<TimeEntryResource>>();
// Not used by the calendar but rather is the maximum/minimum of the graph.
public minHour = 1;
public maxHour = 12;
public labelIntervalHours = 2;
public scaleRatio = 1;
public calendarPlugins = [timeGrid, interactionPlugin];
public calendarEvents:Function;
public calendarHeader:ToolbarInput|boolean = {
@ -70,14 +77,15 @@ export class TimeEntryCalendarComponent implements OnInit, OnDestroy, AfterViewI
center: 'title',
left: 'prev,next today'
};
public calendarSlotLabelFormat = (info:any) => 24 - info.date.hour;
public calendarScrollTime = '24:00:00';
public calendarContentHeight = 545;
public calendarSlotLabelFormat = (info:any) => (this.maxHour - info.date.hour) / this.scaleRatio;
public calendarSlotLabelInterval = `${this.labelIntervalHours}:00:00`;
public calendarContentHeight = 605;
public calendarAllDaySlot = false;
public calendarAllDayText = '';
public calendarDisplayEventTime = false;
public calendarSlotEventOverlap = false;
public calendarEditable = false;
public calendarMinTime = `${this.minHour - 1}:00:00`;
public calendarMaxTime = `${this.maxHour}:00:00`;
public calendarEventOverlap = (stillEvent:any) => !stillEvent.classNames.includes(TIME_ENTRY_CLASS_NAME);
protected memoizedTimeEntries:{start:Date, end:Date, entries:Promise<CollectionResource<TimeEntryResource>>};
@ -151,24 +159,44 @@ export class TimeEntryCalendarComponent implements OnInit, OnDestroy, AfterViewI
}
private buildEntries(entries:TimeEntryResource[], fetchInfo:{ start:Date, end:Date }) {
this.setRatio(entries);
return this.buildTimeEntryEntries(entries)
.concat(this.buildAuxEntries(entries, fetchInfo));
}
private setRatio(entries:TimeEntryResource[]) {
let dateSums = this.calculateDateSums(entries);
let maxHours = Math.max(...Object.values(dateSums), 0);
let oldRatio = this.scaleRatio;
if (maxHours > this.maxHour - this.minHour) {
this.scaleRatio = this.smallerSuitableRatio((this.maxHour - this.minHour) / maxHours);
} else {
this.scaleRatio = 1;
}
if (oldRatio !== this.scaleRatio) {
this.ucCalendar.getApi().render();
}
}
private buildTimeEntryEntries(entries:TimeEntryResource[]) {
let hoursDistribution:{ [key:string]:Moment } = {};
return entries.map((entry) => {
let start:Moment;
let end:Moment;
let hours = this.timezone.toHours(entry.hours);
let hours = this.timezone.toHours(entry.hours) * this.scaleRatio;
if (hoursDistribution[entry.spentOn]) {
start = hoursDistribution[entry.spentOn].clone().subtract(hours, 'h');
end = hoursDistribution[entry.spentOn].clone();
} else {
start = moment(entry.spentOn).add(24 - hours, 'h');
end = moment(entry.spentOn).add(24, 'h');
start = moment(entry.spentOn).add(this.maxHour - hours, 'h');
end = moment(entry.spentOn).add(this.maxHour, 'h');
}
hoursDistribution[entry.spentOn] = start;
@ -180,17 +208,7 @@ export class TimeEntryCalendarComponent implements OnInit, OnDestroy, AfterViewI
}
private buildAuxEntries(entries:TimeEntryResource[], fetchInfo:{ start:Date, end:Date }) {
let dateSums:{ [key:string]:number } = {};
entries.forEach((entry) => {
let hours = this.timezone.toHours(entry.hours);
if (dateSums[entry.spentOn]) {
dateSums[entry.spentOn] += hours;
} else {
dateSums[entry.spentOn] = hours;
}
});
let dateSums = this.calculateDateSums(entries);
let calendarEntries:EventInput[] = [];
@ -199,7 +217,7 @@ export class TimeEntryCalendarComponent implements OnInit, OnDestroy, AfterViewI
calendarEntries.push(this.sumEntry(m, duration));
if (this.memoizedCreateAllowed && duration < 24) {
if (this.memoizedCreateAllowed) {
calendarEntries.push(this.addEntry(m, duration));
}
}
@ -207,17 +225,41 @@ export class TimeEntryCalendarComponent implements OnInit, OnDestroy, AfterViewI
return calendarEntries;
}
private calculateDateSums(entries:TimeEntryResource[]) {
let dateSums:{ [key:string]:number } = {};
entries.forEach((entry) => {
let hours = this.timezone.toHours(entry.hours);
if (dateSums[entry.spentOn]) {
dateSums[entry.spentOn] += hours;
} else {
dateSums[entry.spentOn] = hours;
}
});
return dateSums;
}
protected timeEntry(entry:TimeEntryResource, hours:number, start:Moment, end:Moment) {
const color = this.colors.toHsl(this.entryName(entry));
let classNames = [TIME_ENTRY_CLASS_NAME];
let span = end.diff(start, 'm');
if (span < 40) {
classNames.push('-no-fadeout');
}
return {
title: hours < 0.5 ? '' : this.entryName(entry),
title: span < 20 ? '' : this.entryName(entry),
startEditable: !!entry.update,
start: start.format(),
end: end.format(),
backgroundColor: color,
borderColor: color,
classNames: TIME_ENTRY_CLASS_NAME,
classNames: classNames,
entry: entry
};
}
@ -225,18 +267,26 @@ export class TimeEntryCalendarComponent implements OnInit, OnDestroy, AfterViewI
protected sumEntry(date:Moment, duration:number) {
return {
title: this.i18n.t('js.units.hour', { count: this.formatNumber(duration) }),
start: date.clone().add(24 - Math.min(duration, 23.5) - 0.5, 'h').format(),
end: date.clone().add(24 - Math.min(duration, 23.5), 'h').format(),
start: date.clone().add(this.maxHour - Math.min(duration * this.scaleRatio, this.maxHour - 0.5) - 0.5, 'h').format(),
end: date.clone().add(this.maxHour - Math.min(((duration + 0.05) * this.scaleRatio), this.maxHour - 0.5), 'h').format(),
classNames: DAY_SUM_CLASS_NAME
};
}
protected addEntry(date:Moment, duration:number) {
let classNames = [ADD_ENTRY_CLASS_NAME];
let title = '+';
if (duration >= 24) {
classNames.push(ADD_ENTRY_PROHIBITED_CLASS_NAME);
title = '';
}
return {
title: '+',
title: title,
start: date.clone().format(),
end: date.clone().add(24 - Math.min(duration, 22.5) - 0.5, 'h').format(),
classNames: ADD_ENTRY_CLASS_NAME
end: date.clone().add(this.maxHour - Math.min(duration * this.scaleRatio, this.maxHour - 1) - 0.5, 'h').format(),
classNames: classNames
};
}
@ -278,8 +328,8 @@ export class TimeEntryCalendarComponent implements OnInit, OnDestroy, AfterViewI
private dispatchEventClick(event:CalendarViewEvent) {
if (event.event.extendedProps.entry) {
this.editEvent(event.event.extendedProps.entry);
} else if (event.event.start) {
this.addEvent(moment(event.event.start));
} else if (event.el.classList.contains(ADD_ENTRY_CLASS_NAME) && !event.el.classList.contains(ADD_ENTRY_PROHIBITED_CLASS_NAME)) {
this.addEvent(moment(event.event.start!));
}
}
@ -468,4 +518,16 @@ export class TimeEntryCalendarComponent implements OnInit, OnDestroy, AfterViewI
protected formatNumber(value:number):string {
return this.i18n.toNumber(value, { precision: 2 });
}
private smallerSuitableRatio(value:number):number {
for (let divisor = this.labelIntervalHours + 1; divisor < 100; divisor++) {
let candidate = this.labelIntervalHours / divisor;
if (value >= candidate) {
return candidate;
}
}
return 1;
}
}

@ -11,12 +11,13 @@
[defaultView]="calendarDefaultView"
[firstDay]="calendarFirstDay"
[contentHeight]="calendarContentHeight"
[slotLabelFormat]="calendarSlotLabelFormat"
[slotEventOverlap]="calendarSlotEventOverlap"
[slotLabelInterval]="calendarSlotLabelInterval"
[slotLabelFormat]="calendarSlotLabelFormat"
[allDaySlot]="calendarAllDaySlot"
[allDayText]="calendarAllDayText"
[displayEventTime]="calendarDisplayEventTime"
[scrollTime]="calendarScrollTime"
[minTime]="calendarMinTime"
[maxTime]="calendarMaxTime"
[events]="calendarEvents"
[eventOverlap]="calendarEventOverlap"
[plugins]="calendarPlugins">

Loading…
Cancel
Save