diff --git a/src/components/projects/ProjectHeader.tsx b/src/components/projects/ProjectHeader.tsx index eb7e1f7..af2cccc 100644 --- a/src/components/projects/ProjectHeader.tsx +++ b/src/components/projects/ProjectHeader.tsx @@ -210,30 +210,6 @@ export function ProjectHeader() { {/* Action buttons */}
- {/* Cost visibility toggle - hidden in print */} - - {/* PDF Export dropdown - hidden in print */}
- {isOpen && ( -
-
{title}
-
-
{description}
-
- {formula && ( -
-
Formula:
-
{formula}
-
- )} -
-
Source:
-
{source}
-
-
- )} -
+ ); } @@ -94,12 +67,26 @@ function formatCurrency(amount: number, currencyCode: string): string { } export function ProjectKPICards() { - const { analytics, isLoadingAnalytics, showCosts, showPrices, currencyCode, project } = + const { analytics, isLoadingAnalytics, showPrices, currencyCode, project } = useProjectDetailsStore(); const selectedCompany = useCompanyStore((state) => state.selectedCompany); const companyName = selectedCompany?.name; const projectCode = project?.code; + // Per-widget amount visibility. Keyed by KPI label; resets on reload (not persisted). + const [hiddenCards, setHiddenCards] = useState>(new Set()); + const toggleCardHidden = (label: string) => + setHiddenCards((prev) => { + const next = new Set(prev); + if (next.has(label)) { + next.delete(label); + } else { + next.add(label); + } + return next; + }); + const maskedValue = '•••••'; + if (isLoadingAnalytics) { return (
@@ -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 ( + +
+
-
-

{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 ( -
- +
@@ -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) => {