Cost web ui improvements:

- hovering cost item row displays quick actions
- clicking the quantity cells and cost cells prompts form to edit values
- further style improvements & refactoring
This commit is contained in:
myoualid
2024-09-10 22:48:22 +01:00
parent 794154f6f5
commit 158f11d58c
5 changed files with 265 additions and 122 deletions
@@ -183,10 +183,10 @@ class BlenderNamespace(socketio.AsyncNamespace):
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):
async def on_quantities(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")
blender_messages[sid]["quantities"] = data
await sio.emit("quantities", {"blenderId": sid, "data": data}, namespace="/web")
async def schedules(request):
with open("templates/index.html", "r") as f:
@@ -53,7 +53,6 @@ body {
margin-bottom: 10px;
}
.form-container {
padding: var(--padding-medium);
overflow-y: auto;
@@ -155,7 +154,6 @@ body {
table-layout: fixed;
}
[id^="selected-products"] .form-container {
height: 50vh;
}
@@ -186,7 +184,6 @@ tbody {
td,
th {
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
text-align: center;
}
@@ -304,8 +301,38 @@ form table {
max-height: 50%;
}
.subtotal-row {
background-color: var(--button-hover-background);
font-weight: bold;
}
}
[id^="cost-items"] th:nth-child(1),
[id^="cost-items"] td:nth-child(1) {
width: auto;
}
[id^="cost-items"] th:not(:nth-child(1)),
[id^="cost-items"] td:not(:nth-child(1)) {
width: 10%;
}
[id^="cost-items"] th:last-child,
[id^="cost-items"] td:last-child {
width: fit-content;
min-width: fit-content;
max-width: 50%;
}
.actions-column {
transition: opacity 0.3s ease-in-out;
opacity: 0;
}
[id^="cost-items"] tr:hover .actions-column {
opacity: 1;
}
.clickable-cell:hover {
border: 1px solid #28a746;
border-radius: 6px;
cursor: pointer;
}
@@ -28,53 +28,25 @@ function connectSocket() {
socket.on("cost_items", handleCostItemsData);
socket.on("cost_values", handleCostValuesData);
socket.on("cost_value", handleCostValueData);
socket.on("selected_products", handleSelectedProducts);
socket.on("quantities", handleEditQuantities);
}
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: "Edit Product Assignments for: " + CostUI.getCostItemName(costItemId),
icon: "fa-solid fa-box",
});
const numberOfProducts = CostUI.Text(
"Selection basket : " + products.length + " products",
"fa-solid fa-cart-shopping",
"large"
);
form.appendChild(numberOfProducts);
CostUI.highlightElement(costItemId);
const selectedProductsTable = CostUI.createProductTable({
form,
products,
quantityNames,
costItemId,
function handleEditQuantities(data) {
const costItemId = data.data["quantities"]["cost_item_id"];
const products = data.data["quantities"]["selected_products"];
const assigned_products = data.data["quantities"]["assigned_products"];
const quantityNames = data.data["quantities"]["product_quantity_names"];
CostUI.editQuantities({
costItemId: costItemId,
selectedProducts: products,
assignedProducts: assigned_products,
quantityNames: quantityNames,
callbacks: {
addProductAssignments: addProductAssignments,
getSelectedProducts: getSelectedProducts,
enableEditingQuantities: enableEditingQuantities,
},
});
if (assigned_products.length > 0) {
const assignedProductsText = CostUI.Text(
"Assigned Products",
"fa-solid fa-solid fa-paperclip",
"large"
);
form.appendChild(assignedProductsText);
const assignmentsTable = CostUI.createAssignmentsTable({
form,
products: assigned_products,
quantityNames,
costItemId,
});
}
}
function handlePredefinedTypes(data) {
@@ -322,9 +294,6 @@ function handleCostSchedulesData(data) {
if (costSchedules.length === 0) {
return;
}
const currency = data.data["cost_schedules"]["currency"]
? data.data["cost_schedules"]["currency"]["name"]
: "Undefined";
const costScheduleDiv = document.getElementById("cost-schedules");
costScheduleDiv.innerHTML = "";
costSchedules.forEach((costSchedule) => {
@@ -368,8 +337,13 @@ function handleCostItemsData(data) {
loadedSchedules[data.blenderId] = costScheduleId;
CostUI.highlightElement("schedule-" + costScheduleId);
const currency = data.data["cost_items"]["currency"]
? data.data["cost_items"]["currency"]["name"]
: "Undefined";
CostUI.createCostSchedule({
data: data.data["cost_items"]["cost_items"],
costItems: data.data["cost_items"]["cost_items"],
currency: currency,
costScheduleId: costScheduleId,
blenderID: data.blenderId,
callbacks: {
@@ -380,13 +354,13 @@ function handleCostItemsData(data) {
editCostItemName: editCostItemName,
enableEditingCostValues: enableEditingCostValues,
addSummaryCostItem: addSummaryCostItem,
getSelectedProducts: getSelectedProducts,
enableEditingQuantities: enableEditingQuantities,
},
});
}
function getSelectedProducts(costItemId) {
executeOperator({ type: "getSelectedProducts", costItemId: costItemId });
function enableEditingQuantities(costItemId) {
executeOperator({ type: "enableEditingQuantities", costItemId: costItemId });
}
function duplicateCostItem(costItemId) {
@@ -9,7 +9,7 @@ export class CostUI {
static removeCostSchedule(id) {
document.getElementById("cost-items-" + id).remove();
}
static createTable(id, callbacks) {
static createCostTable(id, currency, callbacks) {
CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null;
const table = document.createElement("table");
@@ -21,9 +21,9 @@ export class CostUI {
"Name",
"Quantity",
"Unit",
"Cost",
"Total Cost",
"Action",
"Cost (" + currency + ")",
"Total Cost (" + currency + ")",
"Actions",
];
const thead = document.createElement("thead");
const tr = document.createElement("tr");
@@ -47,7 +47,6 @@ export class CostUI {
table.appendChild(tbody);
document.getElementById("cost-items").appendChild(table);
CostUI.addTableStyles(id);
CostUI.createContextMenu(callbacks);
table.get_blender_id = function () {
return this.getAttribute("id").split("-")[2];
@@ -56,20 +55,12 @@ export class CostUI {
return [table, tbody];
}
static addTableStyles(id) {
const style = document.createElement("style");
style.textContent = `
#cost-items-${id} th:nth-child(1),
#cost-items-${id} td:nth-child(1) {
width: auto;
}
#cost-items-${id} th:not(:nth-child(1)),
#cost-items-${id} td:not(:nth-child(1)) {
width: 100px; /* Set a fixed width for other columns */
}
`;
document.head.appendChild(style);
static deleteCostItem(costItemId, callback) {
const costItemRow = document.getElementById(costItemId);
let expandedState = JSON.parse(localStorage.getItem("expandedState")) || {};
expandedState = CostUI.deleteCostItemRow(costItemRow, expandedState);
localStorage.setItem("expandedState", JSON.stringify(expandedState));
callback(costItemId);
}
static createContextMenu(callbacks) {
@@ -139,21 +130,17 @@ export class CostUI {
addButton.dataset.listenerAdded = "true";
}
const deleteCostItem = document.getElementById("delete-cost-item");
if (deleteCostItem && !deleteCostItem.hasListener) {
deleteCostItem.addEventListener("click", function () {
const deleteCostItemButton = document.getElementById("delete-cost-item");
if (deleteCostItemButton && !deleteCostItemButton.hasListener) {
deleteCostItemButton.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.deleteCostItemRow(targetRow, expandedState);
localStorage.setItem("expandedState", JSON.stringify(expandedState));
callbacks.deleteCostItem(costItemId);
CostUI.deleteCostItem(costItemId, callbacks.deleteCostItem);
}
document.getElementById("context-menu").style.display = "none";
});
deleteCostItem.hasListener = true;
deleteCostItemButton.hasListener = true;
}
const duplicateButton = document.getElementById("duplicate-button");
@@ -175,7 +162,7 @@ export class CostUI {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.getSelectedProducts(costItemId);
callbacks.enableEditingQuantities(costItemId);
}
document.getElementById("context-menu").style.display = "none";
});
@@ -192,7 +179,7 @@ export class CostUI {
document
.getElementById("cost-items")
.addEventListener("dblclick", function (event) {
.addEventListener("click", function (event) {
const targetRow = event.target.closest("tr");
const targetCell = event.target.closest("td");
if (targetRow && targetCell) {
@@ -202,11 +189,16 @@ export class CostUI {
const costItemId = parseInt(targetRow.getAttribute("id"));
const columnName = getColumnNames("cost-items")[columnIndex];
if (columnName === "Cost") {
if (columnName.includes("Cost") && !columnName.includes("Total")) {
callbacks.enableEditingCostValues
? callbacks.enableEditingCostValues(costItemId)
: null;
}
if (columnName === "Quantity") {
callbacks.enableEditingQuantities
? callbacks.enableEditingQuantities(costItemId)
: null;
}
}
});
}
@@ -363,13 +355,18 @@ export class CostUI {
}
static createCostSchedule({
data,
costItems,
currency,
costScheduleId,
blenderID,
callbacks = {},
}) {
const [table, tbody] = CostUI.createTable(blenderID, callbacks);
if (data.length === 0) {
const [table, tbody] = CostUI.createCostTable(
blenderID,
currency,
callbacks
);
if (costItems.length === 0) {
const tr = document.createElement("tr");
const td = document.createElement("td");
td.colSpan = 6;
@@ -387,24 +384,29 @@ export class CostUI {
td.appendChild(addSummaryCostItemButton);
addSummaryCostItemButton.classList.add("action-button");
} else {
CostUI.createCostItem(data, tbody, 0, null, callbacks);
CostUI.createCostTree(costItems, tbody, 0, null, callbacks);
CostUI.applyExpandedState();
}
}
static createCostItem(
data,
static createCostTree(
costItems,
container,
nestingLevel = 0,
parentID = null,
callbacks = {}
) {
data.forEach((costItem) => {
const row = CostUI.createRow(costItem, nestingLevel, parentID, callbacks);
costItems.forEach((costItem) => {
const row = CostUI.addCostItemRow(
costItem,
nestingLevel,
parentID,
callbacks
);
container.appendChild(row);
if (costItem.is_nested_by && costItem.is_nested_by.length > 0) {
CostUI.createCostItem(
CostUI.createCostTree(
costItem.is_nested_by,
container,
nestingLevel + 1,
@@ -415,7 +417,7 @@ export class CostUI {
});
}
static createRow(costItem, nestingLevel, parentID, callbacks = {}) {
static addCostItemRow(costItem, nestingLevel, parentID, callbacks = {}) {
const totalQuantity = costItem.TotalCostQuantity
? parseFloat(costItem.TotalCostQuantity).toFixed(2)
: "-";
@@ -431,20 +433,21 @@ export class CostUI {
callbacks
);
const totalCostQuantityCell = CostUI.createTableCell(totalQuantity);
totalCostQuantityCell.classList.add("clickable-cell");
const unitSymbolCell = CostUI.createTableCell(costItem.UnitSymbol);
const totalAppliedValueCell = CostUI.createTableCell(appliedValue);
totalAppliedValueCell.classList.add("clickable-cell");
const totalCostCell = CostUI.createTotalCostCell(costItem);
const flexContainerCell = CostUI.createFlexContainerCell(
costItem,
callbacks
);
const actionsCell = CostUI.costItemActions(costItem, callbacks);
actionsCell.classList.add("actions-column");
row.appendChild(nameCell);
row.appendChild(totalCostQuantityCell);
row.appendChild(unitSymbolCell);
row.appendChild(totalAppliedValueCell);
row.appendChild(totalCostCell);
row.appendChild(flexContainerCell);
row.appendChild(actionsCell);
row.get_id = function () {
return this.getAttribute("id");
@@ -636,30 +639,92 @@ export class CostUI {
return totalCostCell;
}
static createFlexContainerCell(costItem, callbacks) {
static costItemActions(costItem, callbacks) {
const divFlex = document.createElement("div");
divFlex.classList.add("row-container");
const selectButton = CostUI.createSelectButton(costItem, callbacks);
divFlex.appendChild(selectButton);
const addCostItem = CostUI.addCostItemButton(
costItem.id,
callbacks.addCostItem
);
const selectButton = CostUI.createSelectButton(
costItem.id,
callbacks.selectAssignedElements
);
const deleteButton = CostUI.deleteCostItemButton(
costItem.id,
callbacks.deleteCostItem
);
const duplicateButton = CostUI.duplicateCostItemButton(
costItem.id,
callbacks.duplicateCostItem
);
const flexContainerCell = document.createElement("td");
flexContainerCell.appendChild(divFlex);
return flexContainerCell;
[addCostItem, duplicateButton, selectButton, deleteButton].forEach(
(button) => {
divFlex.appendChild(button);
}
);
const actionsCell = document.createElement("td");
actionsCell.appendChild(divFlex);
return actionsCell;
}
static createSelectButton(costItem, callbacks) {
const selectButton = document.createElement("button");
selectButton.classList.add("action-button");
selectButton.textContent = "Select";
selectButton.addEventListener("click", function (e) {
e.stopPropagation();
callbacks.selectAssignedElements
? callbacks.selectAssignedElements(costItem.id)
: null;
});
static duplicateCostItemButton(costItemId, duplicateCostItem) {
const duplicateButton = CostUI.createButton(
"Duplicate",
"fa-solid fa-copy"
);
duplicateButton.addEventListener(
"click",
duplicateCostItem.bind(null, costItemId)
);
return duplicateButton;
}
static addCostItemButton(costItemId, addCostItem) {
const addCostItemButton = CostUI.createButton(
"Add Sub-Cost",
"fa-solid fa-plus"
);
addCostItemButton.addEventListener(
"click",
addCostItem.bind(null, costItemId)
);
return addCostItemButton;
}
static createSelectButton(costItemId, selectAssignedElements) {
const selectButton = CostUI.createButton(
"Select",
"fa-solid fa-arrow-pointer"
);
selectButton.addEventListener(
"click",
selectAssignedElements.bind(null, costItemId)
);
return selectButton;
}
static deleteCostItemButton(costItemId, deleteCostItem) {
const deleteButton = CostUI.createButton("Delete", "fa-solid fa-trash");
deleteButton.addEventListener(
"click",
CostUI.deleteCostItem.bind(null, costItemId, deleteCostItem)
);
return deleteButton;
}
static createButton(text, icon) {
const button = document.createElement("button");
!icon ? (button.textContent = text) : null;
icon
? button.classList.add("action-button", ...icon.split(" "))
: button.classList.add("action-button");
button.dataset.tooltip = text;
return button;
}
static highlightElement(id) {
const element = document.getElementById(id);
if (element) {
@@ -1479,6 +1544,7 @@ export class CostUI {
const picker = CostUI.createColorPicker();
const tableFontSize = CostUI.createFontSizePicker();
const currencyPicker = CostUI.createCurrencyPicker();
settingsMenu.appendChild(picker);
settingsMenu.appendChild(tableFontSize);
document.getElementById("settings-menu").style.display = "none";
@@ -1521,6 +1587,32 @@ export class CostUI {
return div;
}
static createCurrencyPicker() {
const currency = CostUI.getCurrency();
const div = document.createElement("div");
const currencyText = document.createElement("p");
currencyText.textContent = "Select a currency for the table:";
div.appendChild(currencyText);
const currencyPicker = document.createElement("input");
currencyPicker.type = "text";
currencyPicker.id = "currency-picker";
currencyPicker.value = currency;
div.appendChild(currencyPicker);
currencyPicker.addEventListener("input", function () {
const currency = currencyPicker.value;
CostUI.saveCurrency(currency);
});
return div;
}
static saveCurrency(currency) {
localStorage.setItem("tableCurrency", currency);
}
static getCurrency() {
return localStorage.getItem("tableCurrency");
}
static saveFontSize(fontSize) {
localStorage.setItem("tableFontSize", fontSize);
}
@@ -1637,4 +1729,50 @@ export class CostUI {
},
});
}
static editQuantities({
costItemId,
selectedProducts,
quantityNames,
assignedProducts,
callbacks,
}) {
const formId = "selected-products-" + costItemId;
const form = CostUI.Form({
id: formId,
name:
"Edit Product Assignments for: " + CostUI.getCostItemName(costItemId),
icon: "fa-solid fa-box",
});
const numberOfProducts = CostUI.Text(
"Selection basket : " + selectedProducts.length + " products",
"fa-solid fa-cart-shopping",
"large"
);
form.appendChild(numberOfProducts);
CostUI.highlightElement(costItemId);
const selectedProductsTable = CostUI.createProductTable({
form,
products: selectedProducts,
quantityNames,
costItemId,
callbacks,
});
if (assignedProducts.length > 0) {
const assignedProductsText = CostUI.Text(
"Assigned Products",
"fa-solid fa-solid fa-paperclip",
"large"
);
form.appendChild(assignedProductsText);
const assignmentsTable = CostUI.createAssignmentsTable({
form,
products: assignedProducts,
quantityNames,
costItemId,
callbacks,
});
}
}
}
+8 -4
View File
@@ -425,7 +425,7 @@ 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":
if operator_data["type"] == "enableEditingQuantities":
cost_item_id = operator_data["costItemId"]
cost_item = ifc_file.by_id(cost_item_id)
if not cost_item:
@@ -446,8 +446,8 @@ class Web(bonsai.core.tool.Web):
"product_quantity_names": names,
"cost_item_id": cost_item_id,
},
data_key="selected_products",
event="selected_products",
data_key="quantities",
event="quantities",
)
if operator_data["type"] == "addSummaryCostItem":
@@ -531,7 +531,11 @@ class Web(bonsai.core.tool.Web):
def load_cost_schedule_web_ui(cls, cost_schedule):
json_data = tool.Cost.create_cost_schedule_json(cost_schedule)
cls.send_webui_data(
data={"cost_items": json_data, "cost_schedule_id": cost_schedule.id()},
data={
"cost_items": json_data,
"cost_schedule_id": cost_schedule.id(),
"currency": tool.Cost.currency(),
},
data_key="cost_items",
event="cost_items",
)