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 (
-