Files
bakery-ia/frontend/src/components/domain/unified-wizard/wizards/SalesEntryWizard.tsx
2025-11-18 07:17:17 +01:00

875 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import {
Edit3,
Upload,
CheckCircle2,
Download,
FileSpreadsheet,
Calendar,
DollarSign,
Package,
CreditCard,
Loader2,
X,
AlertCircle,
} from 'lucide-react';
import { useTenant } from '../../../../stores/tenant.store';
import { salesService } from '../../../../api/services/sales';
import { inventoryService } from '../../../../api/services/inventory';
import { showToast } from '../../../../utils/toast';
// ========================================
// STEP 1: Entry Method Selection
// ========================================
const EntryMethodStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNext }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards');
const [selectedMethod, setSelectedMethod] = useState<'manual' | 'upload'>(
data.entryMethod || 'manual'
);
const handleSelect = (method: 'manual' | 'upload') => {
setSelectedMethod(method);
const newData = { ...data, entryMethod: method };
onDataChange?.(newData);
// Automatically advance to next step after selection
setTimeout(() => onNext?.(), 100);
};
return (
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
{t('salesEntry.entryMethod.title')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('salesEntry.entryMethod.subtitle')}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Manual Entry Option */}
<button
onClick={() => handleSelect('manual')}
className={`
p-6 rounded-xl border-2 transition-all duration-200 text-left
hover:shadow-lg hover:-translate-y-1
focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:ring-offset-2
${
selectedMethod === 'manual'
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5 shadow-md'
: 'border-[var(--border-secondary)] bg-[var(--bg-primary)]'
}
`}
>
<div className="flex items-start gap-4">
<div
className={`
p-3 rounded-lg transition-colors
${
selectedMethod === 'manual'
? 'bg-[var(--color-primary)] text-white'
: 'bg-[var(--bg-secondary)] text-[var(--color-primary)]'
}
`}
>
<Edit3 className="w-6 h-6" />
</div>
<div className="flex-1">
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
{t('salesEntry.entryMethod.manual.title')}
</h4>
<p className="text-sm text-[var(--text-secondary)] mb-3">
{t('salesEntry.entryMethod.manual.description')}
</p>
<div className="space-y-1 text-xs text-[var(--text-tertiary)]">
<p className="flex items-center gap-1.5">
<CheckCircle2 className="w-3.5 h-3.5 text-green-600" />
{t('salesEntry.entryMethod.manual.benefits.1')}
</p>
<p className="flex items-center gap-1.5">
<CheckCircle2 className="w-3.5 h-3.5 text-green-600" />
{t('salesEntry.entryMethod.manual.benefits.2')}
</p>
<p className="flex items-center gap-1.5">
<CheckCircle2 className="w-3.5 h-3.5 text-green-600" />
{t('salesEntry.entryMethod.manual.benefits.3')}
</p>
</div>
</div>
</div>
</button>
{/* File Upload Option */}
<button
onClick={() => handleSelect('upload')}
className={`
relative p-6 rounded-xl border-2 transition-all duration-200 text-left
hover:shadow-lg hover:-translate-y-1
focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:ring-offset-2
${
selectedMethod === 'upload'
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5 shadow-md'
: 'border-[var(--border-secondary)] bg-[var(--bg-primary)]'
}
`}
>
{/* Recommended Badge */}
<div className="absolute top-3 right-3">
<span className="px-2 py-1 text-xs rounded-full bg-gradient-to-r from-amber-100 to-orange-100 text-orange-800 font-semibold">
{t('salesEntry.entryMethod.file.recommended')}
</span>
</div>
<div className="flex items-start gap-4">
<div
className={`
p-3 rounded-lg transition-colors
${
selectedMethod === 'upload'
? 'bg-[var(--color-primary)] text-white'
: 'bg-[var(--bg-secondary)] text-[var(--color-primary)]'
}
`}
>
<Upload className="w-6 h-6" />
</div>
<div className="flex-1">
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
{t('salesEntry.entryMethod.file.title')}
</h4>
<p className="text-sm text-[var(--text-secondary)] mb-3">
{t('salesEntry.entryMethod.file.description')}
</p>
<div className="space-y-1 text-xs text-[var(--text-tertiary)]">
<p className="flex items-center gap-1.5">
<CheckCircle2 className="w-3.5 h-3.5 text-green-600" />
{t('salesEntry.entryMethod.file.benefits.1')}
</p>
<p className="flex items-center gap-1.5">
<CheckCircle2 className="w-3.5 h-3.5 text-green-600" />
{t('salesEntry.entryMethod.file.benefits.2')}
</p>
<p className="flex items-center gap-1.5">
<CheckCircle2 className="w-3.5 h-3.5 text-green-600" />
{t('salesEntry.entryMethod.file.benefits.3')}
</p>
</div>
</div>
</div>
</button>
</div>
</div>
);
};
// ========================================
// STEP 2a: Manual Entry Form
// ========================================
const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNext }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards');
const { currentTenant } = useTenant();
const [products, setProducts] = useState<any[]>([]);
const [loadingProducts, setLoadingProducts] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchProducts();
}, []);
const fetchProducts = async () => {
if (!currentTenant?.id) return;
setLoadingProducts(true);
try {
const result = await inventoryService.getIngredients(currentTenant.id);
// Filter for finished products only
const finishedProducts = result.filter((p: any) => p.category === 'finished_product');
setProducts(finishedProducts);
} catch (err: any) {
console.error('Error fetching products:', err);
setError(t('salesEntry.messages.errorLoadingProducts'));
} finally {
setLoadingProducts(false);
}
};
const handleAddItem = () => {
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 = (data.salesItems || []).map((item: any, i: number) => {
if (i === index) {
const newItem = { ...item, [field]: value };
// If product is selected, auto-fill price
if (field === 'productId') {
const selectedProduct = products.find((p: any) => p.id === value);
if (selectedProduct) {
newItem.product = selectedProduct.name;
newItem.unitPrice = selectedProduct.average_cost || selectedProduct.last_purchase_price || 0;
}
}
// Auto-calculate subtotal
if (field === 'quantity' || field === 'unitPrice' || field === 'productId') {
newItem.subtotal = (newItem.quantity || 0) * (newItem.unitPrice || 0);
}
return newItem;
}
return item;
});
onDataChange?.({ ...data, salesItems: updated });
};
const handleRemoveItem = (index: number) => {
const newItems = (data.salesItems || []).filter((_: any, i: number) => i !== index);
onDataChange?.({ ...data, salesItems: newItems });
};
const calculateTotal = () => {
return (data.salesItems || []).reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0);
};
// Auto-save totalAmount when items change
useEffect(() => {
onDataChange?.({
...data,
totalAmount: calculateTotal(),
});
}, [data.salesItems]);
return (
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
{t('salesEntry.manualEntry.title')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('salesEntry.manualEntry.subtitle')}
</p>
</div>
{/* Date and Payment Method */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
<Calendar className="w-4 h-4 inline mr-1.5" />
{t('salesEntry.manualEntry.fields.saleDate')} *
</label>
<input
type="date"
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>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
<CreditCard className="w-4 h-4 inline mr-1.5" />
{t('salesEntry.manualEntry.fields.paymentMethod')} *
</label>
<select
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">{t('salesEntry.paymentMethods.cash')}</option>
<option value="card">{t('salesEntry.paymentMethods.card')}</option>
<option value="mobile">{t('salesEntry.paymentMethods.mobile')}</option>
<option value="transfer">{t('salesEntry.paymentMethods.transfer')}</option>
<option value="other">{t('salesEntry.paymentMethods.other')}</option>
</select>
</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>
)}
{/* Sales Items */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium text-[var(--text-secondary)]">
<Package className="w-4 h-4 inline mr-1.5" />
{t('salesEntry.manualEntry.products.title')}
</label>
<button
onClick={handleAddItem}
disabled={loadingProducts}
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"
>
{t('salesEntry.manualEntry.products.addProduct')}
</button>
</div>
{loadingProducts ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 animate-spin text-[var(--color-primary)]" />
<span className="ml-3 text-[var(--text-secondary)]">{t('salesEntry.manualEntry.products.loading')}</span>
</div>
) : products.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>{t('salesEntry.manualEntry.products.noFinishedProducts')}</p>
<p className="text-sm">{t('salesEntry.manualEntry.products.addToInventory')}</p>
</div>
) : (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>{t('salesEntry.manualEntry.products.noProductsAdded')}</p>
<p className="text-sm">{t('salesEntry.manualEntry.products.clickToBegin')}</p>
</div>
) : (
<div className="space-y-2">
{(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"
>
<div className="grid grid-cols-12 gap-2 items-center">
<div className="col-span-12 sm:col-span-5">
<select
value={item.productId}
onChange={(e) => handleUpdateItem(index, 'productId', e.target.value)}
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)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
>
<option value="">{t('salesEntry.manualEntry.products.selectProduct')}</option>
{products.map((product: any) => (
<option key={product.id} value={product.id}>
{product.name} - {(product.average_cost || product.last_purchase_price || 0).toFixed(2)}
</option>
))}
</select>
</div>
<div className="col-span-4 sm:col-span-2">
<input
type="number"
placeholder={t('salesEntry.manualEntry.products.quantity')}
value={item.quantity}
onChange={(e) =>
handleUpdateItem(index, 'quantity', parseFloat(e.target.value) || 0)
}
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)]"
min="0"
step="1"
/>
</div>
<div className="col-span-4 sm:col-span-2">
<input
type="number"
placeholder={t('salesEntry.manualEntry.products.price')}
value={item.unitPrice}
onChange={(e) =>
handleUpdateItem(index, 'unitPrice', parseFloat(e.target.value) || 0)
}
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)]"
min="0"
step="0.01"
/>
</div>
<div className="col-span-3 sm:col-span-2 text-sm font-semibold text-[var(--text-primary)]">
{item.subtotal.toFixed(2)}
</div>
<div className="col-span-1 sm:col-span-1 flex justify-end">
<button
onClick={() => handleRemoveItem(index)}
className="p-1 text-red-500 hover:text-red-700 transition-colors"
>
</button>
</div>
</div>
</div>
))}
</div>
)}
{/* Total */}
{(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)]">
{t('salesEntry.manualEntry.products.total')} {calculateTotal().toFixed(2)}
</span>
</div>
)}
</div>
{/* Notes */}
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{t('salesEntry.manualEntry.fields.notes')}
</label>
<textarea
value={data.notes || ''}
onChange={(e) => onDataChange?.({ ...data, notes: e.target.value })}
placeholder={t('salesEntry.manualEntry.fields.notesPlaceholder')}
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>
</div>
);
};
// ========================================
// STEP 2b: File Upload
// ========================================
const FileUploadStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNext }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards');
const { currentTenant } = useTenant();
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) {
onDataChange?.({ ...data, uploadedFile: selectedFile });
setValidationResult(null);
setError(null);
}
};
const handleRemoveFile = () => {
onDataChange?.({ ...data, uploadedFile: null });
setValidationResult(null);
setError(null);
};
const handleValidate = async () => {
if (!data.uploadedFile || !currentTenant?.id) return;
setValidating(true);
setError(null);
try {
const result = await salesService.validateImportFile(currentTenant.id, data.uploadedFile);
setValidationResult(result);
} catch (err: any) {
console.error('Error validating file:', err);
setError(err.response?.data?.detail || t('salesEntry.messages.errorValidatingFile'));
} finally {
setValidating(false);
}
};
const handleImport = async () => {
if (!data.uploadedFile || !currentTenant?.id) return;
setImporting(true);
setError(null);
try {
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 || t('salesEntry.messages.errorImportingFile'));
} 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(t('salesEntry.messages.errorValidatingFile'));
} finally {
setDownloadingTemplate(false);
}
};
return (
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
{t('salesEntry.fileUpload.title')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('salesEntry.fileUpload.subtitle')}
</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>
)}
{/* Download Template Button */}
<div className="flex justify-center">
<button
onClick={handleDownloadTemplate}
disabled={downloadingTemplate}
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"
>
{downloadingTemplate ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
{t('salesEntry.fileUpload.downloading')}
</>
) : (
<>
<Download className="w-4 h-4" />
{t('salesEntry.fileUpload.downloadTemplate')}
</>
)}
</button>
</div>
{/* File Upload Area */}
{!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">
{t('salesEntry.fileUpload.dragDrop.title')}
</h4>
<p className="text-sm text-[var(--text-secondary)] mb-4">
{t('salesEntry.fileUpload.dragDrop.subtitle')}
</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">
{t('salesEntry.fileUpload.dragDrop.button')}
</span>
</label>
<p className="text-xs text-[var(--text-tertiary)] mt-3">
{t('salesEntry.fileUpload.dragDrop.supportedFormats')}
</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)]">{data.uploadedFile.name}</p>
<p className="text-sm text-[var(--text-secondary)]">
{(data.uploadedFile.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">
{t('salesEntry.fileUpload.validated.title')}
</p>
<div className="text-xs text-blue-700 space-y-1">
<p>{t('salesEntry.fileUpload.validated.recordsFound')} {validationResult.total_rows || 0}</p>
<p>{t('salesEntry.fileUpload.validated.validRecords')} {validationResult.valid_rows || 0}</p>
{validationResult.errors?.length > 0 && (
<p className="text-red-600">
{t('salesEntry.fileUpload.validated.errors')} {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" />
{t('salesEntry.fileUpload.validating')}
</>
) : (
<>
<CheckCircle2 className="w-4 h-4" />
{t('salesEntry.fileUpload.validateButton')}
</>
)}
</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" />
{t('salesEntry.fileUpload.importing')}
</>
) : (
<>
<Upload className="w-4 h-4" />
{t('salesEntry.fileUpload.importButton')}
</>
)}
</button>
</div>
</div>
)}
<div className="text-center text-sm text-[var(--text-tertiary)]">
<p>{t('salesEntry.fileUpload.instructions.title')}</p>
<p className="font-mono text-xs mt-1">
{t('salesEntry.fileUpload.instructions.columns')}
</p>
</div>
</div>
);
};
// ========================================
// STEP 3: Review & Confirm
// ========================================
const ReviewStep: React.FC<WizardStepProps> = ({ dataRef }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards');
const isManual = data.entryMethod === 'manual';
const isUpload = data.entryMethod === 'upload';
return (
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<div className="flex items-center justify-center mb-3">
<div className="p-3 bg-green-100 rounded-full">
<CheckCircle2 className="w-8 h-8 text-green-600" />
</div>
</div>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
{t('salesEntry.review.title')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('salesEntry.review.subtitle')}
</p>
</div>
{isManual && data.salesItems && (
<div className="space-y-4">
{/* Summary */}
<div className="p-4 bg-[var(--bg-secondary)]/50 rounded-lg">
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<span className="text-[var(--text-secondary)]">{t('salesEntry.review.fields.date')}</span>
<p className="font-semibold text-[var(--text-primary)]">{data.saleDate}</p>
</div>
<div>
<span className="text-[var(--text-secondary)]">{t('salesEntry.review.fields.paymentMethod')}</span>
<p className="font-semibold text-[var(--text-primary)] capitalize">
{data.paymentMethod}
</p>
</div>
</div>
</div>
{/* Items */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-2">
{t('salesEntry.review.fields.products')} ({(data.salesItems || []).length})
</h4>
<div className="space-y-2">
{(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"
>
<div className="flex-1">
<p className="font-medium text-[var(--text-primary)]">{item.product}</p>
<p className="text-sm text-[var(--text-secondary)]">
{item.quantity} × {item.unitPrice.toFixed(2)}
</p>
</div>
<div className="text-right">
<p className="font-semibold text-[var(--text-primary)]">
{item.subtotal.toFixed(2)}
</p>
</div>
</div>
))}
</div>
</div>
{/* Total */}
<div className="p-4 bg-gradient-to-r from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 rounded-lg border-2 border-[var(--color-primary)]/20">
<div className="flex justify-between items-center">
<span className="text-lg font-semibold text-[var(--text-primary)]">{t('salesEntry.review.fields.total')}</span>
<span className="text-2xl font-bold text-[var(--color-primary)]">
{data.totalAmount?.toFixed(2)}
</span>
</div>
</div>
{/* Notes */}
{data.notes && (
<div className="p-3 bg-[var(--bg-secondary)]/30 rounded-lg">
<p className="text-sm text-[var(--text-secondary)] mb-1">{t('salesEntry.review.fields.notes')}</p>
<p className="text-sm text-[var(--text-primary)]">{data.notes}</p>
</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">
{t('salesEntry.review.imported.title')}
</p>
<div className="text-sm text-green-700 space-y-1">
<p>{t('salesEntry.review.imported.recordsImported')} {data.importResult.successful_imports || 0}</p>
<p>{t('salesEntry.review.imported.recordsFailed')} {data.importResult.failed_imports || 0}</p>
</div>
</div>
</div>
)}
</div>
);
};
// ========================================
// Export Wizard Steps
// ========================================
export const SalesEntryWizardSteps = (
dataRef: React.MutableRefObject<Record<string, any>>,
setData: (data: Record<string, any>) => void
): WizardStep[] => {
const entryMethod = dataRef.current.entryMethod;
// 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: 'salesEntry.steps.entryMethod',
description: 'salesEntry.steps.entryMethodDescription',
component: EntryMethodStep,
},
];
if (entryMethod === 'manual') {
steps.push({
id: 'manual-entry',
title: 'salesEntry.steps.manualEntry',
description: 'salesEntry.steps.manualEntryDescription',
component: ManualEntryStep,
validate: () => {
const data = dataRef.current;
return (data.salesItems || []).length > 0;
},
});
} else if (entryMethod === 'upload') {
steps.push({
id: 'file-upload',
title: 'salesEntry.steps.fileUpload',
description: 'salesEntry.steps.fileUploadDescription',
component: FileUploadStep,
});
}
steps.push({
id: 'review',
title: 'salesEntry.steps.review',
description: 'salesEntry.steps.reviewDescription',
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) {
const { showToast } = await import('../../../../utils/toast');
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);
}
}
const { showToast } = await import('../../../../utils/toast');
showToast.success('Registro de ventas guardado exitosamente');
return true;
} catch (err: any) {
console.error('Error saving sales data:', err);
const { showToast } = await import('../../../../utils/toast');
const errorMessage = err.response?.data?.detail || 'Error al guardar los datos de ventas';
showToast.error(errorMessage);
return false;
}
},
});
return steps;
};