feat: Completely rewrite RecipeWizard with comprehensive improvements

Major improvements:
1. Fixed 'a.map is not a function' error (line 387: result.templates)
2. Removed duplicate Next buttons - now using WizardModal's validate prop
3. Added ALL missing required fields (version, difficulty_level, status defaults)
4. Added comprehensive advanced options section with ALL optional fields:
   - Recipe code/SKU, version, difficulty level
   - Cook time, rest time, total time
   - Batch sizing (min/max, multiplier)
   - Production environment (temp, humidity)
   - Seasonal/signature item flags
   - Descriptions, notes, storage instructions
   - Allergens, dietary tags
   - Target margin percentage
5. Integrated AdvancedOptionsSection component for progressive disclosure
6. Added tooltips for complex fields using existing Tooltip component
7. Proper form validation on each step
8. Real-time data synchronization with useEffect
9. English labels (per project standards)
10. All fields map correctly to backend RecipeCreate schema

Technical changes:
- Created reusable AdvancedOptionsSection component
- Steps now validate using WizardModal's validate prop
- No internal "Continuar" buttons - cleaner UX
- Quality Templates step marked as optional (isOptional: true)
- Ingredients step validates all required data
- Seasonal month selectors conditional on isSeasonal checkbox

This implementation follows UX best practices for progressive disclosure and reduces cognitive load while maintaining access to all backend fields.
This commit is contained in:
Claude
2025-11-10 07:28:20 +00:00
parent 020acc4691
commit 3b66bb869a
3 changed files with 642 additions and 152 deletions

View File

