You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ofbiz.apache.org by jl...@apache.org on 2026/04/20 15:33:52 UTC

(ofbiz-plugins) branch release24.09 updated: Fixed: jsgantt-improved bloks qs.js update (OFBIZ-13339)

This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch release24.09
in repository https://gitbox.apache.org/repos/asf/ofbiz-plugins.git


The following commit(s) were added to refs/heads/release24.09 by this push:
     new 6055fad45 Fixed: jsgantt-improved bloks qs.js update (OFBIZ-13339)
6055fad45 is described below

commit 6055fad455a59c0360b28c228d6aa4a850e4367f
Author: Jacques Le Roux <ja...@les7arts.com>
AuthorDate: Thu Apr 16 19:52:07 2026 +0200

    Fixed: jsgantt-improved bloks qs.js update (OFBIZ-13339)
    
    Reintegrates Gantt functionality into projectmgr application while updating
    jsgantt-improved to its secure version 3.0.0.
    
    This update fixes security issues.
---
 projectmgr/README.md                               |   1 +
 projectmgr/build.gradle                            |  26 +++
 projectmgr/config/ProjectMgrUiLabels.xml           |  24 +++
 .../src/docs/asciidoc/_include/prjm_project.adoc   |   1 +
 .../org/apache/ofbiz/projectmgr/GanttChart.groovy  | 177 +++++++++++++++++++++
 projectmgr/template/project/GanttChart.ftl         |  26 +++
 .../webapp/projectmgr/WEB-INF/controller.xml       |   6 +
 projectmgr/webapp/projectmgr/WEB-INF/web.xml       |   2 +-
 projectmgr/webapp/projectmgr/package-lock.json     |  38 +++++
 projectmgr/webapp/projectmgr/package.json          |   9 ++
 projectmgr/webapp/projectmgr/static/projectmgr.css |  25 +++
 projectmgr/webapp/projectmgr/static/projectmgr.js  |  40 +++++
 projectmgr/widget/CommonScreens.xml                |   1 +
 projectmgr/widget/ProjectMenus.xml                 |   8 +
 projectmgr/widget/ProjectScreens.xml               |  32 ++++
 15 files changed, 415 insertions(+), 1 deletion(-)

diff --git a/projectmgr/README.md b/projectmgr/README.md
index 9ffa2dda5..dec6f01f3 100644
--- a/projectmgr/README.md
+++ b/projectmgr/README.md
@@ -13,6 +13,7 @@ The Project Management application enables organisations to manage their interna
     Resource assignement to projects
     Resource allocation to tasks
     Approval of time spent on tasks
+    Gantt charts for projects, phases and tasks
     Time registration with different rates
     Generate invoice from a project
     Project copy
diff --git a/projectmgr/build.gradle b/projectmgr/build.gradle
new file mode 100644
index 000000000..6e020480f
--- /dev/null
+++ b/projectmgr/build.gradle
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+plugins {
+    id 'ofbiz-node-conventions'
+}
+
+node {
+    nodeProjectDir = file("webapp/projectmgr")
+}
diff --git a/projectmgr/config/ProjectMgrUiLabels.xml b/projectmgr/config/ProjectMgrUiLabels.xml
index b369123db..b07a11874 100644
--- a/projectmgr/config/ProjectMgrUiLabels.xml
+++ b/projectmgr/config/ProjectMgrUiLabels.xml
@@ -833,6 +833,18 @@
         <value xml:lang="zh">项目账单</value>
         <value xml:lang="zh-TW">專案帳單</value>
     </property>
