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'; export interface QualityCheckModalProps { isOpen: boolean; onClose: () => void; batch: ProductionBatchResponse; 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, onComplete }) => { const currentTenant = useCurrentTenant(); const [activeTab, setActiveTab] = useState('inspection'); const [selectedTemplate, setSelectedTemplate] = useState('visual_inspection'); 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); const template = QUALITY_CHECK_TEMPLATES[selectedTemplate as keyof typeof QUALITY_CHECK_TEMPLATES]; const currentCriteria = template?.criteria[currentCriteriaIndex]; const currentResult = currentCriteria ? results[currentCriteria.id] : undefined; const updateResult = useCallback((criterionId: string, updates: Partial) => { setResults(prev => ({ ...prev, [criterionId]: { criterionId, value: '', score: 0, pass: false, timestamp: new Date().toISOString(), ...prev[criterionId], ...updates } })); }, []); const calculateOverallScore = useCallback((): number => { if (!template || Object.keys(results).length === 0) return 0; const totalWeight = template.criteria.reduce((sum, c) => sum + c.weight, 0); const weightedScore = Object.values(results).reduce((sum, result) => { const criterion = template.criteria.find(c => c.id === result.criterionId); return sum + (result.score * (criterion?.weight || 0)); }, 0); return totalWeight > 0 ? weightedScore / totalWeight : 0; }, [template, results]); const getCriticalFailures = useCallback((): string[] => { if (!template) return []; const failures: string[] = []; template.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; }, [template, results]); const handlePhotoUpload = useCallback((event: React.ChangeEvent) => { const files = Array.from(event.target.files || []); setPhotos(prev => [...prev, ...files]); }, []); const handleComplete = async () => { if (!template) 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: template.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: template.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 = template?.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 {template && ( <> {/* Progress Indicator */}
{template.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)}