From 9f542c30b3893f2639acdf5fff57ff167fbbee06 Mon Sep 17 00:00:00 2001 From: myoualid Date: Sun, 22 Sep 2024 20:00:09 +0100 Subject: [PATCH] cost module web ui: - add setting options to hide/show columns - Cost classification column - New Cost Classification Form to display active classification library, add/delete classification references --- src/bonsai/bonsai/bim/data/webui/sioserver.py | 11 +- .../bonsai/bim/data/webui/static/css/cost.css | 81 +- .../bim/data/webui/static/css/shared.css | 1 + .../bonsai/bim/data/webui/static/js/cost.js | 51 ++ .../data/webui/static/js/utilities/costui.js | 764 +++++++++++++----- src/bonsai/bonsai/tool/web.py | 39 + src/ifc5d/ifc5d/ifc2json.py | 13 +- .../ifcopenshell/util/classification.py | 17 + 8 files changed, 760 insertions(+), 217 deletions(-) diff --git a/src/bonsai/bonsai/bim/data/webui/sioserver.py b/src/bonsai/bonsai/bim/data/webui/sioserver.py index eecb1643f9..d14148520a 100644 --- a/src/bonsai/bonsai/bim/data/webui/sioserver.py +++ b/src/bonsai/bonsai/bim/data/webui/sioserver.py @@ -71,7 +71,7 @@ class WebNamespace(socketio.AsyncNamespace): await self.emit("gantt_data", {"blenderId": blenderId, "data": messages["gantt_data"]}, room=sid) if "demo_data" in messages: await self.emit("demo_data", {"blenderId": blenderId, "data": messages["demo_data"]}, room=sid) - if 'cost_items' in messages: + if "cost_items" in messages: await self.emit("cost_items", {"blenderId": blenderId, "data": messages["cost_items"]}, room=sid) async def process_svg(self, file_path): @@ -183,18 +183,25 @@ 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_quantities(self, sid, data): print(f"Selected products from Blender client {sid}") blender_messages[sid]["quantities"] = data await sio.emit("quantities", {"blenderId": sid, "data": data}, namespace="/web") + async def on_classification(self, sid, data): + print(f"Classification from Blender client {sid}") + blender_messages[sid]["classification"] = data + await sio.emit("classification", {"blenderId": sid, "data": data}, namespace="/web") + + async def schedules(request): with open("templates/index.html", "r") as f: template = f.read() html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version}) return web.Response(text=html_content, content_type="text/html") + async def costing(request): with open("templates/costing.html", "r") as f: template = f.read() 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 38b8d028b0..71a973fcac 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/cost.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/cost.css @@ -192,7 +192,6 @@ body { flex: 1; overflow: auto; border: 2px solid #254e3c; - /* shadow to the top */ box-shadow: 0 -5px 5px -0 #254e3c; } @@ -307,10 +306,9 @@ form input, form select { font-size: 12px; line-height: 10px; font-size: inherit; - width: 100%; height: 100%; max-height: 30px; - max-width: 250px; + max-width: 100%; box-sizing: border-box; } @@ -420,4 +418,81 @@ td { i { margin-right: 5px; +} + +.classificationView { + border-left: 5px solid #28a745; + min-height: 200px; + overflow-y: auto; + overflow-x: hidden; + padding: 5px; + max-height: 400px; +} + +.selected { + border-color: #28a745; + outline: none; + box-shadow: 0 0 0 3px #28a745; + color: #ddd; +} + +.selectedClassificationText { + color: #28a745; + +} + +.back-button { + margin-bottom: 20px; +} + +.back-button:disabled { + cursor: not-allowed; + background-color: #ccc; +} + + +.classification-cell{ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; +} + +.classification{ + height: 100%; +} + +#column-selector { + display: flex; + flex-direction: column; + padding: 15px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + max-width: 100%; + font-family: Arial, sans-serif; + height: 10vh; + overflow-y: auto; +} + +#column-selector label { + display: flex; + align-items: center; + margin-bottom: 10px; + cursor: pointer; + padding: 5px; + border-radius: 4px; + transition: background-color 0.3s ease; +} + +#column-selector input[type="checkbox"] { + margin-right: 10px; +} + +#column-selector label:hover { + background-color: var(--highlight-over); +} + +#column-selector label:last-child { + margin-bottom: 0; } \ No newline at end of file diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/shared.css b/src/bonsai/bonsai/bim/data/webui/static/css/shared.css index 5082d95230..a35a5f0663 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/shared.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/shared.css @@ -223,6 +223,7 @@ button:hover { display: flex; justify-content: flex-start; flex-wrap: nowrap; + align-items: center; } .column-container { 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 ff469dedb2..e396ebb88c 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/js/cost.js +++ b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js @@ -29,6 +29,7 @@ function connectSocket() { socket.on("cost_values", handleCostValuesData); socket.on("cost_value", handleCostValueData); socket.on("quantities", handleEditQuantities); + socket.on("classification", handleClassificationData); } function handleEditQuantities(data) { @@ -324,6 +325,7 @@ function handleCostItemsData(data) { addSummaryCostItem: addSummaryCostItem, enableEditingQuantities: enableEditingQuantities, addSumCostValue: addSumCostValue, + enableEditingClassification: enableEditingClassification, }, }); } @@ -371,6 +373,55 @@ function enableEditingCostValues(costItemId) { executeOperator({ type: "enableEditingCostValues", costItemId: costItemId }); } +function enableEditingClassification(costItemId) { + executeOperator({ + type: "enableEditingClassification", + costItemId: costItemId, + }); +} + +function removeClassificationReference(costItemId, classificationId) { + executeOperator({ + type: "removeClassificationReference", + costItemId: costItemId, + classificationId: classificationId, + }); +} + +function addClassificationReference( + costItemId, + classificationName, + classificationId +) { + executeOperator({ + type: "addClassificationReference", + costItemId: costItemId, + classificationName: classificationName, + classificationId: classificationId, + }); +} + +function handleClassificationData(data) { + const costClassifications = + data.data["classification"]["cost_classifications"]; + const classificationElements = + data.data["classification"]["classification_data"]; + const classificationName = data.data["classification"]["classification_name"]; + const costItemId = data.data["classification"]["cost_item_id"]; + + CostUI.createCostClassificationWindow({ + classificationName, + classificationElements, + costClassifications, + costItemId, + callbacks: { + enableEditingClassification: enableEditingClassification, + removeClassificationReference: removeClassificationReference, + addClassificationReference: addClassificationReference, + }, + }); +} + function enableEditingQuantities(costItemId) { executeOperator({ type: "enableEditingQuantities", costItemId: costItemId }); } 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 f388b7723d..a9deaa1b04 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,97 +1,6 @@ export class CostUI { constructor() {} - static isCostScheduleLoaded(id) { - const existingTable = document.getElementById("cost-items-" + id); - return existingTable !== null; - } - - static removeCostSchedule(id) { - const table = document.getElementById("cost-items-" + id); - 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)) { - const table = document.getElementById("cost-items-" + id); - tableWrapper = table.parentElement; - table.remove(); - } else { - tableWrapper = document.createElement("div"); - tableWrapper.classList.add("table-wrapper"); - document.getElementById("cost-items").appendChild(tableWrapper); - const tableHeader = document.createElement("div"); - tableHeader.classList.add("form-header"); - const text = CostUI.Text( - costSchedule.Name, - "fa-solid fa-money-bill-wave", - "x-large" - ); - - const callback = () => { - tableWrapper.remove(); - CostUI.unhighlightElement("schedule-" + id); - }; - let closeButton = CostUI.createCloseButton(callback); - tableHeader.appendChild(text); - tableHeader.appendChild(closeButton); - tableWrapper.appendChild(tableHeader); - } - const table = document.createElement("table"); - table.id = "cost-items-" + id; - const tbody = document.createElement("tbody"); - const columnHeaders = [ - "ID", - "Name", - "Quantity", - "Unit", - "Cost (" + currency + ")", - "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); - - for (let i = 0; i < columnHeaders.length; i++) { - const th = document.createElement("th"); - th.textContent = columnHeaders[i]; - tr.appendChild(th); - - if (i < columnHeaders.length - 1) { - const resizer = document.createElement("div"); - resizer.classList.add("resizer"); - th.appendChild(resizer); - CostUI.addResizer(resizer); - } - } - - table.appendChild(thead); - table.appendChild(tbody); - tableWrapper.appendChild(table); - - CostUI.createContextMenu(callbacks); - table.get_blender_id = function () { - return this.getAttribute("id").split("-")[2]; - }; - - return [table, tbody]; - } - - static deleteCostItem(costItemId) { - const costItemRow = CostUI.getCostItemRow(costItemId); - let expandedState = JSON.parse(localStorage.getItem("expandedState")) || {}; - expandedState = CostUI.deleteCostItemRow(costItemRow, expandedState); - localStorage.setItem("expandedState", JSON.stringify(expandedState)); - } - static createContextMenu(callbacks) { const contextMenu = document.createElement("div"); contextMenu.id = "context-menu"; @@ -197,44 +106,121 @@ export class CostUI { ? callbacks.enableEditingQuantities(costItemId) : null; } + if (columnName === "Classification") { + callbacks.enableEditingClassification + ? callbacks.enableEditingClassification(costItemId) + : null; + } } }); } } - static deleteCostItemRow(targetRow, expandedState) { - if (!targetRow) { - return; - } - delete expandedState[targetRow.id]; - targetRow.remove(); - const subRows = document.querySelectorAll(`[parent-id='${targetRow.id}']`); - subRows.forEach((subRow) => { - CostUI.deleteCostItemRow(subRow, expandedState); - }); - return expandedState; + static getColumnPreferences() { + const preferences = localStorage.getItem("columnPreferences"); + return preferences + ? JSON.parse(preferences) + : { + ID: true, + Name: true, + Quantity: true, + Unit: true, + Cost: true, + TotalCost: true, + Classification: true, + Actions: true, + }; } - static addResizer(resizer) { - let startX, startWidth, th; - - resizer.addEventListener("mousedown", function (e) { - th = e.target.parentElement; - startX = e.pageX; - startWidth = th.offsetWidth; - document.addEventListener("mousemove", resizeColumn); - document.addEventListener("mouseup", stopResize); + static addSettingsMenu() { + const settingsMenu = CostUI.Form({ + id: "settings-menu", + name: "Settings", + icon: "fa-solid fa-gear", + shouldHide: true, }); - function resizeColumn(e) { - const newWidth = startWidth + (e.pageX - startX); - th.style.width = newWidth + "px"; - } + const picker = CostUI.createColorPicker(); + const tableFontSize = CostUI.createFontSizePicker(); + const currencyPicker = CostUI.createCurrencyPicker(); + settingsMenu.appendChild(picker); + settingsMenu.appendChild(tableFontSize); + document.getElementById("settings-menu").style.display = "none"; + CostUI.applySavedSettings(); + const columnSelector = CostUI.createColumnSelector(); + settingsMenu.appendChild(columnSelector); + } - function stopResize() { - document.removeEventListener("mousemove", resizeColumn); - document.removeEventListener("mouseup", stopResize); - } + static createColumnSelector() { + const columnSelector = document.createElement("div"); + columnSelector.id = "column-selector"; + const columns = ["ID", "Quantity", "Unit", "Classification", "Actions"]; + columns.forEach((column) => { + const label = document.createElement("label"); + const input = document.createElement("input"); + input.type = "checkbox"; + input.dataset.column = column; + input.checked = true; + label.appendChild(input); + label.appendChild(document.createTextNode(column)); + columnSelector.appendChild(label); + }); + + columnSelector.querySelectorAll("input").forEach((input) => { + input.addEventListener("change", () => { + const preferences = CostUI.getColumnPreferences(); + preferences[input.dataset.column] = input.checked; + CostUI.setColumnPreferences(preferences); + CostUI.updateColumnVisibility(preferences); + }); + }); + return columnSelector; + } + + static setColumnPreferences(preferences) { + localStorage.setItem("columnPreferences", JSON.stringify(preferences)); + } + + static updateColumnVisibility(preferences) { + const tables = document.querySelectorAll("table[id^='cost-items-']"); + tables.forEach((table) => { + const columns = table.querySelectorAll("th, td"); + columns.forEach((column) => { + const columnName = column.getAttribute("data-column"); + if (preferences[columnName] !== undefined) { + column.style.display = preferences[columnName] ? "" : "none"; + } + }); + }); + } + + static createRibbonBar() { + const ribbonBar = document.createElement("div"); + ribbonBar.className = "switch-bar"; + ribbonBar.innerHTML = ` + + + + `; + return ribbonBar; + } + + static createRibbon() { + CostUI.addSettingsMenu(); + CostUI.addRibbonButton({ + text: "Hide Schedules", + icon: "fa-regular fa-eye-slash", + callback: (button) => { + CostUI.toggleSchedulesContainer(button); + }, + }); + CostUI.addRibbonButton({ + text: "Settings", + icon: "fas fa-cog", + callback: () => { + CostUI.toggleSettingsMenu(); + }, + }); } static generateColorScheme(baseColor) { @@ -389,6 +375,16 @@ export class CostUI { } } + static isCostScheduleLoaded(id) { + const existingTable = document.getElementById("cost-items-" + id); + return existingTable !== null; + } + + static removeCostSchedule(id) { + const table = document.getElementById("cost-items-" + id); + table ? table.parentElement.remove() : null; + } + static createCostTree({ costItems, container, @@ -420,14 +416,90 @@ export class CostUI { }); } - static format_number(number) { - return new Intl.NumberFormat("en-US", { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - useGrouping: true, - }) - .format(number) - .replace(/,/g, " "); + static createCostTable({ costSchedule, currency, callbacks }) { + const preferences = CostUI.getColumnPreferences(); + const isScheduleOfRates = costSchedule.PredefinedType === "SCHEDULEOFRATES"; + let tableWrapper; + const id = costSchedule.id; + if (CostUI.isCostScheduleLoaded(id)) { + const table = document.getElementById("cost-items-" + id); + tableWrapper = table.parentElement; + table.remove(); + } else { + tableWrapper = document.createElement("div"); + tableWrapper.classList.add("table-wrapper"); + document.getElementById("cost-items").appendChild(tableWrapper); + const tableHeader = document.createElement("div"); + tableHeader.classList.add("form-header"); + const text = CostUI.Text( + costSchedule.Name, + "fa-solid fa-money-bill-wave", + "x-large" + ); + + const callback = () => { + tableWrapper.remove(); + CostUI.unhighlightElement("schedule-" + id); + }; + let closeButton = CostUI.createCloseButton(callback); + tableHeader.appendChild(text); + tableHeader.appendChild(closeButton); + tableWrapper.appendChild(tableHeader); + } + const table = document.createElement("table"); + table.id = "cost-items-" + id; + const tbody = document.createElement("tbody"); + const columnHeaders = [ + { name: "ID", visible: preferences.ID }, + { name: "Name", visible: preferences.Name }, + { name: "Quantity", visible: preferences.Quantity }, + { name: "Unit", visible: preferences.Unit }, + { name: "Cost (" + currency + ")", visible: preferences.Cost }, + { name: "Total Cost (" + currency + ")", visible: preferences.TotalCost }, + { name: "Classification", visible: preferences.Classification }, + { name: "Actions", visible: preferences.Actions }, + ]; + if (isScheduleOfRates) { + columnHeaders.splice(2, 1); + columnHeaders[3] = { + name: "Rate (" + currency + ")", + visible: preferences.Cost, + }; + columnHeaders[4] = { + name: "Total Rate (" + currency + ")", + visible: preferences.TotalCost, + }; + } + const thead = document.createElement("thead"); + const tr = document.createElement("tr"); + thead.appendChild(tr); + + for (let i = 0; i < columnHeaders.length; i++) { + if (columnHeaders[i].visible) { + const th = document.createElement("th"); + th.textContent = columnHeaders[i].name; + th.setAttribute("data-column", columnHeaders[i].name); + tr.appendChild(th); + + if (i < columnHeaders.length - 1) { + const resizer = document.createElement("div"); + resizer.classList.add("resizer"); + th.appendChild(resizer); + CostUI.addResizer(resizer); + } + } + } + + table.appendChild(thead); + table.appendChild(tbody); + tableWrapper.appendChild(table); + + CostUI.createContextMenu(callbacks); + table.get_blender_id = function () { + return this.getAttribute("id").split("-")[2]; + }; + + return [table, tbody]; } static addCostItemRow( @@ -437,6 +509,7 @@ export class CostUI { isScheduleOfRates, callbacks = {} ) { + const preferences = CostUI.getColumnPreferences(); const totalQuantity = costItem.TotalCostQuantity ? CostUI.format_number(costItem.TotalCostQuantity) : "-"; @@ -453,47 +526,85 @@ export class CostUI { 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, - nestingLevel, - expandButton, - callbacks - ); - 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); + if (preferences.ID) { + const idCell = CostUI.createTableCell(identification); + idCell.setAttribute("data-column", "ID"); + row.appendChild(idCell); } - const totalAppliedValueCell = CostUI.createCostCell(appliedValue); - totalAppliedValueCell.classList.add("clickable-cell"); - row.appendChild(totalAppliedValueCell); + const expandButton = CostUI.createExpandButton(costItem); + if (preferences.Name) { + const nameCell = CostUI.createNameCell( + costItem, + nestingLevel, + expandButton, + callbacks + ); + nameCell.setAttribute("data-column", "Name"); + row.appendChild(nameCell); + } - const totalCostCell = CostUI.createTotalCostCell( - costItem, - isScheduleOfRates, - callbacks.addSumCostValue - ); - row.appendChild(totalCostCell); + if (isScheduleOfRates) { + if (preferences.Unit) { + const unitBasisUnitSymbol = costItem.UnitBasisUnitSymbol + ? costItem.UnitBasisUnitSymbol + : "-"; + const unitBasisUnitSymbolCell = + CostUI.createTableCell(unitBasisUnitSymbol); + unitBasisUnitSymbolCell.setAttribute("data-column", "Unit"); + row.appendChild(unitBasisUnitSymbolCell); + } + } else { + if (preferences.Quantity) { + const totalCostQuantityCell = CostUI.createTableCell(totalQuantity); + totalCostQuantityCell.setAttribute("data-column", "Quantity"); + row.appendChild(totalCostQuantityCell); + totalCostQuantityCell.classList.add("clickable-cell"); + } + if (preferences.Unit) { + const unitSymbolCell = CostUI.createTableCell(costItem.UnitSymbol); + unitSymbolCell.setAttribute("data-column", "Unit"); + row.appendChild(unitSymbolCell); + } + } - const actionsCell = CostUI.costItemActions(costItem, callbacks); - actionsCell.classList.add("actions-column"); - row.appendChild(actionsCell); + if (preferences.Cost) { + const totalAppliedValueCell = CostUI.createCostCell(appliedValue); + totalAppliedValueCell.setAttribute("data-column", "Cost"); + totalAppliedValueCell.classList.add("clickable-cell"); + row.appendChild(totalAppliedValueCell); + } + + if (preferences.TotalCost) { + const totalCostCell = CostUI.createTotalCostCell( + costItem, + isScheduleOfRates, + callbacks.addSumCostValue + ); + totalCostCell.setAttribute("data-column", "Total Cost"); + row.appendChild(totalCostCell); + } + + if (preferences.Classification) { + const classifications = costItem.Classification + ? costItem.Classification + : []; + const ClassificationCell = document.createElement("td"); + ClassificationCell.setAttribute("data-column", "Classification"); + ClassificationCell.classList.add("clickable-cell"); + row.appendChild(ClassificationCell); + const string_List = classifications + .map((classification) => classification.Identification) + .join(", "); + ClassificationCell.textContent = string_List; + } + + if (preferences.Actions) { + const actionsCell = CostUI.costItemActions(costItem, callbacks); + actionsCell.setAttribute("data-column", "Actions"); + actionsCell.classList.add("actions-column"); + row.appendChild(actionsCell); + } row.get_id = function () { return this.getAttribute("id"); @@ -508,6 +619,45 @@ export class CostUI { return row; } + static addResizer(resizer) { + let startX, startWidth, th; + + resizer.addEventListener("mousedown", function (e) { + th = e.target.parentElement; + startX = e.pageX; + startWidth = th.offsetWidth; + document.addEventListener("mousemove", resizeColumn); + document.addEventListener("mouseup", stopResize); + }); + + function resizeColumn(e) { + const newWidth = startWidth + (e.pageX - startX); + th.style.width = newWidth + "px"; + } + + function stopResize() { + document.removeEventListener("mousemove", resizeColumn); + document.removeEventListener("mouseup", stopResize); + } + } + + static format_number(number) { + return new Intl.NumberFormat("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + useGrouping: true, + }) + .format(number) + .replace(/,/g, " "); + } + + static deleteCostItem(costItemId) { + const costItemRow = CostUI.getCostItemRow(costItemId); + let expandedState = JSON.parse(localStorage.getItem("expandedState")) || {}; + expandedState = CostUI.deleteCostItemRow(costItemRow, expandedState); + localStorage.setItem("expandedState", JSON.stringify(expandedState)); + } + static createCostItemRow(costItem, nestingLevel, parentID) { const row = document.createElement("tr"); row.setAttribute("id", costItem.id); @@ -518,6 +668,19 @@ export class CostUI { return row; } + static deleteCostItemRow(targetRow, expandedState) { + if (!targetRow) { + return; + } + delete expandedState[targetRow.id]; + targetRow.remove(); + const subRows = document.querySelectorAll(`[parent-id='${targetRow.id}']`); + subRows.forEach((subRow) => { + CostUI.deleteCostItemRow(subRow, expandedState); + }); + return expandedState; + } + static createExpandButton(row) { const expandButton = document.createElement("i"); expandButton.classList.add("action-button"); @@ -852,7 +1015,7 @@ export class CostUI { ); cardTitle.classList.add("card-title"); - const cardButton = CostUI.createButton("Load", "fa-solid fa-repeat"); + const cardButton = CostUI.createButton("Load", "fa-solid fa-arrows-rotate"); cardButton.addEventListener("click", callback); cardBody.appendChild(cardTitle); @@ -1753,23 +1916,6 @@ export class CostUI { return addProductAssignmentsButton; } - static addSettingsMenu() { - const settingsMenu = CostUI.Form({ - id: "settings-menu", - name: "Settings", - icon: "fa-solid fa-gear", - shouldHide: true, - }); - - const picker = CostUI.createColorPicker(); - const tableFontSize = CostUI.createFontSizePicker(); - const currencyPicker = CostUI.createCurrencyPicker(); - settingsMenu.appendChild(picker); - settingsMenu.appendChild(tableFontSize); - document.getElementById("settings-menu").style.display = "none"; - CostUI.applySavedSettings(); - } - static createFontSizePicker() { const div = document.createElement("div"); const fontSizeText = document.createElement("p"); @@ -1931,24 +2077,6 @@ export class CostUI { } } - static createRibbon() { - CostUI.addSettingsMenu(); - CostUI.addRibbonButton({ - text: "Hide Schedules", - icon: "fa-regular fa-eye-slash", - callback: (button) => { - CostUI.toggleSchedulesContainer(button); - }, - }); - CostUI.addRibbonButton({ - text: "Settings", - icon: "fas fa-cog", - callback: () => { - CostUI.toggleSettingsMenu(); - }, - }); - } - static enableEditingQuantities({ costItemId, selectedProducts, @@ -2017,17 +2145,6 @@ export class CostUI { } } - static createRibbonBar() { - const ribbonBar = document.createElement("div"); - ribbonBar.className = "switch-bar"; - ribbonBar.innerHTML = ` - - - - `; - return ribbonBar; - } - static createSummarySection({ selectedProducts, assignedProducts, @@ -2354,4 +2471,229 @@ export class CostUI { return addButton; } + + static createCostClassificationWindow({ + classificationName, + classificationElements, + costClassifications, + costItemId, + callbacks, + }) { + const formName = classificationName || "Classification"; + const formContainer = CostUI.Form({ + id: "classificationTree", + name: formName, + icon: "fa-solid fa-sitemap", + }); + this.createLayeredViewPanel( + formContainer, + classificationElements, + formName, + costItemId, + callbacks + ); + this.classificationList = this.createClassificationList({ + costClassifications, + costItemId, + callbacks, + }); + formContainer.appendChild(this.classificationList); + } + + static createClassificationList({ + costClassifications, + costItemId, + callbacks, + }) { + const classificationList = document.createElement("div"); + classificationList.classList.add("classification-list"); + + const header = document.createElement("h2"); + header.textContent = "Classifications"; + classificationList.appendChild(header); + + const classificationContainer = document.createElement("div"); + classificationContainer.classList.add("classification-container"); + classificationList.appendChild(classificationContainer); + console.log(costClassifications); + if (costClassifications.length === 0) { + const text = "No classifications available"; + const noDataMessage = this.Text( + text, + "fa-solid fa-exclamation-circle", + "large" + ); + classificationContainer.appendChild(noDataMessage); + } else { + costClassifications.forEach((classification) => { + const classificationDiv = document.createElement("div"); + classificationDiv.classList.add("classification"); + classificationDiv.textContent = + classification.Name + " (" + classification.Identification + ")"; + + const removeClassificationReferenceButton = this.createButton( + "Remove", + "fa-solid fa-trash" + ); + removeClassificationReferenceButton.addEventListener( + "click", + function (event) { + event.preventDefault(); + console.log( + "Remove classification reference from", + costItemId, + "Classificaition Id", + classification.id + ); + callbacks.removeClassificationReference( + costItemId, + classification.id + ); + callbacks.enableEditingClassification(costItemId); + } + ); + classificationDiv.appendChild(removeClassificationReferenceButton); + classificationContainer.appendChild(classificationDiv); + }); + } + return classificationList; + } + + static createLayeredViewPanel( + formContainer, + classificationElements, + classificationName, + costItemId, + callbacks + ) { + function loadLevel(items) { + container.innerHTML = ""; + items.forEach((item) => { + const itemDiv = document.createElement("div"); + itemDiv.classList.add("item-container"); + itemDiv.classList.add("row-container"); + itemDiv.innerHTML = `${item.Identification}: ${item.Name}`; + + if (item.has_references && item.references.length > 0) { + const nextButton = CostUI.createButton( + "View", + "fa-solid fa-arrow-right" + ); + nextButton.addEventListener("click", function (event) { + event.stopPropagation(); + currentPath.push(item); + loadLevel(item.references); + backButton.disabled = false; + }); + itemDiv.appendChild(nextButton); + } + itemDiv.addEventListener("click", function () { + const allItems = container.querySelectorAll(".item-container"); + allItems.forEach((i) => i.classList.remove("selected")); + itemDiv.classList.add("selected"); + selectedClassification.textContent = `${item.Identification}: ${item.Name}`; + selectedClassification.currentSelection = item.id; + assignClassificationButton.disabled = false; + }); + + container.appendChild(itemDiv); + }); + } + + function goBack() { + currentPath.pop(); + const parent = currentPath.length + ? currentPath[currentPath.length - 1].references + : data; + loadLevel(parent); + + if (currentPath.length === 0) { + backButton.disabled = true; + } + } + let currentPath = []; + let currentData = classificationElements; + + const container = document.createElement("div"); + container.id = "classificationView"; + container.classList.add("classificationView"); + formContainer.appendChild(container); + + if (!classificationElements || classificationElements.length === 0) { + console.log("No data available"); + const text = + "No data available - please activate a classification file with BonsaiBIM "; + const noDataMessage = this.Text( + text, + "fa-solid fa-exclamation-circle", + "large" + ); + const refreshButton = this.createButton( + "Enable Editing Classification", + "fa-solid fa-arrows-rotate" + ); + refreshButton.addEventListener("click", function (event) { + event.preventDefault(); + callbacks.enableEditingClassification(costItemId); + }); + container.appendChild(refreshButton); + container.appendChild(noDataMessage); + return; + } + + const rowContainer = document.createElement("div"); + rowContainer.classList.add("row-container"); + + const selectedClassificationText = this.Text( + "Selected Classification: ", + "fa-solid fa-check", + "medium" + ); + const selectedClassification = document.createElement("span"); + selectedClassificationText.classList.add("selectedClassificationText"); + selectedClassification.classList.add( + "selected", + "row-container", + "item-container" + ); + selectedClassification.id = "selectedClassification"; + selectedClassification.currentSelection = null; + selectedClassificationText.appendChild(selectedClassification); + rowContainer.appendChild(selectedClassificationText); + + const assignClassificationButton = this.createButton( + "Assign Classification", + "fa-solid fa-check" + ); + assignClassificationButton.addEventListener("click", function (event) { + event.preventDefault(); + if (selectedClassification.currentSelection) { + callbacks.addClassificationReference( + costItemId, + classificationName, + selectedClassification.currentSelection + ); + callbacks.enableEditingClassification(costItemId); + } + }); + + assignClassificationButton.disabled = true; + rowContainer.appendChild(assignClassificationButton); + formContainer.appendChild(rowContainer); + + const backButton = CostUI.createButton("Back", "fa-solid fa-arrow-left"); + backButton.classList.add("back-button"); + backButton.addEventListener("click", function (event) { + event.preventDefault(); + if (!backButton.disabled) { + goBack(); + } + }); + + formContainer.insertBefore(backButton, container); + backButton.disabled = true; + + loadLevel(currentData); + return container; + } } diff --git a/src/bonsai/bonsai/tool/web.py b/src/bonsai/bonsai/tool/web.py index 2596b9df2f..c398b20215 100644 --- a/src/bonsai/bonsai/tool/web.py +++ b/src/bonsai/bonsai/tool/web.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from ifcopenshell.util.classification import get_classification_data, get_references import ifcopenshell.util.cost import bpy from bonsai.bim.module.web.data import WebData @@ -42,6 +43,7 @@ from pathlib import Path import bonsai.core.sequence import bonsai.core.cost from ifc5d.ifc2json import ifc5D2json +from bonsai.bim.ifc import IfcStore sio = None ws_process = None @@ -499,6 +501,43 @@ class Web(bonsai.core.tool.Web): cost_schedule = tool.Cost.get_cost_schedule(cost_item) cls.load_cost_schedule_web_ui(cost_schedule) cls.load_cost_item_quantities_ui(cost_item) + if operator_data["type"] == "enableEditingClassification": + cost_item = ifc_file.by_id(operator_data["costItemId"]) + cost_classifications = [] + for reference in get_references(cost_item): + cost_classification_data = reference.get_info() + del cost_classification_data["ReferencedSource"] + cost_classifications.append(cost_classification_data) + classification_data, classification_name = get_classification_data(IfcStore.classification_file) + data = { + "classification_data": classification_data, + "classification_name": classification_name, + "cost_item_id": cost_item.id(), + "cost_classifications": cost_classifications, + } + cls.send_webui_data( + data=data, + data_key="classification", + event="classification", + ) + if operator_data["type"] == "removeClassificationReference": + cost_item = ifc_file.by_id(operator_data["costItemId"]) + cost_schedule = tool.Cost.get_cost_schedule(cost_item) + reference = ifc_file.by_id(operator_data["classificationId"]) + tool.Ifc.run("classification.remove_reference", products=[cost_item], reference=reference) + cls.load_cost_schedule_web_ui(cost_schedule) + if operator_data["type"] == "addClassificationReference": + cost_item = ifc_file.by_id(operator_data["costItemId"]) + cost_schedule = tool.Cost.get_cost_schedule(cost_item) + classification = None + classification_name = operator_data["classificationName"] + for element in tool.Ifc.get().by_type("IfcClassification"): + if element.Name == classification_name: + classification = element + break + reference = IfcStore.classification_file.by_id(operator_data["classificationId"]) + tool.Ifc.run("classification.add_reference", products=[cost_item], reference=reference, classification=classification) + cls.load_cost_schedule_web_ui(cost_schedule) @classmethod def load_cost_item_quantities_ui(cls, cost_item: ifcopenshell.entity_instance) -> None: diff --git a/src/ifc5d/ifc5d/ifc2json.py b/src/ifc5d/ifc5d/ifc2json.py index a94927f408..0f51187223 100644 --- a/src/ifc5d/ifc5d/ifc2json.py +++ b/src/ifc5d/ifc5d/ifc2json.py @@ -3,10 +3,11 @@ import ifcopenshell.util.unit from typing import Any import ifcopenshell.util.cost import ifcopenshell.util.date +from ifcopenshell.util.classification import get_references + CostItem = dict[str, Any] - class ifc5D2json: def __init__(self): self.json: str = None @@ -57,11 +58,21 @@ class ifc5D2json: data["id"] = cost_item.id() data["IsNestedBy"] = [] data["IsSum"] = self.check_if_cost_item_is_sum(cost_item) + data["Classification"] = self.get_cost_classifications(cost_item) json_data.append(data) for rel in cost_item.IsNestedBy or []: for sub_cost in rel.RelatedObjects: self.extract_cost_item_data(sub_cost, data["IsNestedBy"]) + def get_cost_classifications(self, cost_item: ifcopenshell.entity_instance) -> list: + results = [] + if cost_item: + for reference in get_references(cost_item): + data = reference.get_info() + del data["ReferencedSource"] + results.append(data) + return results + def check_if_cost_item_is_sum(self, cost_item: ifcopenshell.entity_instance) -> bool: cost_values = [] if cost_item.is_a("IfcCostItem"): diff --git a/src/ifcopenshell-python/ifcopenshell/util/classification.py b/src/ifcopenshell-python/ifcopenshell/util/classification.py index 42208a90e7..5d2f98809d 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/classification.py +++ b/src/ifcopenshell-python/ifcopenshell/util/classification.py @@ -65,3 +65,20 @@ def get_inherited_references(reference: Optional[ifcopenshell.entity_instance]) results.append(reference) reference = reference.ReferencedSource return results + +def get_classification_data(file: ifcopenshell.file) -> Optional[tuple[list[dict], str]]: + if not file or not file.by_type("IfcClassification"): + return [], "" + classification = file.by_type("IfcClassification")[0] + classification_name = classification.Name + + def process_references(reference): + data = reference.get_info() + del data["ReferencedSource"] + data["referenced_source"] = reference.ReferencedSource.id() if reference.ReferencedSource else None + data["has_references"] = bool(reference.HasReferences) + data["references"] = [process_references(ref) for ref in reference.HasReferences] if reference.HasReferences else [] + return data + + classification_data = [process_references(reference) for reference in classification.HasReferences] + return classification_data, classification_name \ No newline at end of file