Implement 5 UX enhancements for ingredient management
This commit implements the requested enhancements for the ingredient
quick-add system and batch management:
**1. Duplicate Detection**
- Real-time Levenshtein distance-based similarity checking
- Shows warning with top 3 similar ingredients (70%+ similarity)
- Prevents accidental duplicate creation
- Location: QuickAddIngredientModal.tsx
**2. Smart Category Suggestions**
- Auto-populates category based on ingredient name patterns
- Supports Spanish and English ingredient names
- Shows visual indicator when category is AI-suggested
- Pattern matching for: Baking, Dairy, Fruits, Vegetables, Meat, Seafood, Spices
- Location: ingredientHelpers.ts
**3. Quick Templates**
- 10 pre-configured common bakery ingredients
- One-click template application
- Templates include: Flour, Butter, Sugar, Eggs, Yeast, Milk, Chocolate, Vanilla, Salt, Cream
- Each template has sensible defaults (shelf life, refrigeration requirements)
- Location: QuickAddIngredientModal.tsx
**4. Batch Creation Mode**
- BatchAddIngredientsModal component for adding multiple ingredients at once
- Table-based interface for efficient data entry
- "Load from Templates" quick action
- Duplicate detection within batch
- Partial success handling (some ingredients succeed, some fail)
- Location: BatchAddIngredientsModal.tsx
- Integration: UploadSalesDataStep.tsx (2 buttons: "Add One" / "Add Multiple")
**5. Dashboard Alert for Incomplete Ingredients**
- IncompleteIngredientsAlert component on dashboard
- Queries ingredients with needs_review metadata flag
- Shows count badge and first 5 incomplete ingredients
- "Complete Information" button links to inventory page
- Only shows when incomplete ingredients exist
- Location: IncompleteIngredientsAlert.tsx
- Integration: DashboardPage.tsx
**New Files Created:**
- ingredientHelpers.ts - Utilities for duplicate detection, smart suggestions, templates
- BatchAddIngredientsModal.tsx - Batch ingredient creation component
- IncompleteIngredientsAlert.tsx - Dashboard alert component
**Files Modified:**
- QuickAddIngredientModal.tsx - Added duplicate detection, smart suggestions, templates
- UploadSalesDataStep.tsx - Integrated batch creation modal
- DashboardPage.tsx - Added incomplete ingredients alert
**Technical Highlights:**
- Levenshtein distance algorithm for fuzzy name matching
- Pattern-based category suggestions (supports 100+ ingredient patterns)
- Metadata tracking (needs_review, created_context)
- Real-time validation and error handling
- Responsive UI with animations
- Consistent with existing design system
All features built and tested successfully.
Build time: 21.29s
2025-11-06 15:39:30 +00:00
|
|
|
|
import React, { useState, useEffect } from 'react';
|
2025-11-06 15:25:26 +00:00
|
|
|
|
import { useTranslation } from 'react-i18next';
|
Implement 5 UX enhancements for ingredient management
This commit implements the requested enhancements for the ingredient
quick-add system and batch management:
**1. Duplicate Detection**
- Real-time Levenshtein distance-based similarity checking
- Shows warning with top 3 similar ingredients (70%+ similarity)
- Prevents accidental duplicate creation
- Location: QuickAddIngredientModal.tsx
**2. Smart Category Suggestions**
- Auto-populates category based on ingredient name patterns
- Supports Spanish and English ingredient names
- Shows visual indicator when category is AI-suggested
- Pattern matching for: Baking, Dairy, Fruits, Vegetables, Meat, Seafood, Spices
- Location: ingredientHelpers.ts
**3. Quick Templates**
- 10 pre-configured common bakery ingredients
- One-click template application
- Templates include: Flour, Butter, Sugar, Eggs, Yeast, Milk, Chocolate, Vanilla, Salt, Cream
- Each template has sensible defaults (shelf life, refrigeration requirements)
- Location: QuickAddIngredientModal.tsx
**4. Batch Creation Mode**
- BatchAddIngredientsModal component for adding multiple ingredients at once
- Table-based interface for efficient data entry
- "Load from Templates" quick action
- Duplicate detection within batch
- Partial success handling (some ingredients succeed, some fail)
- Location: BatchAddIngredientsModal.tsx
- Integration: UploadSalesDataStep.tsx (2 buttons: "Add One" / "Add Multiple")
**5. Dashboard Alert for Incomplete Ingredients**
- IncompleteIngredientsAlert component on dashboard
- Queries ingredients with needs_review metadata flag
- Shows count badge and first 5 incomplete ingredients
- "Complete Information" button links to inventory page
- Only shows when incomplete ingredients exist
- Location: IncompleteIngredientsAlert.tsx
- Integration: DashboardPage.tsx
**New Files Created:**
- ingredientHelpers.ts - Utilities for duplicate detection, smart suggestions, templates
- BatchAddIngredientsModal.tsx - Batch ingredient creation component
- IncompleteIngredientsAlert.tsx - Dashboard alert component
**Files Modified:**
- QuickAddIngredientModal.tsx - Added duplicate detection, smart suggestions, templates
- UploadSalesDataStep.tsx - Integrated batch creation modal
- DashboardPage.tsx - Added incomplete ingredients alert
**Technical Highlights:**
- Levenshtein distance algorithm for fuzzy name matching
- Pattern-based category suggestions (supports 100+ ingredient patterns)
- Metadata tracking (needs_review, created_context)
- Real-time validation and error handling
- Responsive UI with animations
- Consistent with existing design system
All features built and tested successfully.
Build time: 21.29s
2025-11-06 15:39:30 +00:00
|
|
|
|
import { useCreateIngredient, useIngredients } from '../../../api/hooks/inventory';
|
2025-11-06 15:25:26 +00:00
|
|
|
|
import type { Ingredient } from '../../../api/types/inventory';
|
Implement 5 UX enhancements for ingredient management
This commit implements the requested enhancements for the ingredient
quick-add system and batch management:
**1. Duplicate Detection**
- Real-time Levenshtein distance-based similarity checking
- Shows warning with top 3 similar ingredients (70%+ similarity)
- Prevents accidental duplicate creation
- Location: QuickAddIngredientModal.tsx
**2. Smart Category Suggestions**
- Auto-populates category based on ingredient name patterns
- Supports Spanish and English ingredient names
- Shows visual indicator when category is AI-suggested
- Pattern matching for: Baking, Dairy, Fruits, Vegetables, Meat, Seafood, Spices
- Location: ingredientHelpers.ts
**3. Quick Templates**
- 10 pre-configured common bakery ingredients
- One-click template application
- Templates include: Flour, Butter, Sugar, Eggs, Yeast, Milk, Chocolate, Vanilla, Salt, Cream
- Each template has sensible defaults (shelf life, refrigeration requirements)
- Location: QuickAddIngredientModal.tsx
**4. Batch Creation Mode**
- BatchAddIngredientsModal component for adding multiple ingredients at once
- Table-based interface for efficient data entry
- "Load from Templates" quick action
- Duplicate detection within batch
- Partial success handling (some ingredients succeed, some fail)
- Location: BatchAddIngredientsModal.tsx
- Integration: UploadSalesDataStep.tsx (2 buttons: "Add One" / "Add Multiple")
**5. Dashboard Alert for Incomplete Ingredients**
- IncompleteIngredientsAlert component on dashboard
- Queries ingredients with needs_review metadata flag
- Shows count badge and first 5 incomplete ingredients
- "Complete Information" button links to inventory page
- Only shows when incomplete ingredients exist
- Location: IncompleteIngredientsAlert.tsx
- Integration: DashboardPage.tsx
**New Files Created:**
- ingredientHelpers.ts - Utilities for duplicate detection, smart suggestions, templates
- BatchAddIngredientsModal.tsx - Batch ingredient creation component
- IncompleteIngredientsAlert.tsx - Dashboard alert component
**Files Modified:**
- QuickAddIngredientModal.tsx - Added duplicate detection, smart suggestions, templates
- UploadSalesDataStep.tsx - Integrated batch creation modal
- DashboardPage.tsx - Added incomplete ingredients alert
**Technical Highlights:**
- Levenshtein distance algorithm for fuzzy name matching
- Pattern-based category suggestions (supports 100+ ingredient patterns)
- Metadata tracking (needs_review, created_context)
- Real-time validation and error handling
- Responsive UI with animations
- Consistent with existing design system
All features built and tested successfully.
Build time: 21.29s
2025-11-06 15:39:30 +00:00
|
|
|
|
import {
|
|
|
|
|
|
findSimilarIngredients,
|
|
|
|
|
|
suggestCategory,
|
|
|
|
|
|
commonIngredientTemplates,
|
|
|
|
|
|
type IngredientTemplate
|
|
|
|
|
|
} from './ingredientHelpers';
|
2025-11-06 15:25:26 +00:00
|
|
|
|
|
|
|
|
|
|
interface QuickAddIngredientModalProps {
|
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
|
onCreated: (ingredient: Ingredient) => void;
|
|
|
|
|
|
tenantId: string;
|
|
|
|
|
|
context: 'recipe' | 'supplier' | 'standalone';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const QuickAddIngredientModal: React.FC<QuickAddIngredientModalProps> = ({
|
|
|
|
|
|
isOpen,
|
|
|
|
|
|
onClose,
|
|
|
|
|
|
onCreated,
|
|
|
|
|
|
tenantId,
|
|
|
|
|
|
context
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
const createIngredient = useCreateIngredient();
|
|
|
|
|
|
|
Implement 5 UX enhancements for ingredient management
This commit implements the requested enhancements for the ingredient
quick-add system and batch management:
**1. Duplicate Detection**
- Real-time Levenshtein distance-based similarity checking
- Shows warning with top 3 similar ingredients (70%+ similarity)
- Prevents accidental duplicate creation
- Location: QuickAddIngredientModal.tsx
**2. Smart Category Suggestions**
- Auto-populates category based on ingredient name patterns
- Supports Spanish and English ingredient names
- Shows visual indicator when category is AI-suggested
- Pattern matching for: Baking, Dairy, Fruits, Vegetables, Meat, Seafood, Spices
- Location: ingredientHelpers.ts
**3. Quick Templates**
- 10 pre-configured common bakery ingredients
- One-click template application
- Templates include: Flour, Butter, Sugar, Eggs, Yeast, Milk, Chocolate, Vanilla, Salt, Cream
- Each template has sensible defaults (shelf life, refrigeration requirements)
- Location: QuickAddIngredientModal.tsx
**4. Batch Creation Mode**
- BatchAddIngredientsModal component for adding multiple ingredients at once
- Table-based interface for efficient data entry
- "Load from Templates" quick action
- Duplicate detection within batch
- Partial success handling (some ingredients succeed, some fail)
- Location: BatchAddIngredientsModal.tsx
- Integration: UploadSalesDataStep.tsx (2 buttons: "Add One" / "Add Multiple")
**5. Dashboard Alert for Incomplete Ingredients**
- IncompleteIngredientsAlert component on dashboard
- Queries ingredients with needs_review metadata flag
- Shows count badge and first 5 incomplete ingredients
- "Complete Information" button links to inventory page
- Only shows when incomplete ingredients exist
- Location: IncompleteIngredientsAlert.tsx
- Integration: DashboardPage.tsx
**New Files Created:**
- ingredientHelpers.ts - Utilities for duplicate detection, smart suggestions, templates
- BatchAddIngredientsModal.tsx - Batch ingredient creation component
- IncompleteIngredientsAlert.tsx - Dashboard alert component
**Files Modified:**
- QuickAddIngredientModal.tsx - Added duplicate detection, smart suggestions, templates
- UploadSalesDataStep.tsx - Integrated batch creation modal
- DashboardPage.tsx - Added incomplete ingredients alert
**Technical Highlights:**
- Levenshtein distance algorithm for fuzzy name matching
- Pattern-based category suggestions (supports 100+ ingredient patterns)
- Metadata tracking (needs_review, created_context)
- Real-time validation and error handling
- Responsive UI with animations
- Consistent with existing design system
All features built and tested successfully.
Build time: 21.29s
2025-11-06 15:39:30 +00:00
|
|
|
|
// Fetch existing ingredients for duplicate detection
|
|
|
|
|
|
const { data: existingIngredients = [] } = useIngredients(tenantId, {}, {
|
|
|
|
|
|
enabled: isOpen && !!tenantId
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-06 15:25:26 +00:00
|
|
|
|
// 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<Record<string, string>>({});
|
|
|
|
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
Implement 5 UX enhancements for ingredient management
This commit implements the requested enhancements for the ingredient
quick-add system and batch management:
**1. Duplicate Detection**
- Real-time Levenshtein distance-based similarity checking
- Shows warning with top 3 similar ingredients (70%+ similarity)
- Prevents accidental duplicate creation
- Location: QuickAddIngredientModal.tsx
**2. Smart Category Suggestions**
- Auto-populates category based on ingredient name patterns
- Supports Spanish and English ingredient names
- Shows visual indicator when category is AI-suggested
- Pattern matching for: Baking, Dairy, Fruits, Vegetables, Meat, Seafood, Spices
- Location: ingredientHelpers.ts
**3. Quick Templates**
- 10 pre-configured common bakery ingredients
- One-click template application
- Templates include: Flour, Butter, Sugar, Eggs, Yeast, Milk, Chocolate, Vanilla, Salt, Cream
- Each template has sensible defaults (shelf life, refrigeration requirements)
- Location: QuickAddIngredientModal.tsx
**4. Batch Creation Mode**
- BatchAddIngredientsModal component for adding multiple ingredients at once
- Table-based interface for efficient data entry
- "Load from Templates" quick action
- Duplicate detection within batch
- Partial success handling (some ingredients succeed, some fail)
- Location: BatchAddIngredientsModal.tsx
- Integration: UploadSalesDataStep.tsx (2 buttons: "Add One" / "Add Multiple")
**5. Dashboard Alert for Incomplete Ingredients**
- IncompleteIngredientsAlert component on dashboard
- Queries ingredients with needs_review metadata flag
- Shows count badge and first 5 incomplete ingredients
- "Complete Information" button links to inventory page
- Only shows when incomplete ingredients exist
- Location: IncompleteIngredientsAlert.tsx
- Integration: DashboardPage.tsx
**New Files Created:**
- ingredientHelpers.ts - Utilities for duplicate detection, smart suggestions, templates
- BatchAddIngredientsModal.tsx - Batch ingredient creation component
- IncompleteIngredientsAlert.tsx - Dashboard alert component
**Files Modified:**
- QuickAddIngredientModal.tsx - Added duplicate detection, smart suggestions, templates
- UploadSalesDataStep.tsx - Integrated batch creation modal
- DashboardPage.tsx - Added incomplete ingredients alert
**Technical Highlights:**
- Levenshtein distance algorithm for fuzzy name matching
- Pattern-based category suggestions (supports 100+ ingredient patterns)
- Metadata tracking (needs_review, created_context)
- Real-time validation and error handling
- Responsive UI with animations
- Consistent with existing design system
All features built and tested successfully.
Build time: 21.29s
2025-11-06 15:39:30 +00:00
|
|
|
|
const [showTemplates, setShowTemplates] = useState(false);
|
|
|
|
|
|
const [similarIngredients, setSimilarIngredients] = useState<Array<{ id: string; name: string; similarity: number }>>([]);
|
2025-11-06 15:25:26 +00:00
|
|
|
|
|
|
|
|
|
|
const categoryOptions = [
|
|
|
|
|
|
'Baking Ingredients',
|
|
|
|
|
|
'Dairy',
|
|
|
|
|
|
'Fruits',
|
|
|
|
|
|
'Vegetables',
|
|
|
|
|
|
'Meat',
|
|
|
|
|
|
'Seafood',
|
|
|
|
|
|
'Spices',
|
|
|
|
|
|
'Other'
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const unitOptions = ['kg', 'g', 'L', 'ml', 'units', 'dozen'];
|
|
|
|
|
|
|
Implement 5 UX enhancements for ingredient management
This commit implements the requested enhancements for the ingredient
quick-add system and batch management:
**1. Duplicate Detection**
- Real-time Levenshtein distance-based similarity checking
- Shows warning with top 3 similar ingredients (70%+ similarity)
- Prevents accidental duplicate creation
- Location: QuickAddIngredientModal.tsx
**2. Smart Category Suggestions**
- Auto-populates category based on ingredient name patterns
- Supports Spanish and English ingredient names
- Shows visual indicator when category is AI-suggested
- Pattern matching for: Baking, Dairy, Fruits, Vegetables, Meat, Seafood, Spices
- Location: ingredientHelpers.ts
**3. Quick Templates**
- 10 pre-configured common bakery ingredients
- One-click template application
- Templates include: Flour, Butter, Sugar, Eggs, Yeast, Milk, Chocolate, Vanilla, Salt, Cream
- Each template has sensible defaults (shelf life, refrigeration requirements)
- Location: QuickAddIngredientModal.tsx
**4. Batch Creation Mode**
- BatchAddIngredientsModal component for adding multiple ingredients at once
- Table-based interface for efficient data entry
- "Load from Templates" quick action
- Duplicate detection within batch
- Partial success handling (some ingredients succeed, some fail)
- Location: BatchAddIngredientsModal.tsx
- Integration: UploadSalesDataStep.tsx (2 buttons: "Add One" / "Add Multiple")
**5. Dashboard Alert for Incomplete Ingredients**
- IncompleteIngredientsAlert component on dashboard
- Queries ingredients with needs_review metadata flag
- Shows count badge and first 5 incomplete ingredients
- "Complete Information" button links to inventory page
- Only shows when incomplete ingredients exist
- Location: IncompleteIngredientsAlert.tsx
- Integration: DashboardPage.tsx
**New Files Created:**
- ingredientHelpers.ts - Utilities for duplicate detection, smart suggestions, templates
- BatchAddIngredientsModal.tsx - Batch ingredient creation component
- IncompleteIngredientsAlert.tsx - Dashboard alert component
**Files Modified:**
- QuickAddIngredientModal.tsx - Added duplicate detection, smart suggestions, templates
- UploadSalesDataStep.tsx - Integrated batch creation modal
- DashboardPage.tsx - Added incomplete ingredients alert
**Technical Highlights:**
- Levenshtein distance algorithm for fuzzy name matching
- Pattern-based category suggestions (supports 100+ ingredient patterns)
- Metadata tracking (needs_review, created_context)
- Real-time validation and error handling
- Responsive UI with animations
- Consistent with existing design system
All features built and tested successfully.
Build time: 21.29s
2025-11-06 15:39:30 +00:00
|
|
|
|
// Check for duplicates when name changes
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (formData.name && formData.name.trim().length >= 3) {
|
|
|
|
|
|
const similar = findSimilarIngredients(
|
|
|
|
|
|
formData.name,
|
|
|
|
|
|
existingIngredients.map(ing => ({ id: ing.id, name: ing.name }))
|
|
|
|
|
|
);
|
|
|
|
|
|
setSimilarIngredients(similar);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setSimilarIngredients([]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [formData.name, existingIngredients]);
|
|
|
|
|
|
|
|
|
|
|
|
// Smart category suggestion when name changes
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (formData.name && formData.name.trim().length >= 3 && !formData.category) {
|
|
|
|
|
|
const suggested = suggestCategory(formData.name);
|
|
|
|
|
|
if (suggested) {
|
|
|
|
|
|
setFormData(prev => ({ ...prev, category: suggested }));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [formData.name]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleApplyTemplate = (template: IngredientTemplate) => {
|
|
|
|
|
|
setFormData({
|
|
|
|
|
|
...formData,
|
|
|
|
|
|
name: template.name,
|
|
|
|
|
|
category: template.category,
|
|
|
|
|
|
unit_of_measure: template.unit_of_measure,
|
|
|
|
|
|
estimated_shelf_life_days: template.estimated_shelf_life_days || 30,
|
|
|
|
|
|
requires_refrigeration: template.requires_refrigeration || false,
|
|
|
|
|
|
requires_freezing: template.requires_freezing || false,
|
|
|
|
|
|
});
|
|
|
|
|
|
setShowTemplates(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-06 15:25:26 +00:00
|
|
|
|
const validateForm = (): boolean => {
|
|
|
|
|
|
const newErrors: Record<string, string> = {};
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
Implement 5 UX enhancements for ingredient management
This commit implements the requested enhancements for the ingredient
quick-add system and batch management:
**1. Duplicate Detection**
- Real-time Levenshtein distance-based similarity checking
- Shows warning with top 3 similar ingredients (70%+ similarity)
- Prevents accidental duplicate creation
- Location: QuickAddIngredientModal.tsx
**2. Smart Category Suggestions**
- Auto-populates category based on ingredient name patterns
- Supports Spanish and English ingredient names
- Shows visual indicator when category is AI-suggested
- Pattern matching for: Baking, Dairy, Fruits, Vegetables, Meat, Seafood, Spices
- Location: ingredientHelpers.ts
**3. Quick Templates**
- 10 pre-configured common bakery ingredients
- One-click template application
- Templates include: Flour, Butter, Sugar, Eggs, Yeast, Milk, Chocolate, Vanilla, Salt, Cream
- Each template has sensible defaults (shelf life, refrigeration requirements)
- Location: QuickAddIngredientModal.tsx
**4. Batch Creation Mode**
- BatchAddIngredientsModal component for adding multiple ingredients at once
- Table-based interface for efficient data entry
- "Load from Templates" quick action
- Duplicate detection within batch
- Partial success handling (some ingredients succeed, some fail)
- Location: BatchAddIngredientsModal.tsx
- Integration: UploadSalesDataStep.tsx (2 buttons: "Add One" / "Add Multiple")
**5. Dashboard Alert for Incomplete Ingredients**
- IncompleteIngredientsAlert component on dashboard
- Queries ingredients with needs_review metadata flag
- Shows count badge and first 5 incomplete ingredients
- "Complete Information" button links to inventory page
- Only shows when incomplete ingredients exist
- Location: IncompleteIngredientsAlert.tsx
- Integration: DashboardPage.tsx
**New Files Created:**
- ingredientHelpers.ts - Utilities for duplicate detection, smart suggestions, templates
- BatchAddIngredientsModal.tsx - Batch ingredient creation component
- IncompleteIngredientsAlert.tsx - Dashboard alert component
**Files Modified:**
- QuickAddIngredientModal.tsx - Added duplicate detection, smart suggestions, templates
- UploadSalesDataStep.tsx - Integrated batch creation modal
- DashboardPage.tsx - Added incomplete ingredients alert
**Technical Highlights:**
- Levenshtein distance algorithm for fuzzy name matching
- Pattern-based category suggestions (supports 100+ ingredient patterns)
- Metadata tracking (needs_review, created_context)
- Real-time validation and error handling
- Responsive UI with animations
- Consistent with existing design system
All features built and tested successfully.
Build time: 21.29s
2025-11-06 15:39:30 +00:00
|
|
|
|
setShowTemplates(false);
|
|
|
|
|
|
setSimilarIngredients([]);
|
2025-11-06 15:25:26 +00:00
|
|
|
|
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 */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="fixed inset-0 bg-black/50 z-40 animate-fadeIn"
|
|
|
|
|
|
onClick={handleClose}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Modal */}
|
|
|
|
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 pointer-events-none">
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="bg-[var(--bg-primary)] rounded-lg shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto pointer-events-auto animate-slideUp"
|
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* Header */}
|
|
|
|
|
|
<div className="flex items-center justify-between p-6 border-b border-[var(--border-secondary)]">
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
<div className="w-10 h-10 rounded-full bg-[var(--color-primary)]/10 flex items-center justify-center">
|
|
|
|
|
|
<svg className="w-5 h-5 text-[var(--color-primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
|
|
|
|
|
|
⚡ Agregar Ingrediente Rápido
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)] mt-0.5">
|
|
|
|
|
|
{getContextMessage()}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleClose}
|
|
|
|
|
|
className="p-2 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] rounded-lg transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Form */}
|
|
|
|
|
|
<form onSubmit={handleSubmit} className="p-6 space-y-5">
|
Implement 5 UX enhancements for ingredient management
This commit implements the requested enhancements for the ingredient
quick-add system and batch management:
**1. Duplicate Detection**
- Real-time Levenshtein distance-based similarity checking
- Shows warning with top 3 similar ingredients (70%+ similarity)
- Prevents accidental duplicate creation
- Location: QuickAddIngredientModal.tsx
**2. Smart Category Suggestions**
- Auto-populates category based on ingredient name patterns
- Supports Spanish and English ingredient names
- Shows visual indicator when category is AI-suggested
- Pattern matching for: Baking, Dairy, Fruits, Vegetables, Meat, Seafood, Spices
- Location: ingredientHelpers.ts
**3. Quick Templates**
- 10 pre-configured common bakery ingredients
- One-click template application
- Templates include: Flour, Butter, Sugar, Eggs, Yeast, Milk, Chocolate, Vanilla, Salt, Cream
- Each template has sensible defaults (shelf life, refrigeration requirements)
- Location: QuickAddIngredientModal.tsx
**4. Batch Creation Mode**
- BatchAddIngredientsModal component for adding multiple ingredients at once
- Table-based interface for efficient data entry
- "Load from Templates" quick action
- Duplicate detection within batch
- Partial success handling (some ingredients succeed, some fail)
- Location: BatchAddIngredientsModal.tsx
- Integration: UploadSalesDataStep.tsx (2 buttons: "Add One" / "Add Multiple")
**5. Dashboard Alert for Incomplete Ingredients**
- IncompleteIngredientsAlert component on dashboard
- Queries ingredients with needs_review metadata flag
- Shows count badge and first 5 incomplete ingredients
- "Complete Information" button links to inventory page
- Only shows when incomplete ingredients exist
- Location: IncompleteIngredientsAlert.tsx
- Integration: DashboardPage.tsx
**New Files Created:**
- ingredientHelpers.ts - Utilities for duplicate detection, smart suggestions, templates
- BatchAddIngredientsModal.tsx - Batch ingredient creation component
- IncompleteIngredientsAlert.tsx - Dashboard alert component
**Files Modified:**
- QuickAddIngredientModal.tsx - Added duplicate detection, smart suggestions, templates
- UploadSalesDataStep.tsx - Integrated batch creation modal
- DashboardPage.tsx - Added incomplete ingredients alert
**Technical Highlights:**
- Levenshtein distance algorithm for fuzzy name matching
- Pattern-based category suggestions (supports 100+ ingredient patterns)
- Metadata tracking (needs_review, created_context)
- Real-time validation and error handling
- Responsive UI with animations
- Consistent with existing design system
All features built and tested successfully.
Build time: 21.29s
2025-11-06 15:39:30 +00:00
|
|
|
|
{/* Quick Templates */}
|
|
|
|
|
|
{!showTemplates && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setShowTemplates(!showTemplates)}
|
|
|
|
|
|
className="w-full p-3 mb-2 bg-gradient-to-r from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 border border-[var(--color-primary)]/20 rounded-lg hover:from-[var(--color-primary)]/10 hover:to-[var(--color-primary)]/15 transition-all group"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center justify-center gap-2 text-[var(--color-primary)]">
|
|
|
|
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span className="font-medium">⚡ Usar Plantilla Rápida</span>
|
|
|
|
|
|
<svg className="w-4 h-4 transition-transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{showTemplates && (
|
|
|
|
|
|
<div className="mb-4 p-4 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-secondary)] animate-slideDown">
|
|
|
|
|
|
<div className="flex items-center justify-between mb-3">
|
|
|
|
|
|
<h3 className="text-sm font-semibold text-[var(--text-primary)]">Plantillas Comunes</h3>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setShowTemplates(false)}
|
|
|
|
|
|
className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
|
|
|
|
>
|
|
|
|
|
|
Cerrar
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
|
{commonIngredientTemplates.map((template, index) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={index}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => handleApplyTemplate(template)}
|
|
|
|
|
|
className="p-2.5 bg-[var(--bg-primary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-secondary)] hover:border-[var(--color-primary)] rounded-lg transition-all text-left group"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<span className="text-xl">{template.icon}</span>
|
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
|
<div className="text-sm font-medium text-[var(--text-primary)] truncate group-hover:text-[var(--color-primary)]">
|
|
|
|
|
|
{template.name}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-xs text-[var(--text-tertiary)]">
|
|
|
|
|
|
{template.category}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-11-06 15:25:26 +00:00
|
|
|
|
{/* Required Fields */}
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
|
|
|
|
|
|
Nombre del Ingrediente *
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={formData.name}
|
|
|
|
|
|
onChange={(e) => 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 && (
|
|
|
|
|
|
<p className="text-xs text-[var(--color-error)] mt-1.5 flex items-center gap-1">
|
|
|
|
|
|
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
|
|
|
|
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
{errors.name}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
Implement 5 UX enhancements for ingredient management
This commit implements the requested enhancements for the ingredient
quick-add system and batch management:
**1. Duplicate Detection**
- Real-time Levenshtein distance-based similarity checking
- Shows warning with top 3 similar ingredients (70%+ similarity)
- Prevents accidental duplicate creation
- Location: QuickAddIngredientModal.tsx
**2. Smart Category Suggestions**
- Auto-populates category based on ingredient name patterns
- Supports Spanish and English ingredient names
- Shows visual indicator when category is AI-suggested
- Pattern matching for: Baking, Dairy, Fruits, Vegetables, Meat, Seafood, Spices
- Location: ingredientHelpers.ts
**3. Quick Templates**
- 10 pre-configured common bakery ingredients
- One-click template application
- Templates include: Flour, Butter, Sugar, Eggs, Yeast, Milk, Chocolate, Vanilla, Salt, Cream
- Each template has sensible defaults (shelf life, refrigeration requirements)
- Location: QuickAddIngredientModal.tsx
**4. Batch Creation Mode**
- BatchAddIngredientsModal component for adding multiple ingredients at once
- Table-based interface for efficient data entry
- "Load from Templates" quick action
- Duplicate detection within batch
- Partial success handling (some ingredients succeed, some fail)
- Location: BatchAddIngredientsModal.tsx
- Integration: UploadSalesDataStep.tsx (2 buttons: "Add One" / "Add Multiple")
**5. Dashboard Alert for Incomplete Ingredients**
- IncompleteIngredientsAlert component on dashboard
- Queries ingredients with needs_review metadata flag
- Shows count badge and first 5 incomplete ingredients
- "Complete Information" button links to inventory page
- Only shows when incomplete ingredients exist
- Location: IncompleteIngredientsAlert.tsx
- Integration: DashboardPage.tsx
**New Files Created:**
- ingredientHelpers.ts - Utilities for duplicate detection, smart suggestions, templates
- BatchAddIngredientsModal.tsx - Batch ingredient creation component
- IncompleteIngredientsAlert.tsx - Dashboard alert component
**Files Modified:**
- QuickAddIngredientModal.tsx - Added duplicate detection, smart suggestions, templates
- UploadSalesDataStep.tsx - Integrated batch creation modal
- DashboardPage.tsx - Added incomplete ingredients alert
**Technical Highlights:**
- Levenshtein distance algorithm for fuzzy name matching
- Pattern-based category suggestions (supports 100+ ingredient patterns)
- Metadata tracking (needs_review, created_context)
- Real-time validation and error handling
- Responsive UI with animations
- Consistent with existing design system
All features built and tested successfully.
Build time: 21.29s
2025-11-06 15:39:30 +00:00
|
|
|
|
|
|
|
|
|
|
{/* Duplicate Detection Warning */}
|
|
|
|
|
|
{similarIngredients.length > 0 && (
|
|
|
|
|
|
<div className="mt-2 p-2.5 bg-[var(--color-warning)]/10 border border-[var(--color-warning)]/30 rounded-lg animate-slideDown">
|
|
|
|
|
|
<div className="flex items-start gap-2">
|
|
|
|
|
|
<svg className="w-4 h-4 text-[var(--color-warning)] mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
|
<p className="text-xs font-medium text-[var(--color-warning)] mb-1">
|
|
|
|
|
|
⚠️ Ingredientes similares encontrados:
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<ul className="text-xs text-[var(--text-secondary)] space-y-0.5">
|
|
|
|
|
|
{similarIngredients.map((similar) => (
|
|
|
|
|
|
<li key={similar.id} className="flex items-center gap-2">
|
|
|
|
|
|
<span className="font-medium">{similar.name}</span>
|
|
|
|
|
|
<span className="text-[var(--text-tertiary)]">
|
|
|
|
|
|
({similar.similarity}% similar)
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
<p className="text-xs text-[var(--text-tertiary)] mt-1.5">
|
|
|
|
|
|
Verifica que no sea un duplicado antes de continuar.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-11-06 15:25:26 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
|
|
|
|
|
|
Categoría *
|
Implement 5 UX enhancements for ingredient management
This commit implements the requested enhancements for the ingredient
quick-add system and batch management:
**1. Duplicate Detection**
- Real-time Levenshtein distance-based similarity checking
- Shows warning with top 3 similar ingredients (70%+ similarity)
- Prevents accidental duplicate creation
- Location: QuickAddIngredientModal.tsx
**2. Smart Category Suggestions**
- Auto-populates category based on ingredient name patterns
- Supports Spanish and English ingredient names
- Shows visual indicator when category is AI-suggested
- Pattern matching for: Baking, Dairy, Fruits, Vegetables, Meat, Seafood, Spices
- Location: ingredientHelpers.ts
**3. Quick Templates**
- 10 pre-configured common bakery ingredients
- One-click template application
- Templates include: Flour, Butter, Sugar, Eggs, Yeast, Milk, Chocolate, Vanilla, Salt, Cream
- Each template has sensible defaults (shelf life, refrigeration requirements)
- Location: QuickAddIngredientModal.tsx
**4. Batch Creation Mode**
- BatchAddIngredientsModal component for adding multiple ingredients at once
- Table-based interface for efficient data entry
- "Load from Templates" quick action
- Duplicate detection within batch
- Partial success handling (some ingredients succeed, some fail)
- Location: BatchAddIngredientsModal.tsx
- Integration: UploadSalesDataStep.tsx (2 buttons: "Add One" / "Add Multiple")
**5. Dashboard Alert for Incomplete Ingredients**
- IncompleteIngredientsAlert component on dashboard
- Queries ingredients with needs_review metadata flag
- Shows count badge and first 5 incomplete ingredients
- "Complete Information" button links to inventory page
- Only shows when incomplete ingredients exist
- Location: IncompleteIngredientsAlert.tsx
- Integration: DashboardPage.tsx
**New Files Created:**
- ingredientHelpers.ts - Utilities for duplicate detection, smart suggestions, templates
- BatchAddIngredientsModal.tsx - Batch ingredient creation component
- IncompleteIngredientsAlert.tsx - Dashboard alert component
**Files Modified:**
- QuickAddIngredientModal.tsx - Added duplicate detection, smart suggestions, templates
- UploadSalesDataStep.tsx - Integrated batch creation modal
- DashboardPage.tsx - Added incomplete ingredients alert
**Technical Highlights:**
- Levenshtein distance algorithm for fuzzy name matching
- Pattern-based category suggestions (supports 100+ ingredient patterns)
- Metadata tracking (needs_review, created_context)
- Real-time validation and error handling
- Responsive UI with animations
- Consistent with existing design system
All features built and tested successfully.
Build time: 21.29s
2025-11-06 15:39:30 +00:00
|
|
|
|
{formData.category && suggestCategory(formData.name) === formData.category && (
|
|
|
|
|
|
<span className="ml-2 text-xs text-[var(--color-success)] font-normal">
|
|
|
|
|
|
✨ Sugerido automáticamente
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
2025-11-06 15:25:26 +00:00
|
|
|
|
</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={formData.category}
|
|
|
|
|
|
onChange={(e) => setFormData({ ...formData, category: 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"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="">Seleccionar...</option>
|
|
|
|
|
|
{categoryOptions.map(cat => (
|
|
|
|
|
|
<option key={cat} value={cat}>{cat}</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
{errors.category && (
|
|
|
|
|
|
<p className="text-xs text-[var(--color-error)] mt-1.5">{errors.category}</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
|
|
|
|
|
|
Unidad de Medida *
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={formData.unit_of_measure}
|
|
|
|
|
|
onChange={(e) => setFormData({ ...formData, unit_of_measure: 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"
|
|
|
|
|
|
>
|
|
|
|
|
|
{unitOptions.map(unit => (
|
|
|
|
|
|
<option key={unit} value={unit}>{unit}</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
{errors.unit_of_measure && (
|
|
|
|
|
|
<p className="text-xs text-[var(--color-error)] mt-1.5">{errors.unit_of_measure}</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Optional Fields Toggle */}
|
|
|
|
|
|
<div className="border-t border-[var(--border-secondary)] pt-4">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setShowOptionalFields(!showOptionalFields)}
|
|
|
|
|
|
className="flex items-center justify-between w-full p-3 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors group"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<svg
|
|
|
|
|
|
className={`w-4 h-4 text-[var(--text-secondary)] transition-transform ${showOptionalFields ? 'rotate-90' : ''}`}
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span className="text-sm font-medium text-[var(--text-primary)]">
|
|
|
|
|
|
Detalles Adicionales (Opcional)
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span className="text-xs text-[var(--text-tertiary)] group-hover:text-[var(--text-secondary)]">
|
|
|
|
|
|
{showOptionalFields ? 'Ocultar' : 'Mostrar'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Optional Fields */}
|
|
|
|
|
|
{showOptionalFields && (
|
|
|
|
|
|
<div className="mt-4 space-y-4 animate-slideDown">
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
|
|
|
|
|
|
Stock Inicial
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="0"
|
|
|
|
|
|
step="0.01"
|
|
|
|
|
|
value={formData.stock_quantity}
|
|
|
|
|
|
onChange={(e) => 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 && (
|
|
|
|
|
|
<p className="text-xs text-[var(--color-error)] mt-1.5">{errors.stock_quantity}</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
|
|
|
|
|
|
Costo por Unidad (€)
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="0"
|
|
|
|
|
|
step="0.01"
|
|
|
|
|
|
value={formData.cost_per_unit}
|
|
|
|
|
|
onChange={(e) => 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 && (
|
|
|
|
|
|
<p className="text-xs text-[var(--color-error)] mt-1.5">{errors.cost_per_unit}</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
|
|
|
|
|
|
Días de Caducidad
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="1"
|
|
|
|
|
|
value={formData.estimated_shelf_life_days}
|
|
|
|
|
|
onChange={(e) => 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 && (
|
|
|
|
|
|
<p className="text-xs text-[var(--color-error)] mt-1.5">{errors.estimated_shelf_life_days}</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
|
|
|
|
|
|
Punto de Reorden
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="0"
|
|
|
|
|
|
value={formData.reorder_point}
|
|
|
|
|
|
onChange={(e) => 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)]"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-4 flex-wrap">
|
|
|
|
|
|
<label className="flex items-center gap-2 text-sm text-[var(--text-primary)] cursor-pointer">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={formData.requires_refrigeration}
|
|
|
|
|
|
onChange={(e) => setFormData({ ...formData, requires_refrigeration: e.target.checked })}
|
|
|
|
|
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
|
|
|
|
|
/>
|
|
|
|
|
|
❄️ Refrigeración
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label className="flex items-center gap-2 text-sm text-[var(--text-primary)] cursor-pointer">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={formData.requires_freezing}
|
|
|
|
|
|
onChange={(e) => setFormData({ ...formData, requires_freezing: e.target.checked })}
|
|
|
|
|
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
|
|
|
|
|
/>
|
|
|
|
|
|
🧊 Congelación
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label className="flex items-center gap-2 text-sm text-[var(--text-primary)] cursor-pointer">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={formData.is_seasonal}
|
|
|
|
|
|
onChange={(e) => setFormData({ ...formData, is_seasonal: e.target.checked })}
|
|
|
|
|
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
|
|
|
|
|
/>
|
|
|
|
|
|
🌿 Estacional
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Info Box */}
|
|
|
|
|
|
{!showOptionalFields && (
|
|
|
|
|
|
<div className="bg-[var(--color-info)]/10 border border-[var(--color-info)]/20 rounded-lg p-3">
|
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)] flex items-start gap-2">
|
|
|
|
|
|
<svg className="w-4 h-4 text-[var(--color-info)] mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span>
|
|
|
|
|
|
💡 Puedes completar los detalles de stock y costos después en la gestión de inventario.
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Error Message */}
|
|
|
|
|
|
{errors.submit && (
|
|
|
|
|
|
<div className="bg-[var(--color-error)]/10 border border-[var(--color-error)]/20 rounded-lg p-3">
|
|
|
|
|
|
<p className="text-sm text-[var(--color-error)] flex items-center gap-2">
|
|
|
|
|
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
|
|
|
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
{errors.submit}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Actions */}
|
|
|
|
|
|
<div className="flex gap-3 pt-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={handleClose}
|
|
|
|
|
|
disabled={isSubmitting}
|
|
|
|
|
|
className="flex-1 px-4 py-2.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] rounded-lg transition-colors font-medium disabled:opacity-50"
|
|
|
|
|
|
>
|
|
|
|
|
|
Cancelar
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="submit"
|
|
|
|
|
|
disabled={isSubmitting}
|
|
|
|
|
|
className="flex-1 px-4 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium flex items-center justify-center gap-2"
|
|
|
|
|
|
>
|
|
|
|
|
|
{isSubmitting ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
|
|
|
|
|
Agregando...
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
{getCtaText()}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Animation Styles */}
|
|
|
|
|
|
<style>{`
|
|
|
|
|
|
@keyframes fadeIn {
|
|
|
|
|
|
from { opacity: 0; }
|
|
|
|
|
|
to { opacity: 1; }
|
|
|
|
|
|
}
|
|
|
|
|
|
@keyframes slideUp {
|
|
|
|
|
|
from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateY(20px);
|
|
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
@keyframes slideDown {
|
|
|
|
|
|
from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
max-height: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
max-height: 500px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.animate-fadeIn {
|
|
|
|
|
|
animation: fadeIn 0.2s ease-out;
|
|
|
|
|
|
}
|
|
|
|
|
|
.animate-slideUp {
|
|
|
|
|
|
animation: slideUp 0.3s ease-out;
|
|
|
|
|
|
}
|
|
|
|
|
|
.animate-slideDown {
|
|
|
|
|
|
animation: slideDown 0.3s ease-out;
|
|
|
|
|
|
}
|
|
|
|
|
|
`}</style>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|