From 8c4ab5bf03a9459f242f30c0b97fe27e650f55c6 Mon Sep 17 00:00:00 2001 From: myoualid Date: Sat, 21 Sep 2024 00:16:11 +0100 Subject: [PATCH] enable displaying schedule of rates - web ui ref #5369 --- .../data/webui/static/js/utilities/costui.js | 393 +++++++++++++----- src/bonsai/bonsai/tool/web.py | 8 +- .../ifcopenshell/util/cost.py | 11 +- 3 files changed, 297 insertions(+), 115 deletions(-) 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 468932bb99..f388b7723d 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 @@ -11,6 +11,7 @@ export class CostUI { table ? table.parentElement.remove() : null; } static createCostTable({ costSchedule, currency, callbacks }) { + const isScheduleOfRates = costSchedule.PredefinedType === "SCHEDULEOFRATES"; let tableWrapper; const id = costSchedule.id; if (CostUI.isCostScheduleLoaded(id)) { @@ -50,6 +51,11 @@ export class CostUI { "Total Cost (" + currency + ")", "Actions", ]; + if (isScheduleOfRates) { + columnHeaders.splice(2, 1); + columnHeaders[3] = "Rate (" + currency + ")"; + columnHeaders[4] = "Total Rate (" + currency + ")"; + } const thead = document.createElement("thead"); const tr = document.createElement("tr"); thead.appendChild(tr); @@ -80,7 +86,7 @@ export class CostUI { } static deleteCostItem(costItemId) { - const costItemRow = document.getElementById(costItemId); + const costItemRow = CostUI.getCostItemRow(costItemId); let expandedState = JSON.parse(localStorage.getItem("expandedState")) || {}; expandedState = CostUI.deleteCostItemRow(costItemRow, expandedState); localStorage.setItem("expandedState", JSON.stringify(expandedState)); @@ -149,18 +155,6 @@ export class CostUI { } } - // if (deleteCostItemButton && !deleteCostItemButton.hasListener) { - // deleteCostItemButton.addEventListener("click", function () { - // const targetRow = document.getElementById("context-menu").targetRow; - // if (targetRow) { - // const costItemId = parseInt(targetRow.getAttribute("id")); - // CostUI.deleteCostItem(costItemId, callbacks.deleteCostItem); - // } - // document.getElementById("context-menu").style.display = "none"; - // }); - // deleteCostItemButton.hasListener = true; - // } - handleButtonClick(deleteCostItemButton, callbacks.deleteCostItem); handleButtonClick(duplicateButton, callbacks.duplicateCostItem); handleButtonClick(getSelectedProducts, callbacks.enableEditingQuantities); @@ -187,10 +181,13 @@ export class CostUI { const columnIndex = Array.from(targetRow.children).indexOf( targetCell ); + const tableId = targetRow.parentElement.parentElement.id; const costItemId = parseInt(targetRow.getAttribute("id")); - const columnName = getColumnNames("cost-items")[columnIndex]; - - if (columnName.includes("Cost") && !columnName.includes("Total")) { + const columnName = getColumnNames(tableId)[columnIndex]; + if ( + (columnName.includes("Cost") || columnName.includes("Rate")) && + !columnName.includes("Total") + ) { callbacks.enableEditingCostValues ? callbacks.enableEditingCostValues(costItemId) : null; @@ -374,47 +371,51 @@ export class CostUI { addSummaryCostItemButton.textContent = "Add Summary Cost Item"; addSummaryCostItemButton.addEventListener("click", function () { callbacks.addSummaryCostItem - ? callbacks.addSummaryCostItem(costScheduleId) + ? callbacks.addSummaryCostItem(costSchedule.id) : null; }); td.appendChild(addSummaryCostItemButton); addSummaryCostItemButton.classList.add("action-button"); } else { - CostUI.createCostTree( - costSchedule["cost_items"], - tbody, - 0, - null, - callbacks - ); + CostUI.createCostTree({ + costItems: costSchedule["cost_items"], + container: tbody, + nestingLevel: 0, + parentID: null, + callbacks: callbacks, + isScheduleOfRates: costSchedule.PredefinedType === "SCHEDULEOFRATES", + }); CostUI.applyExpandedState(); } } - static createCostTree( + static createCostTree({ costItems, container, - nestingLevel = 0, - parentID = null, - callbacks = {} - ) { + nestingLevel, + parentID, + callbacks, + isScheduleOfRates = false, + }) { costItems.forEach((costItem) => { const row = CostUI.addCostItemRow( costItem, nestingLevel, parentID, + isScheduleOfRates, callbacks ); container.appendChild(row); if (costItem.IsNestedBy && costItem.IsNestedBy.length > 0) { - CostUI.createCostTree( - costItem.IsNestedBy, - container, - nestingLevel + 1, - costItem.id, - callbacks - ); + CostUI.createCostTree({ + costItems: costItem.IsNestedBy, + container: container, + nestingLevel: nestingLevel + 1, + parentID: costItem.id, + isScheduleOfRates: isScheduleOfRates, + callbacks: callbacks, + }); } }); } @@ -429,18 +430,32 @@ export class CostUI { .replace(/,/g, " "); } - static addCostItemRow(costItem, nestingLevel, parentID, callbacks = {}) { + static addCostItemRow( + costItem, + nestingLevel, + parentID, + isScheduleOfRates, + callbacks = {} + ) { const totalQuantity = costItem.TotalCostQuantity ? CostUI.format_number(costItem.TotalCostQuantity) : "-"; - const appliedValue = costItem.TotalAppliedValue + let appliedValue = costItem.TotalAppliedValue ? CostUI.format_number(costItem.TotalAppliedValue) : "-"; + const unitBasisValue = costItem.UnitBasisValueComponent + ? CostUI.format_number(costItem.UnitBasisValueComponent) + : null; + if (isScheduleOfRates && unitBasisValue) { + appliedValue = appliedValue + " / " + unitBasisValue; + } const row = CostUI.createCostItemRow(costItem, nestingLevel, parentID); const identification = costItem.Identification ? costItem.Identification : "XXX"; const idCell = CostUI.createTableCell(identification); + row.appendChild(idCell); + const expandButton = CostUI.createExpandButton(costItem); const nameCell = CostUI.createNameCell( costItem, @@ -448,33 +463,43 @@ export class CostUI { expandButton, callbacks ); - const totalCostQuantityCell = CostUI.createTableCell(totalQuantity); - totalCostQuantityCell.classList.add("clickable-cell"); - const unitSymbolCell = CostUI.createTableCell(costItem.UnitSymbol); + row.appendChild(nameCell); + + if (isScheduleOfRates) { + const unitBasisUnitSymbol = costItem.UnitBasisUnitSymbol + ? costItem.UnitBasisUnitSymbol + : "-"; + const unitBasisUnitSymbolCell = + CostUI.createTableCell(unitBasisUnitSymbol); + row.appendChild(unitBasisUnitSymbolCell); + } else { + const totalCostQuantityCell = CostUI.createTableCell(totalQuantity); + row.appendChild(totalCostQuantityCell); + totalCostQuantityCell.classList.add("clickable-cell"); + const unitSymbolCell = CostUI.createTableCell(costItem.UnitSymbol); + row.appendChild(unitSymbolCell); + } const totalAppliedValueCell = CostUI.createCostCell(appliedValue); totalAppliedValueCell.classList.add("clickable-cell"); + row.appendChild(totalAppliedValueCell); const totalCostCell = CostUI.createTotalCostCell( costItem, + isScheduleOfRates, callbacks.addSumCostValue ); - const actionsCell = CostUI.costItemActions(costItem, callbacks); - - actionsCell.classList.add("actions-column"); - - row.appendChild(idCell); - row.appendChild(nameCell); - row.appendChild(totalCostQuantityCell); - row.appendChild(unitSymbolCell); - row.appendChild(totalAppliedValueCell); row.appendChild(totalCostCell); + + const actionsCell = CostUI.costItemActions(costItem, callbacks); + actionsCell.classList.add("actions-column"); row.appendChild(actionsCell); row.get_id = function () { return this.getAttribute("id"); }; + row.isRate = isScheduleOfRates; row.get_parent = function () { const parentId = this.getAttribute("parent-id"); return parentId ? document.getElementById(parentId) : null; @@ -657,10 +682,13 @@ export class CostUI { return CostUI.createTableCell(appliedValue); } - static createTotalCostCell(costItem, callback) { + static createTotalCostCell(costItem, isScheduleOfRates, callback) { const totalCostCell = document.createElement("td"); const totalCost = CostUI.format_number(costItem.TotalCost); totalCostCell.textContent = totalCost; + if (isScheduleOfRates) { + return totalCostCell; + } if (costItem.IsSum) { totalCostCell.textContent = totalCost + " *"; } else if (!costItem.TotalCost) { @@ -835,9 +863,13 @@ export class CostUI { return card; } + static getCostItemRow(costItemId) { + return document.getElementById(costItemId); + } + static getRowNameCell(costItemId) { - return document.getElementById(costItemId) - ? document.getElementById(costItemId).querySelector("td input") + return CostUI.getCostItemRow(costItemId) + ? CostUI.getCostItemRow(costItemId).querySelector("td input") : null; } @@ -849,6 +881,10 @@ export class CostUI { static createCostValuesForm({ costItemId, costValues, callbacks }) { const formId = "cost-values-form-" + costItemId; + + const costItemRow = CostUI.getCostItemRow(costItemId); + const isRate = costItemRow.isRate; + let existingForm = document.getElementById(formId); if (existingForm) { existingForm.remove(); @@ -861,22 +897,41 @@ export class CostUI { id: formId, name: formName, }); + let headers; + if (isRate) { + headers = [ + "Type", + "Category", + "Value", + "Unit Symbol", + "Unit Component", + "", + ]; + } else { + headers = ["Type", "Category", "Value", ""]; + } const { tableContainer, table } = CostUI.addTable({ - headers: ["Type", "Category", "Value", ""], + headers: headers, className: "cost-values-table", id: "cost-values-table-" + costItemId, }); costValues.forEach((costValue) => { - const tr = CostUI.createCostvaluesRow(costItemId, costValue, callbacks); + costValue.isRate = isRate; + const tr = CostUI.createCostvaluesRow({ + costItemId, + costValue, + costValueCallbacks: callbacks, + }); table.appendChild(tr); }); - - const addButton = CostUI.createAddCostValueButton(costItemId, callbacks); - form.appendChild(tableContainer); - form.appendChild(addButton); + if (!isRate) { + //TODO: IMPLEMENT ADDING COST VALUES FOR COST RATES + const addButton = CostUI.createAddCostValueButton(costItemId, callbacks); + form.appendChild(addButton); + } return form; } @@ -889,17 +944,35 @@ export class CostUI { if (!table) { return; } - const tr = CostUI.createCostvaluesRow( - costItemId, - { + const costItemRow = CostUI.getCostItemRow(costItemId); + const isRate = costItemRow.isRate; + let newData; + if (isRate) { + newData = { category: "", name: "", applied_value: 0, id: costValueId, parent: costItemId, - }, - costValueCallbacks - ); + unit_data: { + unit_symbol: "", + value_component: 0, + }, + }; + } else { + newData = { + category: "", + name: "", + applied_value: 0, + id: costValueId, + parent: costItemId, + }; + } + const tr = CostUI.createCostvaluesRow({ + costItemId, + costValue: newData, + costValueCallbacks, + }); table.appendChild(tr); } @@ -933,35 +1006,54 @@ export class CostUI { return { tableContainer: div, table: table }; } - static createCostvaluesRow(costItemId, costValue, costValueCallbacks) { - function cleanLabel(value) { - return value.replace(/[^0-9.]/g, ""); - } - + static createCostvaluesRow({ costItemId, costValue, costValueCallbacks }) { 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 costType = CostUI.determineCostType(costValue); + const { dropdown, typeCell } = CostUI.createTypeCell( + tr, + costType, + costItemId, + costValue, + costValueCallbacks + ); + tr.appendChild(typeCell); + + const categoryCell = CostUI.createTableInput( + "text", + "category", + costValue.category + ); + tr.appendChild(categoryCell); + + const value = CostUI.determineValue(costValue); + const valueCell1 = CostUI.createTableInput("number", "value", value); + tr.appendChild(valueCell1); + let unitSymbolCell; + let unitComponentCell; + if (costValue.isRate) { + unitSymbolCell = CostUI.Text(costValue.unit_data.unit_symbol); //TODO CostUI.createTableInput("text", "unit_symbol", costValue.unit_data.unit_symbol); + tr.appendChild(unitSymbolCell); + unitComponentCell = CostUI.createTableInput( + "number", + "unit_value_component", + costValue.unit_data.value_component + ); + tr.appendChild(unitComponentCell); } - const options = ["FIXED", "CATEGORY", "SUM"]; - const typeCell = document.createElement("td"); - const dropdown = CostUI.createTableDropdown({ - name: "type", - options: options, - defaultValue: costType, - }); - typeCell.appendChild(dropdown); + + CostUI.addValueChangeListeners( + tr, + costItemId, + costValue, + costValueCallbacks + ); dropdown.addEventListener("change", function () { const selectedType = this.value; CostUI.updateRowBasedOnType(selectedType, categoryCell, valueCell1); @@ -986,62 +1078,139 @@ export class CostUI { } } }); - tr.appendChild(typeCell); - const categoryCell = CostUI.createTableInput( - "text", - "category", - costValue.category + const deleteCell = CostUI.createDeleteCell( + costItemId, + costValue, + costValueCallbacks, + tr ); - tr.appendChild(categoryCell); + tr.appendChild(deleteCell); - let value; + CostUI.updateRowBasedOnType(costType, categoryCell, valueCell1); + return tr; + } + + static determineCostType(costValue) { + if (costValue.category === "*") { + return "SUM"; + } else if (costValue.category && costValue.category !== "*") { + return "CATEGORY"; + } else if (costValue.applied_value) { + return "FIXED"; + } + return "FIXED"; + } + + static createTypeCell(costType, costItemId, costValue, costValueCallbacks) { + const options = ["FIXED", "CATEGORY", "SUM"]; + const typeCell = document.createElement("td"); + const dropdown = CostUI.createTableDropdown({ + name: "type", + options: options, + defaultValue: costType, + }); + typeCell.appendChild(dropdown); + + return { dropdown, typeCell }; + } + + static determineValue(costValue) { + function cleanLabel(value) { + return value.replace(/[^0-9.]/g, ""); + } if (costValue.category === "*") { - value = cleanLabel(costValue.label); + return cleanLabel(costValue.label); } else { - value = costValue.applied_value; + return costValue.applied_value; } - const valueCell1 = CostUI.createTableInput("number", "value", value); - tr.appendChild(valueCell1); + } - const inputValue = valueCell1.querySelector("input"); + static getCostValueData({ + typeCell, + categoryCell, + valueCell1, + componentCell, + }) { + const costType = typeCell.value; + const costCategory = categoryCell ? categoryCell.value : null; + const appliedValue = parseFloat(valueCell1.value); + const unitBasisValue = componentCell + ? parseFloat(componentCell.value) + : null; + return { costType, costCategory, appliedValue, unitBasisValue }; + } + + static addValueChangeListeners( + tr, + costItemId, + costValue, + costValueCallbacks + ) { function handleCostValueChange(costItemId) { - const costValueData = { - costType: typeCell.querySelector("select").value, - costCategory: categoryCell.querySelector("input").value, - appliedValue: parseFloat(this.value), + let costValueData = CostUI.getCostValueData({ + typeCell, + categoryCell, + valueCell1, + componentCell, + }); + costValueData = { + ...costValueData, id: costValue.id, costItemId: costItemId, }; + if (costValueData.unitBasisValue) { + costValueData.unitComponent = costValue.unit_data.unit_component; + } if (costValueCallbacks.editCostValues) { costValueCallbacks.editCostValues(costItemId, [costValueData]); } } - inputValue.addEventListener( + const typeCell = tr.querySelector("td select[name='type']"); + const categoryCell = tr.querySelector("td input[name='category']"); + const valueCell1 = tr.querySelector("td input[name='value']"); + const componentCell = tr.querySelector( + "td input[name='unit_value_component']" + ); + valueCell1.addEventListener( "change", - handleCostValueChange.bind(inputValue, costItemId) + handleCostValueChange.bind(valueCell1, costItemId) ); - inputValue.addEventListener("keydown", function (event) { + valueCell1.addEventListener("keydown", function (event) { if (event.key === "Enter") { event.preventDefault(); handleCostValueChange.call(this, costItemId); } }); + if (componentCell) { + componentCell.addEventListener( + "change", + handleCostValueChange.bind(componentCell, costItemId) + ); + componentCell.addEventListener("keydown", function (event) { + if (event.key === "Enter") { + event.preventDefault(); + handleCostValueChange.call(this, costItemId); + } + }); + } + } + + static createDeleteCell(costItemId, costValue, costValueCallbacks, tr) { const deleteCell = document.createElement("td"); - tr.appendChild(deleteCell); - const dleteButton = CostUI.createButton("Delete", "fa-solid fa-trash"); - dleteButton.addEventListener("click", function () { + const deleteButton = CostUI.createButton("Delete", "fa-solid fa-trash"); + + deleteButton.addEventListener("click", function () { costValueCallbacks.deleteCostValue(costItemId, costValue.id); tr.remove(); }); - deleteCell.appendChild(dleteButton); - CostUI.updateRowBasedOnType(costType, categoryCell, valueCell1); - return tr; + deleteCell.appendChild(deleteButton); + return deleteCell; } static updateRowBasedOnType(type, categoryCell, valueCell1) { @@ -1126,7 +1295,7 @@ export class CostUI { const costItemId = form.id.split("-").pop(); CostUI.unhighlightElement(costItemId); }; - closeButton = CostUI.createCloseButton(callback); // Pass the function reference without invoking it + closeButton = CostUI.createCloseButton(callback); } form.appendChild(header); form.appendChild(closeButton); diff --git a/src/bonsai/bonsai/tool/web.py b/src/bonsai/bonsai/tool/web.py index 9ba769c938..2596b9df2f 100644 --- a/src/bonsai/bonsai/tool/web.py +++ b/src/bonsai/bonsai/tool/web.py @@ -391,8 +391,8 @@ class Web(bonsai.core.tool.Web): cls.load_cost_schedule_web_ui(cost_schedule) if operator_data["type"] == "deleteCostItem": cost_item = tool.Ifc.get().by_id(operator_data["costItemId"]) - tool.Ifc.run("cost.remove_cost_item", cost_item=cost_item) cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item) + tool.Ifc.run("cost.remove_cost_item", cost_item=cost_item) cls.load_cost_schedule_web_ui(cost_schedule) if operator_data["type"] == "duplicateCostItem": cost_item = tool.Ifc.get().by_id(operator_data["costItemId"]) @@ -439,6 +439,7 @@ class Web(bonsai.core.tool.Web): cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item) for value_data in operator_data["costValues"] or []: value = ifc_file.by_id(value_data["id"]) + attributes = {} if value_data["costType"] == "FIXED": attributes = {"AppliedValue": value_data["appliedValue"], "Category": None} elif value_data["costType"] == "CATEGORY": @@ -448,6 +449,11 @@ class Web(bonsai.core.tool.Web): } elif value_data["costType"] == "SUM": attributes = {"Category": "*"} + if value_data["unitBasisValue"]: + attributes["UnitBasis"] = { + "ValueComponent": value_data["unitBasisValue"], + "UnitComponent": ifc_file.by_id(value_data["unitComponent"]), + } ifcopenshell.api.cost.edit_cost_value(file=ifc_file, cost_value=value, attributes=attributes) cls.load_cost_schedule_web_ui(cost_schedule) if operator_data["type"] == "addProductAssignments": diff --git a/src/ifcopenshell-python/ifcopenshell/util/cost.py b/src/ifcopenshell-python/ifcopenshell/util/cost.py index 36f4b001cc..c13a562aa6 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/cost.py +++ b/src/ifcopenshell-python/ifcopenshell/util/cost.py @@ -22,7 +22,7 @@ from typing import Optional, Union, Literal, Generator, Any import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper from ifcopenshell.util.doc import get_predefined_type_doc from ifcopenshell.util.element import get_psets - +from ifcopenshell.util.unit import get_unit_symbol arithmetic_operator_symbols = {"ADD": "+", "DIVIDE": "/", "MULTIPLY": "*", "SUBTRACT": "-"} symbol_arithmetic_operators = {"+": "ADD", "/": "DIVIDE", "*": "MULTIPLY", "-": "SUBTRACT"} FILTER_BY_TYPE = Literal["PRODUCT", "RESOURCE", "PROCESS"] @@ -255,6 +255,13 @@ def get_cost_values(cost_item: ifcopenshell.entity_instance) -> list[dict[str, s 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)) + unit_data = {"value_component": None, "unit_component": None, "unit_symbol": ""} + if cost_value.UnitBasis: + data = cost_value.UnitBasis.get_info() + unit_data["value_component"] = data["ValueComponent"].wrappedValue + unit_data["unit_component"] = data["UnitComponent"].id() + unit_data["unit_symbol"] = get_unit_symbol(cost_value.UnitBasis.UnitComponent) + results.append( { "id": cost_value.id(), @@ -264,9 +271,9 @@ def get_cost_values(cost_item: ifcopenshell.entity_instance) -> list[dict[str, s "applied_value": ( get_primitive_applied_value(cost_value.AppliedValue) if cost_value.AppliedValue else None ), + "unit_data": unit_data, } ) - print(results) return results