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;
|