Files
bakery-ia/frontend/src/components/domain/procurement/CreatePurchaseOrderModal.tsx

356 lines
12 KiB
TypeScript
Raw Normal View History

2025-09-26 07:46:25 +02:00
import React, { useState, useEffect, useMemo } from 'react';
import { Plus, Package, Calendar, Building2 } from 'lucide-react';
import { AddModal } from '../../ui/AddModal/AddModal';
2025-09-23 12:49:35 +02:00
import { useSuppliers } from '../../../api/hooks/suppliers';
import { useCreatePurchaseOrder } from '../../../api/hooks/suppliers';
2025-09-26 07:46:25 +02:00
import { useIngredients } from '../../../api/hooks/inventory';
2025-09-23 12:49:35 +02:00
import { useTenantStore } from '../../../stores/tenant.store';
import type { ProcurementRequirementResponse, PurchaseOrderItem } from '../../../api/types/orders';
import type { SupplierSummary } from '../../../api/types/suppliers';
2025-09-26 07:46:25 +02:00
import type { IngredientResponse } from '../../../api/types/inventory';
import { statusColors } from '../../../styles/colors';
2025-09-23 12:49:35 +02:00
interface CreatePurchaseOrderModalProps {
isOpen: boolean;
onClose: () => void;
requirements: ProcurementRequirementResponse[];
onSuccess?: () => void;
}
2025-09-26 07:46:25 +02:00
2025-09-23 12:49:35 +02:00
/**
* CreatePurchaseOrderModal - Modal for creating purchase orders from procurement requirements
* Allows supplier selection and purchase order creation for ingredients
* Can also be used for manual purchase order creation when no requirements are provided
*/
export const CreatePurchaseOrderModal: React.FC<CreatePurchaseOrderModalProps> = ({
isOpen,
onClose,
requirements,
onSuccess
}) => {
const [loading, setLoading] = useState(false);
2025-09-26 07:46:25 +02:00
const [selectedSupplier, setSelectedSupplier] = useState<string>('');
2025-09-23 12:49:35 +02:00
// Get current tenant
const { currentTenant } = useTenantStore();
const tenantId = currentTenant?.id || '';
// Fetch suppliers (without status filter to avoid backend enum issue)
const { data: suppliersData, isLoading: isLoadingSuppliers, isError: isSuppliersError, error: suppliersError } = useSuppliers(
tenantId,
{ limit: 100 },
{ enabled: !!tenantId && isOpen }
);
2025-09-24 15:40:32 +02:00
const suppliers = (suppliersData || []).filter(supplier => supplier.status === 'active');
2025-09-23 12:49:35 +02:00
2025-09-26 07:46:25 +02:00
// Fetch ingredients filtered by selected supplier (only when manually adding products)
const { data: ingredientsData = [] } = useIngredients(
tenantId,
selectedSupplier ? { supplier_id: selectedSupplier } : {},
{ enabled: !!tenantId && isOpen && !requirements?.length && !!selectedSupplier }
);
2025-09-23 12:49:35 +02:00
// Create purchase order mutation
const createPurchaseOrderMutation = useCreatePurchaseOrder();
2025-09-26 07:46:25 +02:00
const supplierOptions = useMemo(() => suppliers.map(supplier => ({
value: supplier.id,
label: `${supplier.name} (${supplier.supplier_code})`
})), [suppliers]);
// Create ingredient options from supplier-filtered ingredients
const ingredientOptions = useMemo(() => ingredientsData.map(ingredient => ({
value: ingredient.id,
label: ingredient.name,
data: ingredient // Store full ingredient data for later use
})), [ingredientsData]);
// Unit options for select field
const unitOptions = [
{ value: 'kg', label: 'Kilogramos' },
{ value: 'g', label: 'Gramos' },
{ value: 'l', label: 'Litros' },
{ value: 'ml', label: 'Mililitros' },
{ value: 'units', label: 'Unidades' },
{ value: 'boxes', label: 'Cajas' },
{ value: 'bags', label: 'Bolsas' }
];
const handleSave = async (formData: Record<string, any>) => {
setLoading(true);
2025-09-23 12:49:35 +02:00
2025-09-26 07:46:25 +02:00
// Update selectedSupplier if it changed
if (formData.supplier_id && formData.supplier_id !== selectedSupplier) {
setSelectedSupplier(formData.supplier_id);
2025-09-23 12:49:35 +02:00
}
2025-09-26 07:46:25 +02:00
try {
let items: PurchaseOrderItem[] = [];
if (requirements && requirements.length > 0) {
// Create items from requirements list
const requiredIngredients = formData.required_ingredients || [];
if (requiredIngredients.length === 0) {
throw new Error('Por favor, selecciona al menos un ingrediente');
}
// Validate quantities
const invalidQuantities = requiredIngredients.some((item: any) => item.quantity <= 0);
if (invalidQuantities) {
throw new Error('Todas las cantidades deben ser mayores a 0');
}
// Prepare purchase order items from requirements
items = requiredIngredients.map((item: any) => {
// Find original requirement to get product_id
const originalReq = requirements.find(req => req.id === item.id);
return {
inventory_product_id: originalReq?.product_id || '',
product_code: item.product_sku || '',
product_name: item.product_name,
ordered_quantity: item.quantity,
unit_of_measure: item.unit_of_measure,
unit_price: item.unit_price,
quality_requirements: originalReq?.quality_specifications ? JSON.stringify(originalReq.quality_specifications) : undefined,
notes: originalReq?.special_requirements || undefined
};
});
} else {
// Create items from manual entries
const manualProducts = formData.manual_products || [];
if (manualProducts.length === 0) {
throw new Error('Por favor, agrega al menos un producto');
}
// Validate quantities for manual items
const invalidQuantities = manualProducts.some((item: any) => item.quantity <= 0);
if (invalidQuantities) {
throw new Error('Todas las cantidades deben ser mayores a 0');
}
// Validate required fields
const invalidProducts = manualProducts.some((item: any) => !item.ingredient_id);
if (invalidProducts) {
throw new Error('Todos los productos deben tener un ingrediente seleccionado');
}
// Prepare purchase order items from manual entries with ingredient data
items = manualProducts.map((item: any) => {
// Find the selected ingredient data
const selectedIngredient = ingredientsData.find(ing => ing.id === item.ingredient_id);
return {
inventory_product_id: item.ingredient_id,
product_code: selectedIngredient?.sku || '',
product_name: selectedIngredient?.name || 'Ingrediente desconocido',
ordered_quantity: item.quantity,
unit_of_measure: item.unit_of_measure,
unit_price: item.unit_price,
quality_requirements: undefined,
notes: undefined
};
});
2025-09-23 12:49:35 +02:00
}
// Create purchase order
await createPurchaseOrderMutation.mutateAsync({
2025-09-26 07:46:25 +02:00
supplier_id: formData.supplier_id,
2025-09-23 12:49:35 +02:00
priority: 'normal',
2025-09-26 07:46:25 +02:00
required_delivery_date: formData.delivery_date || undefined,
notes: formData.notes || undefined,
2025-09-23 12:49:35 +02:00
items
});
2025-09-26 07:46:25 +02:00
// Purchase order created successfully
// Trigger success callback
2025-09-23 12:49:35 +02:00
if (onSuccess) {
onSuccess();
}
2025-09-26 07:46:25 +02:00
} catch (error) {
console.error('Error creating purchase order:', error);
throw error; // Let AddModal handle error display
2025-09-23 12:49:35 +02:00
} finally {
setLoading(false);
}
};
2025-09-26 07:46:25 +02:00
const statusConfig = {
color: statusColors.inProgress.primary,
text: 'Nueva Orden',
icon: Plus,
isCritical: false,
isHighlight: true
};
2025-09-23 12:49:35 +02:00
2025-09-26 07:46:25 +02:00
const sections = [
{
title: 'Información del Proveedor',
icon: Building2,
fields: [
{
label: 'Proveedor',
name: 'supplier_id',
type: 'select' as const,
required: true,
options: supplierOptions,
placeholder: 'Seleccionar proveedor...',
span: 2
}
]
},
{
title: 'Detalles de la Orden',
icon: Calendar,
fields: [
{
label: 'Fecha de Entrega Requerida',
name: 'delivery_date',
type: 'date' as const,
helpText: 'Fecha límite para la entrega (opcional)'
},
{
label: 'Notas',
name: 'notes',
type: 'textarea' as const,
placeholder: 'Instrucciones especiales para el proveedor...',
span: 2,
helpText: 'Información adicional o instrucciones especiales'
}
]
},
{
title: requirements && requirements.length > 0 ? 'Ingredientes Requeridos' : 'Productos a Comprar',
icon: Package,
fields: [
requirements && requirements.length > 0 ? {
label: 'Ingredientes Requeridos',
name: 'required_ingredients',
type: 'list' as const,
span: 2,
defaultValue: requirements.map(req => ({
id: req.id,
product_name: req.product_name,
product_sku: req.product_sku || '',
quantity: req.approved_quantity || req.net_requirement || req.required_quantity,
unit_of_measure: req.unit_of_measure,
unit_price: req.estimated_unit_cost || 0,
selected: true
})),
listConfig: {
itemFields: [
{
name: 'product_name',
label: 'Producto',
type: 'text',
required: false // Read-only display
},
{
name: 'product_sku',
label: 'SKU',
type: 'text',
required: false
},
{
name: 'quantity',
label: 'Cantidad Requerida',
type: 'number',
required: true
},
{
name: 'unit_of_measure',
label: 'Unidad',
type: 'text',
required: false
},
{
name: 'unit_price',
label: 'Precio Est. (€)',
type: 'currency',
required: true
}
],
addButtonLabel: 'Agregar Ingrediente',
emptyStateText: 'No hay ingredientes requeridos',
showSubtotals: true,
subtotalFields: { quantity: 'quantity', price: 'unit_price' }
},
helpText: 'Revisa y ajusta las cantidades y precios de los ingredientes requeridos'
} : {
label: 'Productos a Comprar',
name: 'manual_products',
type: 'list' as const,
span: 2,
defaultValue: [],
listConfig: {
itemFields: [
{
name: 'ingredient_id',
label: 'Ingrediente',
type: 'select',
required: true,
options: ingredientOptions,
placeholder: 'Seleccionar ingrediente...',
disabled: false
},
{
name: 'quantity',
label: 'Cantidad',
type: 'number',
required: true,
defaultValue: 1
},
{
name: 'unit_of_measure',
label: 'Unidad',
type: 'select',
required: true,
defaultValue: 'kg',
options: unitOptions
},
{
name: 'unit_price',
label: 'Precio Unitario (€)',
type: 'currency',
required: true,
defaultValue: 0,
placeholder: '0.00'
}
],
addButtonLabel: 'Agregar Ingrediente',
emptyStateText: 'No hay ingredientes disponibles para este proveedor',
showSubtotals: true,
subtotalFields: { quantity: 'quantity', price: 'unit_price' },
disabled: !selectedSupplier
},
helpText: 'Selecciona ingredientes disponibles del proveedor seleccionado'
}
]
},
];
2025-09-23 12:49:35 +02:00
2025-09-26 07:46:25 +02:00
return (
<>
<AddModal
isOpen={isOpen}
onClose={onClose}
title="Crear Orden de Compra"
subtitle={requirements && requirements.length > 0
? "Generar orden de compra desde requerimientos de procuración"
: "Crear orden de compra manual"}
statusIndicator={statusConfig}
sections={sections}
size="xl"
loading={loading}
onSave={handleSave}
/>
</>
2025-09-23 12:49:35 +02:00
);
};
export default CreatePurchaseOrderModal;