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); }); } }) } ] } };