import React, { useState, useCallback, useRef } from 'react'; import { Camera, CheckCircle, XCircle, Star, Upload, FileText, Package, Thermometer, Scale, Eye, AlertTriangle } from 'lucide-react'; import { Modal, Button, Input, Textarea, Badge, Tabs, Card } from '../../ui'; import { TabsList, TabsTrigger, TabsContent } from '../../ui/Tabs'; import { StatusIndicatorConfig } from '../../ui/StatusCard/StatusCard'; import { statusColors } from '../../../styles/colors'; import { productionService, type QualityCheckResponse, QualityCheckStatus } from '../../../api'; import { ProductionBatchResponse } from '../../../api/types/production'; import { useCurrentTenant } from '../../../stores/tenant.store'; import { useQualityTemplatesForStage, useExecuteQualityCheck } from '../../../api/hooks/qualityTemplates'; import { ProcessStage, type QualityCheckTemplate, type QualityCheckExecutionRequest } from '../../../api/types/qualityTemplates'; export interface QualityCheckModalProps { isOpen: boolean; onClose: () => void; batch: ProductionBatchResponse; processStage?: ProcessStage; onComplete?: (result: QualityCheckResult) => void; } export interface QualityCheckCriterion { id: string; category: string; name: string; spanishName: string; description: string; type: 'visual' | 'measurement' | 'temperature' | 'weight' | 'boolean'; required: boolean; weight: number; acceptableCriteria: string; minValue?: number; maxValue?: number; unit?: string; isCritical: boolean; } export interface QualityCheckResult { criterionId: string; value: number | string | boolean; score: number; notes?: string; photos?: File[]; pass: boolean; timestamp: string; } export interface QualityCheckData { batchId: string; checkType: string; inspector: string; startTime: string; results: QualityCheckResult[]; overallScore: number; overallPass: boolean; finalNotes: string; photos: File[]; criticalFailures: string[]; correctiveActions: string[]; } const QUALITY_CHECK_TEMPLATES = { visual_inspection: { id: 'visual_inspection', name: 'Visual Inspection', spanishName: 'Inspección Visual', icon: Eye, criteria: [ { id: 'color_uniformity', category: 'Apariencia', name: 'Color Uniformity', spanishName: 'Uniformidad del Color', description: 'Evalúa la consistencia del color en todo el producto', type: 'visual' as const, required: true, weight: 20, acceptableCriteria: 'Puntuación 7 o superior', minValue: 1, maxValue: 10, isCritical: false, }, { id: 'shape_integrity', category: 'Estructura', name: 'Shape Integrity', spanishName: 'Integridad de la Forma', description: 'Verifica que el producto mantenga su forma prevista', type: 'boolean' as const, required: true, weight: 25, acceptableCriteria: 'Debe ser verdadero', isCritical: true, }, { id: 'surface_texture', category: 'Textura', name: 'Surface Texture', spanishName: 'Textura Superficial', description: 'Evalúa la calidad de la textura de la superficie', type: 'visual' as const, required: true, weight: 15, acceptableCriteria: 'Puntuación 7 o superior', minValue: 1, maxValue: 10, isCritical: false, }, { id: 'defects_presence', category: 'Defectos', name: 'Defects Presence', spanishName: 'Presencia de Defectos', description: 'Detecta defectos visibles o imperfecciones', type: 'boolean' as const, required: true, weight: 20, acceptableCriteria: 'Debe ser falso (sin defectos)', isCritical: true, }, { id: 'size_consistency', category: 'Dimensiones', name: 'Size Consistency', spanishName: 'Consistencia de Tamaño', description: 'Evalúa la consistencia del tamaño dentro del lote', type: 'visual' as const, required: true, weight: 20, acceptableCriteria: 'Puntuación 7 o superior', minValue: 1, maxValue: 10, isCritical: false, } ] }, weight_check: { id: 'weight_check', name: 'Weight Check', spanishName: 'Control de Peso', icon: Scale, criteria: [ { id: 'individual_weight', category: 'Peso', name: 'Individual Weight', spanishName: 'Peso Individual', description: 'Peso individual de cada pieza', type: 'weight' as const, required: true, weight: 50, acceptableCriteria: 'Dentro del ±5% del objetivo', unit: 'g', isCritical: true, }, { id: 'weight_variance', category: 'Consistencia', name: 'Weight Variance', spanishName: 'Variación de Peso', description: 'Variación de peso dentro del lote', type: 'measurement' as const, required: true, weight: 50, acceptableCriteria: 'Menos del 5%', unit: '%', isCritical: true, } ] }, temperature_check: { id: 'temperature_check', name: 'Temperature Check', spanishName: 'Control de Temperatura', icon: Thermometer, criteria: [ { id: 'core_temperature', category: 'Temperatura', name: 'Core Temperature', spanishName: 'Temperatura del Núcleo', description: 'Temperatura interna del producto', type: 'temperature' as const, required: true, weight: 70, acceptableCriteria: '88-95°C', minValue: 85, maxValue: 98, unit: '°C', isCritical: true, }, { id: 'cooling_temperature', category: 'Temperatura', name: 'Cooling Temperature', spanishName: 'Temperatura de Enfriado', description: 'Temperatura durante el enfriado', type: 'temperature' as const, required: false, weight: 30, acceptableCriteria: '20-23°C', minValue: 18, maxValue: 25, unit: '°C', isCritical: false, } ] } }; export const QualityCheckModal: React.FC = ({ isOpen, onClose, batch, processStage, onComplete }) => { const currentTenant = useCurrentTenant(); const tenantId = currentTenant?.id || ''; const [activeTab, setActiveTab] = useState('inspection'); const [selectedTemplate, setSelectedTemplate] = useState(null); const [currentCriteriaIndex, setCurrentCriteriaIndex] = useState(0); const [results, setResults] = useState>({}); const [finalNotes, setFinalNotes] = useState(''); const [photos, setPhotos] = useState([]); const [loading, setLoading] = useState(false); const fileInputRef = useRef(null); // Get available templates for the current stage const currentStage = processStage || batch.current_process_stage; const { data: templatesData, isLoading: templatesLoading, error: templatesError } = useQualityTemplatesForStage(tenantId, currentStage!, true, { enabled: !!currentStage }); // Execute quality check mutation const executeQualityCheckMutation = useExecuteQualityCheck(tenantId); // Initialize selected template React.useEffect(() => { if (templatesData?.templates && templatesData.templates.length > 0 && !selectedTemplate) { setSelectedTemplate(templatesData.templates[0]); } }, [templatesData?.templates, selectedTemplate]); const availableTemplates = templatesData?.templates || []; const updateResult = useCallback((field: string, value: any, score?: number) => { setResults(prev => ({ ...prev, [field]: { value, score: score !== undefined ? score : (typeof value === 'boolean' ? (value ? 10 : 0) : value), pass_check: score !== undefined ? score >= 7 : (typeof value === 'boolean' ? value : value >= 7), timestamp: new Date().toISOString() } })); }, []); const calculateOverallScore = useCallback((): number => { if (!selectedTemplate || Object.keys(results).length === 0) return 0; const templateWeight = selectedTemplate.weight || 1; const resultValues = Object.values(results); if (resultValues.length === 0) return 0; const averageScore = resultValues.reduce((sum: number, result: any) => sum + (result.score || 0), 0) / resultValues.length; return Math.min(10, averageScore * templateWeight); }, [selectedTemplate, results]); const getCriticalFailures = useCallback((): string[] => { if (!selectedTemplate) return []; const failures: string[] = []; selectedTemplate.criteria.forEach(criterion => { if (criterion.isCritical && results[criterion.id]) { const result = results[criterion.id]; if (criterion.type === 'boolean' && !result.value) { failures.push(criterion.spanishName); } else if (result.score < 7) { failures.push(criterion.spanishName); } } }); return failures; }, [selectedTemplate, results]); const handlePhotoUpload = useCallback((event: React.ChangeEvent) => { const files = Array.from(event.target.files || []); setPhotos(prev => [...prev, ...files]); }, []); const handleComplete = async () => { if (!selectedTemplate) return; setLoading(true); try { const overallScore = calculateOverallScore(); const criticalFailures = getCriticalFailures(); const passed = overallScore >= 7.0 && criticalFailures.length === 0; const qualityData: QualityCheckData = { batchId: batch.id, checkType: selectedTemplate.id, inspector: currentTenant?.name || 'Inspector', startTime: new Date().toISOString(), results: Object.values(results), overallScore, overallPass: passed, finalNotes, photos, criticalFailures, correctiveActions: criticalFailures.length > 0 ? [ `Fallas críticas encontradas: ${criticalFailures.join(', ')}`, 'Revisar proceso de producción', 'Re-entrenar personal si es necesario' ] : [] }; // Create quality check via API const checkData = { batch_id: batch.id, check_type: selectedTemplate.id, check_time: new Date().toISOString(), quality_score: overallScore / 10, // Convert to 0-1 scale pass_fail: passed, defect_count: criticalFailures.length, defect_types: criticalFailures, check_notes: finalNotes, corrective_actions: qualityData.correctiveActions }; await productionService.createQualityCheck(currentTenant?.id || '', checkData); onComplete?.(qualityData as any); onClose(); } catch (error) { console.error('Error completing quality check:', error); } finally { setLoading(false); } }; const renderCriteriaInput = (criterion: QualityCheckCriterion) => { const result = results[criterion.id]; if (criterion.type === 'boolean') { return (
); } if (criterion.type === 'measurement' || criterion.type === 'weight' || criterion.type === 'temperature') { return (
{ const value = parseFloat(e.target.value); const score = !isNaN(value) ? 8 : 0; // Simplified scoring updateResult(criterion.id, { value: e.target.value, score, pass: score >= 7 }); }} className="w-32" /> {criterion.unit && {criterion.unit}}
); } // Visual type - 1-10 star rating return (
{[...Array(10)].map((_, i) => { const score = i + 1; const isSelected = result?.score === score; return ( ); })}

1-3: Malo | 4-6: Regular | 7-8: Bueno | 9-10: Excelente

); }; const overallScore = calculateOverallScore(); const criticalFailures = getCriticalFailures(); const isComplete = selectedTemplate?.criteria.filter(c => c.required).every(c => results[c.id]) || false; const statusIndicator: StatusIndicatorConfig = { color: statusColors.inProgress.primary, text: 'Control de Calidad', icon: Package }; return (
{/* Template Selection */}
{Object.entries(QUALITY_CHECK_TEMPLATES).map(([key, tmpl]) => { const Icon = tmpl.icon; return ( ); })}
Inspección Fotos Resumen {selectedTemplate && ( <> {/* Progress Indicator */}
{selectedTemplate.criteria.map((criterion, index) => { const result = results[criterion.id]; const isCompleted = !!result; const isCurrent = index === currentCriteriaIndex; return ( ); })}
{/* Current Criteria */} {currentCriteria && (

{currentCriteria.spanishName}

{currentCriteria.description}

{currentCriteria.acceptableCriteria}

{currentCriteria.isCritical && ( Crítico )} {currentCriteria.required && ( Requerido )}
{renderCriteriaInput(currentCriteria)}