05f6eba5d8
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>
101 lines
4.0 KiB
TypeScript
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>
|
|
);
|
|
}
|