All checks were successful
Dev CI / Backend — Lint (push) Successful in 10m23s
Dev CI / Backend — Unit Tests (push) Successful in 10m17s
Dev CI / Frontend — Lint & Type-check (push) Successful in 11m3s
Dev CI / Frontend — Unit Tests (push) Successful in 10m33s
Dev CI / Notify Failure (push) Has been skipped
199 lines
6.4 KiB
TypeScript
199 lines
6.4 KiB
TypeScript
/**
|
|
* Admin CSV Rates Management Page
|
|
*
|
|
* ADMIN-only page for:
|
|
* - Uploading CSV rate files
|
|
* - Viewing CSV configurations
|
|
* - Managing carrier rate data
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useTranslations, useLocale } from 'next-intl';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
|
import { Loader2, RefreshCw, Trash2 } from 'lucide-react';
|
|
import { CsvUpload } from '@/components/admin/CsvUpload';
|
|
import { listCsvFiles, deleteCsvFile, type CsvFileInfo } from '@/lib/api/admin/csv-rates';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
|
|
export default function AdminCsvRatesPage() {
|
|
const t = useTranslations('dashboard.admin.csvRates');
|
|
const locale = useLocale();
|
|
const dateLocale = locale === 'fr' ? 'fr-FR' : 'en-US';
|
|
|
|
const [files, setFiles] = useState<CsvFileInfo[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const fetchFiles = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const data = await listCsvFiles();
|
|
setFiles(data.files || []);
|
|
} catch (err: any) {
|
|
setError(err?.message || t('loadError'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchFiles();
|
|
}, []);
|
|
|
|
const handleDelete = async (filename: string) => {
|
|
if (!confirm(t('confirmDelete', { filename }))) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await deleteCsvFile(filename);
|
|
alert(t('deleteSuccess', { filename }));
|
|
fetchFiles(); // Refresh list
|
|
} catch (err: any) {
|
|
alert(t('deleteError', { message: err?.message || t('deleteFailedFallback') }));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="container mx-auto py-8 space-y-6">
|
|
{/* Page Header */}
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">{t('title')}</h1>
|
|
<p className="text-muted-foreground mt-2">
|
|
{t('subtitle')}
|
|
</p>
|
|
<Badge variant="destructive" className="mt-2">
|
|
{t('adminBadge')}
|
|
</Badge>
|
|
</div>
|
|
|
|
{/* Upload Section */}
|
|
<CsvUpload />
|
|
|
|
{/* Configurations Table */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle>{t('cardTitle')}</CardTitle>
|
|
<CardDescription>
|
|
{t('cardDescription')}
|
|
</CardDescription>
|
|
</div>
|
|
<Button variant="outline" size="sm" onClick={fetchFiles} disabled={loading}>
|
|
{loading ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<RefreshCw className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{error && (
|
|
<Alert variant="destructive" className="mb-4">
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
</div>
|
|
) : files.length === 0 ? (
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
{t('empty')}
|
|
</div>
|
|
) : (
|
|
<div className="rounded-md border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>{t('table.filename')}</TableHead>
|
|
<TableHead>{t('table.size')}</TableHead>
|
|
<TableHead>{t('table.rows')}</TableHead>
|
|
<TableHead>{t('table.uploadedAt')}</TableHead>
|
|
<TableHead>{t('table.email')}</TableHead>
|
|
<TableHead>{t('table.actions')}</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{files.map((file) => (
|
|
<TableRow key={file.filename}>
|
|
<TableCell className="font-medium font-mono text-xs">{file.filename}</TableCell>
|
|
<TableCell>
|
|
{(file.size / 1024).toFixed(2)} KB
|
|
</TableCell>
|
|
<TableCell>
|
|
{file.rowCount ? (
|
|
<span className="font-semibold">{t('table.rowCount', { count: file.rowCount })}</span>
|
|
) : (
|
|
<span className="text-muted-foreground">-</span>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="text-xs text-muted-foreground">
|
|
{new Date(file.uploadedAt).toLocaleDateString(dateLocale)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="text-xs text-muted-foreground">
|
|
{file.companyEmail ?? '—'}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleDelete(file.filename)}
|
|
>
|
|
<Trash2 className="h-4 w-4 text-red-600" />
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Info Card */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('infoTitle')}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2 text-sm">
|
|
<p>
|
|
<strong>{t('info.formatLabel')}</strong> {t('info.formatBody')}
|
|
</p>
|
|
<p>
|
|
<strong>{t('info.sizeLabel')}</strong> {t('info.sizeBody')}
|
|
</p>
|
|
<p>
|
|
<strong>{t('info.updateLabel')}</strong> {t('info.updateBody')}
|
|
</p>
|
|
<p>
|
|
<strong>{t('info.validationLabel')}</strong> {t('info.validationBody')}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|