diff --git a/frontend/src/components/BudgetBanner.tsx b/frontend/src/components/BudgetBanner.tsx new file mode 100644 index 0000000..b9a2eab --- /dev/null +++ b/frontend/src/components/BudgetBanner.tsx @@ -0,0 +1,23 @@ +interface Props { + projectName: string; + timeExceeded: boolean; + costExceeded: boolean; +} + +export function BudgetBanner({ + projectName, + timeExceeded, + costExceeded, +}: Props) { + if (!timeExceeded && !costExceeded) return null; + + const parts: string[] = []; + if (timeExceeded) parts.push("time budget"); + if (costExceeded) parts.push("cost budget"); + + return ( +
{error}
} + {loading &&Loading…
} + + {/* Budget warnings */} + {report?.projects + .filter((p) => p.breached) + .map((p) => ( +| Project | +Hours | +Cost | +Time budget | +Cost budget | +
|---|---|---|---|---|
| + {p.name} + {p.breached && ( + + ⚠ budget exceeded + + )} + | ++ {minutesToHmm(p.minutes)} + | ++ {centsToEur(p.costCents)} + | +
+
+
+ {Math.round(p.timeBudgetPct)}%
+
+
+ |
+
+
+
+ {Math.round(p.costBudgetPct)}%
+
+
+ |
+
+ No time entries for this month. +
+ )} + + {/* Month totals */} + {report && report.projects.length > 0 && ( +| Project | +Rate | +Logged | +Cost logged | +Time | +Cost | +
|---|---|---|---|---|---|
| + {p.name} + {(p.timeExceeded || p.costExceeded) && ( + + ⚠ exceeded + + )} + | ++ {p.hourlyRateCents != null + ? `${centsToEur(p.hourlyRateCents)}/h` + : "per user"} + | ++ {minutesToHmm(p.minutesLogged)} /{" "} + {minutesToHmm(p.timeBudgetMinutes)} + | ++ {centsToEur(p.costLoggedCents)} /{" "} + {centsToEur(p.costBudgetCents)} + | +
+
+
+ {Math.round(timePct)}%
+
+
+ |
+
+
+
+ {Math.round(costPct)}%
+
+
+ |
+
+ No projects yet. +
+ )} +{error}
} + {loading &&Loading…
} + + {/* Day rows */} + {report?.days.map((day) => { + const dayEntries = entriesByDate[day.date] ?? []; + const isAddingHere = addingForDate === day.date; + + return ( +