Add inventory lot management to onboarding with expiration tracking

Implements Phases 1 & 2 from proposal-inventory-lots-onboarding.md:

**Phase 1 - MVP (Inline Stock Entry):**
- Add stock lots immediately after creating ingredients
- Fields: quantity (required), expiration date, supplier, batch/lot number
- Smart validation with expiration date warnings
- Auto-select supplier if only one exists
- Optional but encouraged with clear skip option
- Help text about FIFO and waste prevention

**Phase 2 - Multi-Lot Support:**
- "Add Another Lot" functionality for multiple lots per ingredient
- Visual list of all lots added with expiration dates
- Delete individual lots before completing setup
- Lot count badge on ingredients with stock

**JTBD Alignment:**
- Addresses "Set up foundational data correctly" (lines 100-104)
- Reduces waste and inefficiency (lines 159-162)
- Enables real-time inventory tracking from day one (lines 173-178)
- Mitigates anxiety about complexity with optional, inline approach

**Technical Implementation:**
- Reuses existing useAddStock hook and StockCreate/StockResponse types
- Production stage defaulted to RAW_INGREDIENT
- Quality status defaulted to 'good'
- Local state management for added lots display
- Inline forms show contextually after each ingredient

Related: frontend/src/components/domain/setup-wizard/steps/InventorySetupStep.tsx:52-322
This commit is contained in:
Claude
2025-11-06 20:15:47 +00:00
parent 376cdc73e1
commit ab0a79060d

View File

