Add improved production UI 2

This commit is contained in:
Urtzi Alfaro
2025-09-23 15:57:22 +02:00
parent 4ae8e14e55
commit 7f871fc933
5 changed files with 1169 additions and 167 deletions

View File

@@ -0,0 +1,345 @@
import React from 'react';
import { Clock, Timer, CheckCircle, AlertCircle, Package, Play, Pause, X, Edit, Eye, History, Settings } from 'lucide-react';
import { StatusCard, StatusIndicatorConfig } from '../../ui/StatusCard/StatusCard';
import { statusColors } from '../../../styles/colors';
import { ProductionBatchResponse, ProductionStatus, ProductionPriority } from '../../../api/types/production';
import { useProductionEnums } from '../../../utils/enumHelpers';
export interface ProductionStatusCardProps {
batch: ProductionBatchResponse;
onView?: (batch: ProductionBatchResponse) => void;
onEdit?: (batch: ProductionBatchResponse) => void;
onStart?: (batch: ProductionBatchResponse) => void;
onPause?: (batch: ProductionBatchResponse) => void;
onComplete?: (batch: ProductionBatchResponse) => void;
onCancel?: (batch: ProductionBatchResponse) => void;
onQualityCheck?: (batch: ProductionBatchResponse) => void;
onViewHistory?: (batch: ProductionBatchResponse) => void;
showDetailedProgress?: boolean;
compact?: boolean;
}
export const ProductionStatusCard: React.FC<ProductionStatusCardProps> = ({
batch,
onView,
onEdit,
onStart,
onPause,
onComplete,
onCancel,
onQualityCheck,
onViewHistory,
showDetailedProgress = true,
compact = false
}) => {
const productionEnums = useProductionEnums();
const getProductionStatusConfig = (status: ProductionStatus, priority: ProductionPriority): StatusIndicatorConfig => {
const statusConfig = {
[ProductionStatus.PENDING]: { icon: Clock },
[ProductionStatus.IN_PROGRESS]: { icon: Timer },
[ProductionStatus.COMPLETED]: { icon: CheckCircle },
[ProductionStatus.CANCELLED]: { icon: X },
[ProductionStatus.ON_HOLD]: { icon: Pause },
[ProductionStatus.QUALITY_CHECK]: { icon: Package },
[ProductionStatus.FAILED]: { icon: AlertCircle },
};
const config = statusConfig[status] || { icon: AlertCircle };
const Icon = config.icon;
const isUrgent = priority === ProductionPriority.URGENT;
const isCritical = status === ProductionStatus.FAILED ||
status === ProductionStatus.CANCELLED ||
(status === ProductionStatus.PENDING && isUrgent);
const isHighlight = isUrgent && status !== ProductionStatus.FAILED && status !== ProductionStatus.CANCELLED;
const getStatusColorForProduction = (status: ProductionStatus) => {
const normalizedStatus = status.toLowerCase();
switch (normalizedStatus) {
case 'pending':
return statusColors.pending.primary;
case 'in_progress':
return statusColors.inProgress.primary;
case 'completed':
return statusColors.completed.primary;
case 'cancelled':
case 'failed':
return statusColors.cancelled.primary;
case 'on_hold':
return statusColors.pending.primary;
case 'quality_check':
return statusColors.inProgress.primary;
default:
return statusColors.other.primary;
}
};
return {
color: getStatusColorForProduction(status),
text: productionEnums.getProductionStatusLabel(status),
icon: Icon,
isCritical,
isHighlight
};
};
const calculateDetailedProgress = (batch: ProductionBatchResponse): number => {
if (batch.status === ProductionStatus.COMPLETED) return 100;
if (batch.status === ProductionStatus.PENDING) return 0;
if (batch.status === ProductionStatus.CANCELLED || batch.status === ProductionStatus.FAILED) return 0;
if (batch.actual_start_time && batch.planned_end_time) {
const now = new Date();
const startTime = new Date(batch.actual_start_time);
const endTime = new Date(batch.planned_end_time);
const totalDuration = endTime.getTime() - startTime.getTime();
const elapsed = now.getTime() - startTime.getTime();
if (totalDuration > 0) {
return Math.min(90, Math.max(10, Math.round((elapsed / totalDuration) * 100)));
}
}
if (batch.status === ProductionStatus.QUALITY_CHECK) return 85;
if (batch.status === ProductionStatus.ON_HOLD) return 50;
return 50;
};
const getProgressLabel = (batch: ProductionBatchResponse): string => {
switch (batch.status) {
case ProductionStatus.PENDING:
return 'Programado';
case ProductionStatus.IN_PROGRESS:
return 'En producción';
case ProductionStatus.QUALITY_CHECK:
return 'Control de calidad';
case ProductionStatus.ON_HOLD:
return 'En pausa';
case ProductionStatus.COMPLETED:
return 'Completado';
case ProductionStatus.CANCELLED:
return 'Cancelado';
case ProductionStatus.FAILED:
return 'Fallido';
default:
return 'Estado desconocido';
}
};
const getQualityStatusInfo = (batch: ProductionBatchResponse): string | undefined => {
if (batch.status === ProductionStatus.QUALITY_CHECK) {
return 'Control de calidad pendiente';
}
if (batch.quality_score !== undefined && batch.quality_score !== null) {
if (batch.quality_score >= 8) {
return `Calidad excelente (${batch.quality_score}/10)`;
} else if (batch.quality_score >= 6) {
return `Calidad aceptable (${batch.quality_score}/10)`;
} else {
return `Calidad baja (${batch.quality_score}/10)`;
}
}
if (batch.status === ProductionStatus.COMPLETED) {
return 'Control de calidad final pendiente';
}
return undefined;
};
const getProductionActions = () => {
const actions = [];
// Debug logging to see what status we're getting
console.log('ProductionStatusCard - Batch:', batch.batch_number, 'Status:', batch.status, 'Type:', typeof batch.status);
if (onView) {
actions.push({
label: 'Ver Detalles',
icon: Eye,
priority: 'primary' as const,
onClick: () => onView(batch)
});
}
if (batch.status === ProductionStatus.PENDING && onStart) {
actions.push({
label: 'Iniciar Producción',
icon: Play,
priority: 'primary' as const,
onClick: () => onStart(batch)
});
}
if (batch.status === ProductionStatus.IN_PROGRESS) {
if (onPause) {
actions.push({
label: 'Pausar',
icon: Pause,
priority: 'secondary' as const,
onClick: () => onPause(batch)
});
}
if (onComplete) {
actions.push({
label: 'Completar',
icon: CheckCircle,
priority: 'primary' as const,
onClick: () => onComplete(batch)
});
}
}
if (batch.status === ProductionStatus.ON_HOLD && onStart) {
actions.push({
label: 'Reanudar',
icon: Play,
priority: 'primary' as const,
onClick: () => onStart(batch)
});
}
if (batch.status === ProductionStatus.QUALITY_CHECK && onQualityCheck) {
console.log('ProductionStatusCard - Adding quality check button for batch:', batch.batch_number);
actions.push({
label: 'Control Calidad',
icon: Package,
priority: 'primary' as const,
onClick: () => onQualityCheck(batch)
});
} else {
console.log('ProductionStatusCard - Quality check condition not met:', {
batchNumber: batch.batch_number,
status: batch.status,
expectedStatus: ProductionStatus.QUALITY_CHECK,
hasOnQualityCheck: !!onQualityCheck,
statusMatch: batch.status === ProductionStatus.QUALITY_CHECK
});
}
if (onEdit && (batch.status === ProductionStatus.PENDING || batch.status === ProductionStatus.ON_HOLD)) {
actions.push({
label: 'Editar',
icon: Edit,
priority: 'secondary' as const,
onClick: () => onEdit(batch)
});
}
if (onCancel && batch.status !== ProductionStatus.COMPLETED && batch.status !== ProductionStatus.CANCELLED) {
actions.push({
label: 'Cancelar',
icon: X,
priority: 'tertiary' as const,
destructive: true,
onClick: () => onCancel(batch)
});
}
if (onViewHistory) {
actions.push({
label: 'Historial',
icon: History,
priority: 'tertiary' as const,
onClick: () => onViewHistory(batch)
});
}
// Debug logging to see final actions array
console.log('ProductionStatusCard - Final actions array for batch', batch.batch_number, ':', actions);
return actions;
};
const getMetadata = (): string[] => {
const metadata = [];
if (batch.planned_start_time) {
const startTime = new Date(batch.planned_start_time);
metadata.push(`Inicio: ${startTime.toLocaleDateString()} ${startTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`);
}
if (batch.planned_end_time) {
const endTime = new Date(batch.planned_end_time);
metadata.push(`Fin: ${endTime.toLocaleDateString()} ${endTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`);
}
if (batch.staff_assigned && batch.staff_assigned.length > 0) {
metadata.push(`Personal: ${batch.staff_assigned.join(', ')}`);
}
if (batch.equipment_used && batch.equipment_used.length > 0) {
metadata.push(`Equipos: ${batch.equipment_used.join(', ')}`);
}
const qualityInfo = getQualityStatusInfo(batch);
if (qualityInfo) {
metadata.push(qualityInfo);
}
if (batch.priority === ProductionPriority.URGENT) {
metadata.push('⚡ Orden urgente');
}
if (batch.is_rush_order) {
metadata.push('🏃 Orden express');
}
if (batch.is_special_recipe) {
metadata.push('🌟 Receta especial');
}
return metadata;
};
const getSecondaryInfo = () => {
if (batch.actual_quantity && batch.planned_quantity) {
const completionPercent = Math.round((batch.actual_quantity / batch.planned_quantity) * 100);
return {
label: 'Progreso Cantidad',
value: `${batch.actual_quantity}/${batch.planned_quantity} (${completionPercent}%)`
};
}
if (batch.staff_assigned && batch.staff_assigned.length > 0) {
return {
label: 'Personal Asignado',
value: batch.staff_assigned.join(', ')
};
}
return {
label: 'Prioridad',
value: productionEnums.getProductionPriorityLabel(batch.priority)
};
};
const statusConfig = getProductionStatusConfig(batch.status, batch.priority);
const progress = showDetailedProgress ? calculateDetailedProgress(batch) : undefined;
return (
<StatusCard
id={batch.id}
statusIndicator={statusConfig}
title={batch.product_name}
subtitle={`Lote: ${batch.batch_number}`}
primaryValue={batch.planned_quantity}
primaryValueLabel="unidades"
secondaryInfo={getSecondaryInfo()}
progress={progress !== undefined ? {
label: getProgressLabel(batch),
percentage: progress,
color: statusConfig.color
} : undefined}
metadata={getMetadata()}
actions={getProductionActions()}
onClick={onView ? () => onView(batch) : undefined}
compact={compact}
/>
);
};
export default ProductionStatusCard;

