diff --git a/src/bonsai/bonsai/bim/data/webui/sioserver.py b/src/bonsai/bonsai/bim/data/webui/sioserver.py index 08565da659..f61e1d3d41 100644 --- a/src/bonsai/bonsai/bim/data/webui/sioserver.py +++ b/src/bonsai/bonsai/bim/data/webui/sioserver.py @@ -180,6 +180,11 @@ class BlenderNamespace(socketio.AsyncNamespace): print(f"Predefined types from Blender client {sid}") blender_messages[sid]["predefined_types"] = data await sio.emit("predefined_types", {"blenderId": sid, "data": data}, namespace="/web") + + async def on_selected_products(self, sid, data): + print(f"Selected products from Blender client {sid}") + blender_messages[sid]["selected_products"] = data + await sio.emit("selected_products", {"blenderId": sid, "data": data}, namespace="/web") async def schedules(request): with open("templates/index.html", "r") as f: diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/components/costTable.css b/src/bonsai/bonsai/bim/data/webui/static/css/components/costTable.css index 0d6f616ddf..f43ddede4c 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/components/costTable.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/components/costTable.css @@ -1,115 +1,135 @@ table { - width: 100%; - border-collapse: collapse; - user-select: none; + width: 100%; + height: 80%; + border-collapse: collapse; + user-select: none; } -td, th { - border: 1px solid #ddd; - padding: 8px; - text-align: left; +.table-container { + max-height: 70vh; + margin-top: 1em; + overflow-y: auto; + overflow-x: auto; +} + +form table { + height: 50%; +} + +form .table-container { + max-height: 50vh; +} + +td, +th { + border: 1px solid #ddd; + padding: 8px; + text-align: left; } td { - align-items: center; + align-items: center; } /* first td of a row, must be flex */ td:first-child { - display: flex; - align-items: center; + display: flex; + align-items: center; } .nested { - display: none; + display: none; } .expand-collapse { - cursor: pointer; + cursor: pointer; } .caret-cell { - border: none; - background: none; - padding: 0; + border: none; + background: none; + padding: 0; } -input, select { - background-color: var(--blender-button-background, var(--bg-color)); - color: var(--blender-button-text, var(--text-color)); - border-color: var(--blender-button-border, var(--border-color)); - transition: filter 0.2s ease; - border-radius: 6px; - padding: 6px 8px; - font-size: 14px; - line-height: 20px; - transition: border-color 0.2s cubic-bezier(0.3, 0, 0.5, 1); - height: inherit; - margin-left: 0.5em; - margin-right: 0.5em; - - min-width: 50px; /* Minimum width to prevent it from becoming too small */ +input, +select { + background-color: var(--blender-button-background, var(--bg-color)); + color: var(--blender-button-text, var(--text-color)); + border-color: var(--blender-button-border, var(--border-color)); + transition: filter 0.2s ease; + border-radius: 6px; + padding: 6px 8px; + font-size: 14px; + line-height: 20px; + transition: border-color 0.2s cubic-bezier(0.3, 0, 0.5, 1); + height: inherit; + margin-left: 0.5em; + margin-right: 0.5em; + min-width: 50px; /* Minimum width to prevent it from becoming too small */ } input:focus { - border-color: #28a745; - outline: none; - box-shadow: 0 0 0 3px #28a745; + border-color: #28a745; + outline: none; + box-shadow: 0 0 0 3px #28a745; } -#cost-items tr:hover, .highlighted { - background-color: #ffb936; - color : black; +#cost-items tr:hover, +.highlighted { + background-color: #ffb936; + color: black; } .highlight { - background-color: #ffb936; - color : black; + background-color: #ffb936; + color: black; } .cost-values-table { - width: 100%; - border-collapse: collapse; + width: 100%; + border-collapse: collapse; } -.cost-values-table th, .cost-values-table td { - border: 1px solid #ddd; - padding: 8px; +.cost-values-table th, +.cost-values-table td { + border: 1px solid #ddd; + padding: 8px; } .cost-values-table th { - background-color: #444444; - text-align: center; + background-color: #444444; + text-align: center; } /* Responsive Styles */ @media (max-width: 768px) { - table { - font-size: 12px; - } + table { + font-size: 12px; + } - td, th { - padding: 4px; - } - - input { - padding: 2px; - font-size: 12px; - } + td, + th { + padding: 4px; + } + input { + padding: 2px; + font-size: 12px; + } } @media (max-width: 480px) { - table { - font-size: 10px; - } + table { + font-size: 10px; + } - td, th { - padding: 2px; - } + td, + th { + padding: 2px; + } - input { - padding: 1px; - font-size: 10px; - } -} \ No newline at end of file + input { + padding: 1px; + font-size: 10px; + } +} diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/cost.css b/src/bonsai/bonsai/bim/data/webui/static/css/cost.css index 888971886e..fc4e82f4a5 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/cost.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/cost.css @@ -1,130 +1,75 @@ @import url("./shared.css"); @import url("./components/card.css"); @import url("./components/contextMenu.css"); - -table { - width: 100%; - border-collapse: collapse; - user-select: none; -} - -td:not(:first-child) { - text-align: center; - justify-content: space-between; - align-items: center; -} - -td { - border-right: 1px solid #ddd; - padding-left: 0.5em; - padding-right: 0.5em; -} - -.nested { - display: none; -} - -.expand-collapse { - cursor: pointer; -} - -.caret-cell { - border: none; - background: none; - padding: 0; -} - -input, select { - background-color: var(--blender-button-background, var(--bg-color)); - color: var(--blender-button-text, var(--text-color)); - border-color: var(--blender-button-border, var(--border-color)); - transition: filter 0.2s ease; - border-radius: 6px; - padding: 6px 8px; - font-size: 14px; - line-height: 20px; - transition: border-color 0.2s cubic-bezier(0.3, 0, 0.5, 1); - height: inherit; - margin-left: 0.5em; - margin-right: 0.5em; -} - -input:focus { - border-color: #28a745; - outline: none; - box-shadow: 0 0 0 3px #28a745; -} - -#cost-items tr:hover, .highlighted { - background-color: #ffb936; - color : black; -} - -.highlight { - background-color: #ffb936; - color : black; -} +@import url("./components/costTable.css"); .form-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; - background-color: #363636; - padding: 10px; + display: flex; + justify-content: flex-start; + align-items: center; + margin-bottom: 10px; + background-color: #363636; + padding: 10px; + /* trext size */ + font-size: 1.5em; +} + +span { + margin-left: var(--margin-small); } .floating-form { - min-width: 40%; - height: auto; - position: absolute; - background-color: #363636; - border: 1px solid black; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); - z-index: 9999; - cursor: move; - padding: 20px; - user-select: none; -} - -.cost-values-table { - width: 100%; - border-collapse: collapse; -} - -.cost-values-table th, .cost-values-table td { - border: 1px solid #ddd; - padding: 8px; -} - -.cost-values-table th { - background-color: #444444; - text-align: center; -} - -.close-button { - position: absolute; - top: 10px; - right: 10px; - font-size: 20px; - cursor: pointer; + min-width: 40%; + max-width: 80%; + max-height: 80vh; + height: auto; + position: absolute; + background-color: #363636; + border: 1px solid black; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + z-index: 9999; + cursor: move; + padding: 20px; + user-select: none; + overflow-y: auto; + overflow-x: hidden; } .action-button { - background-color: #28a745; - border: none; - color: #fff; - padding: 0.5em 1em; - cursor: pointer; - border-radius: 6px; - font-weight: bold; - text-decoration: none; - display: inline-block; - transition: background-color 0.3s ease; - width: fit-content; - margin-left: 0.5em; + background-color: #28a745; + border: none; + color: #fff; + padding: 0.5em 1em; + cursor: pointer; + border-radius: 6px; + font-weight: bold; + text-decoration: none; + display: inline-block; + transition: background-color 0.3s ease; + width: fit-content; + margin: 0.5em; } .action-button:hover { - background-color: #218838; + background-color: #218838; +} + +.close-button { + position: absolute; + top: 10px; + right: 10px; + font-size: 20px; + cursor: pointer; +} + +.settings-menu { + display: none; + position: absolute; + background-color: #363636; + border: 1px solid black; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + z-index: 9999; + cursor: move; + padding: 20px; + user-select: none; } diff --git a/src/bonsai/bonsai/bim/data/webui/static/js/cost.js b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js index b9e2f29919..a8c031281c 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/js/cost.js +++ b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js @@ -3,7 +3,7 @@ import { CostUI } from "./utilities/costui.js"; const connectedClients = {}; let socket; -let isScheduleLoad = {}; +let loadedSchedules = {}; $(document).ready(function () { var defaultTheme = "blender"; @@ -11,7 +11,21 @@ $(document).ready(function () { setTheme(theme); connectSocket(); - CostUI.createColorPicker(); + + const settingsMenu = CostUI.Form({ + id: "settings-menu", + name: "Settings", + icon: "fa-solid fa-gear", + shouldHide: true, + }); + settingsMenu.style.display = "none"; + const picker = CostUI.createColorPicker(); + settingsMenu.appendChild(picker); + + const settingsButton = document.getElementById("settings-button"); + settingsButton.addEventListener("click", function () { + settingsMenu.style.display = "block"; + }); }); function connectSocket() { @@ -28,6 +42,46 @@ function connectSocket() { socket.on("cost_items", handleCostItemsData); socket.on("cost_values", handleCostValuesData); socket.on("cost_value", handleCostValueData); + socket.on("selected_products", handleSelectedProducts); +} + +function handleSelectedProducts(data) { + const costItemId = data.data["selected_products"]["cost_item_id"]; + const products = data.data["selected_products"]["selected_products"]; + const assigned_products = data.data["selected_products"]["assigned_products"]; + const quantityNames = + data.data["selected_products"]["product_quantity_names"]; + const formId = "selected-products-" + costItemId; + const form = CostUI.Form({ + id: formId, + name: "Selected Products", + icon: "fa-solid fa-cart-shopping", + }); + CostUI.highlightElement(costItemId); + const selectedProductsTable = CostUI.createProductTable({ + form, + products, + quantityNames, + costItemId, + callbacks: { + addProductAssignments: addProductAssignments, + getSelectedProducts: getSelectedProducts, + }, + }); + if (assigned_products.length > 0) { + const assignedProductsText = CostUI.Text( + "Assigned Products", + "fa-solid fa-box", + "x-large" + ); + form.appendChild(assignedProductsText); + const assignmentsTable = CostUI.createAssignmentsTable({ + form, + products: assigned_products, + quantityNames, + costItemId, + }); + } } function handlePredefinedTypes(data) { @@ -37,6 +91,15 @@ function handlePredefinedTypes(data) { ); } +function addProductAssignments({ costItemId, selectedQuantity, productIds }) { + executeOperator({ + type: "addProductAssignments", + costItemId: costItemId, + propName: selectedQuantity, + productIds: productIds, + }); +} + function addCostSchedule(name, predefinedType) { executeOperator({ type: "addCostSchedule", @@ -109,7 +172,7 @@ function handleCostValuesData(data) { }, }); - CostUI.highlightCostItem(data.data["cost_values"]["cost_item_id"]); + CostUI.highlightElement(data.data["cost_values"]["cost_item_id"]); } function addCostValue(costItemId) { @@ -200,6 +263,9 @@ function addSummaryCostItem(costScheduleId) { function handleCostSchedulesData(data) { const blenderId = data.blenderId; + const loadedSchedule = loadedSchedules[blenderId] + ? loadedSchedules[blenderId] + : null; const costSchedules = data.data["cost_schedules"]["cost_schedules"]; if (costSchedules.length === 0) { return; @@ -210,24 +276,50 @@ function handleCostSchedulesData(data) { const costScheduleDiv = document.getElementById("cost-schedules"); costScheduleDiv.innerHTML = ""; costSchedules.forEach((costSchedule) => { - costSchedule.UpdateDate = new Date(costSchedule.UpdateDate); - const predefinedType = CostUI.text("Predefined Type: " + costSchedule.PredefinedType) - const updatedOn = CostUI.text("Updated On: " + costSchedule.UpdateDate); + const predefinedType = CostUI.Text( + "Predefined Type: " + costSchedule.PredefinedType, + "fa-solid fa-paperclip" + ); + // simplyfying the date + const updatedDate = new Date(costSchedule.UpdateDate).toLocaleDateString(); + const updatedOn = CostUI.Text( + "Updated On: " + updatedDate, + "fa-solid fa-pen-nib" + ); const div = document.createElement("div"); - + div.appendChild(predefinedType); div.appendChild(updatedOn); const callback = () => loadCostSchedule(costSchedule.id, blenderId); - const card = CostUI.createCard(costSchedule.Name, div, callback); + const card = CostUI.createCard( + costSchedule.id, + costSchedule.Name, + div, + callback + ); costScheduleDiv.append(card); }); + loadedSchedule ? CostUI.highlightElement("schedule-" + loadedSchedule) : null; } function handleCostItemsData(data) { + const cards = document.querySelectorAll("[id^='schedule-']"); + cards.forEach((card) => { + if (card.id === "schedule-" + data.data["cost_items"]["cost_schedule_id"]) { + card.classList.add("highlighted"); + } else { + card.classList.remove("highlighted"); + } + }); + + const costScheduleId = data.data["cost_items"]["cost_schedule_id"]; + loadedSchedules[data.blenderId] = costScheduleId; + CostUI.highlightElement("schedule-" + costScheduleId); + CostUI.createCostSchedule({ data: data.data["cost_items"]["cost_items"], - costScheduleId: data.data["cost_items"]["cost_schedule_id"], + costScheduleId: costScheduleId, blenderID: data.blenderId, callbacks: { addCostItem: addCostItem, @@ -237,10 +329,15 @@ function handleCostItemsData(data) { editCostItemName: editCostItemName, enableEditingCostValues: enableEditingCostValues, addSummaryCostItem: addSummaryCostItem, + getSelectedProducts: getSelectedProducts, }, }); } +function getSelectedProducts(costItemId) { + executeOperator({ type: "getSelectedProducts", costItemId: costItemId }); +} + function duplicateCostItem(costItemId) { executeOperator({ type: "duplicateCostItem", costItemId: costItemId }); } diff --git a/src/bonsai/bonsai/bim/data/webui/static/js/gantt.js b/src/bonsai/bonsai/bim/data/webui/static/js/gantt.js index 59043f3d59..aa83615c77 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/js/gantt.js +++ b/src/bonsai/bonsai/bim/data/webui/static/js/gantt.js @@ -128,7 +128,7 @@ function handleWorkScheduleData(data) { workSchedules.forEach((workSchedule) => { const mainContainer = CostUI.text(new Date(workSchedule.CreationDate).toLocaleDateString()); const callback = () => loadWorkSchedule(workSchedule.id); - const card = CostUI.createCard(workSchedule.Name,mainContainer, callback); + const card = CostUI.createCard(workSchedule.id, workSchedule.Name,mainContainer, callback); workSchedulesDiv.append(card); }); } diff --git a/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js b/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js index 3419c9409a..876e1affef 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js +++ b/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js @@ -69,7 +69,7 @@ export class CostUI { `; document.head.appendChild(style); } -static createContextMenu(callbacks) { + static createContextMenu(callbacks) { const contextMenu = document.createElement("div"); contextMenu.id = "context-menu"; contextMenu.classList.add("context-menu"); @@ -78,93 +78,107 @@ static createContextMenu(callbacks) { + `; document.body.appendChild(contextMenu); document.addEventListener("contextmenu", function (event) { - event.preventDefault(); - const targetRow = event.target.closest("tr"); - const targetCell = event.target.closest("td"); - if (targetRow && targetRow.parentElement.id === "cost-items") { - const contextMenu = document.getElementById("context-menu"); - contextMenu.style.display = "block"; - contextMenu.style.left = `${event.pageX}px`; - contextMenu.style.top = `${event.pageY}px`; + event.preventDefault(); + const targetRow = event.target.closest("tr"); + const targetCell = event.target.closest("td"); + if (targetRow && targetRow.parentElement.id === "cost-items") { + const contextMenu = document.getElementById("context-menu"); + contextMenu.style.display = "block"; + contextMenu.style.left = `${event.pageX}px`; + contextMenu.style.top = `${event.pageY}px`; - contextMenu.targetRow = targetRow; - contextMenu.targetCell = targetCell; - } else { - document.getElementById("context-menu").style.display = "none"; - } + contextMenu.targetRow = targetRow; + contextMenu.targetCell = targetCell; + } else { + document.getElementById("context-menu").style.display = "none"; + } }); document.addEventListener("click", function (event) { - const contextMenu = document.getElementById("context-menu"); - if (!contextMenu.contains(event.target)) { - contextMenu.style.display = "none"; - } + const contextMenu = document.getElementById("context-menu"); + if (!contextMenu.contains(event.target)) { + contextMenu.style.display = "none"; + } }); const editCostValuesButton = document.getElementById( - "edit-cost-values-button" + "edit-cost-values-button" ); editCostValuesButton.addEventListener("click", function () { - const targetRow = document.getElementById("context-menu").targetRow; - const targetCell = document.getElementById("context-menu").targetCell; - if (targetRow) { - const costItemId = parseInt(targetRow.getAttribute("id")); - callbacks.enableEditingCostValues - ? callbacks.enableEditingCostValues(costItemId) - : null; - } - document.getElementById("context-menu").style.display = "none"; + const targetRow = document.getElementById("context-menu").targetRow; + const targetCell = document.getElementById("context-menu").targetCell; + if (targetRow) { + const costItemId = parseInt(targetRow.getAttribute("id")); + callbacks.enableEditingCostValues + ? callbacks.enableEditingCostValues(costItemId) + : null; + } + document.getElementById("context-menu").style.display = "none"; }); const addButton = document.getElementById("add-cost-item-button"); if (!addButton.dataset.listenerAdded) { - function addCostItemHandler(e) { - e.stopPropagation(); - const targetRow = document.getElementById("context-menu").targetRow; - if (targetRow) { - const costItemId = parseInt(targetRow.getAttribute("id")); - callbacks.addCostItem ? callbacks.addCostItem(costItemId) : null; - } - document.getElementById("context-menu").style.display = "none"; + function addCostItemHandler(e) { + e.stopPropagation(); + const targetRow = document.getElementById("context-menu").targetRow; + if (targetRow) { + const costItemId = parseInt(targetRow.getAttribute("id")); + callbacks.addCostItem ? callbacks.addCostItem(costItemId) : null; } - addButton.addEventListener("click", addCostItemHandler); - addButton.dataset.listenerAdded = "true"; + document.getElementById("context-menu").style.display = "none"; + } + addButton.addEventListener("click", addCostItemHandler); + addButton.dataset.listenerAdded = "true"; } const deleteCostItem = document.getElementById("delete-cost-item"); if (deleteCostItem && !deleteCostItem.hasListener) { - deleteCostItem.addEventListener("click", function () { - const targetRow = document.getElementById("context-menu").targetRow; - if (targetRow) { - const costItemId = parseInt(targetRow.getAttribute("id")); - let expandedState = - JSON.parse(localStorage.getItem("expandedState")) || {}; - expandedState = CostUI.deleteRow(targetRow, expandedState); - localStorage.setItem("expandedState", JSON.stringify(expandedState)); - callbacks.deleteCostItem(costItemId); - } - document.getElementById("context-menu").style.display = "none"; - }); - deleteCostItem.hasListener = true; + deleteCostItem.addEventListener("click", function () { + const targetRow = document.getElementById("context-menu").targetRow; + if (targetRow) { + const costItemId = parseInt(targetRow.getAttribute("id")); + let expandedState = + JSON.parse(localStorage.getItem("expandedState")) || {}; + expandedState = CostUI.deleteRow(targetRow, expandedState); + localStorage.setItem("expandedState", JSON.stringify(expandedState)); + callbacks.deleteCostItem(costItemId); + } + document.getElementById("context-menu").style.display = "none"; + }); + deleteCostItem.hasListener = true; } const duplicateButton = document.getElementById("duplicate-button"); if (duplicateButton && !duplicateButton.hasListener) { - duplicateButton.addEventListener("click", function () { - const targetRow = document.getElementById("context-menu").targetRow; - if (targetRow) { - const costItemId = parseInt(targetRow.getAttribute("id")); - callbacks.duplicateCostItem(costItemId); - } - document.getElementById("context-menu").style.display = "none"; - }); - duplicateButton.hasListener = true; + duplicateButton.addEventListener("click", function () { + const targetRow = document.getElementById("context-menu").targetRow; + if (targetRow) { + const costItemId = parseInt(targetRow.getAttribute("id")); + callbacks.duplicateCostItem(costItemId); + } + document.getElementById("context-menu").style.display = "none"; + }); + duplicateButton.hasListener = true; } -} + + const getSelectedProducts = document.getElementById("assign-selection"); + if (getSelectedProducts && !getSelectedProducts.hasListener) { + getSelectedProducts.addEventListener("click", function () { + const targetRow = document.getElementById("context-menu").targetRow; + if (targetRow) { + const costItemId = parseInt(targetRow.getAttribute("id")); + callbacks.getSelectedProducts(costItemId); + } + document.getElementById("context-menu").style.display = "none"; + }); + getSelectedProducts.hasListener = true; + } + } static deleteRow(targetRow, expandedState) { if (!targetRow) { @@ -244,7 +258,6 @@ static createContextMenu(callbacks) { const form = CostUI.Form({ id: "add-cost-schedule-form", name: name, - className: "floating-form", }); const nameLabel = document.createElement("label"); @@ -279,8 +292,6 @@ static createContextMenu(callbacks) { submitButton.classList.add("action-button"); form.appendChild(submitButton); - document.body.appendChild(form); - form.addEventListener("submit", function (event) { event.preventDefault(); const formData = new FormData(form); @@ -296,7 +307,7 @@ static createContextMenu(callbacks) { static createColorPicker() { const div = document.createElement("div"); - div.classList.add("card"); + div.classList.add("row-container"); const colorPicker = document.createElement("input"); colorPicker.type = "color"; colorPicker.id = "color-picker"; @@ -305,7 +316,6 @@ static createContextMenu(callbacks) { colorText.textContent = "Select a color to change row color:"; div.appendChild(colorText); div.appendChild(colorPicker); - document.getElementById("UI").appendChild(div); colorPicker.addEventListener("input", function () { const baseColor = colorPicker.value; @@ -314,6 +324,7 @@ static createContextMenu(callbacks) { }); const colorScheme = CostUI.generateColorScheme("#000000"); CostUI.applyColorScheme("cost-items", colorScheme); + return div; } static createCostSchedule({ @@ -373,7 +384,10 @@ static createContextMenu(callbacks) { const totalQuantity = costItem.TotalCostQuantity ? parseFloat(costItem.TotalCostQuantity).toFixed(2) : "-"; - const row = CostUI.createTableRow(costItem, nestingLevel, parentID); + const appliedValue = costItem.TotalAppliedValue + ? parseFloat(costItem.TotalAppliedValue).toFixed(2) + : "-"; + const row = CostUI.createCostItemRow(costItem, nestingLevel, parentID); const expandButton = CostUI.createExpandButton(costItem); const nameCell = CostUI.createNameCell( costItem, @@ -383,9 +397,7 @@ static createContextMenu(callbacks) { ); const totalCostQuantityCell = CostUI.createTableCell(totalQuantity); const unitSymbolCell = CostUI.createTableCell(costItem.UnitSymbol); - const totalAppliedValueCell = CostUI.createTableCell( - costItem.TotalAppliedValue - ); + const totalAppliedValueCell = CostUI.createTableCell(appliedValue); const totalCostCell = CostUI.createTotalCostCell(costItem); const flexContainerCell = CostUI.createFlexContainerCell( costItem, @@ -411,7 +423,7 @@ static createContextMenu(callbacks) { return row; } - static createTableRow(costItem, nestingLevel, parentID) { + static createCostItemRow(costItem, nestingLevel, parentID) { const row = document.createElement("tr"); row.setAttribute("id", costItem.id); row.setAttribute("parent-id", parentID); @@ -613,17 +625,17 @@ static createContextMenu(callbacks) { return selectButton; } - static highlightCostItem(id) { - const row = document.getElementById(id); - if (row) { - row.classList.add("highlighted"); + static highlightElement(id) { + const element = document.getElementById(id); + if (element) { + element.classList.add("highlighted"); } } - static unhighlightCostItem(id) { - const row = document.getElementById(id); - if (row) { - row.classList.remove("highlighted"); + static unhighlightElement(id) { + const element = document.getElementById(id); + if (element) { + element.classList.remove("highlighted"); } } @@ -636,15 +648,26 @@ static createContextMenu(callbacks) { return ids; } - static text(label) { - const text = document.createElement("p"); + static Text(label, icon = null, size = "medium") { + const text = document.createElement("span"); text.textContent = label; - return text; + if (icon === null) { + return text; + } + const div = document.createElement("div"); + div.classList.add("row-container"); + const i = document.createElement("i"); + i.className = icon; + div.appendChild(i); + div.appendChild(text); + div.style.fontSize = size; + return div; } - static createCard(title, mainContainer, callback) { + static createCard(id, title, mainContainer, callback) { const card = document.createElement("div"); card.classList.add("card"); + card.id = "schedule-" + id; const cardBody = document.createElement("div"); cardBody.classList.add("card-body"); @@ -684,30 +707,26 @@ static createContextMenu(callbacks) { let formName = "Cost Values: " + costItemName; const form = CostUI.Form({ + icon: "fa-dollar-sign", id: formId, name: formName, - className: "floating-form", }); - const table = CostUI.createCostValuesTable(costItemId, [ - "Type", - "Category", - "Value", - "", - ]); + const { tableContainer, table } = CostUI.addTable({ + headers: ["Type", "Category", "Value", ""], + className: "cost-values-table", + id: "cost-values-table-" + costItemId, + }); costValues.forEach((costValue) => { const tr = CostUI.createCostvaluesRow(costItemId, costValue, callbacks); table.appendChild(tr); }); - form.appendChild(table); - const addButton = CostUI.createAddCostValueButton(costItemId, callbacks); + + form.appendChild(tableContainer); form.appendChild(addButton); - - document.body.appendChild(form); - return form; } @@ -735,10 +754,13 @@ static createContextMenu(callbacks) { table.appendChild(tr); } - static createCostValuesTable(costItemId, headers) { + static addTable({ id, className, headers }) { + const div = document.createElement("div"); + div.classList.add("table-container"); const table = document.createElement("table"); - table.classList.add("cost-values-table"); - table.id = "cost-values-table-" + costItemId; + div.appendChild(table); + table.classList.add(className); + table.id = id; const headerRow = document.createElement("tr"); headers.forEach((headerText) => { @@ -746,9 +768,17 @@ static createContextMenu(callbacks) { th.textContent = headerText; headerRow.appendChild(th); }); - table.appendChild(headerRow); - - return table; + const thead = document.createElement("thead"); + thead.appendChild(headerRow); + table.appendChild(thead); + table.get_header_row = function () { + return headerRow; + }; + table.get_header_by_name = function (name) { + const headers = headerRow.querySelectorAll("th"); + return Array.from(headers).find((header) => header.textContent === name); + }; + return { tableContainer: div, table: table }; } static createCostvaluesRow(costItemId, costValue, costValueCallbacks) { @@ -917,19 +947,31 @@ static createContextMenu(callbacks) { return addButton; } - static Form({ id, name, className }) { - const form = document.createElement("form"); - form.id = id; - form.classList.add(className); - form.style.position = "absolute"; - form.style.top = "50%"; - form.style.left = "50%"; - form.style.transform = "translate(-50%, -50%)"; - const header = CostUI.createHeader(name); - const closeButton = CostUI.createCloseButton(form); + static Form({ id, name, icon = "fa-solid fa-file", shouldHide = false }) { + let form; + if (document.getElementById(id)) { + form = document.getElementById(id); + } else { + form = document.createElement("form"); + form.id = id; + form.classList.add("floating-form"); + form.style.position = "absolute"; + form.style.top = "50%"; + form.style.left = "50%"; + form.style.transform = "translate(-50%, -50%)"; + } + + const header = CostUI.createHeader(name, icon); + let closeButton; + if (shouldHide) { + closeButton = CostUI.createHideButton(form); + } else { + closeButton = CostUI.createCloseButton(form); + } form.appendChild(header); form.appendChild(closeButton); CostUI.makeFormDraggable(form); + document.body.appendChild(form); return form; } static makeFormDraggable(form) { @@ -990,23 +1032,389 @@ static createContextMenu(callbacks) { } } - static createHeader(text) { + static createHeader(text, icon) { const header = document.createElement("div"); + const i = document.createElement("i"); + i.className = icon; header.classList.add("form-header"); - header.textContent = text; + const span = document.createElement("span"); + span.textContent = text; + header.appendChild(i); + header.appendChild(span); return header; } + static createHideButton(form) { + const hideButton = document.createElement("span"); + hideButton.classList.add("close-button"); + hideButton.innerHTML = "−"; + hideButton.addEventListener("click", function () { + form.style.display = "none"; + }); + return hideButton; + } + static createCloseButton(form) { const closeButton = document.createElement("span"); closeButton.classList.add("close-button"); closeButton.innerHTML = "×"; closeButton.addEventListener("click", function () { form.remove(); - const costItemId = form.id.split("-")[3]; - CostUI.unhighlightCostItem(costItemId); + const costItemId = form.id.split("-").pop(); + CostUI.unhighlightElement(costItemId); }); return closeButton; } + + static createProductRow(product, shouldAddQto = false) { + const row = document.createElement("tr"); + + const cellData = { + id: product.info.id, + class: product.info.class, + name: product.info.name, + type: product.info.type, + count: "1", + }; + + for (const [key, value] of Object.entries(cellData)) { + const cell = document.createElement("td"); + cell.textContent = value; + row.appendChild(cell); + } + + if (shouldAddQto) { + const quantitySets = product.qtos; + Object.entries(quantitySets).forEach(([qtoName, qto]) => { + Object.entries(qto).forEach(([key, value]) => { + if (key !== "id") { + const cell = document.createElement("td"); + // check if value is a number + cell.textContent = parseFloat(value).toFixed(2); + row.appendChild(cell); + } + }); + }); + } + + return row; + } + + static addQuantityHeaders(table, products, shouldAddQto = true) { + const headerRow = table.get_header_row(); + const countHeader = table.get_header_by_name("Count"); + const quantitySums = { count: 0 }; + const quantityHeaders = { count: countHeader }; + if (!shouldAddQto) return { quantitySums, quantityHeaders }; + if (products.length > 0) { + const quantitySets = products[0].qtos; + Object.entries(quantitySets).forEach(([qtoName, qto]) => { + Object.keys(qto).forEach((key) => { + if (key !== "id") { + const qtoHeader = document.createElement("th"); + qtoHeader.textContent = key; + qtoHeader.style.cursor = "pointer"; + headerRow.appendChild(qtoHeader); + quantitySums[key] = 0; + quantityHeaders[key] = qtoHeader; + } + }); + }); + } + + return { quantitySums, quantityHeaders }; + } + + static getAssinedQuantity(products, costItemId) { + let quantityAssigned = null; + products.forEach((product) => { + const product_id = product.info.id; + const assignedQuantities = product.assigned_quantities; + if (assignedQuantities.length > 0) { + assignedQuantities.forEach((assignedQuantity) => { + if (assignedQuantity.cost_item_id === costItemId) { + product.isProductAssigned = true; + quantityAssigned = assignedQuantity.name; + } + }); + } + }); + return quantityAssigned; + } + + static calculateSums(qtos, quantitySums) { + Object.entries(qtos).forEach(([qtoName, qto]) => { + Object.entries(qto).forEach(([key, value]) => { + if (key !== "id") { + quantitySums[key] += parseFloat(value); + } + }); + }); + } + + static countProducts(products, quantitySums) { + products.forEach((product) => { + quantitySums.count += 1; + }); + } + + static addSubTotalRow(quantitySums, shouldAddQto = true) { + const subtotalRow = document.createElement("tr"); + + const cells = [ + { textContent: "Subtotal", colSpan: 4, style: { border: "none" } }, + { textContent: "", style: { border: "none" } }, + { textContent: "", style: { border: "none" } }, + { textContent: "", style: { border: "none" } }, + { textContent: quantitySums.count }, + ]; + + cells.forEach((cellData) => { + const cell = document.createElement("td"); + if (cellData.textContent !== undefined) + cell.textContent = cellData.textContent; + if (cellData.colSpan !== undefined) cell.colSpan = cellData.colSpan; + if (cellData.style !== undefined) + Object.assign(cell.style, cellData.style); + subtotalRow.appendChild(cell); + }); + + if (shouldAddQto) { + Object.keys(quantitySums).forEach((key) => { + if (key !== "count") { + const subtotalCell = document.createElement("td"); + subtotalCell.textContent = quantitySums[key].toFixed(2); + subtotalRow.appendChild(subtotalCell); + } + }); + } + + subtotalRow.style.fontWeight = "bold"; + + return subtotalRow; + } + + static createProductTable({ + form, + products, + quantityNames, + costItemId, + callbacks, + }) { + const { tableContainer, table } = CostUI.addTable({ + headers: ["Step Id", "Class", "Name", "Type", "Count"], + className: "product-table", + id: "cost-values-table-" + costItemId, + }); + + const tbody = document.createElement("tbody"); + table.appendChild(tbody); + form.appendChild(tableContainer); + + const quantityAssigned = CostUI.getAssinedQuantity(products, costItemId); + const areProductsSameClass = products.every( + (product) => product.info.class === products[0].info.class + ); + + const shouldAddQto = areProductsSameClass ? true : false; + const headerRow = table.get_header_row(); + const { quantitySums, quantityHeaders } = CostUI.addQuantityHeaders( + table, + products, + shouldAddQto + ); + + if (shouldAddQto) { + CostUI.highlightColumn( + table, + headerRow, + quantityHeaders, + quantityAssigned + ); + + products.forEach((product) => { + const row = CostUI.createProductRow(product, shouldAddQto); + tbody.appendChild(row); + + const isProductAssigned = product.assigned_quantities + ? product.assigned_quantities.some( + (assignedQuantity) => assignedQuantity.cost_item_id === costItemId + ) + : false; + isProductAssigned ? row.classList.add("highlighted") : null; + + CostUI.calculateSums(product.qtos, quantitySums); + }); + } else { + products.forEach((product) => { + const row = CostUI.createProductRow(product, shouldAddQto); + tbody.appendChild(row); + }); + } + + CostUI.countProducts(products, quantitySums); + + const subtotalRow = CostUI.addSubTotalRow(quantitySums, shouldAddQto); + tbody.appendChild(subtotalRow); + + const quantitySelect = document.createElement("select"); + + if (shouldAddQto) { + quantityNames.forEach((name) => { + const option = document.createElement("option"); + option.value = name; + option.text = name; + quantitySelect.appendChild(option); + }); + } + + const option = document.createElement("option"); + option.value = "count"; + option.text = "count"; + quantitySelect.appendChild(option); + + quantitySelect.addEventListener("change", (event) => { + const selectedQuantity = event.target.value; + + Object.values(quantityHeaders).forEach((header) => { + header.classList.remove("highlighted"); + }); + table.querySelectorAll("td").forEach((cell) => { + cell.classList.remove("highlighted"); + }); + + if (quantityHeaders[selectedQuantity]) { + quantityHeaders[selectedQuantity].classList.add("highlighted"); + const columnIndex = Array.from(headerRow.children).indexOf( + quantityHeaders[selectedQuantity] + ); + table.querySelectorAll(`tr`).forEach((row) => { + row.children[columnIndex].classList.add("highlighted"); + }); + } + }); + + Object.entries(quantityHeaders).forEach(([key, header]) => { + header.addEventListener("click", () => { + quantitySelect.value = key; + const event = new Event("change"); + quantitySelect.dispatchEvent(event); + }); + }); + + quantityAssigned + ? CostUI.highlightColumn( + table, + headerRow, + quantityHeaders, + quantityAssigned + ) + : null; + + quantitySelect.value = quantityAssigned ? quantityAssigned : "count"; + + const productIds = products.map((product) => product.info.id); + callbacks.emptyForm = () => { + form.innerHTML = ""; + }; + const addProductAssignmentsButton = CostUI.addProductAssignmentsButton( + costItemId, + productIds, + quantitySelect, + callbacks + ); + + form.appendChild(quantitySelect); + form.appendChild(addProductAssignmentsButton); + return table; + } + + static createAssignmentsTable({ + form, + products, + costItemId, + quantityNames, + callbacks, + }) { + const { tableContainer, table } = CostUI.addTable({ + headers: ["Step Id", "Class", "Name", "Type", "Count"], + className: "product-table", + id: "cost-values-table-" + costItemId, + }); + + const tbody = document.createElement("tbody"); + table.appendChild(tbody); + form.appendChild(tableContainer); + + let quantityAssigned = CostUI.getAssinedQuantity(products, costItemId); + const shouldAddQto = quantityAssigned ? true : false; + const headerRow = table.get_header_row(); + const { quantitySums, quantityHeaders } = CostUI.addQuantityHeaders( + table, + products, + shouldAddQto + ); + + shouldAddQto + ? CostUI.highlightColumn( + table, + headerRow, + quantityHeaders, + quantityAssigned + ) + : null; + + products.forEach((product) => { + const row = CostUI.createProductRow(product, shouldAddQto); + tbody.appendChild(row); + shouldAddQto ? CostUI.calculateSums(product.qtos, quantitySums) : null; + }); + + CostUI.countProducts(products, quantitySums); + + const subtotalRow = CostUI.addSubTotalRow(quantitySums, shouldAddQto); + tbody.appendChild(subtotalRow); + } + + static highlightColumn(table, headerRow, quantityHeaders, quantityAssigned) { + if (quantityAssigned) { + if (quantityHeaders[quantityAssigned]) { + quantityHeaders[quantityAssigned].classList.add("highlighted"); + const columnIndex = Array.from(headerRow.children).indexOf( + quantityHeaders[quantityAssigned] + ); + table.querySelectorAll(`tr`).forEach((row) => { + row.children[columnIndex].classList.add("highlighted"); + }); + } + } + } + static addProductAssignmentsButton( + costItemId, + productIds, + quantitySelect, + callbacks + ) { + const addProductAssignmentsButton = document.createElement("button"); + addProductAssignmentsButton.textContent = "Add Product Assignments"; + addProductAssignmentsButton.addEventListener("click", (event) => { + event.preventDefault(); + let propName = quantitySelect.value; + if (propName === "count") { + propName = ""; + } + callbacks.addProductAssignments + ? callbacks.addProductAssignments({ + costItemId, + selectedQuantity: propName, + productIds, + }) + : null; + callbacks.emptyForm ? callbacks.emptyForm() : null; + callbacks.getSelectedProducts(costItemId); + }); + addProductAssignmentsButton.classList.add("action-button"); + return addProductAssignmentsButton; + } } diff --git a/src/bonsai/bonsai/bim/data/webui/templates/costing.html b/src/bonsai/bonsai/bim/data/webui/templates/costing.html index 4afa79ffbc..d165b7007d 100644 --- a/src/bonsai/bonsai/bim/data/webui/templates/costing.html +++ b/src/bonsai/bonsai/bim/data/webui/templates/costing.html @@ -75,6 +75,9 @@ +
-
-
+ +