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
@@ -180,6 +180,11 @@ class BlenderNamespace(socketio.AsyncNamespace):
print(f"Predefined types from Blender client {sid}")
blender_messages[sid]["predefined_types"] = data
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):
with open("templates/index.html", "r") as f:
@@ -1,115 +1,135 @@
table {
width: 100%;
border-collapse: collapse;
user-select: none;
width: 100%;
height: 80%;
border-collapse: collapse;
user-select: none;
}
td, th {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
.table-container {
max-height: 70vh;
margin-top: 1em;
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 {
align-items: center;
align-items: center;
}
/* first td of a row, must be flex */
td:first-child {
display: flex;
align-items: center;
display: flex;
align-items: center;
}
.nested {
display: none;
display: none;
}
.expand-collapse {
cursor: pointer;
cursor: pointer;
}
.caret-cell {
border: none;
background: none;
padding: 0;
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;
min-width: 50px; /* Minimum width to prevent it from becoming too small */
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;
min-width: 50px; /* Minimum width to prevent it from becoming too small */
}
input:focus {
border-color: #28a745;
outline: none;
box-shadow: 0 0 0 3px #28a745;
border-color: #28a745;
outline: none;
box-shadow: 0 0 0 3px #28a745;
}
#cost-items tr:hover, .highlighted {
background-color: #ffb936;
color : black;
#cost-items tr:hover,
.highlighted {
background-color: #ffb936;
color: black;
}
.highlight {
background-color: #ffb936;
color : black;
background-color: #ffb936;
color: black;
}
.cost-values-table {
width: 100%;
border-collapse: collapse;
width: 100%;
border-collapse: collapse;
}
.cost-values-table th, .cost-values-table td {
border: 1px solid #ddd;
padding: 8px;
.cost-values-table th,
.cost-values-table td {
border: 1px solid #ddd;
padding: 8px;
}
.cost-values-table th {
background-color: #444444;
text-align: center;
background-color: #444444;
text-align: center;
}
/* Responsive Styles */
@media (max-width: 768px) {
table {
font-size: 12px;
}
table {
font-size: 12px;
}
td, th {
padding: 4px;
}
input {
padding: 2px;
font-size: 12px;
}
td,
th {
padding: 4px;
}
input {
padding: 2px;
font-size: 12px;
}
}
@media (max-width: 480px) {
table {
font-size: 10px;
}
table {
font-size: 10px;
}
td, th {
padding: 2px;
}
td,
th {
padding: 2px;
}
input {
padding: 1px;
font-size: 10px;
}
}
input {
padding: 1px;
font-size: 10px;
}
}
@@ -1,130 +1,75 @@
@import url("./shared.css");
@import url("./components/card.css");
@import url("./components/contextMenu.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;
}
@import url("./components/costTable.css");
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
background-color: #363636;
padding: 10px;
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 10px;
background-color: #363636;
padding: 10px;
/* trext size */
font-size: 1.5em;
}
span {
margin-left: var(--margin-small);
}
.floating-form {
min-width: 40%;
height: auto;
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;
}
.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: #444444;
text-align: center;
}
.close-button {
position: absolute;
top: 10px;
right: 10px;
font-size: 20px;
cursor: pointer;
min-width: 40%;
max-width: 80%;
max-height: 80vh;
height: auto;
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;
overflow-y: auto;
overflow-x: hidden;
}
.action-button {
background-color: #28a745;
border: none;
color: #fff;
padding: 0.5em 1em;
cursor: pointer;
border-radius: 6px;
font-weight: bold;
text-decoration: none;
display: inline-block;
transition: background-color 0.3s ease;
width: fit-content;
margin-left: 0.5em;
background-color: #28a745;
border: none;
color: #fff;
padding: 0.5em 1em;
cursor: pointer;
border-radius: 6px;
font-weight: bold;
text-decoration: none;
display: inline-block;
transition: background-color 0.3s ease;
width: fit-content;
margin: 0.5em;
}
.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 = {};
let socket;
let isScheduleLoad = {};
let loadedSchedules = {};
$(document).ready(function () {
var defaultTheme = "blender";
@@ -11,7 +11,21 @@ $(document).ready(function () {
setTheme(theme);
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() {
@@ -28,6 +42,46 @@ function connectSocket() {
socket.on("cost_items", handleCostItemsData);
socket.on("cost_values", handleCostValuesData);
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) {
@@ -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) {
executeOperator({
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) {
@@ -200,6 +263,9 @@ function addSummaryCostItem(costScheduleId) {
function handleCostSchedulesData(data) {
const blenderId = data.blenderId;
const loadedSchedule = loadedSchedules[blenderId]
? loadedSchedules[blenderId]
: null;
const costSchedules = data.data["cost_schedules"]["cost_schedules"];
if (costSchedules.length === 0) {
return;
@@ -210,24 +276,50 @@ function handleCostSchedulesData(data) {
const costScheduleDiv = document.getElementById("cost-schedules");
costScheduleDiv.innerHTML = "";
costSchedules.forEach((costSchedule) => {
costSchedule.UpdateDate = new Date(costSchedule.UpdateDate);
const predefinedType = CostUI.text("Predefined Type: " + costSchedule.PredefinedType)
const updatedOn = CostUI.text("Updated On: " + costSchedule.UpdateDate);
const predefinedType = CostUI.Text(
"Predefined Type: " + costSchedule.PredefinedType,
"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");
div.appendChild(predefinedType);
div.appendChild(updatedOn);
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);
});
loadedSchedule ? CostUI.highlightElement("schedule-" + loadedSchedule) : null;
}
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({
data: data.data["cost_items"]["cost_items"],
costScheduleId: data.data["cost_items"]["cost_schedule_id"],
costScheduleId: costScheduleId,
blenderID: data.blenderId,
callbacks: {
addCostItem: addCostItem,
@@ -237,10 +329,15 @@ function handleCostItemsData(data) {
editCostItemName: editCostItemName,
enableEditingCostValues: enableEditingCostValues,
addSummaryCostItem: addSummaryCostItem,
getSelectedProducts: getSelectedProducts,
},
});
}
function getSelectedProducts(costItemId) {
executeOperator({ type: "getSelectedProducts", costItemId: costItemId });
}
function duplicateCostItem(costItemId) {
executeOperator({ type: "duplicateCostItem", costItemId: costItemId });
}
@@ -128,7 +128,7 @@ function handleWorkScheduleData(data) {
workSchedules.forEach((workSchedule) => {
const mainContainer = CostUI.text(new Date(workSchedule.CreationDate).toLocaleDateString());
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);
});
}
@@ -69,7 +69,7 @@ export class CostUI {
`;
document.head.appendChild(style);
}
static createContextMenu(callbacks) {
static createContextMenu(callbacks) {
const contextMenu = document.createElement("div");
contextMenu.id = "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="duplicate-button">Duplicate</button>
<button id="delete-cost-item">Delete</button>
<button id="assign-selection">Assign Selection</button>
`;
document.body.appendChild(contextMenu);
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";
contextMenu.style.left = `${event.pageX}px`;
contextMenu.style.top = `${event.pageY}px`;
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";
contextMenu.style.left = `${event.pageX}px`;
contextMenu.style.top = `${event.pageY}px`;
contextMenu.targetRow = targetRow;
contextMenu.targetCell = targetCell;
} else {
document.getElementById("context-menu").style.display = "none";
}
contextMenu.targetRow = targetRow;
contextMenu.targetCell = targetCell;
} else {
document.getElementById("context-menu").style.display = "none";
}
});
document.addEventListener("click", function (event) {
const contextMenu = document.getElementById("context-menu");
if (!contextMenu.contains(event.target)) {
contextMenu.style.display = "none";
}
const contextMenu = document.getElementById("context-menu");
if (!contextMenu.contains(event.target)) {
contextMenu.style.display = "none";
}
});
const editCostValuesButton = document.getElementById(
"edit-cost-values-button"
"edit-cost-values-button"
);
editCostValuesButton.addEventListener("click", function () {
const targetRow = document.getElementById("context-menu").targetRow;
const targetCell = document.getElementById("context-menu").targetCell;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.enableEditingCostValues
? callbacks.enableEditingCostValues(costItemId)
: null;
}
document.getElementById("context-menu").style.display = "none";
const targetRow = document.getElementById("context-menu").targetRow;
const targetCell = document.getElementById("context-menu").targetCell;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.enableEditingCostValues
? callbacks.enableEditingCostValues(costItemId)
: null;
}
document.getElementById("context-menu").style.display = "none";
});
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";
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;
}
addButton.addEventListener("click", addCostItemHandler);
addButton.dataset.listenerAdded = "true";
document.getElementById("context-menu").style.display = "none";
}
addButton.addEventListener("click", addCostItemHandler);
addButton.dataset.listenerAdded = "true";
}
const deleteCostItem = document.getElementById("delete-cost-item");
if (deleteCostItem && !deleteCostItem.hasListener) {
deleteCostItem.addEventListener("click", function () {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
let expandedState =
JSON.parse(localStorage.getItem("expandedState")) || {};
expandedState = CostUI.deleteRow(targetRow, expandedState);
localStorage.setItem("expandedState", JSON.stringify(expandedState));
callbacks.deleteCostItem(costItemId);
}
document.getElementById("context-menu").style.display = "none";
});
deleteCostItem.hasListener = true;
deleteCostItem.addEventListener("click", function () {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
let expandedState =
JSON.parse(localStorage.getItem("expandedState")) || {};
expandedState = CostUI.deleteRow(targetRow, expandedState);
localStorage.setItem("expandedState", JSON.stringify(expandedState));
callbacks.deleteCostItem(costItemId);
}
document.getElementById("context-menu").style.display = "none";
});
deleteCostItem.hasListener = true;
}
const duplicateButton = document.getElementById("duplicate-button");
if (duplicateButton && !duplicateButton.hasListener) {
duplicateButton.addEventListener("click", function () {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.duplicateCostItem(costItemId);
}
document.getElementById("context-menu").style.display = "none";
});
duplicateButton.hasListener = true;
duplicateButton.addEventListener("click", function () {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.duplicateCostItem(costItemId);
}
document.getElementById("context-menu").style.display = "none";
});
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) {
if (!targetRow) {
@@ -244,7 +258,6 @@ static createContextMenu(callbacks) {
const form = CostUI.Form({
id: "add-cost-schedule-form",
name: name,
className: "floating-form",
});
const nameLabel = document.createElement("label");
@@ -279,8 +292,6 @@ static createContextMenu(callbacks) {
submitButton.classList.add("action-button");
form.appendChild(submitButton);
document.body.appendChild(form);
form.addEventListener("submit", function (event) {
event.preventDefault();
const formData = new FormData(form);
@@ -296,7 +307,7 @@ static createContextMenu(callbacks) {
static createColorPicker() {
const div = document.createElement("div");
div.classList.add("card");
div.classList.add("row-container");
const colorPicker = document.createElement("input");
colorPicker.type = "color";
colorPicker.id = "color-picker";
@@ -305,7 +316,6 @@ static createContextMenu(callbacks) {
colorText.textContent = "Select a color to change row color:";
div.appendChild(colorText);
div.appendChild(colorPicker);
document.getElementById("UI").appendChild(div);
colorPicker.addEventListener("input", function () {
const baseColor = colorPicker.value;
@@ -314,6 +324,7 @@ static createContextMenu(callbacks) {
});
const colorScheme = CostUI.generateColorScheme("#000000");
CostUI.applyColorScheme("cost-items", colorScheme);
return div;
}
static createCostSchedule({
@@ -373,7 +384,10 @@ static createContextMenu(callbacks) {
const totalQuantity = costItem.TotalCostQuantity
? 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 nameCell = CostUI.createNameCell(
costItem,
@@ -383,9 +397,7 @@ static createContextMenu(callbacks) {
);
const totalCostQuantityCell = CostUI.createTableCell(totalQuantity);
const unitSymbolCell = CostUI.createTableCell(costItem.UnitSymbol);
const totalAppliedValueCell = CostUI.createTableCell(
costItem.TotalAppliedValue
);
const totalAppliedValueCell = CostUI.createTableCell(appliedValue);
const totalCostCell = CostUI.createTotalCostCell(costItem);
const flexContainerCell = CostUI.createFlexContainerCell(
costItem,
@@ -411,7 +423,7 @@ static createContextMenu(callbacks) {
return row;
}
static createTableRow(costItem, nestingLevel, parentID) {
static createCostItemRow(costItem, nestingLevel, parentID) {
const row = document.createElement("tr");
row.setAttribute("id", costItem.id);
row.setAttribute("parent-id", parentID);
@@ -613,17 +625,17 @@ static createContextMenu(callbacks) {
return selectButton;
}
static highlightCostItem(id) {
const row = document.getElementById(id);
if (row) {
row.classList.add("highlighted");
static highlightElement(id) {
const element = document.getElementById(id);
if (element) {
element.classList.add("highlighted");
}
}
static unhighlightCostItem(id) {
const row = document.getElementById(id);
if (row) {
row.classList.remove("highlighted");
static unhighlightElement(id) {
const element = document.getElementById(id);
if (element) {
element.classList.remove("highlighted");
}
}
@@ -636,15 +648,26 @@ static createContextMenu(callbacks) {
return ids;
}
static text(label) {
const text = document.createElement("p");
static Text(label, icon = null, size = "medium") {
const text = document.createElement("span");
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");
card.classList.add("card");
card.id = "schedule-" + id;
const cardBody = document.createElement("div");
cardBody.classList.add("card-body");
@@ -684,30 +707,26 @@ static createContextMenu(callbacks) {
let formName = "Cost Values: " + costItemName;
const form = CostUI.Form({
icon: "fa-dollar-sign",
id: formId,
name: formName,
className: "floating-form",
});
const table = CostUI.createCostValuesTable(costItemId, [
"Type",
"Category",
"Value",
"",
]);
const { tableContainer, table } = CostUI.addTable({
headers: ["Type", "Category", "Value", ""],
className: "cost-values-table",
id: "cost-values-table-" + costItemId,
});
costValues.forEach((costValue) => {
const tr = CostUI.createCostvaluesRow(costItemId, costValue, callbacks);
table.appendChild(tr);
});
form.appendChild(table);
const addButton = CostUI.createAddCostValueButton(costItemId, callbacks);
form.appendChild(tableContainer);
form.appendChild(addButton);
document.body.appendChild(form);
return form;
}
@@ -735,10 +754,13 @@ static createContextMenu(callbacks) {
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");
table.classList.add("cost-values-table");
table.id = "cost-values-table-" + costItemId;
div.appendChild(table);
table.classList.add(className);
table.id = id;
const headerRow = document.createElement("tr");
headers.forEach((headerText) => {
@@ -746,9 +768,17 @@ static createContextMenu(callbacks) {
th.textContent = headerText;
headerRow.appendChild(th);
});
table.appendChild(headerRow);
return table;
const thead = document.createElement("thead");
thead.appendChild(headerRow);
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) {
@@ -917,19 +947,31 @@ static createContextMenu(callbacks) {
return addButton;
}
static Form({ id, name, className }) {
const form = document.createElement("form");
form.id = id;
form.classList.add(className);
form.style.position = "absolute";
form.style.top = "50%";
form.style.left = "50%";
form.style.transform = "translate(-50%, -50%)";
const header = CostUI.createHeader(name);
const closeButton = CostUI.createCloseButton(form);
static Form({ id, name, icon = "fa-solid fa-file", shouldHide = false }) {
let form;
if (document.getElementById(id)) {
form = document.getElementById(id);
} else {
form = document.createElement("form");
form.id = id;
form.classList.add("floating-form");
form.style.position = "absolute";
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(closeButton);
CostUI.makeFormDraggable(form);
document.body.appendChild(form);
return form;
}
static makeFormDraggable(form) {
@@ -990,23 +1032,389 @@ static createContextMenu(callbacks) {
}
}
static createHeader(text) {
static createHeader(text, icon) {
const header = document.createElement("div");
const i = document.createElement("i");
i.className = icon;
header.classList.add("form-header");
header.textContent = text;
const span = document.createElement("span");
span.textContent = text;
header.appendChild(i);
header.appendChild(span);
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) {
const closeButton = document.createElement("span");
closeButton.classList.add("close-button");
closeButton.innerHTML = "&times;";
closeButton.addEventListener("click", function () {
form.remove();
const costItemId = form.id.split("-")[3];
CostUI.unhighlightCostItem(costItemId);
const costItemId = form.id.split("-").pop();
CostUI.unhighlightElement(costItemId);
});
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()">
<i class="fas fa-moon"></i>
</button>
<button id="settings-button">
<i class="fa-solid fa-gear"></i>
</button>
</nav>
<div id="connected-list-div">
<button id="show-connected-button" onclick="toggleClientList()">
@@ -91,8 +94,8 @@
</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>
<p>BlenderBIM Version: {{version}}</p>
+3 -12
View File
@@ -318,18 +318,9 @@ class CostItemQuantitiesData:
@classmethod
def product_quantity_names(cls):
total_selected_objects = len(bpy.context.selected_objects)
names = set()
for obj in bpy.context.selected_objects:
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"]
elements = tool.Spatial.get_selected_products()
names = ifcopenshell.util.cost.get_product_quantity_names(elements)
return [(n, n, "") for n in names]
@classmethod
def process_quantity_names(cls):
+1 -1
View File
@@ -288,7 +288,7 @@ class Cost(bonsai.core.tool.Cost):
unit = ""
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 quantity in cost_item.CostQuantities:
for quantity in cost_item.CostQuantities or []:
if quantity == qto:
selected_quantitites.append(quantity)
if not unit:
+81 -5
View File
@@ -19,6 +19,7 @@
import ifcopenshell.util.cost
import bpy
from bonsai.bim.module.web.data import WebData
from ifcopenshell.util.element import get_psets, get_type
import bonsai.core.tool
import bonsai.tool as tool
import ifcopenshell.api.sequence
@@ -342,6 +343,48 @@ class Web(bonsai.core.tool.Web):
Args:
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()
if operator_data["type"] == "getPredefinedTypes":
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")
if operator_data["type"] == "addCostSchedule":
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 cost_schedules]
cost_schedules_json = [cs.get_info(recursive=True) for cs in ifc_file.by_type("IfcCostSchedule")]
currency = tool.Cost.currency()
cls.send_webui_data(
data={"cost_schedules": cost_schedules_json, "currency": currency},
@@ -361,8 +403,7 @@ class Web(bonsai.core.tool.Web):
)
if operator_data["type"] == "getCostSchedules":
print("getting cost schedules")
cost_schedules = ifc_file.by_type("IfcCostSchedule")
cost_schedules_json = [cs.get_info(recursive=True) for cs in cost_schedules]
cost_schedules_json = [cs.get_info(recursive=True) for cs in ifc_file.by_type("IfcCostSchedule")]
currency = tool.Cost.currency()
cls.send_webui_data(
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"])
products = tool.Cost.get_cost_item_products(cost_item, is_deep=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":
cost_schedule = ifc_file.by_id(operator_data["costScheduleId"])
bonsai.core.cost.add_summary_cost_item(tool.Ifc, tool.Cost, cost_schedule=cost_schedule)
cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "addCostItem":
if not operator_data["costItemId"]:
return
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)
@@ -443,6 +507,18 @@ class Web(bonsai.core.tool.Web):
print(value.get_info())
tool.Cost.load_cost_schedule_tree()
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
def load_cost_schedule_web_ui(cls, cost_schedule):
@@ -21,7 +21,7 @@ import ifcopenshell
from typing import Optional, Union, Literal, Generator, Any
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
from ifcopenshell.util.doc import get_predefined_type_doc
from ifcopenshell.util.element import get_psets
arithmetic_operator_symbols = {"ADD": "+", "DIVIDE": "/", "MULTIPLY": "*", "SUBTRACT": "-"}
symbol_arithmetic_operators = {"+": "ADD", "/": "DIVIDE", "*": "MULTIPLY", "-": "SUBTRACT"}
FILTER_BY_TYPE = Literal["PRODUCT", "RESOURCE", "PROCESS"]
@@ -240,11 +240,13 @@ def get_cost_item_assignments(
if not is_deep:
return get_cost_assignments_by_type(cost_item, filter_by_type)
else:
return [
current_assignments = get_cost_assignments_by_type(cost_item, filter_by_type)
nested_assignments = [
product
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)
]
return current_assignments + nested_assignments
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
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:
def parse(self, formula: str):
l = lark.Lark(