// 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, '&').replace(//g, '>');
}
function renderTable({ title, assets, stageOrder, possession, onFilterSelect }) {
if (!assets.length) {
return `
${esc(title)}
No devices in this group.
`;
}
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 => `${esc(c)} | `).join('');
const totalCells = cols.map(c => {
const n = colTotals[c];
return `${n > 0 ? n : '—'} | `;
}).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 `— | `;
const payload = encodeURIComponent(JSON.stringify({ lifecycle: [stage], possession }));
return `${n} | `;
}).join('');
return `| ${esc(type)} | ${cells}${rowTotal} |
`;
}).join('');
return `
| Device Type | ${headerCells}Total |
${rows}
| Total | ${totalCells}${grandTotal} |
`;
}
export async function renderClientDashboard(container, user, { onFilterSelect } = {}) {
container.innerHTML = 'Loading…
';
if (!user?.syncro_customer_id) {
container.innerHTML = 'No company assigned to your account. Contact your administrator.
';
return;
}
let assets;
try {
assets = await getCustomerAssets(user.syncro_customer_id);
} catch (err) {
container.innerHTML = `Failed to load assets: ${esc(err.message)}
`;
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 */ }
});
});
}
}