135 lines
4.9 KiB
JavaScript
Executable file
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
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 */ }
|
|
});
|
|
});
|
|
}
|
|
}
|