mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 18:16:40 +00:00
First Draft Cost Schedule Web UI:
- display cost schedules - load cost items - Add cost items - Edit cost item names
This commit is contained in:
@@ -115,13 +115,21 @@ class BlenderNamespace(socketio.AsyncNamespace):
|
||||
blender_theme = data
|
||||
await sio.emit("theme_data", data, namespace="/web")
|
||||
|
||||
|
||||
# this function will be called when the event demo_data is emitted
|
||||
async def on_demo_data(self, sid, data):
|
||||
print(f"Demo data from Blender client {sid}")
|
||||
blender_messages[sid]["demo_data"] = data
|
||||
await sio.emit("demo_data", {"blenderId": sid, "data": data}, namespace="/web")
|
||||
|
||||
async def on_cost_items(self, sid, data):
|
||||
print(f"Cost items data from Blender client {sid}")
|
||||
blender_messages[sid]["cost_items"] = data
|
||||
await sio.emit("cost_items", {"blenderId": sid, "data": data}, namespace="/web")
|
||||
|
||||
async def on_cost_schedules(self, sid, data):
|
||||
print(f"Cost schedule info from Blender client {sid}")
|
||||
blender_messages[sid]["cost_schedules"] = data
|
||||
await sio.emit("cost_schedules", {"blenderId": sid, "data": data}, namespace="/web")
|
||||
|
||||
async def schedules(request):
|
||||
with open("templates/index.html", "r") as f:
|
||||
@@ -129,6 +137,12 @@ async def schedules(request):
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
|
||||
return web.Response(text=html_content, content_type="text/html")
|
||||
|
||||
async def costing(request):
|
||||
with open("templates/costing.html", "r") as f:
|
||||
template = f.read()
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
|
||||
return web.Response(text=html_content, content_type="text/html")
|
||||
|
||||
|
||||
async def sequencing(request):
|
||||
with open("templates/gantt.html", "r") as f:
|
||||
@@ -179,6 +193,7 @@ sio.register_namespace(BlenderNamespace("/blender"))
|
||||
app.router.add_get("/", schedules)
|
||||
app.router.add_get("/documentation", documentation)
|
||||
app.router.add_get("/sequencing", sequencing)
|
||||
app.router.add_get("/costing", costing)
|
||||
app.router.add_get("/demo", demo)
|
||||
|
||||
# Add static files
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
:root.blender .flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* CSS for the work schedule cards */
|
||||
:root.blender #work_schedules {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
:root.blender .card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
width: 300px;
|
||||
margin: 10px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
:root.blender .card:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
:root.blender .card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
:root.blender .card-title {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
:root.blender .card-text {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
:root.blender .btn-primary {
|
||||
background-color: #007bff;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 1rem;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
:root.blender .btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { CostUI } from './utilities/costui.js';
|
||||
|
||||
const connectedClients = {};
|
||||
let socket;
|
||||
|
||||
$(document).ready(function () {
|
||||
var defaultTheme = "blender";
|
||||
var theme = localStorage.getItem("theme") || defaultTheme;
|
||||
setTheme(theme);
|
||||
|
||||
connectSocket();
|
||||
CostUI.createColorPicker();
|
||||
});
|
||||
|
||||
function connectSocket() {
|
||||
const url = "ws://localhost:" + SOCKET_PORT + "/web";
|
||||
socket = io(url);
|
||||
|
||||
socket.on("blender_connect", handleBlenderConnect);
|
||||
socket.on("blender_disconnect", handleBlenderDisconnect);
|
||||
socket.on("connected_clients", handleConnectedClients);
|
||||
socket.on("theme_data", handleThemeData);
|
||||
socket.on("connect", handleWebConnect);
|
||||
socket.on("cost_schedules", handleCostSchedulesData);
|
||||
socket.on("cost_items", handleCostItemsData);
|
||||
}
|
||||
|
||||
function handleBlenderConnect(blenderId) {
|
||||
if (!connectedClients.hasOwnProperty(blenderId)) {
|
||||
connectedClients[blenderId] = { shown: false, ifc_file: "" };
|
||||
}
|
||||
|
||||
$("#blender-count").text(function (i, text) {
|
||||
return parseInt(text, 10) + 1;
|
||||
});
|
||||
}
|
||||
|
||||
function handleBlenderDisconnect(blenderId) {
|
||||
if (connectedClients.hasOwnProperty(blenderId)) {
|
||||
delete connectedClients[blenderId];
|
||||
removeTableElement(blenderId);
|
||||
}
|
||||
|
||||
$("#blender-count").text(function (i, text) {
|
||||
return parseInt(text, 10) - 1;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function handleConnectedClients(data) {
|
||||
$("#blender-count").text(data.length);
|
||||
|
||||
data.forEach(function (id) {
|
||||
connectedClients[id] = { shown: false, ifc_file: "" };
|
||||
});
|
||||
}
|
||||
|
||||
function handleThemeData(themeData) {
|
||||
function arrayToRgbString(arr) {
|
||||
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
|
||||
if (a !== undefined) {
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
}
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
|
||||
function generateCssVariableRule(theme) {
|
||||
let cssVariables = ":root.blender {\n";
|
||||
for (const key in theme) {
|
||||
const cssVariableName = `--blender-${key.replace(/_/g, "-")}`;
|
||||
const cssVariableValue = arrayToRgbString(theme[key]);
|
||||
cssVariables += ` ${cssVariableName}: ${cssVariableValue};\n`;
|
||||
}
|
||||
cssVariables += "}";
|
||||
return cssVariables;
|
||||
}
|
||||
|
||||
const cssRule = generateCssVariableRule(themeData.theme);
|
||||
|
||||
var styleElement = $("#index-stylesheet")[0];
|
||||
if (styleElement) {
|
||||
var sheet = styleElement.sheet || styleElement.styleSheet;
|
||||
sheet.insertRule(cssRule, sheet.cssRules.length);
|
||||
}
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
$("html").removeClass("light dark blender").addClass(theme);
|
||||
$(":root").css("color-scheme", theme);
|
||||
if (theme === "light") {
|
||||
$("#toggle-theme").html('<i class="fas fa-sun"></i>');
|
||||
} else if (theme === "dark") {
|
||||
$("#toggle-theme").html('<i class="fas fa-moon"></i>');
|
||||
} else if (theme === "blender") {
|
||||
$("#toggle-theme").html('<i class="fas fa-adjust"></i>');
|
||||
}
|
||||
localStorage.setItem("theme", theme);
|
||||
}
|
||||
|
||||
function addCostItem(costItemId) {
|
||||
console.log("addCostItem", costItemId);
|
||||
executeOperator({ type: "addCostItem", costItemId: costItemId });
|
||||
}
|
||||
|
||||
function editCostItemName(costItemId, name) {
|
||||
executeOperator({ type: "editCostItemName", costItemId: costItemId, name: name });
|
||||
}
|
||||
|
||||
function selectAssignedElements(costItemId) {
|
||||
executeOperator({ type: "selectAssignedElements", costItemId: costItemId });
|
||||
}
|
||||
|
||||
function handleWebConnect() {
|
||||
getCostSchedules();
|
||||
}
|
||||
|
||||
function handleCostSchedulesData(data) {
|
||||
const blenderId = data.blenderId;
|
||||
const costSchedules = data.data["cost_schedules"]["cost_schedules"];
|
||||
const currency = data.data["cost_schedules"]["currency"]["name"];
|
||||
|
||||
console.log(data.data["cost_schedules"]);
|
||||
|
||||
const costScheduleDiv = $("#cost-schedules");
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
function handleCostItemsData(data) {
|
||||
console.log(data);
|
||||
CostUI.createCostSchedule({
|
||||
data: data.data["cost_items"],
|
||||
blenderID: data.blenderId,
|
||||
callbacks: {
|
||||
"addCostItem": addCostItem,
|
||||
"selectAssignedElements": selectAssignedElements,
|
||||
'editCostItemName': editCostItemName,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function executeOperator(operator, blenderId) {
|
||||
const msg = {
|
||||
sourcePage: "cost",
|
||||
operator: operator,
|
||||
};
|
||||
if (blenderId !== undefined) {
|
||||
msg.BlenderId = blenderId;
|
||||
}
|
||||
socket.emit("web_operator", msg);
|
||||
}
|
||||
|
||||
function loadCostSchedule(costScheduleId, blenderId) {
|
||||
executeOperator({ type: "loadCostSchedule", costScheduleId: costScheduleId }, blenderId);
|
||||
}
|
||||
|
||||
function getCostSchedules(blenderId) {
|
||||
executeOperator({ type: "getCostSchedules" }, blenderId);
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
export class CostUI {
|
||||
constructor() {}
|
||||
|
||||
createButton() {
|
||||
console.log("Button created");
|
||||
}
|
||||
|
||||
createInput() {
|
||||
console.log("Input created");
|
||||
}
|
||||
|
||||
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) {
|
||||
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"; // Required for the resizer handle
|
||||
tr.appendChild(th);
|
||||
|
||||
// Add resizer handle
|
||||
if (i < columnHeaders.length - 1) { // No resizer for the last column
|
||||
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);
|
||||
|
||||
// Add CSS to set column widths, resizer styles, hover effect, and color scheme
|
||||
CostUI.addTableStyles(id);
|
||||
|
||||
// Create context menu
|
||||
CostUI.createContextMenu();
|
||||
|
||||
table.get_blender_id = function() {
|
||||
return this.getAttribute("id").split("-")[2];
|
||||
};
|
||||
|
||||
return [table, tbody];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
#cost-items-${id} th:not(:nth-child(1)),
|
||||
#cost-items-${id} td:not(:nth-child(1)) {
|
||||
width: 100px; /* Set a fixed width for other columns */
|
||||
}
|
||||
th {
|
||||
position: relative;
|
||||
}
|
||||
.resizer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 5px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
}
|
||||
.context-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
z-index: 1000;
|
||||
}
|
||||
.context-menu button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
background: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.context-menu button:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
#cost-items tr:hover {
|
||||
background-color: #f0f0f0; /* Change this color to your desired hover color */
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
static createContextMenu() {
|
||||
// Create context menu
|
||||
const contextMenu = document.createElement("div");
|
||||
contextMenu.id = "context-menu";
|
||||
contextMenu.classList.add("context-menu");
|
||||
contextMenu.innerHTML = `
|
||||
<button id="edit-button">Edit</button>
|
||||
<button id="delete-button">Delete</button>
|
||||
<button id="duplicate-button">Duplicate</button>
|
||||
`;
|
||||
document.body.appendChild(contextMenu);
|
||||
|
||||
// Add event listeners for context menu
|
||||
document.addEventListener("contextmenu", function(event) {
|
||||
event.preventDefault();
|
||||
const targetRow = event.target.closest("tr");
|
||||
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`;
|
||||
|
||||
// Store the target row in the context menu for later use
|
||||
contextMenu.targetRow = targetRow;
|
||||
} 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";
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("edit-button").addEventListener("click", function() {
|
||||
const targetRow = document.getElementById("context-menu").targetRow;
|
||||
if (targetRow) {
|
||||
// Implement your edit action here
|
||||
console.log("Edit row:", targetRow.getAttribute("id"));
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("delete-button").addEventListener("click", function() {
|
||||
const targetRow = document.getElementById("context-menu").targetRow;
|
||||
if (targetRow) {
|
||||
// Implement your delete action here
|
||||
console.log("Delete row:", targetRow.getAttribute("id"));
|
||||
targetRow.remove();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("duplicate-button").addEventListener("click", function() {
|
||||
const targetRow = document.getElementById("context-menu").targetRow;
|
||||
if (targetRow) {
|
||||
// Implement your duplicate action here
|
||||
console.log("Duplicate row:", targetRow.getAttribute("id"));
|
||||
const newRow = targetRow.cloneNode(true);
|
||||
targetRow.parentElement.appendChild(newRow);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
// This function generates a color scheme based on the base color
|
||||
// For simplicity, we'll just lighten the base color for each level
|
||||
const levels = 7; // Number of levels of nesting
|
||||
const colorScheme = [];
|
||||
for (let i = 0; i < levels; i++) {
|
||||
colorScheme.push(CostUI.lightenColor(baseColor, i * 7));
|
||||
}
|
||||
return colorScheme;
|
||||
}
|
||||
|
||||
static lightenColor(color, percent) {
|
||||
// This function lightens a color by a given percentage
|
||||
const num = parseInt(color.slice(1), 16),
|
||||
amt = Math.round(2.55 * percent),
|
||||
R = (num >> 16) + amt,
|
||||
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; // Assuming level is determined by index for simplicity
|
||||
row.style.backgroundColor = colorScheme[level];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
static createColorPicker() {
|
||||
const colorPicker = document.createElement("input");
|
||||
colorPicker.type = "color";
|
||||
colorPicker.id = "color-picker";
|
||||
colorPicker.value = "#ff0000"; // Default color
|
||||
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);
|
||||
CostUI.createCostItem(data, tbody, 0, null, callbacks);
|
||||
CostUI.applyExpandedState();
|
||||
}
|
||||
|
||||
static createCostItem(data, container, nestingLevel = 0, parentID = null, callbacks = {}) {
|
||||
data.forEach(obj => {
|
||||
const row = CostUI.createRow(obj, nestingLevel, parentID, callbacks);
|
||||
container.appendChild(row);
|
||||
|
||||
if (obj.is_nested_by && obj.is_nested_by.length > 0) {
|
||||
CostUI.createCostItem(obj.is_nested_by, container, nestingLevel + 1, obj.id, callbacks);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static createRow(obj, nestingLevel, parentID, callbacks = {}) {
|
||||
const row = document.createElement("tr");
|
||||
row.setAttribute("id", obj.id);
|
||||
row.setAttribute("parent-id", parentID);
|
||||
if (nestingLevel > 0) {
|
||||
row.classList.add("nested");
|
||||
row.classList.add(`level-${nestingLevel}`);
|
||||
}
|
||||
const expandButton = document.createElement("button");
|
||||
expandButton.classList.add("toggle-button");
|
||||
if (obj.is_nested_by && obj.is_nested_by.length > 0) {
|
||||
expandButton.textContent = ">";
|
||||
} else {
|
||||
expandButton.style.visibility = "hidden";
|
||||
}
|
||||
//row.appendChild(expandButton);
|
||||
|
||||
expandButton.addEventListener("click", function() {
|
||||
CostUI.contractExpandRow.call(this, obj.id);
|
||||
});
|
||||
|
||||
const nameCell = document.createElement("td");
|
||||
const nameInput = document.createElement("input");
|
||||
nameInput.value = obj.name ? obj.name : "Unnamed";
|
||||
|
||||
nameInput.addEventListener("change", function() {
|
||||
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
|
||||
});
|
||||
|
||||
nameInput.addEventListener("keydown", function(event) {
|
||||
if (event.key === "Enter") {
|
||||
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
|
||||
}
|
||||
});
|
||||
nameCell.style.paddingLeft = nestingLevel * 20 + "px";
|
||||
nameCell.appendChild(expandButton);
|
||||
nameCell.appendChild(nameInput);
|
||||
row.appendChild(nameCell);
|
||||
|
||||
const totalCostQuantityCell = document.createElement("td");
|
||||
totalCostQuantityCell.textContent = obj.TotalCostQuantity;
|
||||
row.appendChild(totalCostQuantityCell);
|
||||
|
||||
const unitSymbolCell = document.createElement("td");
|
||||
unitSymbolCell.textContent = obj.UnitSymbol;
|
||||
row.appendChild(unitSymbolCell);
|
||||
|
||||
const totalAppliedValueCell = document.createElement("td");
|
||||
totalAppliedValueCell.textContent = obj.TotalAppliedValue;
|
||||
row.appendChild(totalAppliedValueCell);
|
||||
|
||||
const totalCostCell = document.createElement("td");
|
||||
const totalCost = parseFloat(obj.TotalCost).toFixed(2);
|
||||
|
||||
totalCostCell.textContent = obj.is_sum ? totalCost + " (Σ)" : totalCost;
|
||||
|
||||
row.appendChild(totalCostCell);
|
||||
|
||||
const divFlex = document.createElement("div");
|
||||
divFlex.classList.add("flex-container");
|
||||
const addButton = document.createElement("button");
|
||||
addButton.textContent = "+";
|
||||
addButton.classList.add("add-button");
|
||||
addButton.addEventListener("click", function(e) {
|
||||
e.stopPropagation();
|
||||
callbacks.addCostItem ? callbacks.addCostItem(obj.id) : null;
|
||||
});
|
||||
|
||||
const selectButton = document.createElement("button");
|
||||
selectButton.textContent = "Select";
|
||||
selectButton.addEventListener("click", function(e) {
|
||||
e.stopPropagation();
|
||||
callbacks.selectAssignedElements ? callbacks.selectAssignedElements(obj.id) : null;
|
||||
});
|
||||
|
||||
divFlex.appendChild(addButton);
|
||||
divFlex.appendChild(selectButton);
|
||||
|
||||
const flexContainerCell = document.createElement("td");
|
||||
flexContainerCell.appendChild(divFlex);
|
||||
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 hideNestedRows(parentId) {
|
||||
const rows = document.querySelectorAll(`[parent-id='${parentId}']`);
|
||||
rows.forEach(row => {
|
||||
row.classList.add("nested");
|
||||
const childId = row.getAttribute('id');
|
||||
if (childId) {
|
||||
CostUI.hideNestedRows(childId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static showNestedRows(parentId) {
|
||||
const rows = document.querySelectorAll(`[parent-id='${parentId}']`);
|
||||
rows.forEach(row => {
|
||||
row.classList.remove("nested");
|
||||
const childId = row.getAttribute('id');
|
||||
if (childId && CostUI.isRowExpanded(childId)) {
|
||||
CostUI.showNestedRows(childId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static contractExpandRow(id) {
|
||||
const rows = document.querySelectorAll(`[parent-id='${id}']`);
|
||||
if (rows.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isVisible = false;
|
||||
rows.forEach(row => {
|
||||
if (!row.classList.contains("nested")) {
|
||||
isVisible = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (isVisible) {
|
||||
CostUI.hideNestedRows(id);
|
||||
this.textContent = ">";
|
||||
CostUI.updateExpandedState(id, false);
|
||||
} else {
|
||||
CostUI.showNestedRows(id);
|
||||
this.textContent = "^";
|
||||
CostUI.updateExpandedState(id, true);
|
||||
}
|
||||
}
|
||||
|
||||
static updateExpandedState(id, isExpanded) {
|
||||
const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {};
|
||||
expandedState[id] = isExpanded;
|
||||
localStorage.setItem('expandedState', JSON.stringify(expandedState));
|
||||
}
|
||||
|
||||
static applyExpandedState() {
|
||||
const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {};
|
||||
Object.keys(expandedState).forEach(id => {
|
||||
if (expandedState[id]) {
|
||||
CostUI.showNestedRows(id);
|
||||
const toggleButton = document.querySelector(`[id='${id}'] .toggle-button`);
|
||||
if (toggleButton) {
|
||||
toggleButton.textContent = "^";
|
||||
}
|
||||
} else {
|
||||
CostUI.hideNestedRows(id);
|
||||
const toggleButton = document.querySelector(`[id='${id}'] .toggle-button`);
|
||||
if (toggleButton) {
|
||||
toggleButton.textContent = ">";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static isRowExpanded(id) {
|
||||
const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {};
|
||||
return expandedState[id] || false;
|
||||
}
|
||||
|
||||
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("btn", "btn-primary");
|
||||
cardButton.textContent = "Load";
|
||||
cardButton.addEventListener("click", callback);
|
||||
|
||||
cardBody.appendChild(cardTitle);
|
||||
cardBody.appendChild(mainContainer);
|
||||
cardBody.appendChild(cardButton);
|
||||
card.appendChild(cardBody);
|
||||
|
||||
return card;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="blender">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BlenderBIM Web UI</title>
|
||||
<link rel="stylesheet" href="/static/css/gantt.css" id="index-stylesheet" />
|
||||
<link rel="stylesheet" href="/static/css/components/card.css" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
id="tabulator-stylesheet"
|
||||
href="https://unpkg.com/tabulator-tables/dist/css/tabulator_site_dark.min.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
|
||||
/>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://code.jquery.com/jquery-3.6.0.min.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://unpkg.com/tabulator-tables/dist/js/tabulator.min.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.socket.io/4.0.0/socket.io.min.js"
|
||||
></script>
|
||||
<script>
|
||||
var SOCKET_PORT = {{port}};
|
||||
</script>
|
||||
<script type="module" defer src="./static/js/cost.js"></script>
|
||||
<style>
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.nested {
|
||||
display: none;
|
||||
}
|
||||
.expand-collapse {
|
||||
cursor: pointer;
|
||||
}
|
||||
.caret-cell {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
td {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.flex-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<img
|
||||
src="https://bonsaibim.org/assets/images/blender/blender-logo.png"
|
||||
alt="Logo"
|
||||
class="logo"
|
||||
/>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/"
|
||||
><i class="fa-solid fa-table"></i> Schedules</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/costing" class="active"
|
||||
><i class="fa-solid fa-table"></i>Costing</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/sequencing"
|
||||
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/documentation"
|
||||
><i class="fa-solid fa-images"></i> Documentation</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a><i class="fa-solid fa-camera"></i> BCF Topics</a>
|
||||
</li>
|
||||
<li>
|
||||
<a><i class="fa-solid fa-square-check"></i> IDS Audits</a>
|
||||
</li>
|
||||
<li>
|
||||
<a><i class="fa-solid fa-hotel"></i> Facility Management</a>
|
||||
</li>
|
||||
</ul>
|
||||
<button id="toggle-theme" onclick="toggleTheme()">
|
||||
<i class="fas fa-moon"></i>
|
||||
</button>
|
||||
</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>
|
||||
<div id="cost-schedules"></div>
|
||||
<div id="UI"></div>
|
||||
<div id="cost-items"></div>
|
||||
|
||||
<footer>
|
||||
<p>BlenderBIM Version: {{version}}</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -53,6 +53,11 @@
|
||||
<li>
|
||||
<a href="/"><i class="fa-solid fa-table"></i> Schedules</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/costing"
|
||||
><i class="fa-solid fa-exclamation-triangle"></i>Costing</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/sequencing"
|
||||
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<script>
|
||||
var SOCKET_PORT = {{port}};
|
||||
</script>
|
||||
<script defer src="./static/js/gantt.js"></script>
|
||||
<script type="module" defer src="./static/js/gantt.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="no-print">
|
||||
@@ -35,6 +35,11 @@
|
||||
<li>
|
||||
<a href="/"><i class="fa-solid fa-table"></i> Schedules</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/costing"
|
||||
><i class="fa-solid fa-exclamation-triangle"></i>Costing</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/sequencing" class="active"
|
||||
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
|
||||
@@ -71,6 +76,7 @@
|
||||
</button>
|
||||
<div id="client-list" class="no-print"></div>
|
||||
</div>
|
||||
<div id="work_schedules"></div>
|
||||
<div id="container"></div>
|
||||
<footer>
|
||||
<p>Bonsai Version: {{version}}</p>
|
||||
|
||||
@@ -44,6 +44,11 @@
|
||||
><i class="fa-solid fa-table"></i> Schedules</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/costing"
|
||||
><i class="fa-solid fa-exclamation-triangle"></i>Costing</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/sequencing"
|
||||
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
|
||||
|
||||
@@ -807,3 +807,48 @@ class Cost(bonsai.core.tool.Cost):
|
||||
return {
|
||||
"Currency": currency,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create_cost_schedule_json(cls, cost_schedule: ifcopenshell.entity_instance) -> dict:
|
||||
from bonsai.bim.module.cost.data import CostSchedulesData
|
||||
if not CostSchedulesData.is_loaded:
|
||||
CostSchedulesData.load()
|
||||
cost_items = CostSchedulesData.data["cost_items"]
|
||||
data = []
|
||||
for rel in cost_schedule.Controls or []:
|
||||
for cost_item in rel.RelatedObjects or []:
|
||||
cls.create_cost_item_json(cost_item, cost_items, data)
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def create_cost_item_json(cls, cost_item: ifcopenshell.entity_instance, cost_items: dict, data: list):
|
||||
if cost_item.id() in cost_items.keys():
|
||||
cost_item_data = cost_items[cost_item.id()]
|
||||
cost_item_data["id"] = cost_item.id()
|
||||
cost_item_data["name"] = cost_item.Name
|
||||
cost_item_data["is_nested_by"] = []
|
||||
cost_item_data["is_sum"] = cls.is_cost_item_sum(cost_item)
|
||||
data.append(cost_item_data)
|
||||
else:
|
||||
return None
|
||||
for rel in cost_item.IsNestedBy or []:
|
||||
for sub_cost_item in rel.RelatedObjects or []:
|
||||
cls.create_cost_item_json(sub_cost_item, cost_items,cost_item_data["is_nested_by"])
|
||||
|
||||
@classmethod
|
||||
def is_cost_item_sum(cls, cost_item: ifcopenshell.entity_instance) -> bool:
|
||||
cost_values = []
|
||||
if cost_item.is_a("IfcCostItem"):
|
||||
cost_values = cost_item.CostValues
|
||||
elif cost_item.is_a("IfcCostValue"):
|
||||
cost_values = cost_item.Components
|
||||
for cost_value in cost_values or []:
|
||||
if cost_value.Category == "*":
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def currency(cls):
|
||||
unit = tool.Unit.get_project_currency_unit()
|
||||
if unit:
|
||||
return {"id": unit.id(), "name": unit.Currency}
|
||||
|
||||
@@ -36,6 +36,8 @@ import queue
|
||||
import json
|
||||
from time import sleep
|
||||
from pathlib import Path
|
||||
import bonsai.core.sequence
|
||||
import bonsai.core.cost
|
||||
|
||||
sio = None
|
||||
ws_process = None
|
||||
@@ -310,6 +312,8 @@ class Web(bonsai.core.tool.Web):
|
||||
elif operator["sourcePage"] == "demo":
|
||||
message = operator["operator"]["message"]
|
||||
print(f"Message from demo page: {message}")
|
||||
elif operator["sourcePage"] == "cost":
|
||||
cls.handle_cost_operator(operator["operator"])
|
||||
return 1.0
|
||||
|
||||
@classmethod
|
||||
@@ -328,6 +332,48 @@ class Web(bonsai.core.tool.Web):
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
|
||||
|
||||
@classmethod
|
||||
def handle_cost_operator(cls, operator_data: dict) -> None:
|
||||
"""
|
||||
this method handles the Cost page operators.
|
||||
|
||||
Args:
|
||||
operator_data (dict): A dictionary containing the operator data.
|
||||
"""
|
||||
print("Handling cost operator")
|
||||
ifc_file = tool.Ifc.get()
|
||||
if operator_data["type"] == "getCostSchedules":
|
||||
cost_schedules = ifc_file.by_type("IfcCostSchedule")
|
||||
cost_schedules_json = [cs.get_info(recursive=True) for cs in cost_schedules]
|
||||
currency = tool.Cost.currency()
|
||||
cls.send_webui_data(data={
|
||||
"cost_schedules": cost_schedules_json,
|
||||
"currency": currency
|
||||
}, data_key="cost_schedules", event="cost_schedules")
|
||||
if operator_data["type"] == "loadCostSchedule":
|
||||
cost_schedule = ifc_file.by_id(operator_data["costScheduleId"])
|
||||
bonsai.core.cost.enable_editing_cost_items(tool.Cost, cost_schedule=cost_schedule)
|
||||
json_data = tool.Cost.create_cost_schedule_json(cost_schedule)
|
||||
cls.send_webui_data(data=json_data, data_key="cost_items", event="cost_items")
|
||||
if operator_data["type"] == "addCostItem":
|
||||
bpy.ops.bim.add_cost_item(cost_item=operator_data["costItemId"])
|
||||
cost_schedule = tool.Cost.get_cost_schedule(cost_item=tool.Ifc.get().by_id(operator_data["costItemId"]))
|
||||
json_data = tool.Cost.create_cost_schedule_json(cost_schedule)
|
||||
cls.send_webui_data(data=json_data, data_key="cost_items", event="cost_items")
|
||||
if operator_data["type"] == "selectAssignedElements":
|
||||
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
|
||||
products = tool.Cost.get_cost_item_products(cost_item, is_deep=True)
|
||||
tool.Spatial.select_products(products, unhide=True)
|
||||
if operator_data["type"] == "editCostItemName":
|
||||
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
|
||||
tool.Ifc.run(
|
||||
"cost.edit_cost_item",
|
||||
cost_item=cost_item,
|
||||
attributes = {"Name": operator_data["name"]}
|
||||
)
|
||||
tool.Cost.load_cost_schedule_tree()
|
||||
|
||||
@classmethod
|
||||
def handle_gantt_operator(cls, operator_data: dict) -> None:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user