mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 02:02:22 +00:00
- Add button to generate cost schedule
- Feature to view, edit and add cost item values. ( Right Click - Edit ).
This commit is contained in:
@@ -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 = "×";
|
||||
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,
|
||||
|
||||
@@ -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"}
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user