Files
bakery-ia/frontend/src/components/dashboard/blocks/ProductionStatusBlock.tsx

418 lines
16 KiB
TypeScript
Raw Normal View History

2025-12-10 11:23:53 +01:00
/**
* 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;