"use client"; import { useMemo, useState } from "react"; import { clsx } from "clsx"; import { trpc } from "~/lib/trpc/client.js"; interface WeeklyPhasingViewProps { estimateId: string; canEdit: boolean; } type ViewMode = "by_line" | "by_chapter"; type PhasingPattern = "even" | "front_loaded" | "back_loaded"; function getDefaultDateRange(): { start: string; end: string } { const now = new Date(); const start = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`; const endDate = new Date(now.getFullYear(), now.getMonth() + 6, 0); const end = `${endDate.getFullYear()}-${String(endDate.getMonth() + 1).padStart(2, "0")}-${String(endDate.getDate()).padStart(2, "0")}`; return { start, end }; } function heatColor(hours: number, maxHours: number): string { if (hours === 0 || maxHours === 0) return ""; const ratio = Math.min(hours / maxHours, 1); if (ratio < 0.25) return "bg-blue-50 dark:bg-blue-900/20"; if (ratio < 0.5) return "bg-blue-100 dark:bg-blue-900/30"; if (ratio < 0.75) return "bg-blue-200 dark:bg-blue-900/40"; return "bg-blue-300 dark:bg-blue-900/50"; } export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProps) { const defaults = getDefaultDateRange(); const [startDate, setStartDate] = useState(defaults.start); const [endDate, setEndDate] = useState(defaults.end); const [pattern, setPattern] = useState("even"); const [viewMode, setViewMode] = useState("by_line"); const utils = trpc.useUtils(); const phasingQuery = trpc.estimate.getWeeklyPhasing.useQuery( { estimateId }, { staleTime: 30_000 }, ); const generateMutation = trpc.estimate.generateWeeklyPhasing.useMutation({ onSuccess: () => { void utils.estimate.getWeeklyPhasing.invalidate({ estimateId }); void utils.estimate.getById.invalidate({ id: estimateId }); }, }); const data = phasingQuery.data; // Compute max hours for heat-map coloring const maxHours = useMemo(() => { if (!data?.hasPhasing) return 0; let max = 0; for (const line of data.lines) { for (const h of Object.values(line.weeklyHours)) { if (h > max) max = h; } } return max; }, [data]); // Compute column totals const columnTotals = useMemo(() => { if (!data?.hasPhasing) return {}; const totals: Record = {}; for (const line of data.lines) { for (const [weekKey, hours] of Object.entries(line.weeklyHours)) { totals[weekKey] = Math.round(((totals[weekKey] ?? 0) + hours) * 100) / 100; } } return totals; }, [data]); // Compute chapter column totals const chapterColumnTotals = useMemo(() => { if (!data?.hasPhasing) return {}; const totals: Record = {}; for (const chapterHours of Object.values(data.chapterAggregation)) { for (const [weekKey, hours] of Object.entries(chapterHours)) { totals[weekKey] = Math.round(((totals[weekKey] ?? 0) + hours) * 100) / 100; } } return totals; }, [data]); // Compute max hours for chapter view const maxChapterHours = useMemo(() => { if (!data?.hasPhasing) return 0; let max = 0; for (const chapterHours of Object.values(data.chapterAggregation)) { for (const h of Object.values(chapterHours)) { if (h > max) max = h; } } return max; }, [data]); const handleGenerate = () => { generateMutation.mutate({ estimateId, startDate, endDate, pattern, }); }; // Use config dates from existing phasing if available const effectiveStart = data?.hasPhasing ? data.config.startDate : startDate; const effectiveEnd = data?.hasPhasing ? data.config.endDate : endDate; return (
{/* Header / Controls */}

Weekly Phasing (4Dispo)

{canEdit && (
setStartDate(e.target.value)} className="rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 text-sm" />
setEndDate(e.target.value)} className="rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 text-sm" />
)} {generateMutation.isError && (

{generateMutation.error.message}

)} {generateMutation.isSuccess && (

Phasing generated for {generateMutation.data.linesUpdated} demand lines.

)}
{/* View toggle */} {data?.hasPhasing && (
)} {/* Phasing Grid */} {phasingQuery.isLoading && (
Loading phasing data...
)} {data && !data.hasPhasing && (
No weekly phasing generated yet. Use the controls above to generate a phasing distribution.
)} {data?.hasPhasing && viewMode === "by_line" && (
{data.weeks.map((week) => { const key = `${week.year}-W${String(week.weekNumber).padStart(2, "0")}`; return ( ); })} {data.lines.map((line) => { const lineTotal = Object.values(line.weeklyHours).reduce( (sum, h) => sum + h, 0, ); return ( {data.weeks.map((week) => { const key = `${week.year}-W${String(week.weekNumber).padStart(2, "0")}`; const hours = line.weeklyHours[key] ?? 0; return ( ); })} ); })} {data.weeks.map((week) => { const key = `${week.year}-W${String(week.weekNumber).padStart(2, "0")}`; const total = columnTotals[key] ?? 0; return ( ); })}
Demand Line {week.label} Total
{line.name}
{line.chapter && (
{line.chapter}
)}
{hours > 0 ? hours.toFixed(1) : "-"} {lineTotal.toFixed(1)}
Total {total > 0 ? total.toFixed(1) : "-"} {Object.values(columnTotals) .reduce((sum, h) => sum + h, 0) .toFixed(1)}
)} {data?.hasPhasing && viewMode === "by_chapter" && (
{data.weeks.map((week) => { const key = `${week.year}-W${String(week.weekNumber).padStart(2, "0")}`; return ( ); })} {Object.entries(data.chapterAggregation) .sort(([a], [b]) => a.localeCompare(b)) .map(([chapter, weeklyHours]) => { const chapterTotal = Object.values(weeklyHours).reduce( (sum, h) => sum + h, 0, ); return ( {data.weeks.map((week) => { const key = `${week.year}-W${String(week.weekNumber).padStart(2, "0")}`; const hours = weeklyHours[key] ?? 0; return ( ); })} ); })} {data.weeks.map((week) => { const key = `${week.year}-W${String(week.weekNumber).padStart(2, "0")}`; const total = chapterColumnTotals[key] ?? 0; return ( ); })}
Chapter {week.label} Total
{chapter} {hours > 0 ? hours.toFixed(1) : "-"} {chapterTotal.toFixed(1)}
Total {total > 0 ? total.toFixed(1) : "-"} {Object.values(chapterColumnTotals) .reduce((sum, h) => sum + h, 0) .toFixed(1)}
)} {/* Info about current phasing config */} {data?.hasPhasing && data.config && (

Current phasing:{" "} {data.config.pattern.replace("_", " ")} distribution from{" "} {data.config.startDate} to {data.config.endDate} across{" "} {data.weeks.length} weeks, {data.lines.length} demand lines.

)}
); }