Files
bakery-ia/fdev-ffrontend/src/components/recipes/IngredientList.tsx

323 lines
11 KiB
TypeScript
Raw Normal View History

// frontend/src/components/recipes/IngredientList.tsx
import React from 'react';
import {
Plus,
Minus,
Edit2,
Trash2,
GripVertical,
Info,
AlertCircle,
Package,
Droplets,
Scale,
Euro
} from 'lucide-react';
import { RecipeIngredient } from '../../api/services/recipes.service';
interface IngredientListProps {
ingredients: RecipeIngredient[];
editable?: boolean;
showCosts?: boolean;
showGroups?: boolean;
batchMultiplier?: number;
onAddIngredient?: () => void;
onEditIngredient?: (ingredient: RecipeIngredient) => void;
onRemoveIngredient?: (ingredientId: string) => void;
onReorderIngredients?: (ingredients: RecipeIngredient[]) => void;
className?: string;
}
const IngredientList: React.FC<IngredientListProps> = ({
ingredients,
editable = false,
showCosts = false,
showGroups = true,
batchMultiplier = 1,
onAddIngredient,
onEditIngredient,
onRemoveIngredient,
onReorderIngredients,
className = ''
}) => {
// Group ingredients by ingredient_group
const groupedIngredients = React.useMemo(() => {
if (!showGroups) {
return { 'All Ingredients': ingredients };
}
const groups: Record<string, RecipeIngredient[]> = {};
ingredients.forEach(ingredient => {
const group = ingredient.ingredient_group || 'Other';
if (!groups[group]) {
groups[group] = [];
}
groups[group].push(ingredient);
});
// Sort ingredients within each group by order
Object.keys(groups).forEach(group => {
groups[group].sort((a, b) => a.ingredient_order - b.ingredient_order);
});
return groups;
}, [ingredients, showGroups]);
// Get unit icon
const getUnitIcon = (unit: string) => {
switch (unit.toLowerCase()) {
case 'g':
case 'kg':
return <Scale className="w-4 h-4" />;
case 'ml':
case 'l':
return <Droplets className="w-4 h-4" />;
case 'units':
case 'pieces':
case 'pcs':
return <Package className="w-4 h-4" />;
default:
return <Scale className="w-4 h-4" />;
}
};
// Format quantity with multiplier
const formatQuantity = (quantity: number, unit: string) => {
const adjustedQuantity = quantity * batchMultiplier;
return `${adjustedQuantity} ${unit}`;
};
// Calculate total cost
const getTotalCost = () => {
return ingredients.reduce((total, ingredient) => {
const cost = ingredient.total_cost || 0;
return total + (cost * batchMultiplier);
}, 0);
};
return (
<div className={`bg-white rounded-lg border ${className}`}>
{/* Header */}
<div className="p-4 border-b bg-gray-50 rounded-t-lg">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-gray-900">Ingredients</h3>
<p className="text-sm text-gray-600">
{ingredients.length} ingredient{ingredients.length !== 1 ? 's' : ''}
{batchMultiplier !== 1 && (
<span className="ml-2 px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">
×{batchMultiplier} batch
</span>
)}
</p>
</div>
<div className="flex items-center space-x-2">
{showCosts && (
<div className="text-right">
<div className="text-sm text-gray-600">Total Cost</div>
<div className="text-lg font-semibold text-gray-900 flex items-center">
<Euro className="w-4 h-4 mr-1" />
{getTotalCost().toFixed(2)}
</div>
</div>
)}
{editable && onAddIngredient && (
<button
onClick={onAddIngredient}
className="px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2"
>
<Plus className="w-4 h-4" />
<span>Add Ingredient</span>
</button>
)}
</div>
</div>
</div>
{/* Ingredients List */}
<div className="divide-y">
{Object.entries(groupedIngredients).map(([groupName, groupIngredients]) => (
<div key={groupName}>
{/* Group Header */}
{showGroups && Object.keys(groupedIngredients).length > 1 && (
<div className="px-4 py-2 bg-gray-25 border-b">
<h4 className="text-sm font-medium text-gray-700 uppercase tracking-wide">
{groupName}
</h4>
</div>
)}
{/* Group Ingredients */}
{groupIngredients.map((ingredient, index) => (
<div
key={ingredient.id}
className="p-4 hover:bg-gray-50 transition-colors"
>
<div className="flex items-center space-x-4">
{/* Drag Handle */}
{editable && onReorderIngredients && (
<div className="cursor-move text-gray-400 hover:text-gray-600">
<GripVertical className="w-4 h-4" />
</div>
)}
{/* Order Number */}
<div className="w-6 h-6 bg-gray-200 rounded-full flex items-center justify-center text-xs font-medium text-gray-600">
{ingredient.ingredient_order}
</div>
{/* Ingredient Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-1">
<h4 className="font-medium text-gray-900">
{ingredient.ingredient_id} {/* This would be ingredient name from inventory */}
</h4>
{ingredient.is_optional && (
<span className="px-2 py-1 bg-yellow-100 text-yellow-800 rounded text-xs">
Optional
</span>
)}
</div>
{/* Quantity */}
<div className="flex items-center space-x-2 text-sm text-gray-600">
{getUnitIcon(ingredient.unit)}
<span className="font-medium">
{formatQuantity(ingredient.quantity, ingredient.unit)}
</span>
{ingredient.alternative_quantity && ingredient.alternative_unit && (
<span className="text-gray-500">
( {formatQuantity(ingredient.alternative_quantity, ingredient.alternative_unit)})
</span>
)}
</div>
{/* Preparation Method */}
{ingredient.preparation_method && (
<div className="text-sm text-gray-600 mt-1">
<span className="font-medium">Prep:</span> {ingredient.preparation_method}
</div>
)}
{/* Notes */}
{ingredient.ingredient_notes && (
<div className="text-sm text-gray-600 mt-1 flex items-start">
<Info className="w-3 h-3 mr-1 mt-0.5 flex-shrink-0" />
<span>{ingredient.ingredient_notes}</span>
</div>
)}
{/* Substitutions */}
{ingredient.substitution_options && (
<div className="text-sm text-blue-600 mt-1">
<span className="font-medium">Substitutions available</span>
</div>
)}
</div>
{/* Cost */}
{showCosts && (
<div className="text-right">
<div className="text-sm font-medium text-gray-900">
{((ingredient.total_cost || 0) * batchMultiplier).toFixed(2)}
</div>
{ingredient.unit_cost && (
<div className="text-xs text-gray-600">
{ingredient.unit_cost.toFixed(2)}/{ingredient.unit}
</div>
)}
{ingredient.cost_updated_at && (
<div className="text-xs text-gray-500">
{new Date(ingredient.cost_updated_at).toLocaleDateString()}
</div>
)}
</div>
)}
{/* Actions */}
{editable && (
<div className="flex items-center space-x-2">
<button
onClick={() => onEditIngredient?.(ingredient)}
className="p-1 text-gray-400 hover:text-blue-600 transition-colors"
title="Edit ingredient"
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => onRemoveIngredient?.(ingredient.id)}
className="p-1 text-gray-400 hover:text-red-600 transition-colors"
title="Remove ingredient"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
)}
</div>
</div>
))}
</div>
))}
{/* Empty State */}
{ingredients.length === 0 && (
<div className="p-8 text-center">
<Package className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No ingredients yet</h3>
<p className="text-gray-600 mb-4">
Add ingredients to start building your recipe
</p>
{editable && onAddIngredient && (
<button
onClick={onAddIngredient}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Add First Ingredient
</button>
)}
</div>
)}
</div>
{/* Summary */}
{ingredients.length > 0 && (
<div className="p-4 bg-gray-50 border-t rounded-b-lg">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center space-x-4">
<span className="text-gray-600">
{ingredients.length} total ingredients
</span>
{ingredients.filter(i => i.is_optional).length > 0 && (
<span className="text-yellow-600">
{ingredients.filter(i => i.is_optional).length} optional
</span>
)}
{ingredients.some(i => i.substitution_options) && (
<span className="text-blue-600">
{ingredients.filter(i => i.substitution_options).length} with substitutions
</span>
)}
</div>
{showCosts && (
<div className="font-medium text-gray-900">
Total: {getTotalCost().toFixed(2)}
</div>
)}
</div>
</div>
)}
</div>
);
};
export default IngredientList;