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:
myoualid
2024-09-22 20:00:09 +01:00
parent a053472558
commit 9f542c30b3
8 changed files with 760 additions and 217 deletions
@@ -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;
}
}
+39
View File
@@ -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:
+12 -1
View File
@@ -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