Files
bakery-ia/frontend/src/components/domain/production/QualityControl.tsx
2025-08-28 10:41:04 +02:00

882 lines
30 KiB
TypeScript

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<string, QualityCheckTemplate> = {
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<QualityControlProps> = ({
className = '',
batchId,
checkType,
onQualityCheckCompleted,
onCorrectiveActionRequired,
}) => {
const [qualityChecks, setQualityChecks] = useState<QualityCheckResponse[]>([]);
const [selectedTemplate, setSelectedTemplate] = useState<QualityCheckTemplate | null>(null);
const [activeCheck, setActiveCheck] = useState<QualityCheck | null>(null);
const [inspectionResults, setInspectionResults] = useState<Record<string, QualityInspectionResult>>({});
const [loading, setLoading] = useState(false);
const [isInspectionModalOpen, setIsInspectionModalOpen] = useState(false);
const [uploadedPhotos, setUploadedPhotos] = useState<Record<string, File>>({});
const [currentInspector, setCurrentInspector] = useState<string>('inspector-1');
const fileInputRef = useRef<HTMLInputElement>(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 (
<div className="space-y-6">
<div className="bg-[var(--color-info)]/5 border border-[var(--color-info)]/20 p-4 rounded-lg">
<h3 className="font-semibold text-blue-900 mb-2">{selectedTemplate.spanishName}</h3>
<p className="text-sm text-[var(--color-info)]">
Umbral de aprobación: {selectedTemplate.passThreshold}%
</p>
{selectedTemplate.criticalPoints.length > 0 && (
<p className="text-sm text-[var(--color-info)] mt-1">
Puntos críticos: {selectedTemplate.criticalPoints.length}
</p>
)}
</div>
<div className="space-y-4">
{selectedTemplate.criteria.map((criterion) => (
<Card key={criterion.id} className={`p-4 ${criterion.isCritical ? 'border-orange-200 bg-orange-50' : ''}`}>
<div className="flex justify-between items-start mb-3">
<div className="flex-1">
<h4 className="font-medium text-[var(--text-primary)]">{criterion.spanishDescription}</h4>
<p className="text-sm text-[var(--text-secondary)] mt-1">{criterion.acceptableCriteria}</p>
{criterion.isCritical && (
<Badge className="bg-[var(--color-primary)]/10 text-[var(--color-primary)] text-xs mt-1">
Punto Crítico
</Badge>
)}
</div>
<span className="text-sm text-[var(--text-tertiary)]">Peso: {criterion.weight}%</span>
</div>
<div className="space-y-3">
{criterion.type === 'boolean' && (
<div className="flex gap-4">
<label className="flex items-center">
<input
type="radio"
name={criterion.id}
value="true"
checked={inspectionResults[criterion.id]?.value === true}
onChange={() => updateInspectionResult(criterion.id, true)}
className="mr-2"
/>
/ Pasa
</label>
<label className="flex items-center">
<input
type="radio"
name={criterion.id}
value="false"
checked={inspectionResults[criterion.id]?.value === false}
onChange={() => updateInspectionResult(criterion.id, false)}
className="mr-2"
/>
No / Falla
</label>
</div>
)}
{criterion.type === 'scale' && (
<div className="flex items-center gap-4">
<span className="text-sm">1</span>
<input
type="range"
min={criterion.minValue || 1}
max={criterion.maxValue || 5}
step="1"
value={inspectionResults[criterion.id]?.value || criterion.minValue || 1}
onChange={(e) => updateInspectionResult(criterion.id, parseInt(e.target.value))}
className="flex-1"
/>
<span className="text-sm">{criterion.maxValue || 5}</span>
<span className="min-w-8 text-center font-medium">
{inspectionResults[criterion.id]?.value || criterion.minValue || 1}
</span>
</div>
)}
{criterion.type === 'numeric' && (
<div className="flex items-center gap-2">
<Input
type="number"
min={criterion.minValue}
max={criterion.maxValue}
step="0.1"
value={inspectionResults[criterion.id]?.value || ''}
onChange={(e) => updateInspectionResult(criterion.id, parseFloat(e.target.value) || 0)}
className="w-24"
/>
{criterion.unit && <span className="text-sm text-[var(--text-secondary)]">{criterion.unit}</span>}
</div>
)}
<Input
placeholder="Notas adicionales (opcional)"
value={inspectionResults[criterion.id]?.notes || ''}
onChange={(e) => {
const currentResult = inspectionResults[criterion.id];
if (currentResult) {
updateInspectionResult(criterion.id, currentResult.value, e.target.value);
}
}}
/>
{selectedTemplate.requiresPhotos && (
<div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
handlePhotoUpload(criterion.id, file);
}
}}
className="hidden"
/>
<Button
variant="outline"
size="sm"
onClick={() => fileInputRef.current?.click()}
>
📸 {uploadedPhotos[criterion.id] ? 'Cambiar foto' : 'Tomar foto'}
</Button>
{uploadedPhotos[criterion.id] && (
<p className="text-sm text-[var(--color-success)] mt-1">
Foto capturada: {uploadedPhotos[criterion.id].name}
</p>
)}
</div>
)}
</div>
</Card>
))}
</div>
<Card className="p-4 bg-[var(--bg-secondary)]">
<div className="flex justify-between items-center mb-2">
<span className="font-medium">Puntuación general</span>
<span className="text-2xl font-bold text-[var(--color-info)]">
{calculateOverallScore().toFixed(1)}%
</span>
</div>
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-3 mb-2">
<div
className={`h-3 rounded-full transition-all duration-500 ${
calculateOverallScore() >= selectedTemplate.passThreshold
? 'bg-green-500'
: 'bg-red-500'
}`}
style={{ width: `${calculateOverallScore()}%` }}
/>
</div>
<div className="flex justify-between text-sm text-[var(--text-secondary)]">
<span>Umbral: {selectedTemplate.passThreshold}%</span>
<span className={
calculateOverallScore() >= selectedTemplate.passThreshold
? 'text-[var(--color-success)] font-medium'
: 'text-[var(--color-error)] font-medium'
}>
{calculateOverallScore() >= selectedTemplate.passThreshold ? '✓ APROBADO' : '✗ REPROBADO'}
</span>
</div>
{checkCriticalFailures().length > 0 && (
<div className="mt-3 p-2 bg-[var(--color-error)]/10 border border-red-200 rounded">
<p className="text-sm font-medium text-[var(--color-error)]">Fallas críticas detectadas:</p>
<ul className="text-sm text-[var(--color-error)] mt-1">
{checkCriticalFailures().map((failure, index) => (
<li key={index}> {failure}</li>
))}
</ul>
</div>
)}
</Card>
</div>
);
};
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: (
<Badge className={STATUS_COLORS[check.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'}
</Badge>
),
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: (
<div className="flex gap-2">
{check.status === QualityCheckStatus.SCHEDULED && (
<Button
variant="primary"
size="sm"
onClick={() => {
const template = QUALITY_CHECK_TEMPLATES[check.check_type];
if (template) {
startQualityCheck(template);
}
}}
>
Iniciar
</Button>
)}
<Button variant="outline" size="sm">
Ver detalles
</Button>
</div>
),
}));
return <Table columns={columns} data={data} />;
};
return (
<div className={`space-y-6 ${className}`}>
<div className="flex justify-between items-center">
<div>
<h2 className="text-2xl font-bold text-[var(--text-primary)]">Control de Calidad</h2>
<p className="text-[var(--text-secondary)]">Gestiona las inspecciones de calidad y cumplimiento</p>
</div>
<div className="flex gap-2">
<Select
value={currentInspector}
onChange={(e) => setCurrentInspector(e.target.value)}
className="w-48"
>
<option value="inspector-1">María García (Inspector)</option>
<option value="inspector-2">Juan López (Supervisor)</option>
<option value="inspector-3">Ana Martín (Jefe Calidad)</option>
</Select>
<Button
variant="primary"
onClick={() => {
// Show modal to select quality check type
const template = QUALITY_CHECK_TEMPLATES.visual_inspection;
startQualityCheck(template);
}}
>
Nueva inspección
</Button>
</div>
</div>
{/* Quick Start Templates */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{Object.values(QUALITY_CHECK_TEMPLATES).map((template) => (
<Card
key={template.id}
className="p-4 cursor-pointer hover:shadow-md transition-shadow border-2 border-transparent hover:border-[var(--color-info)]/20"
onClick={() => startQualityCheck(template)}
>
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-[var(--text-primary)]">{template.spanishName}</h3>
{template.requiresPhotos && (
<span className="text-sm bg-[var(--color-info)]/10 text-[var(--color-info)] px-2 py-1 rounded">
📸 Fotos
</span>
)}
</div>
<p className="text-sm text-[var(--text-secondary)] mb-3">
{template.criteria.length} criterios Umbral: {template.passThreshold}%
</p>
<div className="flex flex-wrap gap-1">
{template.productTypes.map((type) => (
<Badge key={type} className="text-xs bg-[var(--bg-tertiary)] text-[var(--text-secondary)]">
{type === 'pan' && 'Pan'}
{type === 'bolleria' && 'Bollería'}
{type === 'reposteria' && 'Repostería'}
</Badge>
))}
</div>
{template.criticalPoints.length > 0 && (
<p className="text-xs text-[var(--color-primary)] mt-2">
{template.criticalPoints.length} puntos críticos
</p>
)}
</Card>
))}
</div>
{/* Quality Checks Table */}
<Card className="p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">Controles de calidad recientes</h3>
<div className="flex gap-2">
<Select className="w-40">
<option value="all">Todos los estados</option>
<option value={QualityCheckStatus.SCHEDULED}>Programados</option>
<option value={QualityCheckStatus.IN_PROGRESS}>En progreso</option>
<option value={QualityCheckStatus.PASSED}>Aprobados</option>
<option value={QualityCheckStatus.FAILED}>Reprobados</option>
</Select>
<Button variant="outline" onClick={loadQualityChecks}>
Actualizar
</Button>
</div>
</div>
{loading ? (
<div className="flex justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
) : (
renderQualityChecksTable()
)}
</Card>
{/* Quality Check Modal */}
<Modal
isOpen={isInspectionModalOpen}
onClose={() => {
setIsInspectionModalOpen(false);
setSelectedTemplate(null);
}}
title={`Control de Calidad: ${selectedTemplate?.spanishName}`}
size="lg"
>
{renderInspectionForm()}
<div className="flex justify-end gap-2 pt-6 border-t">
<Button
variant="outline"
onClick={() => {
setIsInspectionModalOpen(false);
setSelectedTemplate(null);
}}
>
Cancelar
</Button>
<Button
variant="primary"
onClick={completeQualityCheck}
disabled={!selectedTemplate || Object.keys(inspectionResults).length === 0}
>
Completar inspección
</Button>
</div>
</Modal>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-[var(--text-secondary)]">Tasa de aprobación</p>
<p className="text-2xl font-bold text-[var(--color-success)]">94.2%</p>
</div>
<span className="text-2xl"></span>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-[var(--text-secondary)]">Controles pendientes</p>
<p className="text-2xl font-bold text-[var(--color-primary)]">7</p>
</div>
<span className="text-2xl"></span>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-[var(--text-secondary)]">Fallas críticas</p>
<p className="text-2xl font-bold text-[var(--color-error)]">2</p>
</div>
<span className="text-2xl">🚨</span>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-[var(--text-secondary)]">Controles hoy</p>
<p className="text-2xl font-bold text-[var(--color-info)]">23</p>
</div>
<span className="text-2xl">📋</span>
</div>
</Card>
</div>
</div>
);
};
export default QualityControl;