From f2554bc9688790e7574075bc77456c03ca807a5d Mon Sep 17 00:00:00 2001 From: Yassine Oualid Date: Sun, 1 Sep 2024 01:40:53 +0100 Subject: [PATCH] - Add button to generate cost schedule - Feature to view, edit and add cost item values. ( Right Click - Edit ). --- src/bonsai/bonsai/bim/data/webui/sioserver.py | 10 + .../static/css/components/contextMenu.css | 89 ++++ .../bim/data/webui/static/css/gantt.css | 1 + .../bonsai/bim/data/webui/static/js/cost.js | 48 +- .../bonsai/bim/data/webui/static/js/gantt.js | 7 - .../data/webui/static/js/utilities/costui.js | 458 ++++++++++++++---- .../bim/data/webui/templates/costing.html | 3 +- src/bonsai/bonsai/bim/module/cost/__init__.py | 1 + src/bonsai/bonsai/bim/module/cost/data.py | 10 +- src/bonsai/bonsai/bim/module/cost/operator.py | 11 + src/bonsai/bonsai/bim/module/cost/ui.py | 3 +- src/bonsai/bonsai/core/cost.py | 5 + src/bonsai/bonsai/tool/cost.py | 11 +- src/bonsai/bonsai/tool/web.py | 72 ++- .../ifcopenshell/util/cost.py | 14 + 15 files changed, 616 insertions(+), 127 deletions(-) create mode 100644 src/bonsai/bonsai/bim/data/webui/static/css/components/contextMenu.css diff --git a/src/bonsai/bonsai/bim/data/webui/sioserver.py b/src/bonsai/bonsai/bim/data/webui/sioserver.py index fe49b03bdd..9d1651b0a9 100644 --- a/src/bonsai/bonsai/bim/data/webui/sioserver.py +++ b/src/bonsai/bonsai/bim/data/webui/sioserver.py @@ -166,6 +166,16 @@ class BlenderNamespace(socketio.AsyncNamespace): blender_messages[sid]["cost_schedules"] = data await sio.emit("cost_schedules", {"blenderId": sid, "data": data}, namespace="/web") + async def on_cost_values(self, sid, data): + print(f"Cost values from Blender client {sid}") + blender_messages[sid]["cost_values"] = data + await sio.emit("cost_values", {"blenderId": sid, "data": data}, namespace="/web") + + async def on_cost_value(self, sid, data): + print(f"Cost values from Blender client {sid}") + blender_messages[sid]["cost_value"] = data + await sio.emit("cost_value", {"blenderId": sid, "data": data}, namespace="/web") + async def schedules(request): with open("templates/index.html", "r") as f: template = f.read() diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/components/contextMenu.css b/src/bonsai/bonsai/bim/data/webui/static/css/components/contextMenu.css new file mode 100644 index 0000000000..79ba11cc68 --- /dev/null +++ b/src/bonsai/bonsai/bim/data/webui/static/css/components/contextMenu.css @@ -0,0 +1,89 @@ + +th { + position: relative; +} +.resizer { + position: absolute; + right: 0; + top: 0; + width: 5px; + height: 100%; + cursor: col-resize; + user-select: none; +} +.context-menu { + display: none; + position: absolute; + background-color: rgb(51, 51, 51); + border: 1px solid #ccc; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + z-index: 1000; + color: white; +} +.context-menu button { + display: block; + width: 100%; + padding: 8px; + border: none; + background: none; + text-align: left; + cursor: pointer; +} +.context-menu button:hover { + background-color: #f0f0f0; + color: black; +} + +/* Context menu button is clicked, should zoom into object and change color */ +.context-menu button:active { + background-color: #ff9634; + color: rgba(37, 51, 77, 0.466); + /* zoom */ + transform: scale(0.9); +} + +#cost-items tr:hover { + background-color: #f0f0f0; + color : black; +} + +.form-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + background-color: #363636; + padding: 10px; +} + +.cost-values-form { + position: absolute; /* Set initial position to absolute */ + background-color: white; + border: 1px solid black; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + z-index: 9999; + cursor: move; /* Change cursor to move */ +} + +.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: #f2f2f2; + text-align: left; +} + +.close-button { + position: absolute; + top: 10px; + right: 10px; + font-size: 20px; + cursor: pointer; +} \ No newline at end of file diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/gantt.css b/src/bonsai/bonsai/bim/data/webui/static/css/gantt.css index 035fd68f0b..36575b8c74 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/gantt.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/gantt.css @@ -1,4 +1,5 @@ @import url("./components/card.css"); +@import url("./components/contextMenu.css"); :root { --font-family: Arial, sans-serif; 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 1ea91590ed..5a10017b89 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/js/cost.js +++ b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js @@ -23,6 +23,8 @@ function connectSocket() { socket.on("connect", handleWebConnect); socket.on("cost_schedules", handleCostSchedulesData); socket.on("cost_items", handleCostItemsData); + socket.on("cost_values", handleCostValuesData); + socket.on("cost_value", handleCostValueData); } function handleBlenderConnect(blenderId) { @@ -46,8 +48,20 @@ function handleBlenderDisconnect(blenderId) { }); } +function removeTableElement(blenderId) { + $("#cost-items-" + blenderId).remove(); +} +function handleCostValueData(data) { + + const costItemId = data.data["cost_value"]["cost_item_id"]; + const costValueId = data.data["cost_value"]["cost_value_id"]; + console.log("Handling cost value data", costItemId, costValueId); + CostUI.addNewCostValueRow(costItemId, costValueId); + +} + function handleConnectedClients(data) { $("#blender-count").text(data.length); @@ -56,6 +70,29 @@ function handleConnectedClients(data) { }); } +function handleCostValuesData(data) { + CostUI.createCostValuesForm({ + costValues: data.data["cost_values"]["cost_values"], + costItemId: data.data["cost_values"]["cost_item_id"], + callbacks: { + 'editCostValues': editCostValues, + 'addCostValue': addCostValue, + }, + }); +} + +function addCostValue(costItemId) { + executeOperator({ type: "addCostValue", costItemId: costItemId }); +} + +function editCostValues(costItemId, costValues) { + executeOperator({ + type: "editCostValues", + costItemId: costItemId, + costValues: costValues + }); +} + function handleThemeData(themeData) { function arrayToRgbString(arr) { const [r, g, b, a] = arr.map((num) => Math.round(num * 255)); @@ -99,7 +136,6 @@ function setTheme(theme) { } function addCostItem(costItemId) { - console.log("addCostItem", costItemId); executeOperator({ type: "addCostItem", costItemId: costItemId }); } @@ -119,11 +155,7 @@ function handleCostSchedulesData(data) { const blenderId = data.blenderId; const costSchedules = data.data["cost_schedules"]["cost_schedules"]; const currency = data.data["cost_schedules"]["currency"]["name"]; - - console.log(data.data["cost_schedules"]); - const costScheduleDiv = $("#cost-schedules"); - costSchedules.forEach((costSchedule) => { costSchedule.UpdateDate = new Date(costSchedule.UpdateDate); const mainContainer = CostUI.text("Updated On: " + costSchedule.UpdateDate); @@ -136,7 +168,6 @@ function handleCostSchedulesData(data) { } function handleCostItemsData(data) { - console.log(data); CostUI.createCostSchedule({ data: data.data["cost_items"], blenderID: data.blenderId, @@ -144,6 +175,7 @@ function handleCostItemsData(data) { "addCostItem": addCostItem, "selectAssignedElements": selectAssignedElements, 'editCostItemName': editCostItemName, + 'enableEditingCostValues': enableEditingCostValues, } }); } @@ -166,3 +198,7 @@ function loadCostSchedule(costScheduleId, blenderId) { function getCostSchedules(blenderId) { executeOperator({ type: "getCostSchedules" }, blenderId); } + +function enableEditingCostValues(costItemId) { + executeOperator({ type: "enableEditingCostValues", costItemId: costItemId}); +} \ No newline at end of file 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 5942765dfd..473cab82d9 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/js/gantt.js +++ b/src/bonsai/bonsai/bim/data/webui/static/js/gantt.js @@ -82,7 +82,6 @@ function handleBlenderDisconnect(blenderId) { function handleConnectedClients(data) { $("#blender-count").text(data.length); - // console.log(data); data.forEach(function (id) { connectedClients[id] = { shown: false, @@ -115,7 +114,6 @@ function handleThemeData(themeData) { } const cssRule = generateCssVariableRule(themeData.theme); - console.log(cssRule); var styleElement = $("#gantt-stylesheet")[0]; if (styleElement) { @@ -133,7 +131,6 @@ function handleWorkScheduleData(data) { const workSchedules = data["data"]["work_schedule_info"]; workSchedules.forEach((workSchedule) => { - console.log(workSchedule); const mainContainer = CostUI.text(new Date(workSchedule.CreationDate).toLocaleDateString()); const callback = () => loadWorkSchedule(workSchedule.id); const card = CostUI.createCard(workSchedule.Name,mainContainer, callback); @@ -142,11 +139,8 @@ function handleWorkScheduleData(data) { } function handleGanttData(data) { - console.log("running handleGanttData"); const blenderId = data["blenderId"]; - console.log(data); - const filename = data["data"]["ifc_file"]; const ganttTasks = data["data"]["gantt_data"]["tasks"]; const ganttWorkSched = data["data"]["gantt_data"]["work_schedule"]; @@ -183,7 +177,6 @@ function handleDefaultData(data) { const blenderId = data["blenderId"]; const isDirty = data["data"]["is_dirty"]; showWarning(blenderId, isDirty); - console.log('default data',data); } // Function to add a new gantt with data and filename 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 61779592bd..7ec2d6f582 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 @@ -1,14 +1,6 @@ export class CostUI { constructor() {} - createButton() { - console.log("Button created"); - } - - createInput() { - console.log("Input created"); - } - static isCostScheduleLoaded(id) { const existingTable = document.getElementById('cost-items-' + id); return existingTable !== null; @@ -17,7 +9,7 @@ export class CostUI { static removeCostSchedule(id) { document.getElementById("cost-items-" + id).remove(); } - static createTable(id) { + static createTable(id, callbacks) { CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null; const table = document.createElement("table"); @@ -51,7 +43,7 @@ export class CostUI { CostUI.addTableStyles(id); // Create context menu - CostUI.createContextMenu(); + CostUI.createContextMenu(callbacks); table.get_blender_id = function() { return this.getAttribute("id").split("-")[2]; @@ -71,52 +63,19 @@ export class CostUI { #cost-items-${id} td:not(:nth-child(1)) { width: 100px; /* Set a fixed width for other columns */ } - th { - position: relative; - } - .resizer { - position: absolute; - right: 0; - top: 0; - width: 5px; - height: 100%; - cursor: col-resize; - user-select: none; - } - .context-menu { - display: none; - position: absolute; - background-color: white; - border: 1px solid #ccc; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); - z-index: 1000; - } - .context-menu button { - display: block; - width: 100%; - padding: 8px; - border: none; - background: none; - text-align: left; - cursor: pointer; - } - .context-menu button:hover { - background-color: #f0f0f0; - } - #cost-items tr:hover { - background-color: #f0f0f0; /* Change this color to your desired hover color */ - } + `; document.head.appendChild(style); } - static createContextMenu() { + static createContextMenu(callbacks) { // Create context menu const contextMenu = document.createElement("div"); contextMenu.id = "context-menu"; contextMenu.classList.add("context-menu"); contextMenu.innerHTML = ` - + + `; @@ -126,6 +85,7 @@ export class CostUI { 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"; @@ -134,6 +94,7 @@ export class CostUI { // Store the target row in the context menu for later use contextMenu.targetRow = targetRow; + contextMenu.targetCell = targetCell; } else { document.getElementById("context-menu").style.display = "none"; } @@ -146,14 +107,32 @@ export class CostUI { } }); - document.getElementById("edit-button").addEventListener("click", function() { + const editCostValuesButton = document.getElementById("edit-cost-values-button"); + editCostValuesButton.addEventListener("click", function() { + console.log("Edit cost values executed"); const targetRow = document.getElementById("context-menu").targetRow; + const targetCell = document.getElementById("context-menu").targetCell; if (targetRow) { - // Implement your edit action here - console.log("Edit row:", targetRow.getAttribute("id")); + const costItemId = parseInt(targetRow.getAttribute("id")); + callbacks.enableEditingCostValues ? callbacks.enableEditingCostValues(costItemId) : null; } }); + 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"; + } + addButton.addEventListener("click", addCostItemHandler); + addButton.dataset.listenerAdded = "true"; + } + document.getElementById("delete-button").addEventListener("click", function() { const targetRow = document.getElementById("context-menu").targetRow; if (targetRow) { @@ -246,7 +225,7 @@ export class CostUI { } static createCostSchedule({ data, blenderID, title, callbacks = {} }) { - const [table, tbody] = CostUI.createTable(blenderID); + const [table, tbody] = CostUI.createTable(blenderID, callbacks); CostUI.createCostItem(data, tbody, 0, null, callbacks); CostUI.applyExpandedState(); } @@ -263,6 +242,35 @@ export class CostUI { } static createRow(obj, nestingLevel, parentID, callbacks = {}) { + const row = CostUI.createTableRow(obj, nestingLevel, parentID); + const expandButton = CostUI.createExpandButton(obj); + const nameCell = CostUI.createNameCell(obj, nestingLevel, expandButton, callbacks); + const totalCostQuantityCell = CostUI.createTableCell(obj.TotalCostQuantity); + const unitSymbolCell = CostUI.createTableCell(obj.UnitSymbol); + const totalAppliedValueCell = CostUI.createTableCell(obj.TotalAppliedValue); + const totalCostCell = CostUI.createTotalCostCell(obj); + const flexContainerCell = CostUI.createFlexContainerCell(obj, callbacks); + + row.appendChild(nameCell); + row.appendChild(totalCostQuantityCell); + row.appendChild(unitSymbolCell); + row.appendChild(totalAppliedValueCell); + row.appendChild(totalCostCell); + row.appendChild(flexContainerCell); + + row.get_id = function() { + return this.getAttribute("id"); + }; + + row.get_parent = function() { + const parentId = this.getAttribute("parent-id"); + return parentId ? document.getElementById(parentId) : null; + }; + + return row; + } + + static createTableRow(obj, nestingLevel, parentID) { const row = document.createElement("tr"); row.setAttribute("id", obj.id); row.setAttribute("parent-id", parentID); @@ -270,6 +278,10 @@ export class CostUI { row.classList.add("nested"); row.classList.add(`level-${nestingLevel}`); } + return row; + } + + static createExpandButton(obj) { const expandButton = document.createElement("button"); expandButton.classList.add("toggle-button"); if (obj.is_nested_by && obj.is_nested_by.length > 0) { @@ -277,51 +289,62 @@ export class CostUI { } else { expandButton.style.visibility = "hidden"; } - //row.appendChild(expandButton); - expandButton.addEventListener("click", function() { CostUI.contractExpandRow.call(this, obj.id); }); - + return expandButton; + } + + static createNameCell(obj, nestingLevel, expandButton, callbacks) { const nameCell = document.createElement("td"); const nameInput = document.createElement("input"); nameInput.value = obj.name ? obj.name : "Unnamed"; - + nameInput.addEventListener("change", function() { callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null; }); - + nameInput.addEventListener("keydown", function(event) { if (event.key === "Enter") { callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null; } }); + nameCell.style.paddingLeft = nestingLevel * 20 + "px"; nameCell.appendChild(expandButton); nameCell.appendChild(nameInput); - row.appendChild(nameCell); - - const totalCostQuantityCell = document.createElement("td"); - totalCostQuantityCell.textContent = obj.TotalCostQuantity; - row.appendChild(totalCostQuantityCell); - - const unitSymbolCell = document.createElement("td"); - unitSymbolCell.textContent = obj.UnitSymbol; - row.appendChild(unitSymbolCell); - - const totalAppliedValueCell = document.createElement("td"); - totalAppliedValueCell.textContent = obj.TotalAppliedValue; - row.appendChild(totalAppliedValueCell); - + return nameCell; + } + + static createTableCell(content) { + const cell = document.createElement("td"); + cell.textContent = content; + return cell; + } + + static createTotalCostCell(obj) { const totalCostCell = document.createElement("td"); const totalCost = parseFloat(obj.TotalCost).toFixed(2); - totalCostCell.textContent = obj.is_sum ? totalCost + " (Σ)" : totalCost; - - row.appendChild(totalCostCell); - + return totalCostCell; + } + + static createFlexContainerCell(obj, callbacks) { const divFlex = document.createElement("div"); divFlex.classList.add("flex-container"); + + const addButton = CostUI.createAddButton(obj, callbacks); + const selectButton = CostUI.createSelectButton(obj, callbacks); + + divFlex.appendChild(addButton); + divFlex.appendChild(selectButton); + + const flexContainerCell = document.createElement("td"); + flexContainerCell.appendChild(divFlex); + return flexContainerCell; + } + + static createAddButton(obj, callbacks) { const addButton = document.createElement("button"); addButton.textContent = "+"; addButton.classList.add("add-button"); @@ -329,31 +352,17 @@ export class CostUI { e.stopPropagation(); callbacks.addCostItem ? callbacks.addCostItem(obj.id) : null; }); - + return addButton; + } + + static createSelectButton(obj, callbacks) { const selectButton = document.createElement("button"); selectButton.textContent = "Select"; selectButton.addEventListener("click", function(e) { e.stopPropagation(); callbacks.selectAssignedElements ? callbacks.selectAssignedElements(obj.id) : null; }); - - divFlex.appendChild(addButton); - divFlex.appendChild(selectButton); - - const flexContainerCell = document.createElement("td"); - flexContainerCell.appendChild(divFlex); - row.appendChild(flexContainerCell); - - row.get_id = function() { - return this.getAttribute("id"); - }; - - row.get_parent = function() { - const parentId = this.getAttribute("parent-id"); - return parentId ? document.getElementById(parentId) : null; - }; - - return row; + return selectButton; } static hideNestedRows(parentId) { @@ -461,4 +470,267 @@ export class CostUI { return card; } + + static createCostValuesForm({ costItemId, costValues, callbacks }) { + // Check if the form already exists and remove it if it does + const formId = "cost-values-form-" + costItemId; + let existingForm = document.getElementById(formId); + if (existingForm) { + existingForm.remove(); + } + + const form = CostUI.createFormElement(formId, "cost-values-form"); + + const header = CostUI.createHeader("Cost Values Form"); + form.appendChild(header); + + const table = CostUI.createCostValuesTable(costItemId, ["Type", "Category", "Value"]); + + costValues.forEach(costValue => { + const tr = CostUI.createCostvaluesRow(costItemId, costValue); + table.appendChild(tr); + }); + + form.appendChild(table); + + const addButton = CostUI.createAddCostValueButton(costItemId, callbacks); + form.appendChild(addButton); + + const submitButton = CostUI.createSubmitButton(callbacks); + form.appendChild(submitButton); + + const closeButton = CostUI.createCloseButton(form); + form.appendChild(closeButton); + + CostUI.makeHeaderDraggable(header, form); + + document.body.appendChild(form); + + return form; + } + + + static getCostValuesTable(costItemId) { + return document.getElementById("cost-values-table-" + costItemId); + } + + static addNewCostValueRow(costItemId, costValueId) { + const table = CostUI.getCostValuesTable(costItemId); + // check if table exists + if (!table) { + console.log("Cost values table not found for cost item ID:", costItemId); + return; + } + console.log("Adding new cost value row", costValueId); + console.log(table) + const tr = CostUI.createCostvaluesRow(costItemId, {"category": "", "name": "", "applied_value": 0, "id": costValueId, "parent": costItemId}); + table.appendChild(tr); + } + + static createCostValuesTable(costItemId, headers) { + const table = document.createElement("table"); + table.classList.add("cost-values-table"); + table.id = "cost-values-table-" + costItemId; + + const headerRow = document.createElement("tr"); + headers.forEach(headerText => { + const th = document.createElement("th"); + th.textContent = headerText; + headerRow.appendChild(th); + }); + table.appendChild(headerRow); + + return table; + } + + static createCostvaluesRow(costItemId,costValue) { + + function cleanLabel(value) { + // remove anything which is not a dot or a digit + return value.replace(/[^0-9.]/g, ''); + + } + + + if (document.getElementById(costValue.id)) { + return document.getElementById(costValue.id); + } + + const tr = document.createElement("tr"); + tr.isCostValue = true; + tr.parent = costItemId; + tr.id = costValue.id; + + let costType = "FIXED"; + if (costValue.category === "*") { + costType = "SUM"; + } + else if (costValue.category && costValue.category !== "*") { + costType = "CATEGORY"; + } + else if (costValue.applied_value) { + costType = "FIXED"; + } + + const typeCell = CostUI.createTableDropdown("type", costType); + const dropdown = typeCell.querySelector("select"); + dropdown.addEventListener("change", function() { + const selectedType = this.value; + CostUI.updateRowBasedOnType(selectedType, categoryCell, valueCell1); + }); + tr.appendChild(typeCell); + + const categoryCell = CostUI.createTableInput("text", "category", costValue.category); + tr.appendChild(categoryCell); + + let value + + if (costValue.category === "*"){ + value = cleanLabel(costValue.label) + } + else { + value = costValue.applied_value + } + const valueCell1 = CostUI.createTableInput("number", "value", value); + tr.appendChild(valueCell1); + // Apply initial state based on costType + CostUI.updateRowBasedOnType(costType, categoryCell, valueCell1); + + return tr; + } + + static updateRowBasedOnType(type, categoryCell, valueCell1) { + if (type === "FIXED") { + categoryCell.querySelector("input").disabled = true; + categoryCell.querySelector("input").style.backgroundColor = "lightgrey"; + valueCell1.querySelector("input").disabled = false; + valueCell1.querySelector("input").style.backgroundColor = ""; + } else if (type === "CATEGORY") { + categoryCell.querySelector("input").disabled = false; + categoryCell.querySelector("input").style.backgroundColor = ""; + valueCell1.querySelector("input").disabled = false; + valueCell1.querySelector("input").style.backgroundColor = ""; + } else if (type === "SUM") { + categoryCell.querySelector("input").disabled = true; + categoryCell.querySelector("input").style.backgroundColor = "lightgrey"; + valueCell1.querySelector("input").disabled = true; + valueCell1.querySelector("input").style.backgroundColor = "lightgrey"; + } + } + + static createTableDropdown(name, value="FIXED") { + const cell = document.createElement("td"); + const dropdown = document.createElement("select"); + dropdown.name = name; + + const options = ["FIXED", "CATEGORY", "SUM"]; + options.forEach(optionValue => { + const option = document.createElement("option"); + option.value = optionValue; + option.textContent = optionValue; + if (optionValue === value) { + option.selected = true; + } + dropdown.appendChild(option); + }); + + cell.appendChild(dropdown); + return cell; + } + + static createTableInput(type, name, value) { + const cell = document.createElement("td"); + const input = document.createElement("input"); + input.type = type; + input.name = name; + input.value = value; + cell.appendChild(input); + return cell; + } + + static createAddCostValueButton(costItemId, callbacks) { + const addButton = document.createElement("button"); + addButton.textContent = "+"; + addButton.classList.add("add-button"); + addButton.addEventListener("click", function(e) { + e.preventDefault(); + callbacks.addCostValue ? callbacks.addCostValue(costItemId) : null; + }); + return addButton; + } + + static createSubmitButton(callbacks) { + const submitButton = document.createElement("button"); + submitButton.type = "submit"; + submitButton.textContent = "Save"; + + submitButton.addEventListener("click", function(e) { + e.preventDefault(); + const form = this.closest("form"); + const costValues = []; + const rows = Array.from(form.querySelectorAll("tr")).filter(row => row.isCostValue); + + rows.forEach(row => { + const costCategory = row.querySelector("input[name='category']").value; + const appliedValue = parseFloat(row.querySelector("input[name='value']").value); + const costType = row.querySelector("select[name='type']").value; + const id = parseInt(row.id); + costValues.push({ costType, costCategory, appliedValue, id: id, costItemId: row.parent }); + }); + const costItemId = parseInt(form.id.split("-")[3]); + console.log(form.id) + console.log("Cost values to be saved:", costItemId); + callbacks.editCostValues ? callbacks.editCostValues(costItemId, costValues) : null; + form.remove(); + }); + return submitButton; + } + + static createFormElement(id, className) { + const form = document.createElement("form"); + form.id = id; + form.classList.add(className); + form.style.position = "absolute"; // Set initial position to absolute + form.style.top = "50%"; + form.style.left = "50%"; + form.style.transform = "translate(-50%, -50%)"; + return form; + } + + static createHeader(text) { + const header = document.createElement("div"); + header.classList.add("form-header"); + header.textContent = text; + return header; + } + + static createCloseButton(form) { + const closeButton = document.createElement("span"); + closeButton.classList.add("close-button"); + closeButton.innerHTML = "×"; + closeButton.addEventListener("click", function() { + form.remove(); + }); + return closeButton; + } + + static makeHeaderDraggable(header, form) { + header.addEventListener("mousedown", function(e) { + let offsetX = e.clientX - form.getBoundingClientRect().left; + let offsetY = e.clientY - form.getBoundingClientRect().top; + + function mouseMoveHandler(e) { + form.style.left = `${e.clientX - offsetX}px`; + form.style.top = `${e.clientY - offsetY}px`; + } + + function mouseUpHandler() { + document.removeEventListener("mousemove", mouseMoveHandler); + document.removeEventListener("mouseup", mouseUpHandler); + } + + document.addEventListener("mousemove", mouseMoveHandler); + document.addEventListener("mouseup", mouseUpHandler); + }); + } } diff --git a/src/bonsai/bonsai/bim/data/webui/templates/costing.html b/src/bonsai/bonsai/bim/data/webui/templates/costing.html index 6bd6ddeee7..c0297bfac4 100644 --- a/src/bonsai/bonsai/bim/data/webui/templates/costing.html +++ b/src/bonsai/bonsai/bim/data/webui/templates/costing.html @@ -4,8 +4,7 @@ BlenderBIM Web UI - - + ifcopenshell.entity_instance ifc.run("unit.edit_monetary_unit", unit=unit, attributes=attributes) ifc.run("unit.assign_unit", units=[unit]) return unit + + +def generate_cost_schedule_browser(cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance) -> bpy.types.Panel: + cost_schedule_data = cost.create_cost_schedule_json(cost_schedule) + return cost.generate_cost_schedule_browser(cost_schedule_data) \ No newline at end of file diff --git a/src/bonsai/bonsai/tool/cost.py b/src/bonsai/bonsai/tool/cost.py index 5aeb827ae9..5fa98c32e7 100644 --- a/src/bonsai/bonsai/tool/cost.py +++ b/src/bonsai/bonsai/tool/cost.py @@ -811,9 +811,8 @@ class Cost(bonsai.core.tool.Cost): @classmethod def create_cost_schedule_json(cls, cost_schedule: ifcopenshell.entity_instance) -> dict: from bonsai.bim.module.cost.data import CostSchedulesData - if not CostSchedulesData.is_loaded: - CostSchedulesData.load() - cost_items = CostSchedulesData.data["cost_items"] + CostSchedulesData.load() + cost_items = CostSchedulesData.data["cost_items"] data = [] for rel in cost_schedule.Controls or []: for cost_item in rel.RelatedObjects or []: @@ -852,3 +851,9 @@ class Cost(bonsai.core.tool.Cost): unit = tool.Unit.get_project_currency_unit() if unit: return {"id": unit.id(), "name": unit.Currency} + + @classmethod + def generate_cost_schedule_browser(cls, cost_schedule_data: list[dict[str, Any]]) -> None: + if not bpy.context.scene.WebProperties.is_connected: + bpy.ops.bim.connect_websocket_server(page="costing") + tool.Web.send_webui_data(data=cost_schedule_data, data_key="cost_items", event="cost_items") \ No newline at end of file diff --git a/src/bonsai/bonsai/tool/web.py b/src/bonsai/bonsai/tool/web.py index 816cae029d..bfc1e8c6dc 100644 --- a/src/bonsai/bonsai/tool/web.py +++ b/src/bonsai/bonsai/tool/web.py @@ -21,6 +21,7 @@ from bonsai.bim.module.web.data import WebData import bonsai.core.tool import bonsai.tool as tool import ifcopenshell.api.sequence +import ifcopenshell.api.cost from typing import Any, Dict, Optional import time import socket @@ -354,17 +355,15 @@ class Web(bonsai.core.tool.Web): if operator_data["type"] == "loadCostSchedule": cost_schedule = ifc_file.by_id(operator_data["costScheduleId"]) bonsai.core.cost.enable_editing_cost_items(tool.Cost, cost_schedule=cost_schedule) - json_data = tool.Cost.create_cost_schedule_json(cost_schedule) - cls.send_webui_data(data=json_data, data_key="cost_items", event="cost_items") - if operator_data["type"] == "addCostItem": - bpy.ops.bim.add_cost_item(cost_item=operator_data["costItemId"]) - cost_schedule = tool.Cost.get_cost_schedule(cost_item=tool.Ifc.get().by_id(operator_data["costItemId"])) - json_data = tool.Cost.create_cost_schedule_json(cost_schedule) - cls.send_webui_data(data=json_data, data_key="cost_items", event="cost_items") + cls.load_cost_schedule_web_ui(cost_schedule) if operator_data["type"] == "selectAssignedElements": cost_item = tool.Ifc.get().by_id(operator_data["costItemId"]) products = tool.Cost.get_cost_item_products(cost_item, is_deep=True) tool.Spatial.select_products(products, unhide=True) + if operator_data["type"] == "addCostItem": + bpy.ops.bim.add_cost_item(cost_item=operator_data["costItemId"]) + cost_schedule = tool.Cost.get_cost_schedule(cost_item=tool.Ifc.get().by_id(operator_data["costItemId"])) + cls.load_cost_schedule_web_ui(cost_schedule) if operator_data["type"] == "editCostItemName": cost_item = tool.Ifc.get().by_id(operator_data["costItemId"]) tool.Ifc.run( @@ -373,6 +372,65 @@ class Web(bonsai.core.tool.Web): attributes = {"Name": operator_data["name"]} ) tool.Cost.load_cost_schedule_tree() + if operator_data["type"] == "enableEditingCostValues": + cost_item = tool.Ifc.get().by_id(operator_data["costItemId"]) + cost_values = ifcopenshell.util.cost.get_cost_values(cost_item) + cls.send_webui_data(data={ + "cost_values": cost_values, + "cost_item_id": operator_data["costItemId"] + }, data_key="cost_values", event="cost_values") + if operator_data["type"] == "addCostValue": + value = ifcopenshell.api.cost.add_cost_value( + ifc_file, + parent=ifc_file.by_id(operator_data["costItemId"]), + ) + cls.send_webui_data( + data={ + "cost_value_id" : value.id(), + "cost_item_id":operator_data["costItemId"]}, + data_key="cost_value", + event="cost_value" + ) + if operator_data["type"] == "editCostValues": + cost_item_id = operator_data["costItemId"] + print('Editing cost values', cost_item_id) + print(cost_item_id) + print(type(operator_data["costValues"])) + for value_data in operator_data["costValues"] or []: + print(value_data) + value = ifc_file.by_id(value_data["id"]) + print(value.get_info()) + if value_data["costType"] == "FIXED": + attributes= { + "AppliedValue": value_data["appliedValue"], + } + elif value_data["costType"] == "CATEGORY": + attributes= { + "AppliedValue": value_data["appliedValue"], + "Category": value_data["costCategory"], + } + elif value_data["costType"] == "SUM": + attributes= { + "Category": '*' + } + ifcopenshell.api.cost.edit_cost_value( + file=ifc_file, + cost_value=value, + attributes= attributes + ) + tool.Cost.load_cost_schedule_tree() + cost_item = ifc_file.by_id(operator_data["costItemId"]) + if not cost_item: + print("Cost item not found") + return + cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item) + cls.load_cost_schedule_web_ui(cost_schedule) + + + @classmethod + def load_cost_schedule_web_ui(cls, cost_schedule): + json_data = tool.Cost.create_cost_schedule_json(cost_schedule) + cls.send_webui_data(data=json_data, data_key="cost_items", event="cost_items") @classmethod def handle_gantt_operator(cls, operator_data: dict) -> None: diff --git a/src/ifcopenshell-python/ifcopenshell/util/cost.py b/src/ifcopenshell-python/ifcopenshell/util/cost.py index d9ce6f874f..bb3df756bb 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/cost.py +++ b/src/ifcopenshell-python/ifcopenshell/util/cost.py @@ -245,6 +245,20 @@ def get_cost_item_assignments( for product in get_cost_assignments_by_type(nested_cost_item, filter_by_type) ] +def get_cost_values(cost_item: ifcopenshell.entity_instance) -> list[dict[str, str]]: + results = [] + for cost_value in cost_item.CostValues or []: + label = "{0:.2f}".format(calculate_applied_value(cost_item, cost_value)) + label += " = {}".format(serialise_cost_value(cost_value)) + results.append({ + "id": cost_value.id(), + "label": label, + "name": cost_value.Name, + "category": cost_value.Category, + "applied_value": get_primitive_applied_value(cost_value.AppliedValue) if cost_value.AppliedValue else None, + }) + print(results) + return results class CostValueUnserialiser: def parse(self, formula: str):