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,138 @@
"use client";
import { trpc } from "~/lib/trpc/client.js";
import type { WidgetProps } from "~/components/dashboard/widget-registry.js";
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ReferenceLine,
ResponsiveContainer,
Legend,
} from "recharts";
const COLORS = [
"#6366f1", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6",
"#06b6d4", "#84cc16", "#f97316", "#ec4899", "#14b8a6",
];
export function PeakTimesWidget({ config, onConfigChange }: WidgetProps) {
const granularity = (config.granularity as "week" | "month") || "month";
const groupBy = (config.groupBy as "project" | "chapter" | "resource") || "project";
const now = new Date();
const startDate = new Date(now.getFullYear(), now.getMonth() - 2, 1).toISOString();
const endDate = new Date(now.getFullYear(), now.getMonth() + 6, 0).toISOString();
const { data, isLoading } = trpc.dashboard.getPeakTimes.useQuery(
{ startDate, endDate, granularity, groupBy },
{ staleTime: 120_000, placeholderData: (prev) => prev },
);
if (isLoading) {
return (
<div className="animate-pulse flex flex-col gap-3 h-full pt-2">
<div className="flex gap-2">
<div className="h-7 w-28 bg-gray-200 dark:bg-gray-700 rounded-lg" />
<div className="h-7 w-28 bg-gray-200 dark:bg-gray-700 rounded-lg" />
</div>
<div className="flex items-end gap-1 flex-1 px-2">
{[...Array(12)].map((_, i) => (
<div
key={i}
className="flex-1 bg-gray-200 dark:bg-gray-700 rounded-t"
style={{ height: `${30 + Math.random() * 50}%` }}
/>
))}
</div>
</div>
);
}
const periods = data ?? [];
// Collect all group names
const allGroups = new Set<string>();
for (const p of periods) {
for (const g of p.groups) allGroups.add(g.name);
}
const groups = [...allGroups].slice(0, 10);
// Build recharts data
const chartData = periods.map((p) => {
const row: Record<string, number | string> = { period: p.period, capacity: p.capacityHours };
for (const g of p.groups) {
row[g.name] = g.hours;
}
return row;
});
return (
<div className="flex flex-col h-full gap-3">
{/* Controls + info */}
<div className="flex gap-2 items-center">
<select
value={granularity}
onChange={(e) => onConfigChange?.({ granularity: e.target.value })}
className="px-2 py-1 text-xs border border-gray-300 rounded-lg bg-white"
>
<option value="month">Monthly</option>
<option value="week">Weekly</option>
</select>
<select
value={groupBy}
onChange={(e) => onConfigChange?.({ groupBy: e.target.value })}
className="px-2 py-1 text-xs border border-gray-300 rounded-lg bg-white"
>
<option value="project">By Project</option>
<option value="chapter">By Chapter</option>
<option value="resource">By Resource</option>
</select>
<InfoTooltip
content={
<span>
Stacked bars = booked hours per group per period (last 2 months to next 6 months).<br />
Red dashed line = total capacity estimate (all active resources × available hours per day × working days).<br />
Bars exceeding the capacity line indicate over-allocation risk.
</span>
}
width="w-80"
position="bottom"
/>
</div>
{/* Chart */}
<div className="flex-1 min-h-0">
{chartData.length === 0 ? (
<div className="flex items-center justify-center h-full text-sm text-gray-400">
No allocation data in selected period.
</div>
) : (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={chartData} margin={{ top: 4, right: 8, left: -10, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
<XAxis dataKey="period" tick={{ fontSize: 10 }} />
<YAxis tick={{ fontSize: 10 }} />
<Tooltip contentStyle={{ fontSize: 11 }} />
<Legend wrapperStyle={{ fontSize: 11 }} />
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
<ReferenceLine
{...({ dataKey: "capacity" } as any)}
stroke="#ef4444"
strokeDasharray="5 5"
label={{ value: "Capacity", fontSize: 10, fill: "#ef4444" }}
/>
{groups.map((g, i) => (
<Bar key={g} dataKey={g} stackId="a" fill={COLORS[i % COLORS.length]} />
))}
</BarChart>
</ResponsiveContainer>
)}
</div>
</div>
);
}