feat: Add full API integration to Sales Entry and Supplier wizards

Sales Entry Wizard:
- Implemented complete file upload functionality with validation
- Added CSV template download via salesService.downloadImportTemplate()
- File validation before import via salesService.validateImportFile()
- Bulk import via salesService.importSalesData()
- Manual entry saves via salesService.createSalesRecord()
- Removed all mock data and console.log
- Added comprehensive error handling and loading states

Supplier Wizard:
- Replaced mock ingredients with inventoryService.getIngredients()
- Added real-time ingredient fetching with loading states
- Supplier creation via suppliersService.createSupplier()
- Price list creation via suppliersService.createSupplierPriceList()
- Removed all mock data and console.log
- Added comprehensive error handling

Both wizards now fully integrated with backend APIs.
This commit is contained in:
Claude
2025-11-09 09:34:47 +00:00
parent 6675e24a5a
commit c3be9e4cea
2 changed files with 435 additions and 72 deletions

View File

@@ -1,6 +1,9 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import { Building2, Package, Euro, CheckCircle2, Phone, Mail } from 'lucide-react';
import { Building2, Package, Euro, CheckCircle2, Phone, Mail, Loader2, AlertCircle } from 'lucide-react';
import { useTenant } from '../../../../stores/tenant.store';
import { suppliersService } from '../../../../api/services/suppliers';
import { inventoryService } from '../../../../api/services/inventory';
interface WizardDataProps extends WizardStepProps {
data: Record<string, any>;
@@ -84,7 +87,7 @@ const SupplierInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNex
type="email"
value={supplierData.email}
onChange={(e) => setSupplierData({ ...supplierData, email: e.target.value })}
placeholder="pedidos@proveedor.com"
placeholder="contacto@proveedor.com"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
/>
</div>
@@ -93,28 +96,28 @@ const SupplierInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNex
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Dirección
</label>
<textarea
<input
type="text"
value={supplierData.address}
onChange={(e) => setSupplierData({ ...supplierData, address: e.target.value })}
placeholder="Calle, ciudad, código postal..."
rows={2}
placeholder="Calle, Ciudad, País"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Condiciones de Pago
Términos de Pago
</label>
<select
value={supplierData.paymentTerms}
onChange={(e) => setSupplierData({ ...supplierData, paymentTerms: e.target.value })}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
>
<option value="immediate">Pago Inmediato</option>
<option value="net15">Net 15 días</option>
<option value="net30">Net 30 días</option>
<option value="net60">Net 60 días</option>
<option value="immediate">Inmediato</option>
<option value="net30">Neto 30 días</option>
<option value="net60">Neto 60 días</option>
<option value="net90">Neto 90 días</option>
</select>
</div>
@@ -125,8 +128,8 @@ const SupplierInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNex
<textarea
value={supplierData.notes}
onChange={(e) => setSupplierData({ ...supplierData, notes: e.target.value })}
placeholder="Horarios de pedido, condiciones especiales..."
rows={2}
placeholder="Información adicional sobre el proveedor..."
rows={3}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
/>
</div>
@@ -148,15 +151,31 @@ const SupplierInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNex
// Step 2: Products & Pricing
const ProductsPricingStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
const { currentTenant } = useTenant();
const [products, setProducts] = useState(data.products || []);
const [ingredients, setIngredients] = useState<any[]>([]);
const [loadingIngredients, setLoadingIngredients] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
// Mock ingredient list - replace with actual API call
const mockIngredients = [
{ id: 1, name: 'Harina de Trigo', unit: 'kg' },
{ id: 2, name: 'Mantequilla', unit: 'kg' },
{ id: 3, name: 'Azúcar', unit: 'kg' },
{ id: 4, name: 'Levadura', unit: 'kg' },
];
useEffect(() => {
fetchIngredients();
}, []);
const fetchIngredients = async () => {
if (!currentTenant?.id) return;
setLoadingIngredients(true);
try {
const result = await inventoryService.getIngredients(currentTenant.id);
setIngredients(result);
} catch (err: any) {
console.error('Error fetching ingredients:', err);
setError('Error al cargar los ingredientes');
} finally {
setLoadingIngredients(false);
}
};
const handleAddProduct = () => {
setProducts([
@@ -179,12 +198,69 @@ const ProductsPricingStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
setProducts(products.filter((_: any, i: number) => i !== index));
};
const handleConfirm = () => {
onDataChange({ ...data, products });
console.log('Saving supplier:', { ...data, products });
onComplete();
const handleConfirm = async () => {
if (!currentTenant?.id) {
setError('No se pudo obtener información del tenant');
return;
}
setSaving(true);
setError(null);
try {
// Create the supplier
const supplierData = {
name: data.name,
supplier_type: 'ingredients',
contact_person: data.contactPerson || undefined,
email: data.email || undefined,
phone: data.phone,
address: data.address || undefined,
payment_terms: data.paymentTerms || 'net30',
tax_id: undefined,
notes: data.notes || undefined,
status: 'active',
};
const createdSupplier = await suppliersService.createSupplier(currentTenant.id, supplierData);
// Create price list for the products if any
if (products.length > 0 && createdSupplier.id) {
const priceListItems = products.map((product: any) => ({
inventory_product_id: product.ingredientId,
unit_price: product.price,
minimum_order_quantity: product.minimumOrder,
is_active: true,
}));
await suppliersService.createSupplierPriceList(currentTenant.id, createdSupplier.id, {
name: `Lista de Precios - ${data.name}`,
effective_date: new Date().toISOString().split('T')[0],
currency: 'EUR',
is_active: true,
items: priceListItems,
});
}
onDataChange({ ...data, products });
onComplete();
} catch (err: any) {
console.error('Error saving supplier:', err);
setError(err.response?.data?.detail || 'Error al guardar el proveedor');
} finally {
setSaving(false);
}
};
if (loadingIngredients) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
<span className="ml-3 text-[var(--text-secondary)]">Cargando ingredientes...</span>
</div>
);
}
return (
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
@@ -197,6 +273,13 @@ const ProductsPricingStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</p>
</div>
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm flex items-start gap-2">
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
<span>{error}</span>
</div>
)}
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium text-[var(--text-secondary)]">
@@ -204,15 +287,23 @@ const ProductsPricingStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label>
<button
onClick={handleAddProduct}
className="px-3 py-1.5 text-sm bg-[var(--color-primary)] text-white rounded-md hover:bg-[var(--color-primary)]/90 transition-colors"
disabled={ingredients.length === 0}
className="px-3 py-1.5 text-sm bg-[var(--color-primary)] text-white rounded-md hover:bg-[var(--color-primary)]/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
+ Agregar Producto
</button>
</div>
{products.length === 0 ? (
{ingredients.length === 0 ? (
<div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
<p className="text-[var(--text-tertiary)]">
No hay ingredientes disponibles. Crea ingredientes primero en la sección de Inventario.
</p>
</div>
) : products.length === 0 ? (
<div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
<p className="text-[var(--text-tertiary)]">No hay productos agregados</p>
<p className="text-xs text-[var(--text-tertiary)] mt-1">Opcional - puedes agregar productos más tarde</p>
</div>
) : (
<div className="space-y-2">
@@ -229,7 +320,7 @@ const ProductsPricingStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
className="w-full px-2 py-1.5 text-sm border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)]"
>
<option value="">Seleccionar ingrediente...</option>
{mockIngredients.map((ing) => (
{ingredients.map((ing) => (
<option key={ing.id} value={ing.id}>
{ing.name} ({ing.unit})
</option>
@@ -257,10 +348,10 @@ const ProductsPricingStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
min="1"
/>
</div>
<div className="col-span-1">
<div className="col-span-1 flex justify-end">
<button
onClick={() => handleRemoveProduct(index)}
className="p-1 text-red-500 hover:text-red-700"
className="p-1 text-red-500 hover:text-red-700 transition-colors"
>
</button>
@@ -275,10 +366,20 @@ const ProductsPricingStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-primary)]">
<button
onClick={handleConfirm}
className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-semibold inline-flex items-center gap-2"
disabled={saving}
className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-semibold inline-flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<CheckCircle2 className="w-5 h-5" />
Crear Proveedor
{saving ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
Guardando...
</>
) : (
<>
<CheckCircle2 className="w-5 h-5" />
Crear Proveedor
</>
)}
</button>
</div>
</div>
@@ -291,15 +392,14 @@ export const SupplierWizardSteps = (
): WizardStep[] => [
{
id: 'supplier-info',
title: 'Información del Proveedor',
description: 'Datos de contacto y términos',
title: 'Información',
description: 'Datos del proveedor',
component: (props) => <SupplierInfoStep {...props} data={data} onDataChange={setData} />,
},
{
id: 'supplier-products',
id: 'products-pricing',
title: 'Productos y Precios',
description: 'Ingredientes que suministra',
description: 'Lista de precios',
component: (props) => <ProductsPricingStep {...props} data={data} onDataChange={setData} />,
isOptional: true,
},
];