diff --git a/frontend/src/pages/ProjectView.css b/frontend/src/pages/ProjectView.css index 8553f3f..fab25f7 100644 --- a/frontend/src/pages/ProjectView.css +++ b/frontend/src/pages/ProjectView.css @@ -820,6 +820,15 @@ span.conf-none { background: var(--color-danger); } flex: 1; } +.rc-section-label { + font-size: 13px; + font-weight: 700; + color: var(--color-text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 8px; +} + .text-right { text-align: right; } .text-center { text-align: center; } diff --git a/frontend/src/pages/ProjectView.tsx b/frontend/src/pages/ProjectView.tsx index c72cec3..b5c6bd8 100644 --- a/frontend/src/pages/ProjectView.tsx +++ b/frontend/src/pages/ProjectView.tsx @@ -936,6 +936,70 @@ export default function ProjectView() { + {/* Asset Summary Table */} +
Asset Summary
+
+ + + + + + + + + + + + + + {(() => { + // Group ratecard lines by client asset to get unique assets + total hours + const assetMap: Record = {}; + for (const l of ratecard.lines) { + if (!assetMap[l.client_asset_id]) { + const ca = assets.find(a => a.id === l.client_asset_id); + assetMap[l.client_asset_id] = { + name: l.client_asset_name || '', + tier: (ca as any)?.client_tier || '', + gmal_id: l.gmal_id || '', + gmal_name: '', + volume: l.volume, + hours: 0, + }; + } + const effective = l.manual_override ?? l.total_hours ?? 0; + assetMap[l.client_asset_id].hours += effective; + } + const assetList = Object.entries(assetMap).sort(([a], [b]) => Number(a) - Number(b)); + const grandTotal = assetList.reduce((sum, [, a]) => sum + a.hours, 0); + + return ( + <> + {assetList.map(([aid, a], idx) => ( + + + + + + + + + + ))} + + + + + + + ); + })()} + +
#Client AssetTierMatched GMALGMAL NameVolumeTotal Hours
{idx + 1}{a.name}{a.tier ? {a.tier} : }{a.gmal_id}{a.gmal_name}{a.volume}{a.hours.toFixed(2)}
{assetList.reduce((s, [, a]) => s + a.volume, 0)}{grandTotal.toFixed(2)}
+
+ + {/* Role Breakdown */} +
Hours by Role