View File

@@ -0,0 +1,677 @@
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<QualityCheckModalProps> = ({
isOpen,
onClose,
batch,
onComplete
}) => {
const currentTenant = useCurrentTenant();
const [activeTab, setActiveTab] = useState('inspection');
const [selectedTemplate, setSelectedTemplate] = useState<string>('visual_inspection');
const [currentCriteriaIndex, setCurrentCriteriaIndex] = useState(0);
const [results, setResults] = useState<Record<string, QualityCheckResult>>({});
const [finalNotes, setFinalNotes] = useState('');
const [photos, setPhotos] = useState<File[]>([]);
const [loading, setLoading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(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<QualityCheckResult>) => {
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<HTMLInputElement>) => {
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 (
<div className="space-y-4">
<div className="flex gap-4">
<label className="flex items-center">
<input
type="radio"
name={criterion.id}
checked={result?.value === true}
onChange={() => updateResult(criterion.id, { value: true, score: 10, pass: true })}
className="mr-2"
/>
/ Pasa
</label>
<label className="flex items-center">
<input
type="radio"
name={criterion.id}
checked={result?.value === false}
onChange={() => updateResult(criterion.id, { value: false, score: 0, pass: false })}
className="mr-2"
/>
No / Falla
</label>
</div>
</div>
);
}
if (criterion.type === 'measurement' || criterion.type === 'weight' || criterion.type === 'temperature') {
return (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Input
type="number"
placeholder={`Ingrese ${criterion.spanishName.toLowerCase()}`}
value={result?.value as string || ''}
onChange={(e) => {
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 && <span className="text-sm text-[var(--text-secondary)]">{criterion.unit}</span>}
</div>
</div>
);
}
// Visual type - 1-10 star rating
return (
<div className="space-y-4">
<div className="grid grid-cols-5 gap-2">
{[...Array(10)].map((_, i) => {
const score = i + 1;
const isSelected = result?.score === score;
return (
<button
key={score}
className={`p-3 rounded-lg border-2 transition-all ${
isSelected
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/10'
: 'border-[var(--border-primary)] hover:border-[var(--color-primary)]/50'
}`}
onClick={() => {
updateResult(criterion.id, {
value: score,
score,
pass: score >= 7
});
}}
>
<div className="text-center">
<div className="font-bold text-[var(--text-primary)]">{score}</div>
<Star className={`w-4 h-4 mx-auto ${isSelected ? 'text-[var(--color-primary)] fill-current' : 'text-[var(--text-tertiary)]'}`} />
</div>
</button>
);
})}
</div>
<div className="text-sm text-[var(--text-secondary)]">
<p>1-3: Malo | 4-6: Regular | 7-8: Bueno | 9-10: Excelente</p>
</div>
</div>
);
};
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 (
<Modal
isOpen={isOpen}
onClose={onClose}
title={`Control de Calidad - ${batch.product_name}`}
subtitle={`Lote: ${batch.batch_number}`}
size="xl"
>
<div className="space-y-6">
{/* Template Selection */}
<div className="grid grid-cols-3 gap-3">
{Object.entries(QUALITY_CHECK_TEMPLATES).map(([key, tmpl]) => {
const Icon = tmpl.icon;
return (
<button
key={key}
className={`p-3 rounded-lg border-2 transition-all ${
selectedTemplate === key
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/10'
: 'border-[var(--border-primary)] hover:border-[var(--color-primary)]/50'
}`}
onClick={() => {
setSelectedTemplate(key);
setCurrentCriteriaIndex(0);
setResults({});
}}
>
<Icon className="w-6 h-6 mx-auto mb-2 text-[var(--color-primary)]" />
<div className="font-medium text-sm">{tmpl.spanishName}</div>
</button>
);
})}
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="inspection">Inspección</TabsTrigger>
<TabsTrigger value="photos">Fotos</TabsTrigger>
<TabsTrigger value="summary">Resumen</TabsTrigger>
</TabsList>
<TabsContent value="inspection" className="space-y-6">
{template && (
<>
{/* Progress Indicator */}
<div className="grid grid-cols-6 gap-2">
{template.criteria.map((criterion, index) => {
const result = results[criterion.id];
const isCompleted = !!result;
const isCurrent = index === currentCriteriaIndex;
return (
<button
key={criterion.id}
className={`p-2 rounded text-xs transition-all ${
isCurrent
? 'bg-[var(--color-primary)] text-white'
: isCompleted
? 'bg-green-100 text-green-800'
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)]'
}`}
onClick={() => setCurrentCriteriaIndex(index)}
>
{index + 1}
</button>
);
})}
</div>
{/* Current Criteria */}
{currentCriteria && (
<div className="space-y-6">
<Card className={`p-4 ${currentCriteria.isCritical ? 'border-orange-200 bg-orange-50' : ''}`}>
<div className="flex justify-between items-start mb-3">
<div>
<h4 className="font-semibold text-[var(--text-primary)]">{currentCriteria.spanishName}</h4>
<p className="text-sm text-[var(--text-secondary)] mt-1">{currentCriteria.description}</p>
<p className="text-sm text-[var(--color-info)] mt-1">{currentCriteria.acceptableCriteria}</p>
</div>
<div className="flex gap-2">
{currentCriteria.isCritical && (
<Badge className="bg-[var(--color-error)]/10 text-[var(--color-error)]">
Crítico
</Badge>
)}
{currentCriteria.required && (
<Badge className="bg-[var(--color-warning)]/10 text-[var(--color-warning)]">
Requerido
</Badge>
)}
</div>
</div>
{renderCriteriaInput(currentCriteria)}
<Textarea
placeholder="Notas adicionales (opcional)..."
value={currentResult?.notes || ''}
onChange={(e) => {
updateResult(currentCriteria.id, { notes: e.target.value });
}}
rows={2}
className="mt-4"
/>
</Card>
{/* Navigation */}
<div className="flex justify-between">
<Button
variant="outline"
onClick={() => setCurrentCriteriaIndex(Math.max(0, currentCriteriaIndex - 1))}
disabled={currentCriteriaIndex === 0}
>
Anterior
</Button>
<Button
variant="primary"
onClick={() => setCurrentCriteriaIndex(Math.min(template.criteria.length - 1, currentCriteriaIndex + 1))}
disabled={currentCriteriaIndex === template.criteria.length - 1}
>
Siguiente
</Button>
</div>
</div>
)}
</>
)}
</TabsContent>
<TabsContent value="photos" className="space-y-4">
<div className="text-center py-8">
<Camera className="w-12 h-12 text-[var(--text-tertiary)] mx-auto mb-4" />
<p className="text-[var(--text-secondary)] mb-4">
Añade fotos para documentar problemas de calidad o evidencia
</p>
<Button
variant="outline"
onClick={() => fileInputRef.current?.click()}
>
<Upload className="w-4 h-4 mr-2" />
Subir Fotos
</Button>
<input
ref={fileInputRef}
type="file"
accept="image/*"
multiple
onChange={handlePhotoUpload}
className="hidden"
/>
</div>
{photos.length > 0 && (
<div className="grid grid-cols-3 gap-4">
{photos.map((photo, index) => (
<div key={index} className="relative">
<img
src={URL.createObjectURL(photo)}
alt={`Foto de calidad ${index + 1}`}
className="w-full h-32 object-cover rounded-lg"
/>
<button
className="absolute top-2 right-2 w-6 h-6 bg-red-500 text-white rounded-full flex items-center justify-center"
onClick={() => setPhotos(prev => prev.filter((_, i) => i !== index))}
>
×
</button>
</div>
))}
</div>
)}
</TabsContent>
<TabsContent value="summary" className="space-y-6">
{/* Overall Score */}
<Card className="p-6 text-center">
<div className="text-4xl font-bold text-[var(--text-primary)] mb-2">
{overallScore.toFixed(1)}/10
</div>
<div className="text-lg text-[var(--text-secondary)] mb-4">
Puntuación General de Calidad
</div>
<Badge
variant={overallScore >= 7 ? 'success' : overallScore >= 5 ? 'warning' : 'error'}
className="text-lg px-4 py-2"
>
{overallScore >= 7 ? 'APROBADO' : 'REPROBADO'}
</Badge>
{criticalFailures.length > 0 && (
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-center gap-2 text-red-800 font-medium">
<AlertTriangle className="w-5 h-5" />
Fallas Críticas Detectadas
</div>
<ul className="mt-2 text-red-700 text-sm">
{criticalFailures.map((failure, index) => (
<li key={index}> {failure}</li>
))}
</ul>
</div>
)}
</Card>
{/* Final Notes */}
<Textarea
placeholder="Notas finales y recomendaciones..."
value={finalNotes}
onChange={(e) => setFinalNotes(e.target.value)}
rows={4}
/>
</TabsContent>
</Tabs>
{/* Action Buttons */}
<div className="flex justify-between pt-6 border-t border-[var(--border-primary)]">
<Button variant="outline" onClick={onClose}>
Cancelar
</Button>
<Button
variant="primary"
onClick={handleComplete}
disabled={!isComplete || loading}
>
{loading ? 'Completando...' : 'Completar Inspección'}
</Button>
</div>
</div>
</Modal>
);
};
export default QualityCheckModal;

View File

@@ -1,16 +1,16 @@
// Production Domain Components
export { default as ProductionSchedule } from './ProductionSchedule';
export { default as BatchTracker } from './BatchTracker';
export { default as QualityControl } from './QualityControl';
export { default as QualityDashboard } from './QualityDashboard';
export { default as QualityInspection } from './QualityInspection';
export { default as EquipmentManager } from './EquipmentManager';
export { CreateProductionBatchModal } from './CreateProductionBatchModal';
export { default as ProductionStatusCard } from './ProductionStatusCard';
export { default as QualityCheckModal } from './QualityCheckModal';
// Export component props types
export type { ProductionScheduleProps } from './ProductionSchedule';
export type { BatchTrackerProps } from './BatchTracker';
export type { QualityControlProps } from './QualityControl';
export type { QualityDashboardProps, QualityMetrics } from './QualityDashboard';
export type { QualityInspectionProps, InspectionCriteria, InspectionResult } from './QualityInspection';
export type { EquipmentManagerProps, Equipment } from './EquipmentManager';
export type { EquipmentManagerProps, Equipment } from './EquipmentManager';
export type { ProductionStatusCardProps } from './ProductionStatusCard';
export type { QualityCheckModalProps } from './QualityCheckModal';

View File

@@ -107,6 +107,14 @@ export const StatusCard: React.FC<StatusCardProps> = ({
const primaryActions = sortedActions.filter(action => action.priority === 'primary');
const secondaryActions = sortedActions.filter(action => action.priority !== 'primary');
// Debug logging for actions
if (actions.length > 0) {
console.log('StatusCard - Title:', title, 'Actions received:', actions.length);
console.log('StatusCard - Actions:', actions);
console.log('StatusCard - Primary actions:', primaryActions.length, primaryActions);
console.log('StatusCard - Secondary actions:', secondaryActions.length, secondaryActions);
}
return (
<Card
className={`