diff --git a/frontend/src/components/domain/inventory/QuickAddIngredientModal.tsx b/frontend/src/components/domain/inventory/QuickAddIngredientModal.tsx new file mode 100644 index 00000000..5efef137 --- /dev/null +++ b/frontend/src/components/domain/inventory/QuickAddIngredientModal.tsx @@ -0,0 +1,516 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useCreateIngredient } from '../../../api/hooks/inventory'; +import type { Ingredient } from '../../../api/types/inventory'; + +interface QuickAddIngredientModalProps { + isOpen: boolean; + onClose: () => void; + onCreated: (ingredient: Ingredient) => void; + tenantId: string; + context: 'recipe' | 'supplier' | 'standalone'; +} + +export const QuickAddIngredientModal: React.FC = ({ + isOpen, + onClose, + onCreated, + tenantId, + context +}) => { + const { t } = useTranslation(); + const createIngredient = useCreateIngredient(); + + // Form state - minimal required fields + const [formData, setFormData] = useState({ + name: '', + category: '', + unit_of_measure: 'kg', + // Optional fields (collapsed by default) + stock_quantity: 0, + cost_per_unit: 0, + estimated_shelf_life_days: 30, + low_stock_threshold: 0, + reorder_point: 0, + requires_refrigeration: false, + requires_freezing: false, + is_seasonal: false, + notes: '', + }); + + const [showOptionalFields, setShowOptionalFields] = useState(false); + const [errors, setErrors] = useState>({}); + const [isSubmitting, setIsSubmitting] = useState(false); + + const categoryOptions = [ + 'Baking Ingredients', + 'Dairy', + 'Fruits', + 'Vegetables', + 'Meat', + 'Seafood', + 'Spices', + 'Other' + ]; + + const unitOptions = ['kg', 'g', 'L', 'ml', 'units', 'dozen']; + + const validateForm = (): boolean => { + const newErrors: Record = {}; + + if (!formData.name.trim()) { + newErrors.name = 'El nombre es requerido'; + } + if (!formData.category) { + newErrors.category = 'La categoría es requerida'; + } + if (!formData.unit_of_measure) { + newErrors.unit_of_measure = 'La unidad es requerida'; + } + + // Validate optional fields if shown + if (showOptionalFields) { + if (formData.stock_quantity < 0) { + newErrors.stock_quantity = 'El stock no puede ser negativo'; + } + if (formData.cost_per_unit < 0) { + newErrors.cost_per_unit = 'El costo no puede ser negativo'; + } + if (formData.estimated_shelf_life_days <= 0) { + newErrors.estimated_shelf_life_days = 'Los días de caducidad deben ser mayores a 0'; + } + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) return; + + setIsSubmitting(true); + + try { + const ingredientData = { + name: formData.name.trim(), + product_type: 'ingredient', + category: formData.category, + unit_of_measure: formData.unit_of_measure, + low_stock_threshold: showOptionalFields ? formData.low_stock_threshold : 1, + max_stock_level: showOptionalFields ? formData.stock_quantity * 2 : 100, + reorder_point: showOptionalFields ? formData.reorder_point : 2, + shelf_life_days: showOptionalFields ? formData.estimated_shelf_life_days : 30, + requires_refrigeration: formData.requires_refrigeration, + requires_freezing: formData.requires_freezing, + is_seasonal: formData.is_seasonal, + average_cost: showOptionalFields ? formData.cost_per_unit : 0, + notes: formData.notes || `Creado durante ${context === 'recipe' ? 'configuración de receta' : 'configuración de proveedor'}`, + // Track that this was created inline + metadata: { + created_context: context, + is_complete: showOptionalFields, + needs_review: !showOptionalFields, + } + }; + + const createdIngredient = await createIngredient.mutateAsync({ + tenantId, + ingredientData + }); + + // Call parent with created ingredient + onCreated(createdIngredient); + + // Reset and close + resetForm(); + onClose(); + } catch (error) { + console.error('Error creating ingredient:', error); + setErrors({ submit: 'Error al crear el ingrediente. Inténtalo de nuevo.' }); + } finally { + setIsSubmitting(false); + } + }; + + const resetForm = () => { + setFormData({ + name: '', + category: '', + unit_of_measure: 'kg', + stock_quantity: 0, + cost_per_unit: 0, + estimated_shelf_life_days: 30, + low_stock_threshold: 0, + reorder_point: 0, + requires_refrigeration: false, + requires_freezing: false, + is_seasonal: false, + notes: '', + }); + setShowOptionalFields(false); + setErrors({}); + }; + + const handleClose = () => { + resetForm(); + onClose(); + }; + + if (!isOpen) return null; + + const getContextMessage = () => { + switch (context) { + case 'recipe': + return 'El ingrediente se agregará al inventario y estará disponible para usar en esta receta.'; + case 'supplier': + return 'El ingrediente se agregará al inventario y podrás asociarlo con este proveedor.'; + default: + return 'El ingrediente se agregará a tu inventario.'; + } + }; + + const getCtaText = () => { + switch (context) { + case 'recipe': + return 'Agregar y Usar en Receta'; + case 'supplier': + return 'Agregar y Asociar con Proveedor'; + default: + return 'Agregar Ingrediente'; + } + }; + + return ( + <> + {/* Backdrop */} +
+ + {/* Modal */} +
+
e.stopPropagation()} + > + {/* Header */} +
+
+
+ + + +
+
+

