Dynamic Dashboard with Widget Library
This commit is contained in:
@@ -352,6 +352,12 @@ function initialize() {
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dashboard_layouts (
|
||||
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
layout_json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS app_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const express = require('express');
|
||||
const { all, one } = require('../db');
|
||||
const { all, now, one, parseJson, run } = require('../db');
|
||||
const { bookLedger } = require('../services/books');
|
||||
const { runReport } = require('../services/reportEngine');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -27,4 +29,45 @@ router.get('/dashboard', (req, res) => {
|
||||
res.json({ stats, byCategory, byStatus, upcomingTasks, auditTrail });
|
||||
});
|
||||
|
||||
// ---- Per-user dashboard layout (the drag-and-drop widget arrangement) --------
|
||||
|
||||
router.get('/dashboard/layout', (req, res) => {
|
||||
const row = one('SELECT layout_json FROM dashboard_layouts WHERE user_id = ?', [req.user.id]);
|
||||
res.json({ layout: row ? parseJson(row.layout_json, null) : null });
|
||||
});
|
||||
|
||||
router.put('/dashboard/layout', (req, res) => {
|
||||
const layout = Array.isArray(req.body.layout) ? req.body.layout : [];
|
||||
run(
|
||||
`INSERT INTO dashboard_layouts (user_id, layout_json, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET layout_json = excluded.layout_json, updated_at = excluded.updated_at`,
|
||||
[req.user.id, JSON.stringify(layout), now()]
|
||||
);
|
||||
res.json({ layout });
|
||||
});
|
||||
|
||||
// ---- Depreciation aggregator for the dashboard charts (one round-trip) -------
|
||||
// byBook: YTD + accumulated per enabled depreciation book; byCategory: accumulated by category (primary book).
|
||||
router.get('/dashboard/depreciation', (req, res) => {
|
||||
const cid = req.companyId || null;
|
||||
const year = Number(req.query.year) || new Date().getFullYear();
|
||||
const books = all(
|
||||
"SELECT code, is_primary FROM books WHERE (? IS NULL OR entity_id = ?) AND enabled = 1 AND book_type != 'maintenance' ORDER BY is_primary DESC, sort_order, id",
|
||||
[cid, cid]
|
||||
);
|
||||
const byBook = books.map((b) => {
|
||||
const ledger = bookLedger(b.code, year, cid);
|
||||
return { book: b.code, ytd: ledger ? ledger.summary.depreciation : 0, accumulated: ledger ? ledger.summary.accumulated : 0 };
|
||||
});
|
||||
const primary = (books.find((b) => b.is_primary) || books[0] || {}).code || 'GAAP';
|
||||
let byCategory = [];
|
||||
try {
|
||||
byCategory = runReport('depreciation_summary', { book: primary, year, entity_id: cid }).rows
|
||||
.map((r) => ({ category: r.category, accumulated: r.accumulated, depreciation: r.depreciation }));
|
||||
} catch {
|
||||
byCategory = [];
|
||||
}
|
||||
res.json({ year, book: primary, byBook, byCategory });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -1966,6 +1966,25 @@ async function main() {
|
||||
.filter((e) => e.source === `close:${yearClose.id}` && e.account_type === 'Retained earnings');
|
||||
if (!reEntries.length) throw new Error('Year-end roll-forward did not post a Retained Earnings entry');
|
||||
|
||||
// ---- Dashboard designer: per-user layout + depreciation aggregator ----------
|
||||
const freshLayout = await request('/api/dashboard/layout', { headers: auth });
|
||||
if (freshLayout.layout !== null && !Array.isArray(freshLayout.layout)) throw new Error('Dashboard layout endpoint returned an unexpected shape');
|
||||
const myLayout = [
|
||||
{ i: 'portfolio-stats-1', type: 'portfolio-stats', x: 0, y: 0, w: 12, h: 3 },
|
||||
{ i: 'asset-scanner-1', type: 'asset-scanner', x: 0, y: 3, w: 4, h: 5 }
|
||||
];
|
||||
await request('/api/dashboard/layout', { method: 'PUT', headers: auth, body: JSON.stringify({ layout: myLayout }) });
|
||||
const savedLayout = (await request('/api/dashboard/layout', { headers: auth })).layout;
|
||||
if (!Array.isArray(savedLayout) || savedLayout.length !== 2 || savedLayout[0].type !== 'portfolio-stats') throw new Error('Dashboard layout did not persist for the user');
|
||||
// Per-user isolation: a different user has an independent (empty) layout.
|
||||
const otherLayout = (await request('/api/dashboard/layout', { headers: limitedAuth })).layout;
|
||||
if (Array.isArray(otherLayout) && otherLayout.length) throw new Error('Dashboard layout leaked across users');
|
||||
|
||||
// Depreciation aggregator (company A has a depreciating GAAP asset from the close block).
|
||||
const deprAgg = await request('/api/dashboard/depreciation?year=2026', { headers: aHeaders });
|
||||
if (!Array.isArray(deprAgg.byBook) || !deprAgg.byBook.some((b) => b.book === 'GAAP')) throw new Error('Dashboard depreciation aggregator missing byBook/GAAP');
|
||||
if (!Array.isArray(deprAgg.byCategory)) throw new Error('Dashboard depreciation aggregator missing byCategory');
|
||||
|
||||
console.log('Smoke test passed');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user