Cost module web-ui can now:

- assign cost item products from Blender selection
- display list of selected products ( with available quantities, subtotals, and highlighting already assigned products)
- display and refresh assigned products
- highlight loaded cost schedule
This commit is contained in:
myoualid
2024-09-08 01:16:48 +01:00
parent fe317f9231
commit 171c3083da
11 changed files with 887 additions and 329 deletions
@@ -181,6 +181,11 @@ class BlenderNamespace(socketio.AsyncNamespace):
blender_messages[sid]["predefined_types"] = data blender_messages[sid]["predefined_types"] = data
await sio.emit("predefined_types", {"blenderId": sid, "data": data}, namespace="/web") await sio.emit("predefined_types", {"blenderId": sid, "data": data}, namespace="/web")
async def on_selected_products(self, sid, data):
print(f"Selected products from Blender client {sid}")
blender_messages[sid]["selected_products"] = data
await sio.emit("selected_products", {"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()
@@ -1,115 +1,135 @@
table { table {
width: 100%; width: 100%;
border-collapse: collapse; height: 80%;
user-select: none; border-collapse: collapse;
user-select: none;
} }
td, th { .table-container {
border: 1px solid #ddd; max-height: 70vh;
padding: 8px; margin-top: 1em;
text-align: left; overflow-y: auto;
overflow-x: auto;
}
form table {
height: 50%;
}
form .table-container {
max-height: 50vh;
}
td,
th {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
} }
td { td {
align-items: center; align-items: center;
} }
/* first td of a row, must be flex */ /* first td of a row, must be flex */
td:first-child { td:first-child {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.nested { .nested {
display: none; display: none;
} }
.expand-collapse { .expand-collapse {
cursor: pointer; cursor: pointer;
} }
.caret-cell { .caret-cell {
border: none; border: none;
background: none; background: none;
padding: 0; padding: 0;
} }
input, select { input,
background-color: var(--blender-button-background, var(--bg-color)); select {
color: var(--blender-button-text, var(--text-color)); background-color: var(--blender-button-background, var(--bg-color));
border-color: var(--blender-button-border, var(--border-color)); color: var(--blender-button-text, var(--text-color));
transition: filter 0.2s ease; border-color: var(--blender-button-border, var(--border-color));
border-radius: 6px; transition: filter 0.2s ease;
padding: 6px 8px; border-radius: 6px;
font-size: 14px; padding: 6px 8px;
line-height: 20px; font-size: 14px;
transition: border-color 0.2s cubic-bezier(0.3, 0, 0.5, 1); line-height: 20px;
height: inherit; transition: border-color 0.2s cubic-bezier(0.3, 0, 0.5, 1);
margin-left: 0.5em; height: inherit;
margin-right: 0.5em; margin-left: 0.5em;
margin-right: 0.5em;
min-width: 50px; /* Minimum width to prevent it from becoming too small */
min-width: 50px; /* Minimum width to prevent it from becoming too small */
} }
input:focus { input:focus {
border-color: #28a745; border-color: #28a745;
outline: none; outline: none;
box-shadow: 0 0 0 3px #28a745; box-shadow: 0 0 0 3px #28a745;
} }
#cost-items tr:hover, .highlighted { #cost-items tr:hover,
background-color: #ffb936; .highlighted {
color : black; background-color: #ffb936;
color: black;
} }
.highlight { .highlight {
background-color: #ffb936; background-color: #ffb936;
color : black; color: black;
} }
.cost-values-table { .cost-values-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
} }
.cost-values-table th, .cost-values-table td { .cost-values-table th,
border: 1px solid #ddd; .cost-values-table td {
padding: 8px; border: 1px solid #ddd;
padding: 8px;
} }
.cost-values-table th { .cost-values-table th {
background-color: #444444; background-color: #444444;
text-align: center; text-align: center;
} }
/* Responsive Styles */ /* Responsive Styles */
@media (max-width: 768px) { @media (max-width: 768px) {
table { table {
font-size: 12px; font-size: 12px;
} }
td, th { td,
padding: 4px; th {
} padding: 4px;
}
input {
padding: 2px;
font-size: 12px;
}
input {
padding: 2px;
font-size: 12px;
}
} }
@media (max-width: 480px) { @media (max-width: 480px) {
table { table {
font-size: 10px; font-size: 10px;
} }
td, th { td,
padding: 2px; th {
} padding: 2px;
}
input { input {
padding: 1px; padding: 1px;
font-size: 10px; font-size: 10px;
} }
} }
@@ -1,130 +1,75 @@
@import url("./shared.css"); @import url("./shared.css");
@import url("./components/card.css"); @import url("./components/card.css");
@import url("./components/contextMenu.css"); @import url("./components/contextMenu.css");
@import url("./components/costTable.css");
table {
width: 100%;
border-collapse: collapse;
user-select: none;
}
td:not(:first-child) {
text-align: center;
justify-content: space-between;
align-items: center;
}
td {
border-right: 1px solid #ddd;
padding-left: 0.5em;
padding-right: 0.5em;
}
.nested {
display: none;
}
.expand-collapse {
cursor: pointer;
}
.caret-cell {
border: none;
background: none;
padding: 0;
}
input, select {
background-color: var(--blender-button-background, var(--bg-color));
color: var(--blender-button-text, var(--text-color));
border-color: var(--blender-button-border, var(--border-color));
transition: filter 0.2s ease;
border-radius: 6px;
padding: 6px 8px;
font-size: 14px;
line-height: 20px;
transition: border-color 0.2s cubic-bezier(0.3, 0, 0.5, 1);
height: inherit;
margin-left: 0.5em;
margin-right: 0.5em;
}
input:focus {
border-color: #28a745;
outline: none;
box-shadow: 0 0 0 3px #28a745;
}
#cost-items tr:hover, .highlighted {
background-color: #ffb936;
color : black;
}
.highlight {
background-color: #ffb936;
color : black;
}
.form-header { .form-header {
display: flex; display: flex;
justify-content: space-between; justify-content: flex-start;
align-items: center; align-items: center;
margin-bottom: 10px; margin-bottom: 10px;
background-color: #363636; background-color: #363636;
padding: 10px; padding: 10px;
/* trext size */
font-size: 1.5em;
}
span {
margin-left: var(--margin-small);
} }
.floating-form { .floating-form {
min-width: 40%; min-width: 40%;
height: auto; max-width: 80%;
position: absolute; max-height: 80vh;
background-color: #363636; height: auto;
border: 1px solid black; position: absolute;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); background-color: #363636;
z-index: 9999; border: 1px solid black;
cursor: move; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
padding: 20px; z-index: 9999;
user-select: none; cursor: move;
} padding: 20px;
user-select: none;
.cost-values-table { overflow-y: auto;
width: 100%; overflow-x: hidden;
border-collapse: collapse;
}
.cost-values-table th, .cost-values-table td {
border: 1px solid #ddd;
padding: 8px;
}
.cost-values-table th {
background-color: #444444;
text-align: center;
}
.close-button {
position: absolute;
top: 10px;
right: 10px;
font-size: 20px;
cursor: pointer;
} }
.action-button { .action-button {
background-color: #28a745; background-color: #28a745;
border: none; border: none;
color: #fff; color: #fff;
padding: 0.5em 1em; padding: 0.5em 1em;
cursor: pointer; cursor: pointer;
border-radius: 6px; border-radius: 6px;
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
display: inline-block; display: inline-block;
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
width: fit-content; width: fit-content;
margin-left: 0.5em; margin: 0.5em;
} }
.action-button:hover { .action-button:hover {
background-color: #218838; background-color: #218838;
}
.close-button {
position: absolute;
top: 10px;
right: 10px;
font-size: 20px;
cursor: pointer;
}
.settings-menu {
display: none;
position: absolute;
background-color: #363636;
border: 1px solid black;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
z-index: 9999;
cursor: move;
padding: 20px;
user-select: none;
} }
@@ -3,7 +3,7 @@ import { CostUI } from "./utilities/costui.js";
const connectedClients = {}; const connectedClients = {};
let socket; let socket;
let isScheduleLoad = {}; let loadedSchedules = {};
$(document).ready(function () { $(document).ready(function () {
var defaultTheme = "blender"; var defaultTheme = "blender";
@@ -11,7 +11,21 @@ $(document).ready(function () {
setTheme(theme); setTheme(theme);
connectSocket(); connectSocket();
CostUI.createColorPicker();
const settingsMenu = CostUI.Form({
id: "settings-menu",
name: "Settings",
icon: "fa-solid fa-gear",
shouldHide: true,
});
settingsMenu.style.display = "none";
const picker = CostUI.createColorPicker();
settingsMenu.appendChild(picker);
const settingsButton = document.getElementById("settings-button");
settingsButton.addEventListener("click", function () {
settingsMenu.style.display = "block";
});
}); });
function connectSocket() { function connectSocket() {
@@ -28,6 +42,46 @@ function connectSocket() {
socket.on("cost_items", handleCostItemsData); socket.on("cost_items", handleCostItemsData);
socket.on("cost_values", handleCostValuesData); socket.on("cost_values", handleCostValuesData);
socket.on("cost_value", handleCostValueData); socket.on("cost_value", handleCostValueData);
socket.on("selected_products", handleSelectedProducts);
}
function handleSelectedProducts(data) {
const costItemId = data.data["selected_products"]["cost_item_id"];
const products = data.data["selected_products"]["selected_products"];
const assigned_products = data.data["selected_products"]["assigned_products"];
const quantityNames =
data.data["selected_products"]["product_quantity_names"];
const formId = "selected-products-" + costItemId;
const form = CostUI.Form({
id: formId,
name: "Selected Products",
icon: "fa-solid fa-cart-shopping",
});
CostUI.highlightElement(costItemId);
const selectedProductsTable = CostUI.createProductTable({
form,
products,
quantityNames,
costItemId,
callbacks: {
addProductAssignments: addProductAssignments,
getSelectedProducts: getSelectedProducts,
},
});
if (assigned_products.length > 0) {
const assignedProductsText = CostUI.Text(
"Assigned Products",
"fa-solid fa-box",
"x-large"
);
form.appendChild(assignedProductsText);
const assignmentsTable = CostUI.createAssignmentsTable({
form,
products: assigned_products,
quantityNames,
costItemId,
});
}
} }
function handlePredefinedTypes(data) { function handlePredefinedTypes(data) {
@@ -37,6 +91,15 @@ function handlePredefinedTypes(data) {
); );
} }
function addProductAssignments({ costItemId, selectedQuantity, productIds }) {
executeOperator({
type: "addProductAssignments",
costItemId: costItemId,
propName: selectedQuantity,
productIds: productIds,
});
}
function addCostSchedule(name, predefinedType) { function addCostSchedule(name, predefinedType) {
executeOperator({ executeOperator({
type: "addCostSchedule", type: "addCostSchedule",
@@ -109,7 +172,7 @@ function handleCostValuesData(data) {
}, },
}); });
CostUI.highlightCostItem(data.data["cost_values"]["cost_item_id"]); CostUI.highlightElement(data.data["cost_values"]["cost_item_id"]);
} }
function addCostValue(costItemId) { function addCostValue(costItemId) {
@@ -200,6 +263,9 @@ function addSummaryCostItem(costScheduleId) {
function handleCostSchedulesData(data) { function handleCostSchedulesData(data) {
const blenderId = data.blenderId; const blenderId = data.blenderId;
const loadedSchedule = loadedSchedules[blenderId]
? loadedSchedules[blenderId]
: null;
const costSchedules = data.data["cost_schedules"]["cost_schedules"]; const costSchedules = data.data["cost_schedules"]["cost_schedules"];
if (costSchedules.length === 0) { if (costSchedules.length === 0) {
return; return;
@@ -210,24 +276,50 @@ function handleCostSchedulesData(data) {
const costScheduleDiv = document.getElementById("cost-schedules"); const costScheduleDiv = document.getElementById("cost-schedules");
costScheduleDiv.innerHTML = ""; costScheduleDiv.innerHTML = "";
costSchedules.forEach((costSchedule) => { costSchedules.forEach((costSchedule) => {
costSchedule.UpdateDate = new Date(costSchedule.UpdateDate); const predefinedType = CostUI.Text(
const predefinedType = CostUI.text("Predefined Type: " + costSchedule.PredefinedType) "Predefined Type: " + costSchedule.PredefinedType,
const updatedOn = CostUI.text("Updated On: " + costSchedule.UpdateDate); "fa-solid fa-paperclip"
);
// simplyfying the date
const updatedDate = new Date(costSchedule.UpdateDate).toLocaleDateString();
const updatedOn = CostUI.Text(
"Updated On: " + updatedDate,
"fa-solid fa-pen-nib"
);
const div = document.createElement("div"); const div = document.createElement("div");
div.appendChild(predefinedType); div.appendChild(predefinedType);
div.appendChild(updatedOn); div.appendChild(updatedOn);
const callback = () => loadCostSchedule(costSchedule.id, blenderId); const callback = () => loadCostSchedule(costSchedule.id, blenderId);
const card = CostUI.createCard(costSchedule.Name, div, callback); const card = CostUI.createCard(
costSchedule.id,
costSchedule.Name,
div,
callback
);
costScheduleDiv.append(card); costScheduleDiv.append(card);
}); });
loadedSchedule ? CostUI.highlightElement("schedule-" + loadedSchedule) : null;
} }
function handleCostItemsData(data) { function handleCostItemsData(data) {
const cards = document.querySelectorAll("[id^='schedule-']");
cards.forEach((card) => {
if (card.id === "schedule-" + data.data["cost_items"]["cost_schedule_id"]) {
card.classList.add("highlighted");
} else {
card.classList.remove("highlighted");
}
});
const costScheduleId = data.data["cost_items"]["cost_schedule_id"];
loadedSchedules[data.blenderId] = costScheduleId;
CostUI.highlightElement("schedule-" + costScheduleId);
CostUI.createCostSchedule({ CostUI.createCostSchedule({
data: data.data["cost_items"]["cost_items"], data: data.data["cost_items"]["cost_items"],
costScheduleId: data.data["cost_items"]["cost_schedule_id"], costScheduleId: costScheduleId,
blenderID: data.blenderId, blenderID: data.blenderId,
callbacks: { callbacks: {
addCostItem: addCostItem, addCostItem: addCostItem,
@@ -237,10 +329,15 @@ function handleCostItemsData(data) {
editCostItemName: editCostItemName, editCostItemName: editCostItemName,
enableEditingCostValues: enableEditingCostValues, enableEditingCostValues: enableEditingCostValues,
addSummaryCostItem: addSummaryCostItem, addSummaryCostItem: addSummaryCostItem,
getSelectedProducts: getSelectedProducts,
}, },
}); });
} }
function getSelectedProducts(costItemId) {
executeOperator({ type: "getSelectedProducts", costItemId: costItemId });
}
function duplicateCostItem(costItemId) { function duplicateCostItem(costItemId) {
executeOperator({ type: "duplicateCostItem", costItemId: costItemId }); executeOperator({ type: "duplicateCostItem", costItemId: costItemId });
} }
@@ -128,7 +128,7 @@ function handleWorkScheduleData(data) {
workSchedules.forEach((workSchedule) => { workSchedules.forEach((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.id, workSchedule.Name,mainContainer, callback);
workSchedulesDiv.append(card); workSchedulesDiv.append(card);
}); });
} }
@@ -69,7 +69,7 @@ export class CostUI {
`; `;
document.head.appendChild(style); document.head.appendChild(style);
} }
static createContextMenu(callbacks) { static createContextMenu(callbacks) {
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");
@@ -78,93 +78,107 @@ static createContextMenu(callbacks) {
<button id="add-cost-item-button">Add sub-cost</button> <button id="add-cost-item-button">Add sub-cost</button>
<button id="duplicate-button">Duplicate</button> <button id="duplicate-button">Duplicate</button>
<button id="delete-cost-item">Delete</button> <button id="delete-cost-item">Delete</button>
<button id="assign-selection">Assign Selection</button>
`; `;
document.body.appendChild(contextMenu); document.body.appendChild(contextMenu);
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"); 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";
contextMenu.style.left = `${event.pageX}px`; contextMenu.style.left = `${event.pageX}px`;
contextMenu.style.top = `${event.pageY}px`; contextMenu.style.top = `${event.pageY}px`;
contextMenu.targetRow = targetRow; contextMenu.targetRow = targetRow;
contextMenu.targetCell = targetCell; contextMenu.targetCell = targetCell;
} else { } else {
document.getElementById("context-menu").style.display = "none"; document.getElementById("context-menu").style.display = "none";
} }
}); });
document.addEventListener("click", function (event) { document.addEventListener("click", function (event) {
const contextMenu = document.getElementById("context-menu"); const contextMenu = document.getElementById("context-menu");
if (!contextMenu.contains(event.target)) { if (!contextMenu.contains(event.target)) {
contextMenu.style.display = "none"; contextMenu.style.display = "none";
} }
}); });
const editCostValuesButton = document.getElementById( const editCostValuesButton = document.getElementById(
"edit-cost-values-button" "edit-cost-values-button"
); );
editCostValuesButton.addEventListener("click", function () { editCostValuesButton.addEventListener("click", function () {
const targetRow = document.getElementById("context-menu").targetRow; const targetRow = document.getElementById("context-menu").targetRow;
const targetCell = document.getElementById("context-menu").targetCell; const targetCell = document.getElementById("context-menu").targetCell;
if (targetRow) { if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id")); const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.enableEditingCostValues callbacks.enableEditingCostValues
? callbacks.enableEditingCostValues(costItemId) ? callbacks.enableEditingCostValues(costItemId)
: null; : null;
} }
document.getElementById("context-menu").style.display = "none"; document.getElementById("context-menu").style.display = "none";
}); });
const addButton = document.getElementById("add-cost-item-button"); const addButton = document.getElementById("add-cost-item-button");
if (!addButton.dataset.listenerAdded) { if (!addButton.dataset.listenerAdded) {
function addCostItemHandler(e) { function addCostItemHandler(e) {
e.stopPropagation(); e.stopPropagation();
const targetRow = document.getElementById("context-menu").targetRow; const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) { if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id")); const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.addCostItem ? callbacks.addCostItem(costItemId) : null; callbacks.addCostItem ? callbacks.addCostItem(costItemId) : null;
}
document.getElementById("context-menu").style.display = "none";
} }
addButton.addEventListener("click", addCostItemHandler); document.getElementById("context-menu").style.display = "none";
addButton.dataset.listenerAdded = "true"; }
addButton.addEventListener("click", addCostItemHandler);
addButton.dataset.listenerAdded = "true";
} }
const deleteCostItem = document.getElementById("delete-cost-item"); const deleteCostItem = document.getElementById("delete-cost-item");
if (deleteCostItem && !deleteCostItem.hasListener) { if (deleteCostItem && !deleteCostItem.hasListener) {
deleteCostItem.addEventListener("click", function () { deleteCostItem.addEventListener("click", function () {
const targetRow = document.getElementById("context-menu").targetRow; const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) { if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id")); const costItemId = parseInt(targetRow.getAttribute("id"));
let expandedState = let expandedState =
JSON.parse(localStorage.getItem("expandedState")) || {}; JSON.parse(localStorage.getItem("expandedState")) || {};
expandedState = CostUI.deleteRow(targetRow, expandedState); expandedState = CostUI.deleteRow(targetRow, expandedState);
localStorage.setItem("expandedState", JSON.stringify(expandedState)); localStorage.setItem("expandedState", JSON.stringify(expandedState));
callbacks.deleteCostItem(costItemId); callbacks.deleteCostItem(costItemId);
} }
document.getElementById("context-menu").style.display = "none"; document.getElementById("context-menu").style.display = "none";
}); });
deleteCostItem.hasListener = true; deleteCostItem.hasListener = true;
} }
const duplicateButton = document.getElementById("duplicate-button"); const duplicateButton = document.getElementById("duplicate-button");
if (duplicateButton && !duplicateButton.hasListener) { if (duplicateButton && !duplicateButton.hasListener) {
duplicateButton.addEventListener("click", function () { duplicateButton.addEventListener("click", function () {
const targetRow = document.getElementById("context-menu").targetRow; const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) { if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id")); const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.duplicateCostItem(costItemId); callbacks.duplicateCostItem(costItemId);
} }
document.getElementById("context-menu").style.display = "none"; document.getElementById("context-menu").style.display = "none";
}); });
duplicateButton.hasListener = true; duplicateButton.hasListener = true;
} }
}
const getSelectedProducts = document.getElementById("assign-selection");
if (getSelectedProducts && !getSelectedProducts.hasListener) {
getSelectedProducts.addEventListener("click", function () {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.getSelectedProducts(costItemId);
}
document.getElementById("context-menu").style.display = "none";
});
getSelectedProducts.hasListener = true;
}
}
static deleteRow(targetRow, expandedState) { static deleteRow(targetRow, expandedState) {
if (!targetRow) { if (!targetRow) {
@@ -244,7 +258,6 @@ static createContextMenu(callbacks) {
const form = CostUI.Form({ const form = CostUI.Form({
id: "add-cost-schedule-form", id: "add-cost-schedule-form",
name: name, name: name,
className: "floating-form",
}); });
const nameLabel = document.createElement("label"); const nameLabel = document.createElement("label");
@@ -279,8 +292,6 @@ static createContextMenu(callbacks) {
submitButton.classList.add("action-button"); submitButton.classList.add("action-button");
form.appendChild(submitButton); form.appendChild(submitButton);
document.body.appendChild(form);
form.addEventListener("submit", function (event) { form.addEventListener("submit", function (event) {
event.preventDefault(); event.preventDefault();
const formData = new FormData(form); const formData = new FormData(form);
@@ -296,7 +307,7 @@ static createContextMenu(callbacks) {
static createColorPicker() { static createColorPicker() {
const div = document.createElement("div"); const div = document.createElement("div");
div.classList.add("card"); div.classList.add("row-container");
const colorPicker = document.createElement("input"); const colorPicker = document.createElement("input");
colorPicker.type = "color"; colorPicker.type = "color";
colorPicker.id = "color-picker"; colorPicker.id = "color-picker";
@@ -305,7 +316,6 @@ static createContextMenu(callbacks) {
colorText.textContent = "Select a color to change row color:"; colorText.textContent = "Select a color to change row color:";
div.appendChild(colorText); div.appendChild(colorText);
div.appendChild(colorPicker); div.appendChild(colorPicker);
document.getElementById("UI").appendChild(div);
colorPicker.addEventListener("input", function () { colorPicker.addEventListener("input", function () {
const baseColor = colorPicker.value; const baseColor = colorPicker.value;
@@ -314,6 +324,7 @@ static createContextMenu(callbacks) {
}); });
const colorScheme = CostUI.generateColorScheme("#000000"); const colorScheme = CostUI.generateColorScheme("#000000");
CostUI.applyColorScheme("cost-items", colorScheme); CostUI.applyColorScheme("cost-items", colorScheme);
return div;
} }
static createCostSchedule({ static createCostSchedule({
@@ -373,7 +384,10 @@ static createContextMenu(callbacks) {
const totalQuantity = costItem.TotalCostQuantity const totalQuantity = costItem.TotalCostQuantity
? parseFloat(costItem.TotalCostQuantity).toFixed(2) ? parseFloat(costItem.TotalCostQuantity).toFixed(2)
: "-"; : "-";
const row = CostUI.createTableRow(costItem, nestingLevel, parentID); const appliedValue = costItem.TotalAppliedValue
? parseFloat(costItem.TotalAppliedValue).toFixed(2)
: "-";
const row = CostUI.createCostItemRow(costItem, nestingLevel, parentID);
const expandButton = CostUI.createExpandButton(costItem); const expandButton = CostUI.createExpandButton(costItem);
const nameCell = CostUI.createNameCell( const nameCell = CostUI.createNameCell(
costItem, costItem,
@@ -383,9 +397,7 @@ static createContextMenu(callbacks) {
); );
const totalCostQuantityCell = CostUI.createTableCell(totalQuantity); const totalCostQuantityCell = CostUI.createTableCell(totalQuantity);
const unitSymbolCell = CostUI.createTableCell(costItem.UnitSymbol); const unitSymbolCell = CostUI.createTableCell(costItem.UnitSymbol);
const totalAppliedValueCell = CostUI.createTableCell( const totalAppliedValueCell = CostUI.createTableCell(appliedValue);
costItem.TotalAppliedValue
);
const totalCostCell = CostUI.createTotalCostCell(costItem); const totalCostCell = CostUI.createTotalCostCell(costItem);
const flexContainerCell = CostUI.createFlexContainerCell( const flexContainerCell = CostUI.createFlexContainerCell(
costItem, costItem,
@@ -411,7 +423,7 @@ static createContextMenu(callbacks) {
return row; return row;
} }
static createTableRow(costItem, nestingLevel, parentID) { static createCostItemRow(costItem, nestingLevel, parentID) {
const row = document.createElement("tr"); const row = document.createElement("tr");
row.setAttribute("id", costItem.id); row.setAttribute("id", costItem.id);
row.setAttribute("parent-id", parentID); row.setAttribute("parent-id", parentID);
@@ -613,17 +625,17 @@ static createContextMenu(callbacks) {
return selectButton; return selectButton;
} }
static highlightCostItem(id) { static highlightElement(id) {
const row = document.getElementById(id); const element = document.getElementById(id);
if (row) { if (element) {
row.classList.add("highlighted"); element.classList.add("highlighted");
} }
} }
static unhighlightCostItem(id) { static unhighlightElement(id) {
const row = document.getElementById(id); const element = document.getElementById(id);
if (row) { if (element) {
row.classList.remove("highlighted"); element.classList.remove("highlighted");
} }
} }
@@ -636,15 +648,26 @@ static createContextMenu(callbacks) {
return ids; return ids;
} }
static text(label) { static Text(label, icon = null, size = "medium") {
const text = document.createElement("p"); const text = document.createElement("span");
text.textContent = label; text.textContent = label;
return text; if (icon === null) {
return text;
}
const div = document.createElement("div");
div.classList.add("row-container");
const i = document.createElement("i");
i.className = icon;
div.appendChild(i);
div.appendChild(text);
div.style.fontSize = size;
return div;
} }
static createCard(title, mainContainer, callback) { static createCard(id, title, mainContainer, callback) {
const card = document.createElement("div"); const card = document.createElement("div");
card.classList.add("card"); card.classList.add("card");
card.id = "schedule-" + id;
const cardBody = document.createElement("div"); const cardBody = document.createElement("div");
cardBody.classList.add("card-body"); cardBody.classList.add("card-body");
@@ -684,30 +707,26 @@ static createContextMenu(callbacks) {
let formName = "Cost Values: " + costItemName; let formName = "Cost Values: " + costItemName;
const form = CostUI.Form({ const form = CostUI.Form({
icon: "fa-dollar-sign",
id: formId, id: formId,
name: formName, name: formName,
className: "floating-form",
}); });
const table = CostUI.createCostValuesTable(costItemId, [ const { tableContainer, table } = CostUI.addTable({
"Type", headers: ["Type", "Category", "Value", ""],
"Category", className: "cost-values-table",
"Value", id: "cost-values-table-" + costItemId,
"", });
]);
costValues.forEach((costValue) => { costValues.forEach((costValue) => {
const tr = CostUI.createCostvaluesRow(costItemId, costValue, callbacks); const tr = CostUI.createCostvaluesRow(costItemId, costValue, callbacks);
table.appendChild(tr); table.appendChild(tr);
}); });
form.appendChild(table);
const addButton = CostUI.createAddCostValueButton(costItemId, callbacks); const addButton = CostUI.createAddCostValueButton(costItemId, callbacks);
form.appendChild(tableContainer);
form.appendChild(addButton); form.appendChild(addButton);
document.body.appendChild(form);
return form; return form;
} }
@@ -735,10 +754,13 @@ static createContextMenu(callbacks) {
table.appendChild(tr); table.appendChild(tr);
} }
static createCostValuesTable(costItemId, headers) { static addTable({ id, className, headers }) {
const div = document.createElement("div");
div.classList.add("table-container");
const table = document.createElement("table"); const table = document.createElement("table");
table.classList.add("cost-values-table"); div.appendChild(table);
table.id = "cost-values-table-" + costItemId; table.classList.add(className);
table.id = id;
const headerRow = document.createElement("tr"); const headerRow = document.createElement("tr");
headers.forEach((headerText) => { headers.forEach((headerText) => {
@@ -746,9 +768,17 @@ static createContextMenu(callbacks) {
th.textContent = headerText; th.textContent = headerText;
headerRow.appendChild(th); headerRow.appendChild(th);
}); });
table.appendChild(headerRow); const thead = document.createElement("thead");
thead.appendChild(headerRow);
return table; table.appendChild(thead);
table.get_header_row = function () {
return headerRow;
};
table.get_header_by_name = function (name) {
const headers = headerRow.querySelectorAll("th");
return Array.from(headers).find((header) => header.textContent === name);
};
return { tableContainer: div, table: table };
} }
static createCostvaluesRow(costItemId, costValue, costValueCallbacks) { static createCostvaluesRow(costItemId, costValue, costValueCallbacks) {
@@ -917,19 +947,31 @@ static createContextMenu(callbacks) {
return addButton; return addButton;
} }
static Form({ id, name, className }) { static Form({ id, name, icon = "fa-solid fa-file", shouldHide = false }) {
const form = document.createElement("form"); let form;
form.id = id; if (document.getElementById(id)) {
form.classList.add(className); form = document.getElementById(id);
form.style.position = "absolute"; } else {
form.style.top = "50%"; form = document.createElement("form");
form.style.left = "50%"; form.id = id;
form.style.transform = "translate(-50%, -50%)"; form.classList.add("floating-form");
const header = CostUI.createHeader(name); form.style.position = "absolute";
const closeButton = CostUI.createCloseButton(form); form.style.top = "50%";
form.style.left = "50%";
form.style.transform = "translate(-50%, -50%)";
}
const header = CostUI.createHeader(name, icon);
let closeButton;
if (shouldHide) {
closeButton = CostUI.createHideButton(form);
} else {
closeButton = CostUI.createCloseButton(form);
}
form.appendChild(header); form.appendChild(header);
form.appendChild(closeButton); form.appendChild(closeButton);
CostUI.makeFormDraggable(form); CostUI.makeFormDraggable(form);
document.body.appendChild(form);
return form; return form;
} }
static makeFormDraggable(form) { static makeFormDraggable(form) {
@@ -990,23 +1032,389 @@ static createContextMenu(callbacks) {
} }
} }
static createHeader(text) { static createHeader(text, icon) {
const header = document.createElement("div"); const header = document.createElement("div");
const i = document.createElement("i");
i.className = icon;
header.classList.add("form-header"); header.classList.add("form-header");
header.textContent = text; const span = document.createElement("span");
span.textContent = text;
header.appendChild(i);
header.appendChild(span);
return header; return header;
} }
static createHideButton(form) {
const hideButton = document.createElement("span");
hideButton.classList.add("close-button");
hideButton.innerHTML = "&minus;";
hideButton.addEventListener("click", function () {
form.style.display = "none";
});
return hideButton;
}
static createCloseButton(form) { static createCloseButton(form) {
const closeButton = document.createElement("span"); const closeButton = document.createElement("span");
closeButton.classList.add("close-button"); closeButton.classList.add("close-button");
closeButton.innerHTML = "&times;"; closeButton.innerHTML = "&times;";
closeButton.addEventListener("click", function () { closeButton.addEventListener("click", function () {
form.remove(); form.remove();
const costItemId = form.id.split("-")[3]; const costItemId = form.id.split("-").pop();
CostUI.unhighlightCostItem(costItemId); CostUI.unhighlightElement(costItemId);
}); });
return closeButton; return closeButton;
} }
static createProductRow(product, shouldAddQto = false) {
const row = document.createElement("tr");
const cellData = {
id: product.info.id,
class: product.info.class,
name: product.info.name,
type: product.info.type,
count: "1",
};
for (const [key, value] of Object.entries(cellData)) {
const cell = document.createElement("td");
cell.textContent = value;
row.appendChild(cell);
}
if (shouldAddQto) {
const quantitySets = product.qtos;
Object.entries(quantitySets).forEach(([qtoName, qto]) => {
Object.entries(qto).forEach(([key, value]) => {
if (key !== "id") {
const cell = document.createElement("td");
// check if value is a number
cell.textContent = parseFloat(value).toFixed(2);
row.appendChild(cell);
}
});
});
}
return row;
}
static addQuantityHeaders(table, products, shouldAddQto = true) {
const headerRow = table.get_header_row();
const countHeader = table.get_header_by_name("Count");
const quantitySums = { count: 0 };
const quantityHeaders = { count: countHeader };
if (!shouldAddQto) return { quantitySums, quantityHeaders };
if (products.length > 0) {
const quantitySets = products[0].qtos;
Object.entries(quantitySets).forEach(([qtoName, qto]) => {
Object.keys(qto).forEach((key) => {
if (key !== "id") {
const qtoHeader = document.createElement("th");
qtoHeader.textContent = key;
qtoHeader.style.cursor = "pointer";
headerRow.appendChild(qtoHeader);
quantitySums[key] = 0;
quantityHeaders[key] = qtoHeader;
}
});
});
}
return { quantitySums, quantityHeaders };
}
static getAssinedQuantity(products, costItemId) {
let quantityAssigned = null;
products.forEach((product) => {
const product_id = product.info.id;
const assignedQuantities = product.assigned_quantities;
if (assignedQuantities.length > 0) {
assignedQuantities.forEach((assignedQuantity) => {
if (assignedQuantity.cost_item_id === costItemId) {
product.isProductAssigned = true;
quantityAssigned = assignedQuantity.name;
}
});
}
});
return quantityAssigned;
}
static calculateSums(qtos, quantitySums) {
Object.entries(qtos).forEach(([qtoName, qto]) => {
Object.entries(qto).forEach(([key, value]) => {
if (key !== "id") {
quantitySums[key] += parseFloat(value);
}
});
});
}
static countProducts(products, quantitySums) {
products.forEach((product) => {
quantitySums.count += 1;
});
}
static addSubTotalRow(quantitySums, shouldAddQto = true) {
const subtotalRow = document.createElement("tr");
const cells = [
{ textContent: "Subtotal", colSpan: 4, style: { border: "none" } },
{ textContent: "", style: { border: "none" } },
{ textContent: "", style: { border: "none" } },
{ textContent: "", style: { border: "none" } },
{ textContent: quantitySums.count },
];
cells.forEach((cellData) => {
const cell = document.createElement("td");
if (cellData.textContent !== undefined)
cell.textContent = cellData.textContent;
if (cellData.colSpan !== undefined) cell.colSpan = cellData.colSpan;
if (cellData.style !== undefined)
Object.assign(cell.style, cellData.style);
subtotalRow.appendChild(cell);
});
if (shouldAddQto) {
Object.keys(quantitySums).forEach((key) => {
if (key !== "count") {
const subtotalCell = document.createElement("td");
subtotalCell.textContent = quantitySums[key].toFixed(2);
subtotalRow.appendChild(subtotalCell);
}
});
}
subtotalRow.style.fontWeight = "bold";
return subtotalRow;
}
static createProductTable({
form,
products,
quantityNames,
costItemId,
callbacks,
}) {
const { tableContainer, table } = CostUI.addTable({
headers: ["Step Id", "Class", "Name", "Type", "Count"],
className: "product-table",
id: "cost-values-table-" + costItemId,
});
const tbody = document.createElement("tbody");
table.appendChild(tbody);
form.appendChild(tableContainer);
const quantityAssigned = CostUI.getAssinedQuantity(products, costItemId);
const areProductsSameClass = products.every(
(product) => product.info.class === products[0].info.class
);
const shouldAddQto = areProductsSameClass ? true : false;
const headerRow = table.get_header_row();
const { quantitySums, quantityHeaders } = CostUI.addQuantityHeaders(
table,
products,
shouldAddQto
);
if (shouldAddQto) {
CostUI.highlightColumn(
table,
headerRow,
quantityHeaders,
quantityAssigned
);
products.forEach((product) => {
const row = CostUI.createProductRow(product, shouldAddQto);
tbody.appendChild(row);
const isProductAssigned = product.assigned_quantities
? product.assigned_quantities.some(
(assignedQuantity) => assignedQuantity.cost_item_id === costItemId
)
: false;
isProductAssigned ? row.classList.add("highlighted") : null;
CostUI.calculateSums(product.qtos, quantitySums);
});
} else {
products.forEach((product) => {
const row = CostUI.createProductRow(product, shouldAddQto);
tbody.appendChild(row);
});
}
CostUI.countProducts(products, quantitySums);
const subtotalRow = CostUI.addSubTotalRow(quantitySums, shouldAddQto);
tbody.appendChild(subtotalRow);
const quantitySelect = document.createElement("select");
if (shouldAddQto) {
quantityNames.forEach((name) => {
const option = document.createElement("option");
option.value = name;
option.text = name;
quantitySelect.appendChild(option);
});
}
const option = document.createElement("option");
option.value = "count";
option.text = "count";
quantitySelect.appendChild(option);
quantitySelect.addEventListener("change", (event) => {
const selectedQuantity = event.target.value;
Object.values(quantityHeaders).forEach((header) => {
header.classList.remove("highlighted");
});
table.querySelectorAll("td").forEach((cell) => {
cell.classList.remove("highlighted");
});
if (quantityHeaders[selectedQuantity]) {
quantityHeaders[selectedQuantity].classList.add("highlighted");
const columnIndex = Array.from(headerRow.children).indexOf(
quantityHeaders[selectedQuantity]
);
table.querySelectorAll(`tr`).forEach((row) => {
row.children[columnIndex].classList.add("highlighted");
});
}
});
Object.entries(quantityHeaders).forEach(([key, header]) => {
header.addEventListener("click", () => {
quantitySelect.value = key;
const event = new Event("change");
quantitySelect.dispatchEvent(event);
});
});
quantityAssigned
? CostUI.highlightColumn(
table,
headerRow,
quantityHeaders,
quantityAssigned
)
: null;
quantitySelect.value = quantityAssigned ? quantityAssigned : "count";
const productIds = products.map((product) => product.info.id);
callbacks.emptyForm = () => {
form.innerHTML = "";
};
const addProductAssignmentsButton = CostUI.addProductAssignmentsButton(
costItemId,
productIds,
quantitySelect,
callbacks
);
form.appendChild(quantitySelect);
form.appendChild(addProductAssignmentsButton);
return table;
}
static createAssignmentsTable({
form,
products,
costItemId,
quantityNames,
callbacks,
}) {
const { tableContainer, table } = CostUI.addTable({
headers: ["Step Id", "Class", "Name", "Type", "Count"],
className: "product-table",
id: "cost-values-table-" + costItemId,
});
const tbody = document.createElement("tbody");
table.appendChild(tbody);
form.appendChild(tableContainer);
let quantityAssigned = CostUI.getAssinedQuantity(products, costItemId);
const shouldAddQto = quantityAssigned ? true : false;
const headerRow = table.get_header_row();
const { quantitySums, quantityHeaders } = CostUI.addQuantityHeaders(
table,
products,
shouldAddQto
);
shouldAddQto
? CostUI.highlightColumn(
table,
headerRow,
quantityHeaders,
quantityAssigned
)
: null;
products.forEach((product) => {
const row = CostUI.createProductRow(product, shouldAddQto);
tbody.appendChild(row);
shouldAddQto ? CostUI.calculateSums(product.qtos, quantitySums) : null;
});
CostUI.countProducts(products, quantitySums);
const subtotalRow = CostUI.addSubTotalRow(quantitySums, shouldAddQto);
tbody.appendChild(subtotalRow);
}
static highlightColumn(table, headerRow, quantityHeaders, quantityAssigned) {
if (quantityAssigned) {
if (quantityHeaders[quantityAssigned]) {
quantityHeaders[quantityAssigned].classList.add("highlighted");
const columnIndex = Array.from(headerRow.children).indexOf(
quantityHeaders[quantityAssigned]
);
table.querySelectorAll(`tr`).forEach((row) => {
row.children[columnIndex].classList.add("highlighted");
});
}
}
}
static addProductAssignmentsButton(
costItemId,
productIds,
quantitySelect,
callbacks
) {
const addProductAssignmentsButton = document.createElement("button");
addProductAssignmentsButton.textContent = "Add Product Assignments";
addProductAssignmentsButton.addEventListener("click", (event) => {
event.preventDefault();
let propName = quantitySelect.value;
if (propName === "count") {
propName = "";
}
callbacks.addProductAssignments
? callbacks.addProductAssignments({
costItemId,
selectedQuantity: propName,
productIds,
})
: null;
callbacks.emptyForm ? callbacks.emptyForm() : null;
callbacks.getSelectedProducts(costItemId);
});
addProductAssignmentsButton.classList.add("action-button");
return addProductAssignmentsButton;
}
} }
@@ -75,6 +75,9 @@
<button id="toggle-theme" onclick="toggleTheme()"> <button id="toggle-theme" onclick="toggleTheme()">
<i class="fas fa-moon"></i> <i class="fas fa-moon"></i>
</button> </button>
<button id="settings-button">
<i class="fa-solid fa-gear"></i>
</button>
</nav> </nav>
<div id="connected-list-div"> <div id="connected-list-div">
<button id="show-connected-button" onclick="toggleClientList()"> <button id="show-connected-button" onclick="toggleClientList()">
@@ -91,8 +94,8 @@
</div> </div>
<div id="cost-schedules" class="row-container"></div> <div id="cost-schedules" class="row-container"></div>
<div id="UI" class="row-container"></div>
<div id="cost-items"></div> <div id="cost-items" class="table-container"></div>
<footer> <footer>
<p>BlenderBIM Version: {{version}}</p> <p>BlenderBIM Version: {{version}}</p>
+3 -12
View File
@@ -318,18 +318,9 @@ class CostItemQuantitiesData:
@classmethod @classmethod
def product_quantity_names(cls): def product_quantity_names(cls):
total_selected_objects = len(bpy.context.selected_objects) elements = tool.Spatial.get_selected_products()
names = set() names = ifcopenshell.util.cost.get_product_quantity_names(elements)
for obj in bpy.context.selected_objects: return [(n, n, "") for n in names]
element = tool.Ifc.get_entity(obj)
if not element:
continue
potential_names = set()
qtos = ifcopenshell.util.element.get_psets(element, qtos_only=True)
for qset, quantities in qtos.items():
potential_names.update(quantities.keys())
names = names.intersection(potential_names) if names else potential_names
return [(n, n, "") for n in names if n != "id"]
@classmethod @classmethod
def process_quantity_names(cls): def process_quantity_names(cls):
+1 -1
View File
@@ -288,7 +288,7 @@ class Cost(bonsai.core.tool.Cost):
unit = "" unit = ""
for quantities in ifcopenshell.util.element.get_psets(product, qtos_only=True).values(): for quantities in ifcopenshell.util.element.get_psets(product, qtos_only=True).values():
for qto in tool.Ifc.get().by_id(quantities["id"]).Quantities or []: for qto in tool.Ifc.get().by_id(quantities["id"]).Quantities or []:
for quantity in cost_item.CostQuantities: for quantity in cost_item.CostQuantities or []:
if quantity == qto: if quantity == qto:
selected_quantitites.append(quantity) selected_quantitites.append(quantity)
if not unit: if not unit:
+81 -5
View File
@@ -19,6 +19,7 @@
import ifcopenshell.util.cost import ifcopenshell.util.cost
import bpy import bpy
from bonsai.bim.module.web.data import WebData from bonsai.bim.module.web.data import WebData
from ifcopenshell.util.element import get_psets, get_type
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
@@ -342,6 +343,48 @@ class Web(bonsai.core.tool.Web):
Args: Args:
operator_data (dict): A dictionary containing the operator data. operator_data (dict): A dictionary containing the operator data.
""" """
def selection_data(cost_item, elements):
print(cost_item, elements)
if not elements:
return []
if not cost_item:
return [
{
"info": {
"id": element.id(),
"name": element.Name,
"class": element.is_a(),
"type": get_type(element).Name if get_type(element) else None,
},
"qtos": get_psets(element, qtos_only=True),
}
for element in elements or []
]
data = []
for element in elements or []:
psets = get_psets(element, qtos_only=True)
element_type = get_type(element)
quantities, unit = tool.Cost.get_assigned_quantities(cost_item, element)
data.append(
{
"info": {
"id": element.id(),
"name": element.Name,
"class": element.is_a(),
"type": element_type.Name if element_type else None,
},
"qtos": psets,
"assigned_quantities": [{
'cost_item_id': cost_item.id(),
'product_id': element.id(),
"name": q.Name,
"value": q[3],
"type": q.Unit,
} for q in quantities or []],
"unit": unit,
}
)
return data
ifc_file = tool.Ifc.get() ifc_file = tool.Ifc.get()
if operator_data["type"] == "getPredefinedTypes": if operator_data["type"] == "getPredefinedTypes":
print("getting predefined types") print("getting predefined types")
@@ -349,10 +392,9 @@ class Web(bonsai.core.tool.Web):
cls.send_webui_data(data=predefined_types, data_key="predefined_types", event="predefined_types") cls.send_webui_data(data=predefined_types, data_key="predefined_types", event="predefined_types")
if operator_data["type"] == "addCostSchedule": if operator_data["type"] == "addCostSchedule":
bonsai.core.cost.add_cost_schedule( bonsai.core.cost.add_cost_schedule(
tool.Ifc, name=operator_data["name"], predefined_type=operator_data["type"] tool.Ifc, name=operator_data["name"], predefined_type=operator_data["predefinedType"]
) )
cost_schedules = ifc_file.by_type("IfcCostSchedule") cost_schedules_json = [cs.get_info(recursive=True) for cs in ifc_file.by_type("IfcCostSchedule")]
cost_schedules_json = [cs.get_info(recursive=True) for cs in cost_schedules]
currency = tool.Cost.currency() currency = tool.Cost.currency()
cls.send_webui_data( cls.send_webui_data(
data={"cost_schedules": cost_schedules_json, "currency": currency}, data={"cost_schedules": cost_schedules_json, "currency": currency},
@@ -361,8 +403,7 @@ class Web(bonsai.core.tool.Web):
) )
if operator_data["type"] == "getCostSchedules": if operator_data["type"] == "getCostSchedules":
print("getting cost schedules") print("getting cost schedules")
cost_schedules = ifc_file.by_type("IfcCostSchedule") cost_schedules_json = [cs.get_info(recursive=True) for cs in ifc_file.by_type("IfcCostSchedule")]
cost_schedules_json = [cs.get_info(recursive=True) for cs in cost_schedules]
currency = tool.Cost.currency() currency = tool.Cost.currency()
cls.send_webui_data( cls.send_webui_data(
data={"cost_schedules": cost_schedules_json, "currency": currency}, data={"cost_schedules": cost_schedules_json, "currency": currency},
@@ -379,11 +420,34 @@ class Web(bonsai.core.tool.Web):
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"] == "getSelectedProducts":
cost_item_id = operator_data["costItemId"]
cost_item = ifc_file.by_id(cost_item_id)
if not cost_item:
print('---> cost item not found')
return
selected_products = list(tool.Spatial.get_selected_products())
selected_products_data = selection_data(cost_item, selected_products)
assigned_products = tool.Cost.get_cost_item_products(cost_item, is_deep=False)
assigned_products_data = selection_data(cost_item, assigned_products)
names = ifcopenshell.util.cost.get_product_quantity_names(selected_products)
cls.send_webui_data(data={
"selected_products": selected_products_data,
'assigned_products': assigned_products_data,
"product_quantity_names": names,
"cost_item_id": cost_item_id
}, data_key="selected_products", event="selected_products")
if operator_data["type"] == "addSummaryCostItem": if operator_data["type"] == "addSummaryCostItem":
cost_schedule = ifc_file.by_id(operator_data["costScheduleId"]) cost_schedule = ifc_file.by_id(operator_data["costScheduleId"])
bonsai.core.cost.add_summary_cost_item(tool.Ifc, tool.Cost, cost_schedule=cost_schedule) bonsai.core.cost.add_summary_cost_item(tool.Ifc, tool.Cost, cost_schedule=cost_schedule)
cls.load_cost_schedule_web_ui(cost_schedule) cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "addCostItem": if operator_data["type"] == "addCostItem":
if not operator_data["costItemId"]:
return
bpy.ops.bim.add_cost_item(cost_item=operator_data["costItemId"]) 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"])) 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) cls.load_cost_schedule_web_ui(cost_schedule)
@@ -443,6 +507,18 @@ class Web(bonsai.core.tool.Web):
print(value.get_info()) print(value.get_info())
tool.Cost.load_cost_schedule_tree() tool.Cost.load_cost_schedule_tree()
cls.load_cost_schedule_web_ui(cost_schedule) cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "addProductAssignments":
prop_name = operator_data["propName"]
if prop_name == "count":
prop_name = ""
print(prop_name)
bpy.ops.bim.assign_cost_item_quantity(
cost_item=operator_data["costItemId"],
related_object_type= "PRODUCT",
prop_name=prop_name
)
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)
@classmethod @classmethod
def load_cost_schedule_web_ui(cls, cost_schedule): def load_cost_schedule_web_ui(cls, cost_schedule):
@@ -21,7 +21,7 @@ import ifcopenshell
from typing import Optional, Union, Literal, Generator, Any from typing import Optional, Union, Literal, Generator, Any
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
from ifcopenshell.util.doc import get_predefined_type_doc from ifcopenshell.util.doc import get_predefined_type_doc
from ifcopenshell.util.element import get_psets
arithmetic_operator_symbols = {"ADD": "+", "DIVIDE": "/", "MULTIPLY": "*", "SUBTRACT": "-"} arithmetic_operator_symbols = {"ADD": "+", "DIVIDE": "/", "MULTIPLY": "*", "SUBTRACT": "-"}
symbol_arithmetic_operators = {"+": "ADD", "/": "DIVIDE", "*": "MULTIPLY", "-": "SUBTRACT"} symbol_arithmetic_operators = {"+": "ADD", "/": "DIVIDE", "*": "MULTIPLY", "-": "SUBTRACT"}
FILTER_BY_TYPE = Literal["PRODUCT", "RESOURCE", "PROCESS"] FILTER_BY_TYPE = Literal["PRODUCT", "RESOURCE", "PROCESS"]
@@ -240,11 +240,13 @@ def get_cost_item_assignments(
if not is_deep: if not is_deep:
return get_cost_assignments_by_type(cost_item, filter_by_type) return get_cost_assignments_by_type(cost_item, filter_by_type)
else: else:
return [ current_assignments = get_cost_assignments_by_type(cost_item, filter_by_type)
nested_assignments = [
product product
for nested_cost_item in get_all_nested_cost_items(cost_item) for nested_cost_item in get_all_nested_cost_items(cost_item)
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)
] ]
return current_assignments + nested_assignments
def get_cost_values(cost_item: ifcopenshell.entity_instance) -> list[dict[str, str]]: def get_cost_values(cost_item: ifcopenshell.entity_instance) -> list[dict[str, str]]:
@@ -285,6 +287,17 @@ def get_cost_schedule_types(file):
return results return results
def get_product_quantity_names(elements: list[ifcopenshell.entity_instance]) -> list[str]:
names = set()
for element in elements or []:
potential_names = set()
qtos = get_psets(element, qtos_only=True)
for qset, quantities in qtos.items():
potential_names.update(quantities.keys())
names = names.intersection(potential_names) if names else potential_names
return [n for n in names if n != "id"]
class CostValueUnserialiser: class CostValueUnserialiser:
def parse(self, formula: str): def parse(self, formula: str):
l = lark.Lark( l = lark.Lark(