Files
bakery-ia/fdev-ffrontend/src/components/recipes/IngredientList.tsx
2025-08-28 10:41:04 +02:00

323 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;