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:
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user