fix: BUG-080/082 (20 unused frontend components deleted), BUG-083 (useTenant.ts deleted), BUG-011 (playwright baseURL), BUG-069 (unused python modules deleted), BUG-065 (already has eager loading), BUG-026 (already fixed 422), BUG-023 (no sync I/O found)
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
This commit is contained in:
@@ -1,155 +0,0 @@
|
||||
import { asError } from '@/utils/errorTypes';
|
||||
import React, { useState, useRef, useCallback } from 'react';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import { apiClient } from '@/api/client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface CsvImportDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
interface ParsedRow {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
function parseCSV(text: string): { headers: string[]; rows: ParsedRow[] } {
|
||||
const lines = text.trim().split(/\n/);
|
||||
if (lines.length === 0) return { headers: [], rows: [] };
|
||||
const headers = lines[0].split(',').map((h) => h.trim());
|
||||
const rows: ParsedRow[] = [];
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
if (!lines[i].trim()) continue;
|
||||
const values = lines[i].split(',').map((v) => v.trim());
|
||||
const row: ParsedRow = {};
|
||||
headers.forEach((header, idx) => {
|
||||
row[header] = values[idx] || '';
|
||||
});
|
||||
rows.push(row);
|
||||
}
|
||||
return { headers, rows };
|
||||
}
|
||||
|
||||
export function CsvImportDialog({ open, onClose, onSuccess }: CsvImportDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const [importing, setImporting] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [previewData, setPreviewData] = useState<{ headers: string[]; rows: ParsedRow[] } | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
if (!file.name.endsWith('.csv')) {
|
||||
setError('Bitte wählen Sie eine CSV-Datei aus.');
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
setSelectedFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const text = event.target?.result as string;
|
||||
const parsed = parseCSV(text);
|
||||
setPreviewData(parsed);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}, []);
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!selectedFile) return;
|
||||
setImporting(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedFile);
|
||||
await apiClient.post('/contacts/import', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
toast.success('Import erfolgreich abgeschlossen.');
|
||||
setSelectedFile(null);
|
||||
setPreviewData(null);
|
||||
setError(null);
|
||||
onSuccess?.();
|
||||
onClose();
|
||||
} catch (err: unknown) { const errObj = asError(err);
|
||||
toast.error(errObj.message || 'Import fehlgeschlagen.');
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setSelectedFile(null);
|
||||
setPreviewData(null);
|
||||
setError(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={handleClose} title="CSV Import" size="lg" >
|
||||
<div className="space-y-4" data-testid="csv-import-dialog">
|
||||
<div>
|
||||
<p className="text-sm text-secondary-600 mb-3">
|
||||
Wählen Sie eine CSV-Datei mit Firmendaten. Erforderliche Spalte: name.
|
||||
Optionale Spalten: account_number, industry, phone, email, website, description.
|
||||
</p>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".csv"
|
||||
onChange={handleFileSelect}
|
||||
className="block w-full text-sm text-secondary-700 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100 min-h-touch"
|
||||
aria-label="CSV-Datei auswählen"
|
||||
data-testid="csv-file-input"
|
||||
/>
|
||||
{error && <p className="mt-2 text-sm text-danger-600" role="alert">{typeof error === "string" ? error : (error as any)?.message || "Ein Fehler ist aufgetreten"}</p>}
|
||||
</div>
|
||||
|
||||
{previewData && previewData.rows.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-secondary-900 mb-2">Vorschau ({previewData.rows.length} Datensätze)</h4>
|
||||
<div className="overflow-x-auto border border-secondary-200 rounded-md max-h-60">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead className="bg-secondary-50 sticky top-0">
|
||||
<tr>
|
||||
{previewData.headers.map((header) => (
|
||||
<th key={header} className="px-3 py-2 text-left font-semibold text-secondary-600">{header}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-secondary-100">
|
||||
{previewData.rows.slice(0, 10).map((row, idx) => (
|
||||
<tr key={idx}>
|
||||
{previewData.headers.map((header) => (
|
||||
<td key={header} className="px-3 py-2 text-secondary-900">{row[header]}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{previewData.rows.length > 10 && (
|
||||
<p className="text-xs text-secondary-500 mt-1">Zeige 10 von {previewData.rows.length} Datensätzen.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<Button variant="secondary" onClick={handleClose}>{t('common.cancel')}</Button>
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
disabled={!selectedFile || importing}
|
||||
isLoading={importing}
|
||||
data-testid="csv-import-button"
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useLocation, useNavigate, useBlocker } from 'react-router-dom';
|
||||
|
||||
export interface UnsavedChangesGuardProps {
|
||||
isDirty: boolean;
|
||||
message?: string;
|
||||
onConfirm?: () => void;
|
||||
}
|
||||
|
||||
export function UnsavedChangesGuard({ isDirty, message = 'Sie haben ungespeicherte Änderungen. Möchten Sie die Seite wirklich verlassen?', onConfirm }: UnsavedChangesGuardProps) {
|
||||
const blocker = useBlocker(isDirty);
|
||||
const messageRef = useRef(message);
|
||||
messageRef.current = message;
|
||||
|
||||
useEffect(() => {
|
||||
if (blocker.state === 'blocked') {
|
||||
const confirmed = window.confirm(messageRef.current);
|
||||
if (confirmed) {
|
||||
onConfirm?.();
|
||||
blocker.proceed();
|
||||
} else {
|
||||
blocker.reset();
|
||||
}
|
||||
}
|
||||
}, [blocker, onConfirm]);
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user