+    <property key="PageTitleProjectGanttChart">
+        <value xml:lang="de">Projekt Gantt Chart</value>
+        <value xml:lang="en">Project Gantt Chart</value>
+        <value xml:lang="fr">Graphe Gantt du projet</value>
+        <value xml:lang="it">Progetto grafico gantt</value>
+        <value xml:lang="ja">プロジェクトガントチャート</value>
+        <value xml:lang="nl">Gant chart</value>
+        <value xml:lang="th">แผนผังคุมการกำหนดโปรเจค</value>
+        <value xml:lang="vi">Biểu đồ CHUNG của dự án</value>
+        <value xml:lang="zh">项目甘特图</value>
+        <value xml:lang="zh-TW">專案甘特圖</value>
+    </property>
     <property key="PageTitleProjectInformation">
         <value xml:lang="de">Projektinformationen</value>
         <value xml:lang="en">Project Information</value>
@@ -1494,6 +1506,18 @@
         <value xml:lang="zh">周五</value>
         <value xml:lang="zh-TW">週五</value>
     </property>
+    <property key="ProjectMgrGanttChart">
+        <value xml:lang="de">Gantt Chart</value>
+        <value xml:lang="en">Gantt Chart</value>
+        <value xml:lang="fr">Diagramme de Gantt</value>
+        <value xml:lang="it">Grafico gantt</value>
+        <value xml:lang="ja">ガントチャート</value>
+        <value xml:lang="nl">GANT chart</value>
+        <value xml:lang="th">แผนผังคุมการกำหนดงาน</value>
+        <value xml:lang="vi">Biểu đồ CHUNG</value>
+        <value xml:lang="zh">甘特图</value>
+        <value xml:lang="zh-TW">甘特圖</value>
+    </property>
     <property key="ProjectMgrListMailingList">
         <value xml:lang="de">Liste Mailing-List auf</value>
         <value xml:lang="en">List Mailing List</value>
diff --git a/projectmgr/src/docs/asciidoc/_include/prjm_project.adoc b/projectmgr/src/docs/asciidoc/_include/prjm_project.adoc
index 837b28d6c..b986246a7 100644
--- a/projectmgr/src/docs/asciidoc/_include/prjm_project.adoc
+++ b/projectmgr/src/docs/asciidoc/_include/prjm_project.adoc
@@ -23,6 +23,7 @@ It is also the starting point to navigate to the various sub screens related to
 == Sub screens
 Following sub screens are provided:
 
+* Gantt chart
 * Mailing List
 * Phases
 * Tasks
