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:
@@ -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 />;
|
||||
}
|
||||
Reference in New Issue
Block a user