Partly working
This commit is contained in:
@@ -1,25 +1,50 @@
|
||||
class DashboardController < ApplicationController
|
||||
def index
|
||||
joint_column = 'joint'
|
||||
@available_joints = Joint.distinct.pluck(joint_column)
|
||||
@available_data
|
||||
@records = filter_records
|
||||
require 'yaml'
|
||||
|
||||
class DashboardController < ApplicationController
|
||||
before_action :load_manifest
|
||||
|
||||
def index
|
||||
# Renders the main page
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json { render json: @records }
|
||||
format.json { render json: @manifest }
|
||||
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
|
||||
|
||||
def filter_records
|
||||
# Using .where(joint: params[:joint]) handles both present and nil values gracefully
|
||||
if params[:joint].present?
|
||||
Joint.where(joint: params[:joint])
|
||||
else
|
||||
# Return empty array if no joint selected to avoid loading thousands of records
|
||||
[]
|
||||
end
|
||||
def load_manifest
|
||||
@manifest = YAML.load_file(Rails.root.join('config', 'dashboard_manifest.yml'))
|
||||
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 -->
|
||||
<%= form_with url: dashboard_path, method: :get, id: "filter-form" do |f| %>
|
||||
<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 id="filter-area" style="margin-bottom: 20px; display: flex; gap: 15px; font-family: Arial, sans-serif;">
|
||||
<!-- Dropdowns injected here by JS -->
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<hr>
|
||||
|
||||
<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 id="results-area" style="display: flex; gap: 20px;">
|
||||
<!-- Grids injected here by JS -->
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Main Table Layout */
|
||||
.joint-grid {
|
||||
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 */
|
||||
.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; }
|
||||
|
||||
.text-red { color: #C00000; text-align: left !important; }
|
||||
</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>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const jointSelect = document.getElementById("joint");
|
||||
const resultsContainer = document.getElementById("results-container");
|
||||
let manifest = {};
|
||||
let currentSelections = {};
|
||||
|
||||
function fetchFilteredData() {
|
||||
const joint = jointSelect.value;
|
||||
const params = new URLSearchParams();
|
||||
if (joint) params.append("joint", joint);
|
||||
|
||||
fetch(`/dashboard?${params.toString()}`, {
|
||||
headers: { "Accept": "application/json" }
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
renderJointGrid(data);
|
||||
})
|
||||
.catch(error => console.error("Error fetching dashboard data:", error));
|
||||
async function init() {
|
||||
const resp = await fetch(
|
||||
'/dashboard',
|
||||
{
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
manifest = await resp.json();
|
||||
await renderRootFilters();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function transforms a flat JSON record into the
|
||||
* specific grid layout seen in the image.
|
||||
*/
|
||||
function renderJointGrid(records) {
|
||||
resultsContainer.innerHTML = "";
|
||||
|
||||
if (records.length === 0) {
|
||||
resultsContainer.innerHTML = "<p>No records found. Please select a joint.</p>";
|
||||
return;
|
||||
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));
|
||||
}
|
||||
|
||||
records.forEach(record => {
|
||||
// Create a wrapper for the two tables (Info and Ligaments)
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.style.display = "flex";
|
||||
wrapper.style.gap = "20px";
|
||||
async function createDropdown(config) {
|
||||
const container = document.createElement("div");
|
||||
container.id = `container_${config.id}`;
|
||||
|
||||
// --- TABLE 1: Joint Information ---
|
||||
const infoTable = document.createElement("table");
|
||||
infoTable.className = "joint-grid";
|
||||
const label = document.createElement("label");
|
||||
label.textContent = config.label + ": ";
|
||||
label.style.fontWeight = "bold";
|
||||
|
||||
// Use a template literal to define the exact structure of the image
|
||||
// Note: We map JSON keys (record["Joint"]) to specific cells
|
||||
infoTable.innerHTML = `
|
||||
<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>
|
||||
`;
|
||||
const select = document.createElement("select");
|
||||
select.id = config.id;
|
||||
select.innerHTML = `<option value="">Select...</option>`;
|
||||
|
||||
// --- TABLE 2: Ligaments ---
|
||||
const ligTable = document.createElement("table");
|
||||
ligTable.className = "joint-grid";
|
||||
ligTable.style.width = "250px";
|
||||
// Marked callback as async
|
||||
select.addEventListener("change", async (e) => {
|
||||
const val = e.target.value;
|
||||
currentSelections[config.id] = val;
|
||||
|
||||
// Handle ligaments (assuming they might be a comma-separated string or array)
|
||||
let ligsHtml = "";
|
||||
if (record["Ligaments"]) {
|
||||
const ligArray = Array.isArray(record["Ligaments"]) ? record["Ligaments"] : record["Ligaments"].split(",");
|
||||
ligsHtml = ligArray.map(l => `<div class="text-red">${l.trim()}</div>`).join("");
|
||||
// 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_col == undefined) {
|
||||
params = new URLSearchParams({
|
||||
table: config.table,
|
||||
pluck_column: config.pluck_column,
|
||||
});
|
||||
} 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 = `
|
||||
<tr>
|
||||
<td class="bg-dark-blue" style="width: 100px;">Ligaments</td>
|
||||
<td class="bg-pale-orange" style="text-align: left;">
|
||||
<strong>Vertebral Articulating Processes</strong><br>
|
||||
${ligsHtml}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
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";
|
||||
}
|
||||
|
||||
wrapper.appendChild(infoTable);
|
||||
wrapper.appendChild(ligTable);
|
||||
resultsContainer.appendChild(wrapper);
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
jointSelect.addEventListener("change", fetchFilteredData);
|
||||
fetchFilteredData();
|
||||
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>
|
||||
|
||||
@@ -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 "dashboard#index"
|
||||
get "dashboard", to: "dashboard#index"
|
||||
get "dashboard/options", to: "dashboard#options"
|
||||
get "dashboard/data", to: "dashboard#data"
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user