refactor(staffing): decompose 735-line StaffingPanel into focused components

Splits StaffingPanel.tsx into:
- StaffingSearchForm: skill tags, dates, hours input, submit button
- ScoringExplanation: the 3-column scoring breakdown card
- StaffingResultCard: individual suggestion card with details and assign form
- StaffingResultsList: list orchestration with loading/empty states
- StaffingPanel: thin orchestrator (~100 lines) managing state and tRPC query

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 13:17:55 +02:00
parent 594ae4f10b
commit 05f6eba5d8
5 changed files with 710 additions and 659 deletions
@@ -0,0 +1,74 @@
"use client";
import { StaffingResultCard, type SuggestionLike } from "./StaffingResultCard.js";
import type { SearchCriteria } from "./StaffingSearchForm.js";
interface StaffingResultsListProps {
suggestions: SuggestionLike[] | undefined;
isLoading: boolean;
submitted: boolean;
searchCriteria: SearchCriteria;
assignedIds: Set<string>;
onAssigned: (resourceId: string, resourceName: string) => void;
onError: (message: string) => void;
}
export function StaffingResultsList({
suggestions,
isLoading,
submitted,
searchCriteria,
assignedIds,
onAssigned,
onError,
}: StaffingResultsListProps) {
const visible = suggestions?.filter((s) => !assignedIds.has(s.resourceId));
if (isLoading) {
return (
<div className="app-surface-strong py-16 text-center text-sm text-gray-500">
Finding best matches...
</div>
);
}
if (visible && visible.length === 0) {
return (
<div className="app-surface-strong py-16 text-center text-sm text-gray-500">
{assignedIds.size > 0
? "All suggestions have been assigned."
: "No resources found matching your criteria."}
</div>
);
}
if (!submitted) {
return (
<div className="app-surface-strong border-dashed py-20 text-center">
<div className="mx-auto max-w-md">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">No suggestions yet</div>
<p className="mt-2 text-sm text-gray-500">
Add the required skills and date range, then run the search to see ranked staffing matches.
</p>
</div>
</div>
);
}
if (!visible) return null;
return (
<div className="space-y-4">
{visible.map((suggestion, idx) => (
<StaffingResultCard
key={suggestion.resourceId}
suggestion={suggestion}
rank={idx + 1}
searchCriteria={searchCriteria}
onAssigned={onAssigned}
onError={onError}
/>
))}
</div>
);
}