Add improved production UI 2

This commit is contained in:
Urtzi Alfaro
2025-09-23 15:57:22 +02:00
parent 4ae8e14e55
commit 7f871fc933
5 changed files with 1169 additions and 167 deletions

View File

@@ -1,11 +1,11 @@
import React, { useState, useMemo } from 'react';
import { Plus, Clock, AlertCircle, CheckCircle, Timer, ChefHat, Eye, Edit, Package } from 'lucide-react';
import { Button, Input, Card, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { Button, Input, Card, StatsGrid, StatusModal } from '../../../../components/ui';
import { statusColors } from '../../../../styles/colors';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { LoadingSpinner } from '../../../../components/shared';
import { PageHeader } from '../../../../components/layout';
import { ProductionSchedule, BatchTracker, QualityControl, QualityDashboard, QualityInspection, EquipmentManager, CreateProductionBatchModal } from '../../../../components/domain/production';
import { ProductionSchedule, BatchTracker, QualityDashboard, EquipmentManager, CreateProductionBatchModal, ProductionStatusCard, QualityCheckModal } from '../../../../components/domain/production';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import {
useProductionDashboard,
@@ -31,6 +31,7 @@ const ProductionPage: React.FC = () => {
const [selectedBatch, setSelectedBatch] = useState<ProductionBatchResponse | null>(null);
const [showBatchModal, setShowBatchModal] = useState(false);
const [showCreateModal, setShowCreateModal] = useState(false);
const [showQualityModal, setShowQualityModal] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const currentTenant = useCurrentTenant();
@@ -67,54 +68,6 @@ const ProductionPage: React.FC = () => {
}
};
const getProductionStatusConfig = (status: ProductionStatusEnum, priority: ProductionPriorityEnum) => {
const statusConfig = {
[ProductionStatusEnum.PENDING]: { icon: Clock },
[ProductionStatusEnum.IN_PROGRESS]: { icon: Timer },
[ProductionStatusEnum.COMPLETED]: { icon: CheckCircle },
[ProductionStatusEnum.CANCELLED]: { icon: AlertCircle },
[ProductionStatusEnum.ON_HOLD]: { icon: AlertCircle },
[ProductionStatusEnum.QUALITY_CHECK]: { icon: Package },
[ProductionStatusEnum.FAILED]: { icon: AlertCircle },
};
const config = statusConfig[status] || { icon: AlertCircle };
const Icon = config.icon;
const isUrgent = priority === ProductionPriorityEnum.URGENT;
const isCritical = status === ProductionStatusEnum.FAILED || (status === ProductionStatusEnum.PENDING && isUrgent);
// Map production statuses to global status colors
const getStatusColorForProduction = (status: ProductionStatusEnum) => {
// Handle both uppercase (backend) and lowercase (frontend) status values
const normalizedStatus = status.toLowerCase();
switch (normalizedStatus) {
case 'pending':
return statusColors.pending.primary;
case 'in_progress':
return statusColors.inProgress.primary;
case 'completed':
return statusColors.completed.primary;
case 'cancelled':
case 'failed':
return statusColors.cancelled.primary;
case 'on_hold':
return statusColors.pending.primary;
case 'quality_check':
return statusColors.inProgress.primary;
default:
return statusColors.other.primary;
}
};
return {
color: getStatusColorForProduction(status),
text: productionEnums.getProductionStatusLabel(status),
icon: Icon,
isCritical,
isHighlight: isUrgent
};
};
const batches = activeBatchesData?.batches || [];
@@ -156,28 +109,6 @@ const ProductionPage: React.FC = () => {
};
}, [dashboardData]);
// Calculate progress for batches
const calculateProgress = (batch: ProductionBatchResponse): number => {
if (batch.status === 'completed') return 100;
if (batch.status === 'pending') return 0;
if (batch.status === 'cancelled' || batch.status === 'failed') return 0;
// For in-progress batches, calculate based on time elapsed
if (batch.actual_start_time && batch.planned_end_time) {
const now = new Date();
const startTime = new Date(batch.actual_start_time);
const endTime = new Date(batch.planned_end_time);
const totalDuration = endTime.getTime() - startTime.getTime();
const elapsed = now.getTime() - startTime.getTime();
if (totalDuration > 0) {
return Math.min(90, Math.max(10, Math.round((elapsed / totalDuration) * 100)));
}
}
// Default progress for in-progress items
return 50;
};
// Loading state
if (!tenantId || dashboardLoading || batchesLoading) {
@@ -278,26 +209,6 @@ const ProductionPage: React.FC = () => {
>
Programación
</button>
<button
onClick={() => setActiveTab('batches')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'batches'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Lotes de Producción
</button>
<button
onClick={() => setActiveTab('quality')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'quality'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Control de Calidad
</button>
<button
onClick={() => setActiveTab('quality-dashboard')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
@@ -308,16 +219,6 @@ const ProductionPage: React.FC = () => {
>
Dashboard Calidad
</button>
<button
onClick={() => setActiveTab('quality-inspection')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'quality-inspection'
? 'border-orange-500 text-[var(--color-primary)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Inspección
</button>
<button
onClick={() => setActiveTab('equipment')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
@@ -350,54 +251,72 @@ const ProductionPage: React.FC = () => {
{/* Production Batches Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredBatches.map((batch) => {
const statusConfig = getProductionStatusConfig(batch.status, batch.priority);
const progress = calculateProgress(batch);
return (
<StatusCard
key={batch.id}
id={batch.id}
statusIndicator={statusConfig}
title={batch.product_name}
subtitle={`Lote: ${batch.batch_number}`}
primaryValue={batch.planned_quantity}
primaryValueLabel="unidades"
secondaryInfo={{
label: 'Personal',
value: batch.staff_assigned?.join(', ') || 'No asignado'
}}
progress={{
label: 'Progreso',
percentage: progress,
color: statusConfig.color
}}
actions={[
{
label: 'Ver Detalles',
icon: Eye,
variant: 'primary',
priority: 'primary',
onClick: () => {
setSelectedBatch(batch);
setModalMode('view');
setShowBatchModal(true);
}
},
{
label: 'Editar',
icon: Edit,
priority: 'secondary',
onClick: () => {
setSelectedBatch(batch);
setModalMode('edit');
setShowBatchModal(true);
}
}
]}
/>
);
})}
{filteredBatches.map((batch) => (
<ProductionStatusCard
key={batch.id}
batch={batch}
onView={(batch) => {
setSelectedBatch(batch);
setModalMode('view');
setShowBatchModal(true);
}}
onEdit={(batch) => {
setSelectedBatch(batch);
setModalMode('edit');
setShowBatchModal(true);
}}
onStart={async (batch) => {
try {
await updateBatchStatusMutation.mutateAsync({
batchId: batch.id,
updates: { status: ProductionStatusEnum.IN_PROGRESS }
});
} catch (error) {
console.error('Error starting batch:', error);
}
}}
onPause={async (batch) => {
try {
await updateBatchStatusMutation.mutateAsync({
batchId: batch.id,
updates: { status: ProductionStatusEnum.ON_HOLD }
});
} catch (error) {
console.error('Error pausing batch:', error);
}
}}
onComplete={async (batch) => {
try {
await updateBatchStatusMutation.mutateAsync({
batchId: batch.id,
updates: { status: ProductionStatusEnum.QUALITY_CHECK }
});
} catch (error) {
console.error('Error completing batch:', error);
}
}}
onCancel={async (batch) => {
try {
await updateBatchStatusMutation.mutateAsync({
batchId: batch.id,
updates: { status: ProductionStatusEnum.CANCELLED }
});
} catch (error) {
console.error('Error cancelling batch:', error);
}
}}
onQualityCheck={(batch) => {
setSelectedBatch(batch);
setShowQualityModal(true);
}}
onViewHistory={(batch) => {
setSelectedBatch(batch);
setModalMode('view');
setShowBatchModal(true);
}}
showDetailedProgress={true}
/>
))}
</div>
{/* Empty State */}
@@ -422,22 +341,11 @@ const ProductionPage: React.FC = () => {
</>
)}
{activeTab === 'batches' && (
<BatchTracker />
)}
{activeTab === 'quality' && (
<QualityControl />
)}
{activeTab === 'quality-dashboard' && (
<QualityDashboard />
)}
{activeTab === 'quality-inspection' && (
<QualityInspection />
)}
{activeTab === 'equipment' && (
<EquipmentManager />
)}
@@ -455,7 +363,11 @@ const ProductionPage: React.FC = () => {
onModeChange={setModalMode}
title={selectedBatch.product_name}
subtitle={`Lote de Producción #${selectedBatch.batch_number}`}
statusIndicator={getProductionStatusConfig(selectedBatch.status, selectedBatch.priority)}
statusIndicator={{
color: statusColors.inProgress.primary,
text: productionEnums.getProductionStatusLabel(selectedBatch.status),
icon: Package
}}
size="lg"
sections={[
{
@@ -523,6 +435,40 @@ const ProductionPage: React.FC = () => {
}
]
},
{
title: 'Etapas de Producción',
icon: Timer,
fields: [
{
label: 'Etapa Actual',
value: selectedBatch.status === ProductionStatusEnum.IN_PROGRESS
? 'En progreso'
: selectedBatch.status === ProductionStatusEnum.QUALITY_CHECK
? 'Control de calidad'
: productionEnums.getProductionStatusLabel(selectedBatch.status)
},
{
label: 'Progreso del Lote',
value: selectedBatch.status === ProductionStatusEnum.COMPLETED
? '100%'
: selectedBatch.status === ProductionStatusEnum.PENDING
? '0%'
: '50%'
},
{
label: 'Tiempo Transcurrido',
value: selectedBatch.actual_start_time
? `${Math.round((new Date().getTime() - new Date(selectedBatch.actual_start_time).getTime()) / (1000 * 60))} minutos`
: 'No iniciado'
},
{
label: 'Tiempo Estimado Restante',
value: selectedBatch.planned_end_time && selectedBatch.actual_start_time
? `${Math.max(0, Math.round((new Date(selectedBatch.planned_end_time).getTime() - new Date().getTime()) / (1000 * 60)))} minutos`
: 'Calculando...'
}
]
},
{
title: 'Calidad y Costos',
icon: CheckCircle,
@@ -624,6 +570,32 @@ const ProductionPage: React.FC = () => {
onClose={() => setShowCreateModal(false)}
onCreateBatch={handleCreateBatch}
/>
{/* Quality Check Modal */}
{showQualityModal && selectedBatch && (
<QualityCheckModal
isOpen={showQualityModal}
onClose={() => {
setShowQualityModal(false);
setSelectedBatch(null);
}}
batch={selectedBatch}
onComplete={async (result) => {
console.log('Quality check completed:', result);
// Optionally update batch status to completed or quality_passed
try {
await updateBatchStatusMutation.mutateAsync({
batchId: selectedBatch.id,
updates: {
status: result.overallPass ? ProductionStatusEnum.COMPLETED : ProductionStatusEnum.ON_HOLD
}
});
} catch (error) {
console.error('Error updating batch status after quality check:', error);
}
}}
/>
)}
</div>
);
};