const tasks = [{"id":1,"id_gantt":1,"baseline_id":1,"text":"progetto template","start_date":"2025-01-01T08:00:00.000Z","duration":210,"parent":0,"sortorder":2,"progetto":1,"type":"project","amount":"0.00","version":1},{"id":2,"id_gantt":2,"baseline_id":1,"text":"Preparation","start_date":"2025-01-01T08:00:00.000Z","duration":8,"parent":1,"sortorder":3,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":3,"id_gantt":10,"baseline_id":1,"text":"setup project","start_date":"2025-01-01T08:00:00.000Z","duration":6,"parent":2,"sortorder":4,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":4,"id_gantt":9,"baseline_id":1,"text":"conduct kickoff","start_date":"2025-01-08T08:00:00.000Z","duration":1,"parent":2,"sortorder":5,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":5,"id_gantt":8,"baseline_id":1,"text":"quality gate","start_date":"2025-01-09T17:00:00.000Z","duration":0,"parent":2,"sortorder":6,"progetto":1,"type":"milestone","amount":"0.00","version":1},{"id":6,"id_gantt":3,"baseline_id":1,"text":"Blueprint","start_date":"2025-01-10T08:00:00.000Z","duration":41,"parent":1,"sortorder":7,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":7,"id_gantt":13,"baseline_id":1,"text":"Workshops","start_date":"2025-01-10T08:00:00.000Z","duration":41,"parent":3,"sortorder":8,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":8,"id_gantt":12,"baseline_id":1,"text":"Stream 1","start_date":"2025-01-10T08:00:00.000Z","duration":13,"parent":13,"sortorder":9,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":9,"id_gantt":11,"baseline_id":1,"text":"Stream 2","start_date":"2025-01-24T08:00:00.000Z","duration":13,"parent":13,"sortorder":10,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":10,"id_gantt":25,"baseline_id":1,"text":"Stream 3","start_date":"2025-02-07T08:00:00.000Z","duration":13,"parent":13,"sortorder":11,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":11,"id_gantt":4,"baseline_id":1,"text":"Realization","start_date":"2025-02-21T08:00:00.000Z","duration":70,"parent":1,"sortorder":12,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":12,"id_gantt":16,"baseline_id":1,"text":"Configuration","start_date":"2025-02-21T08:00:00.000Z","duration":55,"parent":4,"sortorder":13,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":13,"id_gantt":23,"baseline_id":1,"text":"Configure","start_date":"2025-02-21T08:00:00.000Z","duration":41,"parent":16,"sortorder":14,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":14,"id_gantt":22,"baseline_id":1,"text":"Unit test","start_date":"2025-04-04T07:00:00.000Z","duration":13,"parent":16,"sortorder":15,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":15,"id_gantt":15,"baseline_id":1,"text":"Development","start_date":"2025-02-21T08:00:00.000Z","duration":69,"parent":4,"sortorder":16,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":16,"id_gantt":14,"baseline_id":1,"text":"Develop & Unit Test","start_date":"2025-02-21T08:00:00.000Z","duration":69,"parent":15,"sortorder":17,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":17,"id_gantt":24,"baseline_id":1,"text":"Quality gate","start_date":"2025-05-02T16:00:00.000Z","duration":0,"parent":4,"sortorder":18,"progetto":1,"type":"milestone","amount":"0.00","version":1},{"id":18,"id_gantt":5,"baseline_id":1,"text":"Test & Training","start_date":"2025-05-05T07:00:00.000Z","duration":42,"parent":1,"sortorder":19,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":19,"id_gantt":19,"baseline_id":1,"text":"Training","start_date":"2025-05-05T07:00:00.000Z","duration":25,"parent":5,"sortorder":20,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":20,"id_gantt":18,"baseline_id":1,"text":"Test","start_date":"2025-05-19T07:00:00.000Z","duration":25,"parent":5,"sortorder":21,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":21,"id_gantt":17,"baseline_id":1,"text":"Quality gate","start_date":"2025-06-16T07:00:00.000Z","duration":0,"parent":5,"sortorder":22,"progetto":1,"type":"milestone","amount":"0.00","version":1},{"id":22,"id_gantt":6,"baseline_id":1,"text":"Go live","start_date":"2025-06-17T07:00:00.000Z","duration":43,"parent":1,"sortorder":23,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":23,"id_gantt":21,"baseline_id":1,"text":"Go no go decision","start_date":"2025-06-17T07:00:00.000Z","duration":1,"parent":6,"sortorder":24,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":24,"id_gantt":20,"baseline_id":1,"text":"Hypercare","start_date":"2025-06-18T07:00:00.000Z","duration":41,"parent":6,"sortorder":25,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":25,"id_gantt":26,"baseline_id":1,"text":"Quality gate","start_date":"2025-07-30T16:00:00.000Z","duration":0,"parent":6,"sortorder":26,"progetto":1,"type":"milestone","amount":"0.00","version":1}];
// Add user_amount property initialized to null
tasks.forEach(task => task.user_amount = null);
// Build a map for quick parent lookup
const taskMap = new Map();
tasks.forEach(task => taskMap.set(task.id_gantt, { ...task, children: [] }));
function buildWBS(tasks) {
const roots = [];
tasks.forEach(task => {
const t = taskMap.get(task.id_gantt);
if (task.parent !== 0) {
const parent = taskMap.get(task.parent);
if (parent) parent.children.push(t);
} else {
roots.push(t);
}
});
return roots;
}
// Initialize the dropdown
function initializeDropdown(data) {
// Helper function to assign wbsNumber recursively
function assignWBSNumbers(items, parentId = 0, prefix = '') {
let index = 1;
items
.filter(item => item.parent === parentId)
.sort((a, b) => a.sortorder - b.sortorder)
.forEach(item => {
const wbsNumber = prefix ? prefix + '.' + index : '' + index;
item.wbsNumber = wbsNumber;
// Recursively assign to children
assignWBSNumbers(items, item.id_gantt, wbsNumber);
index++;
});
}
assignWBSNumbers(data);
const select = document.getElementById("element-select");
let firstOption = document.createElement("option");
firstOption.value = "";
firstOption.textContent = "Choose a WBS element to update";
select.appendChild(firstOption);
data.forEach(item => {
const option = document.createElement("option");
option.value = item.id_gantt;
option.textContent = item.wbsNumber + ' ' + item.text;
select.appendChild(option);
});
}
function getWeight(task) {
if (task.parent === 0) return 100;
const parent = taskMap.get(task.parent);
if (!parent) return 0;
const parentDuration = parent.duration === 0 ? 1 : parent.duration;
const taskDuration = task.duration === 0 ? 1 : task.duration;
return ((taskDuration / parentDuration) * 100).toFixed(2);
}
function renderWBS(tasks, container, wbsPrefix = '', level = 1, parentId = null) {
const numberFormat = new Intl.NumberFormat('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
tasks.sort((a, b) => a.sortorder - b.sortorder);
tasks.forEach((task, index) => {
const wbsCode = wbsPrefix ? `${wbsPrefix}.${index + 1}` : `${index + 1}`;
const hasChildren = task.children.length > 0;
const weight = getWeight(task);
const row = document.createElement('tr');
row.setAttribute('data-id', task.id_gantt);
row.setAttribute('data-level', level);
if (parentId !== null) {
row.setAttribute('data-parent', parentId);
row.classList.add('hidden-row');
}
const toggleIcon = hasChildren ? `<span class="toggle-btn" data-toggle-id="${task.id}">▶</span>` : '';
const startDate = new Date(task.start_date).toLocaleDateString();
// Format weight: dot as thousands, comma as decimal
const formattedWeight = numberFormat.format(weight) + '%';
// Format amount: dot as thousands, comma as decimal, prepend € with space
const formattedAmount = '€ ' + numberFormat.format(parseFloat(task.amount));
// Display duration: if 0, show 1 instead
const displayDuration = task.duration === 0 ? 1 : task.duration;
row.innerHTML = `
<td>${wbsCode}</td>
<td>
<span class="indent" style="--level: ${level};">
${toggleIcon}${task.text}
</span>
</td>
<td>${startDate}</td>
<td>${displayDuration}</td>
<td>${formattedWeight}</td>
<td><span class="computed-amount" data-id="${task.id_gantt}">${formattedAmount}</span></td>
`;
container.appendChild(row);
if (hasChildren) {
renderWBS(task.children, container, wbsCode, level + 1, task.id);
}
});
}
function toggleRowChildren(toggleId, expand) {
const rows = document.querySelectorAll(`tr[data-parent='${toggleId}']`);
rows.forEach(row => {
if (expand) {
row.classList.remove('hidden-row');
} else {
row.classList.add('hidden-row');
}
// Collapse children recursively
if (!expand) {
const childId = row.getAttribute('data-id');
toggleRowChildren(childId, false);
const icon = row.querySelector(`[data-toggle-id='${childId}']`);
if (icon) icon.textContent = '▶';
}
});
}
function addToggleHandlers() {
document.querySelectorAll('[data-toggle-id]').forEach(icon => {
icon.addEventListener('click', () => {
const id = icon.getAttribute('data-toggle-id');
const isExpanded = icon.textContent === '▼';
toggleRowChildren(id, !isExpanded);
icon.textContent = isExpanded ? '▶' : '▼';
});
});
}
// Expand/collapse WBS to a certain level
let currentLevel = 1;
function setWBSLevel(level) {
const rows = document.querySelectorAll('#wbsTableBody tr');
let maxLevel = 1;
rows.forEach(row => {
const rowLevel = parseInt(row.getAttribute('data-level'), 10);
if (!isNaN(rowLevel) && rowLevel > maxLevel) maxLevel = rowLevel;
});
// Clamp level to range [1, maxLevel]
if (level < 1) level = 1;
if (level > maxLevel) level = maxLevel;
currentLevel = level;
rows.forEach(row => {
const rowLevel = parseInt(row.getAttribute('data-level'), 10);
if (!isNaN(rowLevel)) {
if (rowLevel <= level) {
row.classList.remove('hidden-row');
// set toggle icon to expanded for rows with children at this level
const icon = row.querySelector('[data-toggle-id]');
if (icon) icon.textContent = (rowLevel < level) ? '▼' : '▶';
} else {
row.classList.add('hidden-row');
}
}
});
// For all rows at the current level, set their toggle icon to collapsed (▶)
rows.forEach(row => {
const rowLevel = parseInt(row.getAttribute('data-level'), 10);
if (!isNaN(rowLevel) && rowLevel === level) {
const icon = row.querySelector('[data-toggle-id]');
if (icon) icon.textContent = '▶';
}
});
}
// Update amounts based on user input and propagate changes
function updateAmounts() {
const numberFormat = new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
// Reset all amounts to 0 before recalculating
taskMap.forEach(task => {
task.amount = 0;
});
// Helper function to update parent amounts recursively
function updateParentAmounts(task) {
if (task.parent === 0) return;
const parent = taskMap.get(task.parent);
if (!parent) return;
let sumAmounts = 0;
parent.children.forEach(child => {
sumAmounts += parseFloat(child.amount);
});
parent.amount = sumAmounts.toFixed(2);
updateParentAmounts(parent);
}
// Recursive function to update amounts for a task and its children
function updateTaskAmount(task) {
//console.log(`Updating task: ${task.text} (ID: ${task.id_gantt})`);
if (task.children.length > 0) {
if (task.user_amount !== null && task.user_amount !== '') {
// Clear user_amount for all children before distributing
task.children.forEach(child => { child.user_amount = null; });
// Distribute user_amount among children by weight
let totalWeight = 0;
task.children.forEach(child => {
totalWeight += parseFloat(getWeight(child));
});
task.children.forEach(child => {
const weight = parseFloat(getWeight(child));
const childUserAmount = (weight / totalWeight) * parseFloat(task.user_amount);
child.user_amount = childUserAmount.toFixed(2);
updateTaskAmount(child);
});
// After children updated, sum their amounts for this task
let sumAmounts = 0;
task.children.forEach(child => {
sumAmounts += parseFloat(child.amount);
});
task.amount = sumAmounts.toFixed(2);
} else {
// If no user_amount for this task, sum children amounts
let sumAmounts = 0;
task.children.forEach(child => {
updateTaskAmount(child);
sumAmounts += parseFloat(child.amount);
});
task.amount = sumAmounts.toFixed(2);
}
} else {
// Leaf task
if (task.user_amount !== null && task.user_amount !== '') {
task.amount = parseFloat(task.user_amount).toFixed(2);
} else {
task.amount = '0.00';
}
}
// Ensure amount is stored as string with two decimals
task.amount = parseFloat(task.amount).toFixed(2);
updateParentAmounts(task);
}
// Always update all root tasks (full hierarchy)
const roots = [];
taskMap.forEach(task => {
if (task.parent === 0) roots.push(task);
});
roots.forEach(root => updateTaskAmount(root));
// After all calculations, update DOM for the full hierarchy
taskMap.forEach(task => {
const amountSpan = document.querySelector(`.computed-amount[data-id='${task.id_gantt}']`);
if (amountSpan) {
// Format amount: dot as thousands, comma as decimal, prepend € with space
amountSpan.textContent = '€ ' + numberFormat.format(parseFloat(task.amount));
}
const inputField = document.querySelector(`.user-amount-input[data-id='${task.id_gantt}']`);
if (inputField) {
// Update input field only if different from current user_amount to avoid infinite loop
const val = inputField.value === '' ? null : parseFloat(inputField.value);
if (val !== task.user_amount) {
inputField.value = task.user_amount !== null ? task.user_amount : '';
}
}
});
// Ensure original tasks array is updated from taskMap
tasks.forEach(task => {
const updated = taskMap.get(task.id_gantt);
if (updated) {
task.amount = updated.amount;
}
});
}
// Init
const tree = buildWBS(tasks);
initializeDropdown(tasks);
const tbody = document.getElementById('wbsTableBody');
renderWBS(tree, tbody);
addToggleHandlers();
// WBS expand/collapse level buttons
document.getElementById('expandLevel').addEventListener('click', function() {
const rows = document.querySelectorAll('#wbsTableBody tr');
let maxLevel = 1;
rows.forEach(row => {
const rowLevel = parseInt(row.getAttribute('data-level'), 10);
if (!isNaN(rowLevel) && rowLevel > maxLevel) maxLevel = rowLevel;
});
if (currentLevel < maxLevel) {
setWBSLevel(currentLevel + 1);
}
});
document.getElementById('collapseLevel').addEventListener('click', function() {
if (currentLevel > 1) {
setWBSLevel(currentLevel - 1);
}
});
//$('#amount-input').number(true,2,',','.');
$('#element-select').change(function (e) {
e.preventDefault();
let idToSearch = $(this).val();
if (idToSearch !== '') {
const task = taskMap.get(Number(idToSearch));
let amountValue = '';
if (task) {
// Prefer user_amount if set, otherwise fallback to calculated amount
//amountValue = task.user_amount !== null && task.user_amount !== undefined ? task.user_amount : task.amount;
amountValue = parseInt(task.amount) == 0 ? '' : task.amount;
}
$('#amount-input').val(amountValue);
} else {
$('#amount-input').val('');
}
});
$('#updateAmount').click(function(e){
e.preventDefault();
let selectedId = $('#element-select').val();
let inputAmount = $('#amount-input').val();
if(selectedId != ''){
const task = taskMap.get(Number(selectedId));
if (task) {
task.user_amount = inputAmount;
updateAmounts();
// Retrieve the selected task again after updateAmounts()
const updatedTask = taskMap.get(Number(selectedId));
if (updatedTask) {
const userAmount = updatedTask.user_amount !== null && updatedTask.user_amount !== '' ? parseFloat(updatedTask.user_amount) : null;
const calculatedAmount = parseFloat(updatedTask.amount);
if (userAmount !== null) {
const diff = Math.abs(userAmount - calculatedAmount);
if (task.children.length > 0 && diff > 0) {
const itNumberFormat = new Intl.NumberFormat('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const formattedUserAmount = itNumberFormat.format(userAmount);
const formattedCalculatedAmount = itNumberFormat.format(calculatedAmount);
const formattedDiff = itNumberFormat.format(diff);
if (typeof $(document).Toasts === "function") {
$(document).Toasts('create', {
autohide: true,
delay: 4000,
title: 'Rounding problem',
class: 'bg-warning',
body: `Task: <b>${updatedTask.text}</b><br>
Importo originale: <b>€ ${formattedUserAmount}</b><br>
Importo risultante: <b>€ ${formattedCalculatedAmount}</b><br>
Differenza: <b>€ ${formattedDiff}</b>`
});
} else {
alert(
"Piccola differenza di arrotondamenton" +
"Task: " + updatedTask.text + "n" +
"Importo originale: € " + formattedUserAmount + "n" +
"Importo risultante: € " + formattedCalculatedAmount + "n" +
"Differenza: € " + formattedDiff
);
}
}
}
}
}
}else{
if (typeof $(document).Toasts === "function") {
$(document).Toasts('create', { autohide: true, delay: 750, title: 'Baseline', class: 'bg-warning', body: 'Please select a task to update' });
} else {
alert("Please select a task to update");
}
}
});
$('#back').click(function(e){
e.preventDefault();
location.replace('gantt.html');
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<div class="content-wrapper">
<div class="content">
<div class="container-fluid" id="target">
<div class="row text-center" style="margin-bottom: 10px;">
<!--<div class="col-md-2 d-none d-sm-block"></div>-->
<div class="col-12">
<h1>WBS Amount Allocation</h1>
<div class="row">
<div class="col-4">
<select id="element-select" class="custom-select form-control-border "></select>
</div>
<div class="col-4">
<div class="input-group">
<input type="text" class="form-control" id="amount-input">
<span class="input-group-append">
<button type="button" class="btn btn-secondary btn-flat" id="updateAmount">Update Amount</button>
</span>
</div>
</div>
<div class="col-2">
<button type="button" class="btn btn-secondary btn-flat" id="expandLevel"><i class="fas fa-search-plus"></i> Zoom In</button>
<button type="button" class="btn btn-secondary btn-flat" id="collapseLevel"><i class="fas fa-search-minus"></i> Zoom Out</button>
</div>
<div class="col-2">
<button type="button" class="btn btn-secondary btn-flat" id="back"><i class="fas fa-long-arrow-alt-left"></i> Back</button>
<button type="button" class="btn btn-secondary btn-flat" id="saveEvBaseline"><i class="fas fa-save"></i> Save</button>
<button type="button" class="btn btn-secondary btn-flat" id="resetEvBaseline"><i class="fas fa-redo-alt"></i> Reset</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div id="output">
<table class="table table-bordered table-striped table-hover align-middle">
<thead class="table-dark">
<tr>
<th>WBS Code</th>
<th>Text</th>
<th>Start Date</th>
<th>Duration</th>
<th>Weight (%)</th>
<th>Amount</th>
</tr>
</thead>
<tbody id="wbsTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>