1/ WEB UI Cost Schedule features to :

- delete cost items
- duplicate cost items
- delete individual cost values
- update cost values on the fly
- Shitf + LMB to recursively hide or show nested rows

2/ Improved UI for cost schedules
3/ Reuse css styles across pages with import statements
4/ Remove console/print statements
This commit is contained in:
`myoualid`
2024-09-04 23:39:46 +01:00
parent e1a5e73106
commit d0712c7186
10 changed files with 704 additions and 496 deletions
@@ -41,49 +41,3 @@ th {
/* zoom */
transform: scale(0.9);
}
#cost-items tr:hover {
background-color: #f0f0f0;
color : black;
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
background-color: #363636;
padding: 10px;
}
.cost-values-form {
position: absolute; /* Set initial position to absolute */
background-color: white;
border: 1px solid black;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
z-index: 9999;
cursor: move; /* Change cursor to move */
}
.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: #f2f2f2;
text-align: left;
}
.close-button {
position: absolute;
top: 10px;
right: 10px;
font-size: 20px;
cursor: pointer;
}
@@ -0,0 +1,137 @@
@import url("./shared.css");
@import url("./components/card.css");
@import url("./components/contextMenu.css");
table {
width: 100%;
border-collapse: collapse;
/* prevent text from being selceted */
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;
}
.flex-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
input {
width: 100%;
border: 1px solid #e1e4e8;
border-radius: 6px;
background-color:inherit;
padding: 6px 8px;
font-size: 14px;
line-height: 20px;
color:inherit;
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: #0366d6;
outline: none;
box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.3);
}
#cost-items tr:hover, .highlighted {
background-color: #ffb936;
color : black;
}
.highlight {
background-color: #ffb936;
color : black;
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
background-color: #363636;
padding: 10px;
}
.cost-values-form {
position: absolute;
background-color: white;
border: 1px solid black;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
z-index: 9999;
cursor: move;
}
.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;
}
.action-button {
background-color: #28a745; /* GitHub green color */
border: none;
color: #fff; /* White text color */
padding: 0.5em 1em; /* Adjust padding as needed */
cursor: pointer;
border-radius: 6px; /* Slightly rounded corners */
font-weight: bold; /* Make the text bold */
text-decoration: none; /* Remove underline */
display: inline-block; /* Make the button inline */
transition: background-color 0.3s ease; /* Add a smooth transition */
}
.action-button:hover {
background-color: #218838; /* Darker green color on hover */
}
@@ -1,5 +1,4 @@
@import url("./components/card.css");
@import url("./components/contextMenu.css");
:root {
--font-family: Arial, sans-serif;
@@ -1,205 +1,4 @@
:root {
--font-family: Arial, sans-serif;
--base-font-size: 16px;
--margin-tiny: 0.125rem;
--margin-small: 0.625rem;
--margin-medium: 1.25rem;
--margin-large: 2.5rem;
--padding-tiny: 0.125rem;
--padding-small: 0.625rem;
--padding-medium: 1rem;
--font-size-large: 1.2rem;
--logo-height: 1.5rem;
--nav-height: 0.625rem;
--border-radius: 0.3rem;
}
:root.dark {
color-scheme: dark;
--bg-color: #252525;
--text-color: #e0e0e0;
--nav-bg-color: #121212;
--nav-border-color: #25682a;
--nav-link-color: #fff;
--nav-link-hover-color: #3fb449;
--warning-color: #FFDB8F;
--border-color: #464444;
}
:root.light {
color-scheme: light;
--bg-color: #ffffff;
--text-color: #000000;
--nav-bg-color: #f8f8f8;
--nav-border-color: #cccccc;
--nav-link-color: #000000;
--nav-link-hover-color: #38a63d;
--warning-color: #FF4500;
--border-color: #222;
}
*:not(.tabulator-cell) {
border-radius: var(--border-radius) !important;
}
html {
font-size: var(--base-font-size);
}
body {
background-color: var(--blender-window-background, var(--bg-color));
color: var(--blender-text, var(--text-color));
margin: 0;
font-family: var(--font-family);
display: flex;
flex-direction: column;
min-height: 100vh;
}
#container {
position: relative;
padding: var(--margin-small);
flex: 1;
}
h3 {
margin-top: 0;
}
nav {
background-color: var(--blender-top-bar-header, var(--nav-bg-color));
height: var(--nav-height);
padding: var(--padding-medium) 0;
display: flex;
align-items: center;
position: relative;
border-bottom: 2px solid var(--blender-tab-outline, var(--nav-border-color));
}
nav .logo {
margin-left: var(--margin-large);
height: var(--logo-height);
}
nav ul {
list-style-type: none;
margin: 0;
display: flex;
flex: 1;
justify-content: center;
}
nav ul li {
margin-right: var(--margin-large);
}
nav ul li a {
text-decoration: none;
color: var(--blender-text, var(--nav-link-color));
}
nav ul li a:hover,
nav ul li a.active {
color: var(--blender-selected-object, var(--nav-link-hover-color));
}
.warning,
.warning-icon {
color: var(--blender-info-warning, var(--warning-color));
}
#toggle-theme {
border: none;
background: none;
cursor: pointer;
font-size: var(--font-size-large);
margin-right: var(--margin-medium);
}
#toggle-theme:focus {
outline: none;
}
#client-list {
position: absolute;
top: calc(0 + var(--nav-height));
overflow-y: auto;
overflow: hidden;
transition: opacity 0.3s ease-out, visibility 0.3s ease-out;
border: 1px solid var(--blender-tab-outline, var(--border-color));
background-color: var(--blender-panel-background, var(--nav-bg-color));
z-index: 1;
opacity: 0;
visibility: hidden;
width: auto;
}
#client-list.show {
opacity: 1;
visibility: visible;
}
#connected-list-div {
display: inline-block;
}
.client {
padding: var(--margin-small);
border-bottom: 1px solid var(--blender-tab-outline, var(--border-color));
cursor: pointer;
}
.client-details {
max-height: 0;
overflow: hidden;
padding-left: var(--padding-small);
transition: max-height 0.2s ease-out, padding 0.2s ease-out, opacity 0.1s ease-out;
background-color: var(--blender-panel-background, var(--nav-bg-color));
margin-top: var(--margin-tiny);
opacity: 0;
}
.client-details.show {
max-height: none;
opacity: 1;
}
.client-detail {
border-bottom: 1px solid var(--blender-tab-outline, var(--border-color));
padding: var(--padding-small);
}
#show-connected-button {
width: auto;
background-color: var(--blender-panel-header, var(--bg-color));
font-size: var(--base-font-size);
color: var(--blender-text, var(--text-color))
}
#show-connected-button:hover,
.scroll-button:hover {
cursor: pointer;
}
.scroll-button {
font-size: var(--base-font-size);
margin-left: var(--margin-tiny);
margin-top: var(--margin-small);
}
footer {
background-color: var(--blender-top-bar-header, var(--nav-bg-color));
text-align: right;
padding: var(--padding-tiny);
border-top: 1px solid var(--blender-tab-outline, var(--nav-border-color));
}
footer p {
margin: 0;
color: var(--blender-text, var(--nav-link-color));
margin-right: var(--margin-small);
font-size: 0.8rem;
}
@import url("./shared.css");
.table-container {
display: block;
@@ -212,18 +11,6 @@ footer p {
margin-top: var(--margin-small);
}
button,
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;
}
button:hover {
filter: brightness(1.2);
}
/* ------------ Overwriting Tabulator CSS rules ------------ */
:root.light .tabulator-header,
@@ -278,15 +65,12 @@ button:hover {
color: var(--blender-text);
}
:root.blender .tabulator-selected .tabulator-cell,
:root.blender .tabulator-row.tabulator-selected {
background-color: var(--blender-active-highlight) !important;
color: var(--blender-active-text-highlight) !important;
}
:root.blender .tabulator-selected:hover .tabulator-cell,
:root.blender .tabulator-row.tabulator-selected:hover {
filter: brightness(1.2)
@@ -0,0 +1,214 @@
:root {
--font-family: Arial, sans-serif;
--base-font-size: 16px;
--margin-tiny: 0.125rem;
--margin-small: 0.625rem;
--margin-medium: 1.25rem;
--margin-large: 2.5rem;
--padding-tiny: 0.125rem;
--padding-small: 0.625rem;
--padding-medium: 1rem;
--font-size-large: 1.2rem;
--logo-height: 1.5rem;
--nav-height: 0.625rem;
--border-radius: 0.3rem;
}
:root.dark {
color-scheme: dark;
--bg-color: #252525;
--text-color: #e0e0e0;
--nav-bg-color: #121212;
--nav-border-color: #25682a;
--nav-link-color: #fff;
--nav-link-hover-color: #3fb449;
--warning-color: #FFDB8F;
--border-color: #464444;
}
:root.light {
color-scheme: light;
--bg-color: #ffffff;
--text-color: #000000;
--nav-bg-color: #f8f8f8;
--nav-border-color: #cccccc;
--nav-link-color: #000000;
--nav-link-hover-color: #38a63d;
--warning-color: #FF4500;
--border-color: #222;
}
*:not(.tabulator-cell) {
border-radius: var(--border-radius) !important;
}
html {
font-size: var(--base-font-size);
}
body {
background-color: var(--blender-window-background, var(--bg-color));
color: var(--blender-text, var(--text-color));
margin: 0;
font-family: var(--font-family);
display: flex;
flex-direction: column;
min-height: 100vh;
}
#container {
position: relative;
padding: var(--margin-small);
flex: 1;
}
h3 {
margin-top: 0;
}
nav {
background-color: var(--blender-top-bar-header, var(--nav-bg-color));
height: var(--nav-height);
padding: var(--padding-medium) 0;
display: flex;
align-items: center;
position: relative;
border-bottom: 2px solid var(--blender-tab-outline, var(--nav-border-color));
}
nav .logo {
margin-left: var(--margin-large);
height: var(--logo-height);
}
nav ul {
list-style-type: none;
margin: 0;
display: flex;
flex: 1;
justify-content: center;
}
nav ul li {
margin-right: var(--margin-large);
}
nav ul li a {
text-decoration: none;
color: var(--blender-text, var(--nav-link-color));
}
nav ul li a:hover,
nav ul li a.active {
color: var(--blender-selected-object, var(--nav-link-hover-color));
}
.warning,
.warning-icon {
color: var(--blender-info-warning, var(--warning-color));
}
#toggle-theme {
border: none;
background: none;
cursor: pointer;
font-size: var(--font-size-large);
margin-right: var(--margin-medium);
}
#toggle-theme:focus {
outline: none;
}
#client-list {
position: absolute;
top: calc(0 + var(--nav-height));
overflow-y: auto;
overflow: hidden;
transition: opacity 0.3s ease-out, visibility 0.3s ease-out;
border: 1px solid var(--blender-tab-outline, var(--border-color));
background-color: var(--blender-panel-background, var(--nav-bg-color));
z-index: 1;
opacity: 0;
visibility: hidden;
width: auto;
}
#client-list.show {
opacity: 1;
visibility: visible;
}
#connected-list-div {
display: inline-block;
}
.client {
padding: var(--margin-small);
border-bottom: 1px solid var(--blender-tab-outline, var(--border-color));
cursor: pointer;
}
.client-details {
max-height: 0;
overflow: hidden;
padding-left: var(--padding-small);
transition: max-height 0.2s ease-out, padding 0.2s ease-out, opacity 0.1s ease-out;
background-color: var(--blender-panel-background, var(--nav-bg-color));
margin-top: var(--margin-tiny);
opacity: 0;
}
.client-details.show {
max-height: none;
opacity: 1;
}
.client-detail {
border-bottom: 1px solid var(--blender-tab-outline, var(--border-color));
padding: var(--padding-small);
}
#show-connected-button {
width: auto;
background-color: var(--blender-panel-header, var(--bg-color));
font-size: var(--base-font-size);
color: var(--blender-text, var(--text-color))
}
#show-connected-button:hover,
.scroll-button:hover {
cursor: pointer;
}
.scroll-button {
font-size: var(--base-font-size);
margin-left: var(--margin-tiny);
margin-top: var(--margin-small);
}
footer {
background-color: var(--blender-top-bar-header, var(--nav-bg-color));
text-align: right;
padding: var(--padding-tiny);
border-top: 1px solid var(--blender-tab-outline, var(--nav-border-color));
}
footer p {
margin: 0;
color: var(--blender-text, var(--nav-link-color));
margin-right: var(--margin-small);
font-size: 0.8rem;
}
button,
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;
}
button:hover {
filter: brightness(1.2);
}
@@ -35,6 +35,7 @@ function handleBlenderConnect(blenderId) {
$("#blender-count").text(function (i, text) {
return parseInt(text, 10) + 1;
});
console.log("Blender connected", blenderId);
}
function handleBlenderDisconnect(blenderId) {
@@ -53,15 +54,25 @@ function removeTableElement(blenderId) {
}
const costValueCallbacks = {
'editCostValues': editCostValues,
'addCostValue': addCostValue,
'deleteCostValue': deleteCostValue,
};
function handleCostValueData(data) {
const costItemId = data.data["cost_value"]["cost_item_id"];
const costValueId = data.data["cost_value"]["cost_value_id"];
console.log("Handling cost value data", costItemId, costValueId);
CostUI.addNewCostValueRow(costItemId, costValueId);
CostUI.addNewCostValueRow(costItemId, costValueId, costValueCallbacks);
}
function deleteCostValue(costItemId, costValueId) {
executeOperator({ type: "deleteCostValue", costItemId: costItemId, costValueId: costValueId });
}
function handleConnectedClients(data) {
$("#blender-count").text(data.length);
@@ -77,8 +88,12 @@ function handleCostValuesData(data) {
callbacks: {
'editCostValues': editCostValues,
'addCostValue': addCostValue,
'deleteCostValue': deleteCostValue,
},
});
CostUI.highlightCostItem(data.data["cost_values"]["cost_item_id"]);
}
function addCostValue(costItemId) {
@@ -93,6 +108,10 @@ function editCostValues(costItemId, costValues) {
});
}
function deleteCostItem(costItemId) {
executeOperator({ type: "deleteCostItem", costItemId: costItemId });
}
function handleThemeData(themeData) {
function arrayToRgbString(arr) {
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
@@ -165,14 +184,23 @@ function handleCostSchedulesData(data) {
const card = CostUI.createCard(costSchedule.Name, mainContainer, callback);
costScheduleDiv.append(card);
});
const firstCostSchedule = costSchedules[0];
if (firstCostSchedule) {
loadCostSchedule(firstCostSchedule.id, blenderId);
}
}
function handleCostItemsData(data) {
console.log("Handling cost items data", data);
CostUI.createCostSchedule({
data: data.data["cost_items"],
blenderID: data.blenderId,
callbacks: {
"addCostItem": addCostItem,
"deleteCostItem": deleteCostItem,
"duplicateCostItem": duplicateCostItem,
"selectAssignedElements": selectAssignedElements,
'editCostItemName': editCostItemName,
'enableEditingCostValues': enableEditingCostValues,
@@ -180,6 +208,10 @@ function handleCostItemsData(data) {
});
}
function duplicateCostItem(costItemId) {
executeOperator({ type: "duplicateCostItem", costItemId: costItemId });
}
function executeOperator(operator, blenderId) {
const msg = {
sourcePage: "cost",
@@ -201,4 +233,4 @@ function getCostSchedules(blenderId) {
function enableEditingCostValues(costItemId) {
executeOperator({ type: "enableEditingCostValues", costItemId: costItemId});
}
}
@@ -29,7 +29,7 @@ function connectSocket() {
socket.on("blender_disconnect", handleBlenderDisconnect);
socket.on("connect", handleWebConnect);
socket.on("connected_clients", handleConnectedClients);
socket.on("default_data", handleDefaultData);
//socket.on("default_data", handleDefaultData);
socket.on("theme_data", handleThemeData);
socket.on("drawings_data", handleDrawingsData);
socket.on("svg_data", handleSvgData);
@@ -37,6 +37,7 @@ function connectSocket() {
// function used to get drawings data from Bonsai
function handleWebConnect() {
console.log("Handle Web Connect")
getDrawingsData();
}
@@ -73,7 +74,6 @@ function handleDefaultData(data) {
// Function to handle 'blender_disconnect' event
function handleBlenderDisconnect(blenderId) {
console.log("blender disconnected: ", blenderId);
if (connectedClients.hasOwnProperty(blenderId)) {
delete connectedClients[blenderId];
removeSelectOption(blenderId);
@@ -97,8 +97,6 @@ function handleConnectedClients(data) {
}
function handleThemeData(themeData) {
// console.log(themeData);
function arrayToRgbString(arr) {
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
if (a !== undefined) {
@@ -129,6 +127,7 @@ function handleThemeData(themeData) {
}
function handleDrawingsData(data) {
console.log("Running handleDrawingsData");
const blenderId = data["blenderId"];
console.log(data);
@@ -23,11 +23,10 @@ export class CostUI {
for (let i = 0; i < columnHeaders.length; i++) {
const th = document.createElement("th");
th.textContent = columnHeaders[i];
th.style.position = "relative"; // Required for the resizer handle
th.style.position = "relative";
tr.appendChild(th);
// Add resizer handle
if (i < columnHeaders.length - 1) { // No resizer for the last column
if (i < columnHeaders.length - 1) {
const resizer = document.createElement("div");
resizer.classList.add("resizer");
th.appendChild(resizer);
@@ -39,12 +38,8 @@ export class CostUI {
table.appendChild(tbody);
document.getElementById("cost-items").appendChild(table);
// Add CSS to set column widths, resizer styles, hover effect, and color scheme
CostUI.addTableStyles(id);
// Create context menu
CostUI.createContextMenu(callbacks);
table.get_blender_id = function() {
return this.getAttribute("id").split("-")[2];
};
@@ -69,19 +64,17 @@ export class CostUI {
}
static createContextMenu(callbacks) {
// Create context menu
const contextMenu = document.createElement("div");
contextMenu.id = "context-menu";
contextMenu.classList.add("context-menu");
contextMenu.innerHTML = `
<button id="edit-cost-values-button">Edit Cost Values</button>
<button id="add-cost-item-button">Add sub-cost</button>
<button id="edit-cost-values-button">Edit</button>
<button id="delete-button">Delete</button>
<button id="duplicate-button">Duplicate</button>
<button id="delete-button">Delete</button>
`;
document.body.appendChild(contextMenu);
// Add event listeners for context menu
document.addEventListener("contextmenu", function(event) {
event.preventDefault();
const targetRow = event.target.closest("tr");
@@ -92,7 +85,6 @@ export class CostUI {
contextMenu.style.left = `${event.pageX}px`;
contextMenu.style.top = `${event.pageY}px`;
// Store the target row in the context menu for later use
contextMenu.targetRow = targetRow;
contextMenu.targetCell = targetCell;
} else {
@@ -109,7 +101,6 @@ export class CostUI {
const editCostValuesButton = document.getElementById("edit-cost-values-button");
editCostValuesButton.addEventListener("click", function() {
console.log("Edit cost values executed");
const targetRow = document.getElementById("context-menu").targetRow;
const targetCell = document.getElementById("context-menu").targetCell;
if (targetRow) {
@@ -133,24 +124,45 @@ export class CostUI {
addButton.dataset.listenerAdded = "true";
}
document.getElementById("delete-button").addEventListener("click", function() {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
// Implement your delete action here
console.log("Delete row:", targetRow.getAttribute("id"));
targetRow.remove();
}
});
const deleteButton = document.getElementById("delete-button");
if (deleteButton && !deleteButton.hasListener) {
deleteButton.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);
}
});
deleteButton.hasListener = true;
}
document.getElementById("duplicate-button").addEventListener("click", function() {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
// Implement your duplicate action here
console.log("Duplicate row:", targetRow.getAttribute("id"));
const newRow = targetRow.cloneNode(true);
targetRow.parentElement.appendChild(newRow);
}
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);
}
});
duplicateButton.hasListener = true;
}
}
static deleteRow(targetRow, expandedState) {
if (!targetRow) {
return
}
delete expandedState[targetRow.id]
targetRow.remove()
const subRows = document.querySelectorAll(`[parent-id='${targetRow.id}']`);
subRows.forEach(subRow => {
CostUI.deleteRow(subRow, expandedState)
});
return expandedState
}
static addResizer(resizer) {
@@ -176,18 +188,15 @@ export class CostUI {
}
static generateColorScheme(baseColor) {
// This function generates a color scheme based on the base color
// For simplicity, we'll just lighten the base color for each level
const levels = 7; // Number of levels of nesting
const levels = 10;
const colorScheme = [];
for (let i = 0; i < levels; i++) {
colorScheme.push(CostUI.lightenColor(baseColor, i * 7));
colorScheme.push(CostUI.lightenColor(baseColor, i * 5));
}
return colorScheme;
}
static lightenColor(color, percent) {
// This function lightens a color by a given percentage
const num = parseInt(color.slice(1), 16),
amt = Math.round(2.55 * percent),
R = (num >> 16) + amt,
@@ -199,7 +208,7 @@ export class CostUI {
static applyColorScheme(tableId, colorScheme) {
const rows = document.querySelectorAll(`#${tableId} tbody tr`);
rows.forEach((row, index) => {
const level = index % colorScheme.length; // Assuming level is determined by index for simplicity
const level = index % colorScheme.length;
row.style.backgroundColor = colorScheme[level];
});
}
@@ -209,7 +218,7 @@ export class CostUI {
const colorPicker = document.createElement("input");
colorPicker.type = "color";
colorPicker.id = "color-picker";
colorPicker.value = "#ff0000"; // Default color
colorPicker.value = "#ff0000";
const colorText = document.createElement("p");
colorText.textContent = "Select a color to change row color:";
document.getElementById("UI").appendChild(colorText);
@@ -231,25 +240,26 @@ export class CostUI {
}
static createCostItem(data, container, nestingLevel = 0, parentID = null, callbacks = {}) {
data.forEach(obj => {
const row = CostUI.createRow(obj, nestingLevel, parentID, callbacks);
data.forEach(costItem => {
const row = CostUI.createRow(costItem, nestingLevel, parentID, callbacks);
container.appendChild(row);
if (obj.is_nested_by && obj.is_nested_by.length > 0) {
CostUI.createCostItem(obj.is_nested_by, container, nestingLevel + 1, obj.id, callbacks);
if (costItem.is_nested_by && costItem.is_nested_by.length > 0) {
CostUI.createCostItem(costItem.is_nested_by, container, nestingLevel + 1, costItem.id, callbacks);
}
});
}
static createRow(obj, nestingLevel, parentID, callbacks = {}) {
const row = CostUI.createTableRow(obj, nestingLevel, parentID);
const expandButton = CostUI.createExpandButton(obj);
const nameCell = CostUI.createNameCell(obj, nestingLevel, expandButton, callbacks);
const totalCostQuantityCell = CostUI.createTableCell(obj.TotalCostQuantity);
const unitSymbolCell = CostUI.createTableCell(obj.UnitSymbol);
const totalAppliedValueCell = CostUI.createTableCell(obj.TotalAppliedValue);
const totalCostCell = CostUI.createTotalCostCell(obj);
const flexContainerCell = CostUI.createFlexContainerCell(obj, callbacks);
static createRow(costItem, nestingLevel, parentID, callbacks = {}) {
const totalQuantity = costItem.TotalCostQuantity ? parseFloat(costItem.TotalCostQuantity).toFixed(2) : '-';
const row = CostUI.createTableRow(costItem, nestingLevel, parentID);
const expandButton = CostUI.createExpandButton(costItem);
const nameCell = CostUI.createNameCell(costItem, nestingLevel, expandButton, callbacks);
const totalCostQuantityCell = CostUI.createTableCell(totalQuantity);
const unitSymbolCell = CostUI.createTableCell(costItem.UnitSymbol);
const totalAppliedValueCell = CostUI.createTableCell(costItem.TotalAppliedValue);
const totalCostCell = CostUI.createTotalCostCell(costItem);
const flexContainerCell = CostUI.createFlexContainerCell(costItem, callbacks);
row.appendChild(nameCell);
row.appendChild(totalCostQuantityCell);
@@ -270,49 +280,167 @@ export class CostUI {
return row;
}
static createTableRow(obj, nestingLevel, parentID) {
static createTableRow(costItem, nestingLevel, parentID) {
const row = document.createElement("tr");
row.setAttribute("id", obj.id);
row.setAttribute("id", costItem.id);
row.setAttribute("parent-id", parentID);
if (nestingLevel > 0) {
row.classList.add("nested");
row.classList.add(`level-${nestingLevel}`);
}
return row;
}
static createExpandButton(row) {
const expandButton = document.createElement("i");
expandButton.classList.add("action-button");
expandButton.classList.add("fa-solid");
expandButton.classList.add("fa-angle-right");
static createExpandButton(obj) {
const expandButton = document.createElement("button");
expandButton.classList.add("toggle-button");
if (obj.is_nested_by && obj.is_nested_by.length > 0) {
expandButton.textContent = ">";
if (row.is_nested_by && row.is_nested_by.length > 0) {
expandButton.classList.add('fa-angle-right');
} else {
expandButton.style.visibility = "hidden";
}
expandButton.addEventListener("click", function() {
CostUI.contractExpandRow.call(this, obj.id);
expandButton.addEventListener("click", function(event) {
event.stopPropagation();
let isExpanded = CostUI.isRowExpanded(row.id);
console.log("Row is expanded", isExpanded);
if (event.shiftKey) {
if (isExpanded) {
CostUI.contractRow(row.id);
CostUI.updateExpandedState(row.id, false, true);
} else {
CostUI.expandRow(row.id);
CostUI.updateExpandedState(row.id, true, true);
}
} else {
if (isExpanded) {
CostUI.contractRow(row.id);
CostUI.updateExpandedState(row.id, false);
}
else {
CostUI.expandRow(row.id);
CostUI.updateExpandedState(row.id, true);
}
}
CostUI.applyExpandedState();
});
return expandButton;
}
static createNameCell(obj, nestingLevel, expandButton, callbacks) {
static hideRow(id) {
const row = document.getElementById(id);
if (row) {
row.classList.add("nested");
}
}
static contractRow(parentId, recursive=false) {
CostUI.updateToggleButton(parentId, 'contract');
const rows = document.querySelectorAll(`[parent-id='${parentId}']`);
rows.forEach(row => {
const childId = row.getAttribute('id');
CostUI.hideRow(childId);
if (recursive || (childId && CostUI.isRowExpanded(childId))) {
CostUI.contractRow(childId, recursive)
}
// CostUI.contractRow(childId);
});
}
static showRow(id) {
const row = document.getElementById(id);
if (row) {
row.classList.remove("nested");
}
}
static updateToggleButton(id, type='contract') {
const toggleButton = document.querySelector(`[id='${id}'] .action-button`);
if (!toggleButton) {
return;
}
if (type === 'expand') {
toggleButton.classList.remove("fa-angle-right");
toggleButton.classList.add("fa-angle-up");
}
else {
toggleButton.classList.remove("fa-angle-up");
toggleButton.classList.add("fa-angle-right");
}
}
static expandRow(parentId, recursive=false) {
CostUI.updateExpandedState(parentId, true);
CostUI.updateToggleButton(parentId, 'expand');
const rows = document.querySelectorAll(`[parent-id='${parentId}']`);
rows.forEach(row => {
const childId = row.getAttribute('id');
CostUI.showRow(childId);
if (recursive || (childId && CostUI.isRowExpanded(childId))) {
CostUI.expandRow(childId, recursive);
}
});
}
static updateExpandedState(id, isExpanded, recursive=false) {
const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {};
expandedState[id] = isExpanded;
localStorage.setItem('expandedState', JSON.stringify(expandedState));
if (recursive) {
const rows = document.querySelectorAll(`[parent-id='${id}']`);
rows.forEach(row => {
CostUI.updateExpandedState(row.id, isExpanded, recursive);
});
}
}
static applyExpandedState() {
const expandedState = JSON.parse(localStorage.getItem('expandedState')) || CostUI.getAllCostItemsIds().reduce((acc, id) => {
acc[id] = true;
return acc;
}, {});
console.log("Expanded state", expandedState);
Object.keys(expandedState).forEach(id => {
if (expandedState[id]) {
CostUI.expandRow(id);
} else {
CostUI.contractRow(id);
}
});
}
static isRowExpanded(id) {
const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {};
return expandedState[id] || false;
}
static createNameCell(costItem, nestingLevel, expandButton, callbacks) {
const nameCell = document.createElement("td");
const nameInput = document.createElement("input");
nameInput.value = obj.name ? obj.name : "Unnamed";
nameInput.value = costItem.name ? costItem.name : "Unnamed";
nameInput.addEventListener("change", function() {
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
callbacks.editCostItemName ? callbacks.editCostItemName(costItem.id, this.value) : null;
});
nameInput.addEventListener("keydown", function(event) {
if (event.key === "Enter") {
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
callbacks.editCostItemName ? callbacks.editCostItemName(costItem.id, this.value) : null;
}
});
nameCell.style.paddingLeft = nestingLevel * 20 + "px";
nameCell.appendChild(expandButton);
nameCell.appendChild(nameInput);
const widthButton = 20;
nameInput.style.width = "calc(100% - " + widthButton + "%)";
return nameCell;
}
@@ -322,19 +450,19 @@ export class CostUI {
return cell;
}
static createTotalCostCell(obj) {
static createTotalCostCell(costItem) {
const totalCostCell = document.createElement("td");
const totalCost = parseFloat(obj.TotalCost).toFixed(2);
totalCostCell.textContent = obj.is_sum ? totalCost + " (Σ)" : totalCost;
const totalCost = parseFloat(costItem.TotalCost).toFixed(2);
totalCostCell.textContent = costItem.is_sum ? totalCost + " (Σ)" : totalCost;
return totalCostCell;
}
static createFlexContainerCell(obj, callbacks) {
static createFlexContainerCell(costItem, callbacks) {
const divFlex = document.createElement("div");
divFlex.classList.add("flex-container");
const addButton = CostUI.createAddButton(obj, callbacks);
const selectButton = CostUI.createSelectButton(obj, callbacks);
const addButton = CostUI.createAddButton(costItem, callbacks);
const selectButton = CostUI.createSelectButton(costItem, callbacks);
divFlex.appendChild(addButton);
divFlex.appendChild(selectButton);
@@ -344,102 +472,52 @@ export class CostUI {
return flexContainerCell;
}
static createAddButton(obj, callbacks) {
static createAddButton(costItem, callbacks) {
const addButton = document.createElement("button");
addButton.textContent = "+";
addButton.classList.add("add-button");
addButton.addEventListener("click", function(e) {
e.stopPropagation();
callbacks.addCostItem ? callbacks.addCostItem(obj.id) : null;
callbacks.addCostItem ? callbacks.addCostItem(costItem.id) : null;
});
return addButton;
}
static createSelectButton(obj, callbacks) {
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(obj.id) : null;
callbacks.selectAssignedElements ? callbacks.selectAssignedElements(costItem.id) : null;
});
return selectButton;
}
static hideNestedRows(parentId) {
const rows = document.querySelectorAll(`[parent-id='${parentId}']`);
rows.forEach(row => {
row.classList.add("nested");
const childId = row.getAttribute('id');
if (childId) {
CostUI.hideNestedRows(childId);
}
});
}
static showNestedRows(parentId) {
const rows = document.querySelectorAll(`[parent-id='${parentId}']`);
rows.forEach(row => {
row.classList.remove("nested");
const childId = row.getAttribute('id');
if (childId && CostUI.isRowExpanded(childId)) {
CostUI.showNestedRows(childId);
}
});
}
static contractExpandRow(id) {
const rows = document.querySelectorAll(`[parent-id='${id}']`);
if (rows.length === 0) {
return;
}
let isVisible = false;
rows.forEach(row => {
if (!row.classList.contains("nested")) {
isVisible = true;
}
});
if (isVisible) {
CostUI.hideNestedRows(id);
this.textContent = ">";
CostUI.updateExpandedState(id, false);
} else {
CostUI.showNestedRows(id);
this.textContent = "^";
CostUI.updateExpandedState(id, true);
static highlightCostItem(id) {
const row = document.getElementById(id);
if (row) {
row.classList.add("highlighted");
}
}
static updateExpandedState(id, isExpanded) {
const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {};
expandedState[id] = isExpanded;
localStorage.setItem('expandedState', JSON.stringify(expandedState));
static unhighlightCostItem(id) {
const row = document.getElementById(id);
if (row) {
row.classList.remove("highlighted");
}
}
static applyExpandedState() {
const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {};
Object.keys(expandedState).forEach(id => {
if (expandedState[id]) {
CostUI.showNestedRows(id);
const toggleButton = document.querySelector(`[id='${id}'] .toggle-button`);
if (toggleButton) {
toggleButton.textContent = "^";
}
} else {
CostUI.hideNestedRows(id);
const toggleButton = document.querySelector(`[id='${id}'] .toggle-button`);
if (toggleButton) {
toggleButton.textContent = ">";
}
}
static getAllCostItemsIds() {
const rows = document.querySelectorAll("#cost-items tr");
const ids = [];
rows.forEach(row => {
ids.push(row.id);
});
return ids;
}
static isRowExpanded(id) {
const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {};
return expandedState[id] || false;
}
static text(label) {
const text = document.createElement("p");
@@ -459,7 +537,7 @@ export class CostUI {
cardTitle.textContent = title;
const cardButton = document.createElement("button");
cardButton.classList.add("btn", "btn-primary");
cardButton.classList.add("action-button");
cardButton.textContent = "Load";
cardButton.addEventListener("click", callback);
@@ -471,8 +549,15 @@ export class CostUI {
return card;
}
static getRowNameCell(costItemId) {
return document.getElementById(costItemId).querySelector("td input");
}
static getCostItemName(costItemId) {
return CostUI.getRowNameCell(costItemId).value;
}
static createCostValuesForm({ costItemId, costValues, callbacks }) {
// Check if the form already exists and remove it if it does
const formId = "cost-values-form-" + costItemId;
let existingForm = document.getElementById(formId);
if (existingForm) {
@@ -480,14 +565,14 @@ export class CostUI {
}
const form = CostUI.createFormElement(formId, "cost-values-form");
const header = CostUI.createHeader("Cost Values Form");
const costItemName = CostUI.getCostItemName(costItemId);
const header = CostUI.createHeader("Cost Values Form for " + costItemName);
form.appendChild(header);
const table = CostUI.createCostValuesTable(costItemId, ["Type", "Category", "Value"]);
const table = CostUI.createCostValuesTable(costItemId, ["Type", "Category", "Value", ""]);
costValues.forEach(costValue => {
const tr = CostUI.createCostvaluesRow(costItemId, costValue);
const tr = CostUI.createCostvaluesRow(costItemId, costValue, callbacks);
table.appendChild(tr);
});
@@ -496,9 +581,6 @@ export class CostUI {
const addButton = CostUI.createAddCostValueButton(costItemId, callbacks);
form.appendChild(addButton);
const submitButton = CostUI.createSubmitButton(callbacks);
form.appendChild(submitButton);
const closeButton = CostUI.createCloseButton(form);
form.appendChild(closeButton);
@@ -514,16 +596,15 @@ export class CostUI {
return document.getElementById("cost-values-table-" + costItemId);
}
static addNewCostValueRow(costItemId, costValueId) {
static addNewCostValueRow(costItemId, costValueId, costValueCallbacks) {
const table = CostUI.getCostValuesTable(costItemId);
// check if table exists
if (!table) {
console.log("Cost values table not found for cost item ID:", costItemId);
return;
}
console.log("Adding new cost value row", costValueId);
console.log(table)
const tr = CostUI.createCostvaluesRow(costItemId, {"category": "", "name": "", "applied_value": 0, "id": costValueId, "parent": costItemId});
const tr = CostUI.createCostvaluesRow(costItemId, {"category": "", "name": "", "applied_value": 0, "id": costValueId, "parent": costItemId}, costValueCallbacks);
table.appendChild(tr);
}
@@ -543,19 +624,14 @@ export class CostUI {
return table;
}
static createCostvaluesRow(costItemId,costValue) {
static createCostvaluesRow(costItemId,costValue, costValueCallbacks) {
function cleanLabel(value) {
// remove anything which is not a dot or a digit
return value.replace(/[^0-9.]/g, '');
}
if (document.getElementById(costValue.id)) {
return document.getElementById(costValue.id);
}
const tr = document.createElement("tr");
tr.isCostValue = true;
tr.parent = costItemId;
@@ -577,6 +653,24 @@ export class CostUI {
dropdown.addEventListener("change", function() {
const selectedType = this.value;
CostUI.updateRowBasedOnType(selectedType, categoryCell, valueCell1);
if (selectedType === "SUM") {
const costValueId = parseInt(this.closest("tr").getAttribute("id"));
const appliedValue = parseFloat(valueCell1.querySelector("input").value);
const costCategory = categoryCell.querySelector("input").value;
if (costValueCallbacks.editCostValues) {
costValueCallbacks.editCostValues(costItemId, [
{
costType: selectedType,
costCategory: costCategory,
appliedValue: appliedValue,
id: costValueId,
costItemId: costItemId
}
]);
}
}
});
tr.appendChild(typeCell);
@@ -593,9 +687,40 @@ export class CostUI {
}
const valueCell1 = CostUI.createTableInput("number", "value", value);
tr.appendChild(valueCell1);
// Apply initial state based on costType
const inputValue = valueCell1.querySelector("input");
function handleCostValueChange(costItemId) {
const costValueData = {
costType: typeCell.querySelector("select").value,
costCategory: categoryCell.querySelector("input").value,
appliedValue: parseFloat(this.value),
id: costValue.id,
costItemId: costItemId
}
if (costValueCallbacks.editCostValues) {
costValueCallbacks.editCostValues(costItemId, [costValueData]);
}
}
inputValue.addEventListener("change", handleCostValueChange.bind(inputValue, costItemId));
inputValue.addEventListener("keydown", function(event) {
if (event.key === "Enter") {
event.preventDefault();
handleCostValueChange.call(this, costItemId);
}
});
const deleteCell = document.createElement("td");
tr.appendChild(deleteCell);
const deleteButton = document.createElement("button");
deleteButton.classList.add("action-button");
deleteButton.textContent = "Delete";
deleteButton.addEventListener("click", function() {
costValueCallbacks.deleteCostValue(costItemId, costValue.id);
tr.remove();
});
deleteCell.appendChild(deleteButton);
CostUI.updateRowBasedOnType(costType, categoryCell, valueCell1);
return tr;
}
@@ -650,8 +775,8 @@ export class CostUI {
static createAddCostValueButton(costItemId, callbacks) {
const addButton = document.createElement("button");
addButton.textContent = "+";
addButton.classList.add("add-button");
addButton.classList.add("action-button");
addButton.textContent = " + Add Cost Value";
addButton.addEventListener("click", function(e) {
e.preventDefault();
callbacks.addCostValue ? callbacks.addCostValue(costItemId) : null;
@@ -659,38 +784,11 @@ export class CostUI {
return addButton;
}
static createSubmitButton(callbacks) {
const submitButton = document.createElement("button");
submitButton.type = "submit";
submitButton.textContent = "Save";
submitButton.addEventListener("click", function(e) {
e.preventDefault();
const form = this.closest("form");
const costValues = [];
const rows = Array.from(form.querySelectorAll("tr")).filter(row => row.isCostValue);
rows.forEach(row => {
const costCategory = row.querySelector("input[name='category']").value;
const appliedValue = parseFloat(row.querySelector("input[name='value']").value);
const costType = row.querySelector("select[name='type']").value;
const id = parseInt(row.id);
costValues.push({ costType, costCategory, appliedValue, id: id, costItemId: row.parent });
});
const costItemId = parseInt(form.id.split("-")[3]);
console.log(form.id)
console.log("Cost values to be saved:", costItemId);
callbacks.editCostValues ? callbacks.editCostValues(costItemId, costValues) : null;
form.remove();
});
return submitButton;
}
static createFormElement(id, className) {
const form = document.createElement("form");
form.id = id;
form.classList.add(className);
form.style.position = "absolute"; // Set initial position to absolute
form.style.position = "absolute";
form.style.top = "50%";
form.style.left = "50%";
form.style.transform = "translate(-50%, -50%)";
@@ -710,7 +808,10 @@ export class CostUI {
closeButton.innerHTML = "&times;";
closeButton.addEventListener("click", function() {
form.remove();
const costItemId = form.id.split("-")[3];
CostUI.unhighlightCostItem(costItemId);
});
return closeButton;
}
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlenderBIM Web UI</title>
<link rel="stylesheet" type="text/css" href="/static/css/gantt.css" id="index-stylesheet" />
<link rel="stylesheet" type="text/css" href="/static/css/cost.css" id="index-stylesheet" />
<link
rel="stylesheet"
id="tabulator-stylesheet"
@@ -32,30 +32,6 @@
<script type="module" defer src="./static/js/cost.js"></script>
<style>
table {
width: 100%;
border-collapse: collapse;
}
.nested {
display: none;
}
.expand-collapse {
cursor: pointer;
}
.caret-cell {
border: none;
background: none;
padding: 0;
}
td {
justify-content: space-between;
}
.flex-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
</style>
</head>
<body>
@@ -73,7 +49,7 @@
</li>
<li>
<a href="/costing" class="active"
><i class="fa-solid fa-table"></i>Costing</a
><i class="fa-solid fa-money-bill-trend-up"></i> Costing</a
>
</li>
<li>
+22 -10
View File
@@ -364,6 +364,15 @@ class Web(bonsai.core.tool.Web):
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)
if operator_data["type"] == "deleteCostItem":
cost_schedule = tool.Cost.get_cost_schedule(cost_item=tool.Ifc.get().by_id(operator_data["costItemId"]))
bpy.ops.bim.remove_cost_item(cost_item=operator_data["costItemId"])
cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "duplicateCostItem":
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item)
new_cost_item = ifcopenshell.api.cost.copy_cost_item(ifc_file, cost_item)
cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "editCostItemName":
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
tool.Ifc.run(
@@ -391,18 +400,25 @@ class Web(bonsai.core.tool.Web):
data_key="cost_value",
event="cost_value"
)
if operator_data["type"] == "deleteCostValue":
bpy.ops.bim.remove_cost_value(
parent=operator_data["costItemId"],
cost_value=operator_data["costValueId"]
)
cost_item = ifc_file.by_id(operator_data["costItemId"])
cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item)
cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "editCostValues":
cost_item_id = operator_data["costItemId"]
print('Editing cost values', cost_item_id)
print(cost_item_id)
print(type(operator_data["costValues"]))
cost_item = ifc_file.by_id(cost_item_id)
cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item)
for value_data in operator_data["costValues"] or []:
print(value_data)
print("editing cost value data", value_data)
value = ifc_file.by_id(value_data["id"])
print(value.get_info())
if value_data["costType"] == "FIXED":
attributes= {
"AppliedValue": value_data["appliedValue"],
"Category": None
}
elif value_data["costType"] == "CATEGORY":
attributes= {
@@ -418,12 +434,8 @@ class Web(bonsai.core.tool.Web):
cost_value=value,
attributes= attributes
)
print(value.get_info())
tool.Cost.load_cost_schedule_tree()
cost_item = ifc_file.by_id(operator_data["costItemId"])
if not cost_item:
print("Cost item not found")
return
cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item)
cls.load_cost_schedule_web_ui(cost_schedule)