2025-09-21 07:45:19 +02:00
|
|
|
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';
|
2025-09-21 17:35:36 +02:00
|
|
|
import { statusColors } from '../../../../styles/colors';
|
2025-09-21 07:45:19 +02:00
|
|
|
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
|
|
|
|
import { LoadingSpinner } from '../../../../components/shared';
|
2025-08-28 10:41:04 +02:00
|
|
|
import { PageHeader } from '../../../../components/layout';
|
2025-09-23 12:49:35 +02:00
|
|
|
import { ProductionSchedule, BatchTracker, QualityControl, QualityDashboard, QualityInspection, EquipmentManager, CreateProductionBatchModal } from '../../../../components/domain/production';
|
2025-09-21 07:45:19 +02:00
|
|
|
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
|
|
|
|
import {
|
|
|
|
|
useProductionDashboard,
|
|
|
|
|
useActiveBatches,
|
|
|
|
|
useCreateProductionBatch,
|
|
|
|
|
useUpdateBatchStatus,
|
|
|
|
|
productionService
|
|
|
|
|
} from '../../../../api';
|
|
|
|
|
import type {
|
|
|
|
|
ProductionBatchResponse,
|
|
|
|
|
ProductionBatchCreate,
|
|
|
|
|
ProductionBatchStatusUpdate
|
|
|
|
|
} from '../../../../api';
|
|
|
|
|
import {
|
|
|
|
|
ProductionStatusEnum,
|
|
|
|
|
ProductionPriorityEnum
|
|
|
|
|
} from '../../../../api';
|
|
|
|
|
import { useProductionEnums } from '../../../../utils/enumHelpers';
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
const ProductionPage: React.FC = () => {
|
|
|
|
|
const [activeTab, setActiveTab] = useState('schedule');
|
2025-08-28 23:40:44 +02:00
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
2025-09-21 07:45:19 +02:00
|
|
|
const [selectedBatch, setSelectedBatch] = useState<ProductionBatchResponse | null>(null);
|
|
|
|
|
const [showBatchModal, setShowBatchModal] = useState(false);
|
|
|
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
2025-08-31 10:46:13 +02:00
|
|
|
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-09-21 07:45:19 +02:00
|
|
|
const currentTenant = useCurrentTenant();
|
|
|
|
|
const tenantId = currentTenant?.id || '';
|
|
|
|
|
const productionEnums = useProductionEnums();
|
|
|
|
|
|
|
|
|
|
// API Data
|
|
|
|
|
const {
|
|
|
|
|
data: dashboardData,
|
|
|
|
|
isLoading: dashboardLoading,
|
|
|
|
|
error: dashboardError
|
|
|
|
|
} = useProductionDashboard(tenantId);
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
data: activeBatchesData,
|
|
|
|
|
isLoading: batchesLoading,
|
|
|
|
|
error: batchesError
|
|
|
|
|
} = useActiveBatches(tenantId);
|
|
|
|
|
|
|
|
|
|
// Mutations
|
|
|
|
|
const createBatchMutation = useCreateProductionBatch();
|
|
|
|
|
const updateBatchStatusMutation = useUpdateBatchStatus();
|
|
|
|
|
|
|
|
|
|
// Handlers
|
|
|
|
|
const handleCreateBatch = async (batchData: ProductionBatchCreate) => {
|
|
|
|
|
try {
|
|
|
|
|
await createBatchMutation.mutateAsync({
|
|
|
|
|
tenantId,
|
|
|
|
|
batchData
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error creating production batch:', error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
2025-08-28 10:41:04 +02:00
|
|
|
};
|
|
|
|
|
|
2025-09-21 07:45:19 +02:00
|
|
|
const getProductionStatusConfig = (status: ProductionStatusEnum, priority: ProductionPriorityEnum) => {
|
2025-08-28 10:41:04 +02:00
|
|
|
const statusConfig = {
|
2025-09-21 07:45:19 +02:00
|
|
|
[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 },
|
2025-08-28 10:41:04 +02:00
|
|
|
};
|
2025-09-21 07:45:19 +02:00
|
|
|
|
|
|
|
|
const config = statusConfig[status] || { icon: AlertCircle };
|
|
|
|
|
const Icon = config.icon;
|
|
|
|
|
const isUrgent = priority === ProductionPriorityEnum.URGENT;
|
|
|
|
|
const isCritical = status === ProductionStatusEnum.FAILED || (status === ProductionStatusEnum.PENDING && isUrgent);
|
|
|
|
|
|
2025-09-21 17:35:36 +02:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-31 10:46:13 +02:00
|
|
|
return {
|
2025-09-21 17:35:36 +02:00
|
|
|
color: getStatusColorForProduction(status),
|
2025-09-21 07:45:19 +02:00
|
|
|
text: productionEnums.getProductionStatusLabel(status),
|
2025-08-31 10:46:13 +02:00
|
|
|
icon: Icon,
|
2025-09-21 07:45:19 +02:00
|
|
|
isCritical,
|
|
|
|
|
isHighlight: isUrgent
|
2025-08-31 10:46:13 +02:00
|
|
|
};
|
2025-08-28 10:41:04 +02:00
|
|
|
};
|
|
|
|
|
|
2025-09-21 07:45:19 +02:00
|
|
|
const batches = activeBatchesData?.batches || [];
|
|
|
|
|
|
|
|
|
|
const filteredBatches = useMemo(() => {
|
|
|
|
|
if (!searchQuery) return batches;
|
|
|
|
|
|
|
|
|
|
const searchLower = searchQuery.toLowerCase();
|
|
|
|
|
return batches.filter(batch =>
|
|
|
|
|
batch.product_name.toLowerCase().includes(searchLower) ||
|
|
|
|
|
batch.batch_number.toLowerCase().includes(searchLower) ||
|
|
|
|
|
(batch.staff_assigned && batch.staff_assigned.some(staff =>
|
|
|
|
|
staff.toLowerCase().includes(searchLower)
|
|
|
|
|
))
|
|
|
|
|
);
|
|
|
|
|
}, [batches, searchQuery]);
|
|
|
|
|
|
|
|
|
|
// Calculate production stats from real data
|
|
|
|
|
const productionStats = useMemo(() => {
|
|
|
|
|
if (!dashboardData) {
|
|
|
|
|
return {
|
|
|
|
|
activeBatches: 0,
|
|
|
|
|
todaysTarget: 0,
|
|
|
|
|
capacityUtilization: 0,
|
|
|
|
|
onTimeCompletion: 0,
|
|
|
|
|
qualityScore: 0,
|
|
|
|
|
totalOutput: 0,
|
|
|
|
|
efficiency: 0
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
activeBatches: dashboardData.active_batches || 0,
|
|
|
|
|
todaysTarget: dashboardData.todays_production_plan?.length || 0,
|
|
|
|
|
capacityUtilization: Math.round(dashboardData.capacity_utilization || 0),
|
|
|
|
|
onTimeCompletion: Math.round(dashboardData.on_time_completion_rate || 0),
|
|
|
|
|
qualityScore: Math.round(dashboardData.average_quality_score || 0),
|
|
|
|
|
totalOutput: dashboardData.total_output_today || 0,
|
|
|
|
|
efficiency: Math.round(dashboardData.efficiency_percentage || 0)
|
|
|
|
|
};
|
|
|
|
|
}, [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) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-center min-h-64">
|
|
|
|
|
<LoadingSpinner text="Cargando producción..." />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Error state
|
|
|
|
|
if (dashboardError || batchesError) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="text-center py-12">
|
|
|
|
|
<AlertCircle className="mx-auto h-12 w-12 text-red-500 mb-4" />
|
|
|
|
|
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
|
|
|
|
|
Error al cargar la producción
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-[var(--text-secondary)] mb-4">
|
|
|
|
|
{(dashboardError || batchesError)?.message || 'Ha ocurrido un error inesperado'}
|
|
|
|
|
</p>
|
|
|
|
|
<Button onClick={() => window.location.reload()}>
|
|
|
|
|
Reintentar
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-08-28 18:07:16 +02:00
|
|
|
|
2025-08-28 10:41:04 +02:00
|
|
|
return (
|
2025-08-30 19:11:15 +02:00
|
|
|
<div className="space-y-6">
|
2025-08-28 10:41:04 +02:00
|
|
|
<PageHeader
|
|
|
|
|
title="Gestión de Producción"
|
|
|
|
|
description="Planifica y controla la producción diaria de tu panadería"
|
2025-08-30 19:11:15 +02:00
|
|
|
actions={[
|
|
|
|
|
{
|
|
|
|
|
id: "new",
|
|
|
|
|
label: "Nueva Orden de Producción",
|
|
|
|
|
variant: "primary" as const,
|
|
|
|
|
icon: Plus,
|
2025-09-21 07:45:19 +02:00
|
|
|
onClick: () => setShowCreateModal(true)
|
2025-08-30 19:11:15 +02:00
|
|
|
}
|
|
|
|
|
]}
|
2025-08-28 10:41:04 +02:00
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Production Stats */}
|
2025-09-21 07:45:19 +02:00
|
|
|
<StatsGrid
|
|
|
|
|
stats={[
|
|
|
|
|
{
|
|
|
|
|
title: 'Lotes Activos',
|
|
|
|
|
value: productionStats.activeBatches,
|
|
|
|
|
variant: 'default' as const,
|
|
|
|
|
icon: Package,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Utilización Capacidad',
|
|
|
|
|
value: `${productionStats.capacityUtilization}%`,
|
|
|
|
|
variant: productionStats.capacityUtilization >= 80 ? 'success' as const : 'warning' as const,
|
|
|
|
|
icon: Timer,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Completado a Tiempo',
|
|
|
|
|
value: `${productionStats.onTimeCompletion}%`,
|
|
|
|
|
variant: productionStats.onTimeCompletion >= 90 ? 'success' as const : 'error' as const,
|
|
|
|
|
icon: CheckCircle,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Puntuación Calidad',
|
|
|
|
|
value: `${productionStats.qualityScore}%`,
|
|
|
|
|
variant: productionStats.qualityScore >= 85 ? 'success' as const : 'warning' as const,
|
|
|
|
|
icon: Package,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Producción Hoy',
|
|
|
|
|
value: formatters.number(productionStats.totalOutput),
|
|
|
|
|
variant: 'info' as const,
|
|
|
|
|
icon: ChefHat,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Eficiencia',
|
|
|
|
|
value: `${productionStats.efficiency}%`,
|
|
|
|
|
variant: productionStats.efficiency >= 75 ? 'success' as const : 'warning' as const,
|
|
|
|
|
icon: Timer,
|
|
|
|
|
},
|
|
|
|
|
]}
|
2025-08-30 19:21:15 +02:00
|
|
|
columns={3}
|
2025-08-30 19:11:15 +02:00
|
|
|
/>
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
{/* Tabs Navigation */}
|
|
|
|
|
<div className="border-b border-[var(--border-primary)]">
|
|
|
|
|
<nav className="-mb-px flex space-x-8">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setActiveTab('schedule')}
|
|
|
|
|
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
|
|
|
|
activeTab === 'schedule'
|
|
|
|
|
? 'border-orange-500 text-[var(--color-primary)]'
|
|
|
|
|
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
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>
|
2025-09-23 12:49:35 +02:00
|
|
|
<button
|
|
|
|
|
onClick={() => setActiveTab('quality-dashboard')}
|
|
|
|
|
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
|
|
|
|
activeTab === 'quality-dashboard'
|
|
|
|
|
? 'border-orange-500 text-[var(--color-primary)]'
|
|
|
|
|
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
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 ${
|
|
|
|
|
activeTab === 'equipment'
|
|
|
|
|
? 'border-orange-500 text-[var(--color-primary)]'
|
|
|
|
|
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
Equipos
|
|
|
|
|
</button>
|
2025-08-28 10:41:04 +02:00
|
|
|
</nav>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-08-30 19:11:15 +02:00
|
|
|
{/* Production Orders Tab */}
|
2025-08-28 10:41:04 +02:00
|
|
|
{activeTab === 'schedule' && (
|
2025-08-30 19:11:15 +02:00
|
|
|
<>
|
2025-09-21 07:45:19 +02:00
|
|
|
{/* Search Controls */}
|
2025-08-30 19:11:15 +02:00
|
|
|
<Card className="p-4">
|
|
|
|
|
<div className="flex flex-col sm:flex-row gap-4">
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<Input
|
2025-09-21 07:45:19 +02:00
|
|
|
placeholder="Buscar lotes por producto, número de lote o personal..."
|
2025-08-30 19:11:15 +02:00
|
|
|
value={searchQuery}
|
|
|
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
|
|
|
className="w-full"
|
|
|
|
|
/>
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-08-30 19:11:15 +02:00
|
|
|
</Card>
|
|
|
|
|
|
2025-09-21 07:45:19 +02:00
|
|
|
{/* Production Batches Grid */}
|
2025-08-30 19:11:15 +02:00
|
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
2025-09-21 07:45:19 +02:00
|
|
|
{filteredBatches.map((batch) => {
|
|
|
|
|
const statusConfig = getProductionStatusConfig(batch.status, batch.priority);
|
|
|
|
|
const progress = calculateProgress(batch);
|
|
|
|
|
|
2025-08-31 10:46:13 +02:00
|
|
|
return (
|
|
|
|
|
<StatusCard
|
2025-09-21 07:45:19 +02:00
|
|
|
key={batch.id}
|
|
|
|
|
id={batch.id}
|
2025-08-31 10:46:13 +02:00
|
|
|
statusIndicator={statusConfig}
|
2025-09-21 07:45:19 +02:00
|
|
|
title={batch.product_name}
|
|
|
|
|
subtitle={`Lote: ${batch.batch_number}`}
|
|
|
|
|
primaryValue={batch.planned_quantity}
|
2025-08-31 10:46:13 +02:00
|
|
|
primaryValueLabel="unidades"
|
|
|
|
|
secondaryInfo={{
|
2025-09-21 07:45:19 +02:00
|
|
|
label: 'Personal',
|
|
|
|
|
value: batch.staff_assigned?.join(', ') || 'No asignado'
|
2025-08-31 10:46:13 +02:00
|
|
|
}}
|
|
|
|
|
progress={{
|
|
|
|
|
label: 'Progreso',
|
2025-09-21 07:45:19 +02:00
|
|
|
percentage: progress,
|
2025-08-31 10:46:13 +02:00
|
|
|
color: statusConfig.color
|
|
|
|
|
}}
|
|
|
|
|
actions={[
|
|
|
|
|
{
|
2025-09-21 07:45:19 +02:00
|
|
|
label: 'Ver Detalles',
|
2025-08-31 10:46:13 +02:00
|
|
|
icon: Eye,
|
2025-09-21 07:45:19 +02:00
|
|
|
variant: 'primary',
|
|
|
|
|
priority: 'primary',
|
2025-08-31 10:46:13 +02:00
|
|
|
onClick: () => {
|
2025-09-21 07:45:19 +02:00
|
|
|
setSelectedBatch(batch);
|
2025-08-31 10:46:13 +02:00
|
|
|
setModalMode('view');
|
2025-09-21 07:45:19 +02:00
|
|
|
setShowBatchModal(true);
|
2025-08-31 10:46:13 +02:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Editar',
|
|
|
|
|
icon: Edit,
|
2025-09-21 07:45:19 +02:00
|
|
|
priority: 'secondary',
|
2025-08-31 10:46:13 +02:00
|
|
|
onClick: () => {
|
2025-09-21 07:45:19 +02:00
|
|
|
setSelectedBatch(batch);
|
2025-08-31 10:46:13 +02:00
|
|
|
setModalMode('edit');
|
2025-09-21 07:45:19 +02:00
|
|
|
setShowBatchModal(true);
|
2025-08-31 10:46:13 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
2025-08-30 19:11:15 +02:00
|
|
|
|
|
|
|
|
{/* Empty State */}
|
2025-09-21 07:45:19 +02:00
|
|
|
{filteredBatches.length === 0 && (
|
2025-08-30 19:11:15 +02:00
|
|
|
<div className="text-center py-12">
|
|
|
|
|
<ChefHat className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
|
|
|
|
|
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
|
2025-09-21 07:45:19 +02:00
|
|
|
No se encontraron lotes de producción
|
2025-08-30 19:11:15 +02:00
|
|
|
</h3>
|
|
|
|
|
<p className="text-[var(--text-secondary)] mb-4">
|
2025-09-21 07:45:19 +02:00
|
|
|
{batches.length === 0
|
|
|
|
|
? 'No hay lotes de producción activos. Crea el primer lote para comenzar.'
|
|
|
|
|
: 'Intenta ajustar la búsqueda o crear un nuevo lote de producción'
|
|
|
|
|
}
|
2025-08-30 19:11:15 +02:00
|
|
|
</p>
|
2025-09-21 07:45:19 +02:00
|
|
|
<Button onClick={() => setShowCreateModal(true)}>
|
2025-08-30 19:11:15 +02:00
|
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
|
|
|
Nueva Orden de Producción
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
2025-08-28 10:41:04 +02:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{activeTab === 'batches' && (
|
|
|
|
|
<BatchTracker />
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{activeTab === 'quality' && (
|
|
|
|
|
<QualityControl />
|
|
|
|
|
)}
|
2025-08-30 19:11:15 +02:00
|
|
|
|
2025-09-23 12:49:35 +02:00
|
|
|
{activeTab === 'quality-dashboard' && (
|
|
|
|
|
<QualityDashboard />
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{activeTab === 'quality-inspection' && (
|
|
|
|
|
<QualityInspection />
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{activeTab === 'equipment' && (
|
|
|
|
|
<EquipmentManager />
|
|
|
|
|
)}
|
|
|
|
|
|
2025-09-21 07:45:19 +02:00
|
|
|
{/* Production Batch Modal */}
|
|
|
|
|
{showBatchModal && selectedBatch && (
|
2025-08-31 10:46:13 +02:00
|
|
|
<StatusModal
|
2025-09-21 07:45:19 +02:00
|
|
|
isOpen={showBatchModal}
|
2025-08-31 10:46:13 +02:00
|
|
|
onClose={() => {
|
2025-09-21 07:45:19 +02:00
|
|
|
setShowBatchModal(false);
|
|
|
|
|
setSelectedBatch(null);
|
2025-08-31 10:46:13 +02:00
|
|
|
setModalMode('view');
|
|
|
|
|
}}
|
|
|
|
|
mode={modalMode}
|
|
|
|
|
onModeChange={setModalMode}
|
2025-09-21 07:45:19 +02:00
|
|
|
title={selectedBatch.product_name}
|
|
|
|
|
subtitle={`Lote de Producción #${selectedBatch.batch_number}`}
|
|
|
|
|
statusIndicator={getProductionStatusConfig(selectedBatch.status, selectedBatch.priority)}
|
2025-08-31 10:46:13 +02:00
|
|
|
size="lg"
|
|
|
|
|
sections={[
|
|
|
|
|
{
|
|
|
|
|
title: 'Información General',
|
|
|
|
|
icon: Package,
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
2025-09-21 07:45:19 +02:00
|
|
|
label: 'Cantidad Planificada',
|
|
|
|
|
value: `${selectedBatch.planned_quantity} unidades`,
|
2025-08-31 10:46:13 +02:00
|
|
|
highlight: true
|
|
|
|
|
},
|
|
|
|
|
{
|
2025-09-21 07:45:19 +02:00
|
|
|
label: 'Cantidad Real',
|
|
|
|
|
value: selectedBatch.actual_quantity
|
|
|
|
|
? `${selectedBatch.actual_quantity} unidades`
|
|
|
|
|
: 'Pendiente',
|
|
|
|
|
editable: modalMode === 'edit',
|
|
|
|
|
type: 'number'
|
2025-08-31 10:46:13 +02:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Prioridad',
|
2025-09-21 07:45:19 +02:00
|
|
|
value: selectedBatch.priority,
|
|
|
|
|
type: 'select',
|
|
|
|
|
editable: modalMode === 'edit',
|
|
|
|
|
options: productionEnums.getProductionPriorityOptions()
|
2025-08-31 10:46:13 +02:00
|
|
|
},
|
|
|
|
|
{
|
2025-09-21 07:45:19 +02:00
|
|
|
label: 'Estado',
|
|
|
|
|
value: selectedBatch.status,
|
|
|
|
|
type: 'select',
|
|
|
|
|
editable: modalMode === 'edit',
|
|
|
|
|
options: productionEnums.getProductionStatusOptions()
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Personal Asignado',
|
|
|
|
|
value: selectedBatch.staff_assigned?.join(', ') || 'No asignado',
|
|
|
|
|
editable: modalMode === 'edit',
|
|
|
|
|
type: 'text'
|
2025-08-31 10:46:13 +02:00
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Cronograma',
|
|
|
|
|
icon: Clock,
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
2025-09-21 07:45:19 +02:00
|
|
|
label: 'Inicio Planificado',
|
|
|
|
|
value: selectedBatch.planned_start_time,
|
|
|
|
|
type: 'datetime'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Fin Planificado',
|
|
|
|
|
value: selectedBatch.planned_end_time,
|
|
|
|
|
type: 'datetime'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Inicio Real',
|
|
|
|
|
value: selectedBatch.actual_start_time || 'Pendiente',
|
2025-08-31 10:46:13 +02:00
|
|
|
type: 'datetime'
|
|
|
|
|
},
|
|
|
|
|
{
|
2025-09-21 07:45:19 +02:00
|
|
|
label: 'Fin Real',
|
|
|
|
|
value: selectedBatch.actual_end_time || 'Pendiente',
|
2025-08-31 10:46:13 +02:00
|
|
|
type: 'datetime'
|
|
|
|
|
}
|
|
|
|
|
]
|
2025-09-21 07:45:19 +02:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Calidad y Costos',
|
|
|
|
|
icon: CheckCircle,
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
label: 'Puntuación de Calidad',
|
|
|
|
|
value: selectedBatch.quality_score
|
|
|
|
|
? `${selectedBatch.quality_score}/10`
|
|
|
|
|
: 'Pendiente'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Rendimiento',
|
|
|
|
|
value: selectedBatch.yield_percentage
|
|
|
|
|
? `${selectedBatch.yield_percentage}%`
|
|
|
|
|
: 'Calculando...'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Costo Estimado',
|
|
|
|
|
value: selectedBatch.estimated_cost || 0,
|
|
|
|
|
type: 'currency'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Costo Real',
|
|
|
|
|
value: selectedBatch.actual_cost || 0,
|
|
|
|
|
type: 'currency'
|
|
|
|
|
}
|
|
|
|
|
]
|
2025-08-31 10:46:13 +02:00
|
|
|
}
|
|
|
|
|
]}
|
2025-09-21 07:45:19 +02:00
|
|
|
onSave={async () => {
|
|
|
|
|
try {
|
|
|
|
|
// Implementation would depend on specific fields changed
|
|
|
|
|
console.log('Saving batch changes:', selectedBatch.id);
|
|
|
|
|
// await updateBatchStatusMutation.mutateAsync({
|
|
|
|
|
// batchId: selectedBatch.id,
|
|
|
|
|
// updates: selectedBatch
|
|
|
|
|
// });
|
|
|
|
|
setShowBatchModal(false);
|
|
|
|
|
setSelectedBatch(null);
|
|
|
|
|
setModalMode('view');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error saving batch:', error);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onFieldChange={(sectionIndex, fieldIndex, value) => {
|
|
|
|
|
if (!selectedBatch) return;
|
|
|
|
|
|
|
|
|
|
const sections = [
|
|
|
|
|
['planned_quantity', 'actual_quantity', 'priority', 'status', 'staff_assigned'],
|
|
|
|
|
['planned_start_time', 'planned_end_time', 'actual_start_time', 'actual_end_time'],
|
|
|
|
|
['quality_score', 'yield_percentage', 'estimated_cost', 'actual_cost']
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Get the field names from modal sections
|
|
|
|
|
const sectionFields = [
|
|
|
|
|
{ fields: ['planned_quantity', 'actual_quantity', 'priority', 'status', 'staff_assigned'] },
|
|
|
|
|
{ fields: ['planned_start_time', 'planned_end_time', 'actual_start_time', 'actual_end_time'] },
|
|
|
|
|
{ fields: ['quality_score', 'yield_percentage', 'estimated_cost', 'actual_cost'] }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const fieldMapping: Record<string, string> = {
|
|
|
|
|
'Cantidad Real': 'actual_quantity',
|
|
|
|
|
'Prioridad': 'priority',
|
|
|
|
|
'Estado': 'status',
|
|
|
|
|
'Personal Asignado': 'staff_assigned'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Get section labels to map back to field names
|
|
|
|
|
const sectionLabels = [
|
|
|
|
|
['Cantidad Planificada', 'Cantidad Real', 'Prioridad', 'Estado', 'Personal Asignado'],
|
|
|
|
|
['Inicio Planificado', 'Fin Planificado', 'Inicio Real', 'Fin Real'],
|
|
|
|
|
['Puntuación de Calidad', 'Rendimiento', 'Costo Estimado', 'Costo Real']
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const fieldLabel = sectionLabels[sectionIndex]?.[fieldIndex];
|
|
|
|
|
const propertyName = fieldMapping[fieldLabel] || sectionFields[sectionIndex]?.fields[fieldIndex];
|
|
|
|
|
|
|
|
|
|
if (propertyName) {
|
|
|
|
|
let processedValue: any = value;
|
|
|
|
|
|
|
|
|
|
if (propertyName === 'staff_assigned' && typeof value === 'string') {
|
|
|
|
|
processedValue = value.split(',').map(s => s.trim()).filter(s => s.length > 0);
|
|
|
|
|
} else if (propertyName === 'actual_quantity') {
|
|
|
|
|
processedValue = parseFloat(value as string) || 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setSelectedBatch({
|
|
|
|
|
...selectedBatch,
|
|
|
|
|
[propertyName]: processedValue
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-08-31 10:46:13 +02:00
|
|
|
}}
|
|
|
|
|
/>
|
2025-08-30 19:11:15 +02:00
|
|
|
)}
|
2025-09-21 07:45:19 +02:00
|
|
|
|
|
|
|
|
{/* Create Production Batch Modal */}
|
|
|
|
|
<CreateProductionBatchModal
|
|
|
|
|
isOpen={showCreateModal}
|
|
|
|
|
onClose={() => setShowCreateModal(false)}
|
|
|
|
|
onCreateBatch={handleCreateBatch}
|
|
|
|
|
/>
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default ProductionPage;
|