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 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): async def on_quantities(self, sid, data):
print(f"Selected products from Blender client {sid}") print(f"Selected products from Blender client {sid}")
blender_messages[sid]["selected_products"] = data blender_messages[sid]["quantities"] = data
await sio.emit("selected_products", {"blenderId": sid, "data": data}, namespace="/web") await sio.emit("quantities", {"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:
@@ -53,7 +53,6 @@ body {
margin-bottom: 10px; margin-bottom: 10px;
} }
.form-container { .form-container {
padding: var(--padding-medium); padding: var(--padding-medium);
overflow-y: auto; overflow-y: auto;
@@ -155,7 +154,6 @@ body {
table-layout: fixed; table-layout: fixed;
} }
[id^="selected-products"] .form-container { [id^="selected-products"] .form-container {
height: 50vh; height: 50vh;
} }
@@ -186,7 +184,6 @@ tbody {
td, td,
th { th {
border-left: 1px solid #ddd;
border-right: 1px solid #ddd; border-right: 1px solid #ddd;
text-align: center; text-align: center;
} }
@@ -304,8 +301,38 @@ form table {
max-height: 50%; max-height: 50%;
} }
.subtotal-row { .subtotal-row {
background-color: var(--button-hover-background); background-color: var(--button-hover-background);
font-weight: bold; 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_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); socket.on("quantities", handleEditQuantities);
} }
function handleSelectedProducts(data) { function handleEditQuantities(data) {
const costItemId = data.data["selected_products"]["cost_item_id"]; const costItemId = data.data["quantities"]["cost_item_id"];
const products = data.data["selected_products"]["selected_products"]; const products = data.data["quantities"]["selected_products"];
const assigned_products = data.data["selected_products"]["assigned_products"]; const assigned_products = data.data["quantities"]["assigned_products"];
const quantityNames = const quantityNames = data.data["quantities"]["product_quantity_names"];
data.data["selected_products"]["product_quantity_names"];
const formId = "selected-products-" + costItemId; CostUI.editQuantities({
const form = CostUI.Form({ costItemId: costItemId,
id: formId, selectedProducts: products,
name: "Edit Product Assignments for: " + CostUI.getCostItemName(costItemId), assignedProducts: assigned_products,
icon: "fa-solid fa-box", quantityNames: quantityNames,
});
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,
callbacks: { callbacks: {
addProductAssignments: addProductAssignments, 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) { function handlePredefinedTypes(data) {
@@ -322,9 +294,6 @@ function handleCostSchedulesData(data) {
if (costSchedules.length === 0) { if (costSchedules.length === 0) {
return; return;
} }
const currency = data.data["cost_schedules"]["currency"]
? data.data["cost_schedules"]["currency"]["name"]
: "Undefined";
const costScheduleDiv = document.getElementById("cost-schedules"); const costScheduleDiv = document.getElementById("cost-schedules");
costScheduleDiv.innerHTML = ""; costScheduleDiv.innerHTML = "";
costSchedules.forEach((costSchedule) => { costSchedules.forEach((costSchedule) => {
@@ -368,8 +337,13 @@ function handleCostItemsData(data) {
loadedSchedules[data.blenderId] = costScheduleId; loadedSchedules[data.blenderId] = costScheduleId;
CostUI.highlightElement("schedule-" + costScheduleId); CostUI.highlightElement("schedule-" + costScheduleId);
const currency = data.data["cost_items"]["currency"]
? data.data["cost_items"]["currency"]["name"]
: "Undefined";
CostUI.createCostSchedule({ CostUI.createCostSchedule({
data: data.data["cost_items"]["cost_items"], costItems: data.data["cost_items"]["cost_items"],
currency: currency,
costScheduleId: costScheduleId, costScheduleId: costScheduleId,
blenderID: data.blenderId, blenderID: data.blenderId,
callbacks: { callbacks: {
@@ -380,13 +354,13 @@ function handleCostItemsData(data) {
editCostItemName: editCostItemName, editCostItemName: editCostItemName,
enableEditingCostValues: enableEditingCostValues, enableEditingCostValues: enableEditingCostValues,
addSummaryCostItem: addSummaryCostItem, addSummaryCostItem: addSummaryCostItem,
getSelectedProducts: getSelectedProducts, enableEditingQuantities: enableEditingQuantities,
}, },
}); });
} }
function getSelectedProducts(costItemId) { function enableEditingQuantities(costItemId) {
executeOperator({ type: "getSelectedProducts", costItemId: costItemId }); executeOperator({ type: "enableEditingQuantities", costItemId: costItemId });
} }
function duplicateCostItem(costItemId) { function duplicateCostItem(costItemId) {
@@ -9,7 +9,7 @@ export class CostUI {
static removeCostSchedule(id) { static removeCostSchedule(id) {
document.getElementById("cost-items-" + id).remove(); document.getElementById("cost-items-" + id).remove();
} }
static createTable(id, callbacks) { static createCostTable(id, currency, callbacks) {
CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null; CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null;
const table = document.createElement("table"); const table = document.createElement("table");
@@ -21,9 +21,9 @@ export class CostUI {
"Name", "Name",
"Quantity", "Quantity",
"Unit", "Unit",
"Cost", "Cost (" + currency + ")",
"Total Cost", "Total Cost (" + currency + ")",
"Action", "Actions",
]; ];
const thead = document.createElement("thead"); const thead = document.createElement("thead");
const tr = document.createElement("tr"); const tr = document.createElement("tr");
@@ -47,7 +47,6 @@ export class CostUI {
table.appendChild(tbody); table.appendChild(tbody);
document.getElementById("cost-items").appendChild(table); document.getElementById("cost-items").appendChild(table);
CostUI.addTableStyles(id);
CostUI.createContextMenu(callbacks); CostUI.createContextMenu(callbacks);
table.get_blender_id = function () { table.get_blender_id = function () {
return this.getAttribute("id").split("-")[2]; return this.getAttribute("id").split("-")[2];
@@ -56,20 +55,12 @@ export class CostUI {
return [table, tbody]; return [table, tbody];
} }
static addTableStyles(id) { static deleteCostItem(costItemId, callback) {
const style = document.createElement("style"); const costItemRow = document.getElementById(costItemId);
style.textContent = ` let expandedState = JSON.parse(localStorage.getItem("expandedState")) || {};
#cost-items-${id} th:nth-child(1), expandedState = CostUI.deleteCostItemRow(costItemRow, expandedState);
#cost-items-${id} td:nth-child(1) { localStorage.setItem("expandedState", JSON.stringify(expandedState));
width: auto; callback(costItemId);
}
#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 createContextMenu(callbacks) { static createContextMenu(callbacks) {
@@ -139,21 +130,17 @@ export class CostUI {
addButton.dataset.listenerAdded = "true"; addButton.dataset.listenerAdded = "true";
} }
const deleteCostItem = document.getElementById("delete-cost-item"); const deleteCostItemButton = document.getElementById("delete-cost-item");
if (deleteCostItem && !deleteCostItem.hasListener) { if (deleteCostItemButton && !deleteCostItemButton.hasListener) {
deleteCostItem.addEventListener("click", function () { deleteCostItemButton.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 = CostUI.deleteCostItem(costItemId, callbacks.deleteCostItem);
JSON.parse(localStorage.getItem("expandedState")) || {};
expandedState = CostUI.deleteCostItemRow(targetRow, expandedState);
localStorage.setItem("expandedState", JSON.stringify(expandedState));
callbacks.deleteCostItem(costItemId);
} }
document.getElementById("context-menu").style.display = "none"; document.getElementById("context-menu").style.display = "none";
}); });
deleteCostItem.hasListener = true; deleteCostItemButton.hasListener = true;
} }
const duplicateButton = document.getElementById("duplicate-button"); const duplicateButton = document.getElementById("duplicate-button");
@@ -175,7 +162,7 @@ export class CostUI {
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.getSelectedProducts(costItemId); callbacks.enableEditingQuantities(costItemId);
} }
document.getElementById("context-menu").style.display = "none"; document.getElementById("context-menu").style.display = "none";
}); });
@@ -192,7 +179,7 @@ export class CostUI {
document document
.getElementById("cost-items") .getElementById("cost-items")
.addEventListener("dblclick", function (event) { .addEventListener("click", function (event) {
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 && targetCell) { if (targetRow && targetCell) {
@@ -202,11 +189,16 @@ export class CostUI {
const costItemId = parseInt(targetRow.getAttribute("id")); const costItemId = parseInt(targetRow.getAttribute("id"));
const columnName = getColumnNames("cost-items")[columnIndex]; const columnName = getColumnNames("cost-items")[columnIndex];
if (columnName === "Cost") { if (columnName.includes("Cost") && !columnName.includes("Total")) {
callbacks.enableEditingCostValues callbacks.enableEditingCostValues
? callbacks.enableEditingCostValues(costItemId) ? callbacks.enableEditingCostValues(costItemId)
: null; : null;
} }
if (columnName === "Quantity") {
callbacks.enableEditingQuantities
? callbacks.enableEditingQuantities(costItemId)
: null;
}
} }
}); });
} }
@@ -363,13 +355,18 @@ export class CostUI {
} }
static createCostSchedule({ static createCostSchedule({
data, costItems,
currency,
costScheduleId, costScheduleId,
blenderID, blenderID,
callbacks = {}, callbacks = {},
}) { }) {
const [table, tbody] = CostUI.createTable(blenderID, callbacks); const [table, tbody] = CostUI.createCostTable(
if (data.length === 0) { blenderID,
currency,
callbacks
);
if (costItems.length === 0) {
const tr = document.createElement("tr"); const tr = document.createElement("tr");
const td = document.createElement("td"); const td = document.createElement("td");
td.colSpan = 6; td.colSpan = 6;
@@ -387,24 +384,29 @@ export class CostUI {
td.appendChild(addSummaryCostItemButton); td.appendChild(addSummaryCostItemButton);
addSummaryCostItemButton.classList.add("action-button"); addSummaryCostItemButton.classList.add("action-button");
} else { } else {
CostUI.createCostItem(data, tbody, 0, null, callbacks); CostUI.createCostTree(costItems, tbody, 0, null, callbacks);
CostUI.applyExpandedState(); CostUI.applyExpandedState();
} }
} }
static createCostItem( static createCostTree(
data, costItems,
container, container,
nestingLevel = 0, nestingLevel = 0,
parentID = null, parentID = null,
callbacks = {} callbacks = {}
) { ) {
data.forEach((costItem) => { costItems.forEach((costItem) => {
const row = CostUI.createRow(costItem, nestingLevel, parentID, callbacks); const row = CostUI.addCostItemRow(
costItem,
nestingLevel,
parentID,
callbacks
);
container.appendChild(row); container.appendChild(row);
if (costItem.is_nested_by && costItem.is_nested_by.length > 0) { if (costItem.is_nested_by && costItem.is_nested_by.length > 0) {
CostUI.createCostItem( CostUI.createCostTree(
costItem.is_nested_by, costItem.is_nested_by,
container, container,
nestingLevel + 1, 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 const totalQuantity = costItem.TotalCostQuantity
? parseFloat(costItem.TotalCostQuantity).toFixed(2) ? parseFloat(costItem.TotalCostQuantity).toFixed(2)
: "-"; : "-";
@@ -431,20 +433,21 @@ export class CostUI {
callbacks callbacks
); );
const totalCostQuantityCell = CostUI.createTableCell(totalQuantity); const totalCostQuantityCell = CostUI.createTableCell(totalQuantity);
totalCostQuantityCell.classList.add("clickable-cell");
const unitSymbolCell = CostUI.createTableCell(costItem.UnitSymbol); const unitSymbolCell = CostUI.createTableCell(costItem.UnitSymbol);
const totalAppliedValueCell = CostUI.createTableCell(appliedValue); const totalAppliedValueCell = CostUI.createTableCell(appliedValue);
totalAppliedValueCell.classList.add("clickable-cell");
const totalCostCell = CostUI.createTotalCostCell(costItem); const totalCostCell = CostUI.createTotalCostCell(costItem);
const flexContainerCell = CostUI.createFlexContainerCell( const actionsCell = CostUI.costItemActions(costItem, callbacks);
costItem,
callbacks actionsCell.classList.add("actions-column");
);
row.appendChild(nameCell); row.appendChild(nameCell);
row.appendChild(totalCostQuantityCell); row.appendChild(totalCostQuantityCell);
row.appendChild(unitSymbolCell); row.appendChild(unitSymbolCell);
row.appendChild(totalAppliedValueCell); row.appendChild(totalAppliedValueCell);
row.appendChild(totalCostCell); row.appendChild(totalCostCell);
row.appendChild(flexContainerCell); row.appendChild(actionsCell);
row.get_id = function () { row.get_id = function () {
return this.getAttribute("id"); return this.getAttribute("id");
@@ -636,30 +639,92 @@ export class CostUI {
return totalCostCell; return totalCostCell;
} }
static createFlexContainerCell(costItem, callbacks) { static costItemActions(costItem, callbacks) {
const divFlex = document.createElement("div"); const divFlex = document.createElement("div");
divFlex.classList.add("row-container"); divFlex.classList.add("row-container");
const selectButton = CostUI.createSelectButton(costItem, callbacks); const addCostItem = CostUI.addCostItemButton(
divFlex.appendChild(selectButton); 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"); [addCostItem, duplicateButton, selectButton, deleteButton].forEach(
flexContainerCell.appendChild(divFlex); (button) => {
return flexContainerCell; divFlex.appendChild(button);
}
);
const actionsCell = document.createElement("td");
actionsCell.appendChild(divFlex);
return actionsCell;
} }
static createSelectButton(costItem, callbacks) { static duplicateCostItemButton(costItemId, duplicateCostItem) {
const selectButton = document.createElement("button"); const duplicateButton = CostUI.createButton(
selectButton.classList.add("action-button"); "Duplicate",
selectButton.textContent = "Select"; "fa-solid fa-copy"
selectButton.addEventListener("click", function (e) { );
e.stopPropagation(); duplicateButton.addEventListener(
callbacks.selectAssignedElements "click",
? callbacks.selectAssignedElements(costItem.id) duplicateCostItem.bind(null, costItemId)
: null; );
}); 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; 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) { static highlightElement(id) {
const element = document.getElementById(id); const element = document.getElementById(id);
if (element) { if (element) {
@@ -1479,6 +1544,7 @@ export class CostUI {
const picker = CostUI.createColorPicker(); const picker = CostUI.createColorPicker();
const tableFontSize = CostUI.createFontSizePicker(); const tableFontSize = CostUI.createFontSizePicker();
const currencyPicker = CostUI.createCurrencyPicker();
settingsMenu.appendChild(picker); settingsMenu.appendChild(picker);
settingsMenu.appendChild(tableFontSize); settingsMenu.appendChild(tableFontSize);
document.getElementById("settings-menu").style.display = "none"; document.getElementById("settings-menu").style.display = "none";
@@ -1521,6 +1587,32 @@ export class CostUI {
return div; 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) { static saveFontSize(fontSize) {
localStorage.setItem("tableFontSize", 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"]) 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": if operator_data["type"] == "enableEditingQuantities":
cost_item_id = operator_data["costItemId"] cost_item_id = operator_data["costItemId"]
cost_item = ifc_file.by_id(cost_item_id) cost_item = ifc_file.by_id(cost_item_id)
if not cost_item: if not cost_item:
@@ -446,8 +446,8 @@ class Web(bonsai.core.tool.Web):
"product_quantity_names": names, "product_quantity_names": names,
"cost_item_id": cost_item_id, "cost_item_id": cost_item_id,
}, },
data_key="selected_products", data_key="quantities",
event="selected_products", event="quantities",
) )
if operator_data["type"] == "addSummaryCostItem": if operator_data["type"] == "addSummaryCostItem":
@@ -531,7 +531,11 @@ class Web(bonsai.core.tool.Web):
def load_cost_schedule_web_ui(cls, cost_schedule): def load_cost_schedule_web_ui(cls, cost_schedule):
json_data = tool.Cost.create_cost_schedule_json(cost_schedule) json_data = tool.Cost.create_cost_schedule_json(cost_schedule)
cls.send_webui_data( 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", data_key="cost_items",
event="cost_items", event="cost_items",
) )