schedulerConfig: any = {
rowHeaderColumns: [
{
uniqueName: "Details",
display: "name",
title: this.dialogTextService.getTranslation('TitDetails'),
width: 120,
hidden: false
},
{
uniqueName: "Description",
display: "description",
title: this.dialogTextService.getTranslation('TitDescription'),
width: 120,
hidden: true
},
{
uniqueName: "FreeField",
display: "freeField",
title: this.dialogTextService.getTranslation('TitOrderMgmtFreeField'),
width: 120,
hidden: true
},
{
uniqueName: "Text1",
display: "text1",
title: this.dialogTextService.getTranslation('TitOrderText1'),
width: 120,
hidden: true
},
{
uniqueName: "Text2",
display: "text2",
title: this.dialogTextService.getTranslation('TitOrderText2'),
width: 120,
hidden: true
},
{
uniqueName: "Text3",
display: "text3",
title: this.dialogTextService.getTranslation('TitOrderText3'),
width: 120,
hidden: true
},
{
uniqueName: "Text4",
display: "text4",
title: this.dialogTextService.getTranslation('TitOrderText4'),
width: 120,
hidden: true
},
{
uniqueName: "Text5",
display: "text5",
title: this.dialogTextService.getTranslation('TitOrderText5'),
width: 120,
hidden: true
},
{
uniqueName: "Text6",
display: "text6",
title: this.dialogTextService.getTranslation('TitOrderText6'),
width: 120,
hidden: true
},
{
uniqueName: "Text7",
display: "text7",
title: this.dialogTextService.getTranslation('TitOrderText7'),
width: 120,
hidden: true
},
{
uniqueName: "Text8",
display: "text8",
title: this.dialogTextService.getTranslation('TitOrderText8'),
width: 120,
hidden: true
},
{
uniqueName: "Text9",
display: "text9",
title: this.dialogTextService.getTranslation('TitOrderText9'),
width: 120,
hidden: true
},
{
uniqueName: "Text10",
display: "text10",
title: this.dialogTextService.getTranslation('TitOrderText10'),
width: 120,
hidden: true
},
{
uniqueName: "WorkCenterIdent",
display: "workCenterIdent",
title: this.dialogTextService.getTranslation('TitWorkcenter'),
width: 120,
hidden: true
},
{
uniqueName: "Workcenter",
display: "workCenter",
title: this.dialogTextService.getTranslation('TitWorkcenterName'),
width: 120,
hidden: true
},
{
uniqueName: "ActivityShortName",
display: "activityShortName",
title: this.dialogTextService.getTranslation('TitActivityShortName'),
width: 120,
hidden: true
},
{
uniqueName: "Activity",
display: "activity",
title: this.dialogTextService.getTranslation('TitActivity'),
width: 120,
hidden: true
},
{
uniqueName: "Unit",
display: "unit",
title: this.dialogTextService.getTranslation('TitUnit'),
width: 120,
hidden: true
},
{
uniqueName: "GroupKey",
display: "groupTypeName",
title: this.dialogTextService.getTranslation('TitGroup'),
width: 120,
hidden: true
},
{
uniqueName: "Total",
display: "total",
title: this.dialogTextService.getTranslation('TitTotal'),
width: 120,
hidden: true
}
],
theme: 'scheduler_time_reporting_board',
keyboardEnabled: true,
rowHeaderScrolling: true,
rowHeaderWidthAutoFit: false, // Die Breite der Spalten kann der Benutzer selbst festlegen und in der DB speichern
durationBarVisible: false,
heightSpec: 'Parent100Pct',
hideBorderFor100PctHeight: true,
locale: this.localeService.localeId,
//cellWidthSpec: 'Auto',
cellWidth: 42,
eventEditMinWidth: 42,
crosshairType: 'Full',
timeHeaders: [
{ 'groupBy': "Month", 'format': "MMMM yyyy" },
{ 'groupBy': 'Week' },
{ 'groupBy': 'Day', 'format': 'd' }
],
scale: 'Day',
showNonBusiness: true,
businessWeekends: false,
floatingEvents: true,
eventHeight: 20,
headerHeight: 20,
eventStackingLineHeight: 100,
rowMarginBottom: 0,
eventMoveHandling: 'Disabled',
eventResizeHandling: 'Disabled',
eventBorderVisible: false,
groupConcurrentEvents: false,
allowEventOverlap: false,
rowHeaderHideIconEnabled: false,
treeEnabled: true,
allowMultiSelect: false,
beforeCellRenderCaching: true,
eventClickHandling: 'Edit',
eventHoverHandling: 'Bubble',
// Since version 2022.2.5302, the columns are merged by default for parent resources.
// That means args.columns is empty in onBeforeRowHeaderRender.
// https://javascript.daypilot.org/daypilot-pro-for-javascript-2022-2-5302/
// https://forums.daypilot.org/question/5782/typeerror-cannot-set-properties-of-undefined-setting-csscla
rowHeaderColumnsMergeParents: false,
bubble: new DayPilot.Bubble({
onLoad: (args) => {
// Do not remove!!!
},
animated: false,
showLoadingLabel: false,
showAfter: 100,
hideAfter: 100
}),
onKeyDown: args => {
if (args.originalEvent.code === "Tab") {
args.originalEvent.preventDefault();
if (args.originalEvent.shiftKey) {
this.scheduler.control.keyboard.move("left");
} else {
this.scheduler.control.keyboard.move("right");
}
}
},
onEventEditKeyDown: args => {
if (args.originalEvent.code === "Tab") {
args.originalEvent.preventDefault();
args.originalEvent.stopPropagation();
args.submit();
if (args.originalEvent.shiftKey) {
this.scheduler.control.keyboard.move("left");
}
else {
this.scheduler.control.keyboard.move("right");
}
}
},
onKeyboardFocusChanged: args => {
if (args.focus.e) {
if (args.focus.e.isEvent) {
let event = args.focus.e;
if (event.tag('rowTypeKey') && event.tag('rowTypeKey') != 'None' && event.tag('rowTypeKey') != 'DebitTime'
&& event.tag('rowTypeKey') != 'Total' && this.canEdit(event)) {
this.scheduler.control.events.edit(args.focus.e);
}
}
}
else if (args.focus.cell) {
this.activateInlineEditor(args.focus.cell);
}
},
onAfterRender: (args) => {
this.busyIndicatorService.hide();
},
onTimeRangeSelecting: (args) => {
// Update the keyboard focus during time range selection (cell click)
if (!args.row.data.frozen) {
this.scheduler.control.keyboard.focusCell(args.start, args.resource);
}
this.globalVariableService.date = moment(args.start.value).subtract(1, 'day');
args.end = new DayPilot.Date(args.start.addHours(12));
},
onTimeRangeSelected: (args) => {
this.globalVariableService.date = moment(args.start.value);
this.scheduler.control.clearSelection();
this.activateInlineEditor(args);
},
onEventClick: (args) => {
// Update the keyboard focus
this.scheduler.control.keyboard.focusEvent(args.e);
if (args.e.tag('rowTypeKey') && (args.e.tag('rowTypeKey') == 'None' || args.e.tag('rowTypeKey') == 'DebitTime'
|| args.e.tag('rowTypeKey') == 'Total') || !this.canEdit(args.e)) {
args.preventDefault();
}
this.globalVariableService.date = moment(args.e.data.start).subtract(1, 'day');
},
onEventClicked: (args) => {
//console.debug('onEventClicked');
let resourceGroupTypeKey = this.getResourceGroupTypeKey(args.e.data.resource);
if (resourceGroupTypeKey == 'Workprofile') {
this.showWorkProfileChangeDialog(args.e);
}
},
onAfterEventEditRender: (args) => {
//console.debug('onAfterEventEditRender');
args.element.classList.add('scheduler_time_reporting_board-place-editor');
args.element.style.resize = 'none';
},
onEventEdit: (args) => {
//console.debug('onEventEdit');
let justCreated = args.e.tag("justCreated");
let value = args.newText.trim();
let invalidValue: boolean;
let resourceGroupTypeKey = this.getResourceGroupTypeKey(args.e.data.resource);
args.e.data.tags.rowTypeKey = resourceGroupTypeKey;
// Contains one of the allowed separators, but more than once
invalidValue = (value.match(/[,]|[;]|[:]|[.]|\s/gm) || []).length > 1;
// Value should always contain only numbers
if (!invalidValue) {
invalidValue = isNaN(parseFloat(value.replace(/[,]|[;]|[:]|\s/gm, '.')));
}
// Is a valid number
let valueNumber = parseFloat(value.replace(/[,]|[;]|[:]|\s/gm, '.'));
if (!invalidValue) {
invalidValue = valueNumber == 0
|| valueNumber < 0 && resourceGroupTypeKey != 'ProjectTime'; // Negativ value down to -24 is only allowed for project times
}
// Empty string or 0 is allowed with existing values, which means deleting
if (!justCreated && (value == '' || valueNumber == 0)) {
args.newText = '';
}
else if (invalidValue || !this.canEdit(args.e) || resourceGroupTypeKey == 'Workprofile') {
args.preventDefault();
}
else {
// Format value
if (resourceGroupTypeKey == 'Absence' || resourceGroupTypeKey == 'Overtime' || resourceGroupTypeKey == 'ProjectTime') {
value = value.replace(/[,]|[;]|\s/gm, '.');
let timeDuration: Duration;
// HH:mm
if (value.includes(':')) {
// Limit to two numbers after :
value = Number(value.replace(':', '.')).toFixed(2).replace('.', ':');
timeDuration = moment.duration(value);
}
else {
timeDuration = moment.duration(value, 'hours');
}
// Limit to 24 hours max.
if (timeDuration.asHours() > 24) {
timeDuration = moment.duration(24, 'hours');
}
// Limit to -24 hours min. (only project time)
if (timeDuration.asHours() < -24) {
timeDuration = moment.duration(-24, 'hours');
}
if (this.clientConfigurationService.timeCreditFormat == TimeSpanFormat.HoursAndMinutes) {
args.newText = timeDuration.format(
'hh:mm', { useToLocaleString: false, trim: false, decimalSeparator: '', groupingSeparator: '' });
}
else {
args.newText = DurationExtension.format(timeDuration, this.clientConfigurationService.timeCreditFormat);
}
}
else {
args.newText = Number(value.replace(/[,]|[;]|[:]|\s/gm, '.')).toFixed(2);
}
}
if (justCreated) {
if (args.canceled || invalidValue || args.newText == '') {
this.scheduler.control.events.remove(args.e);
}
else {
args.e.data.tags.justCreated = false;
}
}
},
onEventEdited: (args) => {
if (args.canceled) {
return;
}
let resourceGroupTypeKey = this.getResourceGroupTypeKey(args.e.data.resource);
let rowItem: TimeReportRowItem;
switch (resourceGroupTypeKey) {
case 'Workprofile':
break;
case 'Absence':
rowItem = this.absenceRowItems.find(r => r.id == args.e.data.resource);
break;
case 'Overtime':
rowItem = this.overtimeRowItems.find(r => r.id == args.e.data.resource);
break;
case 'ProjectTime':
rowItem = this.projectTimeRowItems.find(r => r.id == args.e.data.resource);
break;
case 'ProjectExpense':
rowItem = this.projectExpenseRowItems.find(r => r.id == args.e.data.resource);
break;
case 'PersonExpense':
rowItem = this.personExpenseRowItems.find(r => r.id == args.e.data.resource);
break;
}
let dayNumber = args.e.start().getDay();
let oldValue = resourceGroupTypeKey == 'ProjectExpense' || resourceGroupTypeKey == 'PersonExpense'
? rowItem.dayEntries[dayNumber - 1].expenseAmount
: rowItem.dayEntries[dayNumber - 1].value;
// Update detail row day
rowItem.setDayValue(dayNumber, args.newText);
let newValue = resourceGroupTypeKey == 'ProjectExpense' || resourceGroupTypeKey == 'PersonExpense'
? rowItem.dayEntries[dayNumber - 1].expenseAmount
: rowItem.dayEntries[dayNumber - 1].value;
if (oldValue == newValue) {
return;
}
// Update detail row total
let detailResource = this.getResource(args.e.data.resource);
detailResource.tags.total = rowItem.getTotal();
this.scheduler.control.rows.find(detailResource.id).cells.all().invalidate();
// Spesen werden im Total-Bereich nicht berechnet
if (resourceGroupTypeKey == 'Absence' || resourceGroupTypeKey == 'Overtime' || resourceGroupTypeKey == 'ProjectTime') {
let totalRowItem = this.totalTimeAmountRowItem
.find(row => row.getTotalRowItemTypeKey() == detailResource.tags.totalItemTypeKey);
let totalResource = this.resources.find(res => res.tags && res.tags.totalItemTypeKey &&
res.tags.totalItemTypeKey == totalRowItem.getTotalRowItemTypeKey());
// Calc total row day
let sum = 0;
switch (totalRowItem.getTotalRowItemTypeKey()) {
case 'Absence':
this.absenceRowItems.forEach(r => {
let value = r.dayEntries.find(d => d.dayNo == dayNumber).value;
if (value) {
sum += moment.duration(value).asHours();
}
});
break;
case 'Overtime':
this.overtimeRowItems.forEach(r => {
let value = r.dayEntries.find(d => d.dayNo == dayNumber).value;
if (value) {
sum += moment.duration(value).asHours();
}
});
break;
case 'ProjectTime':
this.projectTimeRowItems.forEach(r => {
let value = r.dayEntries.find(d => d.dayNo == dayNumber).value;
if (value) {
sum += moment.duration(value).asHours();
}
});
break;
}
// Update total row day
totalRowItem.setDayValue(dayNumber, sum.toString());
let dayCell = totalRowItem.getDayItem(totalRowItem.dayEntries[dayNumber - 1]);
if (dayCell) {
let event = this.scheduler.control.events.find(e => e.data.resource == totalResource.id && e.start().getDay() == dayNumber);
if (event) {
event.data.text = dayCell.text;
this.scheduler.control.events.update(event);
}
else {
this.scheduler.control.events.add(new DayPilot.Event(dayCell as any));
}
}
else {
let event = this.scheduler.control.events.find(e => e.data.resource == totalResource.id && e.start().getDay() == dayNumber);
if (event) {
this.scheduler.control.events.remove(event);
}
}
// Update total row total
totalResource.tags.total = totalRowItem.getTotal();
this.scheduler.control.rows.update(this.scheduler.control.rows.find(totalResource.id));
// Update project difference
if (resourceGroupTypeKey == 'ProjectTime') {
totalRowItem = this.totalTimeAmountRowItem
.find(row => row.getTotalRowItemTypeKey() == 'ProjectTimeDifference');
let projectTimeDebitTimeRow = this.totalTimeAmountRowItem
.find(row => row.getTotalRowItemTypeKey() == 'ProjectTimeDebitTime');
let projectDifference: number;
let projectTimeDebitTime = parseFloat(projectTimeDebitTimeRow.dayEntries[dayNumber - 1].value
? moment.duration(projectTimeDebitTimeRow.dayEntries[dayNumber - 1].value).asHours().toString()
: '0.0');
if (this.clientConfigurationService.showProjectTimeDifferenceNegative) {
projectDifference = projectTimeDebitTime - sum;
}
else {
projectDifference = sum - projectTimeDebitTime;
}
let projectDifferenceResource = this.resources.find(res => res.tags && res.tags.totalItemTypeKey &&
res.tags.totalItemTypeKey == totalRowItem.getTotalRowItemTypeKey());
// Update total row day
totalRowItem.setDayValue(dayNumber, projectDifference.toString());
let dayCell = totalRowItem.getDayItem(totalRowItem.dayEntries[dayNumber - 1]);
let event = this.scheduler.control.events.find(e => e.data.resource == projectDifferenceResource.id && e.start().getDay() == dayNumber);
if (event) {
this.scheduler.control.events.remove(event);
}
if (dayCell) {
this.scheduler.control.events.add(new DayPilot.Event(dayCell as any));
}
// Update total row total
projectDifferenceResource.tags.total = totalRowItem.getTotal();
this.scheduler.control.rows.update(this.scheduler.control.rows.find(projectDifferenceResource.id));
}
}
this.hasDataChanges = true;
},
onRowHeaderResized: () => {
this.rowHeaderWidth = this.scheduler.control.rowHeaderWidth;
},
onBeforeRowHeaderRender: (args) => {
//console.debug('onBeforeRowHeaderRender');
// Alternate Row Colors
if (args.row.displayY % 2) {
args.row.backColor = "#FFFFFF";
}
else {
args.row.backColor = "#F5F5F5";
}
if (!this.canEditProject(args.row.id)) {
args.row.backColor = "#D3D3D3";
}
// Highlight group header
if (args.row.children().length > 0) {
args.row.backColor = "#A6A6A6";
args.row.fontColor = "#FFFFFF";
args.row.cssClass = 'time-reporting-board-row-header-group';
}
let resource = this.getResource(args.row.id);
// Highlight total resource header empty space
if (resource && resource.tags && resource.tags.isTotalResourceHeaderEmptySpace) {
args.row.backColor = "#FFFFFF";
}
// Highlight total resource header
if (resource && resource.tags && resource.tags.isTotalResourceHeader) {
args.row.backColor = "#808080";
args.row.fontColor = "#FFFFFF";
args.row.cssClass = "time-reporting-board-total-resource-header";
}
// Spalte "Total" rechtsbündig ausrichten
args.row.columns[19].cssClass = 'time-reporting-board-row-header-total';
},
onBeforeEventRender: (args) => {
// text-align: left
if (args.data.tags && args.data.tags.rowTypeKey && args.data.tags.rowTypeKey == 'Workprofile') {
args.data.cssClass = 'time-reporting-board-event-workprofile';
}
if (args.data.tags && args.data.tags.totalRowItemTypeKey && args.data.tags.totalRowItemTypeKey == 'ProjectTimeDifference') {
args.data.backColor = args.data.tags.rawValue > 0 ? 'green' : 'red';
args.data.fontColor = 'white';
}
let resourceGroupTypeKey = this.getResourceGroupTypeKey(args.data.resource);
// Kommentare sind möglich in den Zeilen Sollzeit sowie
// Projektzeit sowie Projektspesen (sofern Buchungen vorhanden sind)
if (resourceGroupTypeKey == 'DebitTime' || resourceGroupTypeKey == 'ProjectTime'
|| resourceGroupTypeKey == 'ProjectExpense' || resourceGroupTypeKey == 'PersonExpense') {
let rowItem: TimeReportRowItem;
switch (resourceGroupTypeKey) {
case 'DebitTime':
rowItem = this.debitTimeRowItem;
break;
case 'ProjectTime':
rowItem = this.projectTimeRowItems.find(r => r.id == args.data.resource);
break;
case 'ProjectExpense':
rowItem = this.projectExpenseRowItems.find(r => r.id == args.data.resource);
break;
case 'PersonExpense':
rowItem = this.personExpenseRowItems.find(r => r.id == args.data.resource)
break;
}
let dayNumber = args.e.start.getDay();
if (dayNumber == 0) {
return;
}
// Tag nicht gesperrt und Buchung editierbar
let isEditable = !this.person.isDateLocked(moment(args.data.start.value))
&& (this.canEdit(args) || resourceGroupTypeKey == 'DebitTime');
// Ist ein Kommentar vorhanden?
let hasComment = !isNullOrEmpty(rowItem.dayEntries[dayNumber - 1].comment1)
|| !isNullOrEmpty(rowItem.dayEntries[dayNumber - 1].comment2);
let eventId = args.e.id;
if (hasComment) {
args.data.areas = [
{
onClick: (args) => {
if (isEditable) {
this.showCommentDialog(eventId, rowItem, dayNumber, hasComment, resourceGroupTypeKey != 'DebitTime');
}
},
height: 6,
width: 6,
visibility: "Visible",
bottom: 1,
left: 1,
style: "border: 1px solid red; border-radius: 5px; box-sizing: border-box; background-color: red;"
}
];
let comment = '';
if (!isNullOrEmpty(rowItem.dayEntries[dayNumber - 1].comment1)) {
comment = rowItem.dayEntries[dayNumber - 1].comment1;
}
if (!isNullOrEmpty(rowItem.dayEntries[dayNumber - 1].comment2)) {
comment = `${comment}
${rowItem.dayEntries[dayNumber - 1].comment2}`
}
args.data.bubbleHtml = comment;
}
if (isEditable) {
const contextMenuItems: Array = [
{
text: this.dialogTextService.getTranslation(hasComment ? 'TitEditComment' : 'TitAddComment'),
onClick: (args) => { this.showCommentDialog(eventId, rowItem, dayNumber, hasComment, resourceGroupTypeKey != 'DebitTime'); }
}
];
// Context menu
args.data.contextMenu = new DayPilot.Menu({
items: contextMenuItems
});
}
}
},
onBeforeTimeHeaderRender: (args) => {
//console.debug('onBeforeTimeHeaderRender');
// Week row
if (args.header.level === 1) {
args.header.html = `${this.dialogTextService.getTranslation('TitWeek')} ${args.header.start.weekNumberISO().padStart(2, '0')}`;
}
// Day row
if (args.header.level === 2) {
args.header.areas = [];
// Highlight current day
if (
moment(args.header.start.value).isSame(moment(), 'day')) {
args.header.areas.push({
start: args.header.start,
end: args.header.end,
bottom: 0,
height: 2,
backColor: 'blue'
});
}
}
},
onBeforeCellRender: (args) => {
//console.debug('onBeforeCellRender');
let resource = this.getResource(args.cell.resource);
// Alternate Row Colors
if (args.cell.displayY % 2) {
args.cell.backColor = "#FFFFFF";
}
else {
args.cell.backColor = "#F5F5F5";
}
let backColor = this.setCellBackgroundColor(args.cell.start, resource);
// Highlighting day if necessary
if (backColor) {
args.cell.backColor = backColor;
}
// Fertiggemeldete Aufträge dürfen nicht bearbeitet werden
if (!this.canEditProject(args.cell.resource)) {
args.cell.disabled = true;
args.cell.backColor = "#D3D3D3";
}
// Is locked day?
if (!args.cell.disabled) {
let currentDate = moment(args.cell.start.value);
args.cell.disabled = this.person.isDateLocked(currentDate);
}
// Highlight recourse group header
if (resource && resource.tags && resource.tags.isResourceGroup) {
args.cell.backColor = "#A6A6A6";
}
// Highlight total resource header empty space
if (resource && resource.tags && resource.tags.isTotalResourceHeaderEmptySpace) {
args.cell.backColor = "#FFFFFF";
}
// Highlight total resource header
if (resource && resource.tags && resource.tags.isTotalResourceHeader) {
args.cell.backColor = "#808080";
args.cell.fontColor = "#FFFFFF";
args.cell.cssClass = "time-reporting-board-total-resource-header";
}
},
onBeforeCornerRender: (args) => {
//console.debug('onBeforeCornerRender');
args.areas = [
{
left: 5,
top: 10,
height: 20,
width: 20,
action: "ContextMenu",
html: '',
cssClass: "area-open-menu",
menu: new DayPilot.Menu({
onShow: (args) => {
let menu = args.menu;
let scheduler = this.scheduler.control;
let schedulerConfig = this.schedulerConfig;
let busyIndicatorService = this.busyIndicatorService;
args.menu.items = [];
this.schedulerConfig.rowHeaderColumns
.filter(c => c.uniqueName != 'Details')
.sortBy(c => c.title)
.forEach(function (col) {
args.menu.items.push({
_column: col,
text: col.title,
icon: col.hidden ? "" : "scheduler_time_reporting_board_column-selector-icon scheduler_time_reporting_board_column-selector-icon-checked",
onClick: (args) => {
// hide the menu, normally it stays visible until onClick completes
menu.hide();
let column = args.item._column;
column.hidden = !column.hidden;
busyIndicatorService.show();
setTimeout(() => {
scheduler.update({ rowHeaderColumns: schedulerConfig.rowHeaderColumns });
}, 10);
}
} as any);
});
}
})
}
]
}
};