feat: Sprint 4 — scenario planner, report builder, comments, dashboard widgets

What-If Scenario Planner (G5):
- New /projects/[id]/scenario page with side-by-side baseline vs scenario
- simulate mutation: pure cost/hours/headcount/utilization computation
- apply mutation: creates real PROPOSED assignments from scenario
- Impact cards: cost delta, hours delta, headcount, skill coverage %
- Per-resource utilization impact table with over-allocation warnings
- "What-If" button added to project detail page

Custom Report Builder (G7):
- New /reports/builder page with full config panel
- Entity selector (resource/project/assignment), column picker, filter builder
- Dynamic Prisma query with eq/neq/gt/lt/contains/in operators
- Sortable results table with pagination (50/page)
- CSV export via exportReport mutation
- Sidebar nav link under Analytics

Collaboration Layer (G8):
- Comment model in Prisma (entityType/entityId, replies, @mentions, resolved)
- comment router: list, count, create, resolve, delete
- @mention parsing with notification creation + SSE delivery
- CommentInput with @mention autocomplete (arrow nav, Enter/Tab confirm)
- CommentThread with avatar, timestamp, reply, resolve, delete
- Integrated as "Comments" tab in estimate workspace with count badge

Dashboard Widgets:
- BudgetForecastWidget: progress bars per project, burn rate, exhaustion date
- SkillGapWidget: supply vs demand per skill, shortage/surplus indicators
- ProjectHealthWidget: 3-dimension health circles + composite score
- 3 new application use-cases + dashboard router queries
- All registered in widget-registry with lazy imports

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-19 21:47:47 +01:00
parent 6f34659587
commit e1368c7ef7
27 changed files with 3889 additions and 1 deletions
@@ -84,6 +84,16 @@ export default async function ProjectDetailPage({ params }: ProjectDetailPagePro
</div>
<div className="mt-0.5 flex items-center">Win probability: {project.winProbability}%<InfoTooltip content="Likelihood of winning this project (0-100%). Used to calculate weighted pipeline value (budget x probability)." /></div>
</div>
<Link
href={`/projects/${id}/scenario`}
className="inline-flex items-center gap-2 rounded-lg border border-indigo-300 bg-white px-3 py-2 text-sm font-medium text-indigo-700 shadow-sm hover:bg-indigo-50 transition dark:border-indigo-600 dark:bg-gray-800 dark:text-indigo-300 dark:hover:bg-indigo-900/20"
title="Open What-If Scenario Planner"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
What-If
</Link>
<ProjectDetailActions project={project as never} />
</div>
</div>
@@ -0,0 +1,58 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { createCaller } from "~/server/trpc.js";
import { ScenarioPlanner } from "~/components/projects/ScenarioPlanner.js";
interface ScenarioPageProps {
params: Promise<{ id: string }>;
}
export default async function ScenarioPage({ params }: ScenarioPageProps) {
const { id } = await params;
const trpc = await createCaller();
let baseline: Awaited<ReturnType<typeof trpc.scenario.getProjectBaseline>>;
try {
baseline = await trpc.scenario.getProjectBaseline({ projectId: id });
} catch {
notFound();
}
// Load resources and roles for the pickers
const [resources, roles] = await Promise.all([
trpc.resource.list({ isActive: true }),
trpc.role.list({ isActive: true }),
]);
return (
<div className="p-6 max-w-7xl mx-auto space-y-6">
<Link
href={`/projects/${id}`}
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 {baseline.project.name}
</Link>
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
What-If Scenario Planner
</h1>
<p className="mt-1 text-sm text-gray-500">
Explore alternate staffing configurations for{" "}
<span className="font-medium text-gray-700 dark:text-gray-300">{baseline.project.name}</span>{" "}
and see instant cost/schedule impact.
</p>
</div>
<ScenarioPlanner
projectId={id}
baseline={baseline}
resources={resources as never}
roles={roles as never}
/>
</div>
);
}
@@ -0,0 +1,5 @@
import { ReportBuilder } from "~/components/reports/ReportBuilder.js";
export default function ReportBuilderPage() {
return <ReportBuilder />;
}