From d0712c718625b80d84b3ca6dbb5bc17a3608bec4 Mon Sep 17 00:00:00 2001 From: `myoualid` <`bosonprojets@gmail.com`> Date: Wed, 4 Sep 2024 23:39:46 +0100 Subject: [PATCH] 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 --- .../static/css/components/contextMenu.css | 46 -- .../bonsai/bim/data/webui/static/css/cost.css | 137 +++++ .../bim/data/webui/static/css/gantt.css | 1 - .../bim/data/webui/static/css/index.css | 218 +------- .../bim/data/webui/static/css/shared.css | 214 ++++++++ .../bonsai/bim/data/webui/static/js/cost.js | 36 +- .../bim/data/webui/static/js/drawings.js | 7 +- .../data/webui/static/js/utilities/costui.js | 481 +++++++++++------- .../bim/data/webui/templates/costing.html | 28 +- src/bonsai/bonsai/tool/web.py | 32 +- 10 files changed, 704 insertions(+), 496 deletions(-) create mode 100644 src/bonsai/bonsai/bim/data/webui/static/css/cost.css create mode 100644 src/bonsai/bonsai/bim/data/webui/static/css/shared.css diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/components/contextMenu.css b/src/bonsai/bonsai/bim/data/webui/static/css/components/contextMenu.css index 79ba11cc68..be67990261 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/components/contextMenu.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/components/contextMenu.css @@ -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; -} \ No newline at end of file diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/cost.css b/src/bonsai/bonsai/bim/data/webui/static/css/cost.css new file mode 100644 index 0000000000..62378d25b5 --- /dev/null +++ b/src/bonsai/bonsai/bim/data/webui/static/css/cost.css @@ -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 */ +} diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/gantt.css b/src/bonsai/bonsai/bim/data/webui/static/css/gantt.css index 36575b8c74..035fd68f0b 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/gantt.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/gantt.css @@ -1,5 +1,4 @@ @import url("./components/card.css"); -@import url("./components/contextMenu.css"); :root { --font-family: Arial, sans-serif; diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/index.css b/src/bonsai/bonsai/bim/data/webui/static/css/index.css index 510354f2bd..3cc7f9126a 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/index.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/index.css @@ -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) diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/shared.css b/src/bonsai/bonsai/bim/data/webui/static/css/shared.css new file mode 100644 index 0000000000..32a5f0477b --- /dev/null +++ b/src/bonsai/bonsai/bim/data/webui/static/css/shared.css @@ -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); +} \ No newline at end of file diff --git a/src/bonsai/bonsai/bim/data/webui/static/js/cost.js b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js index 5a10017b89..26809f90a0 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/js/cost.js +++ b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js @@ -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}); -} \ No newline at end of file +} diff --git a/src/bonsai/bonsai/bim/data/webui/static/js/drawings.js b/src/bonsai/bonsai/bim/data/webui/static/js/drawings.js index c23215f285..6fb929a228 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/js/drawings.js +++ b/src/bonsai/bonsai/bim/data/webui/static/js/drawings.js @@ -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); diff --git a/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js b/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js index 7ec2d6f582..f2cb5e5e4b 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js +++ b/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js @@ -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 = ` + - - + `; 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 = "×"; closeButton.addEventListener("click", function() { form.remove(); + const costItemId = form.id.split("-")[3]; + CostUI.unhighlightCostItem(costItemId); }); + return closeButton; } diff --git a/src/bonsai/bonsai/bim/data/webui/templates/costing.html b/src/bonsai/bonsai/bim/data/webui/templates/costing.html index c0297bfac4..ff07eeb9a5 100644 --- a/src/bonsai/bonsai/bim/data/webui/templates/costing.html +++ b/src/bonsai/bonsai/bim/data/webui/templates/costing.html @@ -4,7 +4,7 @@