diff --git a/projectmgr/src/main/groovy/org/apache/ofbiz/projectmgr/GanttChart.groovy b/projectmgr/src/main/groovy/org/apache/ofbiz/projectmgr/GanttChart.groovy
new file mode 100644
index 000000000..33e118717
--- /dev/null
+++ b/projectmgr/src/main/groovy/org/apache/ofbiz/projectmgr/GanttChart.groovy
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+*/
+package org.apache.ofbiz.projectmgr
+
+import groovy.json.JsonOutput
+import org.apache.ofbiz.base.util.UtilDateTime
+import org.apache.ofbiz.entity.GenericValue
+import org.apache.ofbiz.entity.condition.EntityCondition
+import org.apache.ofbiz.entity.condition.EntityConditionList
+import org.apache.ofbiz.entity.condition.EntityExpr
+import org.apache.ofbiz.entity.condition.EntityOperator
+
+import javax.servlet.http.HttpSession
+import java.math.RoundingMode
+import java.sql.Timestamp
+
+final String taskStatusCompleted = 'PTS_COMPLETED'
+final int defaultTaskDurationDays = 3
+final String milestoneWorkEffortTypeId = 'MILESTONE'
+final String permissionCheckEntity = 'PROJECTMGR'
+final BigDecimal hundredPercent = new BigDecimal(100)
+
+String projectId = parameters.projectId
+GenericValue userLogin = parameters.userLogin as GenericValue
+
+Map<String, Object> result = runService('getProject', [projectId: projectId, userLogin: userLogin])
+Map<String, Object> project = result.projectInfo as Map<String, Object>
+if (project == null) {
+    return
+}
+
+context.chartStart = project.startDate ?: UtilDateTime.nowTimestamp() // default today's date
+context.chartEnd = project.completionDate ?: UtilDateTime.addDaysToTimestamp(UtilDateTime.nowTimestamp(), 14)
+
+List ganttList = []
+List<Map<String, Object>> ganttItems = []
+
+result = runService('getProjectPhaseList', [userLogin: userLogin, projectId: projectId])
+List<Map<String, Object>> phases = result.phaseList as List<Map<String, Object>>
+if (phases) {
+    phases.each { phase ->
+        Map<String, Object> newPhase = phase
+        newPhase.phaseNr = phase.phaseId
+        if (!newPhase.estimatedStartDate && newPhase.actualStartDate) {
+            newPhase.estimatedStartDate = newPhase.actualStartDate
+        }
+        newPhase.estimatedStartDate = newPhase.estimatedStartDate ?: context.chartStart
+
+        if (!newPhase.estimatedCompletionDate && newPhase.actualCompletionDate) {
+            newPhase.estimatedCompletionDate = newPhase.actualCompletionDateDate
+        }
+        newPhase.estimatedCompletionDate = newPhase.estimatedCompletionDate
+                ?: UtilDateTime.addDaysToTimestamp(newPhase.estimatedStartDate as Timestamp, defaultTaskDurationDays)
+
+        newPhase.workEffortTypeId = 'PHASE'
+        ganttList.add(newPhase)
+
+        ganttItems << [
+                pID: phase.phaseId as Integer,
+                pName: phase.phaseSeqNum ? "${phase.phaseSeqNum}. ${phase.phaseName}" : phase.phaseName,
+                pStart: '',
+                pEnd: '',
+                pPlanStart: '',
+                pPlanEnd: '',
+                pClass: 'ggroupblack',
+                pLink: '',
+                pMile: 0,
+                pRes: '',
+                pComp: 0,
+                pGroup: 1,
+                pParent: 0,
+                pOpen: 1,
+                pDepend: '',
+        ]
+
+        EntityConditionList<EntityExpr> cond = EntityCondition.makeCondition([
+                EntityCondition.makeCondition('currentStatusId', EntityOperator.NOT_EQUAL, 'PTS_CANCELLED'),
+                EntityCondition.makeCondition('workEffortParentId', EntityOperator.EQUALS, phase.phaseId)
+        ], EntityOperator.AND)
+        List<GenericValue> tasks = from('WorkEffort').where(cond).orderBy('sequenceNum', 'workEffortName').queryList()
+
+        if (tasks) {
+            tasks.each { task ->
+                Map<String, Object> resultTaskInfo = runService('getProjectTask', [userLogin: userLogin, taskId: task.workEffortId])
+                Map<String, Object> taskInfo = resultTaskInfo.taskInfo as Map<String, Object>
+                taskInfo.taskNr = task.workEffortId
+                taskInfo.phaseNr = phase.phaseId
+
+                taskInfo.resource = ((taskInfo.plannedHours &&
+                        taskStatusCompleted != taskInfo.currentStatusId &&
+                        taskInfo.plannedHours > taskInfo.actualHours) ? taskInfo.plannedHours : taskInfo.actualHours) +
+                        ' Hrs'
+
+                BigDecimal durationHours = resultTaskInfo.plannedHours as BigDecimal
+                if (taskStatusCompleted == taskInfo.currentStatusId) {
+                    taskInfo.completion = hundredPercent
+                } else {
+                    BigDecimal actualHours = taskInfo.actualHours as BigDecimal
+                    BigDecimal plannedHours = taskInfo.plannedHours as BigDecimal
+                    taskInfo.completion = (actualHours && plannedHours)
+                            ? (actualHours * hundredPercent).divide(plannedHours, 0, RoundingMode.UP)
+                            : 0
+                }
+                if (!taskInfo.estimatedStartDate && taskInfo.actualStartDate) {
+                    taskInfo.estimatedStartDate = taskInfo.actualStartDate
+                }
+                taskInfo.estimatedStartDate = taskInfo.estimatedStartDate ?: newPhase.estimatedStartDate
+
+                if (!taskInfo.estimatedCompletionDate && taskInfo.actualCompletionDate) {
+                    taskInfo.estimatedCompletionDate = taskInfo.actualCompletionDate
+                }
+                if (!taskInfo.estimatedCompletionDate && !durationHours) {
+                    taskInfo.estimatedCompletionDate =
+                            UtilDateTime.addDaysToTimestamp(newPhase.estimatedStartDate as Timestamp,
+                                    defaultTaskDurationDays)
+                } else if (!taskInfo.estimatedCompletionDate && durationHours) {
+                    taskInfo.estimatedCompletionDate =
+                            UtilDateTime.addDaysToTimestamp(newPhase.estimatedStartDate as Timestamp,
+                                    durationHours / 8)
+                }
+
+                taskInfo.workEffortTypeId = task.workEffortTypeId
+                if (security.hasEntityPermission(permissionCheckEntity, '_READ', session as HttpSession) ||
+                        security.hasEntityPermission(permissionCheckEntity, '_ADMIN', session as HttpSession)) {
+                    taskInfo.url = '/projectmgr/control/taskView?workEffortId=' + task.workEffortId
+                } else {
+                    taskInfo.url = ''
+                }
+
+                List<GenericValue> preTaskAssociations = from('WorkEffortAssoc')
+                        .where('workEffortIdTo', task.workEffortId)
+                        .orderBy('workEffortIdFrom')
+                        .queryList()
+
+                taskInfo.preDecessor = preTaskAssociations*.getRelatedOne('FromWorkEffort', false)
+                        .collect { we -> we.workEffortId as String }
+                        .join(',')
+
+                ganttItems << [
+                        pID: taskInfo.taskNr as Integer,
+                        pName: taskInfo.taskSeqNum ? "${taskInfo.taskSeqNum}. ${taskInfo.taskName}" : taskInfo.taskName,
+                        pStart: (taskInfo.estimatedStartDate as Timestamp).toLocalDateTime().dateString,
+                        pEnd: (taskInfo.estimatedCompletionDate as Timestamp).toLocalDateTime().dateString,
+                        pPlanStart: (taskInfo.estimatedStartDate as Timestamp).toLocalDateTime().dateString,
+                        pPlanEnd: (taskInfo.estimatedCompletionDate as Timestamp).toLocalDateTime().dateString,
+                        pClass: taskInfo.workEffortTypeId == milestoneWorkEffortTypeId ? 'gmilestone' : 'gtaskgreen',
+                        pLink: taskInfo.url,
+                        pMile: taskInfo.workEffortTypeId == milestoneWorkEffortTypeId ? 1 : 0,
+                        pRes: taskInfo.resource,
+                        pComp: taskInfo.completion,
+                        pGroup: 0,
+                        pParent: taskInfo.phaseNr,
+                        pOpen: taskInfo.workEffortTypeId == milestoneWorkEffortTypeId ? 0 : 1,
+                        pDepend: taskInfo.preDecessor ?: '',
+                ]
+            }
+        }
+    }
+}
+
+context.phaseTaskListJson = JsonOutput.toJson(ganttItems)
diff --git a/projectmgr/template/project/GanttChart.ftl b/projectmgr/template/project/GanttChart.ftl
new file mode 100644
index 000000000..cb08a3697
--- /dev/null
+++ b/projectmgr/template/project/GanttChart.ftl
@@ -0,0 +1,26 @@
+<#-- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -->
+<#--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<div style="position:relative" class="gantt" id="GanttChartDIV"></div>
+
+<input id="ofbizGantItemsJson" type="hidden" value="${phaseTaskListJson}"/>
+
+<script type="text/javascript" src="/projectmgr/node_modules/jsgantt-improved/dist/jsgantt.js"></script>
+<script type="text/javascript" src="/projectmgr/static/projectmgr.js"></script>
diff --git a/projectmgr/webapp/projectmgr/WEB-INF/controller.xml b/projectmgr/webapp/projectmgr/WEB-INF/controller.xml
index 06d009bad..73800075a 100644
--- a/projectmgr/webapp/projectmgr/WEB-INF/controller.xml
+++ b/projectmgr/webapp/projectmgr/WEB-INF/controller.xml
@@ -34,6 +34,11 @@
         <response name="success" type="view" value="main"/>
     </request-map>
 
