cost module web ui improvements:

-add ribbon bar,
- improve settings menu with ability to change font size for tables.
- Improve cost schedule spreadsheet layout
This commit is contained in:
myoualid
2024-09-08 15:29:46 +01:00
parent 83be26e196
commit c607cbc9d7
9 changed files with 452 additions and 73 deletions
@@ -25,18 +25,18 @@
transform: scale(1.05);
}
:root.blender .card-body {
padding: 20px;
}
:root.blender .card-title {
font-size: 1.25rem;
margin-bottom: 10px;
border-bottom: 1px solid #ddd;
padding: 10px;
}
:root.blender .card-text {
font-size: 1rem;
margin-bottom: 20px;
padding: 10px;
}
:root.blender .btn-primary {
@@ -1,36 +1,72 @@
table {
width: 100%;
height: 80%;
border-collapse: collapse;
user-select: none;
:root {
--fontSize: 14px;
}
body {
margin: 0;
padding: 0;
}
.table-container {
max-height: 70vh;
margin-top: 1em;
overflow-y: auto;
overflow-x: auto;
position: relative;
}
form table {
height: 50%;
#cost-items {
height: calc(100vh - var(--margin-small));
}
form .table-container {
max-height: 50vh;
.table-scrollable-body {
max-height: 70vh;
overflow-y: auto;
display: block;
}
table {
width: 100%;
border-collapse: collapse;
user-select: none;
table-layout: fixed;
font-size: var(--fontSize);
}
thead {
position: sticky;
top: 0;
z-index: 1;
background-color: #727272;
font-size: larger;
}
tbody {
width: 100%;
}
td,
th {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
padding: 0;
margin: 0;
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
text-align: center;
border-bottom: 0;
border-top: 0;
}
th {
border-bottom: 1px solid #ddd;
}
td {
align-items: center;
}
td,
th {
box-sizing: border-box; /* Include padding and border in the element's total width and height */
}
/* first td of a row, must be flex */
td:first-child {
display: flex;
@@ -65,7 +101,7 @@ select {
height: inherit;
margin-left: 0.5em;
margin-right: 0.5em;
font-size: var(--fontSize);
min-width: 50px; /* Minimum width to prevent it from becoming too small */
}
@@ -101,6 +137,7 @@ input:focus {
background-color: #444444;
text-align: center;
}
/* Responsive Styles */
@media (max-width: 768px) {
table {
@@ -132,4 +169,4 @@ input:focus {
padding: 1px;
font-size: 10px;
}
}
}
@@ -0,0 +1,47 @@
.ribbon-bar {
display: flex;
gap: 10px;
margin-bottom: 20px;
border: 1px solid #444;
}
.ribbon-bar .action-button {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border: none;
background-color: #444;
color: #fff;
border-radius: 50%;
cursor: pointer;
transition: background-color 0.3s;
border-left: 1px solid #666;
}
.ribbon-bar .action-button:hover {
background-color: #666;
}
.ribbon-bar .action-button::after {
content: attr(data-tooltip);
position: absolute;
bottom: -30px;
left: 50%;
transform: translateX(-50%);
background-color: #333;
color: #fff;
padding: 5px 10px;
border-radius: 5px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
font-size: 12px;
}
.ribbon-bar .action-button:hover::after {
opacity: 1;
}
@@ -2,6 +2,7 @@
@import url("./components/card.css");
@import url("./components/contextMenu.css");
@import url("./components/costTable.css");
@import url("./components/headerBar.css");
.form-header {
display: flex;
@@ -19,8 +20,8 @@ span {
}
.floating-form {
min-width: 40%;
max-width: 80%;
height: 40vh;
width: 50vw;
max-height: 80vh;
height: auto;
position: absolute;
@@ -219,7 +219,6 @@ button:hover {
}
.row-container {
width: 100%;
border-collapse: collapse;
display: flex;
justify-content: flex-start;
@@ -11,21 +11,22 @@ $(document).ready(function () {
setTheme(theme);
connectSocket();
const settingsMenu = CostUI.Form({
id: "settings-menu",
name: "Settings",
icon: "fa-solid fa-gear",
shouldHide: true,
CostUI.addSettingsMenu();
CostUI.addRibbonButton({
text: "Hide Schedules",
icon: "fa-regular fa-eye-slash",
callback: (button) => {
CostUI.toggleSchedulesContainer(button);
}
});
settingsMenu.style.display = "none";
const picker = CostUI.createColorPicker();
settingsMenu.appendChild(picker);
const settingsButton = document.getElementById("settings-button");
settingsButton.addEventListener("click", function () {
settingsMenu.style.display = "block";
CostUI.addRibbonButton({
text: "Settings",
icon: "fas fa-cog",
callback: (button) => {
CostUI.toggleSettingsMenu(button);
}
});
});
function connectSocket() {
@@ -85,6 +86,7 @@ function handleSelectedProducts(data) {
}
function handlePredefinedTypes(data) {
CostUI.enableAddingCostSchedule(
data.data["predefined_types"],
addCostSchedule
@@ -233,6 +235,65 @@ function setTheme(theme) {
localStorage.setItem("theme", theme);
}
function toggleClientList() {
var clientList = $("#client-list");
if (clientList.hasClass("show")) {
clientList.removeClass("show");
return;
}
clientList.empty();
var counter = 0;
$.each(connectedClients, function (id, client) {
counter++;
const dropdownIcon = $("<i>")
.addClass("fas fa-chevron-down")
.css("margin-left", "0.5rem");
const clientDiv = $("<div>")
.addClass("client")
.text(client.ifc_file || "Blender " + counter);
clientDiv.append(dropdownIcon);
const clientDetailsDiv = $("<div>").addClass("client-details");
if (id) {
const clientId = $("<div>")
.addClass("client-detail")
.text(`Blender ID: ${id}`);
clientDetailsDiv.append(clientId);
}
if (!client.has_drawings) {
const clientShown = $("<div>")
.addClass("client-detail")
.text("No drawings exist for this IFC File yet");
clientDetailsDiv.append(clientShown);
} else {
const clientNumbers = $("<div>")
.addClass("client-detail")
.text(
`${allDrawings[client.ifc_file].drawings.length} Drawing(s),
${allDrawings[client.ifc_file].sheets.length} Sheet(s)`
);
clientDetailsDiv.append(clientNumbers);
}
clientDiv.append(clientDetailsDiv);
clientDiv.on("click", function () {
clientDetailsDiv.toggleClass("show");
});
clientList.append(clientDiv);
});
clientList.addClass("show");
}
function addCostItem(costItemId) {
executeOperator({ type: "addCostItem", costItemId: costItemId });
}
@@ -304,6 +365,7 @@ function handleCostSchedulesData(data) {
}
function handleCostItemsData(data) {
console.log(data);
const cards = document.querySelectorAll("[id^='schedule-']");
cards.forEach((card) => {
if (card.id === "schedule-" + data.data["cost_items"]["cost_schedule_id"]) {
@@ -126,7 +126,10 @@ function handleWorkScheduleData(data) {
const workSchedules = data["data"]["work_schedule_info"];
workSchedules.forEach((workSchedule) => {
const mainContainer = CostUI.text(new Date(workSchedule.CreationDate).toLocaleDateString());
const mainContainer = CostUI.Text(
"Created On: " + new Date(workSchedule.CreationDate).toLocaleDateString(),
"fa-solid fa-pen-nib"
);
const callback = () => loadWorkSchedule(workSchedule.id);
const card = CostUI.createCard(workSchedule.id, workSchedule.Name,mainContainer, callback);
workSchedulesDiv.append(card);
@@ -25,7 +25,10 @@ export class CostUI {
"Total Cost",
"Action",
];
const thead = document.createElement("thead");
const tr = document.createElement("tr");
thead.appendChild(tr);
for (let i = 0; i < columnHeaders.length; i++) {
const th = document.createElement("th");
@@ -41,7 +44,7 @@ export class CostUI {
}
}
tbody.appendChild(tr);
table.appendChild(thead);
table.appendChild(tbody);
document.getElementById("cost-items").appendChild(table);
@@ -69,7 +72,8 @@ export class CostUI {
`;
document.head.appendChild(style);
}
static createContextMenu(callbacks) {
static createContextMenu(callbacks) {
const contextMenu = document.createElement("div");
contextMenu.id = "context-menu";
contextMenu.classList.add("context-menu");
@@ -144,7 +148,7 @@ export class CostUI {
const costItemId = parseInt(targetRow.getAttribute("id"));
let expandedState =
JSON.parse(localStorage.getItem("expandedState")) || {};
expandedState = CostUI.deleteRow(targetRow, expandedState);
expandedState = CostUI.deleteCostItemRow(targetRow, expandedState);
localStorage.setItem("expandedState", JSON.stringify(expandedState));
callbacks.deleteCostItem(costItemId);
}
@@ -178,9 +182,51 @@ export class CostUI {
});
getSelectedProducts.hasListener = true;
}
// Function to get column names from the table header
function getColumnNames(tableId) {
const table = document.getElementById(tableId);
const headerRow = table.querySelector("thead tr");
return Array.from(headerRow.children).map(headerCell => headerCell.textContent.trim());
}
// Add double-click event listener
document.getElementById("cost-items").addEventListener("dblclick", function (event) {
const targetRow = event.target.closest("tr");
const targetCell = event.target.closest("td");
if (targetRow && targetCell) {
const columnIndex = Array.from(targetRow.children).indexOf(targetCell);
const costItemId = parseInt(targetRow.getAttribute("id"));
const cellContent = targetCell.textContent;
// Get column names from the table header
const columnNames = getColumnNames("cost-items");
const cellName = columnNames[columnIndex];
console.log("Column Name: ", cellName);
console.log("Content: ", cellContent);
// Implement different behaviors based on the column index
switch (columnIndex) {
case 0:
// Behavior for the first column
callbacks.firstColumnDoubleClick ? callbacks.firstColumnDoubleClick(costItemId) : null;
break;
case 1:
// Behavior for the second column
callbacks.secondColumnDoubleClick ? callbacks.secondColumnDoubleClick(costItemId) : null;
break;
// Add more cases as needed for other columns
default:
// Default behavior
callbacks.defaultDoubleClick ? callbacks.defaultDoubleClick(costItemId) : null;
break;
}
}
});
}
static deleteRow(targetRow, expandedState) {
static deleteCostItemRow(targetRow, expandedState) {
if (!targetRow) {
return;
}
@@ -188,7 +234,7 @@ export class CostUI {
targetRow.remove();
const subRows = document.querySelectorAll(`[parent-id='${targetRow.id}']`);
subRows.forEach((subRow) => {
CostUI.deleteRow(subRow, expandedState);
CostUI.deleteCostItemRow(subRow, expandedState);
});
return expandedState;
}
@@ -258,6 +304,7 @@ export class CostUI {
const form = CostUI.Form({
id: "add-cost-schedule-form",
name: name,
icon: "fa-solid fa-money-bill-wave",
});
const nameLabel = document.createElement("label");
@@ -672,9 +719,8 @@ export class CostUI {
const cardBody = document.createElement("div");
cardBody.classList.add("card-body");
const cardTitle = document.createElement("h5");
const cardTitle = CostUI.Text(title, "fa-solid fa-money-bill-wave", "large");
cardTitle.classList.add("card-title");
cardTitle.textContent = title;
const cardButton = document.createElement("button");
cardButton.classList.add("action-button");
@@ -1163,10 +1209,10 @@ export class CostUI {
const subtotalRow = document.createElement("tr");
const cells = [
{ textContent: "", style: { border: "none" } },
{ textContent: "", style: { border: "none" } },
{ textContent: "", style: { border: "none" } },
{ textContent: "Subtotal", colSpan: 4, style: { border: "none" } },
{ textContent: "", style: { border: "none" } },
{ textContent: "", style: { border: "none" } },
{ textContent: "", style: { border: "none" } },
{ textContent: quantitySums.count },
];
@@ -1202,15 +1248,25 @@ export class CostUI {
costItemId,
callbacks,
}) {
if (products.length === 0) {
const noProductsMessage = document.createElement("p");
noProductsMessage.textContent = "Your Blender Selection is empty! Select objects first.";
form.appendChild(noProductsMessage);
return;
}
const { tableContainer, table } = CostUI.addTable({
headers: ["Step Id", "Class", "Name", "Type", "Count"],
className: "product-table",
id: "cost-values-table-" + costItemId,
});
form.appendChild(tableContainer);
const scrollableBody = document.createElement("div");
scrollableBody.classList.add("table-scrollable-body");
const tbody = document.createElement("tbody");
table.appendChild(tbody);
form.appendChild(tableContainer);
scrollableBody.appendChild(tbody);
table.appendChild(scrollableBody);
const quantityAssigned = CostUI.getAssinedQuantity(products, costItemId);
const areProductsSameClass = products.every(
@@ -1343,9 +1399,12 @@ export class CostUI {
id: "cost-values-table-" + costItemId,
});
const tbody = document.createElement("tbody");
table.appendChild(tbody);
form.appendChild(tableContainer);
const scrollableBody = document.createElement("div");
scrollableBody.classList.add("table-scrollable-body");
const tbody = document.createElement("tbody");
scrollableBody.appendChild(tbody);
table.appendChild(scrollableBody);
let quantityAssigned = CostUI.getAssinedQuantity(products, costItemId);
const shouldAddQto = quantityAssigned ? true : false;
@@ -1417,4 +1476,174 @@ export class CostUI {
addProductAssignmentsButton.classList.add("action-button");
return addProductAssignmentsButton;
}
static addSettingsMenu() {
if (document.getElementById("settings-menu")) {
return document.getElementById("settings-menu");
}
const settingsMenu = CostUI.Form({
id: "settings-menu",
name: "Settings",
icon: "fa-solid fa-gear",
shouldHide: true,
});
const picker = CostUI.createColorPicker();
const tableFontSize = CostUI.createFontSizePicker();
settingsMenu.appendChild(picker);
settingsMenu.appendChild(tableFontSize);
settingsMenu.style.display = "none";
const settingsButton = document.getElementById("settings-button");
settingsButton.addEventListener("click", function () {
settingsMenu.style.display = "block";
});
// Apply saved settings on page load
CostUI.applySavedSettings();
}
static createFontSizePicker() {
const div = document.createElement("div");
const fontSizeText = document.createElement("p");
fontSizeText.textContent = "Select a font size for the table:";
div.appendChild(fontSizeText);
const fontSizePicker = document.createElement("input");
fontSizePicker.type = "number";
fontSizePicker.id = "font-size-picker";
fontSizePicker.value = localStorage.getItem("tableFontSize") || 14;
div.appendChild(fontSizePicker);
fontSizePicker.addEventListener("input", function () {
const fontSize = fontSizePicker.value + "px";
CostUI.saveFontSize(fontSize);
CostUI.applyFontSizeToAllTables(fontSize);
});
return div;
}
static createColorPicker() {
const div = document.createElement("div");
const colorText = document.createElement("p");
colorText.textContent = "Select a color for the table:";
div.appendChild(colorText);
const colorPicker = document.createElement("input");
colorPicker.type = "color";
colorPicker.id = "color-picker";
colorPicker.value = localStorage.getItem("tableColor") || "#ffffff";
div.appendChild(colorPicker);
colorPicker.addEventListener("input", function () {
const color = colorPicker.value;
CostUI.saveColor(color);
CostUI.applyColorToAllTables(color);
});
return div;
}
static saveFontSize(fontSize) {
localStorage.setItem("tableFontSize", fontSize);
}
static saveColor(color) {
localStorage.setItem("tableColor", color);
}
static applyFontSizeToAllTables(fontSize) {
document.documentElement.style.setProperty('--fontSize', fontSize);
}
static applyColorToAllTables(color) {
const tables = document.querySelectorAll("table");
tables.forEach((table) => {
table.style.backgroundColor = color;
});
}
static applySavedSettings() {
console.log("Applying saved settings");
const savedFontSize = localStorage.getItem("tableFontSize");
if (savedFontSize) {
CostUI.applyFontSizeToAllTables(savedFontSize);
const fontSizePicker = document.getElementById("font-size-picker");
if (fontSizePicker) {
fontSizePicker.value = parseInt(savedFontSize, 10);
}
}
const savedColor = localStorage.getItem("tableColor");
if (savedColor) {
CostUI.applyColorToAllTables(savedColor);
const colorPicker = document.getElementById("color-picker");
if (colorPicker) {
colorPicker.value = savedColor;
}
}
}
static getRibbonBar() {
return document.getElementById("ribbon-bar");
}
static addRibbonButton({ text, icon, callback }) {
const ribbonBar = CostUI.getRibbonBar();
const button = document.createElement("button");
button.classList.add("action-button", ...icon.split(" "));
button.dataset.tooltip = text; // Use dataset to set the tooltip attribute
button.addEventListener("click", () => callback(button));
ribbonBar.appendChild(button);
}
static toggleSchedulesContainer(button) {
const schedulesContainer = CostUI.getSchedulesContainer();
const isHidden = schedulesContainer.style.display === "none";
if (isHidden) {
CostUI.displaySchedulesContainer();
button.classList.remove("fa-eye");
button.classList.add("fa-eye-slash");
button.dataset.tooltip = "Hide Schedules";
} else {
CostUI.hideSchedulesContainer();
button.classList.remove("fa-eye-slash");
button.classList.add("fa-eye");
button.dataset.tooltip = "Display Schedules";
}
}
static getSchedulesContainer() {
return document.getElementById("cost-schedules");
}
static displaySchedulesContainer() {
const schedulesContainer = CostUI.getSchedulesContainer();
schedulesContainer.style.display = "flex";
}
static hideSchedulesContainer() {
const schedulesContainer = CostUI.getSchedulesContainer();
schedulesContainer.style.display = "none";
}
static getSettingsMenu() {
return document.getElementById("settings-menu");
}
static toggleSettingsMenu(button) {
const settingsMenu = CostUI.getSettingsMenu();
const isHidden = settingsMenu.style.display === "none";
if (isHidden) {
settingsMenu.style.display = "block";
button.classList.remove("fa-gear");
button.classList.add("fa-times");
button.dataset.tooltip = "Close Settings";
} else {
settingsMenu.style.display = "none";
button.classList.remove("fa-times");
button.classList.add("fa-gear");
button.dataset.tooltip = "Open Settings";
}
}
}
@@ -4,7 +4,12 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlenderBIM Web UI</title>
<link rel="stylesheet" type="text/css" href="/static/css/cost.css" id="index-stylesheet" />
<link
rel="stylesheet"
type="text/css"
href="/static/css/cost.css"
id="index-stylesheet"
/>
<link
rel="stylesheet"
id="tabulator-stylesheet"
@@ -30,9 +35,6 @@
var SOCKET_PORT = {{port}};
</script>
<script type="module" defer src="./static/js/cost.js"></script>
<style>
</style>
</head>
<body>
<nav>
@@ -43,12 +45,10 @@
/>
<ul>
<li>
<a href="/"
><i class="fa-solid fa-table"></i> Schedules</a
>
<a href="/"><i class="fa-solid fa-table"></i> Schedules</a>
</li>
<li>
<a href="/costing" class="active"
<a href="/costing" class="active"
><i class="fa-solid fa-money-bill-trend-up"></i> Costing</a
>
</li>
@@ -78,25 +78,26 @@
<button id="settings-button">
<i class="fa-solid fa-gear"></i>
</button>
<div id="connected-list-div">
<button id="show-connected-button" onclick="toggleClientList()">
Connected Blenders:
<span id="blender-count">0</span>
<i class="fas fa-chevron-down"></i>
</button>
<div id="client-list"></div>
</div>
</nav>
<div id="connected-list-div">
<button id="show-connected-button" onclick="toggleClientList()">
Connected Blenders:
<span id="blender-count">0</span>
<i class="fas fa-chevron-down"></i>
</button>
<div id="client-list"></div>
<div class="row-container">
<div id="ribbon-bar" class="ribbon-bar">
<button
id="add-cost-schedule"
class="action-button fas fa-plus"
data-tooltip="Add New Schedule"
></button>
</div>
</div>
<h1>Costing</h1>
<div id="" >
<li id="add-cost-schedule" class="action-button fas fa-plus"> </li>
Add New Schedule
</div>
<div id="cost-schedules" class="row-container"></div>
<div id="cost-items" class="table-container"></div>
<footer>
<p>BlenderBIM Version: {{version}}</p>
</footer>