Files
bakery-ia/frontend/src/components/domain/production/RecipeDisplay.tsx
2025-08-28 10:41:04 +02:00

802 lines
29 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useCallback, useMemo } from 'react';
import { Card, Button, Badge, Input, Modal, Table, Select } from '../../ui';
import type { Recipe, RecipeIngredient, RecipeInstruction, NutritionalInfo } from '../../../types/production.types';
// Local enum definition to avoid import issues
enum DifficultyLevel {
BEGINNER = 'beginner',
INTERMEDIATE = 'intermediate',
ADVANCED = 'advanced',
EXPERT = 'expert',
}
interface RecipeDisplayProps {
className?: string;
recipe: Recipe;
editable?: boolean;
showNutrition?: boolean;
showCosting?: boolean;
onScaleChange?: (scaleFactor: number, scaledRecipe: Recipe) => void;
onVersionUpdate?: (newVersion: Recipe) => void;
onCostCalculation?: (totalCost: number, costPerUnit: number) => void;
}
interface ScaledIngredient extends RecipeIngredient {
scaledQuantity: number;
scaledCost?: number;
}
interface ScaledInstruction extends RecipeInstruction {
scaledDuration?: number;
}
interface ScaledRecipe extends Omit<Recipe, 'ingredients' | 'instructions'> {
scaledYieldQuantity: number;
scaledTotalTime: number;
ingredients: ScaledIngredient[];
instructions: ScaledInstruction[];
scaleFactor: number;
estimatedTotalCost?: number;
costPerScaledUnit?: number;
}
interface TimerState {
instructionId: string;
duration: number;
remaining: number;
isActive: boolean;
isComplete: boolean;
}
const DIFFICULTY_COLORS = {
[DifficultyLevel.BEGINNER]: 'bg-[var(--color-success)]/10 text-[var(--color-success)]',
[DifficultyLevel.INTERMEDIATE]: 'bg-yellow-100 text-yellow-800',
[DifficultyLevel.ADVANCED]: 'bg-[var(--color-primary)]/10 text-[var(--color-primary)]',
[DifficultyLevel.EXPERT]: 'bg-[var(--color-error)]/10 text-[var(--color-error)]',
};
const DIFFICULTY_LABELS = {
[DifficultyLevel.BEGINNER]: 'Principiante',
[DifficultyLevel.INTERMEDIATE]: 'Intermedio',
[DifficultyLevel.ADVANCED]: 'Avanzado',
[DifficultyLevel.EXPERT]: 'Experto',
};
const ALLERGEN_ICONS: Record<string, string> = {
gluten: '🌾',
milk: '🥛',
eggs: '🥚',
nuts: '🥜',
soy: '🫘',
sesame: '🌰',
fish: '🐟',
shellfish: '🦐',
};
const EQUIPMENT_ICONS: Record<string, string> = {
oven: '🔥',
mixer: '🥄',
scale: '⚖️',
bowl: '🥣',
whisk: '🔄',
spatula: '🍴',
thermometer: '🌡️',
timer: '⏰',
};
export const RecipeDisplay: React.FC<RecipeDisplayProps> = ({
className = '',
recipe,
editable = false,
showNutrition = true,
showCosting = false,
onScaleChange,
onVersionUpdate,
onCostCalculation,
}) => {
const [scaleFactor, setScaleFactor] = useState<number>(1);
const [activeTimers, setActiveTimers] = useState<Record<string, TimerState>>({});
const [isNutritionModalOpen, setIsNutritionModalOpen] = useState(false);
const [isCostingModalOpen, setIsCostingModalOpen] = useState(false);
const [isEquipmentModalOpen, setIsEquipmentModalOpen] = useState(false);
const [selectedInstruction, setSelectedInstruction] = useState<RecipeInstruction | null>(null);
const [showAllergensDetail, setShowAllergensDetail] = useState(false);
// Mock ingredient costs for demonstration
const ingredientCosts: Record<string, number> = {
flour: 1.2, // €/kg
water: 0.001, // €/l
salt: 0.8, // €/kg
yeast: 8.5, // €/kg
sugar: 1.5, // €/kg
butter: 6.2, // €/kg
milk: 1.3, // €/l
eggs: 0.25, // €/unit
};
const scaledRecipe = useMemo((): ScaledRecipe => {
const scaledIngredients: ScaledIngredient[] = recipe.ingredients.map(ingredient => ({
...ingredient,
scaledQuantity: ingredient.quantity * scaleFactor,
scaledCost: ingredientCosts[ingredient.ingredient_id]
? ingredientCosts[ingredient.ingredient_id] * ingredient.quantity * scaleFactor
: undefined,
}));
const scaledInstructions: ScaledInstruction[] = recipe.instructions.map(instruction => ({
...instruction,
scaledDuration: instruction.duration_minutes ? Math.ceil(instruction.duration_minutes * scaleFactor) : undefined,
}));
const estimatedTotalCost = scaledIngredients.reduce((total, ingredient) =>
total + (ingredient.scaledCost || 0), 0
);
const scaledYieldQuantity = recipe.yield_quantity * scaleFactor;
const costPerScaledUnit = scaledYieldQuantity > 0 ? estimatedTotalCost / scaledYieldQuantity : 0;
return {
...recipe,
scaledYieldQuantity,
scaledTotalTime: Math.ceil(recipe.total_time_minutes * scaleFactor),
ingredients: scaledIngredients,
instructions: scaledInstructions,
scaleFactor,
estimatedTotalCost,
costPerScaledUnit,
};
}, [recipe, scaleFactor, ingredientCosts]);
const handleScaleChange = useCallback((newScaleFactor: number) => {
setScaleFactor(newScaleFactor);
if (onScaleChange) {
onScaleChange(newScaleFactor, scaledRecipe as unknown as Recipe);
}
}, [scaledRecipe, onScaleChange]);
const startTimer = (instruction: RecipeInstruction) => {
if (!instruction.duration_minutes) return;
const timerState: TimerState = {
instructionId: instruction.step_number.toString(),
duration: instruction.duration_minutes * 60, // Convert to seconds
remaining: instruction.duration_minutes * 60,
isActive: true,
isComplete: false,
};
setActiveTimers(prev => ({
...prev,
[instruction.step_number.toString()]: timerState,
}));
// Start countdown
const interval = setInterval(() => {
setActiveTimers(prev => {
const current = prev[instruction.step_number.toString()];
if (!current || current.remaining <= 0) {
clearInterval(interval);
return {
...prev,
[instruction.step_number.toString()]: {
...current,
remaining: 0,
isActive: false,
isComplete: true,
}
};
}
return {
...prev,
[instruction.step_number.toString()]: {
...current,
remaining: current.remaining - 1,
}
};
});
}, 1000);
};
const stopTimer = (instructionId: string) => {
setActiveTimers(prev => ({
...prev,
[instructionId]: {
...prev[instructionId],
isActive: false,
}
}));
};
const formatTime = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}
return `${minutes}:${String(secs).padStart(2, '0')}`;
};
const renderScalingControls = () => (
<Card className="p-4">
<h3 className="font-semibold text-lg mb-4">Escalado de receta</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Factor de escala
</label>
<Input
type="number"
min="0.1"
max="10"
step="0.1"
value={scaleFactor}
onChange={(e) => handleScaleChange(parseFloat(e.target.value) || 1)}
className="w-full"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Cantidad original
</label>
<p className="text-lg font-semibold text-[var(--text-primary)]">
{recipe.yield_quantity} {recipe.yield_unit}
</p>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Cantidad escalada
</label>
<p className="text-lg font-semibold text-[var(--color-info)]">
{scaledRecipe.scaledYieldQuantity} {recipe.yield_unit}
</p>
</div>
</div>
<div className="flex gap-2 mt-4">
<Button
variant="outline"
size="sm"
onClick={() => handleScaleChange(0.5)}
className={scaleFactor === 0.5 ? 'bg-[var(--color-info)]/5 border-blue-300' : ''}
>
1/2
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleScaleChange(1)}
className={scaleFactor === 1 ? 'bg-[var(--color-info)]/5 border-blue-300' : ''}
>
1x
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleScaleChange(2)}
className={scaleFactor === 2 ? 'bg-[var(--color-info)]/5 border-blue-300' : ''}
>
2x
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleScaleChange(3)}
className={scaleFactor === 3 ? 'bg-[var(--color-info)]/5 border-blue-300' : ''}
>
3x
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleScaleChange(5)}
className={scaleFactor === 5 ? 'bg-[var(--color-info)]/5 border-blue-300' : ''}
>
5x
</Button>
</div>
{scaleFactor !== 1 && (
<div className="mt-4 p-3 bg-[var(--color-info)]/5 border border-[var(--color-info)]/20 rounded-lg">
<p className="text-sm text-[var(--color-info)]">
<span className="font-medium">Tiempo total escalado:</span> {Math.ceil(scaledRecipe.scaledTotalTime / 60)}h {scaledRecipe.scaledTotalTime % 60}m
</p>
{showCosting && scaledRecipe.estimatedTotalCost && (
<p className="text-sm text-[var(--color-info)] mt-1">
<span className="font-medium">Costo estimado:</span> {scaledRecipe.estimatedTotalCost.toFixed(2)}
({scaledRecipe.costPerScaledUnit?.toFixed(3)}/{recipe.yield_unit})
</p>
)}
</div>
)}
</Card>
);
const renderRecipeHeader = () => (
<Card className="p-6">
<div className="flex flex-col lg:flex-row gap-6">
<div className="flex-1">
<div className="flex items-start justify-between mb-4">
<div>
<h1 className="text-3xl font-bold text-[var(--text-primary)]">{recipe.name}</h1>
<p className="text-[var(--text-secondary)] mt-1">{recipe.description}</p>
</div>
<div className="flex gap-2">
<Badge className={DIFFICULTY_COLORS[recipe.difficulty_level]}>
{DIFFICULTY_LABELS[recipe.difficulty_level]}
</Badge>
<Badge className="bg-[var(--color-info)]/10 text-[var(--color-info)]">
v{recipe.version}
</Badge>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<p className="text-sm text-[var(--text-secondary)]">Preparación</p>
<p className="font-semibold">{recipe.prep_time_minutes} min</p>
</div>
<div>
<p className="text-sm text-[var(--text-secondary)]">Cocción</p>
<p className="font-semibold">{recipe.cook_time_minutes} min</p>
</div>
<div>
<p className="text-sm text-[var(--text-secondary)]">Total</p>
<p className="font-semibold">{Math.ceil(recipe.total_time_minutes / 60)}h {recipe.total_time_minutes % 60}m</p>
</div>
<div>
<p className="text-sm text-[var(--text-secondary)]">Rendimiento</p>
<p className="font-semibold">{scaledRecipe.scaledYieldQuantity} {recipe.yield_unit}</p>
</div>
</div>
{recipe.allergen_warnings.length > 0 && (
<div className="mt-4">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-[var(--text-secondary)]">Alérgenos:</p>
<Button
variant="ghost"
size="sm"
onClick={() => setShowAllergensDetail(!showAllergensDetail)}
>
{showAllergensDetail ? 'Ocultar' : 'Ver detalles'}
</Button>
</div>
<div className="flex flex-wrap gap-2 mt-2">
{recipe.allergen_warnings.map((allergen) => (
<Badge
key={allergen}
className="bg-[var(--color-error)]/10 text-[var(--color-error)] border border-red-200"
>
{ALLERGEN_ICONS[allergen]} {allergen}
</Badge>
))}
</div>
{showAllergensDetail && (
<div className="mt-3 p-3 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-[var(--color-error)]">
Este producto contiene los siguientes alérgenos. Revisar cuidadosamente
antes del consumo si existe alguna alergia o intolerancia alimentaria.
</p>
</div>
)}
</div>
)}
{recipe.storage_instructions && (
<div className="mt-4 p-3 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-lg">
<p className="text-sm text-[var(--text-secondary)]">
<span className="font-medium">Conservación:</span> {recipe.storage_instructions}
{recipe.shelf_life_hours && (
<span className="ml-2">
Vida útil: {Math.ceil(recipe.shelf_life_hours / 24)} días
</span>
)}
</p>
</div>
)}
</div>
<div className="flex flex-col gap-2 lg:w-48">
{showNutrition && (
<Button
variant="outline"
onClick={() => setIsNutritionModalOpen(true)}
className="w-full"
>
📊 Información nutricional
</Button>
)}
{showCosting && (
<Button
variant="outline"
onClick={() => setIsCostingModalOpen(true)}
className="w-full"
>
💰 Análisis de costos
</Button>
)}
<Button
variant="outline"
onClick={() => setIsEquipmentModalOpen(true)}
className="w-full"
>
🔧 Equipo necesario
</Button>
{editable && onVersionUpdate && (
<Button variant="primary" className="w-full">
Editar receta
</Button>
)}
</div>
</div>
</Card>
);
const renderIngredients = () => (
<Card className="p-6">
<h2 className="text-xl font-semibold mb-4">Ingredientes</h2>
<div className="space-y-3">
{scaledRecipe.ingredients.map((ingredient, index) => (
<div
key={`${ingredient.ingredient_id}-${index}`}
className="flex items-center justify-between py-2 border-b border-[var(--border-primary)] last:border-b-0"
>
<div className="flex-1">
<p className="font-medium text-[var(--text-primary)]">{ingredient.ingredient_name}</p>
{ingredient.preparation_notes && (
<p className="text-sm text-[var(--text-secondary)] italic">{ingredient.preparation_notes}</p>
)}
{ingredient.is_optional && (
<Badge className="bg-[var(--bg-tertiary)] text-[var(--text-secondary)] text-xs mt-1">
Opcional
</Badge>
)}
</div>
<div className="text-right ml-4">
<p className="font-semibold text-lg">
{ingredient.scaledQuantity.toFixed(ingredient.scaledQuantity < 1 ? 2 : 0)} {ingredient.unit}
</p>
{scaleFactor !== 1 && (
<p className="text-sm text-[var(--text-tertiary)]">
(original: {ingredient.quantity} {ingredient.unit})
</p>
)}
{showCosting && ingredient.scaledCost && (
<p className="text-sm text-[var(--color-success)]">
{ingredient.scaledCost.toFixed(2)}
</p>
)}
</div>
</div>
))}
</div>
{showCosting && scaledRecipe.estimatedTotalCost && (
<div className="mt-4 pt-4 border-t border-[var(--border-primary)]">
<div className="flex justify-between items-center">
<span className="font-semibold">Costo total estimado:</span>
<span className="text-xl font-bold text-[var(--color-success)]">
{scaledRecipe.estimatedTotalCost.toFixed(2)}
</span>
</div>
<p className="text-sm text-[var(--text-secondary)] text-right">
{scaledRecipe.costPerScaledUnit?.toFixed(3)} por {recipe.yield_unit}
</p>
</div>
)}
</Card>
);
const renderInstructions = () => (
<Card className="p-6">
<h2 className="text-xl font-semibold mb-4">Instrucciones</h2>
<div className="space-y-4">
{scaledRecipe.instructions.map((instruction, index) => {
const timer = activeTimers[instruction.step_number.toString()];
return (
<div
key={instruction.step_number}
className={`p-4 border rounded-lg ${
instruction.critical_control_point
? 'border-orange-300 bg-orange-50'
: 'border-[var(--border-primary)] bg-[var(--bg-secondary)]'
}`}
>
<div className="flex items-start gap-4">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-blue-600 text-white rounded-full flex items-center justify-center font-semibold">
{instruction.step_number}
</div>
</div>
<div className="flex-1">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
{instruction.duration_minutes && (
<Badge className="bg-[var(--color-info)]/10 text-[var(--color-info)]">
{instruction.scaledDuration || instruction.duration_minutes} min
</Badge>
)}
{instruction.temperature && (
<Badge className="bg-[var(--color-error)]/10 text-[var(--color-error)]">
🌡 {instruction.temperature}°C
</Badge>
)}
{instruction.critical_control_point && (
<Badge className="bg-[var(--color-primary)]/10 text-[var(--color-primary)]">
🚨 PCC
</Badge>
)}
</div>
{instruction.duration_minutes && (
<div className="flex gap-2">
{!timer?.isActive && !timer?.isComplete && (
<Button
variant="outline"
size="sm"
onClick={() => startTimer(instruction)}
>
Iniciar timer
</Button>
)}
{timer?.isActive && (
<div className="flex items-center gap-2">
<span className="text-lg font-mono font-bold text-[var(--color-info)]">
{formatTime(timer.remaining)}
</span>
<Button
variant="outline"
size="sm"
onClick={() => stopTimer(instruction.step_number.toString())}
>
</Button>
</div>
)}
{timer?.isComplete && (
<Badge className="bg-[var(--color-success)]/10 text-[var(--color-success)] animate-pulse">
Completado
</Badge>
)}
</div>
)}
</div>
<p className="text-[var(--text-primary)] leading-relaxed mb-2">
{instruction.instruction}
</p>
{instruction.equipment && instruction.equipment.length > 0 && (
<div className="mb-2">
<p className="text-sm text-[var(--text-secondary)] mb-1">Equipo necesario:</p>
<div className="flex flex-wrap gap-1">
{instruction.equipment.map((equipment, idx) => (
<Badge key={idx} className="bg-[var(--bg-tertiary)] text-[var(--text-secondary)] text-xs">
{EQUIPMENT_ICONS[equipment.toLowerCase()] || '🔧'} {equipment}
</Badge>
))}
</div>
</div>
)}
{instruction.tips && (
<div className="mt-3 p-2 bg-yellow-50 border border-yellow-200 rounded">
<p className="text-sm text-yellow-800">
💡 <span className="font-medium">Tip:</span> {instruction.tips}
</p>
</div>
)}
</div>
</div>
</div>
);
})}
</div>
</Card>
);
const renderNutritionModal = () => (
<Modal
isOpen={isNutritionModalOpen}
onClose={() => setIsNutritionModalOpen(false)}
title="Información Nutricional"
>
{recipe.nutritional_info ? (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="bg-[var(--bg-secondary)] p-4 rounded-lg">
<p className="text-sm text-[var(--text-secondary)]">Calorías por porción</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">
{recipe.nutritional_info.calories_per_serving || 'N/A'}
</p>
</div>
<div className="bg-[var(--bg-secondary)] p-4 rounded-lg">
<p className="text-sm text-[var(--text-secondary)]">Tamaño de porción</p>
<p className="text-lg font-semibold text-[var(--text-primary)]">
{recipe.nutritional_info.serving_size || 'N/A'}
</p>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<div>
<p className="text-sm text-[var(--text-secondary)]">Proteínas</p>
<p className="text-lg font-semibold">{recipe.nutritional_info.protein_g || 0}g</p>
</div>
<div>
<p className="text-sm text-[var(--text-secondary)]">Carbohidratos</p>
<p className="text-lg font-semibold">{recipe.nutritional_info.carbohydrates_g || 0}g</p>
</div>
<div>
<p className="text-sm text-[var(--text-secondary)]">Grasas</p>
<p className="text-lg font-semibold">{recipe.nutritional_info.fat_g || 0}g</p>
</div>
<div>
<p className="text-sm text-[var(--text-secondary)]">Fibra</p>
<p className="text-lg font-semibold">{recipe.nutritional_info.fiber_g || 0}g</p>
</div>
<div>
<p className="text-sm text-[var(--text-secondary)]">Azúcares</p>
<p className="text-lg font-semibold">{recipe.nutritional_info.sugar_g || 0}g</p>
</div>
<div>
<p className="text-sm text-[var(--text-secondary)]">Sodio</p>
<p className="text-lg font-semibold">{recipe.nutritional_info.sodium_mg || 0}mg</p>
</div>
</div>
<div className="bg-[var(--color-info)]/5 p-4 rounded-lg">
<p className="text-sm text-[var(--color-info)]">
<span className="font-medium">Porciones por lote escalado:</span> {
recipe.nutritional_info.servings_per_batch
? Math.ceil(recipe.nutritional_info.servings_per_batch * scaleFactor)
: 'N/A'
}
</p>
</div>
</div>
) : (
<p className="text-[var(--text-tertiary)] text-center py-8">
No hay información nutricional disponible para esta receta.
</p>
)}
</Modal>
);
const renderCostingModal = () => (
<Modal
isOpen={isCostingModalOpen}
onClose={() => setIsCostingModalOpen(false)}
title="Análisis de Costos"
>
<div className="space-y-4">
<div className="bg-green-50 border border-green-200 p-4 rounded-lg">
<div className="flex justify-between items-center mb-2">
<span className="font-semibold text-green-900">Costo total</span>
<span className="text-2xl font-bold text-[var(--color-success)]">
{scaledRecipe.estimatedTotalCost?.toFixed(2)}
</span>
</div>
<p className="text-sm text-[var(--color-success)]">
{scaledRecipe.costPerScaledUnit?.toFixed(3)} por {recipe.yield_unit}
</p>
</div>
<div>
<h4 className="font-semibold mb-3">Desglose por ingrediente</h4>
<div className="space-y-2">
{scaledRecipe.ingredients.map((ingredient, index) => (
<div key={index} className="flex justify-between items-center py-2 border-b border-[var(--border-primary)]">
<div>
<p className="font-medium">{ingredient.ingredient_name}</p>
<p className="text-sm text-[var(--text-secondary)]">
{ingredient.scaledQuantity.toFixed(2)} {ingredient.unit}
</p>
</div>
<p className="font-semibold text-[var(--color-success)]">
{ingredient.scaledCost?.toFixed(2) || '0.00'}
</p>
</div>
))}
</div>
</div>
<div className="bg-[var(--color-info)]/5 p-4 rounded-lg">
<h4 className="font-semibold text-blue-900 mb-2">Análisis de rentabilidad</h4>
<div className="space-y-1 text-sm text-[var(--color-info)]">
<p> Costo de ingredientes: {scaledRecipe.estimatedTotalCost?.toFixed(2)}</p>
<p> Margen sugerido (300%): {((scaledRecipe.estimatedTotalCost || 0) * 3).toFixed(2)}</p>
<p> Precio de venta sugerido por {recipe.yield_unit}: {((scaledRecipe.costPerScaledUnit || 0) * 4).toFixed(2)}</p>
</div>
</div>
</div>
</Modal>
);
const renderEquipmentModal = () => (
<Modal
isOpen={isEquipmentModalOpen}
onClose={() => setIsEquipmentModalOpen(false)}
title="Equipo Necesario"
>
<div className="space-y-4">
<div>
<h4 className="font-semibold mb-3">Equipo general de la receta</h4>
<div className="grid grid-cols-2 gap-2">
{recipe.equipment_needed.map((equipment, index) => (
<div key={index} className="flex items-center gap-2 p-2 bg-[var(--bg-secondary)] rounded">
<span className="text-xl">
{EQUIPMENT_ICONS[equipment.toLowerCase()] || '🔧'}
</span>
<span className="text-sm">{equipment}</span>
</div>
))}
</div>
</div>
<div>
<h4 className="font-semibold mb-3">Equipo por instrucción</h4>
<div className="space-y-2">
{recipe.instructions
.filter(instruction => instruction.equipment && instruction.equipment.length > 0)
.map((instruction) => (
<div key={instruction.step_number} className="border border-[var(--border-primary)] p-3 rounded">
<p className="font-medium text-sm mb-2">
Paso {instruction.step_number}
</p>
<div className="flex flex-wrap gap-1">
{instruction.equipment?.map((equipment, idx) => (
<Badge key={idx} className="bg-[var(--color-info)]/10 text-[var(--color-info)] text-xs">
{EQUIPMENT_ICONS[equipment.toLowerCase()] || '🔧'} {equipment}
</Badge>
))}
</div>
</div>
))}
</div>
</div>
</div>
</Modal>
);
return (
<div className={`space-y-6 ${className}`}>
{renderRecipeHeader()}
{renderScalingControls()}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{renderIngredients()}
<div className="space-y-6">
{renderInstructions()}
</div>
</div>
{renderNutritionModal()}
{renderCostingModal()}
{renderEquipmentModal()}
</div>
);
};
export default RecipeDisplay;