From 1964def3cae50763d4a32d4ab3ebd856440bece3 Mon Sep 17 00:00:00 2001 From: myoualid Date: Wed, 11 Sep 2024 19:00:15 +0100 Subject: [PATCH] web ui cost module features: - improve UI for editing cost quantites - add, edit, delete manual quantities --- .../bonsai/bim/data/webui/static/css/cost.css | 129 +++-- .../bonsai/bim/data/webui/static/js/cost.js | 34 +- .../data/webui/static/js/utilities/costui.js | 457 ++++++++++++++++-- src/bonsai/bonsai/tool/cost.py | 37 +- src/bonsai/bonsai/tool/web.py | 170 ++++--- 5 files changed, 683 insertions(+), 144 deletions(-) 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 c0ad9a8e16..eac13d1945 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/cost.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/cost.css @@ -20,6 +20,22 @@ body { box-sizing: border-box; } +.floating-form { + width: 50vw; + max-height: 80vh; + position: absolute; + background-color: var(--background-color); + border: 1px solid black; + z-index: 9999; + cursor: move; + user-select: none; + padding-bottom: var(--padding-medium); +} + +[id^="enable-editing-quantities"].floating-form { + height: 70vh; +} + .form-header { display: flex; justify-content: flex-start; @@ -30,36 +46,44 @@ body { font-size: large; border-bottom: 1px solid #ddd; border-radius: 6px 6px 0 0; + height: 5%; } -.floating-form span { - padding: var(--padding-small); - margin-left: var(--margin-small); +.form-container { + height: 95%; + padding-left: var(--padding-medium); + padding-right: var(--padding-medium); } -.floating-form { - width: 50vw; - max-height: 80vh; - position: absolute; - background-color: var(--background-color); - border: 1px solid black; - z-index: 9999; - cursor: move; - user-select: none; - resize: both; +.form-section { + margin-bottom: 1em; + height: 65%; + overflow-y: auto; + overflow-x: hidden; } +.form-section h3 { + margin-bottom: 0.5em; +} + +.summary-section { + margin-top: 1em; +} + +.summary-table { + width: 100%; + border-collapse: collapse; + font-size: 14px; + text-align: left; +} + + .floating-form table tr { margin-bottom: 10px; } -.form-container { - padding: var(--padding-medium); - overflow-y: auto; - overflow-x: hidden; -} - .action-button { + position:relative; background-color: var(--button-background); border: none; color: #fff; @@ -72,6 +96,32 @@ body { transition: background-color 0.3s ease; width: fit-content; margin: 0.5em; + z-index: 11000; +} + +.action-button::after { + content: attr(data-tooltip); + position: absolute; + bottom: -30px; + background-color: #333; + color: #fff; + padding: 5px 10px; + border-radius: 5px; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s; + font-size: 12px; +} + +.action-button:hover::after { + opacity: 1; +} + +.active-btn { + background-color: #887821; + /* make a nic shadow */ + box-shadow: 0 0 10px #887821; } .action-button:hover { @@ -141,7 +191,7 @@ body { } .floating-form .table-container { - max-height: 70%; + max-height: 80%; } #cost-items { @@ -149,7 +199,6 @@ body { } [id^="cost-values-form"] table { - /* fixed */ max-height: 50%; table-layout: fixed; } @@ -186,6 +235,7 @@ td, th { border-right: 1px solid #ddd; text-align: center; + height: 100%; } th { @@ -221,15 +271,7 @@ select { 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); - margin-left: 0.5em; - margin-right: 0.5em; - font-size: var(--fontSize); - min-width: 50px; } input:focus { @@ -239,6 +281,31 @@ input:focus { color: #ddd; } +#cost-items input, #cost-items +select { + border-radius: 6px; + padding: 6px 8px; + font-size: 14px; + line-height: 20px; + margin-left: 0.5em; + margin-right: 0.5em; + font-size: var(--fontSize); + min-width: 50px; +} + +form input { + border-radius: 6px; + padding: 3px 4px; + font-size: 12px; + line-height: 10px; + margin-left: 0.2em; + margin-right: 0.2em; + font-size: inherit; + height: inherit; + width: inherit; + box-sizing: border-box; +} + #cost-items tr:hover, .highlighted { background-color: #28a74657; @@ -332,7 +399,7 @@ form table { } .clickable-cell:hover { - border: 1px solid #28a746; + border: 1px solid #94a728; border-radius: 6px; cursor: pointer; -} +} \ No newline at end of file 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 74943c9426..be078a8d3c 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/js/cost.js +++ b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js @@ -36,19 +36,51 @@ function handleEditQuantities(data) { const products = data.data["quantities"]["selected_products"]; const assigned_products = data.data["quantities"]["assigned_products"]; const quantityNames = data.data["quantities"]["product_quantity_names"]; + const costQuantities = data.data["quantities"]["cost_quantities"]; - CostUI.editQuantities({ + CostUI.enableEditingQuantities({ costItemId: costItemId, selectedProducts: products, assignedProducts: assigned_products, quantityNames: quantityNames, + costQuantities: costQuantities, callbacks: { addProductAssignments: addProductAssignments, enableEditingQuantities: enableEditingQuantities, + addQuantity: addQuantity, + editQuantity: editQuantity, + deleteQuantity: deleteQuantity, }, }); } +function addQuantity(costItemId, ifcClass) { + executeOperator({ + type: "AddCostItemQuantity", + costItemId: costItemId, + ifcClass: ifcClass, + }); + + //CostUI.addQuantity(costItemId, quantityName); +} + +function editQuantity(costItemId, quantityId, attributes) { + executeOperator({ + type: "editCostItemQuantity", + costItemId: costItemId, + quantityId: quantityId, + attributes: attributes, + }); +} + +function deleteQuantity(costItemId, quantityId) { + executeOperator({ + type: "deleteCostItemQuantity", + costItemId: costItemId, + quantityId: quantityId, + }); +} + function handlePredefinedTypes(data) { CostUI.enableAddingCostSchedule( data.data["predefined_types"], 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 0a94fb4d50..7a99b57c8a 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 @@ -793,11 +793,15 @@ export class CostUI { } static getRowNameCell(costItemId) { - return document.getElementById(costItemId).querySelector("td input"); + return document.getElementById(costItemId) + ? document.getElementById(costItemId).querySelector("td input") + : null; } static getCostItemName(costItemId) { - return CostUI.getRowNameCell(costItemId).value; + return CostUI.getRowNameCell(costItemId) + ? CostUI.getRowNameCell(costItemId).value + : "Unnamed"; } static createCostValuesForm({ costItemId, costValues, callbacks }) { @@ -840,7 +844,6 @@ export class CostUI { static addNewCostValueRow(costItemId, costValueId, costValueCallbacks) { const table = CostUI.getCostValuesTable(costItemId); if (!table) { - console.log("Cost values table not found for cost item ID:", costItemId); return; } const tr = CostUI.createCostvaluesRow( @@ -908,8 +911,8 @@ export class CostUI { } else if (costValue.applied_value) { costType = "FIXED"; } - - const typeCell = CostUI.createTableDropdown("type", costType); + const options = ["FIXED", "CATEGORY", "SUM"]; + const typeCell = CostUI.createTableDropdown("type", options, costType); const dropdown = typeCell.querySelector("select"); dropdown.addEventListener("change", function () { const selectedType = this.value; @@ -1012,12 +1015,11 @@ export class CostUI { } } - static createTableDropdown(name, value = "FIXED") { + static createTableDropdown(name, options, 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; @@ -1306,7 +1308,7 @@ export class CostUI { } static createProductTable({ - form, + container, products, quantityNames, costItemId, @@ -1316,7 +1318,7 @@ export class CostUI { const noProductsMessage = document.createElement("p"); noProductsMessage.textContent = "Your Blender Selection is empty! Select objects first."; - form.appendChild(noProductsMessage); + container.appendChild(noProductsMessage); return; } const { tableContainer, table } = CostUI.addTable({ @@ -1324,7 +1326,7 @@ export class CostUI { className: "", id: "cost-values-table-" + costItemId, }); - form.appendChild(tableContainer); + container.appendChild(tableContainer); const tbody = document.createElement("tbody"); table.appendChild(tbody); @@ -1432,7 +1434,7 @@ export class CostUI { const productIds = products.map((product) => product.info.id); callbacks.emptyForm = () => { - form.innerHTML = ""; + container.innerHTML = ""; }; const addProductAssignmentsButton = CostUI.addProductAssignmentsButton( costItemId, @@ -1441,13 +1443,13 @@ export class CostUI { callbacks ); - form.appendChild(quantitySelect); - form.appendChild(addProductAssignmentsButton); + container.appendChild(quantitySelect); + container.appendChild(addProductAssignmentsButton); return table; } static createAssignmentsTable({ - form, + container, products, costItemId, quantityNames, @@ -1459,7 +1461,7 @@ export class CostUI { id: "cost-values-table-" + costItemId, }); - form.appendChild(tableContainer); + container.appendChild(tableContainer); const tbody = document.createElement("tbody"); table.appendChild(tbody); @@ -1512,8 +1514,10 @@ export class CostUI { quantitySelect, callbacks ) { - const addProductAssignmentsButton = document.createElement("button"); - addProductAssignmentsButton.textContent = "Add Product Assignments"; + const addProductAssignmentsButton = CostUI.createButton( + "Add Product Assignments", + "fa-solid fa-plus" + ); addProductAssignmentsButton.addEventListener("click", (event) => { event.preventDefault(); let propName = quantitySelect.value; @@ -1528,7 +1532,7 @@ export class CostUI { }) : null; callbacks.emptyForm ? callbacks.emptyForm() : null; - callbacks.getSelectedProducts(costItemId); + callbacks.enableEditingQuantities(costItemId); }); addProductAssignmentsButton.classList.add("action-button"); return addProductAssignmentsButton; @@ -1730,49 +1734,424 @@ export class CostUI { }); } - static editQuantities({ + static enableEditingQuantities({ costItemId, selectedProducts, - quantityNames, assignedProducts, + quantityNames, + costQuantities, callbacks, }) { - const formId = "selected-products-" + costItemId; + CostUI.highlightElement(costItemId); const form = CostUI.Form({ - id: formId, - name: - "Edit Product Assignments for: " + CostUI.getCostItemName(costItemId), + id: "enable-editing-quantities-" + costItemId, + name: "Editing Quantities for: " + CostUI.getCostItemName(costItemId), icon: "fa-solid fa-box", }); - const numberOfProducts = CostUI.Text( - "Selection basket : " + selectedProducts.length + " products", - "fa-solid fa-cart-shopping", - "large" - ); - form.appendChild(numberOfProducts); - CostUI.highlightElement(costItemId); - const selectedProductsTable = CostUI.createProductTable({ - form, + + const ribbonBar = CostUI.createRibbonBar(); + form.appendChild(ribbonBar); + + const selectedProductsSection = CostUI.selectedProductsSection({ products: selectedProducts, quantityNames, costItemId, callbacks, }); - if (assignedProducts.length > 0) { + const assignedProductsSection = CostUI.assignedProductsSection({ + products: assignedProducts, + quantityNames, + costItemId, + callbacks, + }); + + const manualQuantitiesSection = CostUI.manualQuantitiesSection({ + costItemId, + costQuantities, + has_assigned_products: assignedProducts.length > 0, + callbacks, + }); + + form.appendChild(selectedProductsSection); + form.appendChild(assignedProductsSection); + form.appendChild(manualQuantitiesSection); + + const summarySection = CostUI.createSummarySection({ + selectedProducts, + assignedProducts, + costQuantities, + }); + form.appendChild(summarySection); + + CostUI.addEventListeners({ + switchBar: ribbonBar, + costItemId, + selectedProductsSection, + assignedProductsSection, + manualQuantitiesSection, + }); + + const lastActiveSection = + localStorage.getItem("lastActiveSection") || "selected-products"; + const lastActiveButton = document.getElementById( + `${lastActiveSection}-btn` + ); + if (lastActiveButton) { + lastActiveButton.click(); + } + } + + static createRibbonBar() { + const ribbonBar = document.createElement("div"); + ribbonBar.className = "switch-bar"; + ribbonBar.innerHTML = ` + + + + `; + return ribbonBar; + } + + static createSummarySection({ + selectedProducts, + assignedProducts, + costQuantities, + }) { + const summarySection = document.createElement("div"); + summarySection.classList.add("summary-section"); + + const manualQuantities = costQuantities.quantities.filter( + (quantity) => !quantity.fromProduct + ); + const paramaterQuantities = costQuantities.quantities.filter( + (quantity) => quantity.fromProduct + ); + + const table = document.createElement("table"); + table.classList.add("summary-table"); + + const thead = document.createElement("thead"); + const headerRow = document.createElement("tr"); + const headers = ["Category", "Count"]; + headers.forEach((headerText) => { + const th = document.createElement("th"); + th.textContent = headerText; + headerRow.appendChild(th); + }); + thead.appendChild(headerRow); + table.appendChild(thead); + const tbody = document.createElement("tbody"); + + const data = [ + { category: "Assigned Products", count: assignedProducts.length }, + { category: "Manual Quantities", count: manualQuantities.length }, + { + category: "Product derived Quantities", + count: paramaterQuantities.length, + }, + ]; + + data.forEach((item) => { + const row = document.createElement("tr"); + const categoryCell = document.createElement("td"); + categoryCell.textContent = item.category; + const countCell = document.createElement("td"); + countCell.textContent = item.count; + row.appendChild(categoryCell); + row.appendChild(countCell); + tbody.appendChild(row); + }); + + table.appendChild(tbody); + summarySection.appendChild(table); + + return summarySection; + } + + static addEventListeners({ + switchBar, + costItemId, + selectedProductsSection, + assignedProductsSection, + manualQuantitiesSection, + }) { + const buttons = switchBar.querySelectorAll(".action-button"); + + buttons.forEach((button) => { + button.addEventListener("click", (e) => { + e.preventDefault(); + const sectionName = button.id.replace("-btn", ""); + const sectionId = sectionName + "-section-" + costItemId; + + if (selectedProductsSection) + selectedProductsSection.style.display = "none"; + if (assignedProductsSection) + assignedProductsSection.style.display = "none"; + if (manualQuantitiesSection) + manualQuantitiesSection.style.display = "none"; + + const section = document.getElementById(sectionId); + if (section) { + section.style.display = "block"; + } + buttons.forEach((btn) => btn.classList.remove("active-btn")); + button.classList.add("active-btn"); + + localStorage.setItem("lastActiveSection", sectionName); + }); + }); + } + + static getSection(costItemId, sectionName) { + return document.getElementById(`${sectionName}-${costItemId}`); + } + + static selectedProductsSection({ + products, + quantityNames, + costItemId, + callbacks, + }) { + const selectedProductsSection = document.createElement("div"); + selectedProductsSection.id = "selected-products-section-" + costItemId; + selectedProductsSection.classList.add("form-section"); + const numberOfProducts = CostUI.Text( + "Selection basket : " + products.length + " products", + "fa-solid fa-cart-shopping", + "medium" + ); + selectedProductsSection.appendChild(numberOfProducts); + + const selectedProductsTable = CostUI.createProductTable({ + container: selectedProductsSection, + products: products, + quantityNames, + costItemId, + callbacks, + }); + return selectedProductsSection; + } + + static assignedProductsSection({ + products, + quantityNames, + costItemId, + callbacks, + }) { + const assignedProductsSection = document.createElement("div"); + assignedProductsSection.id = "assigned-products-section-" + costItemId; + assignedProductsSection.style.display = "none"; + assignedProductsSection.classList.add("form-section"); + if (products.length > 0) { + let text = " Assigned Products : " + products.length; const assignedProductsText = CostUI.Text( - "Assigned Products", + text, "fa-solid fa-solid fa-paperclip", - "large" + "medium" ); - form.appendChild(assignedProductsText); + assignedProductsSection.appendChild(assignedProductsText); const assignmentsTable = CostUI.createAssignmentsTable({ - form, - products: assignedProducts, + container: assignedProductsSection, + products: products, quantityNames, costItemId, callbacks, }); } + + return assignedProductsSection; + } + + static manualQuantitiesSection({ + costItemId, + costQuantities, + has_assigned_products, + callbacks, + }) { + const manualQuantitiesSection = document.createElement("div"); + manualQuantitiesSection.classList.add("form-section"); + manualQuantitiesSection.id = "manual-quantities-section-" + costItemId; + manualQuantitiesSection.style.display = "none"; + + const title = CostUI.Text( + "Manual Quantities", + "fa-solid fa-ruler", + "medium" + ); + manualQuantitiesSection.appendChild(title); + const unitSymbol = costQuantities.unit_symbol; + + let quantityType = costQuantities.quantity_type; + + if (quantityType) { + const text = CostUI.Text( + quantityType + " (" + unitSymbol + " )", + "fa-solid fa-ruler", + "small" + ); + manualQuantitiesSection.appendChild(text); + } else { + // add dropdown to chose quantity type , from "IfcQuantityArea", "IfcQuantityLength", "IfcQuantityVolume", "IfcQuantityCount", "IfcQuantityWeight " + const quantityTypes = [ + "IfcQuantityArea", + "IfcQuantityLength", + "IfcQuantityVolume", + "IfcQuantityCount", + "IfcQuantityWeight", + ]; + const dropdown = CostUI.createTableDropdown( + "type", + quantityTypes, + "IfcQuantityArea" + ); + dropdown.id = "quantity-type-dropdown-" + costItemId; + manualQuantitiesSection.appendChild(dropdown); + } + + // if quantity is ty IfcQuantityCount, and the costQuantities + if (quantityType === "IfcQuantityCount" && has_assigned_products) { + // write text that one of the quantities is assigned to the product selection + const text = CostUI.Text( + " One of the quantities is assigned to the product selection", + "fa-solid fa-warning", + "small" + ); + manualQuantitiesSection.appendChild(text); + } + + let paramaterQuantities = costQuantities.quantities.filter( + (quantity) => quantity.fromProduct + ); + let manualQuantities = costQuantities.quantities.filter( + (quantity) => !quantity.fromProduct + ); + + let headerNames = ["Name", "Value" + " (" + unitSymbol + " )", "Actions"]; + + if (manualQuantities.length > 0) { + headerNames = []; + Object.entries(manualQuantities[0]).forEach(([key, value]) => { + if (key !== "id" && key !== "fromProduct") { + headerNames.push(key); + } + }); + headerNames.push("Actions"); + } + + const { tableContainer, table } = CostUI.addTable({ + headers: headerNames, + className: "manual-quantities-table", + id: "manual-quantities-table-" + costItemId, + }); + + manualQuantities.forEach((quantity) => { + const tr = CostUI.createManualQuantityRow( + costItemId, + quantity, + callbacks + ); + table.appendChild(tr); + }); + const addButton = CostUI.createAddManualQuantityButton( + costItemId, + quantityType, + callbacks + ); + + manualQuantitiesSection.appendChild(tableContainer); + // manualQuantitiesSection.appendChild(tableContainer2); + manualQuantitiesSection.appendChild(addButton); + return manualQuantitiesSection; + } + + static createManualQuantityRow(costItemId, quantity, callbacks) { + const tr = document.createElement("tr"); + tr.id = quantity.id; + + // get quantity keys and values to create the table row cells + // Label cell + + Object.entries(quantity).forEach(([key, value]) => { + if (key !== "id" && key !== "fromProduct") { + if (key === "Name") { + // create input + const cell = document.createElement("td"); + const input = document.createElement("input"); + input.type = "text"; + input.value = value; + cell.appendChild(input); + tr.appendChild(cell); + + input.addEventListener("keydown", function (e) { + if (e.key === "Enter") { + e.preventDefault(); + callbacks.editQuantity(costItemId, quantity.id, { + [key]: input.value, + }); + } + }); + } + + // if key contains value + else if (key.toLowerCase().includes("value")) { + const cell = document.createElement("td"); + const input = document.createElement("input"); + input.type = "number"; + input.value = value; + cell.appendChild(input); + + input.addEventListener("keydown", function (e) { + if (e.key === "Enter") { + e.preventDefault(); + callbacks.editQuantity(costItemId, quantity.id, { + [key]: parseFloat(input.value), // Dynamically set the key and parse the value as a double + }); + } + }); + tr.appendChild(cell); + } else { + const cell = document.createElement("td"); + cell.textContent = value; + tr.appendChild(cell); + } + } + }); + + // Add delete button + const deleteButton = CostUI.createButton("Delete", "fa-solid fa-trash"); + const deleteCell = document.createElement("td"); + deleteButton.addEventListener("click", function (e) { + e.preventDefault(); + callbacks.deleteQuantity(costItemId, quantity.id); + tr.remove(); + }); + deleteCell.appendChild(deleteButton); + tr.appendChild(deleteCell); + + return tr; + } + + static createAddManualQuantityButton(costItemId, quantityType, callbacks) { + const addButton = CostUI.createButton( + "Add Manual Quantity", + "fa-solid fa-plus" + ); + + addButton.addEventListener("click", function (e) { + e.preventDefault(); + let type = quantityType; + if (!type) { + // get quantityType from dropdown + const qtoSection = document.getElementById( + "manual-quantities-section-" + costItemId + ); + const dropdown = qtoSection.querySelector("select"); + type = dropdown ? dropdown.value : null; + } + callbacks.addQuantity(costItemId, type); + }); + + return addButton; } } diff --git a/src/bonsai/bonsai/tool/cost.py b/src/bonsai/bonsai/tool/cost.py index 8d9945a4ad..4c68d31d12 100644 --- a/src/bonsai/bonsai/tool/cost.py +++ b/src/bonsai/bonsai/tool/cost.py @@ -295,6 +295,15 @@ class Cost(bonsai.core.tool.Cost): unit = cls.get_quantity_unit_symbol(quantity) return selected_quantitites, unit + + @classmethod + def get_assigned_product(cls, cost_item, quantity): + assigned_products = cls.get_cost_item_assignments(cost_item, filter_by_type="PRODUCT", is_deep=False) + for product in assigned_products: + assigned_quantities, _ = cls.get_assigned_quantities(cost_item, product) + if quantity in assigned_quantities: + return product + @classmethod def get_products(cls, related_object_type: RELATED_OBJECT_TYPE) -> list[ifcopenshell.entity_instance]: if related_object_type == "PRODUCT": @@ -859,7 +868,33 @@ class Cost(bonsai.core.tool.Cost): bpy.ops.bim.connect_websocket_server(page="costing") cost_schedule_data = cls.create_cost_schedule_json(cost_chedule) tool.Web.send_webui_data( - data={"cost_items": cost_schedule_data, "cost_schedule_id": cost_chedule.id()}, + data={ + "cost_items": cost_schedule_data, + "cost_schedule_id": cost_chedule.id(), + "currency": cls.currency() + }, data_key="cost_items", event="cost_items", ) + + @classmethod + def get_cost_quantities(cls, cost_item: ifcopenshell.entity_instance) -> dict: + results = { + "quantities": [], + "unit_symbol": None, + } + if not cost_item: + return results + results["quantity_type"] = cost_item.CostQuantities[0].is_a() if cost_item.CostQuantities else None + unit = ifcopenshell.util.unit.get_property_unit(cost_item.CostQuantities[0], tool.Ifc.get()) if cost_item.CostQuantities else None + if unit: + results["unit_symbol"] = ifcopenshell.util.unit.get_unit_symbol(unit) + for quantity in cost_item.CostQuantities or []: + assigned_product = cls.get_assigned_product(cost_item, quantity) + info = quantity.get_info() + info["fromProduct"] = assigned_product.get_info(recursive=True) if assigned_product else None + results["quantities"].append(info) + if results["quantity_type"] == "IfcQuantityCount": + results["unit_symbol"] = "U" + return results + diff --git a/src/bonsai/bonsai/tool/web.py b/src/bonsai/bonsai/tool/web.py index 858c2d2c5f..c8d3f1fb82 100644 --- a/src/bonsai/bonsai/tool/web.py +++ b/src/bonsai/bonsai/tool/web.py @@ -344,52 +344,6 @@ class Web(bonsai.core.tool.Web): operator_data (dict): A dictionary containing the operator data. """ - def selection_data(cost_item, elements): - print(cost_item, elements) - if not elements: - return [] - if not cost_item: - return [ - { - "info": { - "id": element.id(), - "name": element.Name, - "class": element.is_a(), - "type": get_type(element).Name if get_type(element) else None, - }, - "qtos": get_psets(element, qtos_only=True), - } - for element in elements or [] - ] - data = [] - for element in elements or []: - psets = get_psets(element, qtos_only=True) - element_type = get_type(element) - quantities, unit = tool.Cost.get_assigned_quantities(cost_item, element) - data.append( - { - "info": { - "id": element.id(), - "name": element.Name, - "class": element.is_a(), - "type": element_type.Name if element_type else None, - }, - "qtos": psets, - "assigned_quantities": [ - { - "cost_item_id": cost_item.id(), - "product_id": element.id(), - "name": q.Name, - "value": q[3], - "type": q.Unit, - } - for q in quantities or [] - ], - "unit": unit, - } - ) - return data - ifc_file = tool.Ifc.get() if operator_data["type"] == "getPredefinedTypes": print("getting predefined types") @@ -425,31 +379,6 @@ class Web(bonsai.core.tool.Web): 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"] == "enableEditingQuantities": - cost_item_id = operator_data["costItemId"] - cost_item = ifc_file.by_id(cost_item_id) - if not cost_item: - print("---> cost item not found") - return - - selected_products = list(tool.Spatial.get_selected_products()) - selected_products_data = selection_data(cost_item, selected_products) - - assigned_products = tool.Cost.get_cost_item_products(cost_item, is_deep=False) - assigned_products_data = selection_data(cost_item, assigned_products) - - names = ifcopenshell.util.cost.get_product_quantity_names(selected_products) - cls.send_webui_data( - data={ - "selected_products": selected_products_data, - "assigned_products": assigned_products_data, - "product_quantity_names": names, - "cost_item_id": cost_item_id, - }, - data_key="quantities", - event="quantities", - ) - if operator_data["type"] == "addSummaryCostItem": cost_schedule = ifc_file.by_id(operator_data["costScheduleId"]) bonsai.core.cost.add_summary_cost_item(tool.Ifc, tool.Cost, cost_schedule=cost_schedule) @@ -520,12 +449,109 @@ class Web(bonsai.core.tool.Web): prop_name = operator_data["propName"] if prop_name == "count": prop_name = "" - print(prop_name) bpy.ops.bim.assign_cost_item_quantity( cost_item=operator_data["costItemId"], related_object_type="PRODUCT", prop_name=prop_name ) 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"] == "enableEditingQuantities": + cost_item_id = operator_data["costItemId"] + cost_item = ifc_file.by_id(cost_item_id) + cls.enableEditingCostItemQuantities(cost_item) + if operator_data["type"] == "AddCostItemQuantity": + cost_item_id = operator_data["costItemId"] + cost_item = ifc_file.by_id(cost_item_id) + cost_schedule = tool.Cost.get_cost_schedule(cost_item) + bpy.ops.bim.add_cost_item_quantity(cost_item=cost_item_id, ifc_class=operator_data["ifcClass"]) + cls.load_cost_schedule_web_ui(cost_schedule) + cls.enableEditingCostItemQuantities(cost_item) + if operator_data["type"] == "editCostItemQuantity": + cost_item_id = operator_data["costItemId"] + cost_item = ifc_file.by_id(cost_item_id) + cost_schedule = tool.Cost.get_cost_schedule(cost_item) + physical_quantity = tool.Ifc.get().by_id(operator_data["quantityId"]) + attributes = operator_data["attributes"] + tool.Ifc.run("cost.edit_cost_item_quantity", physical_quantity=physical_quantity, attributes=attributes) + tool.Cost.load_cost_item_quantities(ifc_file.by_id(operator_data["costItemId"])) + cls.load_cost_schedule_web_ui(cost_schedule) + cls.enableEditingCostItemQuantities(cost_item) + if operator_data["type"] == "deleteCostItemQuantity": + bpy.ops.bim.remove_cost_item_quantity(cost_item=operator_data["costItemId"], physical_quantity=operator_data["quantityId"]) + cost_item_id = operator_data["costItemId"] + cost_item = ifc_file.by_id(cost_item_id) + cost_schedule = tool.Cost.get_cost_schedule(cost_item) + cls.load_cost_schedule_web_ui(cost_schedule) + cls.enableEditingCostItemQuantities(cost_item) + + @classmethod + def enableEditingCostItemQuantities(cls, cost_item): + if not cost_item: + return + selected_products = list(tool.Spatial.get_selected_products()) + selected_products_data = cls.selection_data(cost_item, selected_products) + + assigned_products = tool.Cost.get_cost_item_products(cost_item, is_deep=False) + assigned_products_data = cls.selection_data(cost_item, assigned_products) + + names = ifcopenshell.util.cost.get_product_quantity_names(selected_products) + cls.send_webui_data( + data={ + "selected_products": selected_products_data, + "assigned_products": assigned_products_data, + "product_quantity_names": names, + "cost_item_id": cost_item.id(), + "cost_quantities": tool.Cost.get_cost_quantities(cost_item), + }, + data_key="quantities", + event="quantities", + ) + + + @classmethod + def selection_data(cls, cost_item, elements): + if not elements: + return [] + if not cost_item: + return [ + { + "info": { + "id": element.id(), + "name": element.Name, + "class": element.is_a(), + "type": get_type(element).Name if get_type(element) else None, + }, + "qtos": get_psets(element, qtos_only=True), + } + for element in elements or [] + ] + data = [] + for element in elements or []: + psets = get_psets(element, qtos_only=True) + element_type = get_type(element) + quantities, unit = tool.Cost.get_assigned_quantities(cost_item, element) + data.append( + { + "info": { + "id": element.id(), + "name": element.Name, + "class": element.is_a(), + "type": element_type.Name if element_type else None, + }, + "qtos": psets, + "assigned_quantities": [ + { + "cost_item_id": cost_item.id(), + "product_id": element.id(), + "name": q.Name, + "value": q[3], + "type": q.Unit, + } + for q in quantities or [] + ], + "unit": unit, + } + ) + return data @classmethod def load_cost_schedule_web_ui(cls, cost_schedule):