- Économie : ~1240 lignes de code dupliqué - Réduction : ~60% du code modal - Amélioration : Cohérence et maintenabilité
187 lines
5.8 KiB
TypeScript
187 lines
5.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
|
import { Upload, FileText, Download, AlertCircle } from 'lucide-react';
|
|
import { BaseModal } from './base/BaseModal';
|
|
import { ErrorDisplay } from './base/ErrorDisplay';
|
|
import { parseCSV, parseExcel, getExpectedColumns, downloadTemplate, validateFileType } from '@/lib/file-utils';
|
|
|
|
interface ImportFileModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onImport: (data: any[]) => void;
|
|
type: 'propositions' | 'participants';
|
|
campaignTitle?: string;
|
|
}
|
|
|
|
export default function ImportFileModal({
|
|
isOpen,
|
|
onClose,
|
|
onImport,
|
|
type,
|
|
campaignTitle
|
|
}: ImportFileModalProps) {
|
|
const [file, setFile] = useState<File | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [preview, setPreview] = useState<any[]>([]);
|
|
|
|
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const selectedFile = e.target.files?.[0];
|
|
if (selectedFile) {
|
|
// Valider le type de fichier
|
|
const validation = validateFileType(selectedFile);
|
|
if (!validation.isValid) {
|
|
setError(validation.error || 'Type de fichier non supporté');
|
|
return;
|
|
}
|
|
|
|
setFile(selectedFile);
|
|
setError('');
|
|
|
|
// Parser le fichier
|
|
const isCSV = selectedFile.type === 'text/csv' || selectedFile.name.toLowerCase().endsWith('.csv');
|
|
const result = isCSV ? await parseCSV(selectedFile) : await parseExcel(selectedFile);
|
|
|
|
if (result.error) {
|
|
setError(result.error);
|
|
return;
|
|
}
|
|
|
|
setPreview(result.data.slice(0, 5)); // Afficher les 5 premières lignes
|
|
}
|
|
};
|
|
|
|
const handleImport = async () => {
|
|
if (!file) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
const isCSV = file.type === 'text/csv' || file.name.toLowerCase().endsWith('.csv');
|
|
const result = isCSV ? await parseCSV(file) : await parseExcel(file);
|
|
|
|
if (result.error) {
|
|
setError(result.error);
|
|
return;
|
|
}
|
|
|
|
onImport(result.data);
|
|
onClose();
|
|
setFile(null);
|
|
setPreview([]);
|
|
} catch (error) {
|
|
setError('Erreur lors de l\'import du fichier.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleClose = () => {
|
|
setFile(null);
|
|
setPreview([]);
|
|
setError('');
|
|
onClose();
|
|
};
|
|
|
|
const footer = (
|
|
<>
|
|
<Button variant="outline" onClick={handleClose}>
|
|
Annuler
|
|
</Button>
|
|
<Button
|
|
onClick={handleImport}
|
|
disabled={!file || loading}
|
|
className="min-w-[100px]"
|
|
>
|
|
{loading ? 'Import...' : 'Importer'}
|
|
</Button>
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<BaseModal
|
|
isOpen={isOpen}
|
|
onClose={handleClose}
|
|
title={`Importer des ${type === 'propositions' ? 'propositions' : 'participants'} depuis un fichier`}
|
|
description={`Importez en masse des ${type === 'propositions' ? 'propositions' : 'participants'} depuis un fichier CSV, ODS, XLSX ou XLS.${campaignTitle ? ` Campagne : ${campaignTitle}` : ''}`}
|
|
footer={footer}
|
|
maxWidth="sm:max-w-[600px]"
|
|
>
|
|
<ErrorDisplay error={error} />
|
|
|
|
{/* Template download */}
|
|
<div className="flex items-center justify-between p-3 bg-slate-50 dark:bg-slate-800 rounded-lg">
|
|
<div className="flex items-center gap-2">
|
|
<FileText className="w-4 h-4 text-slate-600" />
|
|
<span className="text-sm text-slate-600 dark:text-slate-300">
|
|
Téléchargez le modèle
|
|
</span>
|
|
</div>
|
|
<Button variant="outline" size="sm" onClick={() => downloadTemplate(type)}>
|
|
<Download className="w-4 h-4 mr-1" />
|
|
Modèle
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Expected columns */}
|
|
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
|
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">
|
|
Colonnes attendues :
|
|
</h4>
|
|
<div className="text-sm text-blue-800 dark:text-blue-200">
|
|
{getExpectedColumns(type).join(', ')}
|
|
</div>
|
|
</div>
|
|
|
|
{/* File upload */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="file-upload">Sélectionner un fichier (CSV, ODS, XLSX, XLS)</Label>
|
|
<Input
|
|
id="file-upload"
|
|
type="file"
|
|
accept=".csv,.ods,.xlsx,.xls"
|
|
onChange={handleFileChange}
|
|
className="cursor-pointer"
|
|
/>
|
|
</div>
|
|
|
|
{/* Preview */}
|
|
{preview.length > 0 && (
|
|
<div className="space-y-2">
|
|
<Label>Aperçu des données (5 premières lignes)</Label>
|
|
<div className="max-h-40 max-w-full overflow-auto border rounded-lg">
|
|
<div className="min-w-full">
|
|
<table className="w-full text-sm table-fixed">
|
|
<thead className="bg-slate-50 dark:bg-slate-800">
|
|
<tr>
|
|
{Object.keys(preview[0] || {}).map((header) => (
|
|
<th key={header} className="px-2 py-1 text-left font-medium truncate">
|
|
{header}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{preview.map((row, index) => (
|
|
<tr key={index} className="border-t">
|
|
{Object.values(row).map((value, cellIndex) => (
|
|
<td key={cellIndex} className="px-2 py-1 text-xs truncate">
|
|
{String(value)}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</BaseModal>
|
|
);
|
|
}
|