Fix and UI imporvements 3
This commit is contained in:
@@ -0,0 +1,417 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user