- Add button to generate cost schedule

- Feature to view, edit and add cost item values. ( Right Click - Edit ).
This commit is contained in:
Yassine Oualid
2024-09-01 01:40:53 +01:00
parent 53a78f38e5
commit f2554bc968
15 changed files with 616 additions and 127 deletions
@@ -166,6 +166,16 @@ class BlenderNamespace(socketio.AsyncNamespace):
blender_messages[sid]["cost_schedules"] = data
await sio.emit("cost_schedules", {"blenderId": sid, "data": data}, namespace="/web")
async def on_cost_values(self, sid, data):
print(f"Cost values from Blender client {sid}")
blender_messages[sid]["cost_values"] = data
await sio.emit("cost_values", {"blenderId": sid, "data": data}, namespace="/web")
async def on_cost_value(self, sid, data):
print(f"Cost values from Blender client {sid}")
blender_messages[sid]["cost_value"] = data
await sio.emit("cost_value", {"blenderId": sid, "data": data}, namespace="/web")
async def schedules(request):
with open("templates/index.html", "r") as f:
template = f.read()
@@ -0,0 +1,89 @@
th {
position: relative;
}
.resizer {
position: absolute;
right: 0;
top: 0;
width: 5px;
height: 100%;
cursor: col-resize;
user-select: none;
}
.context-menu {
display: none;
position: absolute;
background-color: rgb(51, 51, 51);
border: 1px solid #ccc;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 1000;
color: white;
}
.context-menu button {
display: block;
width: 100%;
padding: 8px;
border: none;
background: none;
text-align: left;
cursor: pointer;
}
.context-menu button:hover {
background-color: #f0f0f0;
color: black;
}
/* Context menu button is clicked, should zoom into object and change color */
.context-menu button:active {
background-color: #ff9634;
color: rgba(37, 51, 77, 0.466);
/* zoom */
transform: scale(0.9);
}
#cost-items tr:hover {
background-color: #f0f0f0;
color : black;
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
background-color: #363636;
padding: 10px;
}
.cost-values-form {
position: absolute; /* Set initial position to absolute */
background-color: white;
border: 1px solid black;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
z-index: 9999;
cursor: move; /* Change cursor to move */
}
.cost-values-table {
width: 100%;
border-collapse: collapse;
}
.cost-values-table th, .cost-values-table td {
border: 1px solid #ddd;
padding: 8px;
}
.cost-values-table th {
background-color: #f2f2f2;
text-align: left;
}
.close-button {
position: absolute;
top: 10px;
right: 10px;
font-size: 20px;
cursor: pointer;
}
@@ -1,4 +1,5 @@
@import url("./components/card.css");
@import url("./components/contextMenu.css");
:root {
--font-family: Arial, sans-serif;
@@ -23,6 +23,8 @@ function connectSocket() {
socket.on("connect", handleWebConnect);
socket.on("cost_schedules", handleCostSchedulesData);
socket.on("cost_items", handleCostItemsData);
socket.on("cost_values", handleCostValuesData);
socket.on("cost_value", handleCostValueData);
}
function handleBlenderConnect(blenderId) {
@@ -46,8 +48,20 @@ function handleBlenderDisconnect(blenderId) {
});
}
function removeTableElement(blenderId) {
$("#cost-items-" + blenderId).remove();
}
function handleCostValueData(data) {
const costItemId = data.data["cost_value"]["cost_item_id"];
const costValueId = data.data["cost_value"]["cost_value_id"];
console.log("Handling cost value data", costItemId, costValueId);
CostUI.addNewCostValueRow(costItemId, costValueId);
}
function handleConnectedClients(data) {
$("#blender-count").text(data.length);
@@ -56,6 +70,29 @@ function handleConnectedClients(data) {
});
}
function handleCostValuesData(data) {
CostUI.createCostValuesForm({
costValues: data.data["cost_values"]["cost_values"],
costItemId: data.data["cost_values"]["cost_item_id"],
callbacks: {
'editCostValues': editCostValues,
'addCostValue': addCostValue,
},
});
}
function addCostValue(costItemId) {
executeOperator({ type: "addCostValue", costItemId: costItemId });
}
function editCostValues(costItemId, costValues) {
executeOperator({
type: "editCostValues",
costItemId: costItemId,
costValues: costValues
});
}
function handleThemeData(themeData) {
function arrayToRgbString(arr) {
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
@@ -99,7 +136,6 @@ function setTheme(theme) {
}
function addCostItem(costItemId) {
console.log("addCostItem", costItemId);
executeOperator({ type: "addCostItem", costItemId: costItemId });
}
@@ -119,11 +155,7 @@ function handleCostSchedulesData(data) {
const blenderId = data.blenderId;
const costSchedules = data.data["cost_schedules"]["cost_schedules"];
const currency = data.data["cost_schedules"]["currency"]["name"];
console.log(data.data["cost_schedules"]);
const costScheduleDiv = $("#cost-schedules");
costSchedules.forEach((costSchedule) => {
costSchedule.UpdateDate = new Date(costSchedule.UpdateDate);
const mainContainer = CostUI.text("Updated On: " + costSchedule.UpdateDate);
@@ -136,7 +168,6 @@ function handleCostSchedulesData(data) {
}
function handleCostItemsData(data) {
console.log(data);
CostUI.createCostSchedule({
data: data.data["cost_items"],
blenderID: data.blenderId,
@@ -144,6 +175,7 @@ function handleCostItemsData(data) {
"addCostItem": addCostItem,
"selectAssignedElements": selectAssignedElements,
'editCostItemName': editCostItemName,
'enableEditingCostValues': enableEditingCostValues,
}
});
}
@@ -166,3 +198,7 @@ function loadCostSchedule(costScheduleId, blenderId) {
function getCostSchedules(blenderId) {
executeOperator({ type: "getCostSchedules" }, blenderId);
}
function enableEditingCostValues(costItemId) {
executeOperator({ type: "enableEditingCostValues", costItemId: costItemId});
}
@@ -82,7 +82,6 @@ function handleBlenderDisconnect(blenderId) {
function handleConnectedClients(data) {
$("#blender-count").text(data.length);
// console.log(data);
data.forEach(function (id) {
connectedClients[id] = {
shown: false,
@@ -115,7 +114,6 @@ function handleThemeData(themeData) {
}
const cssRule = generateCssVariableRule(themeData.theme);
console.log(cssRule);
var styleElement = $("#gantt-stylesheet")[0];
if (styleElement) {
@@ -133,7 +131,6 @@ function handleWorkScheduleData(data) {
const workSchedules = data["data"]["work_schedule_info"];
workSchedules.forEach((workSchedule) => {
console.log(workSchedule);
const mainContainer = CostUI.text(new Date(workSchedule.CreationDate).toLocaleDateString());
const callback = () => loadWorkSchedule(workSchedule.id);
const card = CostUI.createCard(workSchedule.Name,mainContainer, callback);
@@ -142,11 +139,8 @@ function handleWorkScheduleData(data) {
}
function handleGanttData(data) {
console.log("running handleGanttData");
const blenderId = data["blenderId"];
console.log(data);
const filename = data["data"]["ifc_file"];
const ganttTasks = data["data"]["gantt_data"]["tasks"];
const ganttWorkSched = data["data"]["gantt_data"]["work_schedule"];
@@ -183,7 +177,6 @@ function handleDefaultData(data) {
const blenderId = data["blenderId"];
const isDirty = data["data"]["is_dirty"];
showWarning(blenderId, isDirty);
console.log('default data',data);
}
// Function to add a new gantt with data and filename
@@ -1,14 +1,6 @@
export class CostUI {
constructor() {}
createButton() {
console.log("Button created");
}
createInput() {
console.log("Input created");
}
static isCostScheduleLoaded(id) {
const existingTable = document.getElementById('cost-items-' + id);
return existingTable !== null;
@@ -17,7 +9,7 @@ export class CostUI {
static removeCostSchedule(id) {
document.getElementById("cost-items-" + id).remove();
}
static createTable(id) {
static createTable(id, callbacks) {
CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null;
const table = document.createElement("table");
@@ -51,7 +43,7 @@ export class CostUI {
CostUI.addTableStyles(id);
// Create context menu
CostUI.createContextMenu();
CostUI.createContextMenu(callbacks);
table.get_blender_id = function() {
return this.getAttribute("id").split("-")[2];
@@ -71,52 +63,19 @@ export class CostUI {
#cost-items-${id} td:not(:nth-child(1)) {
width: 100px; /* Set a fixed width for other columns */
}
th {
position: relative;
}
.resizer {
position: absolute;
right: 0;
top: 0;
width: 5px;
height: 100%;
cursor: col-resize;
user-select: none;
}
.context-menu {
display: none;
position: absolute;
background-color: white;
border: 1px solid #ccc;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 1000;
}
.context-menu button {
display: block;
width: 100%;
padding: 8px;
border: none;
background: none;
text-align: left;
cursor: pointer;
}
.context-menu button:hover {
background-color: #f0f0f0;
}
#cost-items tr:hover {
background-color: #f0f0f0; /* Change this color to your desired hover color */
}
`;
document.head.appendChild(style);
}
static createContextMenu() {
static createContextMenu(callbacks) {
// Create context menu
const contextMenu = document.createElement("div");
contextMenu.id = "context-menu";
contextMenu.classList.add("context-menu");
contextMenu.innerHTML = `
<button id="edit-button">Edit</button>
<button id="add-cost-item-button">Add sub-cost</button>
<button id="edit-cost-values-button">Edit</button>
<button id="delete-button">Delete</button>
<button id="duplicate-button">Duplicate</button>
`;
@@ -126,6 +85,7 @@ export class CostUI {
document.addEventListener("contextmenu", function(event) {
event.preventDefault();
const targetRow = event.target.closest("tr");
const targetCell = event.target.closest("td");
if (targetRow && targetRow.parentElement.id === "cost-items") {
const contextMenu = document.getElementById("context-menu");
contextMenu.style.display = "block";
@@ -134,6 +94,7 @@ export class CostUI {
// Store the target row in the context menu for later use
contextMenu.targetRow = targetRow;
contextMenu.targetCell = targetCell;
} else {
document.getElementById("context-menu").style.display = "none";
}
@@ -146,14 +107,32 @@ export class CostUI {
}
});
document.getElementById("edit-button").addEventListener("click", function() {
const editCostValuesButton = document.getElementById("edit-cost-values-button");
editCostValuesButton.addEventListener("click", function() {
console.log("Edit cost values executed");
const targetRow = document.getElementById("context-menu").targetRow;
const targetCell = document.getElementById("context-menu").targetCell;
if (targetRow) {
// Implement your edit action here
console.log("Edit row:", targetRow.getAttribute("id"));
const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.enableEditingCostValues ? callbacks.enableEditingCostValues(costItemId) : null;
}
});
const addButton = document.getElementById("add-cost-item-button");
if (!addButton.dataset.listenerAdded) {
function addCostItemHandler(e) {
e.stopPropagation();
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.addCostItem ? callbacks.addCostItem(costItemId) : null;
}
document.getElementById("context-menu").style.display = "none";
}
addButton.addEventListener("click", addCostItemHandler);
addButton.dataset.listenerAdded = "true";
}
document.getElementById("delete-button").addEventListener("click", function() {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
@@ -246,7 +225,7 @@ export class CostUI {
}
static createCostSchedule({ data, blenderID, title, callbacks = {} }) {
const [table, tbody] = CostUI.createTable(blenderID);
const [table, tbody] = CostUI.createTable(blenderID, callbacks);
CostUI.createCostItem(data, tbody, 0, null, callbacks);
CostUI.applyExpandedState();
}
@@ -263,6 +242,35 @@ export class CostUI {
}
static createRow(obj, nestingLevel, parentID, callbacks = {}) {
const row = CostUI.createTableRow(obj, nestingLevel, parentID);
const expandButton = CostUI.createExpandButton(obj);
const nameCell = CostUI.createNameCell(obj, nestingLevel, expandButton, callbacks);
const totalCostQuantityCell = CostUI.createTableCell(obj.TotalCostQuantity);
const unitSymbolCell = CostUI.createTableCell(obj.UnitSymbol);
const totalAppliedValueCell = CostUI.createTableCell(obj.TotalAppliedValue);
const totalCostCell = CostUI.createTotalCostCell(obj);
const flexContainerCell = CostUI.createFlexContainerCell(obj, callbacks);
row.appendChild(nameCell);
row.appendChild(totalCostQuantityCell);
row.appendChild(unitSymbolCell);
row.appendChild(totalAppliedValueCell);
row.appendChild(totalCostCell);
row.appendChild(flexContainerCell);
row.get_id = function() {
return this.getAttribute("id");
};
row.get_parent = function() {
const parentId = this.getAttribute("parent-id");
return parentId ? document.getElementById(parentId) : null;
};
return row;
}
static createTableRow(obj, nestingLevel, parentID) {
const row = document.createElement("tr");
row.setAttribute("id", obj.id);
row.setAttribute("parent-id", parentID);
@@ -270,6 +278,10 @@ export class CostUI {
row.classList.add("nested");
row.classList.add(`level-${nestingLevel}`);
}
return row;
}
static createExpandButton(obj) {
const expandButton = document.createElement("button");
expandButton.classList.add("toggle-button");
if (obj.is_nested_by && obj.is_nested_by.length > 0) {
@@ -277,51 +289,62 @@ export class CostUI {
} else {
expandButton.style.visibility = "hidden";
}
//row.appendChild(expandButton);
expandButton.addEventListener("click", function() {
CostUI.contractExpandRow.call(this, obj.id);
});
return expandButton;
}
static createNameCell(obj, nestingLevel, expandButton, callbacks) {
const nameCell = document.createElement("td");
const nameInput = document.createElement("input");
nameInput.value = obj.name ? obj.name : "Unnamed";
nameInput.addEventListener("change", function() {
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
});
nameInput.addEventListener("keydown", function(event) {
if (event.key === "Enter") {
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
}
});
nameCell.style.paddingLeft = nestingLevel * 20 + "px";
nameCell.appendChild(expandButton);
nameCell.appendChild(nameInput);
row.appendChild(nameCell);
const totalCostQuantityCell = document.createElement("td");
totalCostQuantityCell.textContent = obj.TotalCostQuantity;
row.appendChild(totalCostQuantityCell);
const unitSymbolCell = document.createElement("td");
unitSymbolCell.textContent = obj.UnitSymbol;
row.appendChild(unitSymbolCell);
const totalAppliedValueCell = document.createElement("td");
totalAppliedValueCell.textContent = obj.TotalAppliedValue;
row.appendChild(totalAppliedValueCell);
return nameCell;
}
static createTableCell(content) {
const cell = document.createElement("td");
cell.textContent = content;
return cell;
}
static createTotalCostCell(obj) {
const totalCostCell = document.createElement("td");
const totalCost = parseFloat(obj.TotalCost).toFixed(2);
totalCostCell.textContent = obj.is_sum ? totalCost + " (Σ)" : totalCost;
row.appendChild(totalCostCell);
return totalCostCell;
}
static createFlexContainerCell(obj, callbacks) {
const divFlex = document.createElement("div");
divFlex.classList.add("flex-container");
const addButton = CostUI.createAddButton(obj, callbacks);
const selectButton = CostUI.createSelectButton(obj, callbacks);
divFlex.appendChild(addButton);
divFlex.appendChild(selectButton);
const flexContainerCell = document.createElement("td");
flexContainerCell.appendChild(divFlex);
return flexContainerCell;
}
static createAddButton(obj, callbacks) {
const addButton = document.createElement("button");
addButton.textContent = "+";
addButton.classList.add("add-button");
@@ -329,31 +352,17 @@ export class CostUI {
e.stopPropagation();
callbacks.addCostItem ? callbacks.addCostItem(obj.id) : null;
});
return addButton;
}
static createSelectButton(obj, callbacks) {
const selectButton = document.createElement("button");
selectButton.textContent = "Select";
selectButton.addEventListener("click", function(e) {
e.stopPropagation();
callbacks.selectAssignedElements ? callbacks.selectAssignedElements(obj.id) : null;
});
divFlex.appendChild(addButton);
divFlex.appendChild(selectButton);
const flexContainerCell = document.createElement("td");
flexContainerCell.appendChild(divFlex);
row.appendChild(flexContainerCell);
row.get_id = function() {
return this.getAttribute("id");
};
row.get_parent = function() {
const parentId = this.getAttribute("parent-id");
return parentId ? document.getElementById(parentId) : null;
};
return row;
return selectButton;
}
static hideNestedRows(parentId) {
@@ -461,4 +470,267 @@ export class CostUI {
return card;
}
static createCostValuesForm({ costItemId, costValues, callbacks }) {
// Check if the form already exists and remove it if it does
const formId = "cost-values-form-" + costItemId;
let existingForm = document.getElementById(formId);
if (existingForm) {
existingForm.remove();
}
const form = CostUI.createFormElement(formId, "cost-values-form");
const header = CostUI.createHeader("Cost Values Form");
form.appendChild(header);
const table = CostUI.createCostValuesTable(costItemId, ["Type", "Category", "Value"]);
costValues.forEach(costValue => {
const tr = CostUI.createCostvaluesRow(costItemId, costValue);
table.appendChild(tr);
});
form.appendChild(table);
const addButton = CostUI.createAddCostValueButton(costItemId, callbacks);
form.appendChild(addButton);
const submitButton = CostUI.createSubmitButton(callbacks);
form.appendChild(submitButton);
const closeButton = CostUI.createCloseButton(form);
form.appendChild(closeButton);
CostUI.makeHeaderDraggable(header, form);
document.body.appendChild(form);
return form;
}
static getCostValuesTable(costItemId) {
return document.getElementById("cost-values-table-" + costItemId);
}
static addNewCostValueRow(costItemId, costValueId) {
const table = CostUI.getCostValuesTable(costItemId);
// check if table exists
if (!table) {
console.log("Cost values table not found for cost item ID:", costItemId);
return;
}
console.log("Adding new cost value row", costValueId);
console.log(table)
const tr = CostUI.createCostvaluesRow(costItemId, {"category": "", "name": "", "applied_value": 0, "id": costValueId, "parent": costItemId});
table.appendChild(tr);
}
static createCostValuesTable(costItemId, headers) {
const table = document.createElement("table");
table.classList.add("cost-values-table");
table.id = "cost-values-table-" + costItemId;
const headerRow = document.createElement("tr");
headers.forEach(headerText => {
const th = document.createElement("th");
th.textContent = headerText;
headerRow.appendChild(th);
});
table.appendChild(headerRow);
return table;
}
static createCostvaluesRow(costItemId,costValue) {
function cleanLabel(value) {
// remove anything which is not a dot or a digit
return value.replace(/[^0-9.]/g, '');
}
if (document.getElementById(costValue.id)) {
return document.getElementById(costValue.id);
}
const tr = document.createElement("tr");
tr.isCostValue = true;
tr.parent = costItemId;
tr.id = costValue.id;
let costType = "FIXED";
if (costValue.category === "*") {
costType = "SUM";
}
else if (costValue.category && costValue.category !== "*") {
costType = "CATEGORY";
}
else if (costValue.applied_value) {
costType = "FIXED";
}
const typeCell = CostUI.createTableDropdown("type", costType);
const dropdown = typeCell.querySelector("select");
dropdown.addEventListener("change", function() {
const selectedType = this.value;
CostUI.updateRowBasedOnType(selectedType, categoryCell, valueCell1);
});
tr.appendChild(typeCell);
const categoryCell = CostUI.createTableInput("text", "category", costValue.category);
tr.appendChild(categoryCell);
let value
if (costValue.category === "*"){
value = cleanLabel(costValue.label)
}
else {
value = costValue.applied_value
}
const valueCell1 = CostUI.createTableInput("number", "value", value);
tr.appendChild(valueCell1);
// Apply initial state based on costType
CostUI.updateRowBasedOnType(costType, categoryCell, valueCell1);
return tr;
}
static updateRowBasedOnType(type, categoryCell, valueCell1) {
if (type === "FIXED") {
categoryCell.querySelector("input").disabled = true;
categoryCell.querySelector("input").style.backgroundColor = "lightgrey";
valueCell1.querySelector("input").disabled = false;
valueCell1.querySelector("input").style.backgroundColor = "";
} else if (type === "CATEGORY") {
categoryCell.querySelector("input").disabled = false;
categoryCell.querySelector("input").style.backgroundColor = "";
valueCell1.querySelector("input").disabled = false;
valueCell1.querySelector("input").style.backgroundColor = "";
} else if (type === "SUM") {
categoryCell.querySelector("input").disabled = true;
categoryCell.querySelector("input").style.backgroundColor = "lightgrey";
valueCell1.querySelector("input").disabled = true;
valueCell1.querySelector("input").style.backgroundColor = "lightgrey";
}
}
static createTableDropdown(name, value="FIXED") {
const cell = document.createElement("td");
const dropdown = document.createElement("select");
dropdown.name = name;
const options = ["FIXED", "CATEGORY", "SUM"];
options.forEach(optionValue => {
const option = document.createElement("option");
option.value = optionValue;
option.textContent = optionValue;
if (optionValue === value) {
option.selected = true;
}
dropdown.appendChild(option);
});
cell.appendChild(dropdown);
return cell;
}
static createTableInput(type, name, value) {
const cell = document.createElement("td");
const input = document.createElement("input");
input.type = type;
input.name = name;
input.value = value;
cell.appendChild(input);
return cell;
}
static createAddCostValueButton(costItemId, callbacks) {
const addButton = document.createElement("button");
addButton.textContent = "+";
addButton.classList.add("add-button");
addButton.addEventListener("click", function(e) {
e.preventDefault();
callbacks.addCostValue ? callbacks.addCostValue(costItemId) : null;
});
return addButton;
}
static createSubmitButton(callbacks) {
const submitButton = document.createElement("button");
submitButton.type = "submit";
submitButton.textContent = "Save";
submitButton.addEventListener("click", function(e) {
e.preventDefault();
const form = this.closest("form");
const costValues = [];
const rows = Array.from(form.querySelectorAll("tr")).filter(row => row.isCostValue);
rows.forEach(row => {
const costCategory = row.querySelector("input[name='category']").value;
const appliedValue = parseFloat(row.querySelector("input[name='value']").value);
const costType = row.querySelector("select[name='type']").value;
const id = parseInt(row.id);
costValues.push({ costType, costCategory, appliedValue, id: id, costItemId: row.parent });
});
const costItemId = parseInt(form.id.split("-")[3]);
console.log(form.id)
console.log("Cost values to be saved:", costItemId);
callbacks.editCostValues ? callbacks.editCostValues(costItemId, costValues) : null;
form.remove();
});
return submitButton;
}
static createFormElement(id, className) {
const form = document.createElement("form");
form.id = id;
form.classList.add(className);
form.style.position = "absolute"; // Set initial position to absolute
form.style.top = "50%";
form.style.left = "50%";
form.style.transform = "translate(-50%, -50%)";
return form;
}
static createHeader(text) {
const header = document.createElement("div");
header.classList.add("form-header");
header.textContent = text;
return header;
}
static createCloseButton(form) {
const closeButton = document.createElement("span");
closeButton.classList.add("close-button");
closeButton.innerHTML = "&times;";
closeButton.addEventListener("click", function() {
form.remove();
});
return closeButton;
}
static makeHeaderDraggable(header, form) {
header.addEventListener("mousedown", function(e) {
let offsetX = e.clientX - form.getBoundingClientRect().left;
let offsetY = e.clientY - form.getBoundingClientRect().top;
function mouseMoveHandler(e) {
form.style.left = `${e.clientX - offsetX}px`;
form.style.top = `${e.clientY - offsetY}px`;
}
function mouseUpHandler() {
document.removeEventListener("mousemove", mouseMoveHandler);
document.removeEventListener("mouseup", mouseUpHandler);
}
document.addEventListener("mousemove", mouseMoveHandler);
document.addEventListener("mouseup", mouseUpHandler);
});
}
}
@@ -4,8 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlenderBIM Web UI</title>
<link rel="stylesheet" href="/static/css/gantt.css" id="index-stylesheet" />
<link rel="stylesheet" href="/static/css/components/card.css" />
<link rel="stylesheet" type="text/css" href="/static/css/gantt.css" id="index-stylesheet" />
<link
rel="stylesheet"
id="tabulator-stylesheet"
@@ -79,6 +79,7 @@ classes = (
operator.SelectUnassignedProducts,
operator.UnassignCostItemQuantity,
operator.UnassignCostItemType,
operator.GenerateCostScheduleBrowser,
prop.CostItem,
prop.CostItemQuantity,
prop.CostItemType,
+2 -8
View File
@@ -265,16 +265,10 @@ class CostSchedulesData:
@classmethod
def cost_values(cls):
results = []
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id
if not ifc_id:
return results
cost_item = tool.Ifc.get().by_id(ifc_id)
for cost_value in cost_item.CostValues or []:
label = "{0:.2f}".format(ifcopenshell.util.cost.calculate_applied_value(cost_item, cost_value))
label += " = {}".format(ifcopenshell.util.cost.serialise_cost_value(cost_value))
results.append({"id": cost_value.id(), "label": label, "name": cost_value.Name})
return results
return []
return ifcopenshell.util.cost.get_cost_values(tool.Ifc.get().by_id(ifc_id))
@classmethod
def quantity_types(cls):
@@ -790,3 +790,14 @@ class AddCurrency(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
core.add_currency(tool.Ifc, tool.Cost)
class GenerateCostScheduleBrowser(bpy.types.Operator):
bl_idname = "bim.generate_cost_schedule_browser"
bl_label = "Generate Cost Schedule Browser"
bl_options = {"REGISTER", "UNDO"}
cost_schedule: bpy.props.IntProperty()
def execute(self, context):
core.generate_cost_schedule_browser(tool.Cost, cost_schedule=tool.Ifc.get().by_id(self.cost_schedule))
return {"FINISHED"}
+2 -1
View File
@@ -73,7 +73,8 @@ class BIM_PT_cost_schedules(Panel):
row1.label(text="Schedule tools")
row1 = col.row(align=True)
row1.alignment = "RIGHT"
row1.operator("bim.export_cost_schedules", text="Export", icon="EXPORT").cost_schedule = cost_schedule["id"]
row1.operator("bim.export_cost_schedules", text="Export spreadsheet", icon="EXPORT").cost_schedule = cost_schedule["id"]
row1.operator("bim.generate_cost_schedule_browser", text="Generate spreadsheet browsser", icon="URL").cost_schedule = cost_schedule["id"]
row2 = col.row(align=True)
row2.alignment = "RIGHT"
op = row2.operator("bim.select_cost_schedule_products", icon="RESTRICT_SELECT_OFF", text="Assigned")
+5
View File
@@ -398,3 +398,8 @@ def add_currency(ifc: tool.Ifc, cost: tool.Cost) -> ifcopenshell.entity_instance
ifc.run("unit.edit_monetary_unit", unit=unit, attributes=attributes)
ifc.run("unit.assign_unit", units=[unit])
return unit
def generate_cost_schedule_browser(cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance) -> bpy.types.Panel:
cost_schedule_data = cost.create_cost_schedule_json(cost_schedule)
return cost.generate_cost_schedule_browser(cost_schedule_data)
+8 -3
View File
@@ -811,9 +811,8 @@ class Cost(bonsai.core.tool.Cost):
@classmethod
def create_cost_schedule_json(cls, cost_schedule: ifcopenshell.entity_instance) -> dict:
from bonsai.bim.module.cost.data import CostSchedulesData
if not CostSchedulesData.is_loaded:
CostSchedulesData.load()
cost_items = CostSchedulesData.data["cost_items"]
CostSchedulesData.load()
cost_items = CostSchedulesData.data["cost_items"]
data = []
for rel in cost_schedule.Controls or []:
for cost_item in rel.RelatedObjects or []:
@@ -852,3 +851,9 @@ class Cost(bonsai.core.tool.Cost):
unit = tool.Unit.get_project_currency_unit()
if unit:
return {"id": unit.id(), "name": unit.Currency}
@classmethod
def generate_cost_schedule_browser(cls, cost_schedule_data: list[dict[str, Any]]) -> None:
if not bpy.context.scene.WebProperties.is_connected:
bpy.ops.bim.connect_websocket_server(page="costing")
tool.Web.send_webui_data(data=cost_schedule_data, data_key="cost_items", event="cost_items")
+65 -7
View File
@@ -21,6 +21,7 @@ from bonsai.bim.module.web.data import WebData
import bonsai.core.tool
import bonsai.tool as tool
import ifcopenshell.api.sequence
import ifcopenshell.api.cost
from typing import Any, Dict, Optional
import time
import socket
@@ -354,17 +355,15 @@ class Web(bonsai.core.tool.Web):
if operator_data["type"] == "loadCostSchedule":
cost_schedule = ifc_file.by_id(operator_data["costScheduleId"])
bonsai.core.cost.enable_editing_cost_items(tool.Cost, cost_schedule=cost_schedule)
json_data = tool.Cost.create_cost_schedule_json(cost_schedule)
cls.send_webui_data(data=json_data, data_key="cost_items", event="cost_items")
if operator_data["type"] == "addCostItem":
bpy.ops.bim.add_cost_item(cost_item=operator_data["costItemId"])
cost_schedule = tool.Cost.get_cost_schedule(cost_item=tool.Ifc.get().by_id(operator_data["costItemId"]))
json_data = tool.Cost.create_cost_schedule_json(cost_schedule)
cls.send_webui_data(data=json_data, data_key="cost_items", event="cost_items")
cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "selectAssignedElements":
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
products = tool.Cost.get_cost_item_products(cost_item, is_deep=True)
tool.Spatial.select_products(products, unhide=True)
if operator_data["type"] == "addCostItem":
bpy.ops.bim.add_cost_item(cost_item=operator_data["costItemId"])
cost_schedule = tool.Cost.get_cost_schedule(cost_item=tool.Ifc.get().by_id(operator_data["costItemId"]))
cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "editCostItemName":
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
tool.Ifc.run(
@@ -373,6 +372,65 @@ class Web(bonsai.core.tool.Web):
attributes = {"Name": operator_data["name"]}
)
tool.Cost.load_cost_schedule_tree()
if operator_data["type"] == "enableEditingCostValues":
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
cost_values = ifcopenshell.util.cost.get_cost_values(cost_item)
cls.send_webui_data(data={
"cost_values": cost_values,
"cost_item_id": operator_data["costItemId"]
}, data_key="cost_values", event="cost_values")
if operator_data["type"] == "addCostValue":
value = ifcopenshell.api.cost.add_cost_value(
ifc_file,
parent=ifc_file.by_id(operator_data["costItemId"]),
)
cls.send_webui_data(
data={
"cost_value_id" : value.id(),
"cost_item_id":operator_data["costItemId"]},
data_key="cost_value",
event="cost_value"
)
if operator_data["type"] == "editCostValues":
cost_item_id = operator_data["costItemId"]
print('Editing cost values', cost_item_id)
print(cost_item_id)
print(type(operator_data["costValues"]))
for value_data in operator_data["costValues"] or []:
print(value_data)
value = ifc_file.by_id(value_data["id"])
print(value.get_info())
if value_data["costType"] == "FIXED":
attributes= {
"AppliedValue": value_data["appliedValue"],
}
elif value_data["costType"] == "CATEGORY":
attributes= {
"AppliedValue": value_data["appliedValue"],
"Category": value_data["costCategory"],
}
elif value_data["costType"] == "SUM":
attributes= {
"Category": '*'
}
ifcopenshell.api.cost.edit_cost_value(
file=ifc_file,
cost_value=value,
attributes= attributes
)
tool.Cost.load_cost_schedule_tree()
cost_item = ifc_file.by_id(operator_data["costItemId"])
if not cost_item:
print("Cost item not found")
return
cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item)
cls.load_cost_schedule_web_ui(cost_schedule)
@classmethod
def load_cost_schedule_web_ui(cls, cost_schedule):
json_data = tool.Cost.create_cost_schedule_json(cost_schedule)
cls.send_webui_data(data=json_data, data_key="cost_items", event="cost_items")
@classmethod
def handle_gantt_operator(cls, operator_data: dict) -> None:
@@ -245,6 +245,20 @@ def get_cost_item_assignments(
for product in get_cost_assignments_by_type(nested_cost_item, filter_by_type)
]
def get_cost_values(cost_item: ifcopenshell.entity_instance) -> list[dict[str, str]]:
results = []
for cost_value in cost_item.CostValues or []:
label = "{0:.2f}".format(calculate_applied_value(cost_item, cost_value))
label += " = {}".format(serialise_cost_value(cost_value))
results.append({
"id": cost_value.id(),
"label": label,
"name": cost_value.Name,
"category": cost_value.Category,
"applied_value": get_primitive_applied_value(cost_value.AppliedValue) if cost_value.AppliedValue else None,
})
print(results)
return results
class CostValueUnserialiser:
def parse(self, formula: str):