@@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { SetupStepProps } from '../SetupWizard'; import type { SetupStepProps } from '../SetupWizard';
import { useIngredients, useCreateIngredient, useUpdateIngredient, useSoftDeleteIngredient } from '../../../../api/hooks/inventory'; import { useIngredients, useCreateIngredient, useUpdateIngredient, useSoftDeleteIngredient, useAddStock, useStockByIngredient } from '../../../../api/hooks/inventory';
import { useSuppliers } from '../../../../api/hooks/suppliers';
import { useCurrentTenant } from '../../../../stores/tenant.store'; import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAuthUser } from '../../../../stores/auth.store'; import { useAuthUser } from '../../../../stores/auth.store';
import { UnitOfMeasure, IngredientCategory } from '../../../../api/types/inventory'; import { UnitOfMeasure, IngredientCategory, ProductionStage } from '../../../../api/types/inventory';
import type { IngredientCreate, IngredientUpdate } from '../../../../api/types/inventory'; import type { IngredientCreate, IngredientUpdate, StockCreate, StockResponse } from '../../../../api/types/inventory';
import { ESSENTIAL_INGREDIENTS, COMMON_INGREDIENTS, PACKAGING_ITEMS, type IngredientTemplate, templateToIngredientCreate } from '../data/ingredientTemplates'; import { ESSENTIAL_INGREDIENTS, COMMON_INGREDIENTS, PACKAGING_ITEMS, type IngredientTemplate, templateToIngredientCreate } from '../data/ingredientTemplates';
export const InventorySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplete, canContinue }) => { export const InventorySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplete, canContinue }) => {
@@ -20,10 +21,15 @@ export const InventorySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onCompl
const { data: ingredientsData, isLoading } = useIngredients(tenantId); const { data: ingredientsData, isLoading } = useIngredients(tenantId);
const ingredients = ingredientsData || []; const ingredients = ingredientsData || [];
// Fetch suppliers for stock entry
const { data: suppliersData } = useSuppliers(tenantId, { limit: 100 }, { enabled: !!tenantId });
const suppliers = (suppliersData || []).filter(s => s.status === 'active');
// Mutations // Mutations
const createIngredientMutation = useCreateIngredient(); const createIngredientMutation = useCreateIngredient();
const updateIngredientMutation = useUpdateIngredient(); const updateIngredientMutation = useUpdateIngredient();
const deleteIngredientMutation = useSoftDeleteIngredient(); const deleteIngredientMutation = useSoftDeleteIngredient();
const addStockMutation = useAddStock();
// Form state // Form state
const [isAdding, setIsAdding] = useState(false); const [isAdding, setIsAdding] = useState(false);
@@ -42,6 +48,20 @@ export const InventorySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onCompl
const [showTemplates, setShowTemplates] = useState(ingredients.length === 0); const [showTemplates, setShowTemplates] = useState(ingredients.length === 0);
const [isImporting, setIsImporting] = useState(false); const [isImporting, setIsImporting] = useState(false);
// Stock entry state
const [addingStockForId, setAddingStockForId] = useState<string | null>(null);
const [stockFormData, setStockFormData] = useState({
current_quantity: '',
expiration_date: '',
supplier_id: '',
batch_number: '',
lot_number: '',
});
const [stockErrors, setStockErrors] = useState<Record<string, string>>({});
// Track stocks added per ingredient (for displaying the list)
const [ingredientStocks, setIngredientStocks] = useState<Record<string, StockResponse[]>>({});
// Notify parent when count changes // Notify parent when count changes
useEffect(() => { useEffect(() => {
const count = ingredients.length; const count = ingredients.length;
@@ -195,6 +215,112 @@ export const InventorySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onCompl
setShowTemplates(false); setShowTemplates(false);
}; };
// Stock entry handlers
const handleAddStockClick = (ingredientId: string) => {
setAddingStockForId(ingredientId);
setStockFormData({
current_quantity: '',
expiration_date: '',
supplier_id: suppliers.length === 1 ? suppliers[0].id : '',
batch_number: '',
lot_number: '',
});
setStockErrors({});
};
const handleCancelStock = () => {
setAddingStockForId(null);
setStockFormData({
current_quantity: '',
expiration_date: '',
supplier_id: '',
batch_number: '',
lot_number: '',
});
setStockErrors({});
};
const validateStockForm = (): boolean => {
const newErrors: Record<string, string> = {};
if (!stockFormData.current_quantity || Number(stockFormData.current_quantity) <= 0) {
newErrors.current_quantity = t('setup_wizard:inventory.stock_errors.quantity_required', 'Quantity must be greater than zero');
}
if (stockFormData.expiration_date) {
const expDate = new Date(stockFormData.expiration_date);
const today = new Date();
today.setHours(0, 0, 0, 0);
if (expDate < today) {
newErrors.expiration_date = t('setup_wizard:inventory.stock_errors.expiration_past', 'Expiration date is in the past');
}
const threeDaysFromNow = new Date(today);
threeDaysFromNow.setDate(threeDaysFromNow.getDate() + 3);
if (expDate < threeDaysFromNow) {
newErrors.expiration_warning = t('setup_wizard:inventory.stock_errors.expiring_soon', 'Warning: This ingredient expires very soon!');
}
}
setStockErrors(newErrors);
return Object.keys(newErrors).filter(k => k !== 'expiration_warning').length === 0;
};
const handleSaveStock = async (addAnother: boolean = false) => {
if (!addingStockForId || !validateStockForm()) return;
try {
const stockData: StockCreate = {
ingredient_id: addingStockForId,
current_quantity: Number(stockFormData.current_quantity),
expiration_date: stockFormData.expiration_date || undefined,
supplier_id: stockFormData.supplier_id || undefined,
batch_number: stockFormData.batch_number || undefined,
lot_number: stockFormData.lot_number || undefined,
production_stage: ProductionStage.RAW_INGREDIENT,
quality_status: 'good',
};
const result = await addStockMutation.mutateAsync({
tenantId,
stockData,
});
// Add to local state for display
setIngredientStocks(prev => ({
...prev,
[addingStockForId]: [...(prev[addingStockForId] || []), result],
}));
if (addAnother) {
// Reset form for adding another lot
setStockFormData({
current_quantity: '',
expiration_date: '',
supplier_id: stockFormData.supplier_id, // Keep supplier selected
batch_number: '',
lot_number: '',
});
setStockErrors({});
} else {
handleCancelStock();
}
} catch (error) {
console.error('Error adding stock:', error);
setStockErrors({ submit: t('common:error_saving', 'Error saving. Please try again.') });
}
};
const handleDeleteStock = async (ingredientId: string, stockId: string) => {
// Remove from local state
setIngredientStocks(prev => ({
...prev,
[ingredientId]: (prev[ingredientId] || []).filter(s => s.id !== stockId),
}));
// Note: We don't delete from backend during setup - stocks are created and can be managed later
};
const categoryOptions = [ const categoryOptions = [
{ value: IngredientCategory.FLOUR, label: t('inventory:category.flour', 'Flour') }, { value: IngredientCategory.FLOUR, label: t('inventory:category.flour', 'Flour') },
{ value: IngredientCategory.YEAST, label: t('inventory:category.yeast', 'Yeast') }, { value: IngredientCategory.YEAST, label: t('inventory:category.yeast', 'Yeast') },
@@ -448,59 +574,270 @@ export const InventorySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onCompl
<h4 className="text-sm font-medium text-[var(--text-secondary)]"> <h4 className="text-sm font-medium text-[var(--text-secondary)]">
{t('setup_wizard:inventory.your_ingredients', 'Your Ingredients')} {t('setup_wizard:inventory.your_ingredients', 'Your Ingredients')}
</h4> </h4>
<div className="space-y-2 max-h-96 overflow-y-auto"> <div className="space-y-4 max-h-96 overflow-y-auto">
{ingredients.map((ingredient) => ( {ingredients.map((ingredient) => {
<div const stocks = ingredientStocks[ingredient.id] || [];
key={ingredient.id} const isAddingStock = addingStockForId === ingredient.id;
className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg hover:border-[var(--border-primary)] transition-colors"
> return (
<div className="flex-1 min-w-0"> <div key={ingredient.id} className="space-y-2">
<div className="flex items-center gap-2"> {/* Ingredient Header */}
<h5 className="font-medium text-[var(--text-primary)] truncate">{ingredient.name}</h5> <div className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg hover:border-[var(--border-primary)] transition-colors">
{ingredient.brand && ( <div className="flex-1 min-w-0">
<span className="text-xs text-[var(--text-tertiary)]">({ingredient.brand})</span> <div className="flex items-center gap-2">
)} <h5 className="font-medium text-[var(--text-primary)] truncate">{ingredient.name}</h5>
</div> {ingredient.brand && (
<div className="flex items-center gap-3 mt-1 text-xs text-[var(--text-secondary)]"> <span className="text-xs text-[var(--text-tertiary)]">({ingredient.brand})</span>
<span className="px-2 py-0.5 bg-[var(--bg-primary)] rounded-full"> )}
{categoryOptions.find(opt => opt.value === ingredient.category)?.label || ingredient.category} {stocks.length > 0 && (
</span> <span className="text-xs px-2 py-0.5 bg-[var(--color-success)]/10 text-[var(--color-success)] rounded-full">
<span>{ingredient.unit_of_measure}</span> {stocks.length} {stocks.length === 1 ? 'lot' : 'lots'}
{ingredient.standard_cost && ( </span>
<span className="flex items-center gap-1"> )}
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> </div>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <div className="flex items-center gap-3 mt-1 text-xs text-[var(--text-secondary)]">
<span className="px-2 py-0.5 bg-[var(--bg-primary)] rounded-full">
{categoryOptions.find(opt => opt.value === ingredient.category)?.label || ingredient.category}
</span>
<span>{ingredient.unit_of_measure}</span>
{ingredient.standard_cost && (
<span className="flex items-center gap-1">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
${Number(ingredient.standard_cost).toFixed(2)}
</span>
)}
</div>
</div>
<div className="flex items-center gap-2 ml-4">
<button
type="button"
onClick={() => handleEdit(ingredient)}
className="p-1.5 text-[var(--text-secondary)] hover:text-[var(--color-primary)] hover:bg-[var(--bg-primary)] rounded transition-colors"
aria-label={t('common:edit', 'Edit')}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg> </svg>
${Number(ingredient.standard_cost).toFixed(2)} </button>
</span> <button
)} type="button"
onClick={() => handleDelete(ingredient.id)}
className="p-1.5 text-[var(--text-secondary)] hover:text-[var(--color-error)] hover:bg-[var(--color-error)]/10 rounded transition-colors"
aria-label={t('common:delete', 'Delete')}
disabled={deleteIngredientMutation.isPending}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div> </div>
{/* Stock List - Phase 2 */}
{stocks.length > 0 && (
<div className="ml-6 space-y-1">
{stocks.map((stock) => (
<div
key={stock.id}
className="flex items-center justify-between p-2 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded text-sm"
>
<div className="flex items-center gap-3 text-xs">
<span className="font-medium">{stock.current_quantity} {ingredient.unit_of_measure}</span>
{stock.expiration_date && (
<span className="flex items-center gap-1 text-[var(--text-secondary)]">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Exp: {new Date(stock.expiration_date).toLocaleDateString()}
</span>
)}
{stock.batch_number && (
<span className="text-[var(--text-tertiary)]">Batch: {stock.batch_number}</span>
)}
</div>
<button
type="button"
onClick={() => handleDeleteStock(ingredient.id, stock.id)}
className="p-1 text-[var(--text-secondary)] hover:text-[var(--color-error)] rounded transition-colors"
aria-label="Delete lot"
>
<svg className="w-3 h-3" 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>
))}
</div>
)}
{/* Inline Stock Entry Form - Phase 1 & 2 */}
{isAddingStock ? (
<div className="ml-6 p-4 bg-[var(--color-primary)]/5 border-2 border-[var(--color-primary)] rounded-lg space-y-3">
<div className="flex items-center justify-between mb-2">
<h5 className="text-sm font-medium text-[var(--text-primary)] flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
{t('setup_wizard:inventory.add_stock', 'Add Initial Stock')}
</h5>
<button
type="button"
onClick={handleCancelStock}
className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
>
{t('common:cancel', 'Cancel')}
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{/* Quantity */}
<div>
<label className="block text-xs font-medium text-[var(--text-primary)] mb-1">
{t('setup_wizard:inventory.quantity', 'Quantity')} ({ingredient.unit_of_measure}) <span className="text-[var(--color-error)]">*</span>
</label>
<input
type="number"
step="0.01"
min="0"
value={stockFormData.current_quantity}
onChange={(e) => setStockFormData({ ...stockFormData, current_quantity: e.target.value })}
className={`w-full px-3 py-2 bg-[var(--bg-primary)] border ${stockErrors.current_quantity ? 'border-[var(--color-error)]' : 'border-[var(--border-secondary)]'} rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm`}
placeholder="25.0"
/>
{stockErrors.current_quantity && (
<p className="mt-1 text-xs text-[var(--color-error)]">{stockErrors.current_quantity}</p>
)}
</div>
{/* Expiration Date */}
<div>
<label className="block text-xs font-medium text-[var(--text-primary)] mb-1">
{t('setup_wizard:inventory.expiration_date', 'Expiration Date')}
</label>
<input
type="date"
value={stockFormData.expiration_date}
onChange={(e) => setStockFormData({ ...stockFormData, expiration_date: e.target.value })}
className={`w-full px-3 py-2 bg-[var(--bg-primary)] border ${stockErrors.expiration_date ? 'border-[var(--color-error)]' : 'border-[var(--border-secondary)]'} rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm`}
/>
{stockErrors.expiration_date && (
<p className="mt-1 text-xs text-[var(--color-error)]">{stockErrors.expiration_date}</p>
)}
{stockErrors.expiration_warning && (
<p className="mt-1 text-xs text-[var(--color-warning)] flex items-center gap-1">
<svg className="w-3 h-3" 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>
{stockErrors.expiration_warning}
</p>
)}
</div>
{/* Supplier */}
<div>
<label className="block text-xs font-medium text-[var(--text-primary)] mb-1">
{t('setup_wizard:inventory.supplier', 'Supplier')}
</label>
<select
value={stockFormData.supplier_id}
onChange={(e) => setStockFormData({ ...stockFormData, supplier_id: e.target.value })}
className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm"
>
<option value="">{t('common:none', 'None')}</option>
{suppliers.map((supplier) => (
<option key={supplier.id} value={supplier.id}>
{supplier.name}
</option>
))}
</select>
</div>
{/* Batch Number */}
<div>
<label className="block text-xs font-medium text-[var(--text-primary)] mb-1">
{t('setup_wizard:inventory.batch_number', 'Batch/Lot Number')}
</label>
<input
type="text"
value={stockFormData.batch_number}
onChange={(e) => setStockFormData({ ...stockFormData, batch_number: e.target.value })}
className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm"
placeholder="LOT-2024-11"
/>
</div>
</div>
{/* Help Text */}
<p className="text-xs text-[var(--text-secondary)] flex items-start gap-1">
<svg className="w-3 h-3 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>
{t('setup_wizard:inventory.stock_help', 'Expiration tracking helps prevent waste and enables FIFO inventory management')}
</p>
{/* Action Buttons */}
<div className="flex items-center gap-2 pt-2">
<button
type="button"
onClick={() => handleSaveStock(true)}
disabled={addStockMutation.isPending}
className="px-4 py-2 text-sm bg-[var(--bg-primary)] border border-[var(--border-secondary)] text-[var(--text-primary)] rounded hover:bg-[var(--bg-secondary)] disabled:opacity-50 transition-colors"
>
{t('setup_wizard:inventory.add_another_lot', '+ Add Another Lot')}
</button>
<button
type="button"
onClick={() => handleSaveStock(false)}
disabled={addStockMutation.isPending}
className="px-4 py-2 text-sm bg-[var(--color-primary)] text-white rounded hover:bg-[var(--color-primary-dark)] disabled:opacity-50 transition-colors flex items-center gap-1"
>
{addStockMutation.isPending ? (
<>
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
{t('common:saving', 'Saving...')}
</>
) : (
<>
<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>
{t('common:save', 'Save')}
</>
)}
</button>
</div>
{stockErrors.submit && (
<p className="text-xs text-[var(--color-error)]">{stockErrors.submit}</p>
)}
</div>
) : (
/* Add Stock Button */
!isAdding && (
<div className="ml-6">
<button
type="button"
onClick={() => handleAddStockClick(ingredient.id)}
className="text-xs text-[var(--color-primary)] hover:underline flex items-center gap-1"
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
{stocks.length > 0
? t('setup_wizard:inventory.add_another_stock', 'Add Another Stock Lot')
: t('setup_wizard:inventory.add_initial_stock', 'Add Initial Stock (Optional)')}
</button>
</div>
)
)}
</div> </div>
<div className="flex items-center gap-2 ml-4"> );
<button })}
type="button"
onClick={() => handleEdit(ingredient)}
className="p-1.5 text-[var(--text-secondary)] hover:text-[var(--color-primary)] hover:bg-[var(--bg-primary)] rounded transition-colors"
aria-label={t('common:edit', 'Edit')}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button
type="button"
onClick={() => handleDelete(ingredient.id)}
className="p-1.5 text-[var(--text-secondary)] hover:text-[var(--color-error)] hover:bg-[var(--color-error)]/10 rounded transition-colors"
aria-label={t('common:delete', 'Delete')}
disabled={deleteIngredientMutation.isPending}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
))}
</div> </div>
</div> </div>
)} )}