Create the frontend receipes page to use real API

This commit is contained in:
Urtzi Alfaro
2025-09-19 21:39:04 +02:00
parent 8002d89d2b
commit d18c64ce6e
36 changed files with 3356 additions and 3171 deletions

View File

@@ -1,6 +1,6 @@
import React, { useState, useCallback, useEffect } from 'react';
import { Card, Button, Badge, Input, Modal, Table, Select, DatePicker } from '../../ui';
import { productionService, type ProductionBatchResponse, ProductionBatchStatus, ProductionPriority } from '../../../api/services/production.service';
import { productionService, type ProductionBatchResponse, ProductionBatchStatus, ProductionPriority } from '../../../api';
import type { ProductionBatch, QualityCheck } from '../../../types/production.types';
interface BatchTrackerProps {

View File

@@ -1,6 +1,6 @@
import React, { useState, useCallback, useMemo } from 'react';
import { Card, Button, Badge, Select, DatePicker, Input, Modal, Table } from '../../ui';
import { productionService, type ProductionScheduleEntry, ProductionBatchStatus } from '../../../api/services/production.service';
import { productionService, type ProductionScheduleEntry, ProductionBatchStatus } from '../../../api';
import type { ProductionSchedule as ProductionScheduleType, ProductionBatch, EquipmentReservation, StaffAssignment } from '../../../types/production.types';
interface ProductionScheduleProps {

View File

@@ -1,6 +1,6 @@
import React, { useState, useCallback, useRef } from 'react';
import { Card, Button, Badge, Input, Modal, Table, Select, DatePicker } from '../../ui';
import { productionService, type QualityCheckResponse, QualityCheckStatus } from '../../../api/services/production.service';
import { productionService, type QualityCheckResponse, QualityCheckStatus } from '../../../api';
import type { QualityCheck, QualityCheckCriteria, ProductionBatch, QualityCheckType } from '../../../types/production.types';
interface QualityControlProps {

View File

@@ -0,0 +1,622 @@
import React, { useState, useEffect, useMemo } from 'react';
import { ChefHat, Package, Clock, DollarSign, Star } from 'lucide-react';
import { StatusModal } from '../../ui/StatusModal/StatusModal';
import { RecipeCreate, RecipeIngredientCreate, MeasurementUnit } from '../../../api/types/recipes';
import { useIngredients } from '../../../api/hooks/inventory';
import { useCurrentTenant } from '../../../stores/tenant.store';
interface CreateRecipeModalProps {
isOpen: boolean;
onClose: () => void;
onCreateRecipe?: (recipeData: RecipeCreate) => Promise<void>;
}
/**
* CreateRecipeModal - Modal for creating a new recipe
* Comprehensive form for adding new recipes
*/
export const CreateRecipeModal: React.FC<CreateRecipeModalProps> = ({
isOpen,
onClose,
onCreateRecipe
}) => {
const [formData, setFormData] = useState<RecipeCreate>({
name: '',
recipe_code: '',
finished_product_id: '', // This should come from a product selector
description: '',
category: '',
cuisine_type: '',
difficulty_level: 1,
yield_quantity: 1,
yield_unit: MeasurementUnit.UNITS,
prep_time_minutes: 0,
cook_time_minutes: 0,
total_time_minutes: 0,
rest_time_minutes: 0,
estimated_cost_per_unit: 0,
target_margin_percentage: 30,
suggested_selling_price: 0,
preparation_notes: '',
storage_instructions: '',
quality_standards: '',
serves_count: 1,
is_seasonal: false,
season_start_month: undefined,
season_end_month: undefined,
is_signature_item: false,
batch_size_multiplier: 1.0,
minimum_batch_size: undefined,
maximum_batch_size: undefined,
optimal_production_temperature: undefined,
optimal_humidity: undefined,
allergen_info: '',
dietary_tags: '',
nutritional_info: '',
ingredients: []
});
const [loading, setLoading] = useState(false);
const [mode, setMode] = useState<'overview' | 'edit'>('edit');
// Get tenant and fetch inventory data
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
// Fetch inventory items to populate product and ingredient selectors
const {
data: inventoryItems = [],
isLoading: inventoryLoading
} = useIngredients(tenantId, {});
// Separate finished products and ingredients
const finishedProducts = useMemo(() =>
inventoryItems.filter(item => item.product_type === 'finished_product')
.map(product => ({
value: product.id,
label: `${product.name} (${product.category || 'Sin categoría'})`
})),
[inventoryItems]
);
const availableIngredients = useMemo(() =>
inventoryItems.filter(item => item.product_type === 'ingredient')
.map(ingredient => ({
value: ingredient.id,
label: `${ingredient.name} (${ingredient.unit_of_measure})`,
unit: ingredient.unit_of_measure
})),
[inventoryItems]
);
// Category options
const categoryOptions = [
{ value: 'bread', label: 'Pan' },
{ value: 'pastry', label: 'Bollería' },
{ value: 'cake', label: 'Tarta' },
{ value: 'cookie', label: 'Galleta' },
{ value: 'muffin', label: 'Muffin' },
{ value: 'savory', label: 'Salado' },
{ value: 'desserts', label: 'Postres' },
{ value: 'specialty', label: 'Especialidad' },
{ value: 'other', label: 'Otro' }
];
// Cuisine type options
const cuisineTypeOptions = [
{ value: 'french', label: 'Francés' },
{ value: 'spanish', label: 'Español' },
{ value: 'italian', label: 'Italiano' },
{ value: 'german', label: 'Alemán' },
{ value: 'american', label: 'Americano' },
{ value: 'artisanal', label: 'Artesanal' },
{ value: 'traditional', label: 'Tradicional' },
{ value: 'modern', label: 'Moderno' }
];
// Unit options
const unitOptions = [
{ value: MeasurementUnit.UNITS, label: 'Unidades' },
{ value: MeasurementUnit.PIECES, label: 'Piezas' },
{ value: MeasurementUnit.GRAMS, label: 'Gramos' },
{ value: MeasurementUnit.KILOGRAMS, label: 'Kilogramos' },
{ value: MeasurementUnit.MILLILITERS, label: 'Mililitros' },
{ value: MeasurementUnit.LITERS, label: 'Litros' }
];
// Month options for seasonal recipes
const monthOptions = [
{ value: 1, label: 'Enero' },
{ value: 2, label: 'Febrero' },
{ value: 3, label: 'Marzo' },
{ value: 4, label: 'Abril' },
{ value: 5, label: 'Mayo' },
{ value: 6, label: 'Junio' },
{ value: 7, label: 'Julio' },
{ value: 8, label: 'Agosto' },
{ value: 9, label: 'Septiembre' },
{ value: 10, label: 'Octubre' },
{ value: 11, label: 'Noviembre' },
{ value: 12, label: 'Diciembre' }
];
// Allergen options
const allergenOptions = [
'Gluten', 'Lácteos', 'Huevos', 'Frutos secos', 'Soja', 'Sésamo', 'Pescado', 'Mariscos'
];
// Dietary tags
const dietaryTagOptions = [
'Vegano', 'Vegetariano', 'Sin gluten', 'Sin lácteos', 'Sin frutos secos', 'Sin azúcar', 'Bajo en carbohidratos', 'Keto', 'Orgánico'
];
const handleFieldChange = (sectionIndex: number, fieldIndex: number, value: string | number | boolean) => {
const sections = getModalSections();
const field = sections[sectionIndex]?.fields[fieldIndex];
if (!field) return;
setFormData(prev => ({
...prev,
[field.key]: value
}));
// Auto-calculate total time when prep or cook time changes
if (field.key === 'prep_time_minutes' || field.key === 'cook_time_minutes') {
const prepTime = field.key === 'prep_time_minutes' ? Number(value) : formData.prep_time_minutes || 0;
const cookTime = field.key === 'cook_time_minutes' ? Number(value) : formData.cook_time_minutes || 0;
setFormData(prev => ({
...prev,
total_time_minutes: prepTime + cookTime
}));
}
};
const handleSubmit = async () => {
if (!formData.name.trim()) {
alert('El nombre de la receta es obligatorio');
return;
}
if (!formData.category.trim()) {
alert('Debe seleccionar una categoría');
return;
}
if (!formData.finished_product_id.trim()) {
alert('Debe seleccionar un producto terminado');
return;
}
// Validate seasonal dates if seasonal is enabled
if (formData.is_seasonal) {
if (!formData.season_start_month || !formData.season_end_month) {
alert('Para recetas estacionales, debe especificar los meses de inicio y fin');
return;
}
}
// Validate batch sizes
if (formData.minimum_batch_size && formData.maximum_batch_size) {
if (formData.minimum_batch_size > formData.maximum_batch_size) {
alert('El tamaño mínimo de lote no puede ser mayor que el máximo');
return;
}
}
try {
setLoading(true);
// Generate recipe code if not provided
const recipeCode = formData.recipe_code ||
formData.name.substring(0, 3).toUpperCase() +
String(Date.now()).slice(-3);
// Calculate total time including rest time
const totalTime = (formData.prep_time_minutes || 0) +
(formData.cook_time_minutes || 0) +
(formData.rest_time_minutes || 0);
const recipeData: RecipeCreate = {
...formData,
recipe_code: recipeCode,
total_time_minutes: totalTime,
// Clean up undefined values for optional fields
season_start_month: formData.is_seasonal ? formData.season_start_month : undefined,
season_end_month: formData.is_seasonal ? formData.season_end_month : undefined,
minimum_batch_size: formData.minimum_batch_size || undefined,
maximum_batch_size: formData.maximum_batch_size || undefined,
optimal_production_temperature: formData.optimal_production_temperature || undefined,
optimal_humidity: formData.optimal_humidity || undefined
};
if (onCreateRecipe) {
await onCreateRecipe(recipeData);
}
onClose();
// Reset form
setFormData({
name: '',
recipe_code: '',
finished_product_id: '',
description: '',
category: '',
cuisine_type: '',
difficulty_level: 1,
yield_quantity: 1,
yield_unit: MeasurementUnit.UNITS,
prep_time_minutes: 0,
cook_time_minutes: 0,
total_time_minutes: 0,
rest_time_minutes: 0,
estimated_cost_per_unit: 0,
target_margin_percentage: 30,
suggested_selling_price: 0,
preparation_notes: '',
storage_instructions: '',
quality_standards: '',
serves_count: 1,
is_seasonal: false,
season_start_month: undefined,
season_end_month: undefined,
is_signature_item: false,
batch_size_multiplier: 1.0,
minimum_batch_size: undefined,
maximum_batch_size: undefined,
optimal_production_temperature: undefined,
optimal_humidity: undefined,
allergen_info: '',
dietary_tags: '',
nutritional_info: '',
ingredients: []
});
} catch (error) {
console.error('Error creating recipe:', error);
alert('Error al crear la receta. Por favor, inténtelo de nuevo.');
} finally {
setLoading(false);
}
};
const getModalSections = () => [
{
title: 'Información Básica',
icon: ChefHat,
fields: [
{
key: 'name',
label: 'Nombre de la receta',
value: formData.name,
type: 'text',
required: true,
placeholder: 'Ej: Pan de molde integral'
},
{
key: 'recipe_code',
label: 'Código de receta',
value: formData.recipe_code,
type: 'text',
placeholder: 'Ej: PAN001 (opcional, se genera automáticamente)'
},
{
key: 'description',
label: 'Descripción',
value: formData.description,
type: 'textarea',
placeholder: 'Descripción de la receta...'
},
{
key: 'category',
label: 'Categoría',
value: formData.category,
type: 'select',
options: categoryOptions,
required: true
},
{
key: 'cuisine_type',
label: 'Tipo de cocina',
value: formData.cuisine_type,
type: 'select',
options: cuisineTypeOptions,
placeholder: 'Selecciona el tipo de cocina'
},
{
key: 'difficulty_level',
label: 'Nivel de dificultad',
value: formData.difficulty_level,
type: 'select',
options: [
{ value: 1, label: '1 - Fácil' },
{ value: 2, label: '2 - Medio' },
{ value: 3, label: '3 - Difícil' },
{ value: 4, label: '4 - Muy Difícil' },
{ value: 5, label: '5 - Extremo' }
],
required: true
},
{
key: 'serves_count',
label: 'Número de porciones',
value: formData.serves_count,
type: 'number',
min: 1,
placeholder: 'Cuántas personas sirve'
}
]
},
{
title: 'Rendimiento y Tiempos',
icon: Clock,
fields: [
{
key: 'yield_quantity',
label: 'Cantidad que produce',
value: formData.yield_quantity,
type: 'number',
min: 1,
required: true
},
{
key: 'yield_unit',
label: 'Unidad de medida',
value: formData.yield_unit,
type: 'select',
options: unitOptions,
required: true
},
{
key: 'prep_time_minutes',
label: 'Tiempo de preparación (minutos)',
value: formData.prep_time_minutes,
type: 'number',
min: 0
},
{
key: 'cook_time_minutes',
label: 'Tiempo de cocción (minutos)',
value: formData.cook_time_minutes,
type: 'number',
min: 0
},
{
key: 'rest_time_minutes',
label: 'Tiempo de reposo (minutos)',
value: formData.rest_time_minutes,
type: 'number',
min: 0,
placeholder: 'Tiempo de fermentación o reposo'
},
{
key: 'total_time_minutes',
label: 'Tiempo total (calculado automáticamente)',
value: formData.total_time_minutes,
type: 'number',
disabled: true,
readonly: true
}
]
},
{
title: 'Configuración Financiera',
icon: DollarSign,
fields: [
{
key: 'estimated_cost_per_unit',
label: 'Costo estimado por unidad (€)',
value: formData.estimated_cost_per_unit,
type: 'number',
min: 0,
step: 0.01,
placeholder: '0.00'
},
{
key: 'suggested_selling_price',
label: 'Precio de venta sugerido (€)',
value: formData.suggested_selling_price,
type: 'number',
min: 0,
step: 0.01,
placeholder: '0.00'
},
{
key: 'target_margin_percentage',
label: 'Margen objetivo (%)',
value: formData.target_margin_percentage,
type: 'number',
min: 0,
max: 100,
placeholder: '30'
}
]
},
{
title: 'Configuración de Producción',
icon: Package,
fields: [
{
key: 'batch_size_multiplier',
label: 'Multiplicador de lote',
value: formData.batch_size_multiplier,
type: 'number',
min: 0.1,
step: 0.1,
placeholder: '1.0'
},
{
key: 'minimum_batch_size',
label: 'Tamaño mínimo de lote',
value: formData.minimum_batch_size,
type: 'number',
min: 1,
placeholder: 'Cantidad mínima a producir'
},
{
key: 'maximum_batch_size',
label: 'Tamaño máximo de lote',
value: formData.maximum_batch_size,
type: 'number',
min: 1,
placeholder: 'Cantidad máxima a producir'
},
{
key: 'optimal_production_temperature',
label: 'Temperatura óptima (°C)',
value: formData.optimal_production_temperature,
type: 'number',
placeholder: 'Temperatura ideal de producción'
},
{
key: 'optimal_humidity',
label: 'Humedad óptima (%)',
value: formData.optimal_humidity,
type: 'number',
min: 0,
max: 100,
placeholder: 'Humedad ideal'
}
]
},
{
title: 'Temporalidad y Especiales',
icon: Star,
fields: [
{
key: 'is_signature_item',
label: 'Receta especial/estrella',
value: formData.is_signature_item,
type: 'checkbox'
},
{
key: 'is_seasonal',
label: 'Receta estacional',
value: formData.is_seasonal,
type: 'checkbox'
},
{
key: 'season_start_month',
label: 'Mes de inicio de temporada',
value: formData.season_start_month,
type: 'select',
options: monthOptions,
placeholder: 'Selecciona mes de inicio',
disabled: !formData.is_seasonal
},
{
key: 'season_end_month',
label: 'Mes de fin de temporada',
value: formData.season_end_month,
type: 'select',
options: monthOptions,
placeholder: 'Selecciona mes de fin',
disabled: !formData.is_seasonal
}
]
},
{
title: 'Información Nutricional y Alérgenos',
icon: Package,
fields: [
{
key: 'allergen_info',
label: 'Información de alérgenos',
value: formData.allergen_info,
type: 'text',
placeholder: 'Ej: Gluten, Lácteos, Huevos'
},
{
key: 'dietary_tags',
label: 'Etiquetas dietéticas',
value: formData.dietary_tags,
type: 'text',
placeholder: 'Ej: Vegano, Sin gluten, Orgánico'
},
{
key: 'nutritional_info',
label: 'Información nutricional',
value: formData.nutritional_info,
type: 'textarea',
placeholder: 'Calorías, proteínas, carbohidratos, etc.'
}
]
},
{
title: 'Notas de Preparación',
icon: ChefHat,
fields: [
{
key: 'preparation_notes',
label: 'Notas de preparación',
value: formData.preparation_notes,
type: 'textarea',
placeholder: 'Instrucciones especiales, consejos, técnicas...'
},
{
key: 'storage_instructions',
label: 'Instrucciones de almacenamiento',
value: formData.storage_instructions,
type: 'textarea',
placeholder: 'Cómo almacenar el producto terminado...'
},
{
key: 'quality_standards',
label: 'Estándares de calidad',
value: formData.quality_standards,
type: 'textarea',
placeholder: 'Criterios de calidad, características del producto...'
}
]
},
{
title: 'Producto Terminado',
icon: Package,
fields: [
{
key: 'finished_product_id',
label: 'Producto terminado',
value: formData.finished_product_id,
type: 'select',
options: finishedProducts,
required: true,
placeholder: inventoryLoading ? 'Cargando productos...' : 'Selecciona un producto terminado',
help: 'Selecciona el producto del inventario que produce esta receta',
disabled: inventoryLoading
}
]
}
];
return (
<StatusModal
isOpen={isOpen}
onClose={onClose}
mode={mode}
onModeChange={setMode}
title="Nueva Receta"
subtitle="Crear una nueva receta para la panadería"
statusIndicator={{
color: '#3b82f6',
text: 'Nueva',
icon: ChefHat,
isCritical: false,
isHighlight: true
}}
size="xl"
sections={getModalSections()}
onFieldChange={handleFieldChange}
actions={[
{
label: loading ? 'Creando...' : 'Crear Receta',
icon: ChefHat,
variant: 'primary',
onClick: handleSubmit,
disabled: loading || !formData.name.trim() || !formData.finished_product_id.trim()
}
]}
/>
);
};
export default CreateRecipeModal;

View File

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