220 lines
7.0 KiB
Plaintext
220 lines
7.0 KiB
Plaintext
<h1 style="font-family: Arial, sans-serif;">Dynamic Dashboard</h1>
|
|
|
|
<div id="filter-area" style="margin-bottom: 20px; display: flex; gap: 15px; font-family: Arial, sans-serif;">
|
|
<!-- Dropdowns injected here by JS -->
|
|
</div>
|
|
|
|
<div id="results-area" style="display: flex; gap: 20px;">
|
|
<!-- Grids injected here by JS -->
|
|
</div>
|
|
|
|
<style>
|
|
.dyn-grid { border-collapse: collapse; border: 2px solid #000; font-family: Arial; font-size: 12px; }
|
|
.dyn-grid td { border: 1px solid #000; padding: 4px 8px; text-align: center; }
|
|
.bg-dark-blue { background-color: #002060; color: white; font-weight: bold; }
|
|
.bg-light-blue { background-color: #D9E1F2; font-weight: bold; }
|
|
.bg-pale-yellow { background-color: #FFFFCC; }
|
|
.bg-pale-green { background-color: #E2EFDA; }
|
|
.bg-pale-orange { background-color: #FCE4D6; }
|
|
</style>
|
|
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
let manifest = {};
|
|
let currentSelections = {};
|
|
|
|
async function init() {
|
|
const resp = await fetch(
|
|
'/dashboard',
|
|
{
|
|
headers: {
|
|
'Accept': 'application/json'
|
|
}
|
|
});
|
|
manifest = await resp.json();
|
|
await renderRootFilters();
|
|
}
|
|
|
|
async function renderRootFilters() {
|
|
const rootFilters = manifest.filters.filter(f => f.depends_on === null);
|
|
for (const f of rootFilters) {
|
|
await createDropdown(f);
|
|
}
|
|
//rootFilters.forEach(f => await createDropdown(f));
|
|
}
|
|
|
|
async function createDropdown(config) {
|
|
const container = document.createElement("div");
|
|
container.id = `container_${config.id}`;
|
|
|
|
const label = document.createElement("label");
|
|
label.textContent = config.label + ": ";
|
|
label.style.fontWeight = "bold";
|
|
|
|
const select = document.createElement("select");
|
|
select.id = config.id;
|
|
select.innerHTML = `<option value="">Select...</option>`;
|
|
|
|
// Marked callback as async
|
|
select.addEventListener("change", async (e) => {
|
|
const val = e.target.value;
|
|
currentSelections[config.id] = val;
|
|
|
|
// 1. Clear dependent filters and data
|
|
clearDependents(config.id);
|
|
|
|
// 2. Load options for children (Fixed with for...of)
|
|
const children = manifest.filters.filter(f => f.depends_on === config.id);
|
|
for (const child of children) {
|
|
const options = await fetchOptions(child, val);
|
|
populateDropdown(child.id, options);
|
|
}
|
|
|
|
// 3. Update data grid
|
|
updateGrid();
|
|
});
|
|
|
|
container.appendChild(label);
|
|
container.appendChild(select);
|
|
document.getElementById("filter-area").appendChild(container);
|
|
// Special handling for parent items:
|
|
if (config.depends_on === null) {
|
|
var options = await fetchOptions(config);
|
|
populateDropdown(select.id, options)
|
|
}
|
|
}
|
|
|
|
async function fetchOptions(config, value) {
|
|
var params
|
|
if (config.filter_column == undefined) {
|
|
console.log(config + "Undefined filter col");
|
|
params = new URLSearchParams({
|
|
table: config.table,
|
|
pluck_column: config.pluck_column,
|
|
});
|
|
} else {
|
|
params = new URLSearchParams({
|
|
table: config.table,
|
|
pluck_column: config.pluck_column,
|
|
filter_column: config.filter_column,
|
|
value: value
|
|
});
|
|
}
|
|
const resp = await fetch(`/dashboard/options?${params.toString()}`);
|
|
return await resp.json();
|
|
}
|
|
|
|
function populateDropdown(id, options) {
|
|
const select = document.getElementById(id) || createMissingDropdown(id);
|
|
select.innerHTML = `<option value="">Select...</option>`;
|
|
options.forEach(opt => {
|
|
select.innerHTML += `<option value="${opt}">${opt}</option>`;
|
|
});
|
|
// If this is the last dropdown, it should be visible now
|
|
const container = document.getElementById(`container_${id}`);
|
|
if(container) container.style.display = "block";
|
|
}
|
|
|
|
function createMissingDropdown(id) {
|
|
const config = manifest.filters.find(f => f.id === id);
|
|
const container = document.createElement("div");
|
|
container.id = `container_${id}`;
|
|
container.style.display = "none"; // Hidden until parent is selected
|
|
|
|
const label = document.createElement("label");
|
|
label.textContent = config.label + ": ";
|
|
|
|
const select = document.createElement("select");
|
|
select.id = id;
|
|
|
|
// Marked callback as async
|
|
select.addEventListener("change", async (e) => {
|
|
currentSelections[id] = e.target.value;
|
|
clearDependents(id);
|
|
const children = manifest.filters.filter(f => f.depends_on === id);
|
|
|
|
// Fixed with for...of loop
|
|
for (const child of children) {
|
|
const opts = await fetchOptions(child, e.target.value);
|
|
populateDropdown(child.id, opts);
|
|
}
|
|
updateGrid();
|
|
});
|
|
|
|
container.appendChild(label);
|
|
container.appendChild(select);
|
|
document.getElementById("filter-area").appendChild(container);
|
|
return select;
|
|
}
|
|
|
|
function clearDependents(id) {
|
|
// Recursively find all filters that depend on this ID and reset them
|
|
manifest.filters.forEach(f => {
|
|
if (f.depends_on === id) {
|
|
const select = document.getElementById(f.id);
|
|
if (select) select.innerHTML = `<option value="">Select...</option>`;
|
|
const container = document.getElementById(`container_${f.id}`);
|
|
if (container) container.style.display = "none";
|
|
delete currentSelections[f.id];
|
|
clearDependents(f.id);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function updateGrid() {
|
|
const params = new URLSearchParams(currentSelections);
|
|
const resp = await fetch(`/dashboard/data?${params.toString()}`);
|
|
const data = await resp.json();
|
|
renderLayout(data);
|
|
}
|
|
|
|
function renderLayout(data) {
|
|
const area = document.getElementById("results-area");
|
|
area.innerHTML = "";
|
|
|
|
// Group layout by grid name (defaults to 'main')
|
|
const grids = {};
|
|
manifest.layout.forEach(item => {
|
|
const gridId = item.grid || 'main';
|
|
if (!grids[gridId]) grids[gridId] = [];
|
|
grids[gridId].push(item);
|
|
});
|
|
|
|
Object.keys(grids).forEach(gridId => {
|
|
const table = document.createElement("table");
|
|
table.className = "dyn-grid";
|
|
|
|
const layoutItems = grids[gridId];
|
|
const maxRow = Math.max(...layoutItems.map(i => i.row));
|
|
|
|
for (let r = 0; r <= maxRow; r++) {
|
|
const tr = document.createElement("tr");
|
|
const rowItems = layoutItems.filter(i => i.row === r);
|
|
|
|
rowItems.forEach(item => {
|
|
const td = document.createElement("td");
|
|
td.className = item.class;
|
|
td.colSpan = item.colspan || 1;
|
|
td.rowSpan = item.rowspan || 1;
|
|
|
|
if (item.text) {
|
|
td.textContent = item.text;
|
|
} else if (item.value) {
|
|
// Resolve nested JSON path (e.g., "main_info.joint")
|
|
const parts = item.value.split('.');
|
|
const val = parts.reduce((obj, key) => (obj && obj[key] !== undefined) ? obj[key] : '', data);
|
|
td.textContent = val;
|
|
}
|
|
tr.appendChild(td);
|
|
});
|
|
table.appendChild(tr);
|
|
}
|
|
area.appendChild(table);
|
|
});
|
|
}
|
|
|
|
init();
|
|
});
|
|
</script>
|