mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-17 10:59:17 +00:00
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:
@@ -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 = "−";
|
||||
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 = "×";
|
||||
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>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user