Refactor components and modals
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Plus, Package, Euro, Calendar, Truck, Building2, X, Save, AlertCircle } from 'lucide-react';
|
||||
import { Button } from '../../ui/Button';
|
||||
import { Input } from '../../ui/Input';
|
||||
import { Card } from '../../ui/Card';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { Plus, Package, Calendar, Building2 } from 'lucide-react';
|
||||
import { AddModal } from '../../ui/AddModal/AddModal';
|
||||
import { useSuppliers } from '../../../api/hooks/suppliers';
|
||||
import { useCreatePurchaseOrder } from '../../../api/hooks/suppliers';
|
||||
import { useIngredients } from '../../../api/hooks/inventory';
|
||||
import { useTenantStore } from '../../../stores/tenant.store';
|
||||
import type { ProcurementRequirementResponse, PurchaseOrderItem } from '../../../api/types/orders';
|
||||
import type { SupplierSummary } from '../../../api/types/suppliers';
|
||||
import type { IngredientResponse } from '../../../api/types/inventory';
|
||||
import { statusColors } from '../../../styles/colors';
|
||||
|
||||
interface CreatePurchaseOrderModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -16,6 +17,7 @@ interface CreatePurchaseOrderModalProps {
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* CreatePurchaseOrderModal - Modal for creating purchase orders from procurement requirements
|
||||
* Allows supplier selection and purchase order creation for ingredients
|
||||
@@ -27,29 +29,8 @@ export const CreatePurchaseOrderModal: React.FC<CreatePurchaseOrderModalProps> =
|
||||
requirements,
|
||||
onSuccess
|
||||
}) => {
|
||||
const [selectedSupplierId, setSelectedSupplierId] = useState<string>('');
|
||||
const [deliveryDate, setDeliveryDate] = useState<string>('');
|
||||
const [notes, setNotes] = useState<string>('');
|
||||
const [selectedRequirements, setSelectedRequirements] = useState<Record<string, boolean>>({});
|
||||
const [quantities, setQuantities] = useState<Record<string, number>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// For manual creation when no requirements are provided
|
||||
const [manualItems, setManualItems] = useState<Array<{
|
||||
id: string;
|
||||
product_name: string;
|
||||
product_sku?: string;
|
||||
unit_of_measure: string;
|
||||
unit_price: number;
|
||||
}>>([]);
|
||||
const [manualItemInputs, setManualItemInputs] = useState({
|
||||
product_name: '',
|
||||
product_sku: '',
|
||||
unit_of_measure: '',
|
||||
unit_price: '',
|
||||
quantity: ''
|
||||
});
|
||||
const [selectedSupplier, setSelectedSupplier] = useState<string>('');
|
||||
|
||||
// Get current tenant
|
||||
const { currentTenant } = useTenantStore();
|
||||
@@ -63,657 +44,312 @@ export const CreatePurchaseOrderModal: React.FC<CreatePurchaseOrderModalProps> =
|
||||
);
|
||||
const suppliers = (suppliersData || []).filter(supplier => supplier.status === 'active');
|
||||
|
||||
// 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 }
|
||||
);
|
||||
|
||||
// Create purchase order mutation
|
||||
const createPurchaseOrderMutation = useCreatePurchaseOrder();
|
||||
|
||||
// Initialize quantities when requirements change
|
||||
useEffect(() => {
|
||||
if (requirements && requirements.length > 0) {
|
||||
// Initialize from requirements (existing behavior)
|
||||
const initialQuantities: Record<string, number> = {};
|
||||
requirements.forEach(req => {
|
||||
initialQuantities[req.id] = req.approved_quantity || req.net_requirement || req.required_quantity;
|
||||
});
|
||||
setQuantities(initialQuantities);
|
||||
const supplierOptions = useMemo(() => suppliers.map(supplier => ({
|
||||
value: supplier.id,
|
||||
label: `${supplier.name} (${supplier.supplier_code})`
|
||||
})), [suppliers]);
|
||||
|
||||
// Initialize all requirements as selected
|
||||
const initialSelected: Record<string, boolean> = {};
|
||||
requirements.forEach(req => {
|
||||
initialSelected[req.id] = true;
|
||||
});
|
||||
setSelectedRequirements(initialSelected);
|
||||
|
||||
// Clear manual items when using requirements
|
||||
setManualItems([]);
|
||||
} else {
|
||||
// Reset for manual creation
|
||||
setQuantities({});
|
||||
setSelectedRequirements({});
|
||||
setManualItems([]);
|
||||
}
|
||||
}, [requirements]);
|
||||
// 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]);
|
||||
|
||||
// Group requirements by supplier (only when requirements exist)
|
||||
const groupedRequirements = requirements && requirements.length > 0 ?
|
||||
requirements.reduce((acc, req) => {
|
||||
const supplierId = req.preferred_supplier_id || 'unassigned';
|
||||
if (!acc[supplierId]) {
|
||||
acc[supplierId] = [];
|
||||
}
|
||||
acc[supplierId].push(req);
|
||||
return acc;
|
||||
}, {} as Record<string, ProcurementRequirementResponse[]>) :
|
||||
{};
|
||||
|
||||
const handleQuantityChange = (requirementId: string, value: string) => {
|
||||
const numValue = parseFloat(value) || 0;
|
||||
setQuantities(prev => ({
|
||||
...prev,
|
||||
[requirementId]: numValue
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSelectRequirement = (requirementId: string, checked: boolean) => {
|
||||
setSelectedRequirements(prev => ({
|
||||
...prev,
|
||||
[requirementId]: checked
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSelectAll = (supplierId: string, checked: boolean) => {
|
||||
const supplierRequirements = groupedRequirements[supplierId] || [];
|
||||
const updatedSelected = { ...selectedRequirements };
|
||||
|
||||
supplierRequirements.forEach(req => {
|
||||
updatedSelected[req.id] = checked;
|
||||
});
|
||||
|
||||
setSelectedRequirements(updatedSelected);
|
||||
};
|
||||
|
||||
// Manual item functions
|
||||
const handleAddManualItem = () => {
|
||||
if (!manualItemInputs.product_name || !manualItemInputs.unit_of_measure || !manualItemInputs.unit_price) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newItem = {
|
||||
id: `manual-${Date.now()}`,
|
||||
product_name: manualItemInputs.product_name,
|
||||
product_sku: manualItemInputs.product_sku || undefined,
|
||||
unit_of_measure: manualItemInputs.unit_of_measure,
|
||||
unit_price: parseFloat(manualItemInputs.unit_price) || 0
|
||||
};
|
||||
|
||||
setManualItems(prev => [...prev, newItem]);
|
||||
|
||||
// Reset inputs
|
||||
setManualItemInputs({
|
||||
product_name: '',
|
||||
product_sku: '',
|
||||
unit_of_measure: '',
|
||||
unit_price: '',
|
||||
quantity: ''
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveManualItem = (id: string) => {
|
||||
setManualItems(prev => prev.filter(item => item.id !== id));
|
||||
};
|
||||
|
||||
const handleManualItemQuantityChange = (id: string, value: string) => {
|
||||
const numValue = parseFloat(value) || 0;
|
||||
setQuantities(prev => ({
|
||||
...prev,
|
||||
[id]: numValue
|
||||
}));
|
||||
};
|
||||
|
||||
const handleCreatePurchaseOrder = async () => {
|
||||
if (!selectedSupplierId) {
|
||||
setError('Por favor, selecciona un proveedor');
|
||||
return;
|
||||
}
|
||||
|
||||
let items: PurchaseOrderItem[] = [];
|
||||
|
||||
if (requirements && requirements.length > 0) {
|
||||
// Create items from requirements
|
||||
const selectedReqs = requirements.filter(req => selectedRequirements[req.id]);
|
||||
|
||||
if (selectedReqs.length === 0) {
|
||||
setError('Por favor, selecciona al menos un ingrediente');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate quantities
|
||||
const invalidQuantities = selectedReqs.some(req => quantities[req.id] <= 0);
|
||||
if (invalidQuantities) {
|
||||
setError('Todas las cantidades deben ser mayores a 0');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare purchase order items from requirements
|
||||
items = selectedReqs.map(req => ({
|
||||
inventory_product_id: req.product_id,
|
||||
product_code: req.product_sku || '',
|
||||
product_name: req.product_name,
|
||||
ordered_quantity: quantities[req.id],
|
||||
unit_of_measure: req.unit_of_measure,
|
||||
unit_price: req.estimated_unit_cost || 0,
|
||||
quality_requirements: req.quality_specifications ? JSON.stringify(req.quality_specifications) : undefined,
|
||||
notes: req.special_requirements || undefined
|
||||
}));
|
||||
} else {
|
||||
// Create items from manual entries
|
||||
if (manualItems.length === 0) {
|
||||
setError('Por favor, agrega al menos un producto');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate quantities for manual items
|
||||
const invalidQuantities = manualItems.some(item => quantities[item.id] <= 0);
|
||||
if (invalidQuantities) {
|
||||
setError('Todas las cantidades deben ser mayores a 0');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare purchase order items from manual entries
|
||||
items = manualItems.map(item => ({
|
||||
inventory_product_id: '', // Not applicable for manual items
|
||||
product_code: item.product_sku || '',
|
||||
product_name: item.product_name,
|
||||
ordered_quantity: quantities[item.id],
|
||||
unit_of_measure: item.unit_of_measure,
|
||||
unit_price: item.unit_price,
|
||||
quality_requirements: undefined,
|
||||
notes: undefined
|
||||
}));
|
||||
}
|
||||
// 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);
|
||||
setError(null);
|
||||
|
||||
// Update selectedSupplier if it changed
|
||||
if (formData.supplier_id && formData.supplier_id !== selectedSupplier) {
|
||||
setSelectedSupplier(formData.supplier_id);
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Create purchase order
|
||||
await createPurchaseOrderMutation.mutateAsync({
|
||||
supplier_id: selectedSupplierId,
|
||||
supplier_id: formData.supplier_id,
|
||||
priority: 'normal',
|
||||
required_delivery_date: deliveryDate || undefined,
|
||||
notes: notes || undefined,
|
||||
required_delivery_date: formData.delivery_date || undefined,
|
||||
notes: formData.notes || undefined,
|
||||
items
|
||||
});
|
||||
|
||||
// Close modal and trigger success callback
|
||||
onClose();
|
||||
// Purchase order created successfully
|
||||
|
||||
// Trigger success callback
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error creating purchase order:', err);
|
||||
setError('Error al crear la orden de compra. Por favor, intenta de nuevo.');
|
||||
} catch (error) {
|
||||
console.error('Error creating purchase order:', error);
|
||||
throw error; // Let AddModal handle error display
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Log suppliers when they change for debugging
|
||||
useEffect(() => {
|
||||
// console.log('Suppliers updated:', suppliers);
|
||||
}, [suppliers]);
|
||||
const statusConfig = {
|
||||
color: statusColors.inProgress.primary,
|
||||
text: 'Nueva Orden',
|
||||
icon: Plus,
|
||||
isCritical: false,
|
||||
isHighlight: true
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
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'
|
||||
}
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="fixed top-[var(--header-height)] left-0 right-0 bottom-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-[var(--bg-primary)] rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden mx-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-[var(--border-primary)]">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100">
|
||||
<Plus className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
Crear Orden de Compra
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Selecciona proveedor e ingredientes para crear una orden de compra
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
className="p-2"
|
||||
disabled={loading}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<>
|
||||
<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}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 overflow-y-auto max-h-[calc(90vh-200px)]">
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg flex items-center">
|
||||
<AlertCircle className="w-5 h-5 text-red-500 mr-2 flex-shrink-0" />
|
||||
<span className="text-red-700 text-sm">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Supplier Selection */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Proveedor
|
||||
</label>
|
||||
{!tenantId ? (
|
||||
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg text-yellow-700 text-sm">
|
||||
Cargando información del tenant...
|
||||
</div>
|
||||
) : isLoadingSuppliers ? (
|
||||
<div className="animate-pulse h-10 bg-gray-200 rounded"></div>
|
||||
) : isSuppliersError ? (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
||||
Error al cargar proveedores: {suppliersError?.message || 'Error desconocido'}
|
||||
</div>
|
||||
) : (
|
||||
<select
|
||||
value={selectedSupplierId}
|
||||
onChange={(e) => setSelectedSupplierId(e.target.value)}
|
||||
className="w-full px-3 py-2 text-sm border border-[var(--border-primary)] rounded-lg
|
||||
bg-[var(--bg-primary)] text-[var(--text-primary)]
|
||||
focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]
|
||||
transition-colors duration-200"
|
||||
disabled={loading}
|
||||
>
|
||||
<option value="">Seleccionar proveedor...</option>
|
||||
{suppliers.length > 0 ? (
|
||||
suppliers.map((supplier: SupplierSummary) => (
|
||||
<option key={supplier.id} value={supplier.id}>
|
||||
{supplier.name} ({supplier.supplier_code})
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value="" disabled>
|
||||
No hay proveedores activos disponibles
|
||||
</option>
|
||||
)}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Delivery Date */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Fecha de Entrega Requerida (Opcional)
|
||||
</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={deliveryDate}
|
||||
onChange={(e) => setDeliveryDate(e.target.value)}
|
||||
className="w-full"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Notas (Opcional)
|
||||
</label>
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
placeholder="Instrucciones especiales para el proveedor..."
|
||||
className="w-full px-3 py-2 text-sm border border-[var(--border-primary)] rounded-lg
|
||||
bg-[var(--bg-primary)] text-[var(--text-primary)] placeholder-[var(--text-tertiary)]
|
||||
focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]
|
||||
transition-colors duration-200 resize-vertical min-h-[80px]"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Requirements by Supplier or Manual Items */}
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-md font-semibold text-[var(--text-primary)] border-b pb-2">
|
||||
{requirements && requirements.length > 0 ? 'Ingredientes a Comprar' : 'Productos a Comprar'}
|
||||
</h3>
|
||||
|
||||
{requirements && requirements.length > 0 ? (
|
||||
// Show requirements when they exist
|
||||
Object.entries(groupedRequirements).map(([supplierId, reqs]) => {
|
||||
const supplierName = supplierId === 'unassigned'
|
||||
? 'Sin proveedor asignado'
|
||||
: suppliers.find(s => s.id === supplierId)?.name || 'Proveedor desconocido';
|
||||
|
||||
const allSelected = reqs.every(req => selectedRequirements[req.id]);
|
||||
const someSelected = reqs.some(req => selectedRequirements[req.id]);
|
||||
|
||||
return (
|
||||
<Card key={supplierId} className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Building2 className="w-4 h-4 text-[var(--text-secondary)]" />
|
||||
<h4 className="font-medium text-[var(--text-primary)]">{supplierName}</h4>
|
||||
<span className="text-xs text-[var(--text-secondary)] bg-[var(--bg-secondary)] px-2 py-1 rounded">
|
||||
{reqs.length} {reqs.length === 1 ? 'ingrediente' : 'ingredientes'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`select-all-${supplierId}`}
|
||||
checked={allSelected}
|
||||
ref={el => {
|
||||
if (el) el.indeterminate = someSelected && !allSelected;
|
||||
}}
|
||||
onChange={(e) => handleSelectAll(supplierId, e.target.checked)}
|
||||
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
disabled={loading}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`select-all-${supplierId}`}
|
||||
className="ml-2 text-sm text-[var(--text-secondary)]"
|
||||
>
|
||||
Seleccionar todo
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{reqs.map((req) => (
|
||||
<div
|
||||
key={req.id}
|
||||
className={`flex items-center p-3 rounded-lg border ${
|
||||
selectedRequirements[req.id]
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
||||
: 'border-[var(--border-primary)]'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!selectedRequirements[req.id]}
|
||||
onChange={(e) => handleSelectRequirement(req.id, e.target.checked)}
|
||||
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
disabled={loading}
|
||||
/>
|
||||
<div className="ml-3 flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-[var(--text-primary)] truncate">
|
||||
{req.product_name}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">
|
||||
{req.product_sku || 'Sin SKU'} • {req.unit_of_measure}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center">
|
||||
<Euro className="w-3 h-3 text-[var(--text-secondary)] mr-1" />
|
||||
<span className="text-xs text-[var(--text-secondary)]">
|
||||
{req.estimated_unit_cost?.toFixed(2) || '0.00'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center space-x-2">
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs text-[var(--text-secondary)] mb-1">
|
||||
Cantidad requerida
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={quantities[req.id] || ''}
|
||||
onChange={(e) => handleQuantityChange(req.id, e.target.value)}
|
||||
className="w-24 text-sm"
|
||||
disabled={loading || !selectedRequirements[req.id]}
|
||||
/>
|
||||
<span className="ml-2 text-sm text-[var(--text-secondary)]">
|
||||
{req.unit_of_measure}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs text-[var(--text-secondary)] mb-1">
|
||||
Stock actual
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<span className="text-sm text-[var(--text-primary)]">
|
||||
{req.current_stock_level || 0}
|
||||
</span>
|
||||
<span className="ml-1 text-xs text-[var(--text-secondary)]">
|
||||
{req.unit_of_measure}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs text-[var(--text-secondary)] mb-1">
|
||||
Total estimado
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<Euro className="w-3 h-3 text-[var(--text-secondary)] mr-1" />
|
||||
<span className="text-sm text-[var(--text-primary)]">
|
||||
{((quantities[req.id] || 0) * (req.estimated_unit_cost || 0)).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
// Show manual item creation when no requirements exist
|
||||
<Card className="p-4">
|
||||
<div className="space-y-4">
|
||||
{/* Manual Item Input Form */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-3">
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
||||
Nombre del Producto *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={manualItemInputs.product_name}
|
||||
onChange={(e) => setManualItemInputs(prev => ({
|
||||
...prev,
|
||||
product_name: e.target.value
|
||||
}))}
|
||||
placeholder="Harina de Trigo"
|
||||
className="w-full text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
||||
SKU
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={manualItemInputs.product_sku}
|
||||
onChange={(e) => setManualItemInputs(prev => ({
|
||||
...prev,
|
||||
product_sku: e.target.value
|
||||
}))}
|
||||
placeholder="HT-001"
|
||||
className="w-full text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
||||
Unidad *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={manualItemInputs.unit_of_measure}
|
||||
onChange={(e) => setManualItemInputs(prev => ({
|
||||
...prev,
|
||||
unit_of_measure: e.target.value
|
||||
}))}
|
||||
placeholder="kg"
|
||||
className="w-full text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
||||
Precio Unitario *
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-2 top-1/2 transform -translate-y-1/2 text-[var(--text-secondary)] text-sm">€</span>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={manualItemInputs.unit_price}
|
||||
onChange={(e) => setManualItemInputs(prev => ({
|
||||
...prev,
|
||||
unit_price: e.target.value
|
||||
}))}
|
||||
placeholder="2.50"
|
||||
className="w-full text-sm pl-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleAddManualItem}
|
||||
disabled={!manualItemInputs.product_name || !manualItemInputs.unit_of_measure || !manualItemInputs.unit_price}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Agregar Producto
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Manual Items List */}
|
||||
{manualItems.length > 0 && (
|
||||
<div className="mt-4 space-y-3">
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)]">
|
||||
Productos Agregados ({manualItems.length})
|
||||
</h4>
|
||||
|
||||
{manualItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center p-3 rounded-lg border border-[var(--border-primary)]"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-[var(--text-primary)] truncate">
|
||||
{item.product_name}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">
|
||||
{item.product_sku || 'Sin SKU'} • {item.unit_of_measure}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center">
|
||||
<Euro className="w-3 h-3 text-[var(--text-secondary)] mr-1" />
|
||||
<span className="text-xs text-[var(--text-secondary)]">
|
||||
{item.unit_price.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center space-x-2">
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs text-[var(--text-secondary)] mb-1">
|
||||
Cantidad
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={quantities[item.id] || ''}
|
||||
onChange={(e) => handleManualItemQuantityChange(item.id, e.target.value)}
|
||||
className="w-24 text-sm"
|
||||
disabled={loading}
|
||||
/>
|
||||
<span className="ml-2 text-sm text-[var(--text-secondary)]">
|
||||
{item.unit_of_measure}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs text-[var(--text-secondary)] mb-1">
|
||||
Total
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<Euro className="w-3 h-3 text-[var(--text-secondary)] mr-1" />
|
||||
<span className="text-sm text-[var(--text-primary)]">
|
||||
{((quantities[item.id] || 0) * item.unit_price).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveManualItem(item.id)}
|
||||
disabled={loading}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end space-x-3 p-6 border-t border-[var(--border-primary)]">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleCreatePurchaseOrder}
|
||||
disabled={loading || !selectedSupplierId}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className="w-4 h-4 mr-2 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
||||
Creando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
Crear Orden de Compra
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user