Partly working
This commit is contained in:
@@ -1,25 +1,50 @@
|
|||||||
class DashboardController < ApplicationController
|
require 'yaml'
|
||||||
def index
|
|
||||||
joint_column = 'joint'
|
|
||||||
@available_joints = Joint.distinct.pluck(joint_column)
|
|
||||||
@available_data
|
|
||||||
@records = filter_records
|
|
||||||
|
|
||||||
|
class DashboardController < ApplicationController
|
||||||
|
before_action :load_manifest
|
||||||
|
|
||||||
|
def index
|
||||||
|
# Renders the main page
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html
|
format.html
|
||||||
format.json { render json: @records }
|
format.json { render json: @manifest }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# GET /dashboard/options?table=Joint&filter_col=joint_id&value=1
|
||||||
|
def options
|
||||||
|
table = params[:table].constantize
|
||||||
|
if params[:filter_col].present?
|
||||||
|
options = table.where(params[:filter_col] => params[:value]).pluck(params[:pluck_column])
|
||||||
|
else
|
||||||
|
puts table
|
||||||
|
options = table.distinct.pluck(params[:pluck_column])
|
||||||
|
end
|
||||||
|
render json: options
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /dashboard/data?joint=Acromioclavicular&sub_joint=X
|
||||||
|
def data
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
# Use the data_sources from YAML to aggregate data
|
||||||
|
@manifest['data_sources'].each do |source|
|
||||||
|
table = source['table'].constantize
|
||||||
|
# Find the filter value from the params that matches the filter_column
|
||||||
|
# We assume the filter_column name in DB matches the filter ID in YAML
|
||||||
|
filter_val = params[source['filter_column']] || params['joint'] # Fallback to joint
|
||||||
|
|
||||||
|
record = table.find_by(source['filter_column'] => filter_val)
|
||||||
|
results[source['key']] = record ? record.attributes : {}
|
||||||
|
end
|
||||||
|
|
||||||
|
#debugger
|
||||||
|
render json: results
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def filter_records
|
def load_manifest
|
||||||
# Using .where(joint: params[:joint]) handles both present and nil values gracefully
|
@manifest = YAML.load_file(Rails.root.join('config', 'dashboard_manifest.yml'))
|
||||||
if params[:joint].present?
|
|
||||||
Joint.where(joint: params[:joint])
|
|
||||||
else
|
|
||||||
# Return empty array if no joint selected to avoid loading thousands of records
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+359
-131
@@ -1,164 +1,392 @@
|
|||||||
<h1 style="font-family: Arial, sans-serif;">Excel Replacement Dashboard</h1>
|
<h1 style="font-family: Arial, sans-serif;">Dynamic Dashboard</h1>
|
||||||
|
|
||||||
<!-- Dropdown Filters -->
|
<div id="filter-area" style="margin-bottom: 20px; display: flex; gap: 15px; font-family: Arial, sans-serif;">
|
||||||
<%= form_with url: dashboard_path, method: :get, id: "filter-form" do |f| %>
|
<!-- Dropdowns injected here by JS -->
|
||||||
<div style="margin-bottom: 20px; font-family: Arial, sans-serif;">
|
|
||||||
<%= f.label :joint, "Select Joint:", style: "font-weight: bold;" %>
|
|
||||||
<%= f.select :joint, options_for_select(@available_joints, params[:joint]), include_blank: "Select a joint", id: "joint" %>
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<hr>
|
<div id="results-area" style="display: flex; gap: 20px;">
|
||||||
|
<!-- Grids injected here by JS -->
|
||||||
<h2 style="font-family: Arial, sans-serif;">Dashboard Results</h2>
|
|
||||||
|
|
||||||
<!-- Container where the formatted "Excel-style" grids will be injected -->
|
|
||||||
<div id="results-container" style="display: flex; flex-wrap: wrap; gap: 30px;">
|
|
||||||
<!-- JS will inject the tables here -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Main Table Layout */
|
.dyn-grid { border-collapse: collapse; border: 2px solid #000; font-family: Arial; font-size: 12px; }
|
||||||
.joint-grid {
|
.dyn-grid td { border: 1px solid #000; padding: 4px 8px; text-align: center; }
|
||||||
border-collapse: collapse;
|
|
||||||
border: 2px solid #000;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-size: 12px;
|
|
||||||
table-layout: fixed;
|
|
||||||
width: 600px;
|
|
||||||
}
|
|
||||||
.joint-grid td, .joint-grid th {
|
|
||||||
border: 1px solid #000;
|
|
||||||
padding: 4px 8px;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Color Palette from Image */
|
|
||||||
.bg-dark-blue { background-color: #002060; color: white; font-weight: bold; }
|
.bg-dark-blue { background-color: #002060; color: white; font-weight: bold; }
|
||||||
.bg-light-blue { background-color: #D9E1F2; font-weight: bold; }
|
.bg-light-blue { background-color: #D9E1F2; font-weight: bold; }
|
||||||
.bg-pale-yellow { background-color: #FFFFCC; }
|
.bg-pale-yellow { background-color: #FFFFCC; }
|
||||||
.bg-pale-green { background-color: #E2EFDA; }
|
.bg-pale-green { background-color: #E2EFDA; }
|
||||||
.bg-pale-orange { background-color: #FCE4D6; }
|
.bg-pale-orange { background-color: #FCE4D6; }
|
||||||
|
|
||||||
.text-red { color: #C00000; text-align: left !important; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- <script> -->
|
||||||
|
<!-- document.addEventListener("DOMContentLoaded", () => { -->
|
||||||
|
<!-- let manifest = {}; -->
|
||||||
|
<!-- let currentSelections = {}; -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- async function init() { -->
|
||||||
|
<!-- const resp = await fetch('/dashboard'); -->
|
||||||
|
<!-- manifest = await resp.json(); -->
|
||||||
|
<!-- renderRootFilters(); -->
|
||||||
|
<!-- } -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- function renderRootFilters() { -->
|
||||||
|
<!-- const rootFilters = manifest.filters.filter(f => f.depends_on === null); -->
|
||||||
|
<!-- rootFilters.forEach(f => createDropdown(f)); -->
|
||||||
|
<!-- } -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- 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>`; -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- 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 -->
|
||||||
|
<!-- const children = manifest.filters.filter(f => f.depends_on === config.id); -->
|
||||||
|
<!-- children.forEach(child => { -->
|
||||||
|
<!-- 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); -->
|
||||||
|
<!-- } -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- async function fetchOptions(config, value) { -->
|
||||||
|
<!-- const params = new URLSearchParams({ -->
|
||||||
|
<!-- table: config.table, -->
|
||||||
|
<!-- pluck_column: config.pluck_column, -->
|
||||||
|
<!-- filter_col: 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; -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- select.addEventListener("change", async (e) => { -->
|
||||||
|
<!-- currentSelections[id] = e.target.value; -->
|
||||||
|
<!-- clearDependents(id); -->
|
||||||
|
<!-- const children = manifest.filters.filter(f => f.depends_on === id); -->
|
||||||
|
<!-- children.forEach(child => { -->
|
||||||
|
<!-- 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"; -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- // Find max rows/cols to initialize table structure if needed, -->
|
||||||
|
<!-- // but we can just build rows dynamically. -->
|
||||||
|
<!-- 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> -->
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const jointSelect = document.getElementById("joint");
|
let manifest = {};
|
||||||
const resultsContainer = document.getElementById("results-container");
|
let currentSelections = {};
|
||||||
|
|
||||||
function fetchFilteredData() {
|
async function init() {
|
||||||
const joint = jointSelect.value;
|
const resp = await fetch(
|
||||||
const params = new URLSearchParams();
|
'/dashboard',
|
||||||
if (joint) params.append("joint", joint);
|
{
|
||||||
|
headers: {
|
||||||
fetch(`/dashboard?${params.toString()}`, {
|
'Accept': 'application/json'
|
||||||
headers: { "Accept": "application/json" }
|
}
|
||||||
})
|
});
|
||||||
.then(response => response.json())
|
manifest = await resp.json();
|
||||||
.then(data => {
|
await renderRootFilters();
|
||||||
renderJointGrid(data);
|
|
||||||
})
|
|
||||||
.catch(error => console.error("Error fetching dashboard data:", error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async function renderRootFilters() {
|
||||||
* This function transforms a flat JSON record into the
|
const rootFilters = manifest.filters.filter(f => f.depends_on === null);
|
||||||
* specific grid layout seen in the image.
|
for (const f of rootFilters) {
|
||||||
*/
|
await createDropdown(f);
|
||||||
function renderJointGrid(records) {
|
}
|
||||||
resultsContainer.innerHTML = "";
|
//rootFilters.forEach(f => await createDropdown(f));
|
||||||
|
|
||||||
if (records.length === 0) {
|
|
||||||
resultsContainer.innerHTML = "<p>No records found. Please select a joint.</p>";
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
records.forEach(record => {
|
async function createDropdown(config) {
|
||||||
// Create a wrapper for the two tables (Info and Ligaments)
|
const container = document.createElement("div");
|
||||||
const wrapper = document.createElement("div");
|
container.id = `container_${config.id}`;
|
||||||
wrapper.style.display = "flex";
|
|
||||||
wrapper.style.gap = "20px";
|
|
||||||
|
|
||||||
// --- TABLE 1: Joint Information ---
|
const label = document.createElement("label");
|
||||||
const infoTable = document.createElement("table");
|
label.textContent = config.label + ": ";
|
||||||
infoTable.className = "joint-grid";
|
label.style.fontWeight = "bold";
|
||||||
|
|
||||||
// Use a template literal to define the exact structure of the image
|
const select = document.createElement("select");
|
||||||
// Note: We map JSON keys (record["Joint"]) to specific cells
|
select.id = config.id;
|
||||||
infoTable.innerHTML = `
|
select.innerHTML = `<option value="">Select...</option>`;
|
||||||
<thead>
|
|
||||||
<tr><th colspan="4" class="bg-dark-blue">Joint Information</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="bg-light-blue">Anatomical Name</td>
|
|
||||||
<td class="bg-pale-yellow">${record["Joint"] || ""}</td>
|
|
||||||
<td class="bg-light-blue">Common Name</td>
|
|
||||||
<td class="bg-pale-yellow">${record["Common Name"] || ""}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="bg-light-blue">Diarthrodial Joint Type</td>
|
|
||||||
<td class="bg-pale-yellow">${record["Diarthrodial joint"] || ""}</td>
|
|
||||||
<td class="bg-light-blue">Planes of Freedom</td>
|
|
||||||
<td class="bg-pale-yellow">${record["# of Plains"] || ""}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="bg-pale-green">${record["Alternate Name"] || ""}</td>
|
|
||||||
<td class="bg-pale-green">Ball & Socket</td>
|
|
||||||
<td class="bg-pale-green">${record["# of Plains"] || ""}</td>
|
|
||||||
<td class="bg-pale-green">${record["# of Plains"] || ""}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="bg-light-blue">Arthrokinematic Rule</td>
|
|
||||||
<td class="bg-pale-orange">Osteokinematic</td>
|
|
||||||
<td class="bg-light-blue">Open Chain Roll/Glide</td>
|
|
||||||
<td class="bg-pale-orange">Anterior/Posterior</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="bg-light-blue">Open Pack</td>
|
|
||||||
<td colspan="2" class="bg-pale-green">${record["Open Pack"] || "N/A"}</td>
|
|
||||||
<td class="bg-light-blue">Close Pack</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="4" class="bg-pale-green">${record["Close Pack"] || "N/A"}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// --- TABLE 2: Ligaments ---
|
// Marked callback as async
|
||||||
const ligTable = document.createElement("table");
|
select.addEventListener("change", async (e) => {
|
||||||
ligTable.className = "joint-grid";
|
const val = e.target.value;
|
||||||
ligTable.style.width = "250px";
|
currentSelections[config.id] = val;
|
||||||
|
|
||||||
// Handle ligaments (assuming they might be a comma-separated string or array)
|
// 1. Clear dependent filters and data
|
||||||
let ligsHtml = "";
|
clearDependents(config.id);
|
||||||
if (record["Ligaments"]) {
|
|
||||||
const ligArray = Array.isArray(record["Ligaments"]) ? record["Ligaments"] : record["Ligaments"].split(",");
|
// 2. Load options for children (Fixed with for...of)
|
||||||
ligsHtml = ligArray.map(l => `<div class="text-red">${l.trim()}</div>`).join("");
|
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_col == undefined) {
|
||||||
|
params = new URLSearchParams({
|
||||||
|
table: config.table,
|
||||||
|
pluck_column: config.pluck_column,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
ligsHtml = "No ligaments listed";
|
params = new URLSearchParams({
|
||||||
|
table: config.table,
|
||||||
|
pluck_column: config.pluck_column,
|
||||||
|
filter_col: config.filter_column,
|
||||||
|
value: value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const resp = await fetch(`/dashboard/options?${params.toString()}`);
|
||||||
|
return await resp.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
ligTable.innerHTML = `
|
function populateDropdown(id, options) {
|
||||||
<tr>
|
const select = document.getElementById(id) || createMissingDropdown(id);
|
||||||
<td class="bg-dark-blue" style="width: 100px;">Ligaments</td>
|
select.innerHTML = `<option value="">Select...</option>`;
|
||||||
<td class="bg-pale-orange" style="text-align: left;">
|
options.forEach(opt => {
|
||||||
<strong>Vertebral Articulating Processes</strong><br>
|
select.innerHTML += `<option value="${opt}">${opt}</option>`;
|
||||||
${ligsHtml}
|
});
|
||||||
</td>
|
// If this is the last dropdown, it should be visible now
|
||||||
</tr>
|
const container = document.getElementById(`container_${id}`);
|
||||||
`;
|
if(container) container.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
wrapper.appendChild(infoTable);
|
function createMissingDropdown(id) {
|
||||||
wrapper.appendChild(ligTable);
|
const config = manifest.filters.find(f => f.id === id);
|
||||||
resultsContainer.appendChild(wrapper);
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
jointSelect.addEventListener("change", fetchFilteredData);
|
async function updateGrid() {
|
||||||
fetchFilteredData();
|
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>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Filter Hierarchy: Defines the dropdowns and their dependencies
|
||||||
|
filters:
|
||||||
|
- id: "joint"
|
||||||
|
label: "Select Joint"
|
||||||
|
table: "Joint"
|
||||||
|
pluck_column: "Joint"
|
||||||
|
depends_on: null # Root level
|
||||||
|
# - id: "joint_info"
|
||||||
|
# label: "joint_info"
|
||||||
|
# table: "joint_info" # A separate table in SQLite
|
||||||
|
# pluck_column: "Common Joint Name"
|
||||||
|
# depends_on: "joint" # Only appears after 'joint' is selected
|
||||||
|
# filter_column: "Common Joint Name" # Filter SubJoint where joint_id == selected joint value
|
||||||
|
|
||||||
|
# Data Mapping: Defines which tables to hit for the final grid
|
||||||
|
data_sources:
|
||||||
|
- table: "Joint"
|
||||||
|
key: "main_info" # Nested JSON key
|
||||||
|
filter_column: "joint"
|
||||||
|
# - table: "Ligament"
|
||||||
|
# key: "lig_info"
|
||||||
|
# filter_column: "joint_id"
|
||||||
|
|
||||||
|
# Layout: Defines the visual grid
|
||||||
|
# colors: dark_blue, light_blue, pale_yellow, pale_green, pale_orange
|
||||||
|
layout:
|
||||||
|
- { row: 0, col: 0, colspan: 4, rowspan: 1, text: "Joint Information", class: "bg-dark-blue" }
|
||||||
|
- { row: 1, col: 0, colspan: 1, rowspan: 1, text: "Anatomical Name", class: "bg-light-blue" }
|
||||||
|
- { row: 1, col: 1, colspan: 1, rowspan: 1, value: "main_info.joint", class: "bg-pale-yellow" }
|
||||||
|
- { row: 1, col: 2, colspan: 1, rowspan: 1, text: "Common Name", class: "bg-light-blue" }
|
||||||
|
- { row: 1, col: 3, colspan: 1, rowspan: 1, value: "main_info.common_name", class: "bg-pale-yellow" }
|
||||||
|
- { row: 2, col: 0, colspan: 1, rowspan: 1, text: "Joint Type", class: "bg-light-blue" }
|
||||||
|
- { row: 2, col: 1, colspan: 1, rowspan: 1, value: "main_info.joint_type", class: "bg-pale-yellow" }
|
||||||
|
- { row: 2, col: 2, colspan: 1, rowspan: 1, text: "Planes", class: "bg-light-blue" }
|
||||||
|
- { row: 2, col: 3, colspan: 1, rowspan: 1, value: "main_info.planes", class: "bg-pale-yellow" }
|
||||||
|
- { row: 3, col: 0, colspan: 4, rowspan: 1, value: "main_info.notes", class: "bg-pale-green" }
|
||||||
|
|
||||||
|
# Ligaments Table (separate grid)
|
||||||
|
#- { grid: "ligaments", row: 0, col: 0, colspan: 1, rowspan: 1, text: "Ligaments", class: "bg-dark-blue" }
|
||||||
|
#- { grid: "ligaments", row: 0, col: 1, colspan: 1, rowspan: 1, value: ".list", class: "bg-pale-orange" }
|
||||||
@@ -13,4 +13,6 @@ Rails.application.routes.draw do
|
|||||||
# root "posts#index"
|
# root "posts#index"
|
||||||
root "dashboard#index"
|
root "dashboard#index"
|
||||||
get "dashboard", to: "dashboard#index"
|
get "dashboard", to: "dashboard#index"
|
||||||
|
get "dashboard/options", to: "dashboard#options"
|
||||||
|
get "dashboard/data", to: "dashboard#data"
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user