Refactor components and modals

This commit is contained in:
Urtzi Alfaro
2025-09-26 07:46:25 +02:00
parent cf4405b771
commit d573c38621
80 changed files with 3421 additions and 4617 deletions

View File

@@ -1,9 +1,10 @@
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 React, { useState, useMemo } from 'react';
import { ChefHat, Package, Clock, Star, Plus, Trash2, Settings, FileText } from 'lucide-react';
import { AddModal } from '../../ui/AddModal/AddModal';
import { RecipeCreate, RecipeIngredientCreate, MeasurementUnit } from '../../../api/types/recipes';
import { useIngredients } from '../../../api/hooks/inventory';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { statusColors } from '../../../styles/colors';
interface CreateRecipeModalProps {
isOpen: boolean;
@@ -15,7 +16,7 @@ interface CreateRecipeModalProps {
* CreateRecipeModal - Modal for creating a new recipe
* Comprehensive form for adding new recipes
*/
// Custom Ingredients Component for StatusModal
// Custom Ingredients Component for AddModal
const IngredientsComponent: React.FC<{
value: RecipeIngredientCreate[];
onChange: (value: RecipeIngredientCreate[]) => void;
@@ -84,7 +85,7 @@ const IngredientsComponent: React.FC<{
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"
>
<option value="">Seleccionar...</option>
{availableIngredients.map(ing => (
{(availableIngredients || []).map(ing => (
<option key={ing.value} value={ing.value}>{ing.label}</option>
))}
</select>
@@ -141,16 +142,6 @@ export const CreateRecipeModal: React.FC<CreateRecipeModalProps> = ({
onClose,
onCreateRecipe
}) => {
// Extended form data interface to include UI-specific fields
interface ExtendedFormData extends Omit<RecipeCreate, 'allergen_info' | 'dietary_tags' | 'nutritional_info'> {
allergen_info_text: string;
dietary_tags_text: string;
nutritional_info_text: string;
allergen_info: Record<string, any> | null;
dietary_tags: Record<string, any> | null;
nutritional_info: Record<string, any> | null;
}
const [ingredientsList, setIngredientsList] = useState<RecipeIngredientCreate[]>([{
ingredient_id: '',
quantity: 1,
@@ -158,50 +149,7 @@ export const CreateRecipeModal: React.FC<CreateRecipeModalProps> = ({
ingredient_order: 1,
is_optional: false
}]);
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState<ExtendedFormData>({
name: '',
recipe_code: '',
version: '1.0',
finished_product_id: '', // This should come from a product selector
description: '',
category: '',
cuisine_type: '',
difficulty_level: 1,
yield_quantity: 1,
yield_unit: MeasurementUnit.UNITS,
prep_time_minutes: 0,
cook_time_minutes: 0,
total_time_minutes: 0,
rest_time_minutes: 0,
target_margin_percentage: 30,
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,
season_end_month: undefined,
is_signature_item: false,
batch_size_multiplier: 1.0,
minimum_batch_size: undefined,
maximum_batch_size: undefined,
optimal_production_temperature: undefined,
optimal_humidity: undefined,
allergen_info_text: '',
dietary_tags_text: '',
nutritional_info_text: '',
allergen_info: null,
dietary_tags: null,
nutritional_info: null,
ingredients: []
});
const [loading, setLoading] = useState(false);
// Get tenant and fetch inventory data
const currentTenant = useCurrentTenant();
@@ -215,7 +163,8 @@ export const CreateRecipeModal: React.FC<CreateRecipeModalProps> = ({
// Separate finished products and ingredients
const finishedProducts = useMemo(() =>
inventoryItems.filter(item => item.product_type === 'finished_product')
(inventoryItems || [])
.filter(item => item.product_type === 'finished_product')
.map(product => ({
value: product.id,
label: `${product.name} (${product.category || 'Sin categoría'})`
@@ -225,7 +174,8 @@ export const CreateRecipeModal: React.FC<CreateRecipeModalProps> = ({
// Available ingredients for recipe ingredients
const availableIngredients = useMemo(() =>
inventoryItems.filter(item => item.product_type !== 'finished_product')
(inventoryItems || [])
.filter(item => item.product_type !== 'finished_product')
.map(ingredient => ({
value: ingredient.id,
label: `${ingredient.name} (${ingredient.category || 'Sin categoría'})`
@@ -233,7 +183,6 @@ export const CreateRecipeModal: React.FC<CreateRecipeModalProps> = ({
[inventoryItems]
);
// Category options
const categoryOptions = [
{ value: 'bread', label: 'Pan' },
@@ -285,100 +234,92 @@ export const CreateRecipeModal: React.FC<CreateRecipeModalProps> = ({
{ value: 12, label: 'Diciembre' }
];
// 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;
if (newTotal !== formData.total_time_minutes) {
setFormData(prev => ({
...prev,
total_time_minutes: newTotal
}));
}
}, [formData.prep_time_minutes, formData.cook_time_minutes, formData.rest_time_minutes]);
const handleSubmit = async () => {
if (!formData.name.trim()) {
alert('El nombre de la receta es obligatorio');
return;
const validateIngredients = (ingredients: RecipeIngredientCreate[]): string | null => {
if (!ingredients || ingredients.length === 0) {
return 'Debe agregar al menos un ingrediente';
}
if (!formData.category?.trim()) {
alert('Debe seleccionar una categoría');
return;
const emptyIngredients = ingredients.filter(ing => !ing.ingredient_id.trim());
if (emptyIngredients.length > 0) {
return 'Todos los ingredientes deben tener un ingrediente seleccionado';
}
if (!formData.finished_product_id.trim()) {
alert('Debe seleccionar un producto terminado');
return;
const invalidQuantities = ingredients.filter(ing => ing.quantity <= 0);
if (invalidQuantities.length > 0) {
return 'Todas las cantidades deben ser mayor que 0';
}
return null;
};
const handleSave = async (formData: Record<string, any>) => {
// Validate ingredients
const ingredientError = validateIngredients();
const ingredientError = validateIngredients(formData.ingredients || []);
if (ingredientError) {
alert(ingredientError);
return;
throw new Error(ingredientError);
}
// Validate seasonal dates if seasonal is enabled
if (formData.is_seasonal) {
if (!formData.season_start_month || !formData.season_end_month) {
alert('Para recetas estacionales, debe especificar los meses de inicio y fin');
return;
}
}
// 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;
if (formData.is_seasonal && (!formData.season_start_month || !formData.season_end_month)) {
throw new Error('Para recetas estacionales, debe especificar los meses de inicio y fin');
}
// Validate batch sizes
if (formData.minimum_batch_size && formData.maximum_batch_size) {
if (formData.minimum_batch_size > formData.maximum_batch_size) {
alert('El tamaño mínimo de lote no puede ser mayor que el máximo');
return;
}
if (formData.minimum_batch_size && formData.maximum_batch_size && formData.minimum_batch_size > formData.maximum_batch_size) {
throw new Error('El tamaño mínimo de lote no puede ser mayor que el máximo');
}
setLoading(true);
try {
setIsLoading(true);
// Generate recipe code if not provided
const recipeCode = formData.recipe_code ||
formData.name.substring(0, 3).toUpperCase() +
String(Date.now()).slice(-3);
// Calculate total time including rest time
const totalTime = (formData.prep_time_minutes || 0) +
(formData.cook_time_minutes || 0) +
(formData.rest_time_minutes || 0);
const totalTime = (Number(formData.prep_time_minutes) || 0) +
(Number(formData.cook_time_minutes) || 0) +
(Number(formData.rest_time_minutes) || 0);
const recipeData: RecipeCreate = {
...formData,
version: formData.version || '1.0',
name: formData.name,
recipe_code: recipeCode,
version: formData.version || '1.0',
finished_product_id: formData.finished_product_id,
description: formData.description || '',
category: formData.category,
cuisine_type: formData.cuisine_type || '',
difficulty_level: Number(formData.difficulty_level),
yield_quantity: Number(formData.yield_quantity),
yield_unit: formData.yield_unit as MeasurementUnit,
prep_time_minutes: Number(formData.prep_time_minutes) || 0,
cook_time_minutes: Number(formData.cook_time_minutes) || 0,
total_time_minutes: totalTime,
// Transform string fields to proper objects/dictionaries
rest_time_minutes: Number(formData.rest_time_minutes) || 0,
target_margin_percentage: Number(formData.target_margin_percentage) || 30,
instructions: formData.preparation_notes ? { steps: formData.preparation_notes } : null,
preparation_notes: formData.preparation_notes || '',
storage_instructions: formData.storage_instructions || '',
quality_standards: formData.quality_standards || '',
quality_check_configuration: null,
quality_check_points: null,
common_issues: null,
serves_count: Number(formData.serves_count) || 1,
is_seasonal: formData.is_seasonal || false,
season_start_month: formData.is_seasonal ? Number(formData.season_start_month) : undefined,
season_end_month: formData.is_seasonal ? Number(formData.season_end_month) : undefined,
is_signature_item: formData.is_signature_item || false,
batch_size_multiplier: Number(formData.batch_size_multiplier) || 1.0,
minimum_batch_size: formData.minimum_batch_size ? Number(formData.minimum_batch_size) : undefined,
maximum_batch_size: formData.maximum_batch_size ? Number(formData.maximum_batch_size) : undefined,
optimal_production_temperature: formData.optimal_production_temperature ? Number(formData.optimal_production_temperature) : undefined,
optimal_humidity: formData.optimal_humidity ? Number(formData.optimal_humidity) : undefined,
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,
// Validate and use the ingredients list from state
ingredients: ingredientsList.filter(ing => ing.ingredient_id.trim() !== '')
.map((ing, index) => ({
// Use the ingredients from form data
ingredients: (formData.ingredients || []).filter((ing: RecipeIngredientCreate) => ing.ingredient_id.trim() !== '')
.map((ing: RecipeIngredientCreate, index: number) => ({
...ing,
ingredient_order: index + 1
}))
@@ -388,9 +329,7 @@ export const CreateRecipeModal: React.FC<CreateRecipeModalProps> = ({
await onCreateRecipe(recipeData);
}
onClose();
// Reset form and ingredients list
// Reset ingredients list (AddModal will handle form reset)
setIngredientsList([{
ingredient_id: '',
quantity: 1,
@@ -398,225 +337,87 @@ export const CreateRecipeModal: React.FC<CreateRecipeModalProps> = ({
ingredient_order: 1,
is_optional: false
}]);
setFormData({
name: '',
recipe_code: '',
version: '1.0',
finished_product_id: '',
description: '',
category: '',
cuisine_type: '',
difficulty_level: 1,
yield_quantity: 1,
yield_unit: MeasurementUnit.UNITS,
prep_time_minutes: 0,
cook_time_minutes: 0,
total_time_minutes: 0,
rest_time_minutes: 0,
target_margin_percentage: 30,
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,
season_end_month: undefined,
is_signature_item: false,
batch_size_multiplier: 1.0,
minimum_batch_size: undefined,
maximum_batch_size: undefined,
optimal_production_temperature: undefined,
optimal_humidity: undefined,
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.');
throw error; // Let AddModal handle error display
} finally {
setIsLoading(false);
setLoading(false);
}
};
// 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 statusConfig = {
color: statusColors.inProgress.primary,
text: 'Nueva Receta',
icon: Plus,
isCritical: false,
isHighlight: true
};
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<string, string> = {
'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[] => [
const sections = [
{
title: 'Información Básica',
icon: ChefHat,
fields: [
{
label: 'Nombre',
value: formData.name,
type: 'text',
editable: true,
name: 'name',
type: 'text' as const,
required: true,
placeholder: 'Ej: Pan de molde integral',
validation: (value) => !String(value).trim() ? 'El nombre es obligatorio' : null
validation: (value: string | number) => {
const str = String(value).trim();
return str.length < 2 ? 'El nombre debe tener al menos 2 caracteres' : null;
}
},
{
label: 'Código de receta',
value: formData.recipe_code,
type: 'text',
editable: true,
name: 'recipe_code',
type: 'text' as const,
placeholder: 'Ej: PAN001 (opcional)'
},
{
label: 'Versión',
value: formData.version,
type: 'text',
editable: true,
name: 'version',
type: 'text' as const,
defaultValue: '1.0',
placeholder: '1.0'
},
{
label: 'Descripción',
value: formData.description,
type: 'textarea',
editable: true,
name: 'description',
type: 'textarea' as const,
placeholder: 'Descripción de la receta...',
span: 2
},
{
label: 'Categoría',
value: formData.category,
type: 'select',
editable: true,
name: 'category',
type: 'select' as const,
required: true,
options: categoryOptions,
validation: (value) => !String(value).trim() ? 'La categoría es obligatoria' : null
placeholder: 'Seleccionar categoría...'
},
{
label: 'Producto terminado',
value: formData.finished_product_id,
type: 'select',
editable: true,
name: 'finished_product_id',
type: 'select' as const,
required: true,
options: finishedProducts,
validation: (value) => !String(value).trim() ? 'Debe seleccionar un producto terminado' : null
placeholder: 'Seleccionar producto...'
},
{
label: 'Tipo de cocina',
value: formData.cuisine_type,
type: 'select',
editable: true,
options: cuisineTypeOptions
name: 'cuisine_type',
type: 'select' as const,
options: cuisineTypeOptions,
placeholder: 'Seleccionar tipo...'
},
{
label: 'Nivel de dificultad',
value: formData.difficulty_level,
type: 'select',
editable: true,
name: 'difficulty_level',
type: 'select' as const,
required: true,
defaultValue: 1,
options: [
{ value: 1, label: '1 - Fácil' },
{ value: 2, label: '2 - Medio' },
@@ -627,122 +428,105 @@ export const CreateRecipeModal: React.FC<CreateRecipeModalProps> = ({
}
]
},
{
title: 'Ingredientes',
icon: Package,
fields: [
{
label: 'Lista de Ingredientes',
value: ingredientsList,
type: 'component',
editable: true,
component: IngredientsComponent,
componentProps: {
availableIngredients,
unitOptions,
onChange: setIngredientsList
},
span: 2,
validation: () => validateIngredients()
}
]
},
{
title: 'Rendimiento y Tiempos',
icon: Clock,
fields: [
{
label: 'Cantidad de rendimiento',
value: formData.yield_quantity,
type: 'number',
editable: true,
name: 'yield_quantity',
type: 'number' as const,
required: true,
validation: (value) => Number(value) <= 0 ? 'La cantidad debe ser mayor a 0' : null
defaultValue: 1,
validation: (value: string | number) => {
const num = Number(value);
return num <= 0 ? 'La cantidad debe ser mayor a 0' : null;
}
},
{
label: 'Unidad de rendimiento',
value: formData.yield_unit,
type: 'select',
editable: true,
name: 'yield_unit',
type: 'select' as const,
required: true,
options: unitOptions
options: unitOptions,
defaultValue: MeasurementUnit.UNITS
},
{
label: 'Porciones',
value: formData.serves_count,
type: 'number',
editable: true,
validation: (value) => Number(value) <= 0 ? 'Las porciones deben ser mayor a 0' : null
name: 'serves_count',
type: 'number' as const,
defaultValue: 1,
validation: (value: string | number) => {
const num = Number(value);
return num <= 0 ? 'Las porciones deben ser mayor a 0' : null;
}
},
{
label: 'Tiempo de preparación (min)',
value: formData.prep_time_minutes || 0,
type: 'number',
editable: true,
name: 'prep_time_minutes',
type: 'number' as const,
defaultValue: 0,
helpText: 'Tiempo de preparación de ingredientes'
},
{
label: 'Tiempo de cocción (min)',
value: formData.cook_time_minutes || 0,
type: 'number',
editable: true,
name: 'cook_time_minutes',
type: 'number' as const,
defaultValue: 0,
helpText: 'Tiempo de horneado o cocción'
},
{
label: 'Tiempo de reposo (min)',
value: formData.rest_time_minutes || 0,
type: 'number',
editable: true,
name: 'rest_time_minutes',
type: 'number' as const,
defaultValue: 0,
helpText: 'Tiempo de fermentación o reposo'
}
]
},
{
title: 'Configuración Estacional y Especial',
title: 'Configuración Especial',
icon: Star,
collapsible: true,
fields: [
{
label: 'Receta estacional',
value: formData.is_seasonal ? 'Sí' : 'No',
type: 'select',
editable: true,
name: 'is_seasonal',
type: 'select' as const,
options: [
{ value: 'No', label: 'No' },
{ value: 'Sí', label: 'Sí' }
]
{ value: false, label: 'No' },
{ value: true, label: 'Sí' }
],
defaultValue: false
},
{
label: 'Mes de inicio',
name: 'season_start_month',
type: 'select' as const,
options: monthOptions,
placeholder: 'Seleccionar mes...'
},
{
label: 'Mes de fin',
name: 'season_end_month',
type: 'select' as const,
options: monthOptions,
placeholder: 'Seleccionar mes...'
},
...(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,
name: 'is_signature_item',
type: 'select' as const,
options: [
{ value: 'No', label: 'No' },
{ value: 'Sí', label: 'Sí' }
]
{ value: false, label: 'No' },
{ value: true, label: 'Sí' }
],
defaultValue: false
},
{
label: 'Margen objetivo (%)',
value: formData.target_margin_percentage || 30,
type: 'number',
editable: true,
name: 'target_margin_percentage',
type: 'number' as const,
defaultValue: 30,
helpText: 'Margen de beneficio objetivo para esta receta'
}
]
@@ -750,41 +534,36 @@ export const CreateRecipeModal: React.FC<CreateRecipeModalProps> = ({
{
title: 'Configuración de Producción',
icon: Settings,
collapsible: true,
fields: [
{
label: 'Multiplicador de lote',
value: formData.batch_size_multiplier,
type: 'number',
editable: true,
name: 'batch_size_multiplier',
type: 'number' as const,
defaultValue: 1.0,
helpText: 'Factor de escala para producción'
},
{
label: 'Tamaño mínimo de lote',
value: formData.minimum_batch_size || '',
type: 'number',
editable: true,
name: 'minimum_batch_size',
type: 'number' as const,
helpText: 'Cantidad mínima recomendada'
},
{
label: 'Tamaño máximo de lote',
value: formData.maximum_batch_size || '',
type: 'number',
editable: true,
name: 'maximum_batch_size',
type: 'number' as const,
helpText: 'Cantidad máxima recomendada'
},
{
label: 'Temperatura óptima (°C)',
value: formData.optimal_production_temperature || '',
type: 'number',
editable: true,
name: 'optimal_production_temperature',
type: 'number' as const,
helpText: 'Temperatura ideal de producción'
},
{
label: 'Humedad óptima (%)',
value: formData.optimal_humidity || '',
type: 'number',
editable: true,
name: 'optimal_humidity',
type: 'number' as const,
helpText: 'Humedad ideal de producción'
}
]
@@ -792,64 +571,76 @@ export const CreateRecipeModal: React.FC<CreateRecipeModalProps> = ({
{
title: 'Instrucciones y Calidad',
icon: FileText,
collapsible: true,
fields: [
{
label: 'Notas de preparación',
value: formData.preparation_notes,
type: 'textarea',
editable: true,
name: 'preparation_notes',
type: 'textarea' as const,
placeholder: 'Instrucciones detalladas de preparación...',
span: 2,
helpText: 'Pasos detallados para la preparación'
},
{
label: 'Instrucciones de almacenamiento',
value: formData.storage_instructions,
type: 'textarea',
editable: true,
name: 'storage_instructions',
type: 'textarea' as const,
placeholder: 'Como conservar el producto terminado...',
span: 2,
helpText: 'Condiciones de almacenamiento del producto final'
},
{
label: 'Estándares de calidad',
value: formData.quality_standards,
type: 'textarea',
editable: true,
name: 'quality_standards',
type: 'textarea' as const,
placeholder: 'Criterios de calidad que debe cumplir...',
span: 2,
helpText: 'Criterios que debe cumplir el producto final'
}
]
},
{
title: 'Ingredientes',
icon: Package,
columns: 1,
fields: [
{
label: 'Lista de ingredientes',
name: 'ingredients',
type: 'component' as const,
component: IngredientsComponent,
componentProps: {
availableIngredients,
unitOptions
},
defaultValue: ingredientsList,
span: 2,
helpText: 'Agrega los ingredientes necesarios para esta receta'
}
]
},
{
title: 'Información Nutricional',
icon: Package,
collapsible: true,
columns: 1,
fields: [
{
label: 'Información de alérgenos',
value: formData.allergen_info_text,
type: 'text',
editable: true,
name: 'allergen_info_text',
type: 'text' as const,
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,
name: 'dietary_tags_text',
type: 'text' as const,
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,
name: 'nutritional_info_text',
type: 'textarea' as const,
placeholder: 'Calorías, proteínas, carbohidratos, etc.',
helpText: 'Información nutricional por porción'
}
@@ -858,26 +649,19 @@ export const CreateRecipeModal: React.FC<CreateRecipeModalProps> = ({
];
return (
<StatusModal
<AddModal
isOpen={isOpen}
onClose={onClose}
mode="edit"
title="Nueva Receta"
subtitle="Crear una nueva receta para la panadería"
statusIndicator={{
color: '#10b981',
text: 'Creando',
icon: ChefHat
}}
sections={getSections()}
onFieldChange={handleFieldChange}
onSave={handleSubmit}
statusIndicator={statusConfig}
sections={sections}
size="2xl"
loading={isLoading}
mobileOptimized={true}
showDefaultActions={true}
loading={loading}
onSave={handleSave}
initialData={{ ingredients: ingredientsList }}
/>
);
};
export default CreateRecipeModal;
export default CreateRecipeModal;

View File

@@ -14,7 +14,7 @@ import {
Badge,
Select
} from '../../ui';
import { LoadingSpinner } from '../../shared';
import { LoadingSpinner } from '../../ui';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { useQualityTemplatesForRecipe } from '../../../api/hooks/qualityTemplates';
import {