@@ -6,25 +6,54 @@ import { recipesService } from '../../../../api/services/recipes';
import { inventoryService } from '../../../../api/services/inventory'; import { inventoryService } from '../../../../api/services/inventory';
import { qualityTemplateService } from '../../../../api/services/qualityTemplates'; import { qualityTemplateService } from '../../../../api/services/qualityTemplates';
import { IngredientResponse } from '../../../../api/types/inventory'; import { IngredientResponse } from '../../../../api/types/inventory';
import { RecipeCreate, RecipeIngredientCreate, MeasurementUnit, RecipeQualityConfiguration } from '../../../../api/types/recipes'; import { RecipeCreate, RecipeIngredientCreate, MeasurementUnit, RecipeQualityConfiguration, RecipeStatus } from '../../../../api/types/recipes';
import { QualityCheckTemplateResponse } from '../../../../api/types/qualityTemplates'; import { QualityCheckTemplateResponse } from '../../../../api/types/qualityTemplates';
import { showToast } from '../../../../utils/toast'; import { showToast } from '../../../../utils/toast';
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
import Tooltip from '../../../ui/Tooltip/Tooltip';
interface WizardDataProps extends WizardStepProps { interface WizardDataProps extends WizardStepProps {
data: Record<string, any>; data: Record<string, any>;
onDataChange: (data: Record<string, any>) => void; onDataChange: (data: Record<string, any>) => void;
} }
const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNext }) => { const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
const { currentTenant } = useTenant(); const { currentTenant } = useTenant();
const [recipeData, setRecipeData] = useState({ const [recipeData, setRecipeData] = useState({
// Required fields
name: data.name || '', name: data.name || '',
category: data.category || 'bread', finishedProductId: data.finishedProductId || '',
yieldQuantity: data.yieldQuantity || '', yieldQuantity: data.yieldQuantity || '',
yieldUnit: data.yieldUnit || 'units', yieldUnit: data.yieldUnit || 'units',
// Optional basic fields
category: data.category || 'bread',
prepTime: data.prepTime || '', prepTime: data.prepTime || '',
finishedProductId: data.finishedProductId || '',
instructions: data.instructions || '', instructions: data.instructions || '',
// Advanced optional fields
recipeCode: data.recipeCode || '',
version: data.version || '1.0',
difficultyLevel: data.difficultyLevel || 3,
cookTime: data.cookTime || '',
restTime: data.restTime || '',
totalTime: data.totalTime || '',
description: data.description || '',
preparationNotes: data.preparationNotes || '',
storageInstructions: data.storageInstructions || '',
servesCount: data.servesCount || '',
batchSizeMultiplier: data.batchSizeMultiplier || 1.0,
minBatchSize: data.minBatchSize || '',
maxBatchSize: data.maxBatchSize || '',
optimalProductionTemp: data.optimalProductionTemp || '',
optimalHumidity: data.optimalHumidity || '',
isSeasonal: data.isSeasonal || false,
isSignatureItem: data.isSignatureItem || false,
seasonStartMonth: data.seasonStartMonth || '',
seasonEndMonth: data.seasonEndMonth || '',
allergens: data.allergens || '',
dietaryTags: data.dietaryTags || '',
targetMargin: data.targetMargin || '',
}); });
const [finishedProducts, setFinishedProducts] = useState<IngredientResponse[]>([]); const [finishedProducts, setFinishedProducts] = useState<IngredientResponse[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -33,6 +62,10 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
fetchFinishedProducts(); fetchFinishedProducts();
}, []); }, []);
useEffect(() => {
onDataChange({ ...data, ...recipeData });
}, [recipeData]);
const fetchFinishedProducts = async () => { const fetchFinishedProducts = async () => {
if (!currentTenant?.id) return; if (!currentTenant?.id) return;
setLoading(true); setLoading(true);
@@ -52,75 +85,106 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
<div className="space-y-6"> <div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]"> <div className="text-center pb-4 border-b border-[var(--border-primary)]">
<ChefHat className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" /> <ChefHat className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">Detalles de la Receta</h3> <h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">Recipe Details</h3>
<p className="text-sm text-[var(--text-secondary)]">Essential information about your recipe</p>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2"> {/* Required Fields */}
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Nombre *</label> <div className="space-y-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Recipe Name *
</label>
<input <input
type="text" type="text"
value={recipeData.name} value={recipeData.name}
onChange={(e) => setRecipeData({ ...recipeData, name: e.target.value })} onChange={(e) => setRecipeData({ ...recipeData, name: e.target.value })}
placeholder="Ej: Baguette Tradicional" placeholder="e.g., Traditional Baguette"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]" className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/> />
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Categoría *</label> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Category *
</label>
<select <select
value={recipeData.category} value={recipeData.category}
onChange={(e) => setRecipeData({ ...recipeData, category: e.target.value })} onChange={(e) => setRecipeData({ ...recipeData, category: e.target.value })}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]" className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
> >
<option value="bread">Pan</option> <option value="bread">Bread</option>
<option value="pastry">Pastelería</option> <option value="pastries">Pastries</option>
<option value="cake">Repostería</option> <option value="cakes">Cakes</option>
<option value="cookie">Galletas</option> <option value="cookies">Cookies</option>
<option value="other">Otro</option> <option value="muffins">Muffins</option>
<option value="sandwiches">Sandwiches</option>
<option value="seasonal">Seasonal</option>
<option value="other">Other</option>
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Producto Terminado *</label> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
Finished Product *
<Tooltip content="The final product this recipe produces. Must be created in inventory first.">
<span />
</Tooltip>
</label>
<select <select
value={recipeData.finishedProductId} value={recipeData.finishedProductId}
onChange={(e) => setRecipeData({ ...recipeData, finishedProductId: e.target.value })} onChange={(e) => setRecipeData({ ...recipeData, finishedProductId: e.target.value })}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]" className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
disabled={loading} disabled={loading}
> >
<option value="">Seleccionar producto...</option> <option value="">Select product...</option>
{finishedProducts.map(product => ( {finishedProducts.map(product => (
<option key={product.id} value={product.id}>{product.name}</option> <option key={product.id} value={product.id}>{product.name}</option>
))} ))}
</select> </select>
</div> </div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Rendimiento *</label> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Yield Quantity *
</label>
<input <input
type="number" type="number"
value={recipeData.yieldQuantity} value={recipeData.yieldQuantity}
onChange={(e) => setRecipeData({ ...recipeData, yieldQuantity: e.target.value })} onChange={(e) => setRecipeData({ ...recipeData, yieldQuantity: e.target.value })}
placeholder="12" placeholder="12"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]" className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
min="1" min="0.01"
step="0.01"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Unidad *</label> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Yield Unit *
</label>
<select <select
value={recipeData.yieldUnit} value={recipeData.yieldUnit}
onChange={(e) => setRecipeData({ ...recipeData, yieldUnit: e.target.value })} onChange={(e) => setRecipeData({ ...recipeData, yieldUnit: e.target.value })}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]" className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
> >
<option value="units">Unidades</option> <option value="units">Units</option>
<option value="kg">Kilogramos</option> <option value="pieces">Pieces</option>
<option value="g">Gramos</option> <option value="kg">Kilograms (kg)</option>
<option value="l">Litros</option> <option value="g">Grams (g)</option>
<option value="ml">Mililitros</option> <option value="l">Liters (l)</option>
<option value="pieces">Piezas</option> <option value="ml">Milliliters (ml)</option>
</select> </select>
</div> </div>
</div>
<div> <div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Tiempo de Preparación (min)</label> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Preparation Time (minutes)
</label>
<input <input
type="number" type="number"
value={recipeData.prepTime} value={recipeData.prepTime}
@@ -130,26 +194,357 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
min="0" min="0"
/> />
</div> </div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Instrucciones</label> <div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Instructions
</label>
<textarea <textarea
value={recipeData.instructions} value={recipeData.instructions}
onChange={(e) => setRecipeData({ ...recipeData, instructions: e.target.value })} onChange={(e) => setRecipeData({ ...recipeData, instructions: e.target.value })}
placeholder="Pasos de preparación de la receta..." placeholder="Step-by-step preparation instructions..."
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]" className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
rows={4} rows={4}
/> />
</div> </div>
</div> </div>
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
<button {/* Advanced Options */}
onClick={() => { onDataChange({ ...data, ...recipeData }); onNext(); }} <AdvancedOptionsSection
disabled={!recipeData.name || !recipeData.yieldQuantity || !recipeData.finishedProductId} title="Advanced Options"
className="px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 disabled:opacity-50 disabled:cursor-not-allowed" description="Optional fields for detailed recipe management"
> >
Continuar <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
</button> <div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Recipe Code/SKU
</label>
<input
type="text"
value={recipeData.recipeCode}
onChange={(e) => setRecipeData({ ...recipeData, recipeCode: e.target.value })}
placeholder="RCP-001"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div> </div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Version
</label>
<input
type="text"
value={recipeData.version}
onChange={(e) => setRecipeData({ ...recipeData, version: e.target.value })}
placeholder="1.0"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
Difficulty Level (1-5)
<Tooltip content="1 = Very Easy, 5 = Expert Level">
<span />
</Tooltip>
</label>
<input
type="number"
value={recipeData.difficultyLevel}
onChange={(e) => setRecipeData({ ...recipeData, difficultyLevel: parseInt(e.target.value) || 1 })}
min="1"
max="5"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Cook Time (minutes)
</label>
<input
type="number"
value={recipeData.cookTime}
onChange={(e) => setRecipeData({ ...recipeData, cookTime: e.target.value })}
placeholder="30"
min="0"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
Rest Time (minutes)
<Tooltip content="Time for rising, cooling, or resting">
<span />
</Tooltip>
</label>
<input
type="number"
value={recipeData.restTime}
onChange={(e) => setRecipeData({ ...recipeData, restTime: e.target.value })}
placeholder="60"
min="0"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Total Time (minutes)
</label>
<input
type="number"
value={recipeData.totalTime}
onChange={(e) => setRecipeData({ ...recipeData, totalTime: e.target.value })}
placeholder="90"
min="0"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Serves Count
</label>
<input
type="number"
value={recipeData.servesCount}
onChange={(e) => setRecipeData({ ...recipeData, servesCount: e.target.value })}
placeholder="8"
min="0"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
Batch Size Multiplier
<Tooltip content="Default scaling factor for batch production">
<span />
</Tooltip>
</label>
<input
type="number"
value={recipeData.batchSizeMultiplier}
onChange={(e) => setRecipeData({ ...recipeData, batchSizeMultiplier: parseFloat(e.target.value) || 1 })}
placeholder="1.0"
min="0.1"
step="0.1"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Min Batch Size
</label>
<input
type="number"
value={recipeData.minBatchSize}
onChange={(e) => setRecipeData({ ...recipeData, minBatchSize: e.target.value })}
placeholder="5"
min="0"
step="0.1"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Max Batch Size
</label>
<input
type="number"
value={recipeData.maxBatchSize}
onChange={(e) => setRecipeData({ ...recipeData, maxBatchSize: e.target.value })}
placeholder="100"
min="0"
step="0.1"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Optimal Production Temp (°C)
</label>
<input
type="number"
value={recipeData.optimalProductionTemp}
onChange={(e) => setRecipeData({ ...recipeData, optimalProductionTemp: e.target.value })}
placeholder="24"
step="0.1"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Optimal Humidity (%)
</label>
<input
type="number"
value={recipeData.optimalHumidity}
onChange={(e) => setRecipeData({ ...recipeData, optimalHumidity: e.target.value })}
placeholder="65"
min="0"
max="100"
step="0.1"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Target Margin (%)
</label>
<input
type="number"
value={recipeData.targetMargin}
onChange={(e) => setRecipeData({ ...recipeData, targetMargin: e.target.value })}
placeholder="30"
min="0"
max="100"
step="0.1"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center gap-3">
<input
type="checkbox"
id="isSeasonal"
checked={recipeData.isSeasonal}
onChange={(e) => setRecipeData({ ...recipeData, isSeasonal: e.target.checked })}
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
/>
<label htmlFor="isSeasonal" className="text-sm font-medium text-[var(--text-secondary)]">
Seasonal Item
</label>
</div>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="isSignatureItem"
checked={recipeData.isSignatureItem}
onChange={(e) => setRecipeData({ ...recipeData, isSignatureItem: e.target.checked })}
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
/>
<label htmlFor="isSignatureItem" className="text-sm font-medium text-[var(--text-secondary)]">
Signature Item
</label>
</div>
</div>
{recipeData.isSeasonal && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Season Start Month
</label>
<select
value={recipeData.seasonStartMonth}
onChange={(e) => setRecipeData({ ...recipeData, seasonStartMonth: e.target.value })}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
>
<option value="">Select month...</option>
{Array.from({ length: 12 }, (_, i) => i + 1).map(month => (
<option key={month} value={month}>
{new Date(2000, month - 1).toLocaleString('default', { month: 'long' })}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Season End Month
</label>
<select
value={recipeData.seasonEndMonth}
onChange={(e) => setRecipeData({ ...recipeData, seasonEndMonth: e.target.value })}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
>
<option value="">Select month...</option>
{Array.from({ length: 12 }, (_, i) => i + 1).map(month => (
<option key={month} value={month}>
{new Date(2000, month - 1).toLocaleString('default', { month: 'long' })}
</option>
))}
</select>
</div>
</div>
)}
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Description
</label>
<textarea
value={recipeData.description}
onChange={(e) => setRecipeData({ ...recipeData, description: e.target.value })}
placeholder="Detailed description of the recipe..."
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
rows={3}
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Preparation Notes
</label>
<textarea
value={recipeData.preparationNotes}
onChange={(e) => setRecipeData({ ...recipeData, preparationNotes: e.target.value })}
placeholder="Tips and notes for preparation..."
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
rows={3}
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Storage Instructions
</label>
<textarea
value={recipeData.storageInstructions}
onChange={(e) => setRecipeData({ ...recipeData, storageInstructions: e.target.value })}
placeholder="How to store the finished product..."
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
rows={3}
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Allergens
</label>
<input
type="text"
value={recipeData.allergens}
onChange={(e) => setRecipeData({ ...recipeData, allergens: e.target.value })}
placeholder="e.g., gluten, dairy, eggs (comma-separated)"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Dietary Tags
</label>
<input
type="text"
value={recipeData.dietaryTags}
onChange={(e) => setRecipeData({ ...recipeData, dietaryTags: e.target.value })}
placeholder="e.g., vegan, gluten-free, organic (comma-separated)"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
</AdvancedOptionsSection>
</div> </div>
); );
}; };
@@ -163,7 +558,7 @@ interface SelectedIngredient {
order: number; order: number;
} }
const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNext }) => { const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
const { currentTenant } = useTenant(); const { currentTenant } = useTenant();
const [ingredients, setIngredients] = useState<IngredientResponse[]>([]); const [ingredients, setIngredients] = useState<IngredientResponse[]>([]);
const [selectedIngredients, setSelectedIngredients] = useState<SelectedIngredient[]>(data.ingredients || []); const [selectedIngredients, setSelectedIngredients] = useState<SelectedIngredient[]>(data.ingredients || []);
@@ -175,16 +570,19 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNext
fetchIngredients(); fetchIngredients();
}, []); }, []);
useEffect(() => {
onDataChange({ ...data, ingredients: selectedIngredients });
}, [selectedIngredients]);
const fetchIngredients = async () => { const fetchIngredients = async () => {
if (!currentTenant?.id) return; if (!currentTenant?.id) return;
setLoading(true); setLoading(true);
try { try {
const result = await inventoryService.getIngredients(currentTenant.id); const result = await inventoryService.getIngredients(currentTenant.id);
// Filter out finished products - we only want raw ingredients
const rawIngredients = result.filter(ing => ing.category !== 'finished_product'); const rawIngredients = result.filter(ing => ing.category !== 'finished_product');
setIngredients(rawIngredients); setIngredients(rawIngredients);
} catch (err) { } catch (err) {
setError('Error al cargar ingredientes'); setError('Error loading ingredients');
console.error('Error loading ingredients:', err); console.error('Error loading ingredients:', err);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -215,25 +613,6 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNext
setSelectedIngredients(selectedIngredients.filter(ing => ing.id !== id)); setSelectedIngredients(selectedIngredients.filter(ing => ing.id !== id));
}; };
const handleContinue = () => {
if (selectedIngredients.length === 0) {
setError('Debes agregar al menos un ingrediente');
return;
}
// Validate all ingredients are filled
const invalidIngredients = selectedIngredients.filter(
ing => !ing.ingredientId || ing.quantity <= 0
);
if (invalidIngredients.length > 0) {
setError('Todos los ingredientes deben tener un ingrediente seleccionado y cantidad mayor a 0');
return;
}
onDataChange({ ...data, ingredients: selectedIngredients });
onNext();
};
const filteredIngredients = ingredients.filter(ing => const filteredIngredients = ingredients.filter(ing =>
ing.name.toLowerCase().includes(searchTerm.toLowerCase()) ing.name.toLowerCase().includes(searchTerm.toLowerCase())
); );
@@ -242,7 +621,7 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNext
<div className="space-y-6"> <div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]"> <div className="text-center pb-4 border-b border-[var(--border-primary)]">
<Package className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" /> <Package className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">Ingredientes</h3> <h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">Ingredients</h3>
<p className="text-sm text-[var(--text-secondary)]">{data.name}</p> <p className="text-sm text-[var(--text-secondary)]">{data.name}</p>
</div> </div>
@@ -255,7 +634,7 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNext
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-primary)]" /> <Loader2 className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
<span className="ml-3 text-[var(--text-secondary)]">Cargando ingredientes...</span> <span className="ml-3 text-[var(--text-secondary)]">Loading ingredients...</span>
</div> </div>
) : ( ) : (
<> <>
@@ -263,15 +642,15 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNext
{selectedIngredients.length === 0 ? ( {selectedIngredients.length === 0 ? (
<div className="text-center py-8 border-2 border-dashed border-[var(--border-secondary)] rounded-lg"> <div className="text-center py-8 border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
<Package className="w-12 h-12 mx-auto mb-3 text-[var(--text-tertiary)]" /> <Package className="w-12 h-12 mx-auto mb-3 text-[var(--text-tertiary)]" />
<p className="text-[var(--text-secondary)] mb-2">No hay ingredientes agregados</p> <p className="text-[var(--text-secondary)] mb-2">No ingredients added</p>
<p className="text-sm text-[var(--text-tertiary)]">Haz clic en "Agregar Ingrediente" para comenzar</p> <p className="text-sm text-[var(--text-tertiary)]">Click "Add Ingredient" to begin</p>
</div> </div>
) : ( ) : (
selectedIngredients.map((selectedIng) => ( selectedIngredients.map((selectedIng) => (
<div key={selectedIng.id} className="p-4 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]"> <div key={selectedIng.id} className="p-4 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]">
<div className="grid grid-cols-1 md:grid-cols-12 gap-3 items-start"> <div className="grid grid-cols-1 md:grid-cols-12 gap-3 items-start">
<div className="md:col-span-5"> <div className="md:col-span-5">
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">Ingrediente *</label> <label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">Ingredient *</label>
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-[var(--text-tertiary)]" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-[var(--text-tertiary)]" />
<select <select
@@ -279,7 +658,7 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNext
onChange={(e) => handleUpdateIngredient(selectedIng.id, 'ingredientId', e.target.value)} onChange={(e) => handleUpdateIngredient(selectedIng.id, 'ingredientId', e.target.value)}
className="w-full pl-9 pr-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm" className="w-full pl-9 pr-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm"
> >
<option value="">Seleccionar...</option> <option value="">Select...</option>
{filteredIngredients.map(ing => ( {filteredIngredients.map(ing => (
<option key={ing.id} value={ing.id}> <option key={ing.id} value={ing.id}>
{ing.name} {ing.category ? `(${ing.category})` : ''} {ing.name} {ing.category ? `(${ing.category})` : ''}
@@ -289,7 +668,7 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNext
</div> </div>
</div> </div>
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">Cantidad *</label> <label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">Quantity *</label>
<input <input
type="number" type="number"
value={selectedIng.quantity || ''} value={selectedIng.quantity || ''}
@@ -301,38 +680,39 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNext
/> />
</div> </div>
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">Unidad *</label> <label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">Unit *</label>
<select <select
value={selectedIng.unit} value={selectedIng.unit}
onChange={(e) => handleUpdateIngredient(selectedIng.id, 'unit', e.target.value as MeasurementUnit)} onChange={(e) => handleUpdateIngredient(selectedIng.id, 'unit', e.target.value as MeasurementUnit)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm" className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm"
> >
<option value={MeasurementUnit.GRAMS}>Gramos (g)</option> <option value={MeasurementUnit.GRAMS}>Grams (g)</option>
<option value={MeasurementUnit.KILOGRAMS}>Kilogramos (kg)</option> <option value={MeasurementUnit.KILOGRAMS}>Kilograms (kg)</option>
<option value={MeasurementUnit.MILLILITERS}>Mililitros (ml)</option> <option value={MeasurementUnit.MILLILITERS}>Milliliters (ml)</option>
<option value={MeasurementUnit.LITERS}>Litros (l)</option> <option value={MeasurementUnit.LITERS}>Liters (l)</option>
<option value={MeasurementUnit.UNITS}>Unidades</option> <option value={MeasurementUnit.UNITS}>Units</option>
<option value={MeasurementUnit.PIECES}>Piezas</option> <option value={MeasurementUnit.PIECES}>Pieces</option>
<option value={MeasurementUnit.CUPS}>Tazas</option> <option value={MeasurementUnit.CUPS}>Cups</option>
<option value={MeasurementUnit.TABLESPOONS}>Cucharadas</option> <option value={MeasurementUnit.TABLESPOONS}>Tablespoons</option>
<option value={MeasurementUnit.TEASPOONS}>Cucharaditas</option> <option value={MeasurementUnit.TEASPOONS}>Teaspoons</option>
</select> </select>
</div> </div>
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">Notas</label> <label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">Notes</label>
<input <input
type="text" type="text"
value={selectedIng.notes} value={selectedIng.notes}
onChange={(e) => handleUpdateIngredient(selectedIng.id, 'notes', e.target.value)} onChange={(e) => handleUpdateIngredient(selectedIng.id, 'notes', e.target.value)}
placeholder="Opcional" placeholder="Optional"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm" className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm"
/> />
</div> </div>
<div className="md:col-span-1 flex items-end"> <div className="md:col-span-1 flex items-end">
<button <button
type="button"
onClick={() => handleRemoveIngredient(selectedIng.id)} onClick={() => handleRemoveIngredient(selectedIng.id)}
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors" className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Eliminar ingrediente" title="Remove ingredient"
> >
<X className="w-5 h-5" /> <X className="w-5 h-5" />
</button> </button>
@@ -344,29 +724,19 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNext
</div> </div>
<button <button
type="button"
onClick={handleAddIngredient} onClick={handleAddIngredient}
className="w-full py-3 border-2 border-dashed border-[var(--border-secondary)] rounded-lg text-[var(--text-secondary)] hover:border-[var(--color-primary)] hover:text-[var(--color-primary)] transition-colors inline-flex items-center justify-center gap-2" className="w-full py-3 border-2 border-dashed border-[var(--border-secondary)] rounded-lg text-[var(--text-secondary)] hover:border-[var(--color-primary)] hover:text-[var(--color-primary)] transition-colors inline-flex items-center justify-center gap-2"
> >
<Plus className="w-5 h-5" /> <Plus className="w-5 h-5" />
Agregar Ingrediente Add Ingredient
</button> </button>
</> </>
)} )}
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-primary)]">
<button
onClick={handleContinue}
disabled={selectedIngredients.length === 0 || loading}
className="px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Continuar
</button>
</div>
</div> </div>
); );
}; };
// Step 3: Quality Templates Selection
const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => { const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
const { currentTenant } = useTenant(); const { currentTenant } = useTenant();
const [templates, setTemplates] = useState<QualityCheckTemplateResponse[]>([]); const [templates, setTemplates] = useState<QualityCheckTemplateResponse[]>([]);
@@ -379,15 +749,19 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
fetchTemplates(); fetchTemplates();
}, []); }, []);
useEffect(() => {
onDataChange({ ...data, selectedTemplates: selectedTemplateIds });
}, [selectedTemplateIds]);
const fetchTemplates = async () => { const fetchTemplates = async () => {
if (!currentTenant?.id) return; if (!currentTenant?.id) return;
setLoading(true); setLoading(true);
try { try {
const result = await qualityTemplateService.getTemplates(currentTenant.id, { is_active: true }); const result = await qualityTemplateService.getTemplates(currentTenant.id, { is_active: true });
setTemplates(result); setTemplates(result.templates || []);
} catch (err) { } catch (err) {
console.error('Error loading quality templates:', err); console.error('Error loading quality templates:', err);
setError('Error al cargar plantillas de calidad'); setError('Error loading quality templates');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -403,7 +777,7 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
const handleCreateRecipe = async () => { const handleCreateRecipe = async () => {
if (!currentTenant?.id) { if (!currentTenant?.id) {
setError('No se pudo obtener información del tenant'); setError('Could not obtain tenant information');
return; return;
} }
@@ -411,7 +785,6 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
setError(null); setError(null);
try { try {
// Prepare recipe ingredients
const recipeIngredients: RecipeIngredientCreate[] = data.ingredients.map((ing: any, index: number) => ({ const recipeIngredients: RecipeIngredientCreate[] = data.ingredients.map((ing: any, index: number) => ({
ingredient_id: ing.ingredientId, ingredient_id: ing.ingredientId,
quantity: ing.quantity, quantity: ing.quantity,
@@ -421,7 +794,6 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
ingredient_order: index + 1, ingredient_order: index + 1,
})); }));
// Prepare quality configuration if templates are selected
let qualityConfig: RecipeQualityConfiguration | undefined; let qualityConfig: RecipeQualityConfiguration | undefined;
if (selectedTemplateIds.length > 0) { if (selectedTemplateIds.length > 0) {
qualityConfig = { qualityConfig = {
@@ -447,19 +819,40 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
finished_product_id: data.finishedProductId, finished_product_id: data.finishedProductId,
yield_quantity: parseFloat(data.yieldQuantity), yield_quantity: parseFloat(data.yieldQuantity),
yield_unit: data.yieldUnit as MeasurementUnit, yield_unit: data.yieldUnit as MeasurementUnit,
version: data.version || '1.0',
difficulty_level: data.difficultyLevel || 3,
prep_time_minutes: data.prepTime ? parseInt(data.prepTime) : null, prep_time_minutes: data.prepTime ? parseInt(data.prepTime) : null,
cook_time_minutes: data.cookTime ? parseInt(data.cookTime) : null,
rest_time_minutes: data.restTime ? parseInt(data.restTime) : null,
total_time_minutes: data.totalTime ? parseInt(data.totalTime) : null,
recipe_code: data.recipeCode || null,
description: data.description || null,
preparation_notes: data.preparationNotes || null,
storage_instructions: data.storageInstructions || null,
serves_count: data.servesCount ? parseInt(data.servesCount) : null,
batch_size_multiplier: data.batchSizeMultiplier || 1.0,
minimum_batch_size: data.minBatchSize ? parseFloat(data.minBatchSize) : null,
maximum_batch_size: data.maxBatchSize ? parseFloat(data.maxBatchSize) : null,
optimal_production_temperature: data.optimalProductionTemp ? parseFloat(data.optimalProductionTemp) : null,
optimal_humidity: data.optimalHumidity ? parseFloat(data.optimalHumidity) : null,
target_margin_percentage: data.targetMargin ? parseFloat(data.targetMargin) : null,
is_seasonal: data.isSeasonal || false,
is_signature_item: data.isSignatureItem || false,
season_start_month: data.seasonStartMonth ? parseInt(data.seasonStartMonth) : null,
season_end_month: data.seasonEndMonth ? parseInt(data.seasonEndMonth) : null,
instructions: data.instructions ? { steps: data.instructions } : null, instructions: data.instructions ? { steps: data.instructions } : null,
allergen_info: data.allergens ? data.allergens.split(',').map((a: string) => a.trim()) : null,
dietary_tags: data.dietaryTags ? data.dietaryTags.split(',').map((t: string) => t.trim()) : null,
ingredients: recipeIngredients, ingredients: recipeIngredients,
quality_check_configuration: qualityConfig, quality_check_configuration: qualityConfig,
}; };
await recipesService.createRecipe(currentTenant.id, recipeData); await recipesService.createRecipe(currentTenant.id, recipeData);
showToast.success('Receta creada exitosamente'); showToast.success('Recipe created successfully');
onDataChange({ ...data, selectedTemplates: selectedTemplateIds });
onComplete(); onComplete();
} catch (err: any) { } catch (err: any) {
console.error('Error creating recipe:', err); console.error('Error creating recipe:', err);
const errorMessage = err.response?.data?.detail || 'Error al crear la receta'; const errorMessage = err.response?.data?.detail || 'Error creating recipe';
setError(errorMessage); setError(errorMessage);
showToast.error(errorMessage); showToast.error(errorMessage);
} finally { } finally {
@@ -472,10 +865,10 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
<div className="text-center pb-4 border-b border-[var(--border-primary)]"> <div className="text-center pb-4 border-b border-[var(--border-primary)]">
<ClipboardCheck className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" /> <ClipboardCheck className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2"> <h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
Plantillas de Calidad (Opcional) Quality Templates (Optional)
</h3> </h3>
<p className="text-sm text-[var(--text-secondary)]"> <p className="text-sm text-[var(--text-secondary)]">
Selecciona las plantillas de control de calidad que aplicarán a esta receta Select quality control templates to apply to this recipe
</p> </p>
</div> </div>
@@ -488,16 +881,16 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-primary)]" /> <Loader2 className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
<span className="ml-3 text-[var(--text-secondary)]">Cargando plantillas...</span> <span className="ml-3 text-[var(--text-secondary)]">Loading templates...</span>
</div> </div>
) : ( ) : (
<> <>
{templates.length === 0 ? ( {templates.length === 0 ? (
<div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-lg"> <div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
<ClipboardCheck className="w-12 h-12 mx-auto mb-3 text-[var(--text-tertiary)]" /> <ClipboardCheck className="w-12 h-12 mx-auto mb-3 text-[var(--text-tertiary)]" />
<p className="text-[var(--text-secondary)] mb-2">No hay plantillas de calidad disponibles</p> <p className="text-[var(--text-secondary)] mb-2">No quality templates available</p>
<p className="text-sm text-[var(--text-tertiary)]"> <p className="text-sm text-[var(--text-tertiary)]">
Puedes crear plantillas desde el wizard principal You can create templates from the main wizard
</p> </p>
</div> </div>
) : ( ) : (
@@ -505,6 +898,7 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
{templates.map((template) => ( {templates.map((template) => (
<button <button
key={template.id} key={template.id}
type="button"
onClick={() => toggleTemplate(template.id)} onClick={() => toggleTemplate(template.id)}
className={`w-full p-4 rounded-lg border-2 transition-all text-left ${ className={`w-full p-4 rounded-lg border-2 transition-all text-left ${
selectedTemplateIds.includes(template.id) selectedTemplateIds.includes(template.id)
@@ -518,7 +912,7 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
<h4 className="font-semibold text-[var(--text-primary)]">{template.name}</h4> <h4 className="font-semibold text-[var(--text-primary)]">{template.name}</h4>
{template.is_required && ( {template.is_required && (
<span className="px-2 py-0.5 text-xs bg-orange-100 text-orange-700 rounded-full"> <span className="px-2 py-0.5 text-xs bg-orange-100 text-orange-700 rounded-full">
Obligatorio Required
</span> </span>
)} )}
</div> </div>
@@ -528,9 +922,9 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
</p> </p>
)} )}
<div className="flex items-center gap-3 mt-2 text-xs text-[var(--text-tertiary)]"> <div className="flex items-center gap-3 mt-2 text-xs text-[var(--text-tertiary)]">
<span>Tipo: {template.check_type}</span> <span>Type: {template.check_type}</span>
{template.frequency_days && ( {template.frequency_days && (
<span> Cada {template.frequency_days} días</span> <span> Every {template.frequency_days} days</span>
)} )}
</div> </div>
</div> </div>
@@ -546,16 +940,16 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
{selectedTemplateIds.length > 0 && ( {selectedTemplateIds.length > 0 && (
<div className="p-4 bg-[var(--color-primary)]/5 rounded-lg border border-[var(--color-primary)]/20"> <div className="p-4 bg-[var(--color-primary)]/5 rounded-lg border border-[var(--color-primary)]/20">
<p className="text-sm text-[var(--text-primary)]"> <p className="text-sm text-[var(--text-primary)]">
<strong>{selectedTemplateIds.length}</strong> plantilla(s) seleccionada(s) <strong>{selectedTemplateIds.length}</strong> template(s) selected
</p> </p>
</div> </div>
)} )}
</> </>
)} )}
{/* Action Buttons */} <div className="flex justify-center pt-4 border-t border-[var(--border-primary)]">
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-primary)]">
<button <button
type="button"
onClick={handleCreateRecipe} onClick={handleCreateRecipe}
disabled={saving || loading} disabled={saving || loading}
className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-semibold inline-flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-semibold inline-flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
@@ -563,12 +957,12 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
{saving ? ( {saving ? (
<> <>
<Loader2 className="w-5 h-5 animate-spin" /> <Loader2 className="w-5 h-5 animate-spin" />
Creando receta... Creating recipe...
</> </>
) : ( ) : (
<> <>
<CheckCircle2 className="w-5 h-5" /> <CheckCircle2 className="w-5 h-5" />
Crear Receta Create Recipe
</> </>
)} )}
</button> </button>
@@ -578,7 +972,35 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
}; };
export const RecipeWizardSteps = (data: Record<string, any>, setData: (data: Record<string, any>) => void): WizardStep[] => [ export const RecipeWizardSteps = (data: Record<string, any>, setData: (data: Record<string, any>) => void): WizardStep[] => [
{ id: 'recipe-details', title: 'Detalles de la Receta', description: 'Nombre, categoría, rendimiento', component: (props) => <RecipeDetailsStep {...props} data={data} onDataChange={setData} /> }, {
{ id: 'recipe-ingredients', title: 'Ingredientes', description: 'Selección y cantidades', component: (props) => <IngredientsStep {...props} data={data} onDataChange={setData} /> }, id: 'recipe-details',
{ id: 'recipe-quality-templates', title: 'Plantillas de Calidad', description: 'Controles de calidad aplicables', component: (props) => <QualityTemplatesStep {...props} data={data} onDataChange={setData} /> }, title: 'Recipe Details',
description: 'Name, category, yield',
component: (props) => <RecipeDetailsStep {...props} data={data} onDataChange={setData} />,
validate: () => {
return !!(data.name && data.finishedProductId && data.yieldQuantity);
},
},
{
id: 'recipe-ingredients',
title: 'Ingredients',
description: 'Selection and quantities',
component: (props) => <IngredientsStep {...props} data={data} onDataChange={setData} />,
validate: () => {
if (!data.ingredients || data.ingredients.length === 0) {
return false;
}
const invalidIngredients = data.ingredients.filter(
(ing: any) => !ing.ingredientId || ing.quantity <= 0
);
return invalidIngredients.length === 0;
},
},
{
id: 'recipe-quality-templates',
title: 'Quality Templates',
description: 'Applicable quality controls',
component: (props) => <QualityTemplatesStep {...props} data={data} onDataChange={setData} />,
isOptional: true,
},
]; ];

View File

@@ -0,0 +1,67 @@
import React, { useState } from 'react';
import { ChevronDown, ChevronUp } from 'lucide-react';
interface AdvancedOptionsSectionProps {
children: React.ReactNode;
title?: string;
description?: string;
defaultExpanded?: boolean;
}
export const AdvancedOptionsSection: React.FC<AdvancedOptionsSectionProps> = ({
children,
title = 'Advanced Options',
description = 'These fields are optional but help improve data management',
defaultExpanded = false,
}) => {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
return (
<div className="space-y-4">
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="w-full py-3 px-4 border-2 border-dashed border-[var(--border-secondary)] rounded-lg text-[var(--text-secondary)] hover:border-[var(--color-primary)] hover:text-[var(--color-primary)] transition-colors inline-flex items-center justify-center gap-2 font-medium"
>
{isExpanded ? (
<>
<ChevronUp className="w-5 h-5" />
Hide {title}
</>
) : (
<>
<ChevronDown className="w-5 h-5" />
Show {title}
</>
)}
</button>
{isExpanded && (
<div className="space-y-4 p-4 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]/30 animate-slideDown">
{description && (
<p className="text-sm text-[var(--text-secondary)] pb-2 border-b border-[var(--border-primary)]">
{description}
</p>
)}
{children}
</div>
)}
<style>{`
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-slideDown {
animation: slideDown 0.2s ease-out;
}
`}</style>
</div>
);
};

View File

@@ -0,0 +1 @@
export { AdvancedOptionsSection } from './AdvancedOptionsSection';