@@ -143,11 +130,6 @@ export function ProjectKPICards() {
: 'No budget set in BC',
icon: CalendarDaysIcon,
color: hoursRemaining < 0 ? 'text-red-400' : 'text-blue-400',
- tooltip: {
- title: 'Time Budgeted',
- description: `Budgeted hours from Job Planning Lines. Only includes Resource lines where lineType is "Budget" or "Both Budget and Billable". Days = hours ÷ ${hoursPerDay}.`,
- source: 'BC API: /jobPlanningLines → quantity',
- },
},
{
label: 'Time Spent',
@@ -160,11 +142,6 @@ export function ProjectKPICards() {
progress: hasPlannedHours ? Math.min(percentUsed, 100) : undefined,
progressColor:
percentUsed > 100 ? 'bg-red-500' : percentUsed > 80 ? 'bg-amber-500' : 'bg-thyme-500',
- tooltip: {
- title: 'Time Spent',
- description: `Total hours logged in timesheets for this project. Includes all timesheet statuses: Open, Submitted, and Approved. Days = hours ÷ ${hoursPerDay}.`,
- source: 'BC API: /timeSheetLines → totalQuantity',
- },
},
{
label: 'Time Unposted',
@@ -172,12 +149,6 @@ export function ProjectKPICards() {
subLabel: hoursUnposted > 0 ? 'In timesheets, not posted' : 'All time posted',
icon: ClockIcon,
color: hoursUnposted > 0 ? 'text-amber-400' : 'text-gray-500',
- tooltip: {
- title: 'Time Unposted',
- description: `Hours in timesheets that have not yet been posted to the Job Ledger Entry. These hours are approved but awaiting the "Post Time Sheets" action in BC. Days = hours ÷ ${hoursPerDay}.`,
- formula: 'Time Spent − Time Posted',
- source: 'Calculated',
- },
},
{
label: 'Time Posted',
@@ -185,11 +156,6 @@ export function ProjectKPICards() {
subLabel: 'In Job Ledger Entry',
icon: ClockIcon,
color: 'text-green-400',
- tooltip: {
- title: 'Time Posted',
- description: `Hours that have been posted to the Job Ledger Entry. Posting creates cost and price entries based on the Resource's Unit Cost and Unit Price. Days = hours ÷ ${hoursPerDay}.`,
- source: 'BC API: /timeEntries → quantity',
- },
},
];
@@ -259,8 +225,9 @@ export function ProjectKPICards() {
);
// Financial KPIs - 4 cards matching BC structure
- // Budget Cost and Actual Cost are internal (hideable)
- // Billable Price and Invoiced Price are customer-facing
+ // Budget Cost and Actual Cost are internal; Billable Price and Invoiced Price
+ // are customer-facing. Each card's amount can be hidden individually via its
+ // Eye toggle (see hiddenCards); the value/breakdown masking happens at render.
const financialKpis: {
label: string;
value: string;
@@ -269,51 +236,24 @@ export function ProjectKPICards() {
icon: typeof BanknotesIcon;
color: string;
isInternal?: boolean;
- isHidden?: boolean;
- tooltip: {
- title: string;
- description: string;
- formula?: string;
- source: string;
- };
}[] = [
{
label: 'Budget Cost',
- value: showCosts ? formatCurrency(budgetCost, currencyCode) : '•••••',
- subLabel: showCosts ? jobPlanningLinesLink : 'Hidden',
- breakdown: showCosts ? budgetBreakdown : null,
+ value: formatCurrency(budgetCost, currencyCode),
+ subLabel: jobPlanningLinesLink,
+ breakdown: budgetBreakdown,
icon: BanknotesIcon,
color: 'text-amber-400',
isInternal: true,
- isHidden: !showCosts,
- tooltip: {
- title: 'Budget Cost (Internal)',
- description:
- 'Internal cost budget from Job Planning Lines. This is what the project is expected to cost the company. Broken down by Resource (labour), Item (materials), and G/L Account (overhead).',
- formula: 'quantity × unitCost',
- source: 'BC API: /jobPlanningLines → totalCost',
- },
},
{
label: 'Actual Cost',
- value: showCosts ? formatCurrency(actualCost, currencyCode) : '•••••',
- subLabel: showCosts ? jobLedgerEntryLink : 'Hidden',
- breakdown: showCosts ? actualBreakdown : null,
+ value: formatCurrency(actualCost, currencyCode),
+ subLabel: jobLedgerEntryLink,
+ breakdown: actualBreakdown,
icon: BanknotesIcon,
- color: showCosts
- ? actualCost > budgetCost && budgetCost > 0
- ? 'text-red-400'
- : 'text-amber-400'
- : 'text-gray-500',
+ color: actualCost > budgetCost && budgetCost > 0 ? 'text-red-400' : 'text-amber-400',
isInternal: true,
- isHidden: !showCosts,
- tooltip: {
- title: 'Actual Cost (Internal)',
- description:
- "Internal cost incurred from posted Job Ledger Entries. Calculated when timesheets are posted using each Resource's Unit Cost. Shows £0 if timesheets are approved but not yet posted.",
- formula: 'posted hours × Resource Unit Cost',
- source: 'BC API: /timeEntries → totalCost',
- },
},
{
label: 'Billable Price',
@@ -322,13 +262,6 @@ export function ProjectKPICards() {
breakdown: billableBreakdown,
icon: CurrencyPoundIcon,
color: 'text-blue-400',
- tooltip: {
- title: 'Billable Price (Customer)',
- description:
- 'Customer quote/expected revenue from Job Planning Lines. This is what the customer is expected to pay. Only includes lines where lineType is "Billable" or "Both Budget and Billable".',
- formula: 'quantity × unitPrice',
- source: 'BC API: /jobPlanningLines → totalPrice',
- },
},
{
label: 'Invoiced Price',
@@ -337,13 +270,6 @@ export function ProjectKPICards() {
breakdown: invoicedBreakdown,
icon: CurrencyPoundIcon,
color: 'text-green-400',
- tooltip: {
- title: 'Invoiced Price (Customer)',
- description:
- "Amount actually invoiced to the customer from Job Ledger Entry. Calculated when timesheets are posted using each Resource's Unit Price.",
- formula: 'posted hours × Resource Unit Price',
- source: 'BC API: /timeEntries → totalPrice',
- },
},
];
@@ -351,36 +277,42 @@ export function ProjectKPICards() {
{/* Row 1: Hours (4 cards) */}
- {hoursKpis.map((kpi) => (
-
-
-
-
-
-
-
+ {hoursKpis.map((kpi) => {
+ const isHidden = hiddenCards.has(kpi.label);
+ return (
+
+
+ toggleCardHidden(kpi.label)}
+ label={kpi.label}
+ />
-
-
{kpi.label}
-
{kpi.value}
-
{kpi.subLabel}
- {kpi.progress !== undefined && (
-
- )}
+
+
+
+
+
+
{kpi.label}
+
+ {isHidden ? maskedValue : kpi.value}
+
+
{kpi.subLabel}
+ {kpi.progress !== undefined && !isHidden && (
+
+ )}
+
-
-
- ))}
+
+ );
+ })}
{/* Row 2: Financials (4 cards matching BC) - hidden in print only for "Without Financials" export */}
@@ -391,16 +323,15 @@ export function ProjectKPICards() {
)}
>
{financialKpis.map((kpi) => {
- const isHidden = 'isHidden' in kpi && kpi.isHidden;
- const breakdown = 'breakdown' in kpi ? kpi.breakdown : null;
+ const isHidden = hiddenCards.has(kpi.label);
+ const breakdown = kpi.breakdown;
return (
-
-
+ toggleCardHidden(kpi.label)}
+ label={kpi.label}
/>
@@ -421,7 +352,7 @@ export function ProjectKPICards() {
)}
- {kpi.value}
+ {isHidden ? maskedValue : kpi.value}
{/* Breakdown by type - always show all 3 lines */}
{breakdown && !isHidden && (
diff --git a/src/hooks/useProjectDetailsStore.ts b/src/hooks/useProjectDetailsStore.ts
index 078e492..2680009 100644
--- a/src/hooks/useProjectDetailsStore.ts
+++ b/src/hooks/useProjectDetailsStore.ts
@@ -39,7 +39,7 @@ export const useProjectDetailsStore = create((set, get) =>
error: null,
chartView: 'weekly',
tableGroupBy: 'task',
- showCosts: false, // Internal costs hidden by default
+ showCosts: true, // Internal costs visible by default (per-widget Eye toggles hide individually); also gates the Spend vs Budget chart and PDF export
showPrices: true, // Customer-facing prices always visible by default
fetchProjectDetails: async (projectNumber: string) => {