+    <request-map uri="ganttChart">
+        <security https="true" auth="true"/>
+        <response name="success" type="view" value="ganttChart"/>
+    </request-map>
+
     <request-map uri="ListSubProjects">
         <security https="true" auth="true"/>
         <response name="success" type="view" value="ListSubProjects"/>
@@ -692,6 +697,7 @@
     <view-map name="EditPartyRates" type="screen" page="component://projectmgr/widget/ResourceScreens.xml#EditPartyRates"/>
 
     <view-map name="main" type="screen" page="component://projectmgr/widget/CommonScreens.xml#Main"/>
+    <view-map name="ganttChart" type="screen" page="component://projectmgr/widget/ProjectScreens.xml#ganttChart"/>
     <view-map name="ListSubProjects" type="screen" page="component://projectmgr/widget/ProjectScreens.xml#ListSubProjects"/>
     <view-map name="FindResource" type="screen" page="component://projectmgr/widget/ResourceScreens.xml#FindResource"/>
     <view-map name="FindProject" type="screen" page="component://projectmgr/widget/ProjectScreens.xml#FindProject"/>
diff --git a/projectmgr/webapp/projectmgr/WEB-INF/web.xml b/projectmgr/webapp/projectmgr/WEB-INF/web.xml
index d86c38317..2aa29a5d7 100644
--- a/projectmgr/webapp/projectmgr/WEB-INF/web.xml
+++ b/projectmgr/webapp/projectmgr/WEB-INF/web.xml
@@ -64,7 +64,7 @@
         <filter-class>org.apache.ofbiz.webapp.control.ControlFilter</filter-class>
         <init-param>
             <param-name>allowedPaths</param-name>
