// Dynamically include fragments async function includeFragment(id, url) { const res = await fetch(url); const html = await res.text(); document.getElementById(id).innerHTML = html; } // Load a markdown file into #page async function loadMarkdown(page) { const mdPath = `/md/${page}.md`; try { const response = await fetch(mdPath); const hasMarkdown = response.ok; const mdText = hasMarkdown ? await response.text() : ""; // Load nav metadata (top + sidepanel) const [navRes, sideNavRes] = await Promise.all([ fetch('/api/nav'), fetch('/api/nav/sidepanel') ]); const navItems = await navRes.json(); const sideNav = await sideNavRes.json(); // ---- Locate metadata (nav.json first, then nav-sidepanel.json) ---- let meta = null; // 1) nav.json — brand meta = navItems.find(item => item.brand === true && item.page === page); // 2) nav.json — top-level if (!meta) { meta = navItems.find(item => item.page === page); } // 3) nav.json — children if (!meta) { for (const item of navItems) { if (Array.isArray(item.children)) { const child = item.children.find(c => c.page === page); if (child) { meta = child; break; } } } } // 4) nav-sidepanel.json — section headers if (!meta) { for (const section of Object.values(sideNav)) { if (section.page === page) { meta = section; break; } } } // 5) nav-sidepanel.json — section items if (!meta) { for (const section of Object.values(sideNav)) { if (Array.isArray(section.items)) { const item = section.items.find(i => i.page === page); if (item) { meta = item; break; } } } } // Fallback if (!meta) { meta = { title: page, icon: "" }; } // Fallback title if still not found if (!meta) { meta = { title: page, icon: "" }; } // ---- Build header HTML (NOT passed to marked) ---- const headerHtml = `
Error loading Markdown: ${err.message}`;
}
}
// Dynamically load a JS file if it exists
async function loadPageScript(page) {
const jsPath = `/js/${page}.js`; // ✅ declare properly
try {
// HEAD request checks whether the file exists without downloading it
const res = await fetch(jsPath, { method: 'HEAD' });
if (!res.ok) {
console.warn(`Script not found: ${jsPath}`);
return;
}
// Dynamically inject script into the DOM
const script = document.createElement('script');
script.type = 'module';
script.src = jsPath;
script.defer = true;
document.body.appendChild(script);
console.log(`Loading script: ${jsPath}`);
} catch (err) {
console.warn(`Error loading script ${jsPath}: ${err.message}`);
}
}
// Load Nav
function loadNav() {
fetch('/api/nav')
.then(res => res.json())
.then(navItems => {
const navContainer = document.getElementById('nav');
if (!navContainer) return;
const urlParams = new URLSearchParams(window.location.search);
const currentPage = urlParams.get('page') || 'index';
const enabledItems = navItems.filter(item => item.enabled !== false);
// --------- FIND BRAND ITEM ----------
const brandItem = enabledItems.find(x => x.brand === true);
// --------- CREATE NAV STRUCTURE ----------
const navEl = document.createElement('nav');
navEl.className = 'navbar navbar-expand-lg bg-body-tertiary';
const container = document.createElement('div');
container.className = 'container-fluid';
// BRAND
if (brandItem) {
const brand = document.createElement('a');
brand.className = 'navbar-brand';
brand.href = `index.html?page=${brandItem.page}`;
brand.innerHTML = `${brandItem.title}`;
container.appendChild(brand);
}
// TOGGLER BUTTON
const toggler = document.createElement('button');
toggler.className = 'navbar-toggler';
toggler.type = 'button';
toggler.setAttribute('data-bs-toggle', 'collapse');
toggler.setAttribute('data-bs-target', '#navbarNavDropdown');
toggler.setAttribute('aria-controls', 'navbarNavDropdown');
toggler.setAttribute('aria-expanded', 'false');
toggler.setAttribute('aria-label', 'Toggle navigation');
toggler.innerHTML = '';
container.appendChild(toggler);
// COLLAPSE REGION
const collapse = document.createElement('div');
collapse.className = 'collapse navbar-collapse';
collapse.id = 'navbarNavDropdown';
const ul = document.createElement('ul');
ul.className = 'navbar-nav me-auto mb-2 mb-lg-0';
enabledItems.forEach(item => {
// skip the brand from the menu
if (item.brand === true) return;
const hasChildren = Array.isArray(item.children) && item.children.length > 0;
const li = document.createElement('li');
li.className = hasChildren ? 'nav-item dropdown' : 'nav-item';
// --- NO CHILDREN: NORMAL NAV LINK ---
if (!hasChildren) {
const a = document.createElement('a');
let cls = 'nav-link';
if (item.page === currentPage) cls += ' active';
a.className = cls;
a.href = `index.html?page=${item.page}`;
a.innerHTML = `${item.title}`;
li.appendChild(a);
ul.appendChild(li);
return;
}
// --- HAS CHILDREN: DROPDOWN TOGGLE ---
const toggle = document.createElement('a');
toggle.className = 'nav-link dropdown-toggle';
toggle.href = '#';
toggle.role = 'button';
toggle.setAttribute('data-bs-toggle', 'dropdown');
toggle.setAttribute('aria-expanded', 'false');
toggle.innerHTML = `${item.title}`;
const menu = document.createElement('ul');
menu.className = 'dropdown-menu';
item.children
.filter(child => child.enabled !== false)
.forEach(child => {
const childLi = document.createElement('li');
const childA = document.createElement('a');
let childClass = 'dropdown-item';
if (child.page === currentPage) {
childClass += ' active';
toggle.classList.add('active'); // highlight parent
}
childA.className = childClass;
childA.href = `index.html?page=${child.page}`;
childA.innerHTML =
`${child.icon ? `` : ''}${child.title}`;
childLi.appendChild(childA);
menu.appendChild(childLi);
});
li.appendChild(toggle);
li.appendChild(menu);
ul.appendChild(li);
});
collapse.appendChild(ul);
container.appendChild(collapse);
navEl.appendChild(container);
navContainer.innerHTML = '';
navContainer.appendChild(navEl);
// --------- MANUAL DROPDOWN HANDLERS ---------
const dropdownToggles = navContainer.querySelectorAll('.nav-item.dropdown > .dropdown-toggle');
dropdownToggles.forEach(toggle => {
toggle.addEventListener('click', evt => {
evt.preventDefault();
const parentLi = toggle.parentElement;
const menu = parentLi.querySelector('.dropdown-menu');
const isShown = menu.classList.contains('show');
// close all others
navContainer.querySelectorAll('.dropdown-menu.show').forEach(openMenu => {
openMenu.classList.remove('show');
});
// toggle current
if (!isShown) {
menu.classList.add('show');
}
});
});
// close dropdown when clicking outside nav
document.addEventListener('click', evt => {
if (!navContainer.contains(evt.target)) {
navContainer.querySelectorAll('.dropdown-menu.show').forEach(openMenu => {
openMenu.classList.remove('show');
});
}
});
})
.catch(err => {
console.error('Failed to load nav:', err);
const navContainer = document.getElementById('nav');
if (navContainer) {
navContainer.innerHTML =
'Failed to load navigation menu.
'; } }); } function loadNavTabs() { fetch('/api/nav') .then(res => res.json()) .then(navItems => { const ul = document.createElement('ul'); ul.className = 'nav nav-tabs'; const urlParams = new URLSearchParams(window.location.search); const currentPage = urlParams.get('page') || 'index'; // Filter out disabled items const enabledItems = navItems.filter(item => item.enabled !== false); enabledItems.forEach(item => { const li = document.createElement('li'); li.className = 'nav-item'; const a = document.createElement('a'); a.className = 'nav-link'; a.href = `index.html?page=${item.page}`; a.innerHTML = `${item.title}`; // Apply 'active' class to the current tab if (currentPage === item.page) { a.classList.add('active'); a.setAttribute('aria-current', 'page'); } // Optional: Support for disabled nav items in nav.json if (item.disabled) { a.classList.add('disabled'); a.setAttribute('aria-disabled', 'true'); } li.appendChild(a); ul.appendChild(li); }); const navContainer = document.getElementById('nav'); if (navContainer) { navContainer.innerHTML = ''; // Clear previous content navContainer.appendChild(ul); } }) .catch(err => { console.error("Failed to load nav:", err); document.getElementById('nav').innerHTML = 'Failed to load navigation menu.
'; }); } // =============================== // Side Panel (layout-collapsing) // =============================== async function loadSidePanel() { // console.log('DEBUG: loadSidePanel() called'); const panel = document.getElementById('sidepanel'); const inner = document.getElementById('sidepanel-inner'); const toggleBtn = document.getElementById('sidepanel-toggle'); if (!panel || !inner || !toggleBtn) return; const currentPage = new URLSearchParams(window.location.search).get('page') || 'index'; const isCollapsed = localStorage.getItem('sidepanel_collapsed') === 'true'; if (isCollapsed) { panel.classList.add('collapsed'); toggleBtn.innerHTML = ``; } else { toggleBtn.innerHTML = ``; } try { const res = await fetch('/api/nav/sidepanel'); if (!res.ok) throw new Error(`HTTP ${res.status}`); const sideNav = await res.json(); renderAllSidePanelSections(inner, sideNav, currentPage); } catch (err) { console.error('Failed to load side panel:', err); inner.innerHTML = ''; } // Collapse / expand behavior toggleBtn.onclick = () => { const collapsed = panel.classList.toggle('collapsed'); localStorage.setItem('sidepanel_collapsed', collapsed); toggleBtn.innerHTML = ` `; }; } // ------------------------------- // Find matching section by page // ------------------------------- function findSidePanelSection(sideNav, page) { for (const key in sideNav) { const section = sideNav[key]; if (!section || section.enabled === false) continue; if (Array.isArray(section.items)) { const hit = section.items.find( item => item.enabled !== false && item.page === page ); if (hit) return section; } } return null; } // ------------------------------- // Render side panel content // ------------------------------- function renderAllSidePanelSections(container, sideNav, currentPage) { container.innerHTML = ''; Object.values(sideNav) .filter(section => section.enabled !== false) .forEach(section => { // ---- Section Header (linkable if page exists) ---- const header = document.createElement( section.page ? 'a' : 'div' ); if (section.desc) { header.title = section.desc; } header.className = 'fw-bold px-3 pt-3 pb-2 small nav-link'; // 'fw-bold px-3 pt-3 pb-2 text-uppercase small nav-link'; if (section.page) { header.href = `index.html?page=${section.page}`; if (section.page === currentPage) { header.classList.add('active'); } } header.innerHTML = ` ${section.icon ? `` : ''} ${section.title} `; container.appendChild(header); // ---- Section Items (if any) ---- if (Array.isArray(section.items) && section.items.length > 0) { const ul = document.createElement('ul'); ul.className = 'nav flex-column px-2'; section.items .filter(item => item.enabled !== false) .forEach(item => { const li = document.createElement('li'); li.className = 'nav-item'; const a = document.createElement('a'); a.className = 'nav-link py-2'; if (item.page === currentPage) { a.classList.add('active'); } if (item.desc) { a.title = item.desc; } a.href = `index.html?page=${item.page}`; a.innerHTML = ` ${item.icon ? `` : ''} ${item.title} `; li.appendChild(a); ul.appendChild(li); }); container.appendChild(ul); } }); } // Load Footer function loadFooter() { // Load API Config JSON from /api/config fetch('/api/config') .then(res => res.json()) .then(config => { let html = ''; html += '\n'; // document.getElementById('app-description').innerHTML = config.package.description; document.getElementById('footer').innerHTML = html; }) .catch(err => { console.error('Error loading config:', err); document.getElementById('footer').innerHTML = ''; }); } export function genBootStrapTable(json_obj, tblsize='1') { // json_obj structure // [ // [ // "value1", // "value2", // "value3" // ], // [ // "value1", // "value2", // "value3" // ] // ] // console.log("json_obj: ", JSON.stringify(json_obj, null, 2)); // console.log("tblsize: ", tblsize); // A basic table with rows of content and no headers. let html = ''; html += '| ${json_obj[row][col]} | \n`; } html += `
No results found
\n"; return table_html; } else { table_html += "| ${cols[col]} | \n`; // console.log(`cols[col]: ${cols[col]}`); table_html += `${col_disp_map[cols[col]]} | \n`; } table_html += "||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| ${json_obj[row][cols[col]]} | \n`; break; case 'balance': { const val = Number(json_obj[row][cols[col]]); if (!isNaN(val)) { const formatted = val.toLocaleString('en-US', { style: 'currency', currency: 'USD' }); table_html += `${formatted} | \n`; } else { table_html += `${json_obj[row][cols[col]]} | \n`; } break; } case 'trans_amount': { const val = Number(json_obj[row][cols[col]]); if (!isNaN(val)) { const formatted = val.toLocaleString('en-US', { style: 'currency', currency: 'USD' }); table_html += `${formatted} | \n`; } else { table_html += `${json_obj[row][cols[col]]} | \n`; } break; } case 'trans_id': // Fixed width, monospace for alignment table_html += `${json_obj[row][cols[col]]} | \n`; // table_html += `${json_obj[row][cols[col]]} | \n`; break; case 'trans_date': // Fixed width, monospace for alignment table_html += `${json_obj[row][cols[col]]} | \n`; // table_html += `${json_obj[row][cols[col]]} | \n`; break; case 'notes': table_html += `${json_obj[row][cols[col]]} | \n`; break; default: table_html += `${json_obj[row][cols[col]]} | \n`; break; } // table_html += `${json_obj[row][cols[col]]} | \n`; } table_html += "