chore(repo): initialize planarchy workspace

This commit is contained in:
2026-03-14 14:31:09 +01:00
commit dd55d0e78b
769 changed files with 166461 additions and 0 deletions
@@ -0,0 +1,178 @@
"use client";
import { useState } from "react";
import { trpc } from "~/lib/trpc/client.js";
import { DateInput } from "~/components/ui/DateInput.js";
import { SkillTagInput } from "~/components/ui/SkillTagInput.js";
export function StaffingPanel() {
const [requiredSkills, setRequiredSkills] = useState<string[]>(["TypeScript", "React"]);
const [startDate, setStartDate] = useState(new Date().toISOString().split("T")[0] ?? "");
const [endDate, setEndDate] = useState(
new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString().split("T")[0] ?? "",
);
const [hoursPerDay, setHoursPerDay] = useState(8);
const [submitted, setSubmitted] = useState(false);
const { data: suggestions, isLoading } = trpc.staffing.getSuggestions.useQuery(
{
requiredSkills: requiredSkills,
startDate: new Date(startDate),
endDate: new Date(endDate),
hoursPerDay,
},
{ enabled: submitted },
);
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Search Form */}
<div className="lg:col-span-1">
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Search Criteria</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Required Skills
</label>
<SkillTagInput
value={requiredSkills}
onChange={setRequiredSkills}
placeholder="Add skill…"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Start Date</label>
<DateInput
value={startDate}
onChange={setStartDate}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">End Date</label>
<DateInput
value={endDate}
onChange={setEndDate}
min={startDate}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Hours per Day</label>
<input
type="number"
value={hoursPerDay}
onChange={(e) => setHoursPerDay(Number(e.target.value))}
min={1}
max={24}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500"
/>
</div>
<button
onClick={() => setSubmitted(true)}
className="w-full bg-brand-600 hover:bg-brand-700 text-white font-medium py-2.5 px-4 rounded-lg transition-colors"
>
Find Matches
</button>
</div>
</div>
</div>
{/* Results */}
<div className="lg:col-span-2">
{isLoading && (
<div className="bg-white rounded-xl border border-gray-200 p-8 text-center text-gray-400">
Finding best matches...
</div>
)}
{suggestions && suggestions.length === 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-8 text-center text-gray-400">
No resources found matching your criteria.
</div>
)}
{suggestions && suggestions.length > 0 && (
<div className="space-y-3">
{suggestions.map((suggestion, idx) => (
<div key={suggestion.resourceId} className="bg-white rounded-xl border border-gray-200 p-4">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-brand-100 flex items-center justify-center font-bold text-brand-700">
#{idx + 1}
</div>
<div>
<div className="font-semibold text-gray-900">{suggestion.resourceName}</div>
<div className="text-xs text-gray-500">{suggestion.eid}</div>
</div>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-brand-600">{suggestion.score}</div>
<div className="text-xs text-gray-400">score</div>
</div>
</div>
<div className="mt-4 grid grid-cols-4 gap-3">
<ScoreBar label="Skills" value={suggestion.scoreBreakdown.skillScore} />
<ScoreBar label="Avail." value={suggestion.scoreBreakdown.availabilityScore} />
<ScoreBar label="Cost" value={suggestion.scoreBreakdown.costScore} />
<ScoreBar label="Util." value={suggestion.scoreBreakdown.utilizationScore} />
</div>
<div className="mt-3 flex flex-wrap gap-2">
{suggestion.matchedSkills.map((skill) => (
<span key={skill} className="px-2 py-0.5 bg-green-50 text-green-700 text-xs rounded-full">
{skill}
</span>
))}
{suggestion.missingSkills.map((skill) => (
<span key={skill} className="px-2 py-0.5 bg-red-50 text-red-600 text-xs rounded-full">
{skill} (missing)
</span>
))}
</div>
<div className="mt-3 flex items-center gap-4 text-xs text-gray-500">
<span>
LCR: {(suggestion.estimatedDailyCostCents / 100 / 8).toFixed(0)} /h
</span>
<span>Utilization: {Math.round(suggestion.currentUtilization)}%</span>
{suggestion.availabilityConflicts.length > 0 && (
<span className="text-yellow-600"> {suggestion.availabilityConflicts.length} conflicts</span>
)}
</div>
</div>
))}
</div>
)}
{!submitted && (
<div className="bg-white rounded-xl border border-gray-200 border-dashed p-12 text-center text-gray-300">
Fill in the criteria and click "Find Matches" to see staffing suggestions.
</div>
)}
</div>
</div>
);
}
function ScoreBar({ label, value }: { label: string; value: number }) {
return (
<div>
<div className="text-xs text-gray-500 mb-1">{label}</div>
<div className="h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full bg-brand-500 rounded-full"
style={{ width: `${value}%` }}
/>
</div>
<div className="text-xs text-gray-600 mt-0.5">{value}</div>
</div>
);
}