418 lines
16 KiB
TypeScript
418 lines
16 KiB
TypeScript
/**
|
|
* ProductionStatusBlock - Block 4: "Estado de Produccion"
|
|
*
|
|
* Displays today's production overview:
|
|
* - Late to start batches (should have started but haven't)
|
|
* - Currently running batches (IN_PROGRESS)
|
|
* - Pending batches for today
|
|
* - AI reasoning for batch scheduling
|
|
*/
|
|
|
|
import React, { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
AlertTriangle,
|
|
Brain,
|
|
CheckCircle2,
|
|
ChevronDown,
|
|
ChevronUp,
|
|
Clock,
|
|
Factory,
|
|
Play,
|
|
Timer,
|
|
} from 'lucide-react';
|
|
|
|
interface ProductionStatusBlockProps {
|
|
lateToStartBatches?: any[];
|
|
runningBatches?: any[];
|
|
pendingBatches?: any[];
|
|
onStartBatch?: (batchId: string) => Promise<void>;
|
|
onViewBatch?: (batchId: string) => void;
|
|
loading?: boolean;
|
|
}
|
|
|
|
export function ProductionStatusBlock({
|
|
lateToStartBatches = [],
|
|
runningBatches = [],
|
|
pendingBatches = [],
|
|
onStartBatch,
|
|
onViewBatch,
|
|
loading,
|
|
}: ProductionStatusBlockProps) {
|
|
const { t } = useTranslation(['dashboard', 'common', 'production']);
|
|
const [expandedReasoningId, setExpandedReasoningId] = useState<string | null>(null);
|
|
const [processingId, setProcessingId] = useState<string | null>(null);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="rounded-xl shadow-lg p-6 border border-[var(--border-primary)] bg-[var(--bg-primary)] animate-pulse">
|
|
<div className="flex items-center gap-4 mb-4">
|
|
<div className="w-12 h-12 bg-[var(--bg-secondary)] rounded-full"></div>
|
|
<div className="flex-1 space-y-2">
|
|
<div className="h-5 bg-[var(--bg-secondary)] rounded w-1/3"></div>
|
|
<div className="h-4 bg-[var(--bg-secondary)] rounded w-1/4"></div>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-3">
|
|
<div className="h-20 bg-[var(--bg-secondary)] rounded"></div>
|
|
<div className="h-20 bg-[var(--bg-secondary)] rounded"></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const hasLate = lateToStartBatches.length > 0;
|
|
const hasRunning = runningBatches.length > 0;
|
|
const hasPending = pendingBatches.length > 0;
|
|
const hasAnyProduction = hasLate || hasRunning || hasPending;
|
|
const totalCount = lateToStartBatches.length + runningBatches.length + pendingBatches.length;
|
|
|
|
// Determine header status
|
|
const status = hasLate ? 'error' : hasRunning ? 'info' : hasPending ? 'warning' : 'success';
|
|
|
|
const statusStyles = {
|
|
success: {
|
|
iconBg: 'bg-[var(--color-success-100)]',
|
|
iconColor: 'text-[var(--color-success-600)]',
|
|
},
|
|
warning: {
|
|
iconBg: 'bg-[var(--color-warning-100)]',
|
|
iconColor: 'text-[var(--color-warning-600)]',
|
|
},
|
|
info: {
|
|
iconBg: 'bg-[var(--color-info-100)]',
|
|
iconColor: 'text-[var(--color-info-600)]',
|
|
},
|
|
error: {
|
|
iconBg: 'bg-[var(--color-error-100)]',
|
|
iconColor: 'text-[var(--color-error-600)]',
|
|
},
|
|
};
|
|
|
|
const styles = statusStyles[status];
|
|
|
|
// Handle start batch
|
|
const handleStartBatch = async (batchId: string) => {
|
|
if (!onStartBatch || processingId) return;
|
|
setProcessingId(batchId);
|
|
try {
|
|
await onStartBatch(batchId);
|
|
} finally {
|
|
setProcessingId(null);
|
|
}
|
|
};
|
|
|
|
// Toggle reasoning expansion
|
|
const toggleReasoning = (batchId: string) => {
|
|
setExpandedReasoningId(expandedReasoningId === batchId ? null : batchId);
|
|
};
|
|
|
|
// Format AI reasoning
|
|
const formatReasoning = (batch: any): string | null => {
|
|
const reasoningData = batch.reasoning_data;
|
|
if (!reasoningData) return null;
|
|
|
|
if (typeof reasoningData === 'string') return reasoningData;
|
|
|
|
if (reasoningData.type === 'forecast_demand') {
|
|
return t('dashboard:new_dashboard.production_status.reasoning.forecast_demand', {
|
|
product: reasoningData.parameters?.product_name || batch.product_name,
|
|
demand: reasoningData.parameters?.predicted_demand || batch.planned_quantity,
|
|
});
|
|
}
|
|
|
|
if (reasoningData.type === 'customer_order') {
|
|
return t('dashboard:new_dashboard.production_status.reasoning.customer_order', {
|
|
customer: reasoningData.parameters?.customer_name || 'customer',
|
|
});
|
|
}
|
|
|
|
if (reasoningData.summary) return reasoningData.summary;
|
|
|
|
return null;
|
|
};
|
|
|
|
// Format time
|
|
const formatTime = (isoString: string | null | undefined) => {
|
|
if (!isoString) return '--:--';
|
|
const date = new Date(isoString);
|
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
};
|
|
|
|
// Calculate progress percentage for running batches
|
|
const calculateProgress = (batch: any): number => {
|
|
if (!batch.actual_start_time || !batch.planned_end_time) return 0;
|
|
|
|
const start = new Date(batch.actual_start_time).getTime();
|
|
const end = new Date(batch.planned_end_time).getTime();
|
|
const now = Date.now();
|
|
|
|
if (now >= end) return 100;
|
|
if (now <= start) return 0;
|
|
|
|
return Math.round(((now - start) / (end - start)) * 100);
|
|
};
|
|
|
|
// Render a batch item
|
|
const renderBatchItem = (batch: any, type: 'late' | 'running' | 'pending', index: number, total: number) => {
|
|
const batchId = batch.id || batch.batch_id;
|
|
const isProcessing = processingId === batchId;
|
|
const isExpanded = expandedReasoningId === batchId;
|
|
const reasoning = formatReasoning(batch);
|
|
const progress = type === 'running' ? calculateProgress(batch) : 0;
|
|
|
|
const typeStyles = {
|
|
late: {
|
|
timeBg: 'bg-[var(--color-error-100)]',
|
|
timeColor: 'text-[var(--color-error-700)]',
|
|
icon: <AlertTriangle className="w-4 h-4 text-[var(--color-error-600)]" />,
|
|
},
|
|
running: {
|
|
timeBg: 'bg-[var(--color-info-100)]',
|
|
timeColor: 'text-[var(--color-info-700)]',
|
|
icon: <Timer className="w-4 h-4 text-[var(--color-info-600)]" />,
|
|
},
|
|
pending: {
|
|
timeBg: 'bg-[var(--color-warning-100)]',
|
|
timeColor: 'text-[var(--color-warning-700)]',
|
|
icon: <Clock className="w-4 h-4 text-[var(--text-tertiary)]" />,
|
|
},
|
|
};
|
|
|
|
const batchStyles = typeStyles[type];
|
|
|
|
return (
|
|
<div
|
|
key={batchId || index}
|
|
className={`p-4 ${index < total - 1 ? 'border-b border-[var(--border-primary)]' : ''}`}
|
|
>
|
|
<div className="flex items-start gap-4">
|
|
{/* Batch Info */}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
{batchStyles.icon}
|
|
<span className="font-semibold text-[var(--text-primary)]">
|
|
{batch.product_name || 'Unknown Product'}
|
|
</span>
|
|
{reasoning && (
|
|
<button
|
|
onClick={() => toggleReasoning(batchId)}
|
|
className="flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-[var(--color-primary-50)] text-[var(--color-primary-700)] hover:bg-[var(--color-primary-100)] transition-colors"
|
|
>
|
|
<Brain className="w-3 h-3" />
|
|
AI
|
|
{isExpanded ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<p className="text-sm text-[var(--text-secondary)] mb-2">
|
|
{t('dashboard:new_dashboard.production_status.batch_info', {
|
|
number: batch.batch_number || batchId?.slice(0, 8),
|
|
quantity: batch.planned_quantity || 0,
|
|
})}
|
|
</p>
|
|
|
|
{/* Time/Status Badge */}
|
|
<div className="flex items-center gap-2">
|
|
{type === 'late' && (
|
|
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded ${batchStyles.timeBg} ${batchStyles.timeColor} text-xs font-medium`}>
|
|
<Clock className="w-3 h-3" />
|
|
{t('dashboard:new_dashboard.production_status.should_have_started', {
|
|
time: formatTime(batch.planned_start_time),
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{type === 'running' && (
|
|
<>
|
|
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded ${batchStyles.timeBg} ${batchStyles.timeColor} text-xs font-medium`}>
|
|
<Timer className="w-3 h-3" />
|
|
{t('dashboard:new_dashboard.production_status.started_at', {
|
|
time: formatTime(batch.actual_start_time),
|
|
})}
|
|
</div>
|
|
{/* Progress Bar */}
|
|
<div className="flex-1 max-w-[120px]">
|
|
<div className="h-2 bg-[var(--bg-tertiary)] rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full bg-[var(--color-info-500)] transition-all"
|
|
style={{ width: `${progress}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-xs text-[var(--text-tertiary)]">{progress}%</span>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{type === 'pending' && batch.planned_start_time && (
|
|
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded ${batchStyles.timeBg} ${batchStyles.timeColor} text-xs font-medium`}>
|
|
<Clock className="w-3 h-3" />
|
|
{t('dashboard:new_dashboard.production_status.starts_at', {
|
|
time: formatTime(batch.planned_start_time),
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
{(type === 'late' || type === 'pending') && onStartBatch && (
|
|
<button
|
|
onClick={() => handleStartBatch(batchId)}
|
|
disabled={isProcessing}
|
|
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-[var(--color-success-600)] text-white hover:bg-[var(--color-success-700)] transition-colors disabled:opacity-50"
|
|
>
|
|
<Play className="w-4 h-4" />
|
|
<span className="text-sm font-medium">
|
|
{t('dashboard:new_dashboard.production_status.start_batch')}
|
|
</span>
|
|
</button>
|
|
)}
|
|
|
|
{type === 'running' && onViewBatch && (
|
|
<button
|
|
onClick={() => onViewBatch(batchId)}
|
|
className="flex items-center gap-2 px-3 py-2 rounded-lg border border-[var(--border-primary)] text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] transition-colors"
|
|
>
|
|
<span className="text-sm font-medium">
|
|
{t('dashboard:new_dashboard.production_status.view_details')}
|
|
</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* AI Reasoning (Expanded) */}
|
|
{isExpanded && reasoning && (
|
|
<div className="mt-3 p-3 rounded-lg bg-[var(--color-primary-50)] border border-[var(--color-primary-100)]">
|
|
<div className="flex items-start gap-2">
|
|
<Brain className="w-4 h-4 text-[var(--color-primary-600)] mt-0.5 flex-shrink-0" />
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--color-primary-700)] mb-1">
|
|
{t('dashboard:new_dashboard.production_status.ai_reasoning')}
|
|
</p>
|
|
<p className="text-sm text-[var(--color-primary-600)]">{reasoning}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="rounded-xl shadow-lg border border-[var(--border-primary)] bg-[var(--bg-primary)] overflow-hidden">
|
|
{/* Header */}
|
|
<div className="p-6 pb-4">
|
|
<div className="flex items-center gap-4">
|
|
{/* Icon */}
|
|
<div className={`w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0 ${styles.iconBg}`}>
|
|
{hasLate ? (
|
|
<AlertTriangle className={`w-6 h-6 ${styles.iconColor}`} />
|
|
) : hasAnyProduction ? (
|
|
<Factory className={`w-6 h-6 ${styles.iconColor}`} />
|
|
) : (
|
|
<CheckCircle2 className={`w-6 h-6 ${styles.iconColor}`} />
|
|
)}
|
|
</div>
|
|
|
|
{/* Title & Count */}
|
|
<div className="flex-1">
|
|
<h2 className="text-xl font-bold text-[var(--text-primary)]">
|
|
{t('dashboard:new_dashboard.production_status.title')}
|
|
</h2>
|
|
<p className="text-sm text-[var(--text-secondary)]">
|
|
{hasAnyProduction
|
|
? t('dashboard:new_dashboard.production_status.count', { count: totalCount })
|
|
: t('dashboard:new_dashboard.production_status.no_production')}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Count Badges */}
|
|
<div className="flex items-center gap-2">
|
|
{hasLate && (
|
|
<div className="px-3 py-1 rounded-full bg-[var(--color-error-100)] text-[var(--color-error-700)] font-semibold text-sm">
|
|
{lateToStartBatches.length} {t('dashboard:new_dashboard.production_status.late_badge')}
|
|
</div>
|
|
)}
|
|
{hasRunning && (
|
|
<div className="px-3 py-1 rounded-full bg-[var(--color-info-100)] text-[var(--color-info-700)] font-semibold text-sm">
|
|
{runningBatches.length} {t('dashboard:new_dashboard.production_status.running_badge')}
|
|
</div>
|
|
)}
|
|
{hasPending && (
|
|
<div className="px-3 py-1 rounded-full bg-[var(--color-warning-100)] text-[var(--color-warning-700)] font-semibold text-sm">
|
|
{pendingBatches.length}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
{hasAnyProduction ? (
|
|
<div className="border-t border-[var(--border-primary)]">
|
|
{/* Late to Start Section */}
|
|
{hasLate && (
|
|
<div className="bg-[var(--color-error-50)]">
|
|
<div className="px-6 py-3 border-b border-[var(--color-error-100)]">
|
|
<h3 className="text-sm font-semibold text-[var(--color-error-700)] flex items-center gap-2">
|
|
<AlertTriangle className="w-4 h-4" />
|
|
{t('dashboard:new_dashboard.production_status.late_section')}
|
|
</h3>
|
|
</div>
|
|
{lateToStartBatches.map((batch, index) =>
|
|
renderBatchItem(batch, 'late', index, lateToStartBatches.length)
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Running Section */}
|
|
{hasRunning && (
|
|
<div className="bg-[var(--color-info-50)]">
|
|
<div className="px-6 py-3 border-b border-[var(--color-info-100)]">
|
|
<h3 className="text-sm font-semibold text-[var(--color-info-700)] flex items-center gap-2">
|
|
<Timer className="w-4 h-4" />
|
|
{t('dashboard:new_dashboard.production_status.running_section')}
|
|
</h3>
|
|
</div>
|
|
{runningBatches.map((batch, index) =>
|
|
renderBatchItem(batch, 'running', index, runningBatches.length)
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Pending Section */}
|
|
{hasPending && (
|
|
<div>
|
|
<div className="px-6 py-3 border-b border-[var(--border-primary)] bg-[var(--bg-secondary)]">
|
|
<h3 className="text-sm font-semibold text-[var(--text-secondary)] flex items-center gap-2">
|
|
<Clock className="w-4 h-4" />
|
|
{t('dashboard:new_dashboard.production_status.pending_section')}
|
|
</h3>
|
|
</div>
|
|
{pendingBatches.map((batch, index) =>
|
|
renderBatchItem(batch, 'pending', index, pendingBatches.length)
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
/* Empty State */
|
|
<div className="px-6 pb-6">
|
|
<div className="flex items-center gap-3 p-4 rounded-lg bg-[var(--color-success-50)] border border-[var(--color-success-100)]">
|
|
<CheckCircle2 className="w-6 h-6 text-[var(--color-success-600)]" />
|
|
<p className="text-sm text-[var(--color-success-700)]">
|
|
{t('dashboard:new_dashboard.production_status.all_clear')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default ProductionStatusBlock;
|