-            <param-value>/error:/control:/select:/index.html:/index.jsp:/default.html:/default.jsp:/images</param-value>
+            <param-value>/error:/control:/select:/index.html:/index.jsp:/default.html:/default.jsp:/images:/static:/js:/node_modules/jsgantt-improved</param-value>
         </init-param>
         <init-param>
             <param-name>redirectPath</param-name>
diff --git a/projectmgr/webapp/projectmgr/package-lock.json b/projectmgr/webapp/projectmgr/package-lock.json
new file mode 100644
index 000000000..9dfcbfc20
--- /dev/null
+++ b/projectmgr/webapp/projectmgr/package-lock.json
@@ -0,0 +1,38 @@
+{
+    "name": "ofbiz-projectmgr",
+    "lockfileVersion": 3,
+    "requires": true,
+    "packages": {
+        "": {
+            "name": "ofbiz-projectmgr",
+            "license": "Apache-2.0",
+            "dependencies": {
+                "jsgantt-improved": "^3.0.0"
+            }
+        },
+        "node_modules/@types/node": {
+            "version": "24.12.2",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
+            "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
+            "license": "MIT",
+            "dependencies": {
+                "undici-types": "~7.16.0"
+            }
+        },
+        "node_modules/jsgantt-improved": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/jsgantt-improved/-/jsgantt-improved-3.0.0.tgz",
+            "integrity": "sha512-0OtLBSmb5e0WAroRhp0sT+hJBSYM9x0zsGxXpv+RSVpoQ4HovE5+G5c0SDD+1ogXld9dkJtbDGCmvJQYhxCxwA==",
+            "license": "ISC",
+            "dependencies": {
+                "@types/node": "^24.3.0"
+            }
+        },
+        "node_modules/undici-types": {
+            "version": "7.16.0",
+            "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+            "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+            "license": "MIT"
+        }
+    }
+}
diff --git a/projectmgr/webapp/projectmgr/package.json b/projectmgr/webapp/projectmgr/package.json
new file mode 100644
index 000000000..22902caa1
--- /dev/null
+++ b/projectmgr/webapp/projectmgr/package.json
@@ -0,0 +1,9 @@
+{
+    "name": "ofbiz-projectmgr",
+    "description": "ofbiz-plugins projectmgr NPM dependencies configuration",
+    "repository": "https://github.com/apache/ofbiz-plugins",
+    "license": "Apache-2.0",
+    "dependencies": {
+        "jsgantt-improved": "^3.0.0"
+    }
+}
diff --git a/projectmgr/webapp/projectmgr/static/projectmgr.css b/projectmgr/webapp/projectmgr/static/projectmgr.css
new file mode 100644
index 000000000..ac6061151
--- /dev/null
+++ b/projectmgr/webapp/projectmgr/static/projectmgr.css
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* This CSS file is used for the Project Manager component. */
+
+/* Remove any margins that my have been set on all divs by the themes, such as Rainbowstone */
+.gchartcontainer div {
+    margin: unset
+}
diff --git a/projectmgr/webapp/projectmgr/static/projectmgr.js b/projectmgr/webapp/projectmgr/static/projectmgr.js
new file mode 100644
index 000000000..f506ffd42
--- /dev/null
+++ b/projectmgr/webapp/projectmgr/static/projectmgr.js
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+const ganttItemsJson = document.getElementById("ofbizGantItemsJson").value;
+const ganttItems = JSON.parse(ganttItemsJson);
+
+const g = new JSGantt.GanttChart(document.getElementById('GanttChartDIV'), 'day');
+g.setShowRes(1); // Show/Hide Resource (0/1)
+g.setShowDur(1); // Show/Hide Duration (0/1)
+g.setShowComp(1); // Show/Hide % Complete(0/1)
+g.setShowTaskInfoLink(1);
+g.setShowTaskInfoNotes(0)
+
+g.setDateTaskTableDisplayFormat('dd mon yyyy');
+g.setDayMajorDateDisplayFormat('dd mon');
+
+for (t of ganttItems) {
+    g.AddTaskItem(
+        new JSGantt.TaskItem(t.pID, t.pName, t.pStart, t.pEnd, t.pClass, t.pLink, t.pMile, t.pRes, t.pComp,
+            t.pGroup, t.pParent, t.pOpen, t.pDepend, "", "", g)
+    );
+}
+
+g.Draw();
diff --git a/projectmgr/widget/CommonScreens.xml b/projectmgr/widget/CommonScreens.xml
index 7216090d1..647770272 100644
--- a/projectmgr/widget/CommonScreens.xml
+++ b/projectmgr/widget/CommonScreens.xml
@@ -39,6 +39,7 @@ under the License.
                 <!-- layoutSettings.headerImageUrl can be used to specify an application specific logo; if not set,
                      then the global layoutSettings.commonHeaderImageUrl (specified in GlobalDecorator) will be used. -->
                 <!--<set field="layoutSettings.headerImageUrl" value="/images/ofbiz_logo.png" global="true"/>-->                    
