import React, { useState, useEffect } from 'react'; import { Package, AlertTriangle, Clock, Archive, Thermometer, Plus, Edit, Trash2, CheckCircle, X, Save, ChevronDown, ChevronUp, XCircle } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { EditViewModal } from '../../ui/EditViewModal/EditViewModal'; import { IngredientResponse, StockResponse, StockUpdate } from '../../../api/types/inventory'; import { formatters } from '../../ui/Stats/StatsPresets'; import { statusColors } from '../../../styles/colors'; import { Button } from '../../ui/Button'; import { useSuppliers } from '../../../api/hooks/suppliers'; interface BatchModalProps { isOpen: boolean; onClose: () => void; ingredient: IngredientResponse; batches: StockResponse[]; loading?: boolean; tenantId: string; onAddBatch?: () => void; onEditBatch?: (batchId: string, updateData: StockUpdate) => Promise; onMarkAsWaste?: (batchId: string) => Promise; // Wait-for-refetch support waitForRefetch?: boolean; isRefetching?: boolean; onSaveComplete?: () => Promise; } /** * BatchModal - Card-based batch management modal * Mobile-friendly design with edit and waste marking functionality */ export const BatchModal: React.FC = ({ isOpen, onClose, ingredient, batches = [], loading = false, tenantId, onAddBatch, onEditBatch, onMarkAsWaste, waitForRefetch, isRefetching, onSaveComplete }) => { const { t } = useTranslation(['inventory', 'common']); const [editingBatch, setEditingBatch] = useState(null); const [editData, setEditData] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); const [isWaitingForRefetch, setIsWaitingForRefetch] = useState(false); // Collapsible state - start with all batches collapsed for better UX const [collapsedBatches, setCollapsedBatches] = useState>(new Set()); // Initialize all batches as collapsed when batches change or modal opens useEffect(() => { if (isOpen && batches.length > 0) { setCollapsedBatches(new Set(batches.map(b => b.id))); } }, [isOpen, batches]); // Fetch suppliers for the dropdown const { data: suppliersData } = useSuppliers(tenantId, { limit: 100 }, { enabled: !!tenantId && isOpen }); const suppliers = (suppliersData || []).filter(supplier => supplier.status === 'active'); // Get production stage options using direct i18n const productionStageOptions = Object.values(ingredient.product_type === 'finished_product' ? [] : [ { value: 'raw_ingredient', label: t(`enums.production_stage.raw_ingredient`) }, { value: 'par_baked', label: t(`enums.production_stage.par_baked`) }, { value: 'fully_baked', label: t(`enums.production_stage.fully_baked`) }, { value: 'prepared_dough', label: t(`enums.production_stage.prepared_dough`) }, { value: 'frozen_product', label: t(`enums.production_stage.frozen_product`) } ]); // Create quality status options (matches backend: good, damaged, expired, quarantined) const qualityStatusOptions = [ { value: 'good', label: 'Bueno' }, { value: 'damaged', label: 'Dañado' }, { value: 'expired', label: 'Vencido' }, { value: 'quarantined', label: 'En Cuarentena' } ]; // Create storage location options (predefined common locations) const storageLocationOptions = [ { value: '', label: 'Sin ubicación específica' }, { value: 'estante-a1', label: 'Estante A-1' }, { value: 'estante-a2', label: 'Estante A-2' }, { value: 'estante-a3', label: 'Estante A-3' }, { value: 'estante-b1', label: 'Estante B-1' }, { value: 'estante-b2', label: 'Estante B-2' }, { value: 'frigorifico', label: 'Frigorífico' }, { value: 'congelador', label: 'Congelador' }, { value: 'almacen-principal', label: 'Almacén Principal' }, { value: 'zona-recepcion', label: 'Zona de Recepción' } ]; // Create warehouse zone options const warehouseZoneOptions = [ { value: '', label: 'Sin zona específica' }, { value: 'zona-a', label: 'Zona A' }, { value: 'zona-b', label: 'Zona B' }, { value: 'zona-c', label: 'Zona C' }, { value: 'refrigerado', label: 'Refrigerado' }, { value: 'congelado', label: 'Congelado' }, { value: 'ambiente', label: 'Temperatura Ambiente' } ]; // Create refrigeration requirement options const refrigerationOptions = [ { value: 'no', label: 'No requiere refrigeración' }, { value: 'yes', label: 'Requiere refrigeración' }, { value: 'recommended', label: 'Refrigeración recomendada' } ]; // Create freezing requirement options const freezingOptions = [ { value: 'no', label: 'No requiere congelación' }, { value: 'yes', label: 'Requiere congelación' }, { value: 'recommended', label: 'Congelación recomendada' } ]; // Helper function to get translated category display name const getCategoryDisplayName = (category?: string | null): string => { if (!category) return t('categories.all', 'Sin categoría'); // Try ingredient category translation first const ingredientTranslation = t(`enums.ingredient_category.${category}`, { defaultValue: '' }); if (ingredientTranslation) return ingredientTranslation; // Try product category translation const productTranslation = t(`enums.product_category.${category}`, { defaultValue: '' }); if (productTranslation) return productTranslation; // Fallback to raw category if no translation found return category; }; // Toggle batch collapse state const toggleBatchCollapse = (batchId: string) => { setCollapsedBatches(prev => { const next = new Set(prev); if (next.has(batchId)) { next.delete(batchId); } else { next.add(batchId); } return next; }); }; // Expand all batches const expandAll = () => { setCollapsedBatches(new Set()); }; // Collapse all batches const collapseAll = () => { setCollapsedBatches(new Set(batches.map(b => b.id))); }; // Get batch status based on expiration and availability const getBatchStatus = (batch: StockResponse) => { if (!batch.is_available) { return { label: 'No Disponible', color: statusColors.cancelled.primary, icon: X, isCritical: true }; } if (batch.is_expired) { return { label: 'Vencido', color: statusColors.expired.primary, icon: AlertTriangle, isCritical: true }; } if (!batch.expiration_date) { return { label: 'Sin Vencimiento', color: statusColors.other.primary, icon: Archive, isCritical: false }; } const today = new Date(); const expirationDate = new Date(batch.expiration_date); const daysUntilExpiry = Math.ceil((expirationDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); if (daysUntilExpiry <= 0) { return { label: 'Vencido', color: statusColors.expired.primary, icon: AlertTriangle, isCritical: true }; } else if (daysUntilExpiry <= 3) { return { label: 'Vence Pronto', color: statusColors.low.primary, icon: AlertTriangle, isCritical: true }; } else if (daysUntilExpiry <= 7) { return { label: 'Por Vencer', color: statusColors.pending.primary, icon: Clock, isCritical: false }; } else { return { label: 'Fresco', color: statusColors.completed.primary, icon: CheckCircle, isCritical: false }; } }; const handleEditStart = (batch: StockResponse) => { setEditingBatch(batch.id); // Auto-expand when editing setCollapsedBatches(prev => { const next = new Set(prev); next.delete(batch.id); return next; }); setEditData({ supplier_id: batch.supplier_id || '', batch_number: batch.batch_number || '', lot_number: batch.lot_number || '', supplier_batch_ref: batch.supplier_batch_ref || '', production_stage: batch.production_stage, transformation_reference: batch.transformation_reference || '', current_quantity: batch.current_quantity, reserved_quantity: batch.reserved_quantity, received_date: batch.received_date, expiration_date: batch.expiration_date, best_before_date: batch.best_before_date, original_expiration_date: batch.original_expiration_date, transformation_date: batch.transformation_date, final_expiration_date: batch.final_expiration_date, unit_cost: batch.unit_cost !== null ? batch.unit_cost : undefined, storage_location: batch.storage_location || '', warehouse_zone: batch.warehouse_zone || '', shelf_position: batch.shelf_position || '', is_available: batch.is_available, quality_status: batch.quality_status, requires_refrigeration: batch.requires_refrigeration, requires_freezing: batch.requires_freezing, storage_temperature_min: batch.storage_temperature_min, storage_temperature_max: batch.storage_temperature_max, storage_humidity_max: batch.storage_humidity_max, shelf_life_days: batch.shelf_life_days, storage_instructions: batch.storage_instructions || '' }); }; const handleEditCancel = () => { setEditingBatch(null); setEditData({}); }; const handleEditSave = async (batchId: string) => { if (!onEditBatch) return; // CRITICAL: Capture editData IMMEDIATELY before any async operations const dataToSave = { ...editData }; // Validate we have data to save if (Object.keys(dataToSave).length === 0) { console.error('BatchModal: No edit data to save for batch', batchId); return; } console.log('BatchModal: Saving batch data:', dataToSave); setIsSubmitting(true); try { // Execute the update mutation await onEditBatch(batchId, dataToSave); // If waitForRefetch is enabled, wait for data to refresh if (waitForRefetch && onSaveComplete) { setIsWaitingForRefetch(true); // Trigger the refetch await onSaveComplete(); // Wait for isRefetching to become false (with timeout) const startTime = Date.now(); const refetchTimeout = 3000; await new Promise((resolve) => { const interval = setInterval(() => { const elapsed = Date.now() - startTime; if (elapsed >= refetchTimeout) { clearInterval(interval); console.warn('Refetch timeout reached for batch update'); resolve(); return; } if (!isRefetching) { clearInterval(interval); resolve(); } }, 100); }); setIsWaitingForRefetch(false); } // Clear editing state after save (and optional refetch) completes setEditingBatch(null); setEditData({}); } catch (error) { console.error('Error updating batch:', error); } finally { setIsSubmitting(false); setIsWaitingForRefetch(false); } }; const handleMarkAsWaste = async (batchId: string) => { if (!onMarkAsWaste) return; const confirmed = window.confirm('¿Está seguro que desea marcar este lote como desperdicio? Esta acción no se puede deshacer.'); if (!confirmed) return; setIsSubmitting(true); try { await onMarkAsWaste(batchId); } catch (error) { console.error('Error marking batch as waste:', error); } finally { setIsSubmitting(false); } }; const statusConfig = { color: statusColors.inProgress.primary, text: `${batches.length} lotes`, icon: Package }; // Create card-based batch list const batchCards = batches.length > 0 ? (
{batches.map((batch) => { const status = getBatchStatus(batch); const StatusIcon = status.icon; const isEditing = editingBatch === batch.id; return (
{/* Header */}
{/* Left side: clickable area for collapse/expand */} {/* Right side: action buttons */}
{/* Collapse/Expand chevron */} {!isEditing && ( )} {!isEditing && ( <> {status.isCritical && batch.is_available && ( )} )} {isEditing && ( <> )}
{/* Content - Only show when expanded */} {!collapsedBatches.has(batch.id) && (
{/* Quantities Section */}
{isEditing ? ( setEditData(prev => ({ ...prev, current_quantity: Number(e.target.value) }))} className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--background-primary)] text-[var(--text-primary)] rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> ) : (
{batch.current_quantity} {ingredient.unit_of_measure}
)}
{isEditing ? ( setEditData(prev => ({ ...prev, reserved_quantity: Number(e.target.value) }))} className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--background-primary)] text-[var(--text-primary)] rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> ) : (
{batch.reserved_quantity} {ingredient.unit_of_measure}
)}
{batch.available_quantity} {ingredient.unit_of_measure}
{isEditing ? ( setEditData(prev => ({ ...prev, unit_cost: Number(e.target.value) }))} className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--background-primary)] text-[var(--text-primary)] rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> ) : (
{batch.unit_cost ? formatters.currency(Number(batch.unit_cost)) : 'N/A'}
)}
{formatters.currency(Number(batch.total_cost || 0))}
{/* Batch Identification */}
{isEditing ? ( setEditData(prev => ({ ...prev, lot_number: e.target.value }))} placeholder="Número de lote" className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--background-primary)] text-[var(--text-primary)] rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> ) : (
{batch.lot_number || 'N/A'}
)}
{isEditing ? ( setEditData(prev => ({ ...prev, supplier_batch_ref: e.target.value }))} placeholder="Ref. del proveedor" className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--background-primary)] text-[var(--text-primary)] rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> ) : (
{batch.supplier_batch_ref || 'N/A'}
)}
{isEditing ? ( ) : (
{qualityStatusOptions.find(opt => opt.value === batch.quality_status)?.label || batch.quality_status}
)}
{/* Supplier and Dates */}
{isEditing ? ( ) : (
{batch.supplier_id ? suppliers.find(s => s.id === batch.supplier_id)?.name || 'Proveedor desconocido' : 'Sin proveedor' }
)}
{isEditing ? ( setEditData(prev => ({ ...prev, received_date: e.target.value }))} className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--background-primary)] text-[var(--text-primary)] rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> ) : (
{batch.received_date ? new Date(batch.received_date).toLocaleDateString('es-ES') : 'N/A' }
)}
{isEditing ? ( setEditData(prev => ({ ...prev, expiration_date: e.target.value }))} className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--background-primary)] text-[var(--text-primary)] rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> ) : (
{batch.expiration_date ? new Date(batch.expiration_date).toLocaleDateString('es-ES') : 'Sin vencimiento' }
)}
{isEditing ? ( setEditData(prev => ({ ...prev, best_before_date: e.target.value }))} className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--background-primary)] text-[var(--text-primary)] rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> ) : (
{batch.best_before_date ? new Date(batch.best_before_date).toLocaleDateString('es-ES') : 'N/A' }
)}
{/* Storage Locations */}
{isEditing ? ( ) : (
{storageLocationOptions.find(opt => opt.value === batch.storage_location)?.label || batch.storage_location || 'No especificada'}
)}
{isEditing ? ( ) : (
{warehouseZoneOptions.find(opt => opt.value === batch.warehouse_zone)?.label || batch.warehouse_zone || 'N/A'}
)}
{isEditing ? ( setEditData(prev => ({ ...prev, shelf_position: e.target.value }))} placeholder="Posición" className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--background-primary)] text-[var(--text-primary)] rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> ) : (
{batch.shelf_position || 'N/A'}
)}
{/* Production Stage Information */} {(batch.production_stage !== 'raw_ingredient' || batch.transformation_reference) && (
Información de Transformación
{isEditing && productionStageOptions.length > 0 ? ( ) : (
{t(`enums.production_stage.${batch.production_stage}`, { defaultValue: batch.production_stage })}
)}
{batch.transformation_reference && (
{batch.transformation_reference}
)} {batch.original_expiration_date && (
{new Date(batch.original_expiration_date).toLocaleDateString('es-ES')}
)} {batch.transformation_date && (
{new Date(batch.transformation_date).toLocaleDateString('es-ES')}
)} {batch.final_expiration_date && (
{new Date(batch.final_expiration_date).toLocaleDateString('es-ES')}
)}
)} {/* Storage Requirements */}
Requisitos de Almacenamiento
{isEditing ? ( ) : (
{batch.requires_refrigeration ? 'Sí' : 'No'}
)}
{isEditing ? ( ) : (
{batch.requires_freezing ? 'Sí' : 'No'}
)}
{isEditing ? ( setEditData(prev => ({ ...prev, shelf_life_days: Number(e.target.value) || null }))} placeholder="Días" className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--background-primary)] text-[var(--text-primary)] rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> ) : (
{batch.shelf_life_days ? `${batch.shelf_life_days} días` : 'N/A'}
)}
{isEditing ? ( setEditData(prev => ({ ...prev, storage_temperature_min: Number(e.target.value) || null }))} placeholder="°C" className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--background-primary)] text-[var(--text-primary)] rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> ) : (
{batch.storage_temperature_min !== null ? `${batch.storage_temperature_min}°C` : 'N/A'}
)}
{isEditing ? ( setEditData(prev => ({ ...prev, storage_temperature_max: Number(e.target.value) || null }))} placeholder="°C" className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--background-primary)] text-[var(--text-primary)] rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> ) : (
{batch.storage_temperature_max !== null ? `${batch.storage_temperature_max}°C` : 'N/A'}
)}
{isEditing ? ( setEditData(prev => ({ ...prev, storage_humidity_max: Number(e.target.value) || null }))} placeholder="%" className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--background-primary)] text-[var(--text-primary)] rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> ) : (
{batch.storage_humidity_max !== null ? `≤${batch.storage_humidity_max}%` : 'N/A'}
)}
{isEditing ? (