fix(types): remove unnecessary as any casts in web components

- ProjectHealthWidget: row already typed as ProjectHealthRow with id field
- ResourceDetail: use narrowed unknown cast instead of any for error code
- provider.tsx: same pattern for TRPCClientError data access
- ChatPanel: use intersection type for Next.js typed route push

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 15:13:06 +02:00
parent 9051ff73d0
commit 0d79f97d7a
4 changed files with 431 additions and 206 deletions
@@ -131,10 +131,10 @@ export function ProjectHealthWidget({ config, onConfigChange }: WidgetProps) {
[clients],
);
const { data, isLoading } = trpc.dashboard.getProjectHealth.useQuery(
undefined,
{ staleTime: 60_000, placeholderData: (prev) => prev },
);
const { data, isLoading } = trpc.dashboard.getProjectHealth.useQuery(undefined, {
staleTime: 60_000,
placeholderData: (prev) => prev,
});
const search = ((config.search as string) ?? "").toLowerCase();
const clientId = (config.clientId as string) ?? "";
@@ -142,7 +142,12 @@ export function ProjectHealthWidget({ config, onConfigChange }: WidgetProps) {
const rows = useMemo(() => {
const all = (data ?? []) as ProjectHealthRow[];
return all.filter((r) => {
if (search && !r.projectName.toLowerCase().includes(search) && !r.shortCode.toLowerCase().includes(search)) return false;
if (
search &&
!r.projectName.toLowerCase().includes(search) &&
!r.shortCode.toLowerCase().includes(search)
)
return false;
if (clientId && r.clientId !== clientId) return false;
return true;
});
@@ -170,7 +175,11 @@ export function ProjectHealthWidget({ config, onConfigChange }: WidgetProps) {
if (rows.length === 0) {
return (
<div className="flex flex-col h-full">
<WidgetFilterBar filters={filters} values={config} onChange={onConfigChange ?? (() => {})} />
<WidgetFilterBar
filters={filters}
values={config}
onChange={onConfigChange ?? (() => {})}
/>
<div className="flex items-center justify-center flex-1 text-sm text-gray-400">
No active projects found.
</div>
@@ -186,16 +195,20 @@ export function ProjectHealthWidget({ config, onConfigChange }: WidgetProps) {
<thead className="bg-gray-50 dark:bg-gray-800/50 sticky top-0">
<tr>
<th className="px-3 py-2 text-left font-medium text-gray-500 dark:text-gray-400">
Project <InfoTooltip content="Active projects scored across three health dimensions including visible budget, staffing, and timeline basis." />
Project{" "}
<InfoTooltip content="Active projects scored across three health dimensions including visible budget, staffing, and timeline basis." />
</th>
<th className="px-3 py-2 text-center font-medium text-gray-500 dark:text-gray-400">
B / S / T <InfoTooltip content="Budget health (spent vs budget), Staffing health (filled vs total demanded headcount), Timeline health (end date and remaining horizon)." />
B / S / T{" "}
<InfoTooltip content="Budget health (spent vs budget), Staffing health (filled vs total demanded headcount), Timeline health (end date and remaining horizon)." />
</th>
<th className="px-3 py-2 text-center font-medium text-gray-500 dark:text-gray-400">
Shoring <InfoTooltip content="Offshore staffing ratio: percentage of hours from non-onshore resources. Color indicates threshold status." />
Shoring{" "}
<InfoTooltip content="Offshore staffing ratio: percentage of hours from non-onshore resources. Color indicates threshold status." />
</th>
<th className="px-3 py-2 text-right font-medium text-gray-500 dark:text-gray-400">
Score <InfoTooltip content="Composite score: average of Budget, Staffing, and Timeline health (0-100)" />
Score{" "}
<InfoTooltip content="Composite score: average of Budget, Staffing, and Timeline health (0-100)" />
</th>
</tr>
</thead>
@@ -203,9 +216,14 @@ export function ProjectHealthWidget({ config, onConfigChange }: WidgetProps) {
{rows.map((row) => (
<tr key={row.shortCode} className="hover:bg-gray-50 dark:hover:bg-gray-800/30">
<td className="px-3 py-2 text-gray-900 dark:text-gray-100 max-w-[320px]">
<Link href={`/projects/${(row as any).id}`} className="block hover:text-brand-600 dark:hover:text-brand-400 transition-colors">
<Link
href={`/projects/${row.id}`}
className="block hover:text-brand-600 dark:hover:text-brand-400 transition-colors"
>
<div className="truncate font-medium">
<span className="font-mono text-gray-500 dark:text-gray-400 mr-1">{row.shortCode}</span>
<span className="font-mono text-gray-500 dark:text-gray-400 mr-1">
{row.shortCode}
</span>
{row.projectName}
</div>
</Link>
@@ -213,38 +231,58 @@ export function ProjectHealthWidget({ config, onConfigChange }: WidgetProps) {
<div className="mt-1 space-y-0.5 text-[11px] leading-4 text-gray-500 dark:text-gray-400">
<div>
Budget: {formatMoney(row.spentCents ?? 0)} spent
{row.budgetCents != null ? ` / ${formatMoney(row.budgetCents)} budget` : " / no budget"}
{row.remainingBudgetCents != null ? ` / ${formatMoney(row.remainingBudgetCents)} remaining` : ""}
{row.budgetCents != null
? ` / ${formatMoney(row.budgetCents)} budget`
: " / no budget"}
{row.remainingBudgetCents != null
? ` / ${formatMoney(row.remainingBudgetCents)} remaining`
: ""}
</div>
<div>
Staffing: {row.demandHeadcountFilled ?? 0}/{row.demandHeadcountTotal ?? 0} HC
{typeof row.demandHeadcountOpen === "number" ? `, ${row.demandHeadcountOpen} open` : ""}
{typeof row.demandRequirementCount === "number" ? ` across ${row.demandRequirementCount} demands` : ""}
Staffing: {row.demandHeadcountFilled ?? 0}/{row.demandHeadcountTotal ?? 0}{" "}
HC
{typeof row.demandHeadcountOpen === "number"
? `, ${row.demandHeadcountOpen} open`
: ""}
{typeof row.demandRequirementCount === "number"
? ` across ${row.demandRequirementCount} demands`
: ""}
</div>
<div>
Timeline: {formatShortDate(row.plannedEndDate)} · {formatTimeline(row.daysUntilEndDate, row.timelineStatus)}
Timeline: {formatShortDate(row.plannedEndDate)} ·{" "}
{formatTimeline(row.daysUntilEndDate, row.timelineStatus)}
</div>
{row.derivation ? (
<>
<div>
Spend basis: {row.derivation.calendarContextCount} calendar bases · {row.derivation.holidayAwareAssignmentCount} holiday-aware
{row.derivation.fallbackAssignmentCount > 0 ? ` · ${row.derivation.fallbackAssignmentCount} fallback` : ""}
Spend basis: {row.derivation.calendarContextCount} calendar bases ·{" "}
{row.derivation.holidayAwareAssignmentCount} holiday-aware
{row.derivation.fallbackAssignmentCount > 0
? ` · ${row.derivation.fallbackAssignmentCount} fallback`
: ""}
</div>
<div>
Base {formatMoney(row.derivation.baseSpentCents)} {"->"} Effective {formatMoney(row.derivation.adjustedSpentCents)}
Base {formatMoney(row.derivation.baseSpentCents)} {"->"} Effective{" "}
{formatMoney(row.derivation.adjustedSpentCents)}
</div>
<div>
Holidays -{formatMoney(row.derivation.publicHolidayCostDeductionCents)} ({formatDayEquivalent(row.derivation.publicHolidayDayEquivalent)}d)
Holidays -{formatMoney(row.derivation.publicHolidayCostDeductionCents)}{" "}
({formatDayEquivalent(row.derivation.publicHolidayDayEquivalent)}d)
{" · "}
Absence -{formatMoney(row.derivation.absenceCostDeductionCents)} ({formatDayEquivalent(row.derivation.absenceDayEquivalent)}d)
Absence -{formatMoney(row.derivation.absenceCostDeductionCents)} (
{formatDayEquivalent(row.derivation.absenceDayEquivalent)}d)
</div>
</>
) : null}
{(row.calendarLocations ?? []).length > 0 ? (
<div>
Calendar basis: {(row.calendarLocations ?? [])
Calendar basis:{" "}
{(row.calendarLocations ?? [])
.slice(0, 2)
.map((location) => `${formatLocation(location)} (${formatMoney(location.spentCents)} / ${location.assignmentCount} assign.)`)
.map(
(location) =>
`${formatLocation(location)} (${formatMoney(location.spentCents)} / ${location.assignmentCount} assign.)`,
)
.join(" · ")}
{(row.calendarLocations ?? []).length > 2
? ` · +${(row.calendarLocations ?? []).length - 2} more`
@@ -275,13 +313,14 @@ export function ProjectHealthWidget({ config, onConfigChange }: WidgetProps) {
</div>
{showDetails ? (
<div className="text-center tabular-nums">
S {row.demandHeadcountFilled ?? 0}/{row.demandHeadcountTotal ?? 0} · T {formatTimeline(row.daysUntilEndDate, row.timelineStatus)}
S {row.demandHeadcountFilled ?? 0}/{row.demandHeadcountTotal ?? 0} · T{" "}
{formatTimeline(row.daysUntilEndDate, row.timelineStatus)}
</div>
) : null}
</div>
</td>
<td className="px-3 py-2 text-center">
<ShoringBadge projectId={(row as any).id} />
<ShoringBadge projectId={row.id} />
</td>
<td className="px-3 py-2 text-right">
<span