Files
CapaKraken/apps/web/src/components/staffing/StaffingPanel.tsx
T
Hartmut 05f6eba5d8 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>
2026-04-09 13:17:55 +02:00

101 lines
4.0 KiB
TypeScript

"use client";
import { useState, useCallback } from "react";
import { trpc } from "~/lib/trpc/client.js";
import { SuccessToast } from "~/components/ui/SuccessToast.js";
import { StaffingSearchForm } from "./StaffingSearchForm.js";
import { ScoringExplanation } from "./ScoringExplanation.js";
import { StaffingResultsList } from "./StaffingResultsList.js";
export function StaffingPanel() {
const [requiredSkills, setRequiredSkills] = useState<string[]>(["TypeScript", "React"]);
const [startDate, setStartDate] = useState(() => {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
});
const [endDate, setEndDate] = useState(() => {
const d = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
});
const [hoursPerDay, setHoursPerDay] = useState(8);
const [submitted, setSubmitted] = useState(false);
const [assignedIds, setAssignedIds] = useState<Set<string>>(new Set());
const [toast, setToast] = useState<{ show: boolean; message: string; variant: "success" | "warning" }>({
show: false,
message: "",
variant: "success",
});
const clearToast = useCallback(() => setToast((t) => ({ ...t, show: false })), []);
const { data: suggestions, isLoading } = trpc.staffing.getSuggestions.useQuery(
{
requiredSkills,
startDate: new Date(startDate),
endDate: new Date(endDate),
hoursPerDay,
},
{ enabled: submitted },
);
const handleAssigned = useCallback((resourceId: string, resourceName: string) => {
setAssignedIds((prev) => new Set(prev).add(resourceId));
setToast({ show: true, message: `${resourceName} assigned successfully`, variant: "success" });
}, []);
const searchCriteria = { startDate, endDate, hoursPerDay };
return (
<div className="app-page space-y-6">
<SuccessToast show={toast.show} message={toast.message} variant={toast.variant} onDone={clearToast} />
<div className="app-page-header gap-4">
<div className="space-y-3">
<span className="inline-flex items-center rounded-full border border-brand-200 bg-brand-50 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-brand-700 dark:border-brand-900/60 dark:bg-brand-900/30 dark:text-brand-200">
Staffing
</span>
<div>
<h1 className="app-page-title">Staffing Suggestions</h1>
<p className="app-page-subtitle max-w-2xl">
Match open work with the strongest available people based on skills, availability, utilization, and cost.
</p>
</div>
</div>
<div className="app-surface max-w-xl p-4">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">How scoring works</p>
<p className="mt-1 text-sm text-gray-500">
CapaKraken blends skill fit, free capacity, cost, and current utilization. Add the must-have skills first, then narrow the date window to get cleaner results.
</p>
</div>
</div>
<div className="grid grid-cols-1 gap-6 xl:grid-cols-[360px_minmax(0,1fr)]">
<div className="space-y-6">
<StaffingSearchForm
requiredSkills={requiredSkills}
onSkillsChange={setRequiredSkills}
startDate={startDate}
onStartDateChange={setStartDate}
endDate={endDate}
onEndDateChange={setEndDate}
hoursPerDay={hoursPerDay}
onHoursPerDayChange={setHoursPerDay}
onSubmit={() => setSubmitted(true)}
/>
<ScoringExplanation />
</div>
<StaffingResultsList
suggestions={suggestions}
isLoading={isLoading}
submitted={submitted}
searchCriteria={searchCriteria}
assignedIds={assignedIds}
onAssigned={handleAssigned}
onError={(msg) => setToast({ show: true, message: msg, variant: "warning" })}
/>
</div>
</div>
);
}