+                <set field="layoutSettings.styleSheets[]" value="/projectmgr/static/projectmgr.css" global="true"/>
                 <set field="activeApp" value="projectmgr" global="true"/>
                 <set field="applicationMenuName" value="ProjectMgrAppBar" global="true"/>
                 <set field="applicationMenuLocation" value="${parameters.mainMenuLocation}" global="true"/>
diff --git a/projectmgr/widget/ProjectMenus.xml b/projectmgr/widget/ProjectMenus.xml
index cab8eb9e5..0a0b3f163 100644
--- a/projectmgr/widget/ProjectMenus.xml
+++ b/projectmgr/widget/ProjectMenus.xml
@@ -83,6 +83,14 @@
                 &quot;projectId&quot;, projectId));
                 return result.get(&quot;hasPermission&quot;); }" type="Boolean"/>
         </actions>
+        <menu-item name="ganttchart" title="${uiLabelMap.ProjectMgrGanttChart}">
+            <condition>
+                <if-compare field="hasViewPermission" operator="equals" value="true"/>
+            </condition>
+            <link target="ganttChart">
+                <parameter param-name="projectId"/>
+            </link>
+        </menu-item>
         <menu-item name="projectView" title="${uiLabelMap.ProjectMgrProjectSummary}">
             <condition>
                 <if-compare field="hasViewPermission" operator="equals" value="true"/>
