Our blazing fast Grid component built with pure JavaScript


Post by gokulnaths »

Hello team,

Expecting your support in helping us to find a better feasible solution for the problem I am facing with different zones here, as the date fields are being converted into the local time zones, we are facing a big issue in calculating project dates, as the system is adding a few extra hours based on the UTC and the local time zone; in one single time, we are observing two different end dates at two different regions or even saving the date to our server from region is causing an issue of time difference for the users in other time zones.

Irrespective of fixing the bryntum gantt timezone, we are facing this issue, can you please help us in achive this functionality where irrespective of different timezones the users are in, the grid records should only take the project specifc timezone data, not the respective local time zone coverted data.

Please refer to our current project config and expecting your support in fixing this.

defineBryntumGanttConfig() {
    // console.log("defineBryntumGanttConfig: this.hideOwnerTeam:",this.hideOwnerTeam);
    this.ganttConfig = {
      dependencyIdField: 'wbsCode',
      // dependencyIdField: 'sequenceNumber',
      selectionMode: {
        row: true,
        cell: true,
        // dragSelect: true,
        rowNumber: true,
        
}, preserveScrollOnDatasetChange: true, displaySchedulingIssueResolutionPopup: false, showTaskColorPickers: false, showTooltip: false, taskMenuFeature: { items: { cut: false, editTask: false, // add: { // menu: { // subtask: false, // milestone: false, // } // }, add: false, addTaskAbove: { text: 'Add Task above', icon: 'b-icon-up', onItem: ({ taskRecord }: any) => { console.log(taskRecord.parent, taskRecord.child, taskRecord.successor, taskRecord.predecessor) taskRecord.parent.insertChild({ wbs: '1.1.1', name: 'NEW TASK ABOVE', duration: 1, taskstatus: this.newStatusId, eventColor: this.newTaskColor, manuallyScheduled: true, startDate: moment().format('YYYY-MM-DD'), endDate: moment().format('YYYY-MM-DD') }, taskRecord); // run propagation to calculate new task fields // this.ganttInstance.project.propagate(); } }, addTenTaskAbove: { text: 'Add 10 Task(s) above', icon: 'b-icon-up', onItem: ({ taskRecord }: any) => { console.log(taskRecord.parent, taskRecord.child, taskRecord.successor, taskRecord.predecessor) for (var i = 0; i < 11; i++) { taskRecord.parent.insertChild({ wbs: '1.1.1', name: 'NEW TASK ABOVE', duration: 1, taskstatus: this.newStatusId, eventColor: this.newTaskColor, manuallyScheduled: true, startDate: moment().format('YYYY-MM-DD'), endDate: moment().format('YYYY-MM-DD') }, taskRecord); } // run propagation to calculate new task fields // this.ganttInstance.project.propagate(); } }, addTaskBelow: { text: 'Add Task below', icon: 'b-icon-down', onItem: ({ taskRecord }: any) => { console.log(taskRecord.parent, taskRecord.child, taskRecord.successor, taskRecord.predecessor) taskRecord.parent.insertChild({ wbs: '1.1.1', name: 'NEW TASK BELOW', duration: 1, taskstatus: this.newStatusId, eventColor: this.newTaskColor, manuallyScheduled: true, startDate: moment().format('YYYY-MM-DD'), endDate: moment().format('YYYY-MM-DD') }, taskRecord.nextSibling); // run propagation to calculate new task fields // this.ganttInstance.project.propagate(); } }, addTenTaskBelow: { text: 'Add 10 Task(s) below', icon: 'b-icon-down', onItem: ({ taskRecord }: any) => { // console.log(taskRecord.parent, taskRecord.child, taskRecord.successor, taskRecord.predecessor) for (var i = 0; i < 11; i++) { taskRecord.parent.insertChild({ wbs: '1.1.1', name: 'NEW TASK BELOW', duration: 1, taskstatus: this.newStatusId, eventColor: this.newTaskColor, manuallyScheduled: true, startDate: moment().format('YYYY-MM-DD'), endDate: moment().format('YYYY-MM-DD') }, taskRecord.nextSibling); } // run propagation to calculate new task fields // this.ganttInstance.project.propagate(); } }, // autoScheduleTask: { // text: 'Auto-Schedule', // icon: 'b-fa b-fa-calendar-alt', // onItem: ({ taskRecord }: any) => { // let selectedData = this.ganttInstance.selectedRecords; // const popup = new Popup({ // title: 'Auto Schedule & Retain Current Dates', // closable: true, // modal: true, // items: [ // { // type: 'widget', // html: '<p>Auto-scheduling will modify planned dates based on dependencies or constraints.Do you want to retain the planned dates?</p>', // cls: 'b-popup-message' // } // ], // bbar: [ // { // text: 'Yes', // cls: 'b-btn b-ok b-raised popup-tooltip', // tooltip: { // html: "This sets a 'Start No Earlier Than' constraint to keep an auto-scheduled task's manually assigned dates, unless affected by an intermediate dependency", // align: 'b-t', // cls: 'popup-tooltip' // }, // onClick: () => { // selectedData.forEach((task) => { // if (task.manuallyScheduled === true) { // const fixedStartDate = task.startDate !== null ? new Date(task.startDate).setHours(0, 0, 0, 0) : null; // const fixedEndDate = task.endDate !== null ? new Date(task.endDate).setHours(0, 0, 0, 0) : null; // task.manuallyScheduled = false; // if (fixedStartDate !== null) { // task.setStartDate(fixedStartDate, false); // } // if (fixedEndDate !== null) { // task.setEndDate(fixedEndDate, false); // } // } // }); // console.log('Retain planned dates clicked'); // popup.close(); // } // }, // { // text: 'No', // cls: 'b-btn popup-tooltip', // onClick: () => { // selectedData.forEach((task) => { task.manuallyScheduled = false }); // console.log('OK clicked'); // popup.close(); // } // }, // ] // }); // popup.show(); // } // }, delete: true, convertToMilestone: false, linkTasks: false, unlinkTasks: false } }, projectLinesFeature: { disabled: true }, cellEditFeature: { addNewAtEnd: false }, cellTooltipFeature: { disabled: false, cls: 'custom-css-tooltip', hoverDelay: 100 }, taskTooltipFeature: { disabled: true // template: ({ taskRecord }) => `${taskRecord.name}`, // // Tooltip configs can be used here // align: 'l-r' // Align left to right }, project: { autoCalculatePercentDoneForParentTasks: false, skipNonWorkingTimeWhenSchedulingManually: true, skipNonWorkingTimeInDurationWhenSchedulingManually: true, autoSetConstraints: true, daysPerWeek: 5, daysPerMonth: 20, calendar: 'general', hoursPerDay: 8, calendarsData: [ { id : 'general', name : 'General', intervals : [ { recurrentStartDate : 'on Sat', recurrentEndDate : 'on Mon', isWorking : false }, { recurrentStartDate : 'every weekday at 18:01', recurrentEndDate : 'every weekday at 10:00', isWorking : false } ] } ], // calendarsData: this.simulationData?.calendars?.rows ? this.simulationData.calendars.rows : [ // { // id: "general", // name: "General", // intervals: [ // { // recurrentStartDate: "on Sat at 0:00", // recurrentEndDate: "on Mon at 0:00", // isWorking: false // } // ] // } // ], // calendarsData: [ // { // id: "general", // name: "General", // intervals: [ // { // recurrentStartDate: "on Sat at 0:00", // recurrentEndDate: "on Mon at 0:00", // isWorking: false // } // ] // } // ], // Let the Project know we want to use our own Task model with custom fields / methods // transport: { // load: { // url: 'assets/data/launch-motiva.json' // } // }, autoLoad: true, assignmentStore: { data: this.simulationData.assignments.rows, useRawData: true, }, resourceStore: { data: this.simulationData.resources.rows, useRawData: true, }, taskStore: { data: this.simulationData.tasks.rows, useRawData: true, wbsMode: 'auto' }, dependencyStore: { data: this.simulationData.dependencies.rows, useRawData: true, }, timeRangeStore: { data: [ { "id": 1, "name": moment().format('Do MMM YY'), "startDate": moment().format('YYYY-MM-DD'), "duration": 0, "durationUnit": "d", "cls": "b-fa b-fa-calendar-day" } ] }, listeners: { refresh: ({ records }) => { console.log('Calculations finished'); this.displayGantt = true; this.message.remove(this.loadingIndicator); this.originalStartDate = this.ganttInstance.startDate; this.originalEndDate = this.ganttInstance.endDate; }, }, // The State TrackingManager, which the UndoRedo widget in the toolbar uses stm: { // NOTE, that this option does not enable the STM itself, this is done by the `undoredo` widget, defined in the toolbar // If you don't use `undoredo` widget in your app, you need to enable STM manually: `stm.enable()`, // otherwise, it won't be tracking changes in the data // It's usually best to enable STM after the initial data loading is completed. autoRecord: true }, // This config enables response validation and dumping of found errors to the browser console. // It's meant to be used as a development stage helper only, so please set it to false for production systems. validateResponse: true, delayCalculation: true, useRawData: true }, // startDate: '2019-01-12', // endDate: '2019-03-24', startDate: this.simulationData.project.startDate, endDate: this.simulationData.project.endDate, columns: [ { type: 'sequence', text: '#', width: 50, align: 'center' }, { width: 60, type: 'manuallyscheduled', text: "Manually Scheduled", align: 'center', showCheckAll: true, sortable: false, filterable: false, afterRenderCell({ record, widgets }: any) { // Hide checkboxes in certain rows const wbsLevel = record.wbs.split('.').length; // Hide the checkbox if WBS level is 1 or 2 if (wbsLevel <= 2) { widgets[0].hidden = true; } else { widgets[0].hidden = false; } }, listeners: { // toggle: ({ record, checked }: any) => { // const fixedStartDate = record.startDate !== null ? new Date(record.startDate).setHours(0, 0, 0, 0) : null; // const fixedEndDate = record.endDate !== null ? new Date(record.endDate).setHours(0, 0, 0, 0) : null; // record.manuallyScheduled = true; // if (fixedStartDate !== null) { // record.setStartDate(fixedStartDate, false); // } // if (fixedEndDate !== null) { // record.setEndDate(fixedEndDate, false); // } // if (!checked) { // const popup = new Popup({ // title: 'Auto Schedule & Retain Current Dates', // closable: true, // Allows closing the popup // modal: true, // Prevents interaction with other elements // items: [ // { // type: 'widget', // html: `<div><p>Auto-scheduling will modify planned dates based on dependencies or constraints. // Do you want to retain the planned dates?</p> // <p> Current Planned Start : // <b> ${fixedStartDate ? moment(fixedStartDate).format('MMM D, YYYY') : 'Planned start date not assigned'}</b> // </p> // <p>Current Planned End : // <b> ${fixedEndDate ? moment(fixedEndDate).format('MMM D, YYYY') : ' Planned end date not assigned'}</b> // </p></div>`, // cls: 'b-popup-message' // } // ], // bbar: [ // { // text: 'Yes', // icon: 'b-icon-info-circle', // cls: 'b-btn b-ok b-raised popup-tooltip', // tooltip: { // html: "This sets a 'Start No Earlier Than' constraint to keep an auto-scheduled task's manually assigned dates, unless affected by an intermediate dependency", // align: 'b-t', // Aligns tooltip to the right of the button // cls: 'popup-tooltip' // }, // onClick: () => { // record.manuallyScheduled = false; // if (fixedStartDate !== null) { // record.setStartDate(fixedStartDate, false); // } // if (fixedEndDate !== null) { // record.setEndDate(fixedEndDate, false); // } // console.log('Retain planned dates clicked'); // popup.close(); // } // }, // { // text: 'No', // cls: 'b-btn popup-tooltip', // onClick: () => { // record.manuallyScheduled = false; // console.log('OK clicked'); // popup.close(); // } // }, // ] // }); // popup.show(); // } // }, } }, { type: 'name', width: 450, text: "Title", showWbs: this.showWBS, align: 'center', filterable: false, sortable: false, // tooltipRenderer: ({ record, column }: any) => { // return StringHelper.xss`<p style='font-size:12px;margin-bottom:0px;line-height:1rem;'>${record.name}</p>`; // } indentSize: 0.5 }, { type: 'predecessor', text: "Predecessors", width: 112, align: 'center', sortable: false, filterable: false, hidden: true, }, { type: 'successor', text: "Successors", width: 112, align: 'center', sortable: false, filterable: false, hidden: true, }, { text: 'Status', field: 'taskstatus', width: 120, type: 'template', align: 'center', sortable: false, filterable: false, editor: { type: 'combo', items: this.statusOptions, displayField: 'status', valueField: '_id', listeners:{ change: ({ value, oldValue, source, userAction, valid}) => { console.log('Status changed from', oldValue, 'to', value); const userAccessLevel = this.currentUser.user.accessLevel; const status = this.getStatusForBryntum(value); // console.log("STATUS:",status); if(status.status == "Not relevant" && userAccessLevel != "Super"){ // console.log("userAccessLevel:",userAccessLevel); console.log("yes it is not relevant and the user is not super here"); source.value = oldValue Toast.show({ html : 'This status option is restricted to choose, Thanks for your understanding.', side : 'top', showProgress:true, maxWidth: 400, }); } } } }, template: ({ value = '' }) => { const status = this.getStatusForBryntum(value); if (status != undefined) { return StringHelper.xss` <div class="b-status b-status-${status.category.toLowerCase()}">${status.status}</div>` } return StringHelper.xss` <div class="b-status b-status-na">NA</div>` } }, { text: 'Indicator', headerRenderer(context) { return `Indicator&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, field: 'varianceStatus', width: 120, type: 'template', align: 'center', sortable: false, filterable: false, editor: false, template: ({ value }) => { // console.log("variance status", value) if (value === "overdue") { return StringHelper.xss`<span class="circular-dot dot-overdue"></span>Overdue` } else if (value === "ontime") { return StringHelper.xss`<span class="circular-dot dot-ontime"></span>On Time` } else if (value === "ontrack") { return StringHelper.xss`<span class="circular-dot dot-ontrack"></span>On Track` } else if (value === "earlyend") { return StringHelper.xss`<span class="circular-dot dot-earlyend"></span>Early End` } else if (value === "earlystart") { return StringHelper.xss`<span class="circular-dot dot-earlystart"></span>Early Start` } else if (value === "lateend") { return StringHelper.xss`<span class="circular-dot dot-latestart"></span>Late End` } else if (value === "latestart") { return StringHelper.xss`<span class="circular-dot dot-lateend"></span>Late Start` } else if (value === "notstarted") { return StringHelper.xss`<span class="circular-dot dot-notstarted"></span>Not Started` } else if (value === "delayed") { return StringHelper.xss`<span class="circular-dot dot-delayed"></span>Delayed` } return StringHelper.xss`<span class="circular-dot dot-notapplicable"></span>Not Applicable` // return value; }, hidden: true, }, { width: 120, text: "Delay Log", filterable: false, field: 'delayLog', sortable: false, tooltip: "Shows the history of delays", renderer: ({ value }: { value: string }) => { return value ?? ""; }, hidden: true, }, { type: 'constrainttype', text: "Constraint Type", hidden: true, }, { type: 'constraintdate', text: "Constraint Date", hidden: true, format: 'MMM DD, YYYY', }, { type: 'startdate', format: 'MMM DD, YYYY', text: "Planned Start", align: 'center', width: 100, sortable: false, filterable: false, }, { type: 'enddate', text: "Planned End", headerRenderer(context) { return `Planned End&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, align: 'center', width: 110, sortable: false, filterable: false, editor: false, // Disable editor for all rows (can override later) }, { type: 'date', text: "Actual Start", headerRenderer(context) { return `Actual Start&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, field: 'actualStart', width: 110, sortable: false, filterable: false, editor: false, format: 'MMM DD, YYYY', renderer: ({ record }: any) => { // Render the actual start date value console.log("-----------------------------"); console.log("rec wbs:",record.wbsValue._value); console.log("rec star:",record.startDate); console.log("rec end:",record.endDate); return record.actualStart ? moment(record.actualStart).format('MMM DD, YYYY') : ''; } }, { type: 'date', text: "Actual End", headerRenderer(context) { return `Actual End&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, field: 'actualEnd', sortable: false, width: 110, filterable: false, format: 'MMM DD, YYYY', editor: false, renderer: ({ record }: any) => { // Render the actual start date value return record.actualEnd ? moment(record.actualEnd).format('MMM D, YYYY') : ''; } }, { field: 'calculatedStartDate', text: "Auto Scheduled Start", width: 130, headerRenderer(context) { return `Auto Scheduled Start&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, align: 'center', sortable: false, filterable: false, editor: false, format: 'MMM DD, YYYY', hidden: true, }, { field: 'calculatedEndDate', text: "Auto Scheduled End", width: 130, headerRenderer(context) { return `Auto Scheduled End&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, align: 'center', sortable: false, filterable: false, editor: false, format: 'MMM DD, YYYY', hidden: true, }, { type: 'duration', text: "Duration", align: 'center', sortable: false, filterable: false, hidden: true, }, { field: 'baselineStart', text: "Baseline Start", headerRenderer(context) { return `Baseline Start&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, align: 'center', width: 120, sortable: false, filterable: false, editor: false, renderer: ({ record }: any) => { // Render the actual start date value return record.baselineStart ? moment(record.baselineStart).format('MMM D, YYYY') : ''; }, hidden: true, }, { field: 'baselineEnd', text: "Baseline End", headerRenderer(context) { return `Baseline End&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, align: 'center', width: 110, sortable: false, filterable: false, editor: false, renderer: ({ record }: any) => { // Render the actual start date value return record.baselineEnd ? moment(record.baselineEnd).format('MMM D, YYYY') : ''; }, hidden: true, }, { field: 'baselineDuration', text: "Baseline Duration", align: 'center', sortable: false, filterable: false, hidden: true, }, { field: 'baselineDurationVariance', text: "Baseline Duration Variance", headerRenderer(context) { return `Baseline Duration Variance&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, align: 'center', width: 140, sortable: false, filterable: false, editor: false, renderer: (cellProps: { record: any, cellElement: any, value: any }) => { // Reset any existing styles or classes let value = cellProps.value if (value) { if (value > 0) { cellProps.cellElement.style.color = "green"; } else if (value < 0) { cellProps.cellElement.style.color = "red"; } return value; } else { cellProps.cellElement.style.color = "black"; return 0; } }, hidden: true, }, { type: 'earlystartdate', text: "Early Start", headerRenderer(context) { return `Early Start&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, align: 'center', hidden: true, format: 'MMM DD, YYYY', }, { type: 'earlyenddate', text: "Early End", headerRenderer(context) { return `Early End&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, align: 'center', hidden: true, format: 'MMM DD, YYYY', }, { type: 'latestartdate', text: "Late Start", headerRenderer(context) { return `Late Start&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, align: 'center', hidden: true, format: 'MMM DD, YYYY', }, { type: 'lateenddate', text: "Late End", headerRenderer(context) { return `Late End&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, align: 'center', hidden: true, format: 'MMM DD, YYYY', }, { type: 'totalslack', text: "Total Slack", headerRenderer(context) { return `Total Slack&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, // text: "Predecessors", width: 112, align: 'center', sortable: false, filterable: false, hidden: true, }, { text: 'Workstream Type', field: 'workstreamtype', align: 'center', width: 160, sortable: false, filterable: false, editor: { type: 'combo', items: this.workstreamOptions, displayField: 'name', valueField: '_id' }, renderer: ({ value }: { value: string }) => { const status = this.workstreamOptions.find(option => option._id === value); return status ? status.name : value; } }, { text: 'Owner', field: 'assignedTo', width: 120, align: 'center', sortable: false, filterable: false, editor: { type: 'combo', items: this.resourceOptions, displayField: 'name', valueField: 'id' }, renderer: ({ value, cellElement }) => { const owner = this.resourceOptions.find(option => option.id === value); return owner ? owner.name : "NA"; } }, { text: "Owner's Team", field: 'team', // Key from the backend headerRenderer(context) { return `Owner's Team&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, width: 150, align: 'center', hidden: this.hideOwnerTeam, sortable: false, filterable: false, editor: false, renderer: ({ value, cellElement }) => { const team = this.teamsOptions.find(option => option._id === value); return team ? team.title : "NA"; } }, { text: 'Participants', field: 'participants', width: 120, sortable: false, filterable: false, editor: { type: 'combo', items: this.resourceOptions, displayField: 'name', valueField: 'id', multiSelect: true }, renderer: ({ value, cellElement }) => { // value will be an array of IDs if (!Array.isArray(value)) return "NA"; const participants = value.map(id => this.resourceOptions.find(option => option.id === id)?.name ).filter(Boolean); // console.log("participants",participants) return participants.length ? participants.join(', ') : "NA"; }, hidden: true, }, { text: 'Task Type', field: 'tasktype', width: 120, align: 'center', sortable: false, filterable: false, editor: { type: 'combo', items: this.tasktypeOptions, displayField: 'name', valueField: '_id' }, renderer: ({ value }: { value: string }) => { const status = this.tasktypeOptions.find(option => option._id === value); return status ? status.name : value; }, hidden: true, }, { text: 'Role Type', field: 'roletype', align: 'center', width: 120, sortable: false, filterable: false, editor: { type: 'combo', items: this.roletypeOptions, displayField: 'name', valueField: '_id' }, renderer: ({ value }: { value: string }) => { const status = this.roletypeOptions.find(option => option._id === value); return status ? status.name : value; }, hidden: true, }, { text: "Planned Milestone %", field: 'plannedmilestonepercentage', // Key from the backend headerRenderer(context) { return `Planned Milestone %&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, align: 'center', width: 170, sortable: false, filterable: false, editor: false, hidden: true, }, { text: "Actual Milestone %", field: 'actualmilestonepercentage', // Key from the backend headerRenderer(context) { return `Actual Milestone %&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, align: 'center', width: 150, sortable: false, filterable: false, editor: false, hidden: true, }, { type: 'timeAxis', text: "Time Axis" }, { type: 'number', text: "Overdue Days", field: 'overdueDays', // Key from the backend headerRenderer(context) { return `Overdue Days&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, width: 150, align: 'center', sortable: false, filterable: false, editor: false, hidden: true, }, { text: 'Critical Path', field: 'isCritical', headerRenderer(context) { return `Critical Path&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, width: 150, renderer: ({ record }) => { return record.critical ? 'Yes' : 'No'; }, align: 'center', sortable: false, filterable: false, editor: false, hidden: true, }, { text: "Insights", field: "insights", headerRenderer(context) { return `Insights&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, htmlEncode: false, // Key from the backend, align: 'center', renderer: ({ record }) => { let insightValue: any = []; if (record.insights) { insightValue = StringHelper.encodeHtml(JSON.stringify(record.insights)); // Convert object to string } return `<app-slicer value='${insightValue}'></app-slicer>`; }, width: 150, sortable: false, filterable: false, editor: false, hidden: true, }, { type: 'percentdone', text: 'Percent Done', width: 70, hidden: true }, { type: 'number', text: "Wave Planned Milestone %", field: 'waveplannedmilestonepercentage', // Key from the backend headerRenderer(context) { return `Wave Planned Milestone %&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, align: 'center', width: 150, sortable: false, filterable: false, editor: false, hidden: true, }, { type: 'number', text: "Wave Actual Milestone %", field: 'waveactualmilestonepercentage', // Key from the backend headerRenderer(context) { return `Wave Actual Milestone %&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`; }, align: 'center', width: 150, sortable: false, filterable: false, editor: false, hidden: true, }, ], listeners: { }, rowReorderFeature: { showGrip: true, preserveSorters: true }, fillHandleFeature: true, cellCopyPasteFeature: { // Enable cell copy-paste explicitly enabled: true }, rowCopyPasteFeature: { enabled: true }, taskCopyPasteFeature: { useNativeClipboard: true }, taskDragFeature: true, subGridConfigs: { locked: { width: 1000, collapsed: false }, }, columnLines: false, timeAxis: { useRawData: true }, scrollButtonsFeature: { disabled: false }, rollupsFeature: { disabled: true }, dependenciesFeature: { disabled: false, radius: 50, clickWidth: 50 }, dependencyEditFeature: { disabled: false, }, baselinesFeature: { disabled: true, renderer: ({ baselineRecord, taskRecord, renderData }: any) => { // console.log("Baseline record", baselineRecord) if (baselineRecord.isScheduled && baselineRecord.endDate.getTime() + 24 * 3600 * 1000 < taskRecord.endDate.getTime()) { renderData.className['b-baseline-behind'] = 1; } else if (taskRecord.endDate < baselineRecord.endDate) { renderData.className['b-baseline-ahead'] = 1; } else { renderData.className['b-baseline-on-time'] = 1; } }, }, progressLineFeature: { disabled: true, }, headerMenuFeature: false, filterFeature: false, sortFeature: false, columnReorderFeature: { stretchedDragProxy: true }, pdfExportFeature: { disabled: false }, timeRangesFeature: { showCurrentTimeLine: false, }, labelsFeature: { left: { field: 'name', editor: { type: 'textfield' } } }, tbar: { // @ts-ignore This is an application custom widget type: 'gantttoolbar' } } this.ganttInstance.columns.on({ toggleHidden(column) { // Re-render only if necessary this.ganttInstance.refresh(); } }); this.ganttInstance.timeZone = this.selectedZone; this.columnsToBeUnSelected = ["Auto Scheduled Start", "Auto Scheduled End", "Predecessors", "Successors", "Indicator", "Delay Log", "Constraint Type", "Constraint Date", "Duration", "Baseline Start", "Baseline End", "Baseline Duration", "Baseline Duration Variance", "Early Start", "Early End", "Late Start", "Late End", "Total Slack", "Participants", "Task Type", "Role Type", "Planned Milestone %", "Actual Milestone %", "Percent Done", "Wave Planned Milestone %", "Wave Actual Milestone %", "Insights", "Overdue Days", "Critical Path" ]; // add the column name to be unselected by default this.columnsOption = _.map( _.filter(this.ganttConfig.columns, column => (column["hidden"] !== true && column["text"] != 'Time Axis') || this.columnsToBeUnSelected.includes(column['text'])), "text" ); // Add Column names here where we intially set the column to be hidden // this.columnsOption.push("Auto Scheduled Start"); // this.columnsOption.push("Auto Scheduled End"); this.columnsSelected = this.columnsOption.filter(dt => !this.columnsToBeUnSelected.includes(dt)); console.log("THIS.COLUMNS SELECTED at define bryntu:", this.columnsSelected); let value = this.waveID === null && this.versionID === null ? 2 : 3 if (this.projectLevelOptions && this.projectLevelOptions.length > 0) { setTimeout(() => { this.ganttInstance.features.tree.expandToLevel(this.milestoneLeveltask - value, true); }, 500); } }

and this is how we are sending the task record data to our db server:

startDate: moment(task.startDate).format('YYYY-MM-DD'),
endDate: moment(task.endDate).format('YYYY-MM-DD'),

Post by alex.l »

Hi,

As far as understand, it is intended to have different endDate in timezone that has DST and that doesn't, since it's different amount of hours in the same day for those regions. 23 and 24 hours, or 24 and 25 hours, depends on winter/summer change.

All the best,
Alex Lazarev

How to ask for help? Please read our Support Policy

We do not write the code in bounds of forum support. If you need help with development, contact us via bryntum.com/services


Post by gokulnaths »

Hi Alex,

Thank you for your prompt support.

We would like your help in ensuring that end dates remain consistent across different time zones. Specifically, we want to ignore users' local time zones and instead rely solely on a fixed, project-specific time zone that we have agreed upon internally. Could you please guide us on how to configure Bryntum to consistently use this project-specific time zone across all regions?

Thanks.


Post by alex.l »

All the best,
Alex Lazarev

How to ask for help? Please read our Support Policy

We do not write the code in bounds of forum support. If you need help with development, contact us via bryntum.com/services


Post by gokulnaths »

Hi Alex,

Thanks again for your support; we tried this, and this is working perfectly for the timeline headers and the respective dates there, but our request is regarding the date field columns in the Bryntum grid. Can you please help us here in this context?


Post by alex.l »

Hi,

Please post set of data you used, config you applied, expected and actual result described. I guess I missed something in a middle, can't get what might be wrong.

All the best,
Alex Lazarev

How to ask for help? Please read our Support Policy

We do not write the code in bounds of forum support. If you need help with development, contact us via bryntum.com/services


Post by gokulnaths »

Hi Alex,

We would like your help in ensuring that end dates remain consistent across different time zones. Specifically, we want to ignore users' local time zones and instead rely solely on a fixed, specific time zone that we have agreed upon internally.

Our expectation is to have the above ask in the Planned Start and Planned End Date columns [also for all the date fields] in the Bryntum grid columns.

For Example:
If the Planned End for a task from our db is : "$date": "2025-04-28T00:00:00.000Z"

Then our users in different timezones are seeing different dates and times, based on the utc conversion to their local time zone:

if user 1 is in UTC−11:00, he got the same planned end date as 2025-04-27 13:00:00
if user 2 is in UTC−05:00, he got the same planned end date as 2025-04-27 19:00:00
if user 3 is in UTC+01:00, he got the same planned end date as 2025-04-28 01:00:00
if user 4 is in UTC+05:30, he got the same planned end date as 2025-04-28 05:30:00

But in our case, we want a consistent and constant dates across all the regions, that every one should see the constant 2025-04-28T00:00:00.000.

Our Task Data:

{
  "_id": {
    "$oid": "628b9ed9268b3f737a3485ac"
  },
  "actualDuration": "",
  "actualEffort": 0,
  "actualEnvironment": [],
  "actualProgress": 0,
  "actualStorypoint": 0,
  "actualWeightage": 0,
  "createdBy": "",
  "createdOn": "",
  "inid": "b4b41d06-7bf3-4157-83e7-464b1da619e2",
  "itemID": null,
  "itemLevel": "3",
  "orderID": "1.1.1",
  "participants": [],
  "phase": {
    "$oid": "628b9ed9268b3f737a3485a9"
  },
  "plannedDuration": 16,
  "plannedEffort": 0,
  "plannedEnvironment": [],
  "plannedProgress": 0,
  "plannedStorypoint": 0,
  "plannedWeightage": 0,
  "priority": {
    "$oid": "6493e3caa61712648e0d00a5"
  },
  "savedBy": {
    "$oid": "5ccea0fb6d01b0599e0f1d47"
  },
  "savedOn": "",
  "skip": false,
  "source": "Manual",
  "status": {
    "$oid": "5c552b551c9d440000904725"
  },
  "subPhaseID": {
    "$oid": "628b9ed9268b3f737a3485aa"
  },
  "tags": [],
  "title": "Prepare",
  "type": [],
  "refTaskID": "",
  "plannedFrom": {
    "$date": "2025-04-28T00:00:00.000Z"
  },
  "plannedTo": {
    "$date": "2025-04-29T00:00:00.000Z"
  },
  "startedOn": {
    "$date": "2025-04-25T00:00:00.000Z"
  },
  "completedOn": "",
  "activePercentage": 9.09091,
  "actualLocation": "",
  "assignedTo": "",
  "ganttDuration": 1.9999999885092592,
  "parentID": "628b9ed9268b3f737a3485aa",
  "plannedLocation": "",
  "role": [],
  "workstream": [],
  "constraintDate": "",
  "constraintType": "",
  "delayLog": " ",
  "manuallyScheduled": false
}

Our Config:

ganttConfig: BryntumGanttProps = {
    dependencyIdField: 'wbsCode',
    // dependencyIdField: 'sequenceNumber',
    selectionMode: {
      row: true,
      cell: true,
      // dragSelect: true,
      rowNumber: true,
    },

preserveScrollOnDatasetChange: true,
displaySchedulingIssueResolutionPopup: false,

showTaskColorPickers: false,
showTooltip: false,

taskMenuFeature: {
  items: {
    cut: false,
    editTask: false,
    add: false,
    addTaskAbove: {
      text: 'Add Task above',
      icon: 'b-icon-up',
      onItem: ({ taskRecord }: any) => {
        console.log(taskRecord.parent, taskRecord.child, taskRecord.successor, taskRecord.predecessor)
        taskRecord.parent.insertChild({
          wbs: '1.1.1', name: 'NEW TASK ABOVE', duration: 1, taskstatus: this.newStatusId, eventColor: this.newTaskColor, manuallyScheduled: true, startDate: moment().format('YYYY-MM-DD'), endDate: moment().format('YYYY-MM-DD')
        }, taskRecord);
      }
    },
    addTenTaskAbove: {
      text: 'Add 10 Task(s) above',
      icon: 'b-icon-up',
      onItem: ({ taskRecord }: any) => {
        // console.log(taskRecord.parent, taskRecord.child, taskRecord.successor, taskRecord.predecessor)
        for (var i = 0; i < 11; i++) {
          taskRecord.parent.insertChild({
            wbs: '1.1.1', name: 'NEW TASK ABOVE', duration: 1, taskstatus: this.newStatusId, eventColor: this.newTaskColor, manuallyScheduled: true, startDate: moment().format('YYYY-MM-DD'), endDate: moment().format('YYYY-MM-DD')
          }, taskRecord);
        }
        // run propagation to calculate new task fields
        // this.ganttInstance.project.propagate();
      }
    },
    addTaskBelow: {
      text: 'Add Task below',
      icon: 'b-icon-down',
      onItem: ({ taskRecord }: any) => {
        console.log(taskRecord.parent, taskRecord.child, taskRecord.successor, taskRecord.predecessor)
        taskRecord.parent.insertChild({
          wbs: '1.1.1', name: 'NEW TASK BELOW', duration: 1, taskstatus: this.newStatusId, eventColor: this.newTaskColor, manuallyScheduled: true, startDate: moment().format('YYYY-MM-DD'), endDate: moment().format('YYYY-MM-DD')
        }, taskRecord.nextSibling);
      }
    },
    addTenTaskBelow: {
      text: 'Add 10 Task(s) below',
      icon: 'b-icon-down',
      onItem: ({ taskRecord }: any) => {
        for (var i = 0; i < 11; i++) {
          taskRecord.parent.insertChild({
            wbs: '1.1.1', name: 'NEW TASK BELOW', duration: 1, taskstatus: this.newStatusId, eventColor: this.newTaskColor, manuallyScheduled: true, startDate: moment().format('YYYY-MM-DD'), endDate: moment().format('YYYY-MM-DD')
          }, taskRecord.nextSibling);
        }
      }
    },
    delete: true,
    convertToMilestone: false,
    linkTasks: false,
    unlinkTasks: false
  }
},

projectLinesFeature: {
  disabled: true
},

cellEditFeature: {
  addNewAtEnd: false
},
cellTooltipFeature: {
  disabled: false,
  bodyCls: 'custom-css-tooltip',
  hoverDelay: 100
},

taskTooltipFeature: {
  disabled: true
  // template: ({ taskRecord }) => `${taskRecord.name}`,
  // // Tooltip configs can be used here
  // align: 'l-r' // Align left to right
},

project: {
  autoCalculatePercentDoneForParentTasks: false,
  skipNonWorkingTimeWhenSchedulingManually: true,
  skipNonWorkingTimeInDurationWhenSchedulingManually: true,
  autoSetConstraints: true,
  hoursPerDay: 23.999999999,
  daysPerWeek: 5,
  daysPerMonth: 20,
  calendar: 'general',
  calendarsData: [
    {
      id: "general",
      name: "General",
      intervals: [
        {
          recurrentStartDate: "on Sat at 0:00",
          recurrentEndDate: "on Mon at 0:00",
          isWorking: false
        }
      ]
    }
  ],
  // Let the Project know we want to use our own Task model with custom fields / methods
  // transport: {
  //   load: {
  //     url: 'assets/data/launch-motiva.json'
  //   }
  // },
  autoLoad: true,

  // assignmentStore: {
  //   data: this.simulationData.assignments.rows,
  //   useRawData: true,
  // },

  // resourceStore: {
  //   data: this.simulationData.resources.rows,
  //   useRawData: true,
  // },

  // taskStore: {
  //   data: this.simulationData.tasks.rows,
  //   useRawData: true,
  // },

  // dependencyStore: {
  //   data: this.simulationData.dependencies.rows,
  //   useRawData: true,
  // },

  timeRangeStore: {
    data: [
      {
        "id": 1,
        "name": moment().format('Do MMM YY'),
        "startDate": moment().format('YYYY-MM-DD'),
        "duration": 0,
        "durationUnit": "d",
        "cls": "b-fa b-fa-calendar-day"
      }
    ]
  },

  listeners: {

  },

  // The State TrackingManager, which the UndoRedo widget in the toolbar uses
  stm: {
    // NOTE, that this option does not enable the STM itself, this is done by the `undoredo` widget, defined in the toolbar
    // If you don't use `undoredo` widget in your app, you need to enable STM manually: `stm.enable()`,
    // otherwise, it won't be tracking changes in the data
    // It's usually best to enable STM after the initial data loading is completed.
    autoRecord: true
  },

  // This config enables response validation and dumping of found errors to the browser console.
  // It's meant to be used as a development stage helper only, so please set it to false for production systems.
  validateResponse: true,
  delayCalculation: true,
  useRawData: true
},

startDate: '2020-01-01',
endDate: '2030-01-01',
columns: [
  { type: 'sequence', text: '#', width: 50, align: 'center' },
  {
    width: 60,
    type: 'manuallyscheduled',
    text: "Manually Scheduled",
    align: 'center',
    showCheckAll: true,
    sortable: false,
    filterable: false,
    afterRenderCell({ record, widgets }: any) {
      // Hide checkboxes in certain rows
      const wbsLevel = record.wbs.split('.').length;

      // Hide the checkbox if WBS level is 1 or 2
      if (wbsLevel <= 2) {
        widgets[0].hidden = true;
      } else {
        widgets[0].hidden = false;
      }

    }
  },
  {
    type: 'name',
    width: 450,
    text: "Title",
    showWbs: this.showWBS,
    align: 'center',
    filterable: false,
    sortable: false,
    // tooltipRenderer: ({ record, column }: any) => {
    //   return StringHelper.xss`<p style='font-size:12px;margin-bottom:0px;line-height:1rem;'>${record.name}</p>`;
    // }
  },
  {
    type: 'startdate',
    format: 'DD/MM/YYYY',
    text: "Planned Start",
    align: 'center',
    width: 100,
    sortable: false,
    filterable: false,
  },
  {
    type: 'enddate',
    text: "Planned End",
    headerRenderer(context) {
      return `Planned End&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`;
    },
    align: 'center',
    width: 110,
    sortable: false,
    filterable: false,
    editor: false,  // Disable editor for all rows (can override later)
  },
  {
    type: 'duration', text: "Duration", align: 'center', sortable: false, filterable: false,
  },
  {
    type: 'date',
    text: "Actual Start",
    headerRenderer(context) {
      return `Actual Start&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`;
    },
    field: 'actualStart',
    width: 110,
    sortable: false,
    filterable: false,
    editor: false,
    format: 'DD/MM/YYYY',
    renderer: ({ record }: any) => {
      // Render the actual start date value
      return record.actualStart ? moment(record.actualStart).format('MMM D, YYYY') : '';
    }
  },
  {
    type: 'date',
    text: "Actual End",
    format: 'DD/MM/YYYY',
    headerRenderer(context) {
      return `Actual End&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`;
    },
    field: 'actualEnd',
    sortable: false,
    width: 130,
    filterable: false,
    editor: false,
    renderer: ({ record }: any) => {
      // Render the actual start date value
      return record.actualEnd ? moment(record.actualEnd).format('MMM D, YYYY') : '';
    }
  },
  {
    text: 'Owner',
    field: 'assignedTo',
    width: 120,
    align: 'center',
    sortable: false,
    filterable: false,
    editor: {
      type: 'combo',
      items: this.resourceOptions,
      displayField: 'name',
      valueField: 'id'
    },
    renderer: ({ value, cellElement }) => {
      const owner = this.resourceOptions.find(option => option.id === value);
      return owner ? owner.name : "NA";
    }
  },
  {
    type: 'predecessor',
    text: "Predecessors",
    width: 112,
    align: 'center',
    sortable: false,
    filterable: false,
    hidden: true,
  },
  {
    type: 'successor',
    text: "Successors",
    width: 112,
    align: 'center',
    sortable: false,
    filterable: false,
    hidden: true,
  },
  { type: 'constrainttype', text: "Constraint Type", hidden: true, },
  { type: 'constraintdate', text: "Constraint Date", hidden: true, },
  {
    field: 'calculatedStartDate',
    text: "Auto Scheduled Start",
    width: 130,
    headerRenderer(context) {
      return `Auto Scheduled Start&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`;
    },
    align: 'center',
    sortable: false,
    filterable: false,
    editor: false,
    hidden: true,
  },
  {
    field: 'calculatedEndDate',
    text: "Auto Scheduled End",
    width: 110,
    headerRenderer(context) {
      return `Auto Scheduled End&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`;
    },
    align: 'center',
    sortable: false,
    filterable: false,
    editor: false,
    hidden: true,
    format: 'MMM DD, YYYY',
    // locked:true,
  },
  {
    field: 'baselineStart',
    text: "Baseline Start",
    headerRenderer(context) {
      return `Baseline Start&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`;
    },
    align: 'center',
    width: 120,
    sortable: false,
    filterable: false,
    editor: false,
    renderer: ({ record }: any) => {
      // Render the actual start date value
      return record.baselineStart ? moment(record.baselineStart).format('MMM D, YYYY') : '';
    },
    hidden: true,
  },
  {
    field: 'baselineEnd',
    text: "Baseline End",
    headerRenderer(context) {
      return `Baseline End&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`;
    },
    align: 'center',
    width: 110,
    sortable: false,
    filterable: false,
    editor: false,
    renderer: ({ record }: any) => {
      // Render the actual start date value
      return record.baselineEnd ? moment(record.baselineEnd).format('MMM D, YYYY') : '';
    },
    hidden: true,
  },
  {
    field: 'baselineDuration', text: "Baseline Duration", align: 'center', sortable: false, filterable: false, hidden: true,
  },
  {
    field: 'baselineDurationVariance',
    text: "Baseline Duration Variance",
    headerRenderer(context) {
      return `Baseline Duration Variance&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`;
    },
    align: 'center',
    width: 140,
    sortable: false,
    filterable: false,
    editor: false,
    renderer: (cellProps: { record: any, cellElement: any, value: any }) => {
      // Reset any existing styles or classes
      let value = cellProps.value

      if (value) {
        if (value > 0) {
          cellProps.cellElement.style.color = "green";
        } else if (value < 0) {
          cellProps.cellElement.style.color = "red";
        }
        return value;
      } else {
        cellProps.cellElement.style.color = "black";
        return 0;
      }
    },
    hidden: true,
  },
  {
    type: 'earlystartdate',
    text: "Early Start",
    headerRenderer(context) {
      return `Early Start&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`;
    },
    align: 'center',
    hidden: true,
  },
  {
    type: 'earlyenddate',
    text: "Early End",
    headerRenderer(context) {
      return `Early End&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`;
    },
    align: 'center',
    hidden: true,
  },
  {
    type: 'latestartdate',
    text: "Late Start",
    headerRenderer(context) {
      return `Late Start&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`;
    },
    align: 'center',
    hidden: true,
  },
  {
    type: 'lateenddate',
    text: "Late End",
    headerRenderer(context) {
      return `Late End&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`;
    },
    align: 'center',
    hidden: true,
  },
  {
    type: 'totalslack',
    text: "Total Slack",
    headerRenderer(context) {
      return `Total Slack&nbsp;&nbsp;<i class="fas fa-lock" style="color:red"></i>`;
    },
    // text: "Predecessors",
    width: 112,
    align: 'center',
    sortable: false,
    filterable: false,
    renderer:({value})=>{
      // console.log("AT TOTAL SLCAK");
      // console.log("value:",value);
      // console.log("ROundvalue:",Math.round(value));
      // console.log("parse int:",parseInt(`${value}`));
      // console.log("ceil value:",Math.ceil(value));
      let decimalPart = value % 1; // Get decimal part
      // 0.0417 -> 1Hr of a day [ if the decimal part is >= 0.0417 the next round off value should be displayed]
      var finalValue = decimalPart >= 0.0417 ? Math.ceil(value) : Math.floor(value);
      // console.log("Fin value:",finalValue);
      // console.log("xxxxxxxxxxxxxxxxxxxxxxxxxxxx")
      return `${finalValue} days`;
      // console.log("xxxxxxxxxxxxxxxxxxxxxxxxxxxx")
    },
    hidden: true,
  },
  { type: 'timeAxis', text: "Time Axis" },
],

listeners: {
},

rowReorderFeature: {
  showGrip: true,
  preserveSorters: true
},

fillHandleFeature: true,
cellCopyPasteFeature: true,
rowCopyPasteFeature: {
  rowOptionsOnCellContextMenu: true
},
taskCopyPasteFeature: {
  useNativeClipboard: true
},
taskDragFeature: true,

subGridConfigs: {
  locked: { width: 1000, collapsed: false },
},

columnLines: false,

timeAxis: {
  useRawData: true
},

scrollButtonsFeature: {
  disabled: false
},

rollupsFeature: {
  disabled: true
},

dependenciesFeature: {
  disabled: false,
  radius: 50,
  clickWidth: 50
},

dependencyEditFeature: {
  disabled: false,
},

baselinesFeature: {
  disabled: true,
  renderer: ({ baselineRecord, taskRecord, renderData }: any) => {
    // console.log("Baseline record", baselineRecord)
    if (baselineRecord.isScheduled && baselineRecord.endDate.getTime() + 24 * 3600 * 1000 < taskRecord.endDate.getTime()) {
      renderData.className['b-baseline-behind'] = 1;
    }
    else if (taskRecord.endDate < baselineRecord.endDate) {
      renderData.className['b-baseline-ahead'] = 1;
    }
    else {
      renderData.className['b-baseline-on-time'] = 1;
    }
  },
},

progressLineFeature: {
  disabled: true,
},

headerMenuFeature: false,

filterFeature: false,
sortFeature: false,

columnReorderFeature: {
  stretchedDragProxy: true
},

pdfExportFeature: {
  disabled: false
},

timeRangesFeature: {
  showCurrentTimeLine: false,
},

labelsFeature: {
  left: {
    field: 'name',
    editor: {
      type: 'textfield'
    }
  }
},

tbar: {
  // @ts-ignore This is an application custom widget
  type: 'gantttoolbar'
}
  }
Last edited by gokulnaths on Wed Apr 30, 2025 12:21 pm, edited 1 time in total.

Post by joakim.l »

When you provide dates to the Gantt, it will create native JS Date objects, which means they will be interpreted or converted to the time zone on the client.

To be able to show a time zone different than the one on the client, there is the timeZone config. This will convert the neccessary date fields to the specified time zone (technically it just changes the time). It will not, however, convert any additional fields you are using.

Do I understand the problem correctly?

Regards
Joakim


Post by gokulnaths »

Hi @Joakim,

Thanks for your response — yes, you’ve understood the issue correctly.

To clarify further: our requirement is to ensure that all users, regardless of their local time zones, see consistent and fixed dates in the Gantt chart, specifically in the Planned Start and Planned End fields (and any other custom date fields we may use). For example, if the backend provides 2025-04-28T00:00:00.000Z, we want this exact date and time to be displayed consistently to all users, without being affected by their browser’s time zone.

We understand that the timeZone config can handle the built-in Gantt fields. However, we would like to know how we can extend this behavior to apply the same time zone conversion logic to our custom or additional date fields (like Planned Start, Planned End, etc.) that are part of our column configuration.

Could you please advise on how we can achieve this, or if there’s a recommended approach or API hook we should use to ensure consistent date rendering across all relevant fields?

Thanks again for your support.


Post by joakim.l »

Hi!

We do not support that, currently. It sounds like something we should implement, however, so I've created a feature ticket here: https://github.com/bryntum/support/issues/11283

While waiting, I would recommend trying to handle the conversion manually.
Use the TimeZoneHelper's https://bryntum.com/products/gantt/docs/api/Core/helper/TimeZoneHelper#function-toTimeZone-static function to convert the incoming data. Where and when depends on how you load the data. Please ask again, if you need help on that.

tasks.forEach(task => task.myDate = TimeZoneHelper.toTimeZone(task.myDate, 'myIANATimeZone')

And also, you need to convert the dates back when you sync/save your data. Same goes here, ask again if you need help on how to do stuff before sync/save depending on how you do this in your app.

tasks.forEach(task => task.myDate = TimeZoneHelper.fromTimeZone(task.myDate, 'myIANATimeZone')

Regards
Joakim


Post Reply