Improve the UI add button
This commit is contained in:
@@ -4,7 +4,6 @@ import {
|
||||
Edit3,
|
||||
Upload,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Download,
|
||||
FileSpreadsheet,
|
||||
Calendar,
|
||||
@@ -23,24 +22,18 @@ import { showToast } from '../../../../utils/toast';
|
||||
// STEP 1: Entry Method Selection
|
||||
// ========================================
|
||||
|
||||
interface EntryMethodStepProps extends WizardStepProps {
|
||||
data: Record<string, any>;
|
||||
onDataChange: (data: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
const EntryMethodStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, onNext }) => {
|
||||
const EntryMethodStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNext }) => {
|
||||
const data = dataRef?.current || {};
|
||||
const [selectedMethod, setSelectedMethod] = useState<'manual' | 'upload'>(
|
||||
data.entryMethod || 'manual'
|
||||
);
|
||||
|
||||
const handleSelect = (method: 'manual' | 'upload') => {
|
||||
setSelectedMethod(method);
|
||||
onDataChange({ ...data, entryMethod: method });
|
||||
};
|
||||
|
||||
const handleContinue = () => {
|
||||
onDataChange({ ...data, entryMethod: selectedMethod });
|
||||
onNext();
|
||||
const newData = { ...data, entryMethod: method };
|
||||
onDataChange?.(newData);
|
||||
// Automatically advance to next step after selection
|
||||
setTimeout(() => onNext?.(), 100);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -166,17 +159,6 @@ const EntryMethodStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Continue Button */}
|
||||
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
|
||||
<button
|
||||
onClick={handleContinue}
|
||||
className="px-6 py-3 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 transition-colors font-medium inline-flex items-center gap-2"
|
||||
>
|
||||
Continuar
|
||||
<CheckCircle2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -185,14 +167,9 @@ const EntryMethodStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
// STEP 2a: Manual Entry Form
|
||||
// ========================================
|
||||
|
||||
const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, onNext }) => {
|
||||
const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNext }) => {
|
||||
const data = dataRef?.current || {};
|
||||
const { currentTenant } = useTenant();
|
||||
const [salesItems, setSalesItems] = useState(data.salesItems || []);
|
||||
const [saleDate, setSaleDate] = useState(
|
||||
data.saleDate || new Date().toISOString().split('T')[0]
|
||||
);
|
||||
const [paymentMethod, setPaymentMethod] = useState(data.paymentMethod || 'cash');
|
||||
const [notes, setNotes] = useState(data.notes || '');
|
||||
const [products, setProducts] = useState<any[]>([]);
|
||||
const [loadingProducts, setLoadingProducts] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -219,14 +196,15 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
};
|
||||
|
||||
const handleAddItem = () => {
|
||||
setSalesItems([
|
||||
...salesItems,
|
||||
const newItems = [
|
||||
...(data.salesItems || []),
|
||||
{ id: Date.now(), productId: '', product: '', quantity: 1, unitPrice: 0, subtotal: 0 },
|
||||
]);
|
||||
];
|
||||
onDataChange?.({ ...data, salesItems: newItems });
|
||||
};
|
||||
|
||||
const handleUpdateItem = (index: number, field: string, value: any) => {
|
||||
const updated = salesItems.map((item: any, i: number) => {
|
||||
const updated = (data.salesItems || []).map((item: any, i: number) => {
|
||||
if (i === index) {
|
||||
const newItem = { ...item, [field]: value };
|
||||
|
||||
@@ -247,28 +225,25 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
}
|
||||
return item;
|
||||
});
|
||||
setSalesItems(updated);
|
||||
onDataChange?.({ ...data, salesItems: updated });
|
||||
};
|
||||
|
||||
const handleRemoveItem = (index: number) => {
|
||||
setSalesItems(salesItems.filter((_: any, i: number) => i !== index));
|
||||
const newItems = (data.salesItems || []).filter((_: any, i: number) => i !== index);
|
||||
onDataChange?.({ ...data, salesItems: newItems });
|
||||
};
|
||||
|
||||
const calculateTotal = () => {
|
||||
return salesItems.reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0);
|
||||
return (data.salesItems || []).reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
onDataChange({
|
||||
// Auto-save totalAmount when items change
|
||||
useEffect(() => {
|
||||
onDataChange?.({
|
||||
...data,
|
||||
salesItems,
|
||||
saleDate,
|
||||
paymentMethod,
|
||||
notes,
|
||||
totalAmount: calculateTotal(),
|
||||
});
|
||||
onNext();
|
||||
};
|
||||
}, [data.salesItems]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -290,8 +265,8 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={saleDate}
|
||||
onChange={(e) => setSaleDate(e.target.value)}
|
||||
value={data.saleDate || new Date().toISOString().split('T')[0]}
|
||||
onChange={(e) => onDataChange?.({ ...data, saleDate: 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)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
@@ -302,8 +277,8 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
Método de Pago *
|
||||
</label>
|
||||
<select
|
||||
value={paymentMethod}
|
||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
||||
value={data.paymentMethod || 'cash'}
|
||||
onChange={(e) => onDataChange?.({ ...data, paymentMethod: 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)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
>
|
||||
<option value="cash">Efectivo</option>
|
||||
@@ -349,7 +324,7 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
<p>No hay productos terminados disponibles</p>
|
||||
<p className="text-sm">Agrega productos al inventario primero</p>
|
||||
</div>
|
||||
) : salesItems.length === 0 ? (
|
||||
) : (data.salesItems || []).length === 0 ? (
|
||||
<div className="text-center py-8 border-2 border-dashed border-[var(--border-secondary)] rounded-lg text-[var(--text-tertiary)]">
|
||||
<Package className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p>No hay productos agregados</p>
|
||||
@@ -357,7 +332,7 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{salesItems.map((item: any, index: number) => (
|
||||
{(data.salesItems || []).map((item: any, index: number) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="p-3 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]/30"
|
||||
@@ -421,7 +396,7 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
)}
|
||||
|
||||
{/* Total */}
|
||||
{salesItems.length > 0 && (
|
||||
{(data.salesItems || []).length > 0 && (
|
||||
<div className="pt-3 border-t border-[var(--border-primary)] text-right">
|
||||
<span className="text-lg font-bold text-[var(--text-primary)]">
|
||||
Total: €{calculateTotal().toFixed(2)}
|
||||
@@ -436,24 +411,13 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
Notas (Opcional)
|
||||
</label>
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
value={data.notes || ''}
|
||||
onChange={(e) => onDataChange?.({ ...data, notes: e.target.value })}
|
||||
placeholder="Información adicional sobre esta venta..."
|
||||
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)] bg-[var(--bg-primary)] text-[var(--text-primary)] text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={salesItems.length === 0}
|
||||
className="px-6 py-3 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
|
||||
>
|
||||
Guardar y Continuar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -462,9 +426,9 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
// STEP 2b: File Upload
|
||||
// ========================================
|
||||
|
||||
const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, onNext }) => {
|
||||
const FileUploadStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNext }) => {
|
||||
const data = dataRef?.current || {};
|
||||
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);
|
||||
@@ -474,26 +438,26 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFile = e.target.files?.[0];
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
onDataChange?.({ ...data, uploadedFile: selectedFile });
|
||||
setValidationResult(null);
|
||||
setError(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveFile = () => {
|
||||
setFile(null);
|
||||
onDataChange?.({ ...data, uploadedFile: null });
|
||||
setValidationResult(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleValidate = async () => {
|
||||
if (!file || !currentTenant?.id) return;
|
||||
if (!data.uploadedFile || !currentTenant?.id) return;
|
||||
|
||||
setValidating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await salesService.validateImportFile(currentTenant.id, file);
|
||||
const result = await salesService.validateImportFile(currentTenant.id, data.uploadedFile);
|
||||
setValidationResult(result);
|
||||
} catch (err: any) {
|
||||
console.error('Error validating file:', err);
|
||||
@@ -504,15 +468,15 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!file || !currentTenant?.id) return;
|
||||
if (!data.uploadedFile || !currentTenant?.id) return;
|
||||
|
||||
setImporting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await salesService.importSalesData(currentTenant.id, file, false);
|
||||
onDataChange({ ...data, uploadedFile: file, importResult: result });
|
||||
onNext();
|
||||
const result = await salesService.importSalesData(currentTenant.id, data.uploadedFile, false);
|
||||
onDataChange?.({ ...data, importResult: result });
|
||||
onNext?.();
|
||||
} catch (err: any) {
|
||||
console.error('Error importing file:', err);
|
||||
setError(err.response?.data?.detail || 'Error al importar el archivo');
|
||||
@@ -583,7 +547,7 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
|
||||
</div>
|
||||
|
||||
{/* File Upload Area */}
|
||||
{!file ? (
|
||||
{!data.uploadedFile ? (
|
||||
<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">
|
||||
@@ -613,9 +577,9 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
|
||||
<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="font-medium text-[var(--text-primary)]">{data.uploadedFile.name}</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{(file.size / 1024).toFixed(2)} KB
|
||||
{(data.uploadedFile.size / 1024).toFixed(2)} KB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -701,53 +665,8 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
|
||||
// STEP 3: Review & Confirm
|
||||
// ========================================
|
||||
|
||||
const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
|
||||
const { currentTenant } = useTenant();
|
||||
const [loading, setLoading] = useState(false);
|
||||
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 = {
|
||||
inventory_product_id: item.productId || null, // Include inventory product ID for stock tracking
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
showToast.success('Registro de ventas guardado exitosamente');
|
||||
onComplete();
|
||||
} catch (err: any) {
|
||||
console.error('Error saving sales data:', err);
|
||||
const errorMessage = err.response?.data?.detail || 'Error al guardar los datos de ventas';
|
||||
setError(errorMessage);
|
||||
showToast.error(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const ReviewStep: React.FC<WizardStepProps> = ({ dataRef }) => {
|
||||
const data = dataRef?.current || {};
|
||||
|
||||
const isManual = data.entryMethod === 'manual';
|
||||
const isUpload = data.entryMethod === 'upload';
|
||||
@@ -768,13 +687,6 @@ const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
|
||||
</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>
|
||||
)}
|
||||
|
||||
{isManual && data.salesItems && (
|
||||
<div className="space-y-4">
|
||||
{/* Summary */}
|
||||
@@ -796,10 +708,10 @@ const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
|
||||
{/* Items */}
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-2">
|
||||
Productos ({data.salesItems.length})
|
||||
Productos ({(data.salesItems || []).length})
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{data.salesItems.map((item: any) => (
|
||||
{(data.salesItems || []).map((item: any) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="p-3 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-primary)] flex justify-between items-center"
|
||||
@@ -853,27 +765,6 @@ const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Confirm Button */}
|
||||
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-primary)]">
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
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" />
|
||||
Confirmar y Guardar
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -883,20 +774,19 @@ const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
|
||||
// ========================================
|
||||
|
||||
export const SalesEntryWizardSteps = (
|
||||
data: Record<string, any>,
|
||||
dataRef: React.MutableRefObject<Record<string, any>>,
|
||||
setData: (data: Record<string, any>) => void
|
||||
): WizardStep[] => {
|
||||
const entryMethod = data.entryMethod;
|
||||
const entryMethod = dataRef.current.entryMethod;
|
||||
|
||||
// Dynamic steps based on entry method
|
||||
// New architecture: return direct component references instead of arrow functions
|
||||
// dataRef and onDataChange are now passed through WizardModal props
|
||||
const steps: WizardStep[] = [
|
||||
{
|
||||
id: 'entry-method',
|
||||
title: 'Método de Entrada',
|
||||
description: 'Elige cómo registrar las ventas',
|
||||
component: (props) => (
|
||||
<EntryMethodStep {...props} data={data} onDataChange={setData} />
|
||||
),
|
||||
component: EntryMethodStep,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -905,14 +795,18 @@ export const SalesEntryWizardSteps = (
|
||||
id: 'manual-entry',
|
||||
title: 'Ingresar Datos',
|
||||
description: 'Registra los detalles de la venta',
|
||||
component: (props) => <ManualEntryStep {...props} data={data} onDataChange={setData} />,
|
||||
component: ManualEntryStep,
|
||||
validate: () => {
|
||||
const data = dataRef.current;
|
||||
return (data.salesItems || []).length > 0;
|
||||
},
|
||||
});
|
||||
} else if (entryMethod === 'upload') {
|
||||
steps.push({
|
||||
id: 'file-upload',
|
||||
title: 'Cargar Archivo',
|
||||
description: 'Importa ventas desde archivo',
|
||||
component: (props) => <FileUploadStep {...props} data={data} onDataChange={setData} />,
|
||||
component: FileUploadStep,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -920,7 +814,51 @@ export const SalesEntryWizardSteps = (
|
||||
id: 'review',
|
||||
title: 'Revisar',
|
||||
description: 'Confirma los datos antes de guardar',
|
||||
component: (props) => <ReviewStep {...props} data={data} onDataChange={setData} />,
|
||||
component: ReviewStep,
|
||||
validate: async () => {
|
||||
const { useTenant } = await import('../../../../stores/tenant.store');
|
||||
const { salesService } = await import('../../../../api/services/sales');
|
||||
const { showToast } = await import('../../../../utils/toast');
|
||||
|
||||
const data = dataRef.current;
|
||||
const { currentTenant } = useTenant.getState();
|
||||
|
||||
if (!currentTenant?.id) {
|
||||
showToast.error('No se pudo obtener información del tenant');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (data.entryMethod === 'manual' && data.salesItems) {
|
||||
// Create individual sales records for each item
|
||||
for (const item of data.salesItems) {
|
||||
const salesData = {
|
||||
inventory_product_id: item.productId || null,
|
||||
product_name: item.product,
|
||||
product_category: 'general',
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
showToast.success('Registro de ventas guardado exitosamente');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
console.error('Error saving sales data:', err);
|
||||
const errorMessage = err.response?.data?.detail || 'Error al guardar los datos de ventas';
|
||||
showToast.error(errorMessage);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return steps;
|
||||
|
||||
Reference in New Issue
Block a user