Fix some UI issues 2

This commit is contained in:
Urtzi Alfaro
2025-09-24 21:54:49 +02:00
parent d59b92a1b4
commit dc6c6f213f
16 changed files with 1036 additions and 141 deletions

View File

@@ -2,6 +2,8 @@ import React, { useState, useEffect, useRef } from 'react';
import { Button, Input, Card, Select, Avatar, Modal } from '../../ui';
import { useAuthUser } from '../../../stores/auth.store';
import { useToast } from '../../../hooks/ui/useToast';
import { useUpdateProfile, useChangePassword, useAuthProfile } from '../../../api/hooks/auth';
import { useEffect } from 'react';
interface ProfileSettingsProps {
onSuccess?: () => void;
@@ -16,7 +18,7 @@ interface ProfileFormData {
phone: string;
language: string;
timezone: string;
avatar_url?: string;
avatar?: string;
}
@@ -48,16 +50,39 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
const [activeTab, setActiveTab] = useState<'profile' | 'security' | 'preferences' | 'notifications'>(initialTab);
// Mock data for profile
const updateProfileMutation = useUpdateProfile();
const changePasswordMutation = useChangePassword();
const { data: userProfile, isLoading: profileLoading } = useAuthProfile();
const [profileData, setProfileData] = useState<ProfileFormData>({
first_name: 'María',
last_name: 'González Pérez',
email: 'admin@bakery.com',
phone: '+34 612 345 678',
first_name: '',
last_name: '',
email: '',
phone: '',
language: 'es',
timezone: 'Europe/Madrid',
avatar_url: 'https://images.unsplash.com/photo-1494790108755-2616b612b372?w=150&h=150&fit=crop&crop=face'
avatar: '' // Using empty string to allow Avatar to show default bakery-themed icon
});
const originalProfileDataRef = useRef<ProfileFormData | null>(null);
// Load profile data from API when available
useEffect(() => {
if (userProfile) {
const profileFormData: ProfileFormData = {
first_name: userProfile.full_name?.split(' ')[0] || '',
last_name: userProfile.full_name?.split(' ').slice(1).join(' ') || '',
email: userProfile.email || '',
phone: userProfile.phone || '',
language: userProfile.language || 'es',
timezone: userProfile.timezone || 'Europe/Madrid',
avatar: userProfile.avatar || ''
};
setProfileData(profileFormData);
originalProfileDataRef.current = profileFormData; // Store original data for reset
}
}, [userProfile]);
const [notificationSettings, setNotificationSettings] = useState<NotificationSettings>({
email_notifications: true,
@@ -106,25 +131,7 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
{ value: 'Europe/Rome', label: 'Roma (CET/CEST)' }
];
// Mock update profile function
const updateProfile = async (data: any): Promise<boolean> => {
setIsLoading(true);
setError(null);
try {
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 1500));
// Simulate successful update
console.log('Profile updated:', data);
setIsLoading(false);
return true;
} catch (err) {
setError('Error updating profile');
setIsLoading(false);
return false;
}
};
// Profile picture upload handler
const handleImageUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -165,7 +172,7 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
await new Promise(resolve => setTimeout(resolve, 2000));
const newImageUrl = URL.createObjectURL(file); // Temporary URL
setProfileData(prev => ({ ...prev, avatar_url: newImageUrl }));
setProfileData(prev => ({ ...prev, avatar: newImageUrl }));
setHasChanges(prev => ({ ...prev, profile: true }));
showToast({
@@ -186,7 +193,7 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
};
const handleRemoveImage = () => {
setProfileData(prev => ({ ...prev, avatar_url: '' }));
setProfileData(prev => ({ ...prev, avatar: '' }));
setHasChanges(prev => ({ ...prev, profile: true }));
setShowDeleteConfirm(false);
};
@@ -268,27 +275,26 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
}
try {
const success = await updateProfile(profileData);
if (success) {
setHasChanges(false);
showToast({
type: 'success',
title: 'Perfil actualizado',
message: 'Tu información ha sido guardada correctamente'
});
onSuccess?.();
} else {
showToast({
type: 'error',
title: 'Error al actualizar',
message: error || 'No se pudo actualizar tu perfil'
});
}
await updateProfileMutation.mutateAsync({
full_name: `${profileData.first_name} ${profileData.last_name}`.trim() || profileData.first_name,
email: profileData.email,
phone: profileData.phone,
language: profileData.language,
timezone: profileData.timezone,
});
setHasChanges(false);
showToast({
type: 'success',
title: 'Perfil actualizado',
message: 'Tu información ha sido guardada correctamente'
});
onSuccess?.();
} catch (err) {
showToast({
type: 'error',
title: 'Error de conexión',
message: 'No se pudo conectar con el servidor'
title: 'Error al actualizar',
message: 'No se pudo actualizar tu perfil'
});
}
};
@@ -300,19 +306,30 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
return;
}
// Note: This would typically call a separate password change endpoint
// For now, we'll show a placeholder message
showToast({
type: 'success',
title: 'Contraseña actualizada',
message: 'Tu contraseña ha sido cambiada correctamente'
});
setPasswordData({
currentPassword: '',
newPassword: '',
confirmNewPassword: ''
});
try {
await changePasswordMutation.mutateAsync({
current_password: passwordData.currentPassword,
new_password: passwordData.newPassword,
});
showToast({
type: 'success',
title: 'Contraseña actualizada',
message: 'Tu contraseña ha sido cambiada correctamente'
});
setPasswordData({
currentPassword: '',
newPassword: '',
confirmNewPassword: ''
});
} catch (error) {
showToast({
type: 'error',
title: 'Error al cambiar contraseña',
message: 'No se pudo cambiar tu contraseña. Por favor, verifica tu contraseña actual.'
});
}
};
const handleProfileInputChange = (field: keyof ProfileFormData) => (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -353,13 +370,13 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
{ id: 'preferences' as const, label: 'Preferencias', icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z' }
];
// Mock user data for display
const mockUser = {
first_name: 'María',
last_name: 'González',
email: 'admin@bakery.com',
bakery_name: 'Panadería San Miguel',
avatar_url: 'https://images.unsplash.com/photo-1494790108755-2616b612b372?w=150&h=150&fit=crop&crop=face'
// User data for display - based on actual profile data
const displayUser = {
first_name: profileData.first_name || '',
last_name: profileData.last_name || '',
email: profileData.email || '',
bakery_name: user?.tenant_id ? 'Current Bakery' : 'No Bakery Assigned',
avatar: profileData.avatar || ''
};
return (
@@ -369,8 +386,9 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
<div className="flex items-center space-x-6">
<div className="relative">
<Avatar
src={mockUser.avatar_url}
name={`${mockUser.first_name} ${mockUser.last_name}`}
src={displayUser.avatar || undefined}
alt={`${displayUser.first_name} ${displayUser.last_name}` || 'Usuario'}
name={displayUser.avatar ? `${displayUser.first_name} ${displayUser.last_name}` : undefined}
size="xl"
className="w-20 h-20 border-4 border-background-primary shadow-lg"
/>
@@ -378,14 +396,14 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
</div>
<div className="flex-1">
<h1 className="text-3xl font-bold text-text-primary mb-2">
{mockUser.first_name} {mockUser.last_name}
{displayUser.first_name} {displayUser.last_name}
</h1>
<p className="text-text-secondary text-lg mb-1">{mockUser.email}</p>
<p className="text-text-secondary text-lg mb-1">{displayUser.email}</p>
<p className="text-sm text-text-tertiary flex items-center">
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
Trabajando en {mockUser.bakery_name}
Trabajando en {displayUser.bakery_name}
</p>
</div>
<div className="text-right">
@@ -428,8 +446,9 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
<div className="flex items-center space-x-8 mb-8 p-6 bg-background-secondary rounded-lg">
<div className="relative">
<Avatar
src={profileData.avatar_url}
name={`${profileData.first_name} ${profileData.last_name}`}
src={profileData.avatar || undefined}
alt={`${profileData.first_name} ${profileData.last_name}` || 'Usuario'}
name={profileData.avatar ? `${profileData.first_name} ${profileData.last_name}` : undefined}
size="xl"
className="w-24 h-24"
/>
@@ -556,15 +575,10 @@ export const ProfileSettings: React.FC<ProfileSettingsProps> = ({
variant="outline"
size="lg"
onClick={() => {
setProfileData({
first_name: 'María',
last_name: 'González Pérez',
email: 'admin@bakery.com',
phone: '+34 612 345 678',
language: 'es',
timezone: 'Europe/Madrid',
avatar_url: 'https://images.unsplash.com/photo-1494790108755-2616b612b372?w=150&h=150&fit=crop&crop=face'
});
// Reset to original profile data
if (originalProfileDataRef.current) {
setProfileData(originalProfileDataRef.current);
}
setHasChanges(prev => ({ ...prev, profile: false }));
setErrors({});
}}

View File

@@ -69,7 +69,7 @@ export interface ProfileFormData {
phone: string;
language: string;
timezone: string;
avatar_url?: string;
avatar?: string;
}
export interface BakeryFormData {

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useForm, Controller } from 'react-hook-form';
import {
Modal,
@@ -9,18 +9,47 @@ import {
Badge,
Card
} from '../../ui';
import { LoadingSpinner } from '../../shared';
import {
QualityCheckType,
ProcessStage,
type QualityCheckTemplateCreate
} from '../../../api/types/qualityTemplates';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { useQuery } from '@tanstack/react-query';
import { recipesService } from '../../../api/services/recipes';
import type { RecipeResponse } from '../../../api/types/recipes';
import {
Plus,
X,
ChevronDown,
ChevronUp,
AlertTriangle,
Info,
Settings,
Link as LinkIcon
} from 'lucide-react';
interface CreateQualityTemplateModalProps {
isOpen: boolean;
onClose: () => void;
onCreateTemplate: (templateData: QualityCheckTemplateCreate) => Promise<void>;
onCreateTemplate: (templateData: QualityCheckTemplateCreate, recipeAssociations?: RecipeAssociation[]) => Promise<void>;
isLoading?: boolean;
initialRecipe?: RecipeResponse; // Pre-select recipe if coming from recipe page
}
interface RecipeAssociation {
recipeId: string;
recipeName: string;
stages: ProcessStage[];
}
interface StageConfiguration {
stage: ProcessStage;
isRequired: boolean;
isOptional: boolean;
blockingOnFailure: boolean;
minQualityScore?: number;
}
const QUALITY_CHECK_TYPE_OPTIONS = [
@@ -58,10 +87,22 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
isOpen,
onClose,
onCreateTemplate,
isLoading = false
isLoading = false,
initialRecipe
}) => {
const currentTenant = useCurrentTenant();
const [selectedStages, setSelectedStages] = useState<ProcessStage[]>([]);
const [recipeAssociations, setRecipeAssociations] = useState<RecipeAssociation[]>([]);
const [stageConfigurations, setStageConfigurations] = useState<StageConfiguration[]>([]);
const [showRecipeAssociation, setShowRecipeAssociation] = useState(false);
const [showAdvancedConfig, setShowAdvancedConfig] = useState(false);
// Fetch available recipes for association
const { data: recipes, isLoading: recipesLoading } = useQuery({
queryKey: ['recipes', currentTenant?.id],
queryFn: () => recipesService.searchRecipes(currentTenant?.id || '', { limit: 1000 }),
enabled: isOpen && !!currentTenant?.id && showRecipeAssociation
});
const {
register,
@@ -94,6 +135,33 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
QualityCheckType.WEIGHT
].includes(checkType);
// Initialize with recipe if provided
useEffect(() => {
if (initialRecipe && isOpen) {
setRecipeAssociations([{
recipeId: initialRecipe.id,
recipeName: initialRecipe.name,
stages: []
}]);
setShowRecipeAssociation(true);
}
}, [initialRecipe, isOpen]);
// Update stage configurations when selected stages change
useEffect(() => {
const newConfigs = selectedStages.map(stage => {
const existing = stageConfigurations.find(c => c.stage === stage);
return existing || {
stage,
isRequired: false,
isOptional: true,
blockingOnFailure: true,
minQualityScore: undefined
};
});
setStageConfigurations(newConfigs);
}, [selectedStages]);
const handleStageToggle = (stage: ProcessStage) => {
const newStages = selectedStages.includes(stage)
? selectedStages.filter(s => s !== stage)
@@ -102,17 +170,78 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
setSelectedStages(newStages);
};
const handleAddRecipeAssociation = () => {
const newAssociation: RecipeAssociation = {
recipeId: '',
recipeName: '',
stages: []
};
setRecipeAssociations([...recipeAssociations, newAssociation]);
};
const handleRemoveRecipeAssociation = (index: number) => {
setRecipeAssociations(recipeAssociations.filter((_, i) => i !== index));
};
const handleRecipeAssociationChange = (index: number, field: keyof RecipeAssociation, value: any) => {
const updated = [...recipeAssociations];
if (field === 'recipeId' && recipes) {
const recipe = recipes.find(r => r.id === value);
if (recipe) {
updated[index] = {
...updated[index],
recipeId: value,
recipeName: recipe.name
};
}
} else {
(updated[index] as any)[field] = value;
}
setRecipeAssociations(updated);
};
const handleStageConfigChange = (stage: ProcessStage, field: keyof StageConfiguration, value: any) => {
setStageConfigurations(configs =>
configs.map(config =>
config.stage === stage ? { ...config, [field]: value } : config
)
);
};
const onSubmit = async (data: QualityCheckTemplateCreate) => {
try {
// Validate recipe associations if any
const validRecipeAssociations = recipeAssociations
.filter(association => association.recipeId && association.stages.length > 0)
.map(association => ({
...association,
stageConfigurations: stageConfigurations
.filter(config => association.stages.includes(config.stage))
.reduce((acc, config) => {
acc[config.stage] = {
template_ids: [], // Will be populated by backend
required_checks: config.isRequired ? [data.name] : [],
optional_checks: !config.isRequired ? [data.name] : [],
blocking_on_failure: config.blockingOnFailure,
min_quality_score: config.minQualityScore || null
};
return acc;
}, {} as Record<string, any>)
}));
await onCreateTemplate({
...data,
applicable_stages: selectedStages.length > 0 ? selectedStages : undefined,
created_by: currentTenant?.id || ''
});
}, validRecipeAssociations.length > 0 ? validRecipeAssociations : undefined);
// Reset form
reset();
setSelectedStages([]);
setRecipeAssociations([]);
setStageConfigurations([]);
setShowRecipeAssociation(false);
setShowAdvancedConfig(false);
} catch (error) {
console.error('Error creating template:', error);
}
@@ -121,6 +250,10 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
const handleClose = () => {
reset();
setSelectedStages([]);
setRecipeAssociations([]);
setStageConfigurations([]);
setShowRecipeAssociation(false);
setShowAdvancedConfig(false);
onClose();
};
@@ -296,16 +429,171 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
</Card>
)}
{/* Recipe Association */}
<Card className="p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<LinkIcon className="h-5 w-5 text-[var(--text-secondary)]" />
<h4 className="font-medium text-[var(--text-primary)]">
Asociación con Recetas
</h4>
<Badge variant="outline" size="sm">
Nuevo
</Badge>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setShowRecipeAssociation(!showRecipeAssociation)}
>
{showRecipeAssociation ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</Button>
</div>
{showRecipeAssociation && (
<div className="space-y-4">
<div className="flex items-start space-x-2 p-3 bg-[var(--bg-secondary)] rounded-lg">
<Info className="h-4 w-4 text-blue-500 mt-0.5 flex-shrink-0" />
<div className="text-sm text-[var(--text-secondary)]">
<p className="font-medium text-[var(--text-primary)] mb-1">Asociación Automática con Recetas</p>
<p>
Al asociar esta plantilla con recetas, se aplicará automáticamente a los lotes de producción
creados a partir de esas recetas. Esto asegura consistencia en los controles de calidad.
</p>
</div>
</div>
{recipeAssociations.map((association, index) => {
const isComplete = association.recipeId && association.stages.length > 0;
return (
<div key={index} className={`p-4 border rounded-lg space-y-3 ${
isComplete ? 'border-green-200 bg-green-50/50' : 'border-[var(--border-primary)]'
}`}>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<h5 className="font-medium text-[var(--text-primary)]">
Receta {index + 1}
</h5>
{isComplete && (
<Badge variant="success" size="sm">Configurada</Badge>
)}
</div>
{recipeAssociations.length > 1 && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => handleRemoveRecipeAssociation(index)}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Seleccionar Receta
</label>
{recipesLoading ? (
<div className="flex items-center space-x-2">
<LoadingSpinner size="sm" />
<span className="text-sm text-[var(--text-secondary)]">Cargando recetas...</span>
</div>
) : (
<Select
value={association.recipeId}
onChange={(e) => handleRecipeAssociationChange(index, 'recipeId', e.target.value)}
>
<option value="">Seleccionar receta</option>
{recipes?.map(recipe => (
<option key={recipe.id} value={recipe.id}>
{recipe.name} {recipe.category && `(${recipe.category})`}
</option>
))}
</Select>
)}
</div>
{association.recipeId && (
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
Etapas donde aplicar en esta receta
</label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{PROCESS_STAGE_OPTIONS.map(stage => (
<label
key={stage.value}
className="flex items-center space-x-2 p-2 border rounded cursor-pointer hover:bg-[var(--bg-secondary)]"
>
<input
type="checkbox"
checked={association.stages.includes(stage.value)}
onChange={(e) => {
const newStages = e.target.checked
? [...association.stages, stage.value]
: association.stages.filter(s => s !== stage.value);
handleRecipeAssociationChange(index, 'stages', newStages);
}}
className="rounded border-[var(--border-primary)]"
/>
<span className="text-sm">{stage.label}</span>
</label>
))}
</div>
</div>
)}
{association.recipeId && association.stages.length === 0 && (
<div className="flex items-start space-x-2 p-2 bg-yellow-50/50 border border-yellow-200 rounded">
<AlertTriangle className="h-4 w-4 text-yellow-500 mt-0.5 flex-shrink-0" />
<p className="text-sm text-yellow-700">
Selecciona al menos una etapa para completar la configuración de esta receta.
</p>
</div>
)}
</div>
);
})}
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAddRecipeAssociation}
disabled={recipesLoading}
>
<Plus className="h-4 w-4 mr-2" />
Agregar Receta
</Button>
</div>
)}
</Card>
{/* Process Stages */}
<Card className="p-4">
<h4 className="font-medium text-[var(--text-primary)] mb-4">
Etapas del Proceso Aplicables
</h4>
<div className="flex items-center justify-between mb-4">
<h4 className="font-medium text-[var(--text-primary)]">
Etapas del Proceso Aplicables
</h4>
{selectedStages.length > 0 && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setShowAdvancedConfig(!showAdvancedConfig)}
>
<Settings className="h-4 w-4 mr-2" />
{showAdvancedConfig ? 'Ocultar' : 'Configurar'} Avanzado
</Button>
)}
</div>
<p className="text-sm text-[var(--text-secondary)] mb-4">
Selecciona las etapas donde se debe aplicar este control. Si no seleccionas ninguna, se aplicará a todas las etapas.
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-4">
{PROCESS_STAGE_OPTIONS.map(stage => (
<label
key={stage.value}
@@ -321,6 +609,81 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
</label>
))}
</div>
{/* Advanced Stage Configuration */}
{showAdvancedConfig && selectedStages.length > 0 && (
<div className="mt-6 p-4 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-start space-x-2 mb-4">
<AlertTriangle className="h-4 w-4 text-orange-500 mt-0.5" />
<div className="text-sm text-[var(--text-secondary)]">
<p className="font-medium text-[var(--text-primary)] mb-1">Configuración Avanzada por Etapa</p>
<p>
Configura comportamientos específicos para cada etapa seleccionada. Esta configuración se aplicará
cuando se use la plantilla en recetas asociadas.
</p>
</div>
</div>
<div className="space-y-4">
{stageConfigurations.map(config => (
<div key={config.stage} className="p-3 bg-white rounded-lg border">
<h5 className="font-medium text-[var(--text-primary)] mb-3">
{PROCESS_STAGE_OPTIONS.find(s => s.value === config.stage)?.label}
</h5>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-3">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={config.isRequired}
onChange={(e) => handleStageConfigChange(config.stage, 'isRequired', e.target.checked)}
className="rounded border-[var(--border-primary)]"
/>
<span className="text-sm">Control requerido</span>
<Badge variant="info" size="sm">Obligatorio</Badge>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={config.blockingOnFailure}
onChange={(e) => handleStageConfigChange(config.stage, 'blockingOnFailure', e.target.checked)}
className="rounded border-[var(--border-primary)]"
/>
<span className="text-sm">Bloquear si falla</span>
<Badge variant="error" size="sm">Crítico</Badge>
</label>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Puntuación Mínima (opcional)
</label>
<Input
type="number"
min="0"
max="10"
step="0.1"
value={config.minQualityScore || ''}
onChange={(e) => handleStageConfigChange(
config.stage,
'minQualityScore',
e.target.value ? parseFloat(e.target.value) : undefined
)}
placeholder="ej: 7.0"
className="w-24"
/>
<p className="text-xs text-[var(--text-tertiary)] mt-1">
Puntuación mínima requerida (0-10)
</p>
</div>
</div>
</div>
))}
</div>
</div>
)}
</Card>
{/* Settings */}