diff --git a/projectmgr/widget/ProjectScreens.xml b/projectmgr/widget/ProjectScreens.xml
index fddf47acd..0ee555b9f 100644
--- a/projectmgr/widget/ProjectScreens.xml
+++ b/projectmgr/widget/ProjectScreens.xml
@@ -419,6 +419,29 @@ under the License.
             </widgets>
         </section>
     </screen>
+    <screen name="ganttChart">
+        <section>
+            <actions>
+                <set field="titleProperty" value="ProjectMgrGanttChart"/>
+                <set field="tabButtonItem" value="ganttchart"/>
+                <set field="layoutSettings.styleSheets[]" value="/projectmgr/node_modules/jsgantt-improved/dist/jsgantt.css" global="true"/>
+                <set field="layoutSettings.styleSheets[]" value="/projectmgr/static/projectmgr.css" global="true"/>
+                <script location="component://projectmgr/src/main/groovy/org/apache/ofbiz/projectmgr/GanttChart.groovy"/>
+            </actions>
+            <widgets>
+                <label text="${project}"/>
+                <decorator-screen name="CommonProjectDecorator" location="${parameters.mainDecoratorLocation}">
+                    <decorator-section name="body">
+                        <screenlet title="${uiLabelMap.PageTitleProjectGanttChart}">
+                            <platform-specific>
+                                <html><html-template multi-block="true" location="component://projectmgr/template/project/GanttChart.ftl"/></html>
+                            </platform-specific>
+                        </screenlet>
+                    </decorator-section>
+                </decorator-screen>
+            </widgets>
+        </section>
+    </screen>
     <screen name="SubProjectsInfo">
         <section>
             <actions>
@@ -957,6 +980,8 @@ under the License.
         <section>
             <actions>
                 <property-map resource="ProjectMgrUiLabels" map-name="uiLabelMap" global="true"/>
+                <set field="layoutSettings.styleSheets[]" value="/projectmgr/node_modules/jsgantt-improved/dist/jsgantt.css" global="true"/>
+                <set field="layoutSettings.styleSheets[]" value="/projectmgr/static/projectmgr.css" global="true"/>
                 <entity-condition entity-name="WorkEffortPartyAssignByGroup" list="projects" filter-by-date="true">
                     <condition-list combine="and">
                         <condition-expr field-name="partyId" from-field="userLogin.partyId"/>
@@ -974,6 +999,7 @@ under the License.
                         <actions>
                             <set field="projectId" from-field="project.workEffortId"/>
                             <set field="parameters.projectId" from-field="project.workEffortId"/>
+                            <script location="component://projectmgr/src/main/groovy/org/apache/ofbiz/projectmgr/GanttChart.groovy"/>
                             <service service-name="getProject" result-map="result">
                                 <field-map field-name="projectId" from-field="projectId"/>
                             </service>
@@ -1016,6 +1042,12 @@ under the License.
                                     </widgets>
                                 </section>
                             </container>
+                            <container style="clear"/>
+                            <screenlet title="${uiLabelMap.PageTitleProjectGanttChart}">
+                                <platform-specific>
+                                    <html><html-template multi-block="true" location="component://projectmgr/template/project/GanttChart.ftl"/></html>
+                                </platform-specific>
+                            </screenlet>
                         </widgets>
                     </section>
                 </iterate-section>