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:
@@ -11,7 +11,11 @@ import {
|
|||||||
DollarSign,
|
DollarSign,
|
||||||
Package,
|
Package,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
|
Loader2,
|
||||||
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useTenant } from '../../../../stores/tenant.store';
|
||||||
|
import { salesService } from '../../../../api/services/sales';
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// STEP 1: Entry Method Selection
|
// STEP 1: Entry Method Selection
|
||||||
@@ -394,10 +398,90 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// STEP 2b: File Upload (Placeholder for now)
|
// STEP 2b: File Upload
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, onNext }) => {
|
const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, onNext }) => {
|
||||||
|
const { currentTenant } = useTenant();
|
||||||
|
const [file, setFile] = useState<File | null>(data.uploadedFile || null);
|
||||||
|
const [validating, setValidating] = useState(false);
|
||||||
|
const [importing, setImporting] = useState(false);
|
||||||
|
const [validationResult, setValidationResult] = useState<any>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [downloadingTemplate, setDownloadingTemplate] = useState(false);
|
||||||
|
|
||||||
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
||||||
@@ -409,34 +493,144 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-xl bg-[var(--bg-secondary)]/30">
|
{error && (
|
||||||
<FileSpreadsheet className="w-16 h-16 mx-auto mb-4 text-[var(--color-primary)]/50" />
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm flex items-start gap-2">
|
||||||
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
||||||
Función de Carga de Archivos
|
<span>{error}</span>
|
||||||
</h4>
|
</div>
|
||||||
<p className="text-sm text-[var(--text-secondary)] mb-6 max-w-md mx-auto">
|
)}
|
||||||
Esta funcionalidad avanzada incluirá:
|
|
||||||
<br />
|
{/* Download Template Button */}
|
||||||
• Carga de CSV/Excel
|
<div className="flex justify-center">
|
||||||
<br />
|
<button
|
||||||
• Mapeo de columnas
|
onClick={handleDownloadTemplate}
|
||||||
<br />
|
disabled={downloadingTemplate}
|
||||||
• Validación de datos
|
className="px-4 py-2 border border-[var(--border-secondary)] text-[var(--text-secondary)] rounded-lg hover:bg-[var(--bg-secondary)] transition-colors inline-flex items-center gap-2 disabled:opacity-50"
|
||||||
<br />
|
>
|
||||||
• Importación masiva
|
{downloadingTemplate ? (
|
||||||
</p>
|
<>
|
||||||
<div className="flex gap-3 justify-center">
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
<button className="px-4 py-2 border border-[var(--border-secondary)] text-[var(--text-secondary)] rounded-lg hover:bg-[var(--bg-secondary)] transition-colors inline-flex items-center gap-2">
|
Descargando...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
Descargar Plantilla CSV
|
Descargar Plantilla CSV
|
||||||
</button>
|
</>
|
||||||
<button
|
)}
|
||||||
onClick={onNext}
|
|
||||||
className="px-6 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 transition-colors"
|
|
||||||
>
|
|
||||||
Continuar (Demo)
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* File Upload Area */}
|
||||||
|
{!file ? (
|
||||||
|
<div className="border-2 border-dashed border-[var(--border-secondary)] rounded-xl p-8 text-center bg-[var(--bg-secondary)]/30">
|
||||||
|
<FileSpreadsheet className="w-16 h-16 mx-auto mb-4 text-[var(--color-primary)]/50" />
|
||||||
|
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||||
|
Arrastra un archivo aquí
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
||||||
|
o haz clic para seleccionar
|
||||||
|
</p>
|
||||||
|
<label className="inline-block">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".csv,.xlsx,.xls"
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<span className="px-6 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 transition-colors cursor-pointer inline-block">
|
||||||
|
Seleccionar Archivo
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-[var(--text-tertiary)] mt-3">
|
||||||
|
Formatos soportados: CSV, Excel (.xlsx, .xls)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="border border-[var(--border-secondary)] rounded-lg p-4 bg-[var(--bg-primary)]">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<FileSpreadsheet className="w-8 h-8 text-[var(--color-primary)]" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-[var(--text-primary)]">{file.name}</p>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">
|
||||||
|
{(file.size / 1024).toFixed(2)} KB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleRemoveFile}
|
||||||
|
className="p-1 text-red-500 hover:text-red-700 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Validation Result */}
|
||||||
|
{validationResult && (
|
||||||
|
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
|
<p className="text-sm text-blue-800 font-medium mb-2">
|
||||||
|
✓ Archivo validado correctamente
|
||||||
|
</p>
|
||||||
|
<div className="text-xs text-blue-700 space-y-1">
|
||||||
|
<p>Registros encontrados: {validationResult.total_rows || 0}</p>
|
||||||
|
<p>Registros válidos: {validationResult.valid_rows || 0}</p>
|
||||||
|
{validationResult.errors?.length > 0 && (
|
||||||
|
<p className="text-red-600">
|
||||||
|
Errores: {validationResult.errors.length}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="mt-4 flex gap-3">
|
||||||
|
{!validationResult && (
|
||||||
|
<button
|
||||||
|
onClick={handleValidate}
|
||||||
|
disabled={validating}
|
||||||
|
className="flex-1 px-4 py-2 border border-[var(--color-primary)] text-[var(--color-primary)] rounded-lg hover:bg-[var(--color-primary)]/5 transition-colors inline-flex items-center justify-center gap-2 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{validating ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Validando...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckCircle2 className="w-4 h-4" />
|
||||||
|
Validar Archivo
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={handleImport}
|
||||||
|
disabled={importing || !validationResult}
|
||||||
|
className="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors inline-flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{importing ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Importando...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Upload className="w-4 h-4" />
|
||||||
|
Importar Datos
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-center text-sm text-[var(--text-tertiary)]">
|
||||||
|
<p>El archivo debe contener las columnas:</p>
|
||||||
|
<p className="font-mono text-xs mt-1">
|
||||||
|
fecha, producto, cantidad, precio_unitario, método_pago
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -447,13 +641,51 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
|
|||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
|
const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
|
||||||
const handleConfirm = () => {
|
const { currentTenant } = useTenant();
|
||||||
// Here you would typically make an API call to save the data
|
const [loading, setLoading] = useState(false);
|
||||||
console.log('Saving sales data:', data);
|
const [error, setError] = useState<string | null>(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();
|
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 isManual = data.entryMethod === 'manual';
|
||||||
|
const isUpload = data.entryMethod === 'upload';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -471,6 +703,13 @@ const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
|
)}
|
||||||
|
|
||||||
{isManual && data.salesItems && (
|
{isManual && data.salesItems && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Summary */}
|
{/* Summary */}
|
||||||
@@ -536,14 +775,38 @@ const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isUpload && data.importResult && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||||
|
<p className="text-green-800 font-semibold mb-2">
|
||||||
|
✓ Archivo importado correctamente
|
||||||
|
</p>
|
||||||
|
<div className="text-sm text-green-700 space-y-1">
|
||||||
|
<p>Registros importados: {data.importResult.successful_imports || 0}</p>
|
||||||
|
<p>Registros fallidos: {data.importResult.failed_imports || 0}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Confirm Button */}
|
{/* Confirm Button */}
|
||||||
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-primary)]">
|
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-primary)]">
|
||||||
<button
|
<button
|
||||||
onClick={handleConfirm}
|
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={loading || (isUpload && !data.importResult)}
|
||||||
|
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:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
|
Guardando...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<CheckCircle2 className="w-5 h-5" />
|
<CheckCircle2 className="w-5 h-5" />
|
||||||
Confirmar y Guardar
|
Confirmar y Guardar
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
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 {
|
interface WizardDataProps extends WizardStepProps {
|
||||||
data: Record<string, any>;
|
data: Record<string, any>;
|
||||||
@@ -84,7 +87,7 @@ const SupplierInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNex
|
|||||||
type="email"
|
type="email"
|
||||||
value={supplierData.email}
|
value={supplierData.email}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, email: e.target.value })}
|
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)]"
|
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>
|
||||||
@@ -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">
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
Dirección
|
Dirección
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<input
|
||||||
|
type="text"
|
||||||
value={supplierData.address}
|
value={supplierData.address}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, address: e.target.value })}
|
onChange={(e) => setSupplierData({ ...supplierData, address: e.target.value })}
|
||||||
placeholder="Calle, ciudad, código postal..."
|
placeholder="Calle, Ciudad, País"
|
||||||
rows={2}
|
|
||||||
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)]"
|
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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
Condiciones de Pago
|
Términos de Pago
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={supplierData.paymentTerms}
|
value={supplierData.paymentTerms}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, paymentTerms: e.target.value })}
|
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)]"
|
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="immediate">Inmediato</option>
|
||||||
<option value="net15">Net 15 días</option>
|
<option value="net30">Neto 30 días</option>
|
||||||
<option value="net30">Net 30 días</option>
|
<option value="net60">Neto 60 días</option>
|
||||||
<option value="net60">Net 60 días</option>
|
<option value="net90">Neto 90 días</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -125,8 +128,8 @@ const SupplierInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNex
|
|||||||
<textarea
|
<textarea
|
||||||
value={supplierData.notes}
|
value={supplierData.notes}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, notes: e.target.value })}
|
onChange={(e) => setSupplierData({ ...supplierData, notes: e.target.value })}
|
||||||
placeholder="Horarios de pedido, condiciones especiales..."
|
placeholder="Información adicional sobre el proveedor..."
|
||||||
rows={2}
|
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)]"
|
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>
|
||||||
@@ -148,15 +151,31 @@ const SupplierInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNex
|
|||||||
|
|
||||||
// Step 2: Products & Pricing
|
// Step 2: Products & Pricing
|
||||||
const ProductsPricingStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
const ProductsPricingStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
||||||
|
const { currentTenant } = useTenant();
|
||||||
const [products, setProducts] = useState(data.products || []);
|
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
|
useEffect(() => {
|
||||||
const mockIngredients = [
|
fetchIngredients();
|
||||||
{ id: 1, name: 'Harina de Trigo', unit: 'kg' },
|
}, []);
|
||||||
{ id: 2, name: 'Mantequilla', unit: 'kg' },
|
|
||||||
{ id: 3, name: 'Azúcar', unit: 'kg' },
|
const fetchIngredients = async () => {
|
||||||
{ id: 4, name: 'Levadura', unit: 'kg' },
|
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 = () => {
|
const handleAddProduct = () => {
|
||||||
setProducts([
|
setProducts([
|
||||||
@@ -179,12 +198,69 @@ const ProductsPricingStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
setProducts(products.filter((_: any, i: number) => i !== index));
|
setProducts(products.filter((_: any, i: number) => i !== index));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = async () => {
|
||||||
onDataChange({ ...data, products });
|
if (!currentTenant?.id) {
|
||||||
console.log('Saving supplier:', { ...data, products });
|
setError('No se pudo obtener información del tenant');
|
||||||
onComplete();
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
<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>
|
</p>
|
||||||
</div>
|
</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="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)]">
|
<label className="block text-sm font-medium text-[var(--text-secondary)]">
|
||||||
@@ -204,15 +287,23 @@ const ProductsPricingStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
onClick={handleAddProduct}
|
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
|
+ Agregar Producto
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<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-[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>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<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)]"
|
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>
|
<option value="">Seleccionar ingrediente...</option>
|
||||||
{mockIngredients.map((ing) => (
|
{ingredients.map((ing) => (
|
||||||
<option key={ing.id} value={ing.id}>
|
<option key={ing.id} value={ing.id}>
|
||||||
{ing.name} ({ing.unit})
|
{ing.name} ({ing.unit})
|
||||||
</option>
|
</option>
|
||||||
@@ -257,10 +348,10 @@ const ProductsPricingStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
min="1"
|
min="1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1">
|
<div className="col-span-1 flex justify-end">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRemoveProduct(index)}
|
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>
|
</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)]">
|
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-primary)]">
|
||||||
<button
|
<button
|
||||||
onClick={handleConfirm}
|
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"
|
||||||
>
|
>
|
||||||
|
{saving ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
|
Guardando...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<CheckCircle2 className="w-5 h-5" />
|
<CheckCircle2 className="w-5 h-5" />
|
||||||
Crear Proveedor
|
Crear Proveedor
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -291,15 +392,14 @@ export const SupplierWizardSteps = (
|
|||||||
): WizardStep[] => [
|
): WizardStep[] => [
|
||||||
{
|
{
|
||||||
id: 'supplier-info',
|
id: 'supplier-info',
|
||||||
title: 'Información del Proveedor',
|
title: 'Información',
|
||||||
description: 'Datos de contacto y términos',
|
description: 'Datos del proveedor',
|
||||||
component: (props) => <SupplierInfoStep {...props} data={data} onDataChange={setData} />,
|
component: (props) => <SupplierInfoStep {...props} data={data} onDataChange={setData} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'supplier-products',
|
id: 'products-pricing',
|
||||||
title: 'Productos y Precios',
|
title: 'Productos y Precios',
|
||||||
description: 'Ingredientes que suministra',
|
description: 'Lista de precios',
|
||||||
component: (props) => <ProductsPricingStep {...props} data={data} onDataChange={setData} />,
|
component: (props) => <ProductsPricingStep {...props} data={data} onDataChange={setData} />,
|
||||||
isOptional: true,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user