5ffc0d92e4
- xlsx dynamically imported via cached singleton in excel.ts and skillMatrixParser.ts (removes ~100 kB from 4 routes) - recharts extracted into lazy-loaded SkillDistributionChart and PeakTimesChart components (removes ~60 kB from 3 routes) - EstimateWorkspaceClient: 7 tab components + 2 editors loaded via next/dynamic (reduces /estimates/[id] from 323 kB to 138 kB) - ImportModal lazy-loaded in ResourcesClient (deferred until open) - NavItem memoized with React.memo, top 5 routes get prefetch=true - Raw <img> replaced with next/image in ProjectsClient, CoverArtSection - tRPC QueryClient: refetchOnWindowFocus/Reconnect disabled globally Heaviest routes reduced 39-66% First Load JS: /analytics/skills: 383→132 kB (-66%) /estimates/[id]: 323→138 kB (-57%) /resources/[id]: 458→210 kB (-54%) /estimates: 310→170 kB (-45%) /resources: 363→222 kB (-39%) Co-Authored-By: claude-flow <ruv@ruv.net>
105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
"use client";
|
||
|
||
import dynamic from "next/dynamic";
|
||
import { trpc } from "~/lib/trpc/client.js";
|
||
import type { WidgetProps } from "~/components/dashboard/widget-registry.js";
|
||
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
|
||
|
||
const PeakTimesChart = dynamic(
|
||
() => import("~/components/dashboard/widgets/PeakTimesChart.js"),
|
||
{ ssr: false, loading: () => <div className="flex-1 shimmer-skeleton rounded-xl" /> },
|
||
);
|
||
|
||
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="flex flex-col gap-3 h-full pt-2">
|
||
<div className="flex gap-2">
|
||
<div className="h-7 w-28 shimmer-skeleton rounded-lg" />
|
||
<div className="h-7 w-28 shimmer-skeleton rounded-lg" />
|
||
</div>
|
||
<div className="flex items-end gap-1 flex-1 px-2">
|
||
{[...Array(12)].map((_, i) => (
|
||
<div
|
||
key={i}
|
||
className="flex-1 shimmer-skeleton 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">
|
||
<PeakTimesChart chartData={chartData} groups={groups} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|