- 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 blender_messages[sid]["cost_schedules"] = data
await sio.emit("cost_schedules", {"blenderId": sid, "data": data}, namespace="/web") 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): async def schedules(request):
with open("templates/index.html", "r") as f: with open("templates/index.html", "r") as f:
template = f.read() 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/card.css");
@import url("./components/contextMenu.css");
:root { :root {
--font-family: Arial, sans-serif; --font-family: Arial, sans-serif;
@@ -23,6 +23,8 @@ function connectSocket() {
socket.on("connect", handleWebConnect); socket.on("connect", handleWebConnect);
socket.on("cost_schedules", handleCostSchedulesData); socket.on("cost_schedules", handleCostSchedulesData);
socket.on("cost_items", handleCostItemsData); socket.on("cost_items", handleCostItemsData);
socket.on("cost_values", handleCostValuesData);
socket.on("cost_value", handleCostValueData);
} }
function handleBlenderConnect(blenderId) { 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) { function handleConnectedClients(data) {
$("#blender-count").text(data.length); $("#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 handleThemeData(themeData) {
function arrayToRgbString(arr) { function arrayToRgbString(arr) {
const [r, g, b, a] = arr.map((num) => Math.round(num * 255)); const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
@@ -99,7 +136,6 @@ function setTheme(theme) {
} }
function addCostItem(costItemId) { function addCostItem(costItemId) {
console.log("addCostItem", costItemId);
executeOperator({ type: "addCostItem", costItemId: costItemId }); executeOperator({ type: "addCostItem", costItemId: costItemId });
} }
@@ -119,11 +155,7 @@ function handleCostSchedulesData(data) {
const blenderId = data.blenderId; const blenderId = data.blenderId;
const costSchedules = data.data["cost_schedules"]["cost_schedules"]; const costSchedules = data.data["cost_schedules"]["cost_schedules"];
const currency = data.data["cost_schedules"]["currency"]["name"]; const currency = data.data["cost_schedules"]["currency"]["name"];
console.log(data.data["cost_schedules"]);
const costScheduleDiv = $("#cost-schedules"); const costScheduleDiv = $("#cost-schedules");
costSchedules.forEach((costSchedule) => { costSchedules.forEach((costSchedule) => {
costSchedule.UpdateDate = new Date(costSchedule.UpdateDate); costSchedule.UpdateDate = new Date(costSchedule.UpdateDate);
const mainContainer = CostUI.text("Updated On: " + costSchedule.UpdateDate); const mainContainer = CostUI.text("Updated On: " + costSchedule.UpdateDate);
@@ -136,7 +168,6 @@ function handleCostSchedulesData(data) {
} }
function handleCostItemsData(data) { function handleCostItemsData(data) {
console.log(data);
CostUI.createCostSchedule({ CostUI.createCostSchedule({
data: data.data["cost_items"], data: data.data["cost_items"],
blenderID: data.blenderId, blenderID: data.blenderId,
@@ -144,6 +175,7 @@ function handleCostItemsData(data) {
"addCostItem": addCostItem, "addCostItem": addCostItem,
"selectAssignedElements": selectAssignedElements, "selectAssignedElements": selectAssignedElements,
'editCostItemName': editCostItemName, 'editCostItemName': editCostItemName,
'enableEditingCostValues': enableEditingCostValues,
} }
}); });
} }
@@ -166,3 +198,7 @@ function loadCostSchedule(costScheduleId, blenderId) {
function getCostSchedules(blenderId) { function getCostSchedules(blenderId) {
executeOperator({ type: "getCostSchedules" }, blenderId); executeOperator({ type: "getCostSchedules" }, blenderId);
} }
function enableEditingCostValues(costItemId) {
executeOperator({ type: "enableEditingCostValues", costItemId: costItemId});
}
@@ -82,7 +82,6 @@ function handleBlenderDisconnect(blenderId) {
function handleConnectedClients(data) { function handleConnectedClients(data) {
$("#blender-count").text(data.length); $("#blender-count").text(data.length);
// console.log(data);
data.forEach(function (id) { data.forEach(function (id) {
connectedClients[id] = { connectedClients[id] = {
shown: false, shown: false,
@@ -115,7 +114,6 @@ function handleThemeData(themeData) {
} }
const cssRule = generateCssVariableRule(themeData.theme); const cssRule = generateCssVariableRule(themeData.theme);
console.log(cssRule);
var styleElement = $("#gantt-stylesheet")[0]; var styleElement = $("#gantt-stylesheet")[0];
if (styleElement) { if (styleElement) {
@@ -133,7 +131,6 @@ function handleWorkScheduleData(data) {
const workSchedules = data["data"]["work_schedule_info"]; const workSchedules = data["data"]["work_schedule_info"];
workSchedules.forEach((workSchedule) => { workSchedules.forEach((workSchedule) => {
console.log(workSchedule);
const mainContainer = CostUI.text(new Date(workSchedule.CreationDate).toLocaleDateString()); const mainContainer = CostUI.text(new Date(workSchedule.CreationDate).toLocaleDateString());
const callback = () => loadWorkSchedule(workSchedule.id); const callback = () => loadWorkSchedule(workSchedule.id);
const card = CostUI.createCard(workSchedule.Name,mainContainer, callback); const card = CostUI.createCard(workSchedule.Name,mainContainer, callback);
@@ -142,11 +139,8 @@ function handleWorkScheduleData(data) {
} }
function handleGanttData(data) { function handleGanttData(data) {
console.log("running handleGanttData");
const blenderId = data["blenderId"]; const blenderId = data["blenderId"];
console.log(data);
const filename = data["data"]["ifc_file"]; const filename = data["data"]["ifc_file"];
const ganttTasks = data["data"]["gantt_data"]["tasks"]; const ganttTasks = data["data"]["gantt_data"]["tasks"];
const ganttWorkSched = data["data"]["gantt_data"]["work_schedule"]; const ganttWorkSched = data["data"]["gantt_data"]["work_schedule"];
@@ -183,7 +177,6 @@ function handleDefaultData(data) {
const blenderId = data["blenderId"]; const blenderId = data["blenderId"];
const isDirty = data["data"]["is_dirty"]; const isDirty = data["data"]["is_dirty"];
showWarning(blenderId, isDirty); showWarning(blenderId, isDirty);
console.log('default data',data);
} }
// Function to add a new gantt with data and filename // Function to add a new gantt with data and filename
@@ -1,14 +1,6 @@
export class CostUI { export class CostUI {
constructor() {} constructor() {}
createButton() {
console.log("Button created");
}
createInput() {
console.log("Input created");
}
static isCostScheduleLoaded(id) { static isCostScheduleLoaded(id) {
const existingTable = document.getElementById('cost-items-' + id); const existingTable = document.getElementById('cost-items-' + id);
return existingTable !== null; return existingTable !== null;
@@ -17,7 +9,7 @@ export class CostUI {
static removeCostSchedule(id) { static removeCostSchedule(id) {
document.getElementById("cost-items-" + id).remove(); document.getElementById("cost-items-" + id).remove();
} }
static createTable(id) { static createTable(id, callbacks) {
CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null; CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null;
const table = document.createElement("table"); const table = document.createElement("table");
@@ -51,7 +43,7 @@ export class CostUI {
CostUI.addTableStyles(id); CostUI.addTableStyles(id);
// Create context menu // Create context menu
CostUI.createContextMenu(); CostUI.createContextMenu(callbacks);
table.get_blender_id = function() { table.get_blender_id = function() {
return this.getAttribute("id").split("-")[2]; return this.getAttribute("id").split("-")[2];
@@ -71,52 +63,19 @@ export class CostUI {
#cost-items-${id} td:not(:nth-child(1)) { #cost-items-${id} td:not(:nth-child(1)) {
width: 100px; /* Set a fixed width for other columns */ 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); document.head.appendChild(style);
} }
static createContextMenu() { static createContextMenu(callbacks) {
// Create context menu // Create context menu
const contextMenu = document.createElement("div"); const contextMenu = document.createElement("div");
contextMenu.id = "context-menu"; contextMenu.id = "context-menu";
contextMenu.classList.add("context-menu"); contextMenu.classList.add("context-menu");
contextMenu.innerHTML = ` 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="delete-button">Delete</button>
<button id="duplicate-button">Duplicate</button> <button id="duplicate-button">Duplicate</button>
`; `;
@@ -126,6 +85,7 @@ export class CostUI {
document.addEventListener("contextmenu", function(event) { document.addEventListener("contextmenu", function(event) {
event.preventDefault(); event.preventDefault();
const targetRow = event.target.closest("tr"); const targetRow = event.target.closest("tr");
const targetCell = event.target.closest("td");
if (targetRow && targetRow.parentElement.id === "cost-items") { if (targetRow && targetRow.parentElement.id === "cost-items") {
const contextMenu = document.getElementById("context-menu"); const contextMenu = document.getElementById("context-menu");
contextMenu.style.display = "block"; contextMenu.style.display = "block";
@@ -134,6 +94,7 @@ export class CostUI {
// Store the target row in the context menu for later use // Store the target row in the context menu for later use
contextMenu.targetRow = targetRow; contextMenu.targetRow = targetRow;
contextMenu.targetCell = targetCell;
} else { } else {
document.getElementById("context-menu").style.display = "none"; 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 targetRow = document.getElementById("context-menu").targetRow;
const targetCell = document.getElementById("context-menu").targetCell;
if (targetRow) { if (targetRow) {
// Implement your edit action here const costItemId = parseInt(targetRow.getAttribute("id"));
console.log("Edit row:", 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() { document.getElementById("delete-button").addEventListener("click", function() {
const targetRow = document.getElementById("context-menu").targetRow; const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) { if (targetRow) {
@@ -246,7 +225,7 @@ export class CostUI {
} }
static createCostSchedule({ data, blenderID, title, callbacks = {} }) { 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.createCostItem(data, tbody, 0, null, callbacks);
CostUI.applyExpandedState(); CostUI.applyExpandedState();
} }
@@ -263,6 +242,35 @@ export class CostUI {
} }
static createRow(obj, nestingLevel, parentID, callbacks = {}) { 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"); const row = document.createElement("tr");
row.setAttribute("id", obj.id); row.setAttribute("id", obj.id);
row.setAttribute("parent-id", parentID); row.setAttribute("parent-id", parentID);
@@ -270,6 +278,10 @@ export class CostUI {
row.classList.add("nested"); row.classList.add("nested");
row.classList.add(`level-${nestingLevel}`); row.classList.add(`level-${nestingLevel}`);
} }
return row;
}
static createExpandButton(obj) {
const expandButton = document.createElement("button"); const expandButton = document.createElement("button");
expandButton.classList.add("toggle-button"); expandButton.classList.add("toggle-button");
if (obj.is_nested_by && obj.is_nested_by.length > 0) { if (obj.is_nested_by && obj.is_nested_by.length > 0) {
@@ -277,51 +289,62 @@ export class CostUI {
} else { } else {
expandButton.style.visibility = "hidden"; expandButton.style.visibility = "hidden";
} }
//row.appendChild(expandButton);
expandButton.addEventListener("click", function() { expandButton.addEventListener("click", function() {
CostUI.contractExpandRow.call(this, obj.id); CostUI.contractExpandRow.call(this, obj.id);
}); });
return expandButton;
}
static createNameCell(obj, nestingLevel, expandButton, callbacks) {
const nameCell = document.createElement("td"); const nameCell = document.createElement("td");
const nameInput = document.createElement("input"); const nameInput = document.createElement("input");
nameInput.value = obj.name ? obj.name : "Unnamed"; nameInput.value = obj.name ? obj.name : "Unnamed";
nameInput.addEventListener("change", function() { nameInput.addEventListener("change", function() {
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null; callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
}); });
nameInput.addEventListener("keydown", function(event) { nameInput.addEventListener("keydown", function(event) {
if (event.key === "Enter") { if (event.key === "Enter") {
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null; callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
} }
}); });
nameCell.style.paddingLeft = nestingLevel * 20 + "px"; nameCell.style.paddingLeft = nestingLevel * 20 + "px";
nameCell.appendChild(expandButton); nameCell.appendChild(expandButton);
nameCell.appendChild(nameInput); nameCell.appendChild(nameInput);
row.appendChild(nameCell); return nameCell;
}
const totalCostQuantityCell = document.createElement("td");
totalCostQuantityCell.textContent = obj.TotalCostQuantity; static createTableCell(content) {
row.appendChild(totalCostQuantityCell); const cell = document.createElement("td");
cell.textContent = content;
const unitSymbolCell = document.createElement("td"); return cell;
unitSymbolCell.textContent = obj.UnitSymbol; }
row.appendChild(unitSymbolCell);
static createTotalCostCell(obj) {
const totalAppliedValueCell = document.createElement("td");
totalAppliedValueCell.textContent = obj.TotalAppliedValue;
row.appendChild(totalAppliedValueCell);
const totalCostCell = document.createElement("td"); const totalCostCell = document.createElement("td");
const totalCost = parseFloat(obj.TotalCost).toFixed(2); const totalCost = parseFloat(obj.TotalCost).toFixed(2);
totalCostCell.textContent = obj.is_sum ? totalCost + " (Σ)" : totalCost; totalCostCell.textContent = obj.is_sum ? totalCost + " (Σ)" : totalCost;
return totalCostCell;
row.appendChild(totalCostCell); }
static createFlexContainerCell(obj, callbacks) {
const divFlex = document.createElement("div"); const divFlex = document.createElement("div");
divFlex.classList.add("flex-container"); 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"); const addButton = document.createElement("button");
addButton.textContent = "+"; addButton.textContent = "+";
addButton.classList.add("add-button"); addButton.classList.add("add-button");
@@ -329,31 +352,17 @@ export class CostUI {
e.stopPropagation(); e.stopPropagation();
callbacks.addCostItem ? callbacks.addCostItem(obj.id) : null; callbacks.addCostItem ? callbacks.addCostItem(obj.id) : null;
}); });
return addButton;
}
static createSelectButton(obj, callbacks) {
const selectButton = document.createElement("button"); const selectButton = document.createElement("button");
selectButton.textContent = "Select"; selectButton.textContent = "Select";
selectButton.addEventListener("click", function(e) { selectButton.addEventListener("click", function(e) {
e.stopPropagation(); e.stopPropagation();
callbacks.selectAssignedElements ? callbacks.selectAssignedElements(obj.id) : null; callbacks.selectAssignedElements ? callbacks.selectAssignedElements(obj.id) : null;
}); });
return selectButton;
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;
} }
static hideNestedRows(parentId) { static hideNestedRows(parentId) {
@@ -461,4 +470,267 @@ export class CostUI {
return card; 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 charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlenderBIM Web UI</title> <title>BlenderBIM Web UI</title>
<link rel="stylesheet" href="/static/css/gantt.css" id="index-stylesheet" /> <link rel="stylesheet" type="text/css" href="/static/css/gantt.css" id="index-stylesheet" />
<link rel="stylesheet" href="/static/css/components/card.css" />
<link <link
rel="stylesheet" rel="stylesheet"
id="tabulator-stylesheet" id="tabulator-stylesheet"
@@ -79,6 +79,7 @@ classes = (
operator.SelectUnassignedProducts, operator.SelectUnassignedProducts,
operator.UnassignCostItemQuantity, operator.UnassignCostItemQuantity,
operator.UnassignCostItemType, operator.UnassignCostItemType,
operator.GenerateCostScheduleBrowser,
prop.CostItem, prop.CostItem,
prop.CostItemQuantity, prop.CostItemQuantity,
prop.CostItemType, prop.CostItemType,
+2 -8
View File
@@ -265,16 +265,10 @@ class CostSchedulesData:
@classmethod @classmethod
def cost_values(cls): def cost_values(cls):
results = []
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id
if not ifc_id: if not ifc_id:
return results return []
cost_item = tool.Ifc.get().by_id(ifc_id) return ifcopenshell.util.cost.get_cost_values(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
@classmethod @classmethod
def quantity_types(cls): def quantity_types(cls):
@@ -790,3 +790,14 @@ class AddCurrency(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context): def _execute(self, context):
core.add_currency(tool.Ifc, tool.Cost) 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.label(text="Schedule tools")
row1 = col.row(align=True) row1 = col.row(align=True)
row1.alignment = "RIGHT" 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 = col.row(align=True)
row2.alignment = "RIGHT" row2.alignment = "RIGHT"
op = row2.operator("bim.select_cost_schedule_products", icon="RESTRICT_SELECT_OFF", text="Assigned") 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.edit_monetary_unit", unit=unit, attributes=attributes)
ifc.run("unit.assign_unit", units=[unit]) ifc.run("unit.assign_unit", units=[unit])
return 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 @classmethod
def create_cost_schedule_json(cls, cost_schedule: ifcopenshell.entity_instance) -> dict: def create_cost_schedule_json(cls, cost_schedule: ifcopenshell.entity_instance) -> dict:
from bonsai.bim.module.cost.data import CostSchedulesData from bonsai.bim.module.cost.data import CostSchedulesData
if not CostSchedulesData.is_loaded: CostSchedulesData.load()
CostSchedulesData.load() cost_items = CostSchedulesData.data["cost_items"]
cost_items = CostSchedulesData.data["cost_items"]
data = [] data = []
for rel in cost_schedule.Controls or []: for rel in cost_schedule.Controls or []:
for cost_item in rel.RelatedObjects 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() unit = tool.Unit.get_project_currency_unit()
if unit: if unit:
return {"id": unit.id(), "name": unit.Currency} 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.core.tool
import bonsai.tool as tool import bonsai.tool as tool
import ifcopenshell.api.sequence import ifcopenshell.api.sequence
import ifcopenshell.api.cost
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
import time import time
import socket import socket
@@ -354,17 +355,15 @@ class Web(bonsai.core.tool.Web):
if operator_data["type"] == "loadCostSchedule": if operator_data["type"] == "loadCostSchedule":
cost_schedule = ifc_file.by_id(operator_data["costScheduleId"]) cost_schedule = ifc_file.by_id(operator_data["costScheduleId"])
bonsai.core.cost.enable_editing_cost_items(tool.Cost, cost_schedule=cost_schedule) bonsai.core.cost.enable_editing_cost_items(tool.Cost, cost_schedule=cost_schedule)
json_data = tool.Cost.create_cost_schedule_json(cost_schedule) cls.load_cost_schedule_web_ui(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")
if operator_data["type"] == "selectAssignedElements": if operator_data["type"] == "selectAssignedElements":
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"]) cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
products = tool.Cost.get_cost_item_products(cost_item, is_deep=True) products = tool.Cost.get_cost_item_products(cost_item, is_deep=True)
tool.Spatial.select_products(products, unhide=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": if operator_data["type"] == "editCostItemName":
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"]) cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
tool.Ifc.run( tool.Ifc.run(
@@ -373,6 +372,65 @@ class Web(bonsai.core.tool.Web):
attributes = {"Name": operator_data["name"]} attributes = {"Name": operator_data["name"]}
) )
tool.Cost.load_cost_schedule_tree() 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 @classmethod
def handle_gantt_operator(cls, operator_data: dict) -> None: 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) 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: class CostValueUnserialiser:
def parse(self, formula: str): def parse(self, formula: str):