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