import { useRef, useEffect, useMemo } from "react"; import { useShallow } from "zustand/react/shallow"; import { useMachineStore } from "../stores/useMachineStore"; import { usePatternStore } from "../stores/usePatternStore"; import { CheckCircleIcon, ArrowRightIcon, CircleStackIcon, PlayIcon, ChartBarIcon, ArrowPathIcon, } from "@heroicons/react/24/solid"; import { MachineStatus } from "../types/machine"; import { canStartSewing, canStartMaskTrace, canResumeSewing, } from "../utils/machineStateHelpers"; import { calculatePatternTime } from "../utils/timeCalculation"; import { Card, CardHeader, CardTitle, CardDescription, CardContent, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Progress } from "@/components/ui/progress"; import { ScrollArea } from "@/components/ui/scroll-area"; export function ProgressMonitor() { // Machine store const { machineStatus, patternInfo, sewingProgress, isDeleting, startMaskTrace, startSewing, resumeSewing, } = useMachineStore( useShallow((state) => ({ machineStatus: state.machineStatus, patternInfo: state.patternInfo, sewingProgress: state.sewingProgress, isDeleting: state.isDeleting, startMaskTrace: state.startMaskTrace, startSewing: state.startSewing, resumeSewing: state.resumeSewing, })), ); // Pattern store const pesData = usePatternStore((state) => state.pesData); const currentBlockRef = useRef(null); // State indicators const isMaskTraceComplete = machineStatus === MachineStatus.MASK_TRACE_COMPLETE; // Use PEN stitch count as fallback when machine reports 0 total stitches const totalStitches = patternInfo ? patternInfo.totalStitches === 0 && pesData?.penStitches ? pesData.penStitches.stitches.length : patternInfo.totalStitches : 0; const progressPercent = totalStitches > 0 ? ((sewingProgress?.currentStitch || 0) / totalStitches) * 100 : 0; // Calculate color block information from decoded penStitches const colorBlocks = useMemo(() => { if (!pesData || !pesData.penStitches) return []; const blocks: Array<{ colorIndex: number; threadHex: string; startStitch: number; endStitch: number; stitchCount: number; threadCatalogNumber: string | null; threadBrand: string | null; threadDescription: string | null; threadChart: string | null; }> = []; // Use the pre-computed color blocks from decoded PEN data for (const penBlock of pesData.penStitches.colorBlocks) { const thread = pesData.threads[penBlock.colorIndex]; blocks.push({ colorIndex: penBlock.colorIndex, threadHex: thread?.hex || "#000000", threadCatalogNumber: thread?.catalogNumber ?? null, threadBrand: thread?.brand ?? null, threadDescription: thread?.description ?? null, threadChart: thread?.chart ?? null, startStitch: penBlock.startStitchIndex, endStitch: penBlock.endStitchIndex, stitchCount: penBlock.endStitchIndex - penBlock.startStitchIndex, }); } return blocks; }, [pesData]); // Determine current color block based on current stitch const currentStitch = sewingProgress?.currentStitch || 0; const currentBlockIndex = colorBlocks.findIndex( (block) => currentStitch >= block.startStitch && currentStitch < block.endStitch, ); // Calculate time based on color blocks (matches Brother app calculation) const { totalMinutes, elapsedMinutes } = useMemo(() => { if (colorBlocks.length === 0) { return { totalMinutes: 0, elapsedMinutes: 0 }; } const result = calculatePatternTime(colorBlocks, currentStitch); return { totalMinutes: result.totalMinutes, elapsedMinutes: result.elapsedMinutes, }; }, [colorBlocks, currentStitch]); // Auto-scroll to current block useEffect(() => { if (currentBlockRef.current) { currentBlockRef.current.scrollIntoView({ behavior: "smooth", block: "nearest", }); } }, [currentBlockIndex]); return (
Sewing Progress {sewingProgress && ( {progressPercent.toFixed(1)}% complete )}
{/* Pattern Info */} {patternInfo && (
Total Stitches {totalStitches.toLocaleString()}
Total Time {totalMinutes} min
Speed {patternInfo.speed} spm
)} {/* Progress Bar */} {sewingProgress && (
Current Stitch {sewingProgress.currentStitch.toLocaleString()} /{" "} {totalStitches.toLocaleString()}
Time {elapsedMinutes} / {totalMinutes} min
)} {/* Color Blocks */} {colorBlocks.length > 0 && (

Color Blocks

{colorBlocks.map((block, index) => { const isCompleted = currentStitch >= block.endStitch; const isCurrent = index === currentBlockIndex; // Calculate progress within current block let blockProgress = 0; if (isCurrent) { blockProgress = ((currentStitch - block.startStitch) / block.stitchCount) * 100; } else if (isCompleted) { blockProgress = 100; } return (
{/* Color swatch */}
{/* Thread info */}
Thread {block.colorIndex + 1} {(block.threadBrand || block.threadChart || block.threadDescription || block.threadCatalogNumber) && ( {" "} ( {(() => { // Primary metadata: brand and catalog number const primaryMetadata = [ block.threadBrand, block.threadCatalogNumber ? `#${block.threadCatalogNumber}` : null, ] .filter(Boolean) .join(" "); // Secondary metadata: chart and description const secondaryMetadata = [ block.threadChart, block.threadDescription, ] .filter(Boolean) .join(" "); return [primaryMetadata, secondaryMetadata] .filter(Boolean) .join(" • "); })()} ) )}
{block.stitchCount.toLocaleString()} stitches
{/* Status icon */} {isCompleted ? ( ) : isCurrent ? ( ) : ( )}
{/* Progress bar for current block */} {isCurrent && ( )}
); })}
)} {/* Action buttons */}
{/* Resume has highest priority when available */} {canResumeSewing(machineStatus) && ( )} {/* Start Sewing - primary action, takes more space */} {canStartSewing(machineStatus) && !canResumeSewing(machineStatus) && ( )} {/* Start Mask Trace - secondary action */} {canStartMaskTrace(machineStatus) && ( )}
); }