Our state of the art Gantt chart


Post by revill »

Hi,
I'm trying to achieve similar functionality as it is in the https://www.bryntum.com/products/gantt/examples/split-tasks/
In my Salesforce project, I would like to display the phases of the task in one row, not in separate rows.
Based on the example mentioned above I have added a segments array to my Task structure which looks like that right now:

              this.projects.forEach(element => {
                    let children = [];
                    for ( const phase of phases) {
                        let startDate = element[phase.Start_Date_Field__c];
                        let endDate = element[phase.End_Date_Field__c];
                        if (endDate != undefined && endDate != null) {
                            let phaseRow = {"id" : phase.Name + idNumber,"rollup" : true, "name" : phase.Name, "startDate" : startDate, "endDate" : endDate,"manuallyScheduled": true };
                            children.push(phaseRow);
                        }
                    }

                let projectStartDate = element.sitetracker__Project_Start_Date_A__c;
                let projectEndDate = element.Start_of_construction_A__c;
                if (projectEndDate != undefined && projectEndDate != null && projectStartDate != undefined) {
                    let taskRow = {"id" : idNumber,"rollup" : true, "segments" :children, "manuallyScheduled": true,"name" : element.sitetracker__Site__r.Name,"startDate" : projectStartDate, "endDate" : projectEndDate, "salesforceId" : element.Id, "expanded" : true,"ProjectTemplate" : element.sitetracker__Project_Template__c, "Country" : element.Country__c, "ProjectTemplate" : element.sitetracker__Project_Template__c, "GCBudgetCode" : element.GC_Budget_Code__c, "ConstructionBudget" : element.Construction_Budget_Code__c, "ProjectStatus" : element.sitetracker__Project_Status__c};
                    projectTasks.push(taskRow);
                    idNumber++;
                }
            });

and also added a taskRenderer function inside Gantt:

const project = new bryntum.gantt.ProjectModel({
            startDate : startDate,
            calendar: data.project.calendar,
            tasksData: tasks,
        });
            
