// 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 `

${esc(title)}

${grandTotal} device${grandTotal !== 1 ? 's' : ''}
${headerCells}${rows}${totalCells}
Device TypeTotal
Total${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 */ } }); }); } }