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' ;
2025-10-21 19:50:07 +02:00
import { suppliersService } from '../../../api/services/suppliers' ;
2025-09-23 12:49:35 +02:00
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-10-21 19:50:07 +02:00
const [ formData , setFormData ] = useState < Record < string , any > > ( { } ) ;
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-10-21 19:50:07 +02:00
// State for supplier products
const [ supplierProductIds , setSupplierProductIds ] = useState < string [ ] > ( [ ] ) ;
const [ isLoadingSupplierProducts , setIsLoadingSupplierProducts ] = useState ( false ) ;
// Fetch ALL ingredients (we'll filter client-side based on supplier products)
const { data : allIngredientsData = [ ] , isLoading : isLoadingIngredients } = useIngredients (
2025-09-26 07:46:25 +02:00
tenantId ,
2025-10-21 19:50:07 +02:00
{ } ,
{ enabled : ! ! tenantId && isOpen && ! requirements ? . length }
2025-09-26 07:46:25 +02:00
) ;
2025-10-21 19:50:07 +02:00
// Fetch supplier products when supplier is selected
useEffect ( ( ) = > {
const fetchSupplierProducts = async ( ) = > {
if ( ! selectedSupplier || ! tenantId ) {
setSupplierProductIds ( [ ] ) ;
return ;
}
setIsLoadingSupplierProducts ( true ) ;
try {
const products = await suppliersService . getSupplierProducts ( tenantId , selectedSupplier ) ;
const productIds = products . map ( p = > p . inventory_product_id ) ;
setSupplierProductIds ( productIds ) ;
} catch ( error ) {
console . error ( 'Error fetching supplier products:' , error ) ;
setSupplierProductIds ( [ ] ) ;
} finally {
setIsLoadingSupplierProducts ( false ) ;
}
} ;
fetchSupplierProducts ( ) ;
} , [ selectedSupplier , tenantId ] ) ;
// Filter ingredients based on supplier products
const ingredientsData = useMemo ( ( ) = > {
if ( ! selectedSupplier || supplierProductIds . length === 0 ) {
return [ ] ;
}
return allIngredientsData . filter ( ing = > supplierProductIds . includes ( ing . id ) ) ;
} , [ allIngredientsData , supplierProductIds , 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 ] ) ;
2025-10-21 19:50:07 +02:00
// Reset selected supplier when modal closes
useEffect ( ( ) = > {
if ( ! isOpen ) {
setSelectedSupplier ( '' ) ;
setFormData ( { } ) ;
}
} , [ isOpen ] ) ;
2025-09-26 07:46:25 +02:00
// 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
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-10-21 19:50:07 +02:00
// Build sections dynamically based on selectedSupplier
const sections = useMemo ( ( ) = > {
const supplierSection = {
2025-09-26 07:46:25 +02:00
title : 'Información del Proveedor' ,
icon : Building2 ,
fields : [
{
label : 'Proveedor' ,
name : 'supplier_id' ,
type : 'select' as const ,
required : true ,
options : supplierOptions ,
placeholder : 'Seleccionar proveedor...' ,
2025-10-21 19:50:07 +02:00
span : 2 ,
validation : ( value : any ) = > {
// Update selectedSupplier when supplier changes
if ( value && value !== selectedSupplier ) {
setTimeout ( ( ) = > setSelectedSupplier ( value ) , 0 ) ;
}
return null ;
}
2025-09-26 07:46:25 +02:00
}
]
2025-10-21 19:50:07 +02:00
} ;
const orderDetailsSection = {
2025-09-26 07:46:25 +02:00
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'
}
]
2025-10-21 19:50:07 +02:00
} ;
const ingredientsSection = {
2025-09-26 07:46:25 +02:00
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'
} : {
2025-10-21 19:50:07 +02:00
label : selectedSupplier ? 'Productos a Comprar' : 'Selecciona un proveedor primero' ,
2025-09-26 07:46:25 +02:00
name : 'manual_products' ,
type : 'list' as const ,
span : 2 ,
defaultValue : [ ] ,
listConfig : {
itemFields : [
{
name : 'ingredient_id' ,
label : 'Ingrediente' ,
type : 'select' ,
required : true ,
options : ingredientOptions ,
2025-10-21 19:50:07 +02:00
placeholder : isLoadingSupplierProducts || isLoadingIngredients ? 'Cargando ingredientes...' : ingredientOptions . length === 0 ? 'No hay ingredientes disponibles para este proveedor' : 'Seleccionar ingrediente...' ,
disabled : ! selectedSupplier || isLoadingIngredients || isLoadingSupplierProducts
2025-09-26 07:46:25 +02:00
} ,
{
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' ,
2025-10-21 19:50:07 +02:00
emptyStateText : ! selectedSupplier
? 'Selecciona un proveedor para agregar ingredientes'
: isLoadingSupplierProducts || isLoadingIngredients
? 'Cargando ingredientes del proveedor...'
: ingredientOptions . length === 0
? 'Este proveedor no tiene ingredientes asignados en su lista de precios'
: 'No hay ingredientes agregados' ,
2025-09-26 07:46:25 +02:00
showSubtotals : true ,
subtotalFields : { quantity : 'quantity' , price : 'unit_price' } ,
disabled : ! selectedSupplier
} ,
2025-10-21 19:50:07 +02:00
helpText : ! selectedSupplier
? 'Primero selecciona un proveedor en la sección anterior'
: 'Selecciona ingredientes disponibles del proveedor seleccionado'
2025-09-26 07:46:25 +02:00
}
]
2025-10-21 19:50:07 +02:00
} ;
return [ supplierSection , orderDetailsSection , ingredientsSection ] ;
} , [ requirements , supplierOptions , ingredientOptions , selectedSupplier , isLoadingIngredients , unitOptions ] ) ;
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 ;