Add improved production UI
This commit is contained in:
@@ -0,0 +1,722 @@
|
||||
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 }
|
||||
);
|
||||
const suppliers = (suppliersData?.data || []).filter(
|
||||
supplier => supplier.status === 'active'
|
||||
);
|
||||
|
||||
// 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;
|
||||
Reference in New Issue
Block a user