xpeditis2.0/apps/frontend/src/components/admin/CsvUpload.tsx
2026-05-12 21:01:52 +02:00

225 lines
7.1 KiB
TypeScript

/**
* CSV Upload Component (ADMIN only)
*
* Upload CSV rate files for carrier companies
* Protected: Only accessible to users with ADMIN role
*/
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Upload, CheckCircle, XCircle, Loader2 } from 'lucide-react';
import { uploadCsvRates } from '@/lib/api/admin/csv-rates';
export function CsvUpload() {
const t = useTranslations('components.csvUpload');
const router = useRouter();
const [companyName, setCompanyName] = useState('');
const [companyEmail, setCompanyEmail] = useState('');
const [file, setFile] = useState<File | null>(null);
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0] || null;
setFile(selectedFile);
setError(null);
setSuccess(null);
if (selectedFile && !selectedFile.name.endsWith('.csv')) {
setError(t('fileTypeError'));
setFile(null);
}
if (selectedFile && selectedFile.size > 10 * 1024 * 1024) {
setError(t('fileSizeError'));
setFile(null);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!file || !companyName || !companyEmail) {
setError(t('fillAllFields'));
return;
}
setLoading(true);
setError(null);
setSuccess(null);
try {
const formData = new FormData();
formData.append('file', file);
formData.append('companyName', companyName);
formData.append('companyEmail', companyEmail);
const result = await uploadCsvRates(formData);
setSuccess(t('successMessage', { count: result.rowCount, companyName: result.companyName }));
setCompanyName('');
setCompanyEmail('');
setFile(null);
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) {
fileInput.value = '';
}
setTimeout(() => {
router.refresh();
}, 2000);
} catch (err: any) {
setError(err?.message || t('uploadError'));
} finally {
setLoading(false);
}
};
const handleReset = () => {
setCompanyName('');
setCompanyEmail('');
setFile(null);
setError(null);
setSuccess(null);
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) {
fileInput.value = '';
}
};
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Upload className="h-5 w-5" />
{t('title')}
</CardTitle>
<CardDescription>{t('description')}</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Company Name */}
<div className="space-y-2">
<Label htmlFor="company-name">
{t('form.companyName')} <span className="text-red-500">*</span>
</Label>
<Input
id="company-name"
value={companyName}
onChange={e => setCompanyName(e.target.value)}
placeholder={t('form.companyNamePlaceholder')}
required
disabled={loading}
/>
<p className="text-xs text-muted-foreground">{t('form.companyNameHint')}</p>
</div>
{/* Company Email */}
<div className="space-y-2">
<Label htmlFor="company-email">
{t('form.companyEmail')} <span className="text-red-500">*</span>
</Label>
<Input
id="company-email"
type="email"
value={companyEmail}
onChange={e => setCompanyEmail(e.target.value)}
placeholder={t('form.companyEmailPlaceholder')}
required
disabled={loading}
/>
<p className="text-xs text-muted-foreground">{t('form.companyEmailHint')}</p>
</div>
{/* File Input */}
<div className="space-y-2">
<Label htmlFor="file-input">
{t('form.fileLabel')} <span className="text-red-500">*</span>
</Label>
<Input
id="file-input"
type="file"
accept=".csv"
onChange={handleFileChange}
required
disabled={loading}
/>
{file && (
<p className="text-sm text-muted-foreground">
{t('form.fileSelected')} <strong>{file.name}</strong> (
{(file.size / 1024).toFixed(2)} KB)
</p>
)}
</div>
{/* CSV Format Info */}
<Alert>
<AlertTitle>{t('form.formatTitle')}</AlertTitle>
<AlertDescription className="text-xs space-y-1">
<p>{t('form.formatIntro')}</p>
<ul className="list-disc list-inside mt-2">
<li>companyName, origin, destination, containerType</li>
<li>minVolumeCBM, maxVolumeCBM, minWeightKG, maxWeightKG</li>
<li>palletCount, pricePerCBM, pricePerKG</li>
<li>basePriceUSD, basePriceEUR, currency</li>
<li>hasSurcharges, surchargeBAF, surchargeCAF</li>
<li>transitDays, validFrom, validUntil</li>
</ul>
</AlertDescription>
</Alert>
{/* Error Alert */}
{error && (
<Alert variant="destructive">
<XCircle className="h-4 w-4" />
<AlertTitle>{t('form.errorTitle')}</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* Success Alert */}
{success && (
<Alert className="border-green-500 text-green-600">
<CheckCircle className="h-4 w-4" />
<AlertTitle>{t('form.successTitle')}</AlertTitle>
<AlertDescription>{success}</AlertDescription>
</Alert>
)}
{/* Actions */}
<div className="flex gap-2">
<Button type="submit" disabled={loading || !file || !companyName || !companyEmail}>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t('form.submitting')}
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
{t('form.submit')}
</>
)}
</Button>
<Button type="button" variant="outline" onClick={handleReset} disabled={loading}>
{t('form.reset')}
</Button>
</div>
</form>
</CardContent>
</Card>
);
}