2025-09-23 12:49:35 +02:00
|
|
|
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 { useSuppliers } from '../../../api/hooks/suppliers';
|
|
|
|
|
import { useCreatePurchaseOrder } from '../../../api/hooks/suppliers';
|
|
|
|
|
import { useTenantStore } from '../../../stores/tenant.store';
|
|
|
|
|
import type { ProcurementRequirementResponse, PurchaseOrderItem } from '../../../api/types/orders';
|
|
|
|
|
import type { SupplierSummary } from '../../../api/types/suppliers';
|
|
|
|
|
|
|
|
|
|
interface CreatePurchaseOrderModalProps {
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
requirements: ProcurementRequirementResponse[];
|
|
|
|
|
onSuccess?: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 [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: ''
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
// 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]);
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Create purchase order
|
|
|
|
|
await createPurchaseOrderMutation.mutateAsync({
|
|
|
|
|
supplier_id: selectedSupplierId,
|
|
|
|
|
priority: 'normal',
|
|
|
|
|
required_delivery_date: deliveryDate || undefined,
|
|
|
|
|
notes: notes || undefined,
|
|
|
|
|
items
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Close modal and trigger success callback
|
|
|
|
|
onClose();
|
|
|
|
|
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.');
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Log suppliers when they change for debugging
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
// console.log('Suppliers updated:', suppliers);
|
|
|
|
|
}, [suppliers]);
|
|
|
|
|
|
|
|
|
|
if (!isOpen) return null;
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
|
|
|
|
{/* 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>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default CreatePurchaseOrderModal;
|