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
This commit is contained in:
Claude
2025-11-06 15:39:30 +00:00
parent 9162fc32a5
commit 000e352ef9
6 changed files with 987 additions and 16 deletions

View File

@@ -0,0 +1,107 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { useIngredients } from '../../../api/hooks/inventory';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { AlertTriangle, ExternalLink } from 'lucide-react';
export const IncompleteIngredientsAlert: React.FC = () => {
const navigate = useNavigate();
const currentTenant = useCurrentTenant();
// Fetch all ingredients
const { data: ingredients = [], isLoading } = useIngredients(currentTenant?.id || '', {}, {
enabled: !!currentTenant?.id
});
// Filter ingredients that need review (created via quick add or batch with incomplete data)
const incompleteIngredients = React.useMemo(() => {
return ingredients.filter(ing => {
// Check metadata for needs_review flag
const metadata = ing.metadata as any;
return metadata?.needs_review === true;
});
}, [ingredients]);
// Don't show if no incomplete ingredients or still loading
if (isLoading || incompleteIngredients.length === 0) {
return null;
}
const handleViewIncomplete = () => {
// Navigate to inventory page
// TODO: In the future, this could pass a filter parameter to show only incomplete items
navigate('/app/operations/inventory');
};
return (
<div className="bg-gradient-to-r from-[var(--color-warning)]/10 to-[var(--color-warning)]/5 border border-[var(--color-warning)]/30 rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow">
<div className="flex items-start gap-4">
{/* Icon */}
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-[var(--color-warning)]/20 flex items-center justify-center">
<AlertTriangle className="w-5 h-5 text-[var(--color-warning)]" />
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-base font-semibold text-[var(--text-primary)]">
Ingredientes con información incompleta
</h3>
<span className="px-2 py-0.5 bg-[var(--color-warning)] text-white text-xs font-bold rounded-full">
{incompleteIngredients.length}
</span>
</div>
<p className="text-sm text-[var(--text-secondary)] mb-3">
{incompleteIngredients.length === 1
? 'Hay 1 ingrediente que fue agregado rápidamente y necesita información completa.'
: `Hay ${incompleteIngredients.length} ingredientes que fueron agregados rápidamente y necesitan información completa.`}
</p>
{/* Incomplete ingredients list */}
<div className="mb-3 flex flex-wrap gap-2">
{incompleteIngredients.slice(0, 5).map((ing) => (
<span
key={ing.id}
className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-md text-xs text-[var(--text-primary)]"
>
<span className="font-medium">{ing.name}</span>
<span className="text-[var(--text-tertiary)]">({ing.category})</span>
</span>
))}
{incompleteIngredients.length > 5 && (
<span className="inline-flex items-center px-2.5 py-1 text-xs text-[var(--text-secondary)]">
+{incompleteIngredients.length - 5} más
</span>
)}
</div>
{/* What's missing info box */}
<div className="mb-3 p-2.5 bg-[var(--bg-secondary)] rounded-md border border-[var(--border-secondary)]">
<p className="text-xs text-[var(--text-secondary)]">
<span className="font-medium text-[var(--text-primary)]">Información faltante típica:</span>
{' '}Stock inicial, costo por unidad, vida útil, punto de reorden, requisitos de almacenamiento
</p>
</div>
{/* Action button */}
<button
onClick={handleViewIncomplete}
className="inline-flex items-center gap-2 px-4 py-2 bg-[var(--color-warning)] hover:bg-[var(--color-warning-dark)] text-white rounded-lg transition-colors font-medium text-sm"
>
<span>Completar Información</span>
<ExternalLink className="w-4 h-4" />
</button>
</div>
{/* Dismiss button (optional - could be added later) */}
{/* <button
className="flex-shrink-0 p-1 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] rounded transition-colors"
title="Dismiss"
>
<X className="w-4 h-4" />
</button> */}
</div>
</div>
);
};