Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 9 additions & 17 deletions src/components/terminal/DetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -201,24 +200,17 @@ export function DetailPanel({ job, onStart, onPause, onComplete, stepUrl, pdfUrl
</div>
</div>

{/* QRM Section - Next Cell and Routing - Compact */}
<div className="p-2 border-b border-border bg-muted/20 space-y-2">
{/* Next Cell Capacity */}
{nextOperation && (
<NextCellInfo
nextCellName={nextOperation.cell?.name || 'Unknown Cell'}
metrics={nextCellMetrics}
/>
)}

{/* Routing Visualization */}
{job.jobId && (
<RoutingVisualization
{/* Job Flow Progress - Compact routing indicator */}
{job.jobId && (
<div className="px-3 py-2 border-b border-border bg-muted/20">
<JobFlowProgress
jobId={job.jobId}
currentCellId={job.cellId}
nextCellName={nextOperation?.cell?.name}
nextCellMetrics={nextCellMetrics}
/>
)}
</div>
</div>
)}

{/* CNC Program QR Code - Compact inline display */}
{job.cncProgramName && (
Expand Down
183 changes: 183 additions & 0 deletions src/components/terminal/JobFlowProgress.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={cn("flex items-center gap-2 text-muted-foreground text-xs", className)}>
<Loader2 className="w-3 h-3 animate-spin" />
<span>{t('terminal.flowProgress.loading', 'Loading routing...')}</span>
</div>
);
}

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 (
<div className={cn("space-y-2", className)}>
{/* Progress indicator row */}
<div className="flex items-center justify-between gap-2">
{/* Step dots */}
<div className="flex items-center gap-1">
{routing.map((step, index) => {
const isCompleted = step.completed_operations === step.operation_count && step.operation_count > 0;
const isCurrent = step.cell_id === currentCellId;

return (
<div
key={step.cell_id}
className={cn(
"w-2 h-2 rounded-full transition-all",
isCompleted && "bg-status-completed",
isCurrent && "bg-primary ring-2 ring-primary/30 scale-125",
!isCompleted && !isCurrent && "bg-muted-foreground/30"
)}
title={step.cell_name}
/>
);
})}
</div>

{/* Step counter */}
<span className="text-[10px] text-muted-foreground font-medium">
{t('terminal.flowProgress.step', 'Step')} {currentStep}/{totalSteps}
</span>
</div>

{/* Current → Next with capacity */}
<div className={cn(
"flex items-center gap-2 px-2.5 py-2 rounded-lg border",
capacityStatus?.status === 'blocked' && "bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800",
capacityStatus?.status === 'warning' && "bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800",
(!capacityStatus || capacityStatus.status === 'ok') && "bg-muted/30 border-border"
)}>
{/* Current cell */}
<div className="flex items-center gap-1.5 min-w-0">
<div className="w-2 h-2 rounded-full bg-primary animate-pulse shrink-0" />
<span className="text-sm font-semibold text-foreground truncate">
{routing[currentIndex]?.cell_name || t('terminal.flowProgress.unknown', 'Unknown')}
</span>
</div>

{/* Arrow to next */}
{!isLastStep && (
<>
<ArrowRight className="w-4 h-4 text-muted-foreground shrink-0" />

{/* Next cell with capacity */}
<div className="flex items-center gap-1.5 min-w-0 flex-1">
<span className={cn(
"text-sm font-medium truncate",
capacityStatus?.status === 'blocked' && "text-red-700 dark:text-red-300",
capacityStatus?.status === 'warning' && "text-amber-700 dark:text-amber-300",
capacityStatus?.status === 'ok' && "text-foreground"
)}>
{nextCellName || routing[currentIndex + 1]?.cell_name}
</span>

{/* Capacity indicator */}
{capacityStatus && (
<div className={cn(
"flex items-center gap-1 text-[10px] font-medium shrink-0",
capacityStatus.status === 'blocked' && "text-red-600 dark:text-red-400",
capacityStatus.status === 'warning' && "text-amber-600 dark:text-amber-400",
capacityStatus.status === 'ok' && "text-emerald-600 dark:text-emerald-400"
)}>
{capacityStatus.status === 'blocked' && <XCircle className="w-3 h-3" />}
{capacityStatus.status === 'warning' && <AlertTriangle className="w-3 h-3" />}
{capacityStatus.status === 'ok' && <CheckCircle2 className="w-3 h-3" />}
{capacityStatus.count && (
<span className="font-mono">{capacityStatus.count}</span>
)}
</div>
)}
</div>
</>
)}

{/* Last step indicator */}
{isLastStep && (
<div className="flex items-center gap-1.5 text-xs text-emerald-600 dark:text-emerald-400">
<CheckCircle2 className="w-3.5 h-3.5" />
<span className="font-medium">{t('terminal.flowProgress.finalStep', 'Final step')}</span>
</div>
)}
</div>

{/* Blocking message */}
{capacityStatus?.status === 'blocked' && nextCellMetrics?.enforce_limit && (
<div className="flex items-center gap-1.5 text-[10px] text-red-600 dark:text-red-400 font-medium px-1">
<XCircle className="w-3 h-3" />
<span>{t('terminal.flowProgress.cannotProceed', 'Cannot proceed - next cell at capacity')}</span>
</div>
)}
</div>
);
}
11 changes: 11 additions & 0 deletions src/i18n/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
11 changes: 11 additions & 0 deletions src/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
11 changes: 11 additions & 0 deletions src/i18n/locales/nl/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading