import React, { useState, useCallback, useRef } from 'react'; import { Card, Button, Badge, Input, Modal, Table, Select, DatePicker } from '../../ui'; import { productionService, type QualityCheckResponse, QualityCheckStatus } from '../../../services/api/production.service'; import type { QualityCheck, QualityCheckCriteria, ProductionBatch, QualityCheckType } from '../../../types/production.types'; interface QualityControlProps { className?: string; batchId?: string; checkType?: QualityCheckType; onQualityCheckCompleted?: (result: QualityCheck) => void; onCorrectiveActionRequired?: (check: QualityCheck, actions: string[]) => void; } interface QualityCheckTemplate { id: string; name: string; spanishName: string; productTypes: string[]; criteria: QualityChecklistItem[]; requiresPhotos: boolean; passThreshold: number; criticalPoints: string[]; } interface QualityChecklistItem { id: string; category: string; description: string; spanishDescription: string; type: 'boolean' | 'numeric' | 'scale' | 'text'; required: boolean; minValue?: number; maxValue?: number; unit?: string; acceptableCriteria?: string; weight: number; isCritical: boolean; } interface QualityInspectionResult { checklistId: string; value: string | number | boolean; notes?: string; photo?: File; timestamp: string; inspector: string; } const QUALITY_CHECK_TEMPLATES: Record = { visual_inspection: { id: 'visual_inspection', name: 'Visual Inspection', spanishName: 'Inspección Visual', productTypes: ['pan', 'bolleria', 'reposteria'], requiresPhotos: true, passThreshold: 80, criticalPoints: ['color_defects', 'structural_defects'], criteria: [ { id: 'color_uniformity', category: 'appearance', description: 'Color uniformity', spanishDescription: 'Uniformidad del color', type: 'scale', required: true, minValue: 1, maxValue: 5, acceptableCriteria: 'Score 3 or higher', weight: 20, isCritical: false, }, { id: 'surface_texture', category: 'appearance', description: 'Surface texture quality', spanishDescription: 'Calidad de la textura superficial', type: 'scale', required: true, minValue: 1, maxValue: 5, acceptableCriteria: 'Score 3 or higher', weight: 15, isCritical: false, }, { id: 'shape_integrity', category: 'structure', description: 'Shape and form integrity', spanishDescription: 'Integridad de forma y estructura', type: 'boolean', required: true, acceptableCriteria: 'Must be true', weight: 25, isCritical: true, }, { id: 'size_consistency', category: 'dimensions', description: 'Size consistency within batch', spanishDescription: 'Consistencia de tamaño en el lote', type: 'scale', required: true, minValue: 1, maxValue: 5, acceptableCriteria: 'Score 3 or higher', weight: 20, isCritical: false, }, { id: 'defects_presence', category: 'defects', description: 'Visible defects or imperfections', spanishDescription: 'Defectos visibles o imperfecciones', type: 'boolean', required: true, acceptableCriteria: 'Must be false', weight: 20, isCritical: true, }, ], }, weight_check: { id: 'weight_check', name: 'Weight Check', spanishName: 'Control de Peso', productTypes: ['pan', 'bolleria', 'reposteria'], requiresPhotos: false, passThreshold: 95, criticalPoints: ['weight_variance'], criteria: [ { id: 'individual_weight', category: 'weight', description: 'Individual piece weight', spanishDescription: 'Peso individual de la pieza', type: 'numeric', required: true, minValue: 0, unit: 'g', acceptableCriteria: 'Within ±5% of target', weight: 40, isCritical: true, }, { id: 'batch_average_weight', category: 'weight', description: 'Batch average weight', spanishDescription: 'Peso promedio del lote', type: 'numeric', required: true, minValue: 0, unit: 'g', acceptableCriteria: 'Within ±3% of target', weight: 30, isCritical: true, }, { id: 'weight_variance', category: 'consistency', description: 'Weight variance within batch', spanishDescription: 'Variación de peso dentro del lote', type: 'numeric', required: true, minValue: 0, unit: '%', acceptableCriteria: 'Less than 5%', weight: 30, isCritical: true, }, ], }, temperature_check: { id: 'temperature_check', name: 'Temperature Check', spanishName: 'Control de Temperatura', productTypes: ['pan', 'bolleria'], requiresPhotos: false, passThreshold: 90, criticalPoints: ['core_temperature'], criteria: [ { id: 'core_temperature', category: 'temperature', description: 'Core temperature', spanishDescription: 'Temperatura del núcleo', type: 'numeric', required: true, minValue: 85, maxValue: 98, unit: '°C', acceptableCriteria: '88-95°C', weight: 60, isCritical: true, }, { id: 'cooling_temperature', category: 'temperature', description: 'Cooling temperature', spanishDescription: 'Temperatura de enfriado', type: 'numeric', required: false, minValue: 18, maxValue: 25, unit: '°C', acceptableCriteria: '20-23°C', weight: 40, isCritical: false, }, ], }, packaging_quality: { id: 'packaging_quality', name: 'Packaging Quality', spanishName: 'Calidad del Empaquetado', productTypes: ['pan', 'bolleria', 'reposteria'], requiresPhotos: true, passThreshold: 85, criticalPoints: ['seal_integrity', 'labeling_accuracy'], criteria: [ { id: 'seal_integrity', category: 'packaging', description: 'Package seal integrity', spanishDescription: 'Integridad del sellado del envase', type: 'boolean', required: true, acceptableCriteria: 'Must be true', weight: 30, isCritical: true, }, { id: 'labeling_accuracy', category: 'labeling', description: 'Label accuracy and placement', spanishDescription: 'Precisión y colocación de etiquetas', type: 'scale', required: true, minValue: 1, maxValue: 5, acceptableCriteria: 'Score 4 or higher', weight: 25, isCritical: true, }, { id: 'package_appearance', category: 'appearance', description: 'Overall package appearance', spanishDescription: 'Apariencia general del envase', type: 'scale', required: true, minValue: 1, maxValue: 5, acceptableCriteria: 'Score 3 or higher', weight: 20, isCritical: false, }, { id: 'barcode_readability', category: 'labeling', description: 'Barcode readability', spanishDescription: 'Legibilidad del código de barras', type: 'boolean', required: true, acceptableCriteria: 'Must be true', weight: 25, isCritical: false, }, ], }, }; const STATUS_COLORS = { [QualityCheckStatus.SCHEDULED]: 'bg-[var(--color-info)]/10 text-[var(--color-info)]', [QualityCheckStatus.IN_PROGRESS]: 'bg-yellow-100 text-yellow-800', [QualityCheckStatus.PASSED]: 'bg-[var(--color-success)]/10 text-[var(--color-success)]', [QualityCheckStatus.FAILED]: 'bg-[var(--color-error)]/10 text-[var(--color-error)]', [QualityCheckStatus.REQUIRES_REVIEW]: 'bg-[var(--color-primary)]/10 text-[var(--color-primary)]', [QualityCheckStatus.CANCELLED]: 'bg-[var(--bg-tertiary)] text-[var(--text-primary)]', }; export const QualityControl: React.FC = ({ className = '', batchId, checkType, onQualityCheckCompleted, onCorrectiveActionRequired, }) => { const [qualityChecks, setQualityChecks] = useState([]); const [selectedTemplate, setSelectedTemplate] = useState(null); const [activeCheck, setActiveCheck] = useState(null); const [inspectionResults, setInspectionResults] = useState>({}); const [loading, setLoading] = useState(false); const [isInspectionModalOpen, setIsInspectionModalOpen] = useState(false); const [uploadedPhotos, setUploadedPhotos] = useState>({}); const [currentInspector, setCurrentInspector] = useState('inspector-1'); const fileInputRef = useRef(null); const loadQualityChecks = useCallback(async () => { setLoading(true); try { const params: any = {}; if (batchId) params.batch_id = batchId; if (checkType) params.check_type = checkType; const response = await productionService.getQualityChecks(params); if (response.success && response.data) { setQualityChecks(response.data.items || []); } } catch (error) { console.error('Error loading quality checks:', error); } finally { setLoading(false); } }, [batchId, checkType]); const startQualityCheck = (template: QualityCheckTemplate, batch?: any) => { setSelectedTemplate(template); setInspectionResults({}); setUploadedPhotos({}); setIsInspectionModalOpen(true); // Initialize empty results for all criteria template.criteria.forEach(criterion => { setInspectionResults(prev => ({ ...prev, [criterion.id]: { checklistId: criterion.id, value: criterion.type === 'boolean' ? false : criterion.type === 'numeric' ? 0 : '', timestamp: new Date().toISOString(), inspector: currentInspector, } })); }); }; const updateInspectionResult = (criterionId: string, value: string | number | boolean, notes?: string) => { setInspectionResults(prev => ({ ...prev, [criterionId]: { ...prev[criterionId], value, notes, timestamp: new Date().toISOString(), } })); }; const handlePhotoUpload = (criterionId: string, file: File) => { setUploadedPhotos(prev => ({ ...prev, [criterionId]: file, })); // Update inspection result with photo reference setInspectionResults(prev => ({ ...prev, [criterionId]: { ...prev[criterionId], photo: file, } })); }; const calculateOverallScore = (): number => { if (!selectedTemplate) return 0; let totalScore = 0; let totalWeight = 0; selectedTemplate.criteria.forEach(criterion => { const result = inspectionResults[criterion.id]; if (result) { let score = 0; if (criterion.type === 'boolean') { score = result.value ? 100 : 0; } else if (criterion.type === 'scale') { const numValue = Number(result.value); score = (numValue / (criterion.maxValue || 5)) * 100; } else if (criterion.type === 'numeric') { // For numeric values, assume pass/fail based on acceptable range score = 100; // Simplified - would need more complex logic } totalScore += score * criterion.weight; totalWeight += criterion.weight; } }); return totalWeight > 0 ? totalScore / totalWeight : 0; }; const checkCriticalFailures = (): string[] => { if (!selectedTemplate) return []; const failures: string[] = []; selectedTemplate.criteria.forEach(criterion => { if (criterion.isCritical) { const result = inspectionResults[criterion.id]; if (result) { if (criterion.type === 'boolean' && !result.value) { failures.push(criterion.spanishDescription); } else if (criterion.type === 'scale' && Number(result.value) < 3) { failures.push(criterion.spanishDescription); } } } }); return failures; }; const completeQualityCheck = async () => { if (!selectedTemplate) return; const overallScore = calculateOverallScore(); const criticalFailures = checkCriticalFailures(); const passed = overallScore >= selectedTemplate.passThreshold && criticalFailures.length === 0; const checkData = { status: passed ? QualityCheckStatus.PASSED : QualityCheckStatus.FAILED, results: { overallScore, criticalFailures, individualResults: inspectionResults, photos: Object.keys(uploadedPhotos), }, notes: `Inspección completada. Puntuación: ${overallScore.toFixed(1)}%`, corrective_actions: criticalFailures.length > 0 ? [ `Fallas críticas encontradas: ${criticalFailures.join(', ')}`, 'Revisar proceso de producción', 'Re-entrenar personal si es necesario' ] : undefined, }; try { // This would be the actual check ID from the created quality check const mockCheckId = 'check-id'; const response = await productionService.completeQualityCheck(mockCheckId, checkData); if (response.success) { if (onQualityCheckCompleted) { onQualityCheckCompleted(response.data as unknown as QualityCheck); } if (!passed && checkData.corrective_actions && onCorrectiveActionRequired) { onCorrectiveActionRequired(response.data as unknown as QualityCheck, checkData.corrective_actions); } setIsInspectionModalOpen(false); setSelectedTemplate(null); await loadQualityChecks(); } } catch (error) { console.error('Error completing quality check:', error); } }; const renderInspectionForm = () => { if (!selectedTemplate) return null; return (

{selectedTemplate.spanishName}

Umbral de aprobación: {selectedTemplate.passThreshold}%

{selectedTemplate.criticalPoints.length > 0 && (

Puntos críticos: {selectedTemplate.criticalPoints.length}

)}
{selectedTemplate.criteria.map((criterion) => (

{criterion.spanishDescription}

{criterion.acceptableCriteria}

{criterion.isCritical && ( Punto Crítico )}
Peso: {criterion.weight}%
{criterion.type === 'boolean' && (
)} {criterion.type === 'scale' && (
1 updateInspectionResult(criterion.id, parseInt(e.target.value))} className="flex-1" /> {criterion.maxValue || 5} {inspectionResults[criterion.id]?.value || criterion.minValue || 1}
)} {criterion.type === 'numeric' && (
updateInspectionResult(criterion.id, parseFloat(e.target.value) || 0)} className="w-24" /> {criterion.unit && {criterion.unit}}
)} { const currentResult = inspectionResults[criterion.id]; if (currentResult) { updateInspectionResult(criterion.id, currentResult.value, e.target.value); } }} /> {selectedTemplate.requiresPhotos && (
{ const file = e.target.files?.[0]; if (file) { handlePhotoUpload(criterion.id, file); } }} className="hidden" /> {uploadedPhotos[criterion.id] && (

✓ Foto capturada: {uploadedPhotos[criterion.id].name}

)}
)}
))}
Puntuación general {calculateOverallScore().toFixed(1)}%
= selectedTemplate.passThreshold ? 'bg-green-500' : 'bg-red-500' }`} style={{ width: `${calculateOverallScore()}%` }} />
Umbral: {selectedTemplate.passThreshold}% = selectedTemplate.passThreshold ? 'text-[var(--color-success)] font-medium' : 'text-[var(--color-error)] font-medium' }> {calculateOverallScore() >= selectedTemplate.passThreshold ? '✓ APROBADO' : '✗ REPROBADO'}
{checkCriticalFailures().length > 0 && (

Fallas críticas detectadas:

    {checkCriticalFailures().map((failure, index) => (
  • • {failure}
  • ))}
)}
); }; const renderQualityChecksTable = () => { const columns = [ { key: 'batch', label: 'Lote', sortable: true }, { key: 'type', label: 'Tipo de control', sortable: true }, { key: 'status', label: 'Estado', sortable: true }, { key: 'inspector', label: 'Inspector', sortable: false }, { key: 'scheduled_date', label: 'Fecha programada', sortable: true }, { key: 'completed_date', label: 'Fecha completada', sortable: true }, { key: 'actions', label: 'Acciones', sortable: false }, ]; const data = qualityChecks.map(check => ({ id: check.id, batch: `#${check.batch?.batch_number || 'N/A'}`, type: QUALITY_CHECK_TEMPLATES[check.check_type]?.spanishName || check.check_type, status: ( {check.status === QualityCheckStatus.SCHEDULED && 'Programado'} {check.status === QualityCheckStatus.IN_PROGRESS && 'En progreso'} {check.status === QualityCheckStatus.PASSED && 'Aprobado'} {check.status === QualityCheckStatus.FAILED && 'Reprobado'} {check.status === QualityCheckStatus.REQUIRES_REVIEW && 'Requiere revisión'} ), inspector: check.inspector || 'No asignado', scheduled_date: check.scheduled_date ? new Date(check.scheduled_date).toLocaleDateString('es-ES') : '-', completed_date: check.completed_date ? new Date(check.completed_date).toLocaleDateString('es-ES') : '-', actions: (
{check.status === QualityCheckStatus.SCHEDULED && ( )}
), })); return ; }; return (

Control de Calidad

Gestiona las inspecciones de calidad y cumplimiento

{/* Quick Start Templates */}
{Object.values(QUALITY_CHECK_TEMPLATES).map((template) => ( startQualityCheck(template)} >

{template.spanishName}

{template.requiresPhotos && ( 📸 Fotos )}

{template.criteria.length} criterios • Umbral: {template.passThreshold}%

{template.productTypes.map((type) => ( {type === 'pan' && 'Pan'} {type === 'bolleria' && 'Bollería'} {type === 'reposteria' && 'Repostería'} ))}
{template.criticalPoints.length > 0 && (

{template.criticalPoints.length} puntos críticos

)}
))}
{/* Quality Checks Table */}

Controles de calidad recientes

{loading ? (
) : ( renderQualityChecksTable() )}
{/* Quality Check Modal */} { setIsInspectionModalOpen(false); setSelectedTemplate(null); }} title={`Control de Calidad: ${selectedTemplate?.spanishName}`} size="lg" > {renderInspectionForm()}
{/* Stats Cards */}

Tasa de aprobación

94.2%

Controles pendientes

7

Fallas críticas

2

🚨

Controles hoy

23

📋
); }; export default QualityControl;