Refactor components and modals
This commit is contained in:
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user