Files
mes-budgets-participatifs/src/components/ImportFileModal.tsx
Yannick Le Duc dc388bf371 refactoring majeur (code dupliqué, mort, ...)
- Économie : ~1240 lignes de code dupliqué
- Réduction : ~60% du code modal
- Amélioration : Cohérence et maintenabilité
2025-08-27 12:45:37 +02:00

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>
);
}