let columns = [ { type: "name", width: 250 }, { type: "startdate" }, { type: "enddate", field : 'endDate' }, { type: "duration" }, { text : "country", hidden : false, field : 'Country' }, { text : "Full Site Name", hidden : this.hideFullSiteName, field : 'FullSiteName'}, { text : "Project Template" , hidden : this.hideProjectTemplate, field : 'ProjectTemplate'}, { text : "GC Budget Code", hidden : this.hideGCBudgetCodeCol, field : 'GCBudgetCode'}, { text : "Construction Budet Code", hidden : this.hideConstructionBudgetCol, field : 'ConstructionBudget'}, { text : "Project Status" , hidden : this.hideProjectStatusCol, field : 'ProjectStatus' }, { type: "addnew" } ]; const gantt = new bryntum.gantt.Gantt({ project, appendTo: this.template.querySelector(".container"), startDate: startDate, cls : 'custom-styling', // Custom task content taskRenderer({ taskRecord }) { console.log('Test taskRenderer ', taskRecord); console.log('Test taskRenderer isEventSegment ', taskRecord.isEventSegment); console.log('Test taskRenderer.name ', taskRecord.name); // Display segment names if (taskRecord.isEventSegment) { return bryntum.gantt.StringHelper(taskRecord.name); } return ''; }, tbar : [ { type: 'button', text : 'Expand All', onClick : (event) => { const gantevent = event.source.up('gantt'); gantevent.expandAll(); } }, { type : 'button', text : 'Collapse All', onClick : (event) => { const collapseEvent = event.source.up ('gantt'); collapseEvent.collapseAll(); } } ], dependencyIdField: "sequenceNumber", columns: columns, subGridConfigs: { locked: { flex: 3 }, normal: { flex: 4 } }, columnLines: false, features: { cellEdit : false, taskEdit : false, taskMenu : false, taskDrag : false, taskResize : false, taskDragCreate : false, rollups: { disabled: true }, baselines: { disabled: true }, progressLine: { disabled: true, statusDate: new Date(2019, 0, 25) }, criticalPaths : false, filter: true, dependencyEdit: true, timeRanges: { showCurrentTimeLine: true }, labels: { left: { field: "name", editor: { type: "textfield" } } } } });

but I'm getting an error that I don't know how to solve:

Uncaught (in promise) TypeError: Class constructor N cannot be invoked without 'new'
    at EF.taskRenderer (gantt_component.js:1:25451)
    at EB.internalPopulateTaskRenderData (gantt.lwc.module.js:53:1655986)
    at Aq.generateSegmentRenderData (gantt.lwc.module.js:53:1247230)
    at eval (gantt.lwc.module.js:53:1248426)
    at Array.map (<anonymous>)
    at Aq.appendDOMConfig (gantt.lwc.module.js:53:1248388)
    at Aq.onTaskDataGenerated (gantt.lwc.module.js:53:1249284)
    at functionChainRunner (gantt.lwc.module.js:10:903135)
    at EM.<computed> [as onTaskDataGenerated] (gantt.lwc.module.js:10:902667)
    at EB.populateTaskRenderData (gantt.lwc.module.js:53:1657572)

Thank You in advance


Post by mats »

You made a mistake here:

                return bryntum.gantt.StringHelper(taskRecord.name);

should be

                return bryntum.gantt.StringHelper.encodeHtml(taskRecord.name);

Post by revill »

Thanks, Mats. I must be blind.

Unfortunately I have another issue related to the task splitting.
It is splitting tasks properly if I'm defining segments manually like that:

                        segments.push({"name" : "DDOs", "startDate" : "2022-01-14", "endDate" : "2022-03-18"},{"name" : "Ports", "startDate" : "2023-01-21", "endDate" : "2023-06-22"});
                            projectRow = {"id" : idNumber,"manuallyScheduled": true, "segments" : segments, "rollup" : true, "name" : element.sitetracker__Site__r.Name, "endDate" : projectEndDate, "salesforceId" : element.Id, "expanded" : true};

If I'm trying to build a segments array in a loop using fields from a different object it is not working and it just displays the whole project bar in the row

                    for ( const phase of phases) {
                        let forecastFieldStartField = this.checkActualForecastField(phase.Start_Date_Field__c,element);
                        let forecastFieldEndField = this.checkActualForecastField(phase.End_Date_Field__c,element);
                        let startDate = element[phase.Start_Date_Field__c] == undefined ? element[forecastFieldStartField] : element[phase.Start_Date_Field__c];
                        let endDate = element[phase.End_Date_Field__c] == undefined ? element[forecastFieldEndField] : element[phase.End_Date_Field__c];
                        if (endDate != undefined && endDate != null) {
                            segments.push({"name" : phase.Name, "startDate" : startDate, "endDate" : endDate});
                        }
                    }

but if I add to this array segments created manually as it is in the first part of the code, it's displaying splited tasks in the Gantt chart.

I'm struggling with this issue for a longer time and right now I don't have any more ideas on what to do, to make it work with the segments created in the loop with the data taken from different object.


Post by mats »

Can you please wrap this code up in a form that we can take it and run it locally?


Post by revill »

I tried to do that, but originally it uses data from the Salesforce org, but when I'm trying to use mock data defined inside the component to provide a working example, it is working properly as expected and Project is splitted into parts.

In the meantime, I found out that when I'm adding segments defined in the loop, there is some recalculation of the endDate of the project which is not the expected end date. I have set in the projectRow attribute "manuallyScheduled" as true, but it seems to not work when the segments created dynamically are added. If segments are written manually, then recalculation is not executed and task splitting works as expected.

                            projectRow = {"id" : idNumber,"manuallyScheduled": true, "segments" : segments, "rollup" : true, "name" : element.sitetracker__Site__r.Name, "endDate" : projectEndDate, "salesforceId" : element.Id, "expanded" : true};

I'm not sure, but it looks to me like there is some problem with the date format when it's taken from the external object. I tried to pass it to the segment definition as a string or as a Date object, but it still was not working properly for me.


Post by alex.l »

Hi revill,

We need a code example to help you with that. We need to reproduce that for debugging.

I found out that when I'm adding segments defined in the loop, there is some recalculation of the endDate of the project which is not the expected end date.

Can't you collect segments and set them all at once outside of your loop?

All the best,
Alex


Post by revill »

Hi Alex,
I'm collecting segments in the loops and later in the main loop for a project I'm adding a segment array for each project row.

Please, find the code below. In this version, splitting task is not working properly. But if I add commented line nr 289 it is working properly.

/* globals bryntum : true */
import { LightningElement, api, wire } from "lwc";
import { ShowToastEvent } from "lightning/platformShowToastEvent";
import { loadScript, loadStyle } from "lightning/platformResourceLoader";
import GANTT from "@salesforce/resourceUrl/bryntum_gantt";
import GanttToolbarMixin from "./lib/GanttToolbar";
import data from './data/launch-saas'
import getProjects from '@salesforce/apex/GanttChartApexHandler.getProjects';
import getGanttChartConfigurationsWrapper from '@salesforce/apex/GanttChartApexHandler.getGanttChartConfigurationsWrapper'
import { getObjectInfo } from 'lightning/uiObjectInfoApi';
import PHASE_EVENT_CONFIG_OBJECT from '@salesforce/schema/Phase_Event_Gantt_Chart__c';

export default class Gantt_component extends LightningElement {
    projects;
    error;
    hideCountryColumn = true;
    hideFullSiteName = true;
    hideProjectTemplate = true;
    hideGCBudgetCodeCol = true;
    hideConstructionBudgetCol = true;
    hideProjectStatusCol = true;
    configurationList = [];
    currentConfigurationUsed;
    currentconfigPhasesAndEvents;
    configurationNames = [];
    mapOfConfigurationsByname = new Map();

@wire(getObjectInfo, { objectApiName: PHASE_EVENT_CONFIG_OBJECT })
phaseEventObjectInfo;

get recordTypeId() {
    // Returns a map of record type Ids 
    const rtis = this.phaseEventObjectInfo.data.recordTypeInfos;
    console.log('recordTypeId map : ',rtis );
    return Object.keys(rtis).find(rti => rtis[rti].name === 'Phase');
}


renderedCallback() {
    if (this.bryntumInitialized) {
        return;
    }
    this.bryntumInitialized = true;

    Promise.all([
        loadScript(this, GANTT + "/gantt.lwc.module.js"),
        loadStyle(this, GANTT + "/gantt.stockholm.css")
    ])
        .then(() => {
            this.handleConfigurations();
            this.handleProjectLoad();
        })
        .catch(error => {
            this.dispatchEvent(
                new ShowToastEvent({
                    title: "Error loading Bryntum Gantt",
                    message: error,
                    variant: "error"
                })
            );
        });
}

createGantt(tasks, startDate) {
    console.log('tasks', tasks);
    const GanttToolbar = GanttToolbarMixin(bryntum.gantt.Toolbar);
    const project = new bryntum.gantt.ProjectModel({
        startDate : startDate,
        calendar: data.project.calendar,
        tasksData: tasks
    });
        
    let columns = [
        { type: "name", width: 250 },
        { type: "startdate" },
        { type: "enddate",  field : 'endDate' },
        { type: "duration" },
        { text  : "country", hidden : false, field : 'Country' },
        { text  : "Full Site Name", hidden : this.hideFullSiteName,  field : 'FullSiteName'},
        { text  : "Project Template" , hidden : this.hideProjectTemplate, field : 'ProjectTemplate'},
        { text  : "GC Budget Code", hidden : this.hideGCBudgetCodeCol, field : 'GCBudgetCode'},
        { text  : "Construction Budet Code", hidden : this.hideConstructionBudgetCol, field : 'ConstructionBudget'},
        { text  : "Project Status" , hidden : this.hideProjectStatusCol, field : 'ProjectStatus' },
        { type: "addnew" }
    ];

    const gantt = new bryntum.gantt.Gantt({
        project,
        appendTo: this.template.querySelector(".container"),
        startDate: startDate,
        cls : 'custom-styling',
        taskRenderer({ taskRecord }) {
            console.log('Test taskRenderer isEventSegment ', taskRecord.isEventSegment + ' ' + taskRecord.name);

            // Display segment names
            if (taskRecord.isEventSegment) {
                return bryntum.gantt.StringHelper.encodeHtml(taskRecord.name);
            }

            return '';
        },
        columns: columns,
        subGridConfigs: {
            locked: {
                flex: 3
            },
            normal: {
                flex: 4
            }
        },
        tbar : [
            {
                type : 'button',
                text : '<<',
                onClick : (event) => {
                    const collapseEvent = event.source.up ('gantt');
                    collapseEvent.shiftPrevious(); 
                }
            },
            {
                type : 'button',
                text : '>>',
                onClick : (event) => {
                    const collapseEvent = event.source.up ('gantt');
                    collapseEvent.shiftNext(); 
                }
            },
            {
                type : 'button',
                text : 'Zoom to fit',
                onClick : (event) => {
                    const collapseEvent = event.source.up ('gantt');
                    collapseEvent.zoomToFit(); 
                }
            },
            {
                type : 'button',
                text : '+',
                onClick : (event) => {
                    const collapseEvent = event.source.up ('gantt');
                    collapseEvent.zoomIn(); 
                }
            },{
                type : 'button',
                text : '-',
                onClick : (event) => {
                    const collapseEvent = event.source.up ('gantt');
                    collapseEvent.zoomOut(); 
                }
            }                   
        ],
        columnLines: false,
        features: {
            cellEdit       : false,
            taskEdit       : false,
            taskMenu       : false,
            taskDrag       : false,
            taskResize     : false,
            taskDragCreate : false,
            rollups: {
                disabled: true
            },
            baselines: {
                disabled: true
            },
            progressLine: {
                disabled: true,
                statusDate: new Date(2019, 0, 25)
            },
            criticalPaths : false,
            filter: true,
            dependencyEdit: true,
            timeRanges: {
                showCurrentTimeLine: true
            },
            labels: {
                left: {
                    field: "name",
                    editor: {
                        type: "textfield"
                    }
                }
            }
        }
    });

    project.commitAsync().then(() => {
        // console.timeEnd("load data");
        const stm = gantt.project.stm;

        stm.enable();
        stm.autoRecord = true;

        // let's track scheduling conflicts happened
        project.on("schedulingconflict", context => {
            // show notification to user
            bryntum.gantt.Toast.show(
                "Scheduling conflict has happened ..recent changes were reverted"
            );
            // as the conflict resolution approach let's simply cancel the changes
            context.continueWithResolutionResult(
                bryntum.gantt.EffectResolutionResult.Cancel
            );
        });
    });
}

handleProjectLoad() {
    getProjects()
    .then((result) => {
        let phases = [];
        let events = [];
        console.log('test this.currentconfigPhasesAndEvents ',this.currentconfigPhasesAndEvents);

        this.currentconfigPhasesAndEvents.forEach (element => {
            if (element.RecordTypeId == this.recordTypeId) {
                phases.push (element);
            } else {
                events.push (element);
            }
        })

        this.projects = result;
        this.error = undefined;
            let projectTasks = new Array;
            let idNumber = 1;
            let earliestStartDate = new Date();
            console.log('test this.projects ',this.projects);

            this.projects.forEach(element => {
                let segments = [];
                for ( const phase of phases) {
                    if ((phase.Name === 'Grid Connection' || phase.Name === 'Building Permit') && (!element.Grid_Connection_required__c || !element.Building_Permit_required__c) ) {
                        continue;
                    }
                    let forecastFieldStartField = this.checkActualForecastField(phase.Start_Date_Field__c,element);
                    let forecastFieldEndField = this.checkActualForecastField(phase.End_Date_Field__c,element);
                    let startDate = element[phase.Start_Date_Field__c] == undefined ? element[forecastFieldStartField] : element[phase.Start_Date_Field__c];
                    let endDate = element[phase.End_Date_Field__c] == undefined ? element[forecastFieldEndField] : element[phase.End_Date_Field__c];
                    if (endDate != undefined && endDate != null) {
                        let parsedDate = new Date(startDate);
                        if (parsedDate.getMonth() == '0' && parsedDate.getDate() == '1' && parsedDate.getFullYear() == '1900') {
                            console.log('continue parsed date parsedDate: ',parsedDate.getDate());
                            continue;
                        }
                        let phaseEndDate = new Date(endDate);
                        let phaseStartDate = new Date(startDate);
                        let phaseOneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds
                        let durationPhase = 0;
                        if (phaseEndDate !== undefined && phaseStartDate !== undefined) {
                            durationPhase = Math.round(Math.abs((phaseEndDate.getTime() - phaseStartDate.getTime()) / phaseOneDay));
                        }

                        segments.push({"name" : phase.Name, "duration" : durationPhase, "startDate" : phaseStartDate, "endDate" : phaseEndDate});
                    }
                }

                for (const event of events) {
                    let forecastFieldStartField = this.checkActualForecastField(event.Start_Date_Field__c,element);
                    let startDate = element[event.Start_Date_Field__c] == undefined ? element[forecastFieldStartField] : element[event.Start_Date_Field__c];
                    if (startDate != undefined && startDate != null) {
                        let parsedDate = new Date(startDate);
                        if (parsedDate.getMonth() == '0' && parsedDate.getDate() == '1' && parsedDate.getFullYear() == '1900') {
                            console.log('continue parsed date parsedDate: ',parsedDate.getDate());
                            continue;
                        }

                        let endEventDate = new Date(startDate);
                        let newstartEventDate = new Date(startDate);

                        endEventDate.setDate(newstartEventDate.getDate() + 1);
                        segments.push({"name" : event.Name, "startDate" : newstartEventDate, "duration" : 1});
                    }
                }
                
                let projectStartDate =  element.sitetracker__Project_Start_Date_A__c == undefined ? element.sitetracker__Project_Start_Date_F__c : element.sitetracker__Project_Start_Date_A__c;
                let projectEndDate = element.Start_of_construction_A__c == undefined ? element.Start_of_construction_F__c : element.Start_of_construction_A__c;

                const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds
                let duration = 0;
                if (projectEndDate !== undefined && projectStartDate !== undefined) {
                    duration = Math.round(Math.abs((new Date(projectEndDate).getTime() - new Date(projectStartDate).getTime()) / oneDay));
                }

                if (projectEndDate != undefined && projectEndDate != null && projectStartDate != undefined) {
                    let parsedDate = new Date(projectStartDate);
                    let parsedEndDate = new Date(projectEndDate);
                    let projectRow;
                    // segments.push({"name" : "DDOs", "startDate" : "2022-01-14", "endDate" : "2022-03-18"},{"name" : "Ports", "startDate" : "2023-01-21", "endDate" : "2023-06-22"});

                    if (parsedDate.getDate() =='1' && parsedDate.getMonth() == '0' && parsedDate.getFullYear() == '1900') {
                        projectRow = {"id" : idNumber,"manuallyScheduled": true,  autoCalculateEndDate : false,  "duration" : duration, "segments" : segments, "name" : element.sitetracker__Site__r.Name, "endDate" : parsedEndDate, "salesforceId" : element.Id, "ProjectTemplate" : element.sitetracker__Project_Template__c, "Country" : element.Country__c, "ProjectTemplate" : element.sitetracker__Project_Template__c, "GCBudgetCode" : element.GC_Budget_Code__c, "ConstructionBudget" : element.Construction_Budget_Code__c, "ProjectStatus" : element.sitetracker__Project_Status__c};
                    } else {
                        if (parsedDate < earliestStartDate) {
                            earliestStartDate = parsedDate;
                        }
                        projectRow = {"endDate" : parsedEndDate, autoCalculateEndDate : false, "id" : idNumber, "duration" : duration, "manuallyScheduled": true, "name" : element.sitetracker__Site__r.Name, "segments" : segments, "startDate" : parsedDate };//, "children" : children};
                    }
                    projectTasks.push(projectRow);
                    idNumber++;
                }
            });

            this.createGantt(projectTasks, earliestStartDate);
    })
    .catch((error) => {
        this.error = error;
        console.log('ERROR: ', error);
        this.projects = undefined;
    });
};

checkActualForecastField (fieldActual, object) {
    const sufix = 'F__c';
    if (fieldActual != null && fieldActual != undefined && object != null && object != undefined) {
        fieldActual += '';
        let fieldNameWithoutSufix = fieldActual.substring(0, fieldActual.length-4);
        console.log('fild name without sufx ', fieldNameWithoutSufix);
        return fieldNameWithoutSufix.concat(sufix);
    }
};

handleConfigurations() {
    let configwrapper;
    getGanttChartConfigurationsWrapper()
    .then((configWrapper) => {
        let mainConfigObjects = configWrapper.configurations;
        this.configurationList = configWrapper.configurations;
        if (this.configurationNames?.length === 0 || this.mapOfConfigurationsByname.length === 0) {
            console.log('test vconfigWrapper.configurations ',configWrapper.configurations);
            configWrapper.configurations.forEach(element => {    
                if (element?.Name != undefined) {
                    this.configurationNames.push(element.Name);
                    this.mapOfConfigurationsByname.set(element.Name, element);
                }
            })
            this.currentConfigurationUsed = mainConfigObjects[0];   
        }
       

        this.currentconfigPhasesAndEvents = configWrapper.phaseEvents;
        let fieldsToBeDisplayed = this.currentConfigurationUsed.Display_Fields__c;
        let groupByField = this.currentConfigurationUsed.Grouping_By__c;
    })
    .catch((error) => {
        console.log('error: ', error);
        this.error = error;
        this.configWrapper = undefined;
    });
}
}

I have tried to mock the project's data, but when I'm mocking a structure like that:

                 this.projects = [{"sitetracker__Site__r" : {Name: 'mock1', Id: 'a0p7Q000000YWVWQA4'},
                 "Opening_of_Site_A" : new Date ("2023-05-12"),
                 "Hand_in_final_Building_Permit_A" : new Date ("2022-01-24"),
                 "Building_Permit_Approved_A" : new Date ("2022-03-25"),
                 "Location_Agreement_signed_by_all_A" : new Date ("2022-03-25"),
                 "Contract_signed_by_Fastned_A" : new Date ("2021-10-22"), "sitetracker__Project_Start_Date_A__c" : new Date ("2021-03-11"), "Start_of_construction_A__c" :  new Date ("2023-04-09")},{"sitetracker__Site__r" : {Name: 'mock2', Id: 'a0p7Q000000YWVWQA4'},"Opening_of_Site_A" :  new Date ("2023-05-12"),
                "Hand_in_final_Building_Permit_A" : new Date ("2022-01-24"),
                "Building_Permit_Approved_A" : new Date ("2022-03-25"),
                "Location_Agreement_signed_by_all_A" : new Date ("2022-03-25"),
 "sitetracker__Project_Start_Date_A__c" : new Date ("2021-03-11"), "Start_of_construction_A__c" :  new Date ("2023-04-09")},{"sitetracker__Site__r" : {Name: 'mock3', Id: 'a0p7Q000000YWVWQA4'},"Opening_of_Site_A" :  new Date ("2023-05-12"),
                "Hand_in_final_Building_Permit_A" : new Date ("2022-01-24"),
                "Building_Permit_Approved_A" : new Date ("2022-03-25"),
                "Location_Agreement_signed_by_all_A" : new Date ("2022-03-25"),
                "Contract_signed_by_Fastned_A" : new Date ("2021-10-22"), "sitetracker__Project_Start_Date_A__c" : new Date ("2022-03-11"), "Start_of_construction_A__c" :  new Date ("2024-04-09")}];

I cannot reproduce the same behavior as I have when I use data from the Salesforce org. With mock data, it is splitted correctly.


Post by alex.l »

Maybe the problem in data format then? Looks like when you mock data, you use that you expect to have, but in real app it's different.
Try to put a breakpoint and check what's actually came from the server? First of all, If it's Date objects or some formatted strings that cannot be parsed correctly.

All the best,
Alex


Post by revill »

I thought so, but I'm not able to find the issue. I have checked the date format and it's retrieved from server as a string in the format "yyyy-mm-dd". I was trying to pass it like that, or parse it to a Date object and it still doesn't work.
When I'm checking it in the debugger both version looks exactly the same. Here first is with data from server only which is not working:
Image
And here are added just 2 segments manually and with those it is working:
Image

I have no idea why. And why adding this two segments created manually is causing that the endDate recalculation is working again :/


Post by alex.l »

Hi,

We analysed the information provided. Unfortunately, we are out of ideas now. We need to see dataset you used to replicate the problem and debug it. Could you please attach JSON here and provide steps to reproduce?

All the best,
Alex


Post Reply