From 7cd047681217bd4ff52a7751399fa83e97e0f634 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Thu, 25 Sep 2025 19:52:55 +0200 Subject: [PATCH] Fix modals --- .../domain/recipes/CreateRecipeModal.tsx | 745 ++++++++++++------ .../components/ui/StatusModal/StatusModal.tsx | 327 ++++++-- 2 files changed, 760 insertions(+), 312 deletions(-) diff --git a/frontend/src/components/domain/recipes/CreateRecipeModal.tsx b/frontend/src/components/domain/recipes/CreateRecipeModal.tsx index 004462a2..d20bc644 100644 --- a/frontend/src/components/domain/recipes/CreateRecipeModal.tsx +++ b/frontend/src/components/domain/recipes/CreateRecipeModal.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect, useMemo } from 'react'; -import { ChefHat, Package, Clock, Euro, Star } from 'lucide-react'; -import { StatusModal } from '../../ui/StatusModal/StatusModal'; +import React, { useState, useMemo, useEffect } from 'react'; +import { ChefHat, Package, Clock, Euro, Star, Plus, Trash2, X, Timer, Thermometer, Settings, FileText } from 'lucide-react'; +import { StatusModal, StatusModalField, StatusModalSection } from '../../ui/StatusModal/StatusModal'; import { RecipeCreate, RecipeIngredientCreate, MeasurementUnit } from '../../../api/types/recipes'; import { useIngredients } from '../../../api/hooks/inventory'; import { useCurrentTenant } from '../../../stores/tenant.store'; @@ -15,14 +15,155 @@ interface CreateRecipeModalProps { * CreateRecipeModal - Modal for creating a new recipe * Comprehensive form for adding new recipes */ +// Custom Ingredients Component for StatusModal +const IngredientsComponent: React.FC<{ + value: RecipeIngredientCreate[]; + onChange: (value: RecipeIngredientCreate[]) => void; + availableIngredients: Array<{value: string; label: string}>; + unitOptions: Array<{value: MeasurementUnit; label: string}>; +}> = ({ value, onChange, availableIngredients, unitOptions }) => { + const addIngredient = () => { + const newIngredient: RecipeIngredientCreate = { + ingredient_id: '', + quantity: 1, + unit: MeasurementUnit.GRAMS, + ingredient_order: value.length + 1, + is_optional: false + }; + onChange([...value, newIngredient]); + }; + + const removeIngredient = (index: number) => { + if (value.length > 1) { + onChange(value.filter((_, i) => i !== index)); + } + }; + + const updateIngredient = (index: number, field: keyof RecipeIngredientCreate, newValue: any) => { + const updated = value.map((ingredient, i) => + i === index ? { ...ingredient, [field]: newValue } : ingredient + ); + onChange(updated); + }; + + return ( +
+
+

Lista de Ingredientes

+ +
+ +
+ {value.map((ingredient, index) => ( +
+
+ Ingrediente #{index + 1} + +
+ +
+
+ + +
+ +
+ + updateIngredient(index, 'quantity', parseFloat(e.target.value) || 0)} + className="w-full px-2 py-1.5 border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] text-sm" + min="0" + step="0.1" + /> +
+
+ +
+
+ + +
+ +
+ +
+
+
+ ))} +
+
+ ); +}; + export const CreateRecipeModal: React.FC = ({ isOpen, onClose, onCreateRecipe }) => { - const [formData, setFormData] = useState({ + // Extended form data interface to include UI-specific fields + interface ExtendedFormData extends Omit { + allergen_info_text: string; + dietary_tags_text: string; + nutritional_info_text: string; + allergen_info: Record | null; + dietary_tags: Record | null; + nutritional_info: Record | null; + } + + const [ingredientsList, setIngredientsList] = useState([{ + ingredient_id: '', + quantity: 1, + unit: MeasurementUnit.GRAMS, + ingredient_order: 1, + is_optional: false + }]); + const [isLoading, setIsLoading] = useState(false); + + const [formData, setFormData] = useState({ name: '', recipe_code: '', + version: '1.0', finished_product_id: '', // This should come from a product selector description: '', category: '', @@ -34,12 +175,14 @@ export const CreateRecipeModal: React.FC = ({ cook_time_minutes: 0, total_time_minutes: 0, rest_time_minutes: 0, - estimated_cost_per_unit: 0, target_margin_percentage: 30, - suggested_selling_price: 0, + instructions: null, preparation_notes: '', storage_instructions: '', quality_standards: '', + quality_check_configuration: null, + quality_check_points: null, + common_issues: null, serves_count: 1, is_seasonal: false, season_start_month: undefined, @@ -50,14 +193,15 @@ export const CreateRecipeModal: React.FC = ({ maximum_batch_size: undefined, optimal_production_temperature: undefined, optimal_humidity: undefined, - allergen_info: '', - dietary_tags: '', - nutritional_info: '', + allergen_info_text: '', + dietary_tags_text: '', + nutritional_info_text: '', + allergen_info: null, + dietary_tags: null, + nutritional_info: null, ingredients: [] }); - const [loading, setLoading] = useState(false); - const [mode, setMode] = useState<'view' | 'edit'>('edit'); // Get tenant and fetch inventory data const currentTenant = useCurrentTenant(); @@ -79,16 +223,17 @@ export const CreateRecipeModal: React.FC = ({ [inventoryItems] ); + // Available ingredients for recipe ingredients const availableIngredients = useMemo(() => - inventoryItems.filter(item => item.product_type === 'ingredient') + inventoryItems.filter(item => item.product_type !== 'finished_product') .map(ingredient => ({ value: ingredient.id, - label: `${ingredient.name} (${ingredient.unit_of_measure})`, - unit: ingredient.unit_of_measure + label: `${ingredient.name} (${ingredient.category || 'Sin categoría'})` })), [inventoryItems] ); + // Category options const categoryOptions = [ { value: 'bread', label: 'Pan' }, @@ -140,37 +285,21 @@ export const CreateRecipeModal: React.FC = ({ { value: 12, label: 'Diciembre' } ]; - // Allergen options - const allergenOptions = [ - 'Gluten', 'Lácteos', 'Huevos', 'Frutos secos', 'Soja', 'Sésamo', 'Pescado', 'Mariscos' - ]; - // Dietary tags - const dietaryTagOptions = [ - 'Vegano', 'Vegetariano', 'Sin gluten', 'Sin lácteos', 'Sin frutos secos', 'Sin azúcar', 'Bajo en carbohidratos', 'Keto', 'Orgánico' - ]; + // Auto-calculate total time when time values change + useEffect(() => { + const prepTime = formData.prep_time_minutes || 0; + const cookTime = formData.cook_time_minutes || 0; + const restTime = formData.rest_time_minutes || 0; + const newTotal = prepTime + cookTime + restTime; - const handleFieldChange = (sectionIndex: number, fieldIndex: number, value: string | number | boolean) => { - const sections = getModalSections(); - const field = sections[sectionIndex]?.fields[fieldIndex]; - - if (!field) return; - - setFormData(prev => ({ - ...prev, - [field.key]: value - })); - - // Auto-calculate total time when prep or cook time changes - if (field.key === 'prep_time_minutes' || field.key === 'cook_time_minutes') { - const prepTime = field.key === 'prep_time_minutes' ? Number(value) : formData.prep_time_minutes || 0; - const cookTime = field.key === 'cook_time_minutes' ? Number(value) : formData.cook_time_minutes || 0; + if (newTotal !== formData.total_time_minutes) { setFormData(prev => ({ ...prev, - total_time_minutes: prepTime + cookTime + total_time_minutes: newTotal })); } - }; + }, [formData.prep_time_minutes, formData.cook_time_minutes, formData.rest_time_minutes]); const handleSubmit = async () => { if (!formData.name.trim()) { @@ -178,7 +307,7 @@ export const CreateRecipeModal: React.FC = ({ return; } - if (!formData.category.trim()) { + if (!formData.category?.trim()) { alert('Debe seleccionar una categoría'); return; } @@ -188,6 +317,13 @@ export const CreateRecipeModal: React.FC = ({ return; } + // Validate ingredients + const ingredientError = validateIngredients(); + if (ingredientError) { + alert(ingredientError); + return; + } + // Validate seasonal dates if seasonal is enabled if (formData.is_seasonal) { if (!formData.season_start_month || !formData.season_end_month) { @@ -196,6 +332,12 @@ export const CreateRecipeModal: React.FC = ({ } } + // Validate difficulty level + if (!formData.difficulty_level || formData.difficulty_level < 1 || formData.difficulty_level > 5) { + alert('Debe especificar un nivel de dificultad válido (1-5)'); + return; + } + // Validate batch sizes if (formData.minimum_batch_size && formData.maximum_batch_size) { if (formData.minimum_batch_size > formData.maximum_batch_size) { @@ -205,7 +347,7 @@ export const CreateRecipeModal: React.FC = ({ } try { - setLoading(true); + setIsLoading(true); // Generate recipe code if not provided const recipeCode = formData.recipe_code || @@ -219,15 +361,27 @@ export const CreateRecipeModal: React.FC = ({ const recipeData: RecipeCreate = { ...formData, + version: formData.version || '1.0', recipe_code: recipeCode, total_time_minutes: totalTime, + // Transform string fields to proper objects/dictionaries + instructions: formData.preparation_notes ? { steps: formData.preparation_notes } : null, + allergen_info: formData.allergen_info_text ? { allergens: formData.allergen_info_text.split(',').map((a: string) => a.trim()) } : null, + dietary_tags: formData.dietary_tags_text ? { tags: formData.dietary_tags_text.split(',').map((t: string) => t.trim()) } : null, + nutritional_info: formData.nutritional_info_text ? { info: formData.nutritional_info_text } : null, // Clean up undefined values for optional fields season_start_month: formData.is_seasonal ? formData.season_start_month : undefined, season_end_month: formData.is_seasonal ? formData.season_end_month : undefined, minimum_batch_size: formData.minimum_batch_size || undefined, maximum_batch_size: formData.maximum_batch_size || undefined, optimal_production_temperature: formData.optimal_production_temperature || undefined, - optimal_humidity: formData.optimal_humidity || undefined + optimal_humidity: formData.optimal_humidity || undefined, + // Validate and use the ingredients list from state + ingredients: ingredientsList.filter(ing => ing.ingredient_id.trim() !== '') + .map((ing, index) => ({ + ...ing, + ingredient_order: index + 1 + })) }; if (onCreateRecipe) { @@ -236,10 +390,18 @@ export const CreateRecipeModal: React.FC = ({ onClose(); - // Reset form + // Reset form and ingredients list + setIngredientsList([{ + ingredient_id: '', + quantity: 1, + unit: MeasurementUnit.GRAMS, + ingredient_order: 1, + is_optional: false + }]); setFormData({ name: '', recipe_code: '', + version: '1.0', finished_product_id: '', description: '', category: '', @@ -251,12 +413,14 @@ export const CreateRecipeModal: React.FC = ({ cook_time_minutes: 0, total_time_minutes: 0, rest_time_minutes: 0, - estimated_cost_per_unit: 0, target_margin_percentage: 30, - suggested_selling_price: 0, + instructions: null, preparation_notes: '', storage_instructions: '', quality_standards: '', + quality_check_configuration: null, + quality_check_points: null, + common_issues: null, serves_count: 1, is_seasonal: false, season_start_month: undefined, @@ -267,84 +431,219 @@ export const CreateRecipeModal: React.FC = ({ maximum_batch_size: undefined, optimal_production_temperature: undefined, optimal_humidity: undefined, - allergen_info: '', - dietary_tags: '', - nutritional_info: '', + allergen_info_text: '', + dietary_tags_text: '', + nutritional_info_text: '', + allergen_info: null, + dietary_tags: null, + nutritional_info: null, ingredients: [] }); } catch (error) { console.error('Error creating recipe:', error); alert('Error al crear la receta. Por favor, inténtelo de nuevo.'); } finally { - setLoading(false); + setIsLoading(false); } }; - const getModalSections = () => { - const sections = [ + // Helper functions for ingredients management + const addIngredient = () => { + const newIngredient: RecipeIngredientCreate = { + ingredient_id: '', + quantity: 1, + unit: MeasurementUnit.GRAMS, + ingredient_order: ingredientsList.length + 1, + is_optional: false + }; + setIngredientsList(prev => [...prev, newIngredient]); + }; + + const removeIngredient = (index: number) => { + if (ingredientsList.length > 1) { + setIngredientsList(prev => prev.filter((_, i) => i !== index)); + } + }; + + const updateIngredient = (index: number, field: keyof RecipeIngredientCreate, value: any) => { + setIngredientsList(prev => prev.map((ingredient, i) => + i === index ? { ...ingredient, [field]: value } : ingredient + )); + }; + + const validateIngredients = (): string | null => { + if (ingredientsList.length === 0) { + return 'Debe agregar al menos un ingrediente'; + } + + const emptyIngredients = ingredientsList.filter(ing => !ing.ingredient_id.trim()); + if (emptyIngredients.length > 0) { + return 'Todos los ingredientes deben tener un ingrediente seleccionado'; + } + + const invalidQuantities = ingredientsList.filter(ing => ing.quantity <= 0); + if (invalidQuantities.length > 0) { + return 'Todas las cantidades deben ser mayor que 0'; + } + + return null; + }; + + // Removed the getModalSections function since we're now using a custom modal + + // Field change handler for StatusModal + const handleFieldChange = (sectionIndex: number, fieldIndex: number, value: string | number) => { + const sections = getSections(); + const section = sections[sectionIndex]; + const field = section.fields[fieldIndex]; + + // Special handling for ingredients component + if (field.type === 'component' && field.componentProps?.onChange) { + // This is handled directly by the component + return; + } + + // Update form data based on field mapping + const fieldMapping: Record = { + 'Nombre': 'name', + 'Código de receta': 'recipe_code', + 'Versión': 'version', + 'Descripción': 'description', + 'Categoría': 'category', + 'Producto terminado': 'finished_product_id', + 'Tipo de cocina': 'cuisine_type', + 'Nivel de dificultad': 'difficulty_level', + 'Cantidad de rendimiento': 'yield_quantity', + 'Unidad de rendimiento': 'yield_unit', + 'Tiempo de preparación (min)': 'prep_time_minutes', + 'Tiempo de cocción (min)': 'cook_time_minutes', + 'Tiempo de reposo (min)': 'rest_time_minutes', + 'Porciones': 'serves_count', + 'Margen objetivo (%)': 'target_margin_percentage', + 'Receta estacional': 'is_seasonal', + 'Mes de inicio': 'season_start_month', + 'Mes de fin': 'season_end_month', + 'Receta estrella': 'is_signature_item', + 'Multiplicador de lote': 'batch_size_multiplier', + 'Tamaño mínimo de lote': 'minimum_batch_size', + 'Tamaño máximo de lote': 'maximum_batch_size', + 'Temperatura óptima (°C)': 'optimal_production_temperature', + 'Humedad óptima (%)': 'optimal_humidity', + 'Notas de preparación': 'preparation_notes', + 'Instrucciones de almacenamiento': 'storage_instructions', + 'Estándares de calidad': 'quality_standards', + 'Información de alérgenos': 'allergen_info_text', + 'Etiquetas dietéticas': 'dietary_tags_text', + 'Información nutricional': 'nutritional_info_text', + }; + + const fieldKey = fieldMapping[field.label]; + if (fieldKey) { + if (fieldKey === 'is_seasonal' || fieldKey === 'is_signature_item') { + setFormData(prev => ({ ...prev, [fieldKey]: value === 'Sí' })); + } else if (typeof value === 'string' && ['difficulty_level', 'yield_quantity', 'prep_time_minutes', 'cook_time_minutes', 'rest_time_minutes', 'serves_count', 'target_margin_percentage', 'season_start_month', 'season_end_month', 'batch_size_multiplier', 'minimum_batch_size', 'maximum_batch_size', 'optimal_production_temperature', 'optimal_humidity'].includes(fieldKey)) { + setFormData(prev => ({ ...prev, [fieldKey]: Number(value) || 0 })); + } else { + setFormData(prev => ({ ...prev, [fieldKey]: value })); + } + } + }; + + // Create sections for StatusModal + const getSections = (): StatusModalSection[] => [ { title: 'Información Básica', icon: ChefHat, fields: [ { - key: 'name', - label: 'Nombre de la receta', + label: 'Nombre', value: formData.name, type: 'text', + editable: true, required: true, - placeholder: 'Ej: Pan de molde integral' + placeholder: 'Ej: Pan de molde integral', + validation: (value) => !String(value).trim() ? 'El nombre es obligatorio' : null }, { - key: 'recipe_code', label: 'Código de receta', value: formData.recipe_code, type: 'text', - placeholder: 'Ej: PAN001 (opcional, se genera automáticamente)' + editable: true, + placeholder: 'Ej: PAN001 (opcional)' + }, + { + label: 'Versión', + value: formData.version, + type: 'text', + editable: true, + placeholder: '1.0' }, { - key: 'description', label: 'Descripción', value: formData.description, type: 'textarea', - placeholder: 'Descripción de la receta...' + editable: true, + placeholder: 'Descripción de la receta...', + span: 2 }, { - key: 'category', label: 'Categoría', value: formData.category, type: 'select', + editable: true, + required: true, options: categoryOptions, - required: true + validation: (value) => !String(value).trim() ? 'La categoría es obligatoria' : null + }, + { + label: 'Producto terminado', + value: formData.finished_product_id, + type: 'select', + editable: true, + required: true, + options: finishedProducts, + validation: (value) => !String(value).trim() ? 'Debe seleccionar un producto terminado' : null }, { - key: 'cuisine_type', label: 'Tipo de cocina', value: formData.cuisine_type, type: 'select', - options: cuisineTypeOptions, - placeholder: 'Selecciona el tipo de cocina' + editable: true, + options: cuisineTypeOptions }, { - key: 'difficulty_level', label: 'Nivel de dificultad', value: formData.difficulty_level, type: 'select', + editable: true, + required: true, options: [ { value: 1, label: '1 - Fácil' }, { value: 2, label: '2 - Medio' }, { value: 3, label: '3 - Difícil' }, { value: 4, label: '4 - Muy Difícil' }, { value: 5, label: '5 - Extremo' } - ], - required: true - }, + ] + } + ] + }, + { + title: 'Ingredientes', + icon: Package, + fields: [ { - key: 'serves_count', - label: 'Número de porciones', - value: formData.serves_count, - type: 'number', - min: 1, - placeholder: 'Cuántas personas sirve' + label: 'Lista de Ingredientes', + value: ingredientsList, + type: 'component', + editable: true, + component: IngredientsComponent, + componentProps: { + availableIngredients, + unitOptions, + onChange: setIngredientsList + }, + span: 2, + validation: () => validateIngredients() } ] }, @@ -353,252 +652,211 @@ export const CreateRecipeModal: React.FC = ({ icon: Clock, fields: [ { - key: 'yield_quantity', - label: 'Cantidad que produce', + label: 'Cantidad de rendimiento', value: formData.yield_quantity, type: 'number', - min: 1, - required: true + editable: true, + required: true, + validation: (value) => Number(value) <= 0 ? 'La cantidad debe ser mayor a 0' : null }, { - key: 'yield_unit', - label: 'Unidad de medida', + label: 'Unidad de rendimiento', value: formData.yield_unit, type: 'select', - options: unitOptions, - required: true + editable: true, + required: true, + options: unitOptions }, { - key: 'prep_time_minutes', - label: 'Tiempo de preparación (minutos)', - value: formData.prep_time_minutes, + label: 'Porciones', + value: formData.serves_count, type: 'number', - min: 0 + editable: true, + validation: (value) => Number(value) <= 0 ? 'Las porciones deben ser mayor a 0' : null }, { - key: 'cook_time_minutes', - label: 'Tiempo de cocción (minutos)', - value: formData.cook_time_minutes, + label: 'Tiempo de preparación (min)', + value: formData.prep_time_minutes || 0, type: 'number', - min: 0 + editable: true, + helpText: 'Tiempo de preparación de ingredientes' }, { - key: 'rest_time_minutes', - label: 'Tiempo de reposo (minutos)', - value: formData.rest_time_minutes, + label: 'Tiempo de cocción (min)', + value: formData.cook_time_minutes || 0, type: 'number', - min: 0, - placeholder: 'Tiempo de fermentación o reposo' + editable: true, + helpText: 'Tiempo de horneado o cocción' }, { - key: 'total_time_minutes', - label: 'Tiempo total (calculado automáticamente)', - value: formData.total_time_minutes, + label: 'Tiempo de reposo (min)', + value: formData.rest_time_minutes || 0, type: 'number', - disabled: true, - readonly: true + editable: true, + helpText: 'Tiempo de fermentación o reposo' } ] }, { - title: 'Configuración Financiera', - icon: Euro, + title: 'Configuración Estacional y Especial', + icon: Star, + collapsible: true, fields: [ { - key: 'estimated_cost_per_unit', - label: 'Costo estimado por unidad (€)', - value: formData.estimated_cost_per_unit, - type: 'number', - min: 0, - step: 0.01, - placeholder: '0.00' + label: 'Receta estacional', + value: formData.is_seasonal ? 'Sí' : 'No', + type: 'select', + editable: true, + options: [ + { value: 'No', label: 'No' }, + { value: 'Sí', label: 'Sí' } + ] + }, + ...(formData.is_seasonal ? [ + { + label: 'Mes de inicio', + value: formData.season_start_month || '', + type: 'select' as const, + editable: true, + options: monthOptions + }, + { + label: 'Mes de fin', + value: formData.season_end_month || '', + type: 'select' as const, + editable: true, + options: monthOptions + } + ] : []), + { + label: 'Receta estrella', + value: formData.is_signature_item ? 'Sí' : 'No', + type: 'select', + editable: true, + options: [ + { value: 'No', label: 'No' }, + { value: 'Sí', label: 'Sí' } + ] }, { - key: 'suggested_selling_price', - label: 'Precio de venta sugerido (€)', - value: formData.suggested_selling_price, - type: 'number', - min: 0, - step: 0.01, - placeholder: '0.00' - }, - { - key: 'target_margin_percentage', label: 'Margen objetivo (%)', - value: formData.target_margin_percentage, + value: formData.target_margin_percentage || 30, type: 'number', - min: 0, - max: 100, - placeholder: '30' + editable: true, + helpText: 'Margen de beneficio objetivo para esta receta' } ] }, { title: 'Configuración de Producción', - icon: Package, + icon: Settings, + collapsible: true, fields: [ { - key: 'batch_size_multiplier', label: 'Multiplicador de lote', value: formData.batch_size_multiplier, type: 'number', - min: 0.1, - step: 0.1, - placeholder: '1.0' + editable: true, + helpText: 'Factor de escala para producción' }, { - key: 'minimum_batch_size', label: 'Tamaño mínimo de lote', - value: formData.minimum_batch_size, + value: formData.minimum_batch_size || '', type: 'number', - min: 1, - placeholder: 'Cantidad mínima a producir' + editable: true, + helpText: 'Cantidad mínima recomendada' }, { - key: 'maximum_batch_size', label: 'Tamaño máximo de lote', - value: formData.maximum_batch_size, + value: formData.maximum_batch_size || '', type: 'number', - min: 1, - placeholder: 'Cantidad máxima a producir' + editable: true, + helpText: 'Cantidad máxima recomendada' }, { - key: 'optimal_production_temperature', label: 'Temperatura óptima (°C)', - value: formData.optimal_production_temperature, + value: formData.optimal_production_temperature || '', type: 'number', - placeholder: 'Temperatura ideal de producción' + editable: true, + helpText: 'Temperatura ideal de producción' }, { - key: 'optimal_humidity', label: 'Humedad óptima (%)', - value: formData.optimal_humidity, + value: formData.optimal_humidity || '', type: 'number', - min: 0, - max: 100, - placeholder: 'Humedad ideal' + editable: true, + helpText: 'Humedad ideal de producción' } ] }, { - title: 'Temporalidad y Especiales', - icon: Star, + title: 'Instrucciones y Calidad', + icon: FileText, + collapsible: true, fields: [ { - key: 'is_signature_item', - label: 'Receta especial/estrella', - value: formData.is_signature_item, - type: 'checkbox' - }, - { - key: 'is_seasonal', - label: 'Receta estacional', - value: formData.is_seasonal, - type: 'checkbox' - }, - { - key: 'season_start_month', - label: 'Mes de inicio de temporada', - value: formData.season_start_month, - type: 'select', - options: monthOptions, - placeholder: 'Selecciona mes de inicio', - disabled: !formData.is_seasonal - }, - { - key: 'season_end_month', - label: 'Mes de fin de temporada', - value: formData.season_end_month, - type: 'select', - options: monthOptions, - placeholder: 'Selecciona mes de fin', - disabled: !formData.is_seasonal - } - ] - }, - { - title: 'Información Nutricional y Alérgenos', - icon: Package, - fields: [ - { - key: 'allergen_info', - label: 'Información de alérgenos', - value: formData.allergen_info, - type: 'text', - placeholder: 'Ej: Gluten, Lácteos, Huevos' - }, - { - key: 'dietary_tags', - label: 'Etiquetas dietéticas', - value: formData.dietary_tags, - type: 'text', - placeholder: 'Ej: Vegano, Sin gluten, Orgánico' - }, - { - key: 'nutritional_info', - label: 'Información nutricional', - value: formData.nutritional_info, - type: 'textarea', - placeholder: 'Calorías, proteínas, carbohidratos, etc.' - } - ] - }, - { - title: 'Notas de Preparación', - icon: ChefHat, - fields: [ - { - key: 'preparation_notes', label: 'Notas de preparación', value: formData.preparation_notes, type: 'textarea', - placeholder: 'Instrucciones especiales, consejos, técnicas...' + editable: true, + placeholder: 'Instrucciones detalladas de preparación...', + span: 2, + helpText: 'Pasos detallados para la preparación' }, { - key: 'storage_instructions', label: 'Instrucciones de almacenamiento', value: formData.storage_instructions, type: 'textarea', - placeholder: 'Cómo almacenar el producto terminado...' + editable: true, + placeholder: 'Como conservar el producto terminado...', + span: 2, + helpText: 'Condiciones de almacenamiento del producto final' }, { - key: 'quality_standards', label: 'Estándares de calidad', value: formData.quality_standards, type: 'textarea', - placeholder: 'Criterios de calidad, características del producto...' + editable: true, + placeholder: 'Criterios de calidad que debe cumplir...', + span: 2, + helpText: 'Criterios que debe cumplir el producto final' } ] }, { - title: 'Producto Terminado', + title: 'Información Nutricional', icon: Package, + collapsible: true, + columns: 1, fields: [ { - key: 'finished_product_id', - label: 'Producto terminado', - value: formData.finished_product_id, - type: 'select', - options: finishedProducts, - required: true, - placeholder: inventoryLoading ? 'Cargando productos...' : 'Selecciona un producto terminado', - help: 'Selecciona el producto del inventario que produce esta receta', - disabled: inventoryLoading + label: 'Información de alérgenos', + value: formData.allergen_info_text, + type: 'text', + editable: true, + placeholder: 'Ej: Gluten, Lácteos, Huevos', + helpText: 'Separar con comas los alérgenos presentes' + }, + { + label: 'Etiquetas dietéticas', + value: formData.dietary_tags_text, + type: 'text', + editable: true, + placeholder: 'Ej: Vegano, Sin gluten, Orgánico', + helpText: 'Separar con comas las etiquetas dietéticas' + }, + { + label: 'Información nutricional', + value: formData.nutritional_info_text, + type: 'textarea', + editable: true, + placeholder: 'Calorías, proteínas, carbohidratos, etc.', + helpText: 'Información nutricional por porción' } ] } ]; - // Add editable: true to all fields since this is a creation modal - return sections.map(section => ({ - ...section, - fields: section.fields.map(field => ({ - ...field, - editable: field.readonly !== true // Make editable unless explicitly readonly - })) - })); - }; - return ( = ({ title="Nueva Receta" subtitle="Crear una nueva receta para la panadería" statusIndicator={{ - color: '#3b82f6', - text: 'Nueva', - icon: ChefHat, - isCritical: false, - isHighlight: true + color: '#10b981', + text: 'Creando', + icon: ChefHat }} - size="xl" - sections={getModalSections()} + sections={getSections()} onFieldChange={handleFieldChange} - showDefaultActions={true} onSave={handleSubmit} - onCancel={onClose} + size="2xl" + loading={isLoading} + mobileOptimized={true} + showDefaultActions={true} /> ); }; -export default CreateRecipeModal; \ No newline at end of file +export default CreateRecipeModal; diff --git a/frontend/src/components/ui/StatusModal/StatusModal.tsx b/frontend/src/components/ui/StatusModal/StatusModal.tsx index 054abd68..198a3e7c 100644 --- a/frontend/src/components/ui/StatusModal/StatusModal.tsx +++ b/frontend/src/components/ui/StatusModal/StatusModal.tsx @@ -10,19 +10,27 @@ import { formatters } from '../Stats/StatsPresets'; export interface StatusModalField { label: string; value: string | number | React.ReactNode; - type?: 'text' | 'currency' | 'date' | 'datetime' | 'percentage' | 'list' | 'status' | 'image' | 'email' | 'tel' | 'number' | 'select'; + type?: 'text' | 'currency' | 'date' | 'datetime' | 'percentage' | 'list' | 'status' | 'image' | 'email' | 'tel' | 'number' | 'select' | 'textarea' | 'component'; highlight?: boolean; - span?: 1 | 2; // For grid layout + span?: 1 | 2 | 3; // For grid layout - added 3 for full width on larger screens editable?: boolean; // Whether this field can be edited required?: boolean; // Whether this field is required placeholder?: string; // Placeholder text for inputs options?: Array<{label: string; value: string | number}>; // For select fields + validation?: (value: string | number) => string | null; // Custom validation function + helpText?: string; // Help text displayed below the field + component?: React.ComponentType; // For custom components + componentProps?: Record; // Props for custom components } export interface StatusModalSection { title: string; icon?: LucideIcon; fields: StatusModalField[]; + collapsible?: boolean; // Whether section can be collapsed + collapsed?: boolean; // Initial collapsed state + description?: string; // Section description + columns?: 1 | 2 | 3; // Override grid columns for this section } export interface StatusModalAction { @@ -59,6 +67,14 @@ export interface StatusModalProps { // Layout size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl'; loading?: boolean; + + // Enhanced features + mobileOptimized?: boolean; // Enable mobile-first responsive design + showStepIndicator?: boolean; // Show step indicator for multi-step workflows + currentStep?: number; // Current step in workflow + totalSteps?: number; // Total steps in workflow + validationErrors?: Record; // Field validation errors + onValidationError?: (errors: Record) => void; // Validation error handler } /** @@ -116,14 +132,27 @@ const formatFieldValue = (value: string | number | React.ReactNode, type: Status * Render editable field based on type and mode */ const renderEditableField = ( - field: StatusModalField, - isEditMode: boolean, - onChange?: (value: string | number) => void + field: StatusModalField, + isEditMode: boolean, + onChange?: (value: string | number) => void, + validationError?: string ): React.ReactNode => { if (!isEditMode || !field.editable) { return formatFieldValue(field.value, field.type); } + // Handle custom components + if (field.type === 'component' && field.component) { + const Component = field.component; + return ( + + ); + } + const handleChange = (e: React.ChangeEvent) => { const value = field.type === 'number' || field.type === 'currency' ? Number(e.target.value) : e.target.value; onChange?.(value); @@ -180,41 +209,94 @@ const renderEditableField = ( ); case 'list': return ( -