Add improved production UI 2
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user