asset_browser/public/modules/clientDashboard.js
2026-03-27 09:18:39 -04:00

135 lines
4.9 KiB
JavaScript
Executable file

// clientDashboard.js — Quick View summary tables for client role users.
// Shows device fleet at a glance: two tables split by possession status.
import { getCustomerAssets } from '../api/syncro.js';
// Lifecycle stages shown per table (only columns with data will render)
const IT_POSSESSION_STAGES = ['Inventory', 'Pre-Deployment', 'For Repair', 'For Upgrade', 'For Parts', 'Decommissioned', 'Disposed of'];
const DEPLOYED_STAGES = ['Active', 'For Repair', 'For Upgrade', 'Decommissioned', 'Disposed of'];
function getPossessionGroup(asset) {
const p = asset.properties?.['Possession Status'];
if (p === 'In IT Possession') return 'it';
if (p === 'Deployed' || p === 'In User Possession') return 'deployed';
return 'it'; // default unmapped assets to IT bucket
}
function getLifecycle(asset) {
return asset.properties?.['Lifecycle Stage'] ?? 'Unknown';
}
function getDeviceType(asset) {
return asset.asset_type || 'Other';
}
// Build a count matrix: { [deviceType]: { [lifecycleStage]: count } }
function buildMatrix(assets) {
const matrix = {};
for (const asset of assets) {
const type = getDeviceType(asset);
const stage = getLifecycle(asset);
if (!matrix[type]) matrix[type] = {};
matrix[type][stage] = (matrix[type][stage] ?? 0) + 1;
}
return matrix;
}
// Only include columns that have at least one non-zero value
function activeColumns(matrix, stageCandidates) {
return stageCandidates.filter(stage =>
Object.values(matrix).some(row => (row[stage] ?? 0) > 0)
);
}
function esc(s) {
return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function renderTable({ title, assets, stageOrder, possession, onFilterSelect }) {
if (!assets.length) {
return `<div class="qv-section"><h2>${esc(title)}</h2><p class="qv-empty">No devices in this group.</p></div>`;
}
const matrix = buildMatrix(assets);
const cols = activeColumns(matrix, stageOrder);
const types = Object.keys(matrix).sort();
// Column totals
const colTotals = {};
for (const col of cols) colTotals[col] = 0;
for (const type of types) {
for (const col of cols) {
colTotals[col] += matrix[type][col] ?? 0;
}
}
const grandTotal = Object.values(colTotals).reduce((a, b) => a + b, 0);
const headerCells = cols.map(c => `<th>${esc(c)}</th>`).join('');
const totalCells = cols.map(c => {
const n = colTotals[c];
return `<td>${n > 0 ? n : '<span class="qv-cell-zero">—</span>'}</td>`;
}).join('');
const rows = types.map(type => {
const rowTotal = cols.reduce((s, c) => s + (matrix[type][c] ?? 0), 0);
const cells = cols.map(stage => {
const n = matrix[type][stage] ?? 0;
if (n === 0) return `<td><span class="qv-cell-zero">—</span></td>`;
const payload = encodeURIComponent(JSON.stringify({ lifecycle: [stage], possession }));
return `<td><span class="qv-cell-link" data-filter="${payload}">${n}</span></td>`;
}).join('');
return `<tr><td>${esc(type)}</td>${cells}<td>${rowTotal}</td></tr>`;
}).join('');
return `
<div class="qv-section">
<div class="qv-card-header">
<h2>${esc(title)}</h2>
<span class="qv-card-count">${grandTotal} device${grandTotal !== 1 ? 's' : ''}</span>
</div>
<div class="qv-card-body">
<table class="qv-table">
<thead><tr><th>Device Type</th>${headerCells}<th>Total</th></tr></thead>
<tbody>${rows}</tbody>
<tfoot><tr><td>Total</td>${totalCells}<td>${grandTotal}</td></tr></tfoot>
</table>
</div>
</div>`;
}
export async function renderClientDashboard(container, user, { onFilterSelect } = {}) {
container.innerHTML = '<div class="qv-loading">Loading…</div>';
if (!user?.syncro_customer_id) {
container.innerHTML = '<div class="qv-empty">No company assigned to your account. Contact your administrator.</div>';
return;
}
let assets;
try {
assets = await getCustomerAssets(user.syncro_customer_id);
} catch (err) {
container.innerHTML = `<div class="qv-empty">Failed to load assets: ${esc(err.message)}</div>`;
return;
}
const itAssets = assets.filter(a => getPossessionGroup(a) === 'it');
const deployedAssets = assets.filter(a => getPossessionGroup(a) === 'deployed');
const html =
renderTable({ title: 'In IT Possession', assets: itAssets, stageOrder: IT_POSSESSION_STAGES, possession: 'IT', onFilterSelect }) +
renderTable({ title: 'Out in the Field', assets: deployedAssets, stageOrder: DEPLOYED_STAGES, possession: 'Deployed', onFilterSelect });
container.innerHTML = html;
// Wire up click-through filtering
if (onFilterSelect) {
container.querySelectorAll('.qv-cell-link').forEach(el => {
el.addEventListener('click', () => {
try {
const { lifecycle, possession } = JSON.parse(decodeURIComponent(el.dataset.filter));
onFilterSelect({ lifecycle, possession });
} catch { /* ignore malformed data */ }
});
});
}
}