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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user