mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 09:48:32 +00:00
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
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -223,6 +223,7 @@ button:hover {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.column-container {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 = `
|
||||
<button class="action-button" id="selected-products-btn">Selected Products</button>
|
||||
<button class="action-button" id="assigned-products-btn">Assigned Products</button>
|
||||
<button class="action-button" id="manual-quantities-btn">Manual Quantities</button>
|
||||
`;
|
||||
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 = `
|
||||
<button class="action-button" id="selected-products-btn">Selected Products</button>
|
||||
<button class="action-button" id="assigned-products-btn">Assigned Products</button>
|
||||
<button class="action-button" id="manual-quantities-btn">Manual Quantities</button>
|
||||
`;
|
||||
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 = `<span>${item.Identification}: ${item.Name}</span>`;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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:
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user