cost web ui:

- add "Linked rate" column,
- assign a cost rate to multiple cost items by selecting linked rate cells & clicking the "assign" button on the chosen cost rate
#5369
This commit is contained in:
myoualid
2024-09-23 20:19:06 +01:00
parent ab8a6b80e4
commit 455012de4c
9 changed files with 267 additions and 200 deletions
@@ -1,58 +1,27 @@
:root.blender .flex-row {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.card-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* CSS for the work schedule cards */
:root.blender #work_schedules {
display: flex;
flex-wrap: wrap;
gap: 20px;
padding: 20px;
}
:root.blender .card {
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
width: 300px;
margin: 10px;
transition: transform 0.2s;
}
:root.blender .card:hover {
transform: scale(1.05);
}
:root.blender .card-title {
font-size: 1.25rem;
margin-bottom: 10px;
border-bottom: 1px solid #ddd;
padding: 10px;
}
:root.blender .card-text {
font-size: 1rem;
margin-bottom: 20px;
padding: 10px;
}
:root.blender .btn-primary {
background-color: #007bff;
border: none;
color: white;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 1rem;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.2s;
}
:root.blender .btn-primary:hover {
background-color: #0056b3;
}
.card {
background-color: var(--blender-even-row, var(--nav-bg-color));
display: flex;
flex-direction: column;
overflow-y: auto;
flex: 1;
}
.card-header {
background-color: var(--blender-panel-header, var(--bg-color));
border: 1px solid var(--blender-tab-outline, var(--border-color));
flex-shrink: 0;
}
.card-body {
background-color: var(--blender-panel-background, var(--bg-color));
border: 1px solid var(--blender-tab-outline, var(--border-color));
overflow-y: auto;
flex: 1;
}
@@ -392,7 +392,7 @@ form table {
opacity: 1;
}
.clickable-cell:hover {
.selected-cell, .clickable-cell:hover {
border: 2px solid var(--highlight-over);
cursor: pointer;
}
@@ -560,3 +560,10 @@ i {
background-color: #ccffcc;
color: darkgreen;
}
.card {
margin: 10px;
}
.card-body {
height: 10vh;
}
@@ -1,3 +1,5 @@
@import url("./components/card.css");
:root {
--font-family: Arial, sans-serif;
--base-font-size: 16px;
@@ -250,33 +252,7 @@ button:hover {
overflow: hidden;
}
.card-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.card {
background-color: var(--blender-even-row, var(--nav-bg-color));
display: flex;
flex-direction: column;
overflow-y: auto;
flex: 1;
}
.card-header {
background-color: var(--blender-panel-header, var(--bg-color));
border: 1px solid var(--blender-tab-outline, var(--border-color));
flex-shrink: 0;
}
.card-body {
background-color: var(--blender-panel-background, var(--bg-color));
border: 1px solid var(--blender-tab-outline, var(--border-color));
overflow-y: auto;
flex: 1;
}
li.svg-name {
list-style-type: none;
@@ -3,8 +3,6 @@ import { CostUI } from "./utilities/costui.js";
const connectedClients = {};
let socket;
let loadedSchedules = {};
$(document).ready(function () {
var defaultTheme = "blender";
var theme = localStorage.getItem("theme") || defaultTheme;
@@ -58,6 +56,19 @@ function handleEditQuantities(data) {
});
}
function assignCostValues(costRateId, callback){
const selectedCostItems = CostUI.getSelectedCostItems();
if (selectedCostItems.length === 0) {
return;
}
executeOperator({
type: "assignCostValues",
costRateId: costRateId,
costItemIds: selectedCostItems,
});
}
function addSumCostValue(costItemId) {
executeOperator({ type: "addSumCostValue", costItemId: costItemId });
}
@@ -276,9 +287,6 @@ function toggleClientList() {
function handleCostSchedulesData(data) {
const blenderId = data.blenderId;
const loadedSchedule = loadedSchedules[blenderId]
? loadedSchedules[blenderId]
: null;
const costSchedules = data.data["cost_schedules"]["cost_schedules"];
if (costSchedules.length === 0) {
return;
@@ -309,17 +317,10 @@ function handleCostSchedulesData(data) {
);
costScheduleDiv.append(card);
});
loadedSchedule ? CostUI.highlightElement("schedule-" + loadedSchedule) : null;
}
function handleCostItemsData(data) {
const costScheduleId = data.data["cost_items"]["schedule_data"][0]["id"];
const cards = document.querySelectorAll("[id^='schedule-']");
cards.forEach((card) => {
card.classList.remove("highlighted");
});
CostUI.highlightElement("schedule-" + costScheduleId);
loadedSchedules[data.blenderId] = costScheduleId;
const currency = data.data["cost_items"]["currency"]
? data.data["cost_items"]["currency"]["name"]
: "Undefined";
@@ -338,6 +339,7 @@ function handleCostItemsData(data) {
enableEditingQuantities: enableEditingQuantities,
addSumCostValue: addSumCostValue,
enableEditingClassification: enableEditingClassification,
assignCostValues: assignCostValues,
},
});
}
@@ -70,66 +70,78 @@ export class CostUI {
handleButtonClick(editCostValuesButton, callbacks.enableEditingCostValues);
handleButtonClick(addButton, callbacks.addCostItem);
handleButtonClick(deleteCostItemButton, callbacks.deleteCostItem);
addTableListeners(callbacks);
function addTableListeners(callbacks) {
function getColumnNames(tableId) {
const table = document.getElementById(tableId);
const headerRow = table.querySelector("thead tr");
return Array.from(headerRow.children).map((headerCell) =>
headerCell.textContent.trim()
);
}
document
.getElementById("cost-items")
.addEventListener("click", function (event) {
const targetRow = event.target.closest("tr");
const targetCell = event.target.closest("td");
if (targetRow && targetCell) {
const columnIndex = Array.from(targetRow.children).indexOf(
targetCell
);
const tableId = targetRow.parentElement.parentElement.id;
const costItemId = parseInt(targetRow.getAttribute("id"));
const columnName = getColumnNames(tableId)[columnIndex];
if (
(columnName.includes("Cost") || columnName.includes("Rate")) &&
!columnName.includes("Total")
) {
callbacks.enableEditingCostValues
? callbacks.enableEditingCostValues(costItemId)
: null;
}
if (columnName === "Quantity") {
callbacks.enableEditingQuantities
? callbacks.enableEditingQuantities(costItemId)
: null;
}
if (columnName === "Classification") {
callbacks.enableEditingClassification
? callbacks.enableEditingClassification(costItemId)
: null;
}
}
});
}
}
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 addTableCallbacks(callbacks, table) {
if (table.listenersAdded) {
return;
}
function getColumnNames(tableId) {
const table = document.getElementById(tableId);
const headerRow = table.querySelector("thead tr");
return Array.from(headerRow.children).map((headerCell) =>
headerCell.textContent.trim()
);
}
function addTableListeners(callbacks) {
table.addEventListener("click", function (event) {
const targetRow = event.target.closest("tr");
const targetCell = event.target.closest("td");
if (targetRow && targetCell) {
const columnIndex = Array.from(targetRow.children).indexOf(
targetCell
);
const tableId = table.id;
const costItemId = parseInt(targetRow.getAttribute("id"));
const columnName = getColumnNames(tableId)[columnIndex];
if (
(columnName.includes("Cost") || columnName.includes("Rate")) &&
!columnName.includes("Total") &&
!columnName.includes("Linked")
) {
callbacks.enableEditingCostValues
? callbacks.enableEditingCostValues(costItemId)
: null;
}
if (columnName === "Quantity") {
callbacks.enableEditingQuantities
? callbacks.enableEditingQuantities(costItemId)
: null;
}
if (columnName === "Classification") {
callbacks.enableEditingClassification
? callbacks.enableEditingClassification(costItemId)
: null;
}
if (columnName === "Linked Rate") {
if (!targetRow.isRate) {
if (targetCell.classList.contains("selected-cell")) {
document.selectedCostItems = document.selectedCostItems.filter(
(costItem) => costItem !== targetRow.id
);
targetCell.classList.remove("selected-cell");
} else {
document.selectedCostItems.push(targetRow.id);
targetCell.classList.add("selected-cell");
}
}
}
}
});
}
if (!document.selectedCostItems) {
document.selectedCostItems = [];
}
document.selectedCostItems = [];
addTableListeners(callbacks);
table.listenersAdded = true;
}
static addSettingsMenu() {
@@ -154,7 +166,14 @@ export class CostUI {
static createColumnSelector() {
const columnSelector = document.createElement("div");
columnSelector.id = "column-selector";
const columns = ["ID", "Quantity", "Unit", "Classification", "Actions"];
const columns = [
"Identification",
"Quantity",
"Unit",
"Classification",
"Actions",
"Linked Rate",
];
columns.forEach((column) => {
const label = document.createElement("label");
const input = document.createElement("input");
@@ -181,6 +200,23 @@ export class CostUI {
localStorage.setItem("columnPreferences", JSON.stringify(preferences));
}
static getColumnPreferences() {
const preferences = localStorage.getItem("columnPreferences");
return preferences
? JSON.parse(preferences)
: {
Identification: true,
Name: true,
Quantity: true,
Unit: true,
Cost: true,
TotalCost: true,
Classification: true,
CostRate: true,
Actions: true,
};
}
static updateColumnVisibility(preferences) {
const tables = document.querySelectorAll("table[id^='cost-items-']");
tables.forEach((table) => {
@@ -352,6 +388,7 @@ export class CostUI {
currency,
callbacks,
});
this.addTableCallbacks(callbacks, table);
if (costSchedule["cost_items"].length === 0) {
const tr = document.createElement("tr");
const td = document.createElement("td");
@@ -439,11 +476,8 @@ export class CostUI {
const cells = row.querySelectorAll("td, th");
let rowText = [];
console.log();
if (CostUI.getCostItemRow(row.id)) {
const costItem = CostUI.getCostItemRow(row.id);
console.log(costItem.nestingLevel);
rowText.push(costItem.nestingLevel);
} else {
rowText.push("Hierarchy Level");
@@ -510,7 +544,6 @@ export class CostUI {
const callback = () => {
tableWrapper.remove();
CostUI.unhighlightElement("schedule-" + id);
};
let closeButton = CostUI.createCloseButton(callback);
tableHeader.appendChild(text);
@@ -520,26 +553,36 @@ export class CostUI {
const table = document.createElement("table");
table.id = "cost-items-" + id;
const tbody = document.createElement("tbody");
const columnHeaders = [
{ name: "ID", visible: preferences.ID },
let columnHeaders = [
{ name: "Identification", visible: preferences.Identification },
{ 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: "Linked Rate", visible: preferences.CostRate },
{ 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,
};
columnHeaders = columnHeaders.filter(
(header) => header.name !== "Quantity"
);
columnHeaders = columnHeaders.filter(
(header) => header.name !== "Linked Rate"
);
columnHeaders = columnHeaders.map((header) => {
if (header.name === "Unit") {
return { name: "Rate (" + currency + ")", visible: preferences.Cost };
} else if (header.name === "Cost (" + currency + ")") {
return {
name: "Total Rate (" + currency + ")",
visible: preferences.TotalCost,
};
}
return header;
});
}
const thead = document.createElement("thead");
const tr = document.createElement("tr");
@@ -597,9 +640,9 @@ export class CostUI {
const identification = costItem.Identification
? costItem.Identification
: "XXX";
if (preferences.ID) {
if (preferences.Identification) {
const idCell = CostUI.createTableCell(identification);
idCell.setAttribute("data-column", "ID");
idCell.setAttribute("data-column", "Identification");
row.appendChild(idCell);
}
@@ -633,7 +676,11 @@ export class CostUI {
totalCostQuantityCell.classList.add("clickable-cell");
}
if (preferences.Unit) {
const unitSymbolCell = CostUI.createTableCell(costItem.UnitSymbol);
let symbol = costItem.UnitSymbol;
if (symbol === "-" && costItem.UnitBasisUnitSymbol) {
symbol = costItem.UnitBasisUnitSymbol;
}
const unitSymbolCell = CostUI.createTableCell(symbol);
unitSymbolCell.setAttribute("data-column", "Unit");
row.appendChild(unitSymbolCell);
}
@@ -670,8 +717,28 @@ export class CostUI {
ClassificationCell.textContent = string_List;
}
let costRateName = " ";
let costItemRateId;
if (costItem.CostRate.id !== null) {
costItemRateId = costItem.CostRate.id;
const costItemRateName = costItem.CostRate.Name;
const costItemRateIdentification = costItem.CostRate.Identification;
costRateName = costItemRateIdentification + " - " + costItemRateName;
}
if (preferences.CostRate && !isScheduleOfRates) {
const costRateCell = CostUI.createTableCell(costRateName);
costRateCell.classList.add("rate-cell", "clickable-cell");
costRateCell.setAttribute("data-column", "Linked Rate");
row.appendChild(costRateCell);
}
if (preferences.Actions) {
const actionsCell = CostUI.costItemActions(costItem, callbacks);
const actionsCell = CostUI.costItemActions(
costItem,
isScheduleOfRates,
callbacks
);
actionsCell.setAttribute("data-column", "Actions");
actionsCell.classList.add("actions-column");
row.appendChild(actionsCell);
@@ -946,7 +1013,12 @@ export class CostUI {
return div;
}
static costItemActions(costItem, callbacks) {
static getSelectedCostItems() {
const selectedCostItems = document.selectedCostItems || [];
return selectedCostItems.map((item) => parseInt(item, 10));
}
static costItemActions(costItem, isScheduleOfRates, callbacks) {
const divFlex = document.createElement("div");
divFlex.classList.add("row-container");
const addCostItem = CostUI.addCostItemButton(
@@ -965,12 +1037,22 @@ export class CostUI {
costItem.id,
callbacks.duplicateCostItem
);
[addCostItem, duplicateButton, selectButton, deleteButton].forEach(
(button) => {
divFlex.appendChild(button);
}
const assignCostItemButton = CostUI.createButton(
"Assign",
"fa-solid fa-link"
);
assignCostItemButton.addEventListener("click", function () {
callbacks.assignCostValues(costItem.id);
});
let buttons = [addCostItem, duplicateButton, selectButton, deleteButton];
if (isScheduleOfRates) {
buttons.push(assignCostItemButton);
}
buttons.forEach((button) => {
divFlex.appendChild(button);
});
const actionsCell = document.createElement("td");
actionsCell.appendChild(divFlex);
@@ -1107,12 +1189,13 @@ export class CostUI {
return success;
}
static createCard(id, title, mainContainer, callback) {
static createCard(id, title, cardBody, callback) {
const card = document.createElement("div");
card.classList.add("card");
card.id = "schedule-" + id;
const cardBody = document.createElement("div");
const cardContainer = document.createElement("div");
cardContainer.classList.add("card-body");
cardBody.classList.add("card-body");
const cardTitle = CostUI.Text(
@@ -1120,15 +1203,16 @@ export class CostUI {
"fa-solid fa-money-bill-wave",
"large"
);
cardTitle.classList.add("card-title");
cardTitle.classList.add("card-header");
const cardButton = CostUI.createButton("Load", "fa-solid fa-arrows-rotate");
cardButton.addEventListener("click", callback);
cardTitle.appendChild(cardButton);
cardBody.appendChild(cardTitle);
cardBody.appendChild(mainContainer);
cardBody.appendChild(cardButton);
card.appendChild(cardBody);
card.appendChild(cardTitle);
card.appendChild(cardContainer);
cardContainer.appendChild(cardBody);
return card;
}
@@ -2714,7 +2798,6 @@ export class CostUI {
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(
@@ -2738,12 +2821,6 @@ export class CostUI {
"click",
function (event) {
event.preventDefault();
console.log(
"Remove classification reference from",
costItemId,
"Classificaition Id",
classification.id
);
callbacks.removeClassificationReference(
costItemId,
classification.id
@@ -2819,7 +2896,6 @@ export class CostUI {
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(
+1 -5
View File
@@ -667,11 +667,7 @@ class Cost(bonsai.core.tool.Cost):
@classmethod
def get_cost_schedule(cls, cost_item: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
for rel in cost_item.HasAssignments or []:
if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcCostSchedule"):
return rel.RelatingControl
for rel in cost_item.Nests or []:
return cls.get_cost_schedule(rel.RelatingObject)
return ifcopenshell.util.cost.get_cost_schedule(cost_item)
@classmethod
def is_cost_schedule_active(cls, cost_schedule: ifcopenshell.entity_instance) -> bool:
+7
View File
@@ -594,6 +594,13 @@ class Web(bonsai.core.tool.Web):
data_key="message",
event="message",
)
if operator_data["type"] == "assignCostValues":
cost_rate = ifc_file.by_id(operator_data["costRateId"])
cost_schedule = tool.Cost.get_cost_schedule(ifc_file.by_id(operator_data["costItemIds"][0]))
for cost_item_id in operator_data["costItemIds"]:
cost_item = ifc_file.by_id(cost_item_id)
tool.Ifc.run("cost.assign_cost_value", cost_item=cost_item, cost_rate=cost_rate)
cls.load_cost_schedule_web_ui(cost_schedule)
@classmethod
def load_cost_item_quantities_ui(cls, cost_item: ifcopenshell.entity_instance) -> None:
+6
View File
@@ -55,11 +55,17 @@ class ifc5D2json:
self.extract_cost_item_quantities(cost_item, data)
self.populate_cost_values(cost_item, data)
self.populate_nesting_index(cost_item, data)
cost_rate = ifcopenshell.util.cost.get_cost_rate(self.file, cost_item)
data = {**data, **cost_item.get_info(recursive=True)}
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)
data["CostRate"] = {
"id": cost_rate.id() if cost_rate else None,
"Identification": cost_rate.Identification if cost_rate else None,
"Name": cost_rate.Name if cost_rate else None,
}
json_data.append(data)
for rel in cost_item.IsNestedBy or []:
for sub_cost in rel.RelatedObjects:
@@ -307,6 +307,34 @@ def get_product_quantity_names(elements: list[ifcopenshell.entity_instance]) ->
return [n for n in names if n != "id"]
def get_cost_schedule(cost_item: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
"""Returns the cost schedule of a cost item.
"""
for rel in cost_item.HasAssignments or []:
if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcCostSchedule"):
return rel.RelatingControl
for rel in cost_item.Nests or []:
return get_cost_schedule(rel.RelatingObject)
def get_cost_rate(
file: ifcopenshell_wrapper.file, cost_item: ifcopenshell.entity_instance
) -> Optional[ifcopenshell.entity_instance]:
"""Returns the cost rate of a cost item.
"""
# There is no direct relationship between a cost item and a cost rate in IFC, so we need to infer it, based on the assumption that cost_rate.CostValues == cost_item.CostValues.
if get_cost_schedule(cost_item).PredefinedType == "SCHEDULEOFRATES":
return None # Cost item is already a cost rate
if cost_item.CostValues:
potential_rates = file.get_inverse(cost_item.CostValues[0])
for potential_rate in potential_rates:
schedule = get_cost_schedule(potential_rate)
if schedule.PredefinedType == "SCHEDULEOFRATES":
return potential_rate
return None
class CostValueUnserialiser:
def parse(self, formula: str):
l = lark.Lark(