diff --git a/src/components/terminal/DetailPanel.tsx b/src/components/terminal/DetailPanel.tsx index a01f9c79..0a94a511 100644 --- a/src/components/terminal/DetailPanel.tsx +++ b/src/components/terminal/DetailPanel.tsx @@ -12,8 +12,7 @@ import { OperationWithDetails } from '@/lib/database'; import { cn } from '@/lib/utils'; import IssueForm from '@/components/operator/IssueForm'; import ProductionQuantityModal from '@/components/operator/ProductionQuantityModal'; -import { NextCellInfo } from './NextCellInfo'; -import { RoutingVisualization } from './RoutingVisualization'; +import { JobFlowProgress } from './JobFlowProgress'; import { useCellQRMMetrics } from '@/hooks/useQRMMetrics'; import { useAuth } from '@/contexts/AuthContext'; import { createPortal } from 'react-dom'; @@ -201,24 +200,17 @@ export function DetailPanel({ job, onStart, onPause, onComplete, stepUrl, pdfUrl - {/* QRM Section - Next Cell and Routing - Compact */} -
- {/* Next Cell Capacity */} - {nextOperation && ( - - )} - - {/* Routing Visualization */} - {job.jobId && ( - + - )} -
+ + )} {/* CNC Program QR Code - Compact inline display */} {job.cncProgramName && ( diff --git a/src/components/terminal/JobFlowProgress.tsx b/src/components/terminal/JobFlowProgress.tsx new file mode 100644 index 00000000..641e5fda --- /dev/null +++ b/src/components/terminal/JobFlowProgress.tsx @@ -0,0 +1,183 @@ +import React from 'react'; +import { CheckCircle2, Circle, ArrowRight, AlertTriangle, XCircle, Loader2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { useJobRouting } from '@/hooks/useQRMMetrics'; +import type { CellQRMMetrics } from '@/types/qrm'; +import { useTranslation } from 'react-i18next'; + +interface JobFlowProgressProps { + jobId: string; + currentCellId?: string; + nextCellName?: string; + nextCellMetrics: CellQRMMetrics | null; + className?: string; +} + +/** + * Compact job flow progress indicator that replaces the verbose + * NextCellInfo + RoutingVisualization combo with a cleaner UX. + * + * Shows: [progress dots] Current Cell → Next Cell [capacity status] + */ +export function JobFlowProgress({ + jobId, + currentCellId, + nextCellName, + nextCellMetrics, + className +}: JobFlowProgressProps) { + const { t } = useTranslation(); + const { routing, loading, error } = useJobRouting(jobId); + + if (loading) { + return ( +
+ + {t('terminal.flowProgress.loading', 'Loading routing...')} +
+ ); + } + + if (error || !routing || routing.length === 0) { + return null; + } + + const currentIndex = routing.findIndex(step => step.cell_id === currentCellId); + const totalSteps = routing.length; + const currentStep = currentIndex + 1; + const isLastStep = currentIndex === totalSteps - 1; + + // Determine next cell capacity status + const getCapacityStatus = () => { + if (!nextCellMetrics || isLastStep) return null; + + const hasLimit = nextCellMetrics.wip_limit !== null && nextCellMetrics.wip_limit !== undefined; + if (!hasLimit) return { status: 'ok', label: t('terminal.flowProgress.noLimit', 'No limit') }; + + const currentWip = nextCellMetrics.current_wip || 0; + const wipLimit = nextCellMetrics.wip_limit || 0; + const utilizationPercent = wipLimit > 0 ? (currentWip / wipLimit) * 100 : 0; + + if (currentWip >= wipLimit) { + return { + status: 'blocked', + label: t('terminal.flowProgress.atCapacity', 'At capacity'), + count: `${currentWip}/${wipLimit}` + }; + } else if (utilizationPercent >= 80) { + return { + status: 'warning', + label: t('terminal.flowProgress.nearCapacity', 'Near capacity'), + count: `${currentWip}/${wipLimit}` + }; + } + return { + status: 'ok', + label: t('terminal.flowProgress.available', 'Available'), + count: `${currentWip}/${wipLimit}` + }; + }; + + const capacityStatus = getCapacityStatus(); + + return ( +
+ {/* Progress indicator row */} +
+ {/* Step dots */} +
+ {routing.map((step, index) => { + const isCompleted = step.completed_operations === step.operation_count && step.operation_count > 0; + const isCurrent = step.cell_id === currentCellId; + + return ( +
+ ); + })} +
+ + {/* Step counter */} + + {t('terminal.flowProgress.step', 'Step')} {currentStep}/{totalSteps} + +
+ + {/* Current → Next with capacity */} +
+ {/* Current cell */} +
+
+ + {routing[currentIndex]?.cell_name || t('terminal.flowProgress.unknown', 'Unknown')} + +
+ + {/* Arrow to next */} + {!isLastStep && ( + <> + + + {/* Next cell with capacity */} +
+ + {nextCellName || routing[currentIndex + 1]?.cell_name} + + + {/* Capacity indicator */} + {capacityStatus && ( +
+ {capacityStatus.status === 'blocked' && } + {capacityStatus.status === 'warning' && } + {capacityStatus.status === 'ok' && } + {capacityStatus.count && ( + {capacityStatus.count} + )} +
+ )} +
+ + )} + + {/* Last step indicator */} + {isLastStep && ( +
+ + {t('terminal.flowProgress.finalStep', 'Final step')} +
+ )} +
+ + {/* Blocking message */} + {capacityStatus?.status === 'blocked' && nextCellMetrics?.enforce_limit && ( +
+ + {t('terminal.flowProgress.cannotProceed', 'Cannot proceed - next cell at capacity')} +
+ )} +
+ ); +} diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index 9644435a..ca7a7f6d 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -1497,6 +1497,17 @@ "inProgress": "In Bearbeitung", "completed": "Abgeschlossen" } + }, + "flowProgress": { + "loading": "Route wird geladen...", + "step": "Schritt", + "noLimit": "Kein Limit", + "atCapacity": "Kapazität erreicht", + "nearCapacity": "Fast voll", + "available": "Verfügbar", + "unknown": "Unbekannt", + "finalStep": "Letzter Schritt", + "cannotProceed": "Kann nicht fortfahren - nächste Zelle auf Kapazität" } }, "activityMonitor": { diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 5e4eb88f..382cacd8 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -1740,6 +1740,17 @@ "inProgress": "In Progress", "completed": "Completed" } + }, + "flowProgress": { + "loading": "Loading routing...", + "step": "Step", + "noLimit": "No limit", + "atCapacity": "At capacity", + "nearCapacity": "Near capacity", + "available": "Available", + "unknown": "Unknown", + "finalStep": "Final step", + "cannotProceed": "Cannot proceed - next cell at capacity" } }, "activityMonitor": { diff --git a/src/i18n/locales/nl/translation.json b/src/i18n/locales/nl/translation.json index e2a14eb3..7139f4db 100644 --- a/src/i18n/locales/nl/translation.json +++ b/src/i18n/locales/nl/translation.json @@ -1767,6 +1767,17 @@ "inProgress": "In bewerking", "completed": "Voltooid" } + }, + "flowProgress": { + "loading": "Route laden...", + "step": "Stap", + "noLimit": "Geen limiet", + "atCapacity": "Op capaciteit", + "nearCapacity": "Bijna vol", + "available": "Beschikbaar", + "unknown": "Onbekend", + "finalStep": "Laatste stap", + "cannotProceed": "Kan niet doorgaan - volgende cel op capaciteit" } }, "activityMonitor": {