diff --git a/frontend/src/components/domain/unified-wizard/wizards/SalesEntryWizard.tsx b/frontend/src/components/domain/unified-wizard/wizards/SalesEntryWizard.tsx index 73598314..a55837b4 100644 --- a/frontend/src/components/domain/unified-wizard/wizards/SalesEntryWizard.tsx +++ b/frontend/src/components/domain/unified-wizard/wizards/SalesEntryWizard.tsx @@ -11,7 +11,11 @@ import { DollarSign, Package, CreditCard, + Loader2, + X, } from 'lucide-react'; +import { useTenant } from '../../../../stores/tenant.store'; +import { salesService } from '../../../../api/services/sales'; // ======================================== // STEP 1: Entry Method Selection @@ -394,10 +398,90 @@ const ManualEntryStep: React.FC = ({ data, onDataChange, o }; // ======================================== -// STEP 2b: File Upload (Placeholder for now) +// STEP 2b: File Upload // ======================================== const FileUploadStep: React.FC = ({ data, onDataChange, onNext }) => { + const { currentTenant } = useTenant(); + const [file, setFile] = useState(data.uploadedFile || null); + const [validating, setValidating] = useState(false); + const [importing, setImporting] = useState(false); + const [validationResult, setValidationResult] = useState(null); + const [error, setError] = useState(null); + const [downloadingTemplate, setDownloadingTemplate] = useState(false); + + const handleFileSelect = (e: React.ChangeEvent) => { + const selectedFile = e.target.files?.[0]; + if (selectedFile) { + setFile(selectedFile); + setValidationResult(null); + setError(null); + } + }; + + const handleRemoveFile = () => { + setFile(null); + setValidationResult(null); + setError(null); + }; + + const handleValidate = async () => { + if (!file || !currentTenant?.id) return; + + setValidating(true); + setError(null); + + try { + const result = await salesService.validateImportFile(currentTenant.id, file); + setValidationResult(result); + } catch (err: any) { + console.error('Error validating file:', err); + setError(err.response?.data?.detail || 'Error al validar el archivo'); + } finally { + setValidating(false); + } + }; + + const handleImport = async () => { + if (!file || !currentTenant?.id) return; + + setImporting(true); + setError(null); + + try { + const result = await salesService.importSalesData(currentTenant.id, file, false); + onDataChange({ ...data, uploadedFile: file, importResult: result }); + onNext(); + } catch (err: any) { + console.error('Error importing file:', err); + setError(err.response?.data?.detail || 'Error al importar el archivo'); + } finally { + setImporting(false); + } + }; + + const handleDownloadTemplate = async () => { + if (!currentTenant?.id) return; + + setDownloadingTemplate(true); + try { + const blob = await salesService.downloadImportTemplate(currentTenant.id); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'plantilla_ventas.csv'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + } catch (err: any) { + console.error('Error downloading template:', err); + setError('Error al descargar la plantilla'); + } finally { + setDownloadingTemplate(false); + } + }; + return (
@@ -409,34 +493,144 @@ const FileUploadStep: React.FC = ({ data, onDataChange, on

-
- -

- Función de Carga de Archivos -

-

- Esta funcionalidad avanzada incluirá: -
- • Carga de CSV/Excel -
- • Mapeo de columnas -
- • Validación de datos -
- • Importación masiva -

-
- - + {error && ( +
+ + {error}
+ )} + + {/* Download Template Button */} +
+ +
+ + {/* File Upload Area */} + {!file ? ( +
+ +

+ Arrastra un archivo aquí +

+

+ o haz clic para seleccionar +

+ +

+ Formatos soportados: CSV, Excel (.xlsx, .xls) +

+
+ ) : ( +
+
+
+ +
+

{file.name}

+

+ {(file.size / 1024).toFixed(2)} KB +

+
+
+ +
+ + {/* Validation Result */} + {validationResult && ( +
+

+ ✓ Archivo validado correctamente +

+
+

Registros encontrados: {validationResult.total_rows || 0}

+

Registros válidos: {validationResult.valid_rows || 0}

+ {validationResult.errors?.length > 0 && ( +

+ Errores: {validationResult.errors.length} +

+ )} +
+
+ )} + + {/* Action Buttons */} +
+ {!validationResult && ( + + )} + +
+
+ )} + +
+

El archivo debe contener las columnas:

+

+ fecha, producto, cantidad, precio_unitario, método_pago +

); @@ -447,13 +641,51 @@ const FileUploadStep: React.FC = ({ data, onDataChange, on // ======================================== const ReviewStep: React.FC = ({ data, onComplete }) => { - const handleConfirm = () => { - // Here you would typically make an API call to save the data - console.log('Saving sales data:', data); - onComplete(); + const { currentTenant } = useTenant(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleConfirm = async () => { + if (!currentTenant?.id) { + setError('No se pudo obtener información del tenant'); + return; + } + + setLoading(true); + setError(null); + + try { + if (data.entryMethod === 'manual' && data.salesItems) { + // Create individual sales records for each item + for (const item of data.salesItems) { + const salesData = { + product_name: item.product, + product_category: 'general', // Could be enhanced with category selection + quantity_sold: item.quantity, + unit_price: item.unitPrice, + total_amount: item.subtotal, + sale_date: data.saleDate, + sales_channel: 'retail', + source: 'manual', + payment_method: data.paymentMethod, + notes: data.notes, + }; + + await salesService.createSalesRecord(currentTenant.id, salesData); + } + } + + onComplete(); + } catch (err: any) { + console.error('Error saving sales data:', err); + setError(err.response?.data?.detail || 'Error al guardar los datos de ventas'); + } finally { + setLoading(false); + } }; const isManual = data.entryMethod === 'manual'; + const isUpload = data.entryMethod === 'upload'; return (
@@ -471,6 +703,13 @@ const ReviewStep: React.FC = ({ data, onComplete }) => {

+ {error && ( +
+ + {error} +
+ )} + {isManual && data.salesItems && (
{/* Summary */} @@ -536,14 +775,38 @@ const ReviewStep: React.FC = ({ data, onComplete }) => {
)} + {isUpload && data.importResult && ( +
+
+

+ ✓ Archivo importado correctamente +

+
+

Registros importados: {data.importResult.successful_imports || 0}

+

Registros fallidos: {data.importResult.failed_imports || 0}

+
+
+
+ )} + {/* Confirm Button */}
diff --git a/frontend/src/components/domain/unified-wizard/wizards/SupplierWizard.tsx b/frontend/src/components/domain/unified-wizard/wizards/SupplierWizard.tsx index 9bb815e1..53ebc24d 100644 --- a/frontend/src/components/domain/unified-wizard/wizards/SupplierWizard.tsx +++ b/frontend/src/components/domain/unified-wizard/wizards/SupplierWizard.tsx @@ -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; @@ -84,7 +87,7 @@ const SupplierInfoStep: React.FC = ({ 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)]" />
@@ -93,28 +96,28 @@ const SupplierInfoStep: React.FC = ({ data, onDataChange, onNex -