From d6098a0b6d60e1e67333e08b7197c245efcbfdaa Mon Sep 17 00:00:00 2001 From: `myoualid` <`bosonprojets@gmail.com`> Date: Fri, 6 Sep 2024 08:55:10 +0100 Subject: [PATCH] - web ui features to add a new cost schedules, and root cost items - improved ui for cost schedules - sharing more css across files - refactor get_predefined_types(cost_schedule) --- src/bonsai/bonsai/bim/data/webui/sioserver.py | 5 + .../bonsai/bim/data/webui/static/css/cost.css | 55 +- .../bim/data/webui/static/css/gantt.css | 455 ++--- .../bim/data/webui/static/css/shared.css | 271 +-- .../bonsai/bim/data/webui/static/js/cost.js | 109 +- .../data/webui/static/js/utilities/costui.js | 1786 +++++++++-------- .../bim/data/webui/templates/costing.html | 10 +- .../bim/data/webui/templates/gantt.html | 4 +- src/bonsai/bonsai/bim/module/cost/data.py | 16 +- src/bonsai/bonsai/tool/web.py | 35 +- .../ifcopenshell/util/cost.py | 17 +- 11 files changed, 1456 insertions(+), 1307 deletions(-) diff --git a/src/bonsai/bonsai/bim/data/webui/sioserver.py b/src/bonsai/bonsai/bim/data/webui/sioserver.py index 9d1651b0a9..08565da659 100644 --- a/src/bonsai/bonsai/bim/data/webui/sioserver.py +++ b/src/bonsai/bonsai/bim/data/webui/sioserver.py @@ -176,6 +176,11 @@ class BlenderNamespace(socketio.AsyncNamespace): blender_messages[sid]["cost_value"] = data await sio.emit("cost_value", {"blenderId": sid, "data": data}, namespace="/web") + async def on_predefined_types(self, sid, data): + 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 schedules(request): with open("templates/index.html", "r") as f: template = f.read() diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/cost.css b/src/bonsai/bonsai/bim/data/webui/static/css/cost.css index 62378d25b5..888971886e 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/cost.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/cost.css @@ -5,24 +5,19 @@ 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 { @@ -39,22 +34,15 @@ td { padding: 0; } -.flex-container { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; -} - -input { - width: 100%; - border: 1px solid #e1e4e8; +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; - 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; @@ -62,12 +50,11 @@ input { } input:focus { - border-color: #0366d6; + border-color: #28a745; outline: none; - box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.3); + box-shadow: 0 0 0 3px #28a745; } - #cost-items tr:hover, .highlighted { background-color: #ffb936; color : black; @@ -87,13 +74,17 @@ input:focus { padding: 10px; } -.cost-values-form { +.floating-form { + min-width: 40%; + height: auto; position: absolute; - background-color: white; + 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 { @@ -120,18 +111,20 @@ input:focus { } .action-button { - background-color: #28a745; /* GitHub green color */ + background-color: #28a745; border: none; - color: #fff; /* White text color */ - padding: 0.5em 1em; /* Adjust padding as needed */ + color: #fff; + padding: 0.5em 1em; 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 */ + 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; } .action-button:hover { - background-color: #218838; /* Darker green color on hover */ + background-color: #218838; } 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 035fd68f0b..6c9b30e089 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/gantt.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/gantt.css @@ -1,288 +1,166 @@ @import url("./components/card.css"); - -: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-small: 1rem; - --font-size-large: 1.2rem; - --logo-height: 1.5rem; - --nav-height: 0.625rem; - --border-radius: 0.3rem; - --folder-collapse-font-size: 0.75rem; - --folder-collapse-font-family: Courier, "Courier New", monospace; - --box-shadow: 0 0 0.7rem #5f5f5f66; -} - -:root.dark { - color-scheme: dark; - --bg-color: #252525; - --primary-text-color: #e0e0e0; - --secondary-text-color: #c7c7c7; - --nav-bg-color: #121212; - --nav-border-color: #25682a; - --nav-link-color: #fff; - --nav-link-hover-color: #3fb449; - --warning-color: #FFDB8F; - --border-color: #464444; - --hover-bg-color: #3a3a3a; - --highlight-color: #009136; - --task-complete-bg-color: #777777; - --milestone-border-color: #ffffff; - --group-item-bg-color: #4b4b4b; - --input-bg-color: #3b3b3b; - --input-border-color: #000; - --border-color: #464444; -} - -:root.light { - color-scheme: light; - --bg-color: #ffffff; - --primary-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(td, tr, td, .gmain) { - 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(--primary-text-color)); - margin: 0; - font-family: var(--font-family); - display: flex; - flex-direction: column; - min-height: 100vh; -} - -#container { - flex: 1; - padding: var(--margin-small); -} - -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)); -} +@import url("./shared.css"); .table-description { - margin-bottom: var(--margin-medium); - width: 50%; - background-color: transparent; - border-collapse: collapse; - border-spacing: 0; - border-color: var(--blender-tab-outline, var(--border-color)); - border: 1px solid var(--blender-tab-outline, var(--border-color)); - border-radius: 0.25rem; - box-shadow: var(--box-shadow); - text-align: center; + margin-bottom: var(--margin-medium); + width: 50%; + background-color: transparent; + border-collapse: collapse; + border-spacing: 0; + border-color: var(--blender-tab-outline, var(--border-color)); + border: 1px solid var(--blender-tab-outline, var(--border-color)); + border-radius: 0.25rem; + box-shadow: var(--box-shadow); + text-align: center; } .table-description tr { - display: table-row; - border-bottom: 1px solid var(--blender-tab-outline, var(--border-color)); + display: table-row; + border-bottom: 1px solid var(--blender-tab-outline, var(--border-color)); } .table-description tr:nth-child(even) { - background-color: var(--blender-even-row); + background-color: var(--blender-even-row); } .table-description tr:nth-child(odd) { - background-color: var(--blender-odd-row); + background-color: var(--blender-odd-row); } #toggle-theme { - border: none; - background: none; - cursor: pointer; - font-size: var(--font-size-large); - margin-right: var(--margin-medium); + border: none; + background: none; + cursor: pointer; + font-size: var(--font-size-large); + margin-right: var(--margin-medium); } #toggle-theme:focus { - outline: none; + 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; + 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; + opacity: 1; + visibility: visible; } #connected-list-div { - display: inline-block; + display: inline-block; } .client { - padding: var(--margin-small); - border-bottom: 1px solid var(--blender-tab-outline, var(--border-color)); - cursor: pointer; + 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; + 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; + max-height: none; + opacity: 1; } .client-detail { - border-bottom: 1px solid var(--blender-tab-outline, var(--border-color)); - padding: var(--padding-small); + 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); + width: auto; + background-color: var(--blender-panel-header, var(--bg-color)); + font-size: var(--base-font-size); } #show-connected-button:hover, .scroll-button:hover { - cursor: pointer; + cursor: pointer; } .scroll-button { - font-size: var(--base-font-size); - margin-left: var(--margin-tiny); - margin-top: var(--margin-small); + 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)); + 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; + margin: 0; + color: var(--blender-text, var(--nav-link-color)); + margin-right: var(--margin-small); + font-size: 0.8rem; } .gantt-info { - margin: 0.5rem; - font-size: var(--font-size-small); - font-weight: bold; + margin: 0.5rem; + font-size: var(--font-size-small); + font-weight: bold; } .btn { - margin: 0.5rem; - font-size: var(--font-size-small); - cursor: pointer; + margin: 0.5rem; + font-size: var(--font-size-small); + cursor: pointer; } @media print { - .no-print { - display: none; - } + .no-print { + display: none; + } } - 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; + 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); + filter: brightness(1.2); } /* ------------ Overwriting JSGantt CSS rules ------------ */ /* ------------------- for dark theme ------------------- */ :root.dark div.gantt { - background-color: var(--bg-color); - color: var(--secondary-text-color); + background-color: var(--bg-color); + color: var(--secondary-text-color); } :root.dark .gantt table, :root.dark .gantt td { - border-color: var(--border-color); + border-color: var(--border-color); } /* Headings and cell defaults */ @@ -295,8 +173,8 @@ button:hover { :root.dark .gname, :root.dark .ggroupitem, :root.dark .gtaskheading { - background-color: var(--bg-color); - border-color: var(--border-color); + background-color: var(--bg-color); + border-color: var(--border-color); } :root.dark .gtaskheading, @@ -312,129 +190,128 @@ button:hover { :root.dark .gchartlbl, :root.dark .gcontainercol, :root.dark .genddate { - color: var(--secondary-text-color); - border-color: var(--border-color); + color: var(--secondary-text-color); + border-color: var(--border-color); } -:root.dark .gtaskname>div { - color: var(--secondary-text-color); +:root.dark .gtaskname > div { + color: var(--secondary-text-color); } :root.dark .gtaskbarcontainer.gplan, :root.dark .gchartlbl.gcontainercol { - background: var(--bg-color); - border-color: var(--border-color); + background: var(--bg-color); + border-color: var(--border-color); } :root.dark .gtaskname div, :root.dark .gtaskheading div, :root.dark .gtaskname span { - color: var(--secondary-text-color); + color: var(--secondary-text-color); } :root.dark .gtasklist, :root.dark .gadditional { - border: var(--border-color) 1px solid; + border: var(--border-color) 1px solid; } :root.dark .gchartgrid { - background-color: var(--bg-color); + background-color: var(--bg-color); } :root.dark .glistgrid, :root.dark .glistlbl { - background-color: var(--bg-color); - border-color: var(--border-color); + background-color: var(--bg-color); + border-color: var(--border-color); } /* Dark mode scrollbar */ :root.dark .frame::-webkit-scrollbar-thumb, :root.dark .frame::-webkit-scrollbar-track { - background-color: var(--bg-color); + background-color: var(--bg-color); } /* Highlight row */ :root.dark .gitemhighlight td { - background-color: var(--highlight-color); - color: var(--nav-link-color); + background-color: var(--highlight-color); + color: var(--nav-link-color); } /* Differentiate Group, Milestone and Ordinary task items (applied to row) */ :root.dark .ggroupitem { - background-color: var(--bg-color); - font-weight: bold; - border-color: var(--border-color); + background-color: var(--bg-color); + font-weight: bold; + border-color: var(--border-color); } :root.dark .gmileitem, :root.dark .glineitem { - background-color: var(--bg-color); + background-color: var(--bg-color); } /* Task bar caption text styles */ :root.dark .gmilecaption, :root.dark .ggroupcaption, :root.dark .gcaption { - color: var(--nav-link-color); + color: var(--nav-link-color); } /* Task complete %age bar */ :root.dark .gtaskcomplete { - background-color: var(--task-complete-bg-color); + background-color: var(--task-complete-bg-color); } /* Milestones */ :root.dark .gmdtop, :root.dark .gmdbottom { - border-bottom: 5px solid var(--milestone-border-color); + border-bottom: 5px solid var(--milestone-border-color); } :root.dark .gfoldercollapse { - color: var(--secondary-text-color); - cursor: pointer; - font-weight: bold; - font-size: var(--folder-collapse-font-size); - font-family: var(--folder-collapse-font-family); + color: var(--secondary-text-color); + cursor: pointer; + font-weight: bold; + font-size: var(--folder-collapse-font-size); + font-family: var(--folder-collapse-font-family); } /* Highlight for collapsible row */ :root.dark .gname.ggroupitem { - background-color: var(--group-item-bg-color); + background-color: var(--group-item-bg-color); } /* Form label and selected highlighting */ :root.dark .gformlabel { - background-color: var(--formlabel-bg-color); - color: var(--secondary-text-color); - border: var(--formlabel-border-color) 1px solid; + background-color: var(--formlabel-bg-color); + color: var(--secondary-text-color); + border: var(--formlabel-border-color) 1px solid; } :root.dark span.gformlabel:hover { - background-color: var(--hover-bg-color); - border-color: var(--border-color); + background-color: var(--hover-bg-color); + border-color: var(--border-color); } :root.dark span.gselected { - background-color: var(--formlabel-selected-bg-color); - border-color: var(--task-complete-bg-color); - color: var(--nav-link-color); + background-color: var(--formlabel-selected-bg-color); + border-color: var(--task-complete-bg-color); + color: var(--nav-link-color); } :root.dark .gantt-inputtable { - background-color: var(--input-bg-color); - box-sizing: border-box; - border: 1px solid var(--input-border-color); - color: var(--secondary-text-color); + background-color: var(--input-bg-color); + box-sizing: border-box; + border: 1px solid var(--input-border-color); + color: var(--secondary-text-color); } /* ------------------- for blender generated theme ------------------- */ - :root.blender div.gantt, :root.blender .gantt table, :root.blender .gantt td { - color: var(--blender-text); - border-color: var(--blender-tab-outline); + color: var(--blender-text); + border-color: var(--blender-tab-outline); } :root.blender .gchartgrid, @@ -443,11 +320,10 @@ button:hover { :root.blender .gchartlbl.gcontainercol, :root.blender .gminorheading, :root.blender .gtaskcell { - background-color: var(--blender-odd-row); - border-color: var(--blender-tab-outline); + background-color: var(--blender-odd-row); + border-color: var(--blender-tab-outline); } - /* Headings and cell defaults */ :root.blender .gtaskheading, :root.blender .gchartlbl.gcontainercol .gmajorheading, @@ -460,13 +336,13 @@ button:hover { :root.blender .gchartlbl.gcontainercol .ggroupitem, :root.blender .glistlbl.gcontainercol .gtaskname, :root.blender .glistlbl.gcontainercol .gtasklist { - background-color: var(--blender-panel-header); - border-color: var(--blender-tab-outline); + background-color: var(--blender-panel-header); + border-color: var(--blender-tab-outline); } :root.blender .gtaskcellcurrent { - background-color: var(--blender-odd-row); - border-color: var(--blender-tab-outline); + background-color: var(--blender-odd-row); + border-color: var(--blender-tab-outline); } :root.blender .gtaskheading, @@ -482,103 +358,102 @@ button:hover { :root.blender .gchartlbl, :root.blender .gcontainercol, :root.blender .genddate { - color: var(--blender-text); - border-color: var(--blender-tab-outline); + color: var(--blender-text); + border-color: var(--blender-tab-outline); } -:root.blender .gtaskname>div, +:root.blender .gtaskname > div, :root.blender .gtaskname div, :root.blender .gtaskheading div, :root.blender .gtaskname span { - color: var(--blender-text); + color: var(--blender-text); } :root.blender .gantt-chart tr:not(.gname.glineitem):not(.gname.ggroupitem) { - background-color: var(--blender-odd-row); + background-color: var(--blender-odd-row); } :root.blender .gtasklist, :root.blender .gadditional { - border: var(--blender-tab-outline) 1px solid; + border: var(--blender-tab-outline) 1px solid; } :root.blender .glistgrid, :root.blender .glistlbl { - /* This is a border that needs background color, somehow. */ - background-color: var(--blender-tab-outline); - border-color: var(--blender-tab-outline); + /* This is a border that needs background color, somehow. */ + background-color: var(--blender-tab-outline); + border-color: var(--blender-tab-outline); } - /* Highlight row */ :root.blender .gitemhighlight td, :root.blender .gitemhighlight .gantt-inputtable, :root.blender .gitemhighlight .gcaption, :root.blender .gitemhighlight .ggroupcaption { - background-color: var(--blender-active-highlight) !important; - color: var(--blender-active-text-highlight) !important; + background-color: var(--blender-active-highlight) !important; + color: var(--blender-active-text-highlight) !important; } /* Differentiate Group, Milestone and Ordinary task items (applied to row) */ :root.blender .ggroupitem { - font-weight: bold; - border-color: var(--border-color); + font-weight: bold; + border-color: var(--border-color); } :root.blender .gmileitem, :root.blender .glineitem { - background-color: var(--blender-odd-row); + background-color: var(--blender-odd-row); } /* Task bar caption text styles */ :root.blender .gmilecaption, :root.blender .ggroupcaption, :root.blender .gcaption { - color: var(--blender-text); + color: var(--blender-text); } /* Milestones */ :root.blender .gmdtop, :root.blender .gmdbottom { - border-bottom: 5px solid var(--blender-tab-outline); + border-bottom: 5px solid var(--blender-tab-outline); } :root.blender .gfoldercollapse { - color: var(--blender-text); - cursor: pointer; - font-weight: bold; - font-size: var(--folder-collapse-font-size); - font-family: var(--folder-collapse-font-family); + color: var(--blender-text); + cursor: pointer; + font-weight: bold; + font-size: var(--folder-collapse-font-size); + font-family: var(--folder-collapse-font-family); } /* Highlight for collapsible row */ :root.blender .gname.ggroupitem { - background-color: var(--blender-panel-background); + background-color: var(--blender-panel-background); } /* Form label and selected highlighting */ :root.blender .gformlabel { - background-color: var(--blender-panel-header); - color: var(--blender-text); - border: var(--blender-tab-outline) 1px solid; + background-color: var(--blender-panel-header); + color: var(--blender-text); + border: var(--blender-tab-outline) 1px solid; } :root.blender span.gformlabel:hover, :root.blender span.gselected { - background-color: var(--blender-active-highlight); - color: var(--blender-active-text-highlight); - border-color: var(--blender-tab-outline); + background-color: var(--blender-active-highlight); + color: var(--blender-active-text-highlight); + border-color: var(--blender-tab-outline); } :root.blender .gantt-inputtable { - background-color: var(--blender-panel-header); - box-sizing: border-box; - border: 2px solid var(--blender-button-border); - color: var(--blender-text); + background-color: var(--blender-panel-header); + box-sizing: border-box; + border: 2px solid var(--blender-button-border); + color: var(--blender-text); } /* Dark mode scrollbar */ :root.blender .frame::-webkit-scrollbar-thumb, :root.blender .frame::-webkit-scrollbar-track { - background-color: var(--blender-window-background); -} \ No newline at end of file + background-color: var(--blender-window-background); +} diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/shared.css b/src/bonsai/bonsai/bim/data/webui/static/css/shared.css index 32a5f0477b..2f0837b90f 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/shared.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/shared.css @@ -1,214 +1,239 @@ :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; + --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-small: 1rem; + --font-size-large: 1.2rem; + --logo-height: 1.5rem; + --nav-height: 0.625rem; + --border-radius: 0.3rem; + --folder-collapse-font-size: 0.75rem; + --folder-collapse-font-family: Courier, "Courier New", monospace; + --box-shadow: 0 0 0.7rem #5f5f5f66; } :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; + color-scheme: dark; + --bg-color: #252525; + --primary-text-color: #e0e0e0; + --secondary-text-color: #c7c7c7; + --nav-bg-color: #121212; + --nav-border-color: #25682a; + --nav-link-color: #fff; + --nav-link-hover-color: #3fb449; + --warning-color: #ffdb8f; + --border-color: #464444; + --hover-bg-color: #3a3a3a; + --highlight-color: #009136; + --task-complete-bg-color: #777777; + --milestone-border-color: #ffffff; + --group-item-bg-color: #4b4b4b; + --input-bg-color: #3b3b3b; + --input-border-color: #000; + --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; + color-scheme: light; + --bg-color: #ffffff; + --primary-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; +*:not(td, tr, td, .gmain) { + border-radius: var(--border-radius) !important; } html { - font-size: var(--base-font-size); + 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; + background-color: var(--blender-window-background, var(--bg-color)); + color: var(--blender-text, var(--primary-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; + flex: 1; + padding: var(--margin-small); } h3 { - margin-top: 0; + 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)); + 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); + margin-left: var(--margin-large); + height: var(--logo-height); } nav ul { - list-style-type: none; - margin: 0; - display: flex; - flex: 1; - justify-content: center; + list-style-type: none; + margin: 0; + display: flex; + flex: 1; + justify-content: center; } nav ul li { - margin-right: var(--margin-large); + margin-right: var(--margin-large); } nav ul li a { - text-decoration: none; - color: var(--blender-text, var(--nav-link-color)); + 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)); + 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); + color: var(--blender-info-warning, var(--warning-color)); } #toggle-theme:focus { - outline: none; + 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; + 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; + opacity: 1; + visibility: visible; } #connected-list-div { - display: inline-block; + display: inline-block; } .client { - padding: var(--margin-small); - border-bottom: 1px solid var(--blender-tab-outline, var(--border-color)); - cursor: pointer; + 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; + 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; + max-height: none; + opacity: 1; } .client-detail { - border-bottom: 1px solid var(--blender-tab-outline, var(--border-color)); - padding: var(--padding-small); + 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)) + 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; + cursor: pointer; } .scroll-button { - font-size: var(--base-font-size); - margin-left: var(--margin-tiny); - margin-top: var(--margin-small); + 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)); + 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; + 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; + 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 + filter: brightness(1.2); +} + +.row-container { + width: 100%; + border-collapse: collapse; + display: flex; + justify-content: flex-start; + flex-wrap: wrap; +} + +.column-container { + display: flex; + flex-direction: column; + justify-content: flex-start; + flex-wrap: wrap; +} + +label { + margin-right: var(--margin-small); + margin-bottom: var(--margin-small); +} 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 26809f90a0..2b78e031c3 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/js/cost.js +++ b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js @@ -1,8 +1,10 @@ -import { CostUI } from './utilities/costui.js'; +import { CostUI } from "./utilities/costui.js"; const connectedClients = {}; let socket; +let isScheduleLoad = {}; + $(document).ready(function () { var defaultTheme = "blender"; var theme = localStorage.getItem("theme") || defaultTheme; @@ -21,12 +23,28 @@ function connectSocket() { socket.on("connected_clients", handleConnectedClients); socket.on("theme_data", handleThemeData); socket.on("connect", handleWebConnect); + socket.on("predefined_types", handlePredefinedTypes); socket.on("cost_schedules", handleCostSchedulesData); socket.on("cost_items", handleCostItemsData); socket.on("cost_values", handleCostValuesData); socket.on("cost_value", handleCostValueData); } +function handlePredefinedTypes(data) { + CostUI.enableAddingCostSchedule( + data.data["predefined_types"], + addCostSchedule + ); +} + +function addCostSchedule(name, predefinedType) { + executeOperator({ + type: "addCostSchedule", + name: name, + predefinedType: predefinedType, + }); +} + function handleBlenderConnect(blenderId) { if (!connectedClients.hasOwnProperty(blenderId)) { connectedClients[blenderId] = { shown: false, ifc_file: "" }; @@ -35,7 +53,6 @@ function handleBlenderConnect(blenderId) { $("#blender-count").text(function (i, text) { return parseInt(text, 10) + 1; }); - console.log("Blender connected", blenderId); } function handleBlenderDisconnect(blenderId) { @@ -53,24 +70,24 @@ function removeTableElement(blenderId) { $("#cost-items-" + blenderId).remove(); } - const costValueCallbacks = { - 'editCostValues': editCostValues, - 'addCostValue': addCostValue, - 'deleteCostValue': deleteCostValue, + 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, costValueCallbacks); - } function deleteCostValue(costItemId, costValueId) { - executeOperator({ type: "deleteCostValue", costItemId: costItemId, costValueId: costValueId }); + executeOperator({ + type: "deleteCostValue", + costItemId: costItemId, + costValueId: costValueId, + }); } function handleConnectedClients(data) { @@ -86,14 +103,13 @@ function handleCostValuesData(data) { costValues: data.data["cost_values"]["cost_values"], costItemId: data.data["cost_values"]["cost_item_id"], callbacks: { - 'editCostValues': editCostValues, - 'addCostValue': addCostValue, - 'deleteCostValue': deleteCostValue, + editCostValues: editCostValues, + addCostValue: addCostValue, + deleteCostValue: deleteCostValue, }, }); CostUI.highlightCostItem(data.data["cost_values"]["cost_item_id"]); - } function addCostValue(costItemId) { @@ -101,10 +117,10 @@ function addCostValue(costItemId) { } function editCostValues(costItemId, costValues) { - executeOperator({ - type: "editCostValues", - costItemId: costItemId, - costValues: costValues + executeOperator({ + type: "editCostValues", + costItemId: costItemId, + costValues: costValues, }); } @@ -159,7 +175,11 @@ function addCostItem(costItemId) { } function editCostItemName(costItemId, name) { - executeOperator({ type: "editCostItemName", costItemId: costItemId, name: name }); + executeOperator({ + type: "editCostItemName", + costItemId: costItemId, + name: name, + }); } function selectAssignedElements(costItemId) { @@ -167,44 +187,52 @@ function selectAssignedElements(costItemId) { } function handleWebConnect() { + executeOperator({ type: "getPredefinedTypes" }); getCostSchedules(); } +function addSummaryCostItem(costScheduleId) { + executeOperator({ + type: "addSummaryCostItem", + costScheduleId: costScheduleId, + }); +} + function handleCostSchedulesData(data) { const blenderId = data.blenderId; const costSchedules = data.data["cost_schedules"]["cost_schedules"]; - const currency = data.data["cost_schedules"]["currency"]["name"]; - const costScheduleDiv = $("#cost-schedules"); + if (costSchedules.length === 0) { + return; + } + const currency = data.data["cost_schedules"]["currency"] + ? data.data["cost_schedules"]["currency"]["name"] + : "Undefined"; + const costScheduleDiv = document.getElementById("cost-schedules"); + costScheduleDiv.innerHTML = ""; costSchedules.forEach((costSchedule) => { costSchedule.UpdateDate = new Date(costSchedule.UpdateDate); const mainContainer = CostUI.text("Updated On: " + costSchedule.UpdateDate); const callback = () => loadCostSchedule(costSchedule.id, blenderId); - 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"], + data: data.data["cost_items"]["cost_items"], + costScheduleId: data.data["cost_items"]["cost_schedule_id"], blenderID: data.blenderId, callbacks: { - "addCostItem": addCostItem, - "deleteCostItem": deleteCostItem, - "duplicateCostItem": duplicateCostItem, - "selectAssignedElements": selectAssignedElements, - 'editCostItemName': editCostItemName, - 'enableEditingCostValues': enableEditingCostValues, - } + addCostItem: addCostItem, + deleteCostItem: deleteCostItem, + duplicateCostItem: duplicateCostItem, + selectAssignedElements: selectAssignedElements, + editCostItemName: editCostItemName, + enableEditingCostValues: enableEditingCostValues, + addSummaryCostItem: addSummaryCostItem, + }, }); } @@ -224,7 +252,10 @@ function executeOperator(operator, blenderId) { } function loadCostSchedule(costScheduleId, blenderId) { - executeOperator({ type: "loadCostSchedule", costScheduleId: costScheduleId }, blenderId); + executeOperator( + { type: "loadCostSchedule", costScheduleId: costScheduleId }, + blenderId + ); } function getCostSchedules(blenderId) { @@ -232,5 +263,5 @@ function getCostSchedules(blenderId) { } function enableEditingCostValues(costItemId) { - executeOperator({ type: "enableEditingCostValues", costItemId: costItemId}); + executeOperator({ type: "enableEditingCostValues", costItemId: costItemId }); } 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 f2cb5e5e4b..d42c4032e1 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 @@ -1,55 +1,62 @@ export class CostUI { - constructor() {} + constructor() {} - static isCostScheduleLoaded(id) { - const existingTable = document.getElementById('cost-items-' + id); - return existingTable !== null; + static isCostScheduleLoaded(id) { + const existingTable = document.getElementById("cost-items-" + id); + return existingTable !== null; + } + + static removeCostSchedule(id) { + document.getElementById("cost-items-" + id).remove(); + } + static createTable(id, callbacks) { + CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null; + + const table = document.createElement("table"); + table.id = "cost-items-" + id; + const tbody = document.createElement("tbody"); + tbody.setAttribute("id", "cost-items"); + + const columnHeaders = [ + "Name", + "Quantity", + "Unit", + "Cost", + "Total Cost", + "Action", + ]; + const tr = document.createElement("tr"); + + for (let i = 0; i < columnHeaders.length; i++) { + const th = document.createElement("th"); + th.textContent = columnHeaders[i]; + th.style.position = "relative"; + tr.appendChild(th); + + if (i < columnHeaders.length - 1) { + const resizer = document.createElement("div"); + resizer.classList.add("resizer"); + th.appendChild(resizer); + CostUI.addResizer(resizer); + } } - static removeCostSchedule(id) { - document.getElementById("cost-items-" + id).remove(); - } - static createTable(id, callbacks) { - CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null; + tbody.appendChild(tr); + table.appendChild(tbody); + document.getElementById("cost-items").appendChild(table); - const table = document.createElement("table"); - table.id = 'cost-items-' + id; - const tbody = document.createElement("tbody"); - tbody.setAttribute("id", "cost-items"); + CostUI.addTableStyles(id); + CostUI.createContextMenu(callbacks); + table.get_blender_id = function () { + return this.getAttribute("id").split("-")[2]; + }; - const columnHeaders = ["Name", "Quantity", "Unit", "Cost", "Total Cost", "Action"]; - const tr = document.createElement("tr"); + return [table, tbody]; + } - for (let i = 0; i < columnHeaders.length; i++) { - const th = document.createElement("th"); - th.textContent = columnHeaders[i]; - th.style.position = "relative"; - tr.appendChild(th); - - if (i < columnHeaders.length - 1) { - const resizer = document.createElement("div"); - resizer.classList.add("resizer"); - th.appendChild(resizer); - CostUI.addResizer(resizer); - } - } - - tbody.appendChild(tr); - table.appendChild(tbody); - document.getElementById("cost-items").appendChild(table); - - CostUI.addTableStyles(id); - CostUI.createContextMenu(callbacks); - table.get_blender_id = function() { - return this.getAttribute("id").split("-")[2]; - }; - - return [table, tbody]; - } - - static addTableStyles(id) { - const style = document.createElement("style"); - style.textContent = ` + static addTableStyles(id) { + const style = document.createElement("style"); + style.textContent = ` #cost-items-${id} th:nth-child(1), #cost-items-${id} td:nth-child(1) { width: auto; @@ -60,778 +67,949 @@ export class CostUI { } `; - document.head.appendChild(style); - } + document.head.appendChild(style); + } +static createContextMenu(callbacks) { + const contextMenu = document.createElement("div"); + contextMenu.id = "context-menu"; + contextMenu.classList.add("context-menu"); + contextMenu.innerHTML = ` + + + + + `; + document.body.appendChild(contextMenu); - static createContextMenu(callbacks) { - const contextMenu = document.createElement("div"); - contextMenu.id = "context-menu"; - contextMenu.classList.add("context-menu"); - contextMenu.innerHTML = ` - - - - - `; - 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`; - - contextMenu.targetRow = targetRow; - contextMenu.targetCell = targetCell; - } else { - document.getElementById("context-menu").style.display = "none"; - } - }); - - document.addEventListener("click", function(event) { + 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"); - if (!contextMenu.contains(event.target)) { - contextMenu.style.display = "none"; - } - }); + contextMenu.style.display = "block"; + contextMenu.style.left = `${event.pageX}px`; + contextMenu.style.top = `${event.pageY}px`; - const editCostValuesButton = document.getElementById("edit-cost-values-button"); - editCostValuesButton.addEventListener("click", function() { + 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 editCostValuesButton = document.getElementById( + "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 addButton = document.getElementById("add-cost-item-button"); + if (!addButton.dataset.listenerAdded) { + function addCostItemHandler(e) { + e.stopPropagation(); 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; + callbacks.addCostItem ? callbacks.addCostItem(costItemId) : null; } - }); + document.getElementById("context-menu").style.display = "none"; + } + addButton.addEventListener("click", addCostItemHandler); + addButton.dataset.listenerAdded = "true"; + } - 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"; + 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); } - addButton.addEventListener("click", addCostItemHandler); - addButton.dataset.listenerAdded = "true"; - } - - 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; - } - - 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) + document.getElementById("context-menu").style.display = "none"; }); - return expandedState - } - - static addResizer(resizer) { - let startX, startWidth, th; - - resizer.addEventListener("mousedown", function(e) { - th = e.target.parentElement; - startX = e.pageX; - startWidth = th.offsetWidth; - document.addEventListener("mousemove", resizeColumn); - document.addEventListener("mouseup", stopResize); - }); - - function resizeColumn(e) { - const newWidth = startWidth + (e.pageX - startX); - th.style.width = newWidth + "px"; - } - - function stopResize() { - document.removeEventListener("mousemove", resizeColumn); - document.removeEventListener("mouseup", stopResize); - } - } - - static generateColorScheme(baseColor) { - const levels = 10; - const colorScheme = []; - for (let i = 0; i < levels; i++) { - colorScheme.push(CostUI.lightenColor(baseColor, i * 5)); - } - return colorScheme; - } - - static lightenColor(color, percent) { - const num = parseInt(color.slice(1), 16), - amt = Math.round(2.55 * percent), - R = (num >> 16) + amt, - G = (num >> 8 & 0x00FF) + amt, - B = (num & 0x0000FF) + amt; - return `#${(0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1).toUpperCase()}`; - } - - static applyColorScheme(tableId, colorScheme) { - const rows = document.querySelectorAll(`#${tableId} tbody tr`); - rows.forEach((row, index) => { - const level = index % colorScheme.length; - row.style.backgroundColor = colorScheme[level]; - }); - } - - - static createColorPicker() { - const colorPicker = document.createElement("input"); - colorPicker.type = "color"; - colorPicker.id = "color-picker"; - colorPicker.value = "#ff0000"; - const colorText = document.createElement("p"); - colorText.textContent = "Select a color to change row color:"; - document.getElementById("UI").appendChild(colorText); - document.getElementById("UI").appendChild(colorPicker); - - colorPicker.addEventListener("input", function() { - const baseColor = colorPicker.value; - const colorScheme = CostUI.generateColorScheme(baseColor); - CostUI.applyColorScheme("cost-items", colorScheme); - }); - const colorScheme = CostUI.generateColorScheme("#000000"); - CostUI.applyColorScheme("cost-items", colorScheme); - } - - static createCostSchedule({ data, blenderID, title, callbacks = {} }) { - const [table, tbody] = CostUI.createTable(blenderID, callbacks); - CostUI.createCostItem(data, tbody, 0, null, callbacks); - CostUI.applyExpandedState(); + deleteCostItem.hasListener = true; } - static createCostItem(data, container, nestingLevel = 0, parentID = null, callbacks = {}) { - data.forEach(costItem => { - const row = CostUI.createRow(costItem, nestingLevel, parentID, callbacks); - container.appendChild(row); - - if (costItem.is_nested_by && costItem.is_nested_by.length > 0) { - CostUI.createCostItem(costItem.is_nested_by, container, nestingLevel + 1, costItem.id, callbacks); + 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"; }); - } - - 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); - row.appendChild(unitSymbolCell); - row.appendChild(totalAppliedValueCell); - row.appendChild(totalCostCell); - row.appendChild(flexContainerCell); - - row.get_id = function() { - return this.getAttribute("id"); - }; - - row.get_parent = function() { - const parentId = this.getAttribute("parent-id"); - return parentId ? document.getElementById(parentId) : null; - }; - - return row; - } - - static createTableRow(costItem, nestingLevel, parentID) { - const row = document.createElement("tr"); - row.setAttribute("id", costItem.id); - row.setAttribute("parent-id", parentID); - if (nestingLevel > 0) { - 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"); - - 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(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 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 = costItem.name ? costItem.name : "Unnamed"; - - nameInput.addEventListener("change", function() { - callbacks.editCostItemName ? callbacks.editCostItemName(costItem.id, this.value) : null; - }); - - nameInput.addEventListener("keydown", function(event) { - if (event.key === "Enter") { - 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; - } - - static createTableCell(content) { - const cell = document.createElement("td"); - cell.textContent = content; - return cell; - } - - static createTotalCostCell(costItem) { - const totalCostCell = document.createElement("td"); - const totalCost = parseFloat(costItem.TotalCost).toFixed(2); - totalCostCell.textContent = costItem.is_sum ? totalCost + " (Σ)" : totalCost; - return totalCostCell; - } - - static createFlexContainerCell(costItem, callbacks) { - const divFlex = document.createElement("div"); - divFlex.classList.add("flex-container"); - - const addButton = CostUI.createAddButton(costItem, callbacks); - const selectButton = CostUI.createSelectButton(costItem, callbacks); - - divFlex.appendChild(addButton); - divFlex.appendChild(selectButton); - - const flexContainerCell = document.createElement("td"); - flexContainerCell.appendChild(divFlex); - return flexContainerCell; - } - - 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(costItem.id) : null; - }); - return addButton; - } - - static createSelectButton(costItem, callbacks) { - const selectButton = document.createElement("button"); - selectButton.classList.add("action-button"); - selectButton.textContent = "Select"; - selectButton.addEventListener("click", function(e) { - e.stopPropagation(); - callbacks.selectAssignedElements ? callbacks.selectAssignedElements(costItem.id) : null; - }); - return selectButton; - } - - static highlightCostItem(id) { - const row = document.getElementById(id); - if (row) { - row.classList.add("highlighted"); - } - } - - static unhighlightCostItem(id) { - const row = document.getElementById(id); - if (row) { - row.classList.remove("highlighted"); - } - } - - static getAllCostItemsIds() { - const rows = document.querySelectorAll("#cost-items tr"); - const ids = []; - rows.forEach(row => { - ids.push(row.id); - }); - return ids; - } - - - - static text(label) { - const text = document.createElement("p"); - text.textContent = label; - return text; - } - - static createCard(title, mainContainer, callback) { - const card = document.createElement("div"); - card.classList.add("card"); - - const cardBody = document.createElement("div"); - cardBody.classList.add("card-body"); - - const cardTitle = document.createElement("h5"); - cardTitle.classList.add("card-title"); - cardTitle.textContent = title; - - const cardButton = document.createElement("button"); - cardButton.classList.add("action-button"); - cardButton.textContent = "Load"; - cardButton.addEventListener("click", callback); - - cardBody.appendChild(cardTitle); - cardBody.appendChild(mainContainer); - cardBody.appendChild(cardButton); - card.appendChild(cardBody); - - 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 }) { - const formId = "cost-values-form-" + costItemId; - let existingForm = document.getElementById(formId); - if (existingForm) { - existingForm.remove(); - } - - const form = CostUI.createFormElement(formId, "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", ""]); - - costValues.forEach(costValue => { - const tr = CostUI.createCostvaluesRow(costItemId, costValue, callbacks); - table.appendChild(tr); - }); - - form.appendChild(table); - - const addButton = CostUI.createAddCostValueButton(costItemId, callbacks); - form.appendChild(addButton); - - const closeButton = CostUI.createCloseButton(form); - form.appendChild(closeButton); - - CostUI.makeHeaderDraggable(header, form); - - document.body.appendChild(form); - - return form; - } - - - static getCostValuesTable(costItemId) { - return document.getElementById("cost-values-table-" + costItemId); - } - - static addNewCostValueRow(costItemId, costValueId, costValueCallbacks) { - const table = CostUI.getCostValuesTable(costItemId); - 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}, costValueCallbacks); - table.appendChild(tr); - } - - static createCostValuesTable(costItemId, headers) { - const table = document.createElement("table"); - table.classList.add("cost-values-table"); - table.id = "cost-values-table-" + costItemId; - - const headerRow = document.createElement("tr"); - headers.forEach(headerText => { - const th = document.createElement("th"); - th.textContent = headerText; - headerRow.appendChild(th); - }); - table.appendChild(headerRow); - - return table; - } - - static createCostvaluesRow(costItemId,costValue, costValueCallbacks) { - function cleanLabel(value) { - 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; - tr.id = costValue.id; - - let costType = "FIXED"; - if (costValue.category === "*") { - costType = "SUM"; - } - else if (costValue.category && costValue.category !== "*") { - costType = "CATEGORY"; - } - else if (costValue.applied_value) { - costType = "FIXED"; - } - - const typeCell = CostUI.createTableDropdown("type", costType); - const dropdown = typeCell.querySelector("select"); - 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); - - const categoryCell = CostUI.createTableInput("text", "category", costValue.category); - tr.appendChild(categoryCell); - - let value - - if (costValue.category === "*"){ - value = cleanLabel(costValue.label) - } - else { - value = costValue.applied_value - } - const valueCell1 = CostUI.createTableInput("number", "value", value); - tr.appendChild(valueCell1); - - 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; - } - - static updateRowBasedOnType(type, categoryCell, valueCell1) { - if (type === "FIXED") { - categoryCell.querySelector("input").disabled = true; - categoryCell.querySelector("input").style.backgroundColor = "lightgrey"; - valueCell1.querySelector("input").disabled = false; - valueCell1.querySelector("input").style.backgroundColor = ""; - } else if (type === "CATEGORY") { - categoryCell.querySelector("input").disabled = false; - categoryCell.querySelector("input").style.backgroundColor = ""; - valueCell1.querySelector("input").disabled = false; - valueCell1.querySelector("input").style.backgroundColor = ""; - } else if (type === "SUM") { - categoryCell.querySelector("input").disabled = true; - categoryCell.querySelector("input").style.backgroundColor = "lightgrey"; - valueCell1.querySelector("input").disabled = true; - valueCell1.querySelector("input").style.backgroundColor = "lightgrey"; - } - } - - static createTableDropdown(name, value="FIXED") { - const cell = document.createElement("td"); - const dropdown = document.createElement("select"); - dropdown.name = name; - - const options = ["FIXED", "CATEGORY", "SUM"]; - options.forEach(optionValue => { - const option = document.createElement("option"); - option.value = optionValue; - option.textContent = optionValue; - if (optionValue === value) { - option.selected = true; - } - dropdown.appendChild(option); - }); - - cell.appendChild(dropdown); - return cell; - } - - static createTableInput(type, name, value) { - const cell = document.createElement("td"); - const input = document.createElement("input"); - input.type = type; - input.name = name; - input.value = value; - cell.appendChild(input); - return cell; - } - - static createAddCostValueButton(costItemId, callbacks) { - const addButton = document.createElement("button"); - addButton.classList.add("action-button"); - addButton.textContent = " + Add Cost Value"; - addButton.addEventListener("click", function(e) { - e.preventDefault(); - callbacks.addCostValue ? callbacks.addCostValue(costItemId) : null; - }); - return addButton; - } - - static createFormElement(id, 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%)"; - return form; - } - - static createHeader(text) { - const header = document.createElement("div"); - header.classList.add("form-header"); - header.textContent = text; - return header; - } - - 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); - }); - - return closeButton; - } - - static makeHeaderDraggable(header, form) { - header.addEventListener("mousedown", function(e) { - let offsetX = e.clientX - form.getBoundingClientRect().left; - let offsetY = e.clientY - form.getBoundingClientRect().top; - - function mouseMoveHandler(e) { - form.style.left = `${e.clientX - offsetX}px`; - form.style.top = `${e.clientY - offsetY}px`; - } - - function mouseUpHandler() { - document.removeEventListener("mousemove", mouseMoveHandler); - document.removeEventListener("mouseup", mouseUpHandler); - } - - document.addEventListener("mousemove", mouseMoveHandler); - document.addEventListener("mouseup", mouseUpHandler); - }); + 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) { + let startX, startWidth, th; + + resizer.addEventListener("mousedown", function (e) { + th = e.target.parentElement; + startX = e.pageX; + startWidth = th.offsetWidth; + document.addEventListener("mousemove", resizeColumn); + document.addEventListener("mouseup", stopResize); + }); + + function resizeColumn(e) { + const newWidth = startWidth + (e.pageX - startX); + th.style.width = newWidth + "px"; + } + + function stopResize() { + document.removeEventListener("mousemove", resizeColumn); + document.removeEventListener("mouseup", stopResize); + } + } + + static generateColorScheme(baseColor) { + const levels = 10; + const colorScheme = []; + for (let i = 0; i < levels; i++) { + colorScheme.push(CostUI.lightenColor(baseColor, i * 5)); + } + return colorScheme; + } + + static lightenColor(color, percent) { + const num = parseInt(color.slice(1), 16), + amt = Math.round(2.55 * percent), + R = (num >> 16) + amt, + G = ((num >> 8) & 0x00ff) + amt, + B = (num & 0x0000ff) + amt; + return `#${( + 0x1000000 + + (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 + + (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 + + (B < 255 ? (B < 1 ? 0 : B) : 255) + ) + .toString(16) + .slice(1) + .toUpperCase()}`; + } + + static applyColorScheme(tableId, colorScheme) { + const rows = document.querySelectorAll(`#${tableId} tbody tr`); + rows.forEach((row, index) => { + const level = index % colorScheme.length; + row.style.backgroundColor = colorScheme[level]; + }); + } + + static enableAddingCostSchedule(types, addCostSchedule) { + const addNewSchedule = document.getElementById("add-cost-schedule"); + + addNewSchedule.addEventListener("click", function () { + const name = "Add New Cost Schedule"; + + const form = CostUI.Form({ + id: "add-cost-schedule-form", + name: name, + className: "floating-form", + }); + + const nameLabel = document.createElement("label"); + nameLabel.textContent = "Name:"; + form.appendChild(nameLabel); + form.classList.add("column-container"); + + const nameInput = document.createElement("input"); + nameInput.type = "text"; + nameInput.name = "name"; + form.appendChild(nameInput); + + const typeLabel = document.createElement("label"); + typeLabel.textContent = "Type:"; + form.appendChild(typeLabel); + + const typeSelect = document.createElement("select"); + typeSelect.name = "Predefined Type"; + + console.log(types); + types.forEach((type) => { + const option = document.createElement("option"); + option.value = type.name; + option.textContent = type.name; + option.title = type.description; + typeSelect.appendChild(option); + }); + form.appendChild(typeSelect); + + const submitButton = document.createElement("button"); + submitButton.type = "submit"; + submitButton.textContent = "Add"; + submitButton.classList.add("action-button"); + form.appendChild(submitButton); + + document.body.appendChild(form); + + form.addEventListener("submit", function (event) { + event.preventDefault(); + const formData = new FormData(form); + const data = {}; + formData.forEach((value, key) => { + data[key] = value; + }); + console.log("Data:", data); + addCostSchedule(data.name, data["Predefined Type"]); + form.remove(); + }); + }); + } + + static createColorPicker() { + const div = document.createElement("div"); + div.classList.add("card"); + const colorPicker = document.createElement("input"); + colorPicker.type = "color"; + colorPicker.id = "color-picker"; + colorPicker.value = "#ff0000"; + const colorText = document.createElement("p"); + 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; + const colorScheme = CostUI.generateColorScheme(baseColor); + CostUI.applyColorScheme("cost-items", colorScheme); + }); + const colorScheme = CostUI.generateColorScheme("#000000"); + CostUI.applyColorScheme("cost-items", colorScheme); + } + + static createCostSchedule({ + data, + costScheduleId, + blenderID, + callbacks = {}, + }) { + const [table, tbody] = CostUI.createTable(blenderID, callbacks); + if (data.length === 0) { + const tr = document.createElement("tr"); + const td = document.createElement("td"); + td.colSpan = 6; + td.textContent = "No cost items found"; + tr.appendChild(td); + tbody.appendChild(tr); + + const addSummaryCostItemButton = document.createElement("button"); + addSummaryCostItemButton.textContent = "Add Summary Cost Item"; + addSummaryCostItemButton.addEventListener("click", function () { + callbacks.addSummaryCostItem + ? callbacks.addSummaryCostItem(costScheduleId) + : null; + }); + td.appendChild(addSummaryCostItemButton); + addSummaryCostItemButton.classList.add("action-button"); + } else { + CostUI.createCostItem(data, tbody, 0, null, callbacks); + CostUI.applyExpandedState(); + } + } + + static createCostItem( + data, + container, + nestingLevel = 0, + parentID = null, + callbacks = {} + ) { + data.forEach((costItem) => { + const row = CostUI.createRow(costItem, nestingLevel, parentID, callbacks); + container.appendChild(row); + + 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(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); + row.appendChild(unitSymbolCell); + row.appendChild(totalAppliedValueCell); + row.appendChild(totalCostCell); + row.appendChild(flexContainerCell); + + row.get_id = function () { + return this.getAttribute("id"); + }; + + row.get_parent = function () { + const parentId = this.getAttribute("parent-id"); + return parentId ? document.getElementById(parentId) : null; + }; + + return row; + } + + static createTableRow(costItem, nestingLevel, parentID) { + const row = document.createElement("tr"); + row.setAttribute("id", costItem.id); + row.setAttribute("parent-id", parentID); + if (nestingLevel > 0) { + 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"); + + 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 (event) { + event.stopPropagation(); + let isExpanded = CostUI.isRowExpanded(row.id); + console.log("Row is expanded", isExpanded); + if (event.shiftKey) { + if (isExpanded) { + CostUI.contractRow(row.id, true); + CostUI.updateExpandedState(row.id, false, true); + } else { + CostUI.expandRow(row.id, true); + CostUI.updateExpandedState(row.id, true, true); + } + } else { + if (isExpanded) { + CostUI.contractRow(row.id, true); + CostUI.updateExpandedState(row.id, false); + } else { + CostUI.expandRow(row.id); + CostUI.updateExpandedState(row.id, true); + } + } + }); + + return expandButton; + } + + 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); + } + }); + } + + 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; + }, {}); + + 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 = costItem.name ? costItem.name : "Unnamed"; + + nameInput.addEventListener("change", function () { + callbacks.editCostItemName + ? callbacks.editCostItemName(costItem.id, this.value) + : null; + }); + + nameInput.addEventListener("keydown", function (event) { + if (event.key === "Enter") { + 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; + } + + static createTableCell(content) { + const cell = document.createElement("td"); + cell.textContent = content; + return cell; + } + + static createTotalCostCell(costItem) { + const totalCostCell = document.createElement("td"); + const totalCost = parseFloat(costItem.TotalCost).toFixed(2); + totalCostCell.textContent = costItem.is_sum + ? totalCost + " (Σ)" + : totalCost; + return totalCostCell; + } + + static createFlexContainerCell(costItem, callbacks) { + const divFlex = document.createElement("div"); + divFlex.classList.add("row-container"); + const selectButton = CostUI.createSelectButton(costItem, callbacks); + divFlex.appendChild(selectButton); + + const flexContainerCell = document.createElement("td"); + flexContainerCell.appendChild(divFlex); + return flexContainerCell; + } + + static createSelectButton(costItem, callbacks) { + const selectButton = document.createElement("button"); + selectButton.classList.add("action-button"); + selectButton.textContent = "Select"; + selectButton.addEventListener("click", function (e) { + e.stopPropagation(); + callbacks.selectAssignedElements + ? callbacks.selectAssignedElements(costItem.id) + : null; + }); + return selectButton; + } + + static highlightCostItem(id) { + const row = document.getElementById(id); + if (row) { + row.classList.add("highlighted"); + } + } + + static unhighlightCostItem(id) { + const row = document.getElementById(id); + if (row) { + row.classList.remove("highlighted"); + } + } + + static getAllCostItemsIds() { + const rows = document.querySelectorAll("#cost-items tr"); + const ids = []; + rows.forEach((row) => { + ids.push(row.id); + }); + return ids; + } + + static text(label) { + const text = document.createElement("p"); + text.textContent = label; + return text; + } + + static createCard(title, mainContainer, callback) { + const card = document.createElement("div"); + card.classList.add("card"); + + const cardBody = document.createElement("div"); + cardBody.classList.add("card-body"); + + const cardTitle = document.createElement("h5"); + cardTitle.classList.add("card-title"); + cardTitle.textContent = title; + + const cardButton = document.createElement("button"); + cardButton.classList.add("action-button"); + cardButton.textContent = "Load"; + cardButton.addEventListener("click", callback); + + cardBody.appendChild(cardTitle); + cardBody.appendChild(mainContainer); + cardBody.appendChild(cardButton); + card.appendChild(cardBody); + + 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 }) { + const formId = "cost-values-form-" + costItemId; + let existingForm = document.getElementById(formId); + if (existingForm) { + existingForm.remove(); + } + const costItemName = CostUI.getCostItemName(costItemId); + + let formName = "Cost Values: " + costItemName; + const form = CostUI.Form({ + id: formId, + name: formName, + className: "floating-form", + }); + + const table = CostUI.createCostValuesTable(costItemId, [ + "Type", + "Category", + "Value", + "", + ]); + + costValues.forEach((costValue) => { + const tr = CostUI.createCostvaluesRow(costItemId, costValue, callbacks); + table.appendChild(tr); + }); + + form.appendChild(table); + + const addButton = CostUI.createAddCostValueButton(costItemId, callbacks); + form.appendChild(addButton); + + document.body.appendChild(form); + + return form; + } + + static getCostValuesTable(costItemId) { + return document.getElementById("cost-values-table-" + costItemId); + } + + static addNewCostValueRow(costItemId, costValueId, costValueCallbacks) { + const table = CostUI.getCostValuesTable(costItemId); + if (!table) { + console.log("Cost values table not found for cost item ID:", costItemId); + return; + } + const tr = CostUI.createCostvaluesRow( + costItemId, + { + category: "", + name: "", + applied_value: 0, + id: costValueId, + parent: costItemId, + }, + costValueCallbacks + ); + table.appendChild(tr); + } + + static createCostValuesTable(costItemId, headers) { + const table = document.createElement("table"); + table.classList.add("cost-values-table"); + table.id = "cost-values-table-" + costItemId; + + const headerRow = document.createElement("tr"); + headers.forEach((headerText) => { + const th = document.createElement("th"); + th.textContent = headerText; + headerRow.appendChild(th); + }); + table.appendChild(headerRow); + + return table; + } + + static createCostvaluesRow(costItemId, costValue, costValueCallbacks) { + function cleanLabel(value) { + 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; + tr.id = costValue.id; + + let costType = "FIXED"; + if (costValue.category === "*") { + costType = "SUM"; + } else if (costValue.category && costValue.category !== "*") { + costType = "CATEGORY"; + } else if (costValue.applied_value) { + costType = "FIXED"; + } + + const typeCell = CostUI.createTableDropdown("type", costType); + const dropdown = typeCell.querySelector("select"); + 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); + + const categoryCell = CostUI.createTableInput( + "text", + "category", + costValue.category + ); + tr.appendChild(categoryCell); + + let value; + + if (costValue.category === "*") { + value = cleanLabel(costValue.label); + } else { + value = costValue.applied_value; + } + const valueCell1 = CostUI.createTableInput("number", "value", value); + tr.appendChild(valueCell1); + + 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 deleteCostValue = document.createElement("button"); + deleteCostValue.classList.add("action-button"); + deleteCostValue.textContent = "Delete"; + deleteCostValue.addEventListener("click", function () { + costValueCallbacks.deleteCostValue(costItemId, costValue.id); + tr.remove(); + }); + deleteCell.appendChild(deleteCostValue); + CostUI.updateRowBasedOnType(costType, categoryCell, valueCell1); + return tr; + } + + static updateRowBasedOnType(type, categoryCell, valueCell1) { + if (type === "FIXED") { + categoryCell.querySelector("input").disabled = true; + categoryCell.querySelector("input").style.backgroundColor = "lightgrey"; + valueCell1.querySelector("input").disabled = false; + valueCell1.querySelector("input").style.backgroundColor = ""; + } else if (type === "CATEGORY") { + categoryCell.querySelector("input").disabled = false; + categoryCell.querySelector("input").style.backgroundColor = ""; + valueCell1.querySelector("input").disabled = false; + valueCell1.querySelector("input").style.backgroundColor = ""; + } else if (type === "SUM") { + categoryCell.querySelector("input").disabled = true; + categoryCell.querySelector("input").style.backgroundColor = "lightgrey"; + valueCell1.querySelector("input").disabled = true; + valueCell1.querySelector("input").style.backgroundColor = "lightgrey"; + } + } + + static createTableDropdown(name, value = "FIXED") { + const cell = document.createElement("td"); + const dropdown = document.createElement("select"); + dropdown.name = name; + + const options = ["FIXED", "CATEGORY", "SUM"]; + options.forEach((optionValue) => { + const option = document.createElement("option"); + option.value = optionValue; + option.textContent = optionValue; + if (optionValue === value) { + option.selected = true; + } + dropdown.appendChild(option); + }); + + cell.appendChild(dropdown); + return cell; + } + + static createTableInput(type, name, value) { + const cell = document.createElement("td"); + const input = document.createElement("input"); + input.type = type; + input.name = name; + input.value = value; + cell.appendChild(input); + return cell; + } + + static createAddCostValueButton(costItemId, callbacks) { + const addButton = document.createElement("button"); + addButton.classList.add("action-button"); + addButton.textContent = " + Add Cost Value"; + addButton.addEventListener("click", function (e) { + e.preventDefault(); + callbacks.addCostValue ? callbacks.addCostValue(costItemId) : null; + }); + 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); + form.appendChild(header); + form.appendChild(closeButton); + CostUI.makeFormDraggable(form); + return form; + } + static makeFormDraggable(form) { + let pos1 = 0, + pos2 = 0, + pos3 = 0, + pos4 = 0; + form.onmousedown = function (e) { + if (!e.target.closest("input, select, button")) { + dragMouseDown(e); + } + }; + + const formElements = form.querySelectorAll("input, select, button"); + formElements.forEach((element) => { + element.onmousedown = function (e) { + e.stopImmediatePropagation(); + }; + }); + + function dragMouseDown(e) { + e = e || window.event; + e.preventDefault(); + pos3 = e.clientX; + pos4 = e.clientY; + document.onmouseup = closeDragElement; + document.onmousemove = elementDrag; + } + + function elementDrag(e) { + e = e || window.event; + e.preventDefault(); + pos1 = pos3 - e.clientX; + pos2 = pos4 - e.clientY; + pos3 = e.clientX; + pos4 = e.clientY; + + let newTop = form.offsetTop - pos2; + let newLeft = form.offsetLeft - pos1; + + const minTop = form.offsetHeight / 2; + const minLeft = form.offsetWidth / 2; + const maxTop = window.innerHeight - form.offsetHeight / 2; + const maxLeft = window.innerWidth - form.offsetWidth / 2; + + if (newTop < minTop) newTop = minTop; + if (newTop > maxTop) newTop = maxTop; + if (newLeft < minLeft) newLeft = minLeft; + if (newLeft > maxLeft) newLeft = maxLeft; + + form.style.top = newTop + "px"; + form.style.left = newLeft + "px"; + } + + function closeDragElement() { + document.onmouseup = null; + document.onmousemove = null; + } + } + + static createHeader(text) { + const header = document.createElement("div"); + header.classList.add("form-header"); + header.textContent = text; + return header; + } + + 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); + }); + + 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 ff07eeb9a5..4afa79ffbc 100644 --- a/src/bonsai/bonsai/bim/data/webui/templates/costing.html +++ b/src/bonsai/bonsai/bim/data/webui/templates/costing.html @@ -84,8 +84,14 @@
-
-
+

Costing

+
+
  • + Add New Schedule +
    + +
    +