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": {