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,251 @@
import { notFound } from "next/navigation";
import { formatDate } from "~/lib/format.js";
import Link from "next/link";
import { createCaller } from "~/server/trpc.js";
import { BudgetStatusCard } from "~/components/projects/BudgetStatusCard.js";
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
interface ProjectDetailPageProps {
params: Promise<{ id: string }>;
}
const STATUS_COLORS: Record<string, string> = {
DRAFT: "bg-gray-100 text-gray-700",
ACTIVE: "bg-green-100 text-green-700",
ON_HOLD: "bg-yellow-100 text-yellow-700",
COMPLETED: "bg-blue-100 text-blue-700",
CANCELLED: "bg-red-100 text-red-700",
};
const ORDER_TYPE_COLORS: Record<string, string> = {
BD: "bg-purple-100 text-purple-700",
CHARGEABLE: "bg-green-100 text-green-700",
INTERNAL: "bg-blue-100 text-blue-700",
OVERHEAD: "bg-gray-100 text-gray-700",
};
const ALLOC_STATUS_COLORS: Record<string, string> = {
ACTIVE: "bg-green-100 text-green-700",
PROPOSED: "bg-yellow-100 text-yellow-700",
CONFIRMED: "bg-blue-100 text-blue-700",
CANCELLED: "bg-gray-100 text-gray-500",
};
export default async function ProjectDetailPage({ params }: ProjectDetailPageProps) {
const { id } = await params;
const trpc = await createCaller();
let project: Awaited<ReturnType<typeof trpc.project.getById>>;
try {
project = await trpc.project.getById({ id });
} catch {
notFound();
}
const activeAssignments = project.assignments.filter((assignment) => assignment.status !== "CANCELLED");
const activeDemands = project.demands.filter((demand) => demand.status !== "CANCELLED");
const requestedSeats = activeDemands.reduce((sum, demand) => sum + demand.requestedHeadcount, 0);
const unfilledSeats = activeDemands.reduce((sum, demand) => sum + demand.unfilledHeadcount, 0);
return (
<div className="p-6 max-w-5xl mx-auto space-y-6">
{/* Back link */}
<Link
href="/projects"
className="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-gray-800 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Back to Projects
</Link>
{/* Project header */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-start justify-between gap-4 mb-4">
<div>
<div className="flex items-center gap-3 mb-1">
<span className="font-mono text-sm font-medium text-gray-500">{project.shortCode}</span>
<span className={`inline-block px-2 py-0.5 text-xs rounded-full ${STATUS_COLORS[project.status] ?? ""}`}>
{project.status}
</span>
<span className={`inline-block px-2 py-0.5 text-xs rounded-full ${ORDER_TYPE_COLORS[project.orderType] ?? ""}`}>
{project.orderType}
</span>
</div>
<h1 className="text-2xl font-bold text-gray-900">{project.name}</h1>
</div>
<div className="text-right text-sm text-gray-500 flex-shrink-0">
<div className="font-medium text-gray-800">
{formatDate(project.startDate)}
{" — "}
{formatDate(project.endDate)}
</div>
<div className="mt-0.5">Win probability: {project.winProbability}%</div>
</div>
</div>
<dl className="grid grid-cols-2 gap-x-8 gap-y-3 sm:grid-cols-4 mt-4 pt-4 border-t border-gray-100">
<div>
<dt className="text-xs text-gray-500">Chargecode</dt>
<dd className="mt-0.5 text-sm font-mono font-medium text-gray-900">{project.shortCode}</dd>
</div>
<div>
<dt className="text-xs text-gray-500">Order Type</dt>
<dd className="mt-0.5 text-sm text-gray-900">{project.orderType}</dd>
</div>
<div>
<dt className="text-xs text-gray-500">Allocation Type</dt>
<dd className="mt-0.5 text-sm text-gray-900">{project.allocationType}</dd>
</div>
<div>
<dt className="text-xs text-gray-500">Assignments</dt>
<dd className="mt-0.5 text-sm text-gray-900">{activeAssignments.length} active</dd>
</div>
<div>
<dt className="text-xs text-gray-500">Open Demands</dt>
<dd className="mt-0.5 text-sm text-gray-900">
{activeDemands.length} items · {unfilledSeats}/{requestedSeats} seats unfilled
</dd>
</div>
{project.responsiblePerson && (
<div className="sm:col-span-2">
<dt className="text-xs text-gray-500">Responsible Person</dt>
<dd className="mt-0.5 text-sm font-medium text-gray-900">{project.responsiblePerson}</dd>
</div>
)}
</dl>
</div>
{/* Budget status card (client component) */}
<BudgetStatusCard projectId={project.id} />
{/* Assignments table */}
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-sm font-semibold text-gray-700 uppercase tracking-wider">
Assignments ({project.assignments.length})
</h2>
</div>
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Resource</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Role <InfoTooltip content="Role this allocation was created for." />
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Period <InfoTooltip content="Start and end date of the allocation." />
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
<span className="inline-flex items-center justify-end gap-0.5">
Hours/Day <InfoTooltip content="Planned working hours per calendar day." />
</span>
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
<span className="inline-flex items-center justify-end gap-0.5">
Daily Cost <InfoTooltip content="Resource LCR × hours per day." />
</span>
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status <InfoTooltip content="PROPOSED = requested · CONFIRMED = approved · ACTIVE = ongoing · COMPLETED = finished · CANCELLED = removed." />
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{project.assignments.map((assignment) => (
<tr key={assignment.id} className="hover:bg-gray-50 transition-colors">
<td className="px-4 py-3 text-sm font-medium text-gray-900">
{assignment.resource?.displayName ?? "—"}
{assignment.resource?.eid && (
<span className="ml-1.5 text-xs text-gray-400 font-mono">{assignment.resource.eid}</span>
)}
</td>
<td className="px-4 py-3 text-sm text-gray-600">{assignment.role || "—"}</td>
<td className="px-4 py-3 text-xs text-gray-500">
{formatDate(assignment.startDate)}
{" → "}
{formatDate(assignment.endDate)}
</td>
<td className="px-4 py-3 text-sm text-gray-900">{assignment.hoursPerDay}h</td>
<td className="px-4 py-3 text-sm text-gray-900">
{(assignment.dailyCostCents / 100).toLocaleString("de-DE", { minimumFractionDigits: 0 })}
</td>
<td className="px-4 py-3">
<span
className={`inline-block px-2 py-0.5 text-xs rounded-full ${ALLOC_STATUS_COLORS[assignment.status] ?? "bg-gray-100 text-gray-600"}`}
>
{assignment.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
{project.assignments.length === 0 && (
<div className="text-center py-12 text-gray-500 text-sm">No assignments for this project.</div>
)}
</div>
{/* Open demands table */}
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-sm font-semibold text-gray-700 uppercase tracking-wider">
Open Demands ({project.demands.length})
</h2>
</div>
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Role
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Period
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Requested
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Unfilled
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Hours/Day
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{project.demands.map((demand) => (
<tr key={demand.id} className="hover:bg-gray-50 transition-colors">
<td className="px-4 py-3 text-sm text-gray-900">
{demand.roleEntity?.name ?? demand.role ?? "Unassigned"}
</td>
<td className="px-4 py-3 text-xs text-gray-500">
{formatDate(demand.startDate)}
{" → "}
{formatDate(demand.endDate)}
</td>
<td className="px-4 py-3 text-sm text-right text-gray-900">{demand.requestedHeadcount}</td>
<td className="px-4 py-3 text-sm text-right text-gray-900">{demand.unfilledHeadcount}</td>
<td className="px-4 py-3 text-sm text-right text-gray-900">{demand.hoursPerDay}h</td>
<td className="px-4 py-3">
<span
className={`inline-block px-2 py-0.5 text-xs rounded-full ${ALLOC_STATUS_COLORS[demand.status] ?? "bg-gray-100 text-gray-600"}`}
>
{demand.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
{project.demands.length === 0 && (
<div className="text-center py-12 text-gray-500 text-sm">No open demands for this project.</div>
)}
</div>
</div>
);
}