+ ⚡ Agregar Ingrediente Rápido +

+

+ {getContextMessage()} +

+
+
+ +
+ + {/* Form */} +
+ {/* Required Fields */} +
+
+ + setFormData({ ...formData, name: e.target.value })} + className="w-full px-3 py-2.5 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent text-[var(--text-primary)] transition-all" + placeholder="Ej: Harina de trigo integral" + autoFocus + /> + {errors.name && ( +

+ + + + {errors.name} +

+ )} +
+ +
+
+ + + {errors.category && ( +

{errors.category}

+ )} +
+ +
+ + + {errors.unit_of_measure && ( +

{errors.unit_of_measure}

+ )} +
+
+
+ + {/* Optional Fields Toggle */} +
+ + + {/* Optional Fields */} + {showOptionalFields && ( +
+
+
+ + setFormData({ ...formData, stock_quantity: parseFloat(e.target.value) || 0 })} + className="w-full px-3 py-2.5 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]" + /> + {errors.stock_quantity && ( +

{errors.stock_quantity}

+ )} +
+ +
+ + setFormData({ ...formData, cost_per_unit: parseFloat(e.target.value) || 0 })} + className="w-full px-3 py-2.5 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]" + /> + {errors.cost_per_unit && ( +

{errors.cost_per_unit}

+ )} +
+ +
+ + setFormData({ ...formData, estimated_shelf_life_days: parseInt(e.target.value) || 30 })} + className="w-full px-3 py-2.5 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]" + /> + {errors.estimated_shelf_life_days && ( +

{errors.estimated_shelf_life_days}

+ )} +
+ +
+ + setFormData({ ...formData, reorder_point: parseInt(e.target.value) || 0 })} + className="w-full px-3 py-2.5 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]" + /> +
+
+ +
+ + + +
+
+ )} +
+ + {/* Info Box */} + {!showOptionalFields && ( +
+

+ + + + + 💡 Puedes completar los detalles de stock y costos después en la gestión de inventario. + +

+
+ )} + + {/* Error Message */} + {errors.submit && ( +
+

+ + + + {errors.submit} +

+
+ )} + + {/* Actions */} +
+ + +
+
+
+
+ + {/* Animation Styles */} + + + ); +}; diff --git a/frontend/src/components/domain/setup-wizard/steps/RecipesSetupStep.tsx b/frontend/src/components/domain/setup-wizard/steps/RecipesSetupStep.tsx index 643f720c..c0a5c1ab 100644 --- a/frontend/src/components/domain/setup-wizard/steps/RecipesSetupStep.tsx +++ b/frontend/src/components/domain/setup-wizard/steps/RecipesSetupStep.tsx @@ -8,6 +8,7 @@ import { useAuthUser } from '../../../../stores/auth.store'; import { MeasurementUnit } from '../../../../api/types/recipes'; import type { RecipeCreate, RecipeIngredientCreate } from '../../../../api/types/recipes'; import { getAllRecipeTemplates, matchIngredientToTemplate, type RecipeTemplate } from '../data/recipeTemplates'; +import { QuickAddIngredientModal } from '../../inventory/QuickAddIngredientModal'; interface RecipeIngredientForm { ingredient_id: string; @@ -53,6 +54,10 @@ export const RecipesSetupStep: React.FC = ({ onUpdate, onComplet const [selectedTemplate, setSelectedTemplate] = useState(null); const allTemplates = getAllRecipeTemplates(); + // Quick add ingredient modal state + const [showQuickAddModal, setShowQuickAddModal] = useState(false); + const [pendingIngredientIndex, setPendingIngredientIndex] = useState(null); + // Notify parent when count changes useEffect(() => { const count = recipes.length; @@ -217,6 +222,29 @@ export const RecipesSetupStep: React.FC = ({ onUpdate, onComplet setSelectedTemplate(template); }; + // Quick add ingredient handlers + const handleQuickAddIngredient = (index: number) => { + setPendingIngredientIndex(index); + setShowQuickAddModal(true); + }; + + const handleIngredientCreated = async (ingredient: any) => { + // Ingredient is already created in the database by the modal + // Now we need to select it for the recipe + + if (pendingIngredientIndex === -1) { + // This was for the finished product + setFormData({ ...formData, finished_product_id: ingredient.id }); + } else if (pendingIngredientIndex !== null) { + // Update the ingredient at the pending index + updateIngredient(pendingIngredientIndex, 'ingredient_id', ingredient.id); + } + + // Clear pending state + setPendingIngredientIndex(null); + setShowQuickAddModal(false); + }; + const unitOptions = [ { value: MeasurementUnit.GRAMS, label: t('recipes:unit.g', 'Grams (g)') }, { value: MeasurementUnit.KILOGRAMS, label: t('recipes:unit.kg', 'Kilograms (kg)') }, @@ -526,7 +554,14 @@ export const RecipesSetupStep: React.FC = ({ onUpdate, onComplet {errors.finished_product_id &&

{errors.finished_product_id}

}
@@ -595,11 +633,17 @@ export const RecipesSetupStep: React.FC = ({ onUpdate, onComplet
{recipeIngredients.map((ing, index) => (
-
+
{errors[`ingredient_${index}_id`] &&

{errors[`ingredient_${index}_id`]}

}
@@ -734,6 +781,18 @@ export const RecipesSetupStep: React.FC = ({ onUpdate, onComplet
)} + + {/* Quick Add Ingredient Modal */} + { + setShowQuickAddModal(false); + setPendingIngredientIndex(null); + }} + onCreated={handleIngredientCreated} + tenantId={tenantId} + context="recipe" + />
); }; diff --git a/frontend/src/components/domain/setup-wizard/steps/SupplierProductManager.tsx b/frontend/src/components/domain/setup-wizard/steps/SupplierProductManager.tsx index d3787813..3fbfb01d 100644 --- a/frontend/src/components/domain/setup-wizard/steps/SupplierProductManager.tsx +++ b/frontend/src/components/domain/setup-wizard/steps/SupplierProductManager.tsx @@ -8,6 +8,7 @@ import { } from '../../../../api/hooks/suppliers'; import { useIngredients } from '../../../../api/hooks/inventory'; import type { SupplierPriceListCreate, SupplierPriceListResponse } from '../../../../api/types/suppliers'; +import { QuickAddIngredientModal } from '../../inventory/QuickAddIngredientModal'; interface SupplierProductManagerProps { tenantId: string; @@ -53,6 +54,9 @@ export const SupplierProductManager: React.FC = ({ const [productForms, setProductForms] = useState>({}); const [errors, setErrors] = useState>({}); + // Quick add modal state + const [showQuickAddModal, setShowQuickAddModal] = useState(false); + // Filter available products (not already in price list) const availableProducts = inventoryItems.filter( item => !priceLists.some(pl => pl.inventory_product_id === item.id) @@ -85,6 +89,13 @@ export const SupplierProductManager: React.FC = ({ }); }; + // Quick add handlers + const handleIngredientCreated = (ingredient: any) => { + // Ingredient created - auto-select it + handleToggleProduct(ingredient.id); + setShowQuickAddModal(false); + }; + const handleUpdateForm = (productId: string, field: string, value: string) => { setProductForms(prev => ({ ...prev, @@ -354,6 +365,22 @@ export const SupplierProductManager: React.FC = ({ )}
))} + + {/* Add New Product Button */} + {/* Actions */} @@ -400,6 +427,15 @@ export const SupplierProductManager: React.FC = ({ ⚠️ {t('setup_wizard:suppliers.no_products_warning', 'Add at least 1 product to enable automatic purchase orders')} )} + + {/* Quick Add Ingredient Modal */} + setShowQuickAddModal(false)} + onCreated={handleIngredientCreated} + tenantId={tenantId} + context="supplier" + /> ); };