geraete/app/(app)/users/UsersCsvImportButton.tsx
2025-11-26 08:02:48 +01:00

316 lines
8.3 KiB
TypeScript

// app/(app)/users/UsersCsvImportButton.tsx
'use client';
import { useRef, useState } from 'react';
import { useRouter } from 'next/navigation';
import Button from '@/components/ui/Button';
type SimpleGroup = {
id: string;
name: string;
};
type Props = {
groups: SimpleGroup[];
};
type ParsedRow = {
nwkennung: string;
lastName: string;
firstName: string;
arbeitsname: string;
groupName: string | null;
rawLine: string;
lineNumber: number;
};
export default function UsersCsvImportButton({ groups }: Props) {
const router = useRouter();
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [importing, setImporting] = useState(false);
const [importError, setImportError] = useState<string | null>(null);
const [importSummary, setImportSummary] = useState<string | null>(null);
const [importProgress, setImportProgress] = useState<string | null>(null);
async function handleImportCsv(
e: React.ChangeEvent<HTMLInputElement>,
): Promise<void> {
const file = e.target.files?.[0];
if (!file) return;
setImporting(true);
setImportError(null);
setImportSummary(null);
setImportProgress('Lese Datei …');
try {
const text = await file.text();
setImportProgress('Analysiere CSV …');
const lines = text.split(/\r?\n/);
const parsedRows: ParsedRow[] = [];
let skippedCount = 0;
for (let index = 0; index < lines.length; index++) {
const raw = lines[index];
const trimmed = raw.trim();
if (!trimmed) continue;
// erste Zeile = Header
if (index === 0) continue;
const parts = trimmed.split(';');
if (parts.length < 4) {
console.warn('Zeile übersprungen (falsches Format):', trimmed);
skippedCount++;
continue;
}
const [
nwkennungRaw,
lastNameRaw,
firstNameRaw,
arbeitsnameRaw,
groupRaw,
] = parts;
const nwkennung = (nwkennungRaw ?? '').trim().toLowerCase();
const lastName = (lastNameRaw ?? '').trim();
const firstName = (firstNameRaw ?? '').trim();
const arbeitsname = (arbeitsnameRaw ?? '').trim();
const groupNameRaw = (groupRaw ?? '').trim();
const groupName = groupNameRaw ? groupNameRaw : null;
if (!nwkennung || !lastName || !firstName || !arbeitsname) {
console.warn(
'Zeile übersprungen (Pflichtfelder leer):',
trimmed,
);
skippedCount++;
continue;
}
parsedRows.push({
nwkennung,
lastName,
firstName,
arbeitsname,
groupName,
rawLine: trimmed,
lineNumber: index + 1,
});
}
if (parsedRows.length === 0) {
setImportError(
'Keine gültigen Zeilen gefunden. Bitte CSV prüfen.',
);
return;
}
// 1) Gruppen vorbereiten: bestehende + neue
setImportProgress('Ermittle Gruppen …');
const groupCache = new Map<string, string>();
for (const g of groups) {
const name = g.name.trim();
if (name) groupCache.set(name, g.id);
}
const knownGroupNames = new Set<string>(
Array.from(groupCache.keys()),
);
const newGroupNamesSet = new Set<string>();
for (const row of parsedRows) {
if (!row.groupName) continue;
const gName = row.groupName.trim();
if (!gName) continue;
if (!knownGroupNames.has(gName)) {
newGroupNamesSet.add(gName);
}
}
const newGroupNames = Array.from(newGroupNamesSet.values());
// 2) Neue Gruppen in einem Rutsch anlegen
if (newGroupNames.length > 0) {
setImportProgress(
`Lege ${newGroupNames.length} neue Gruppe(n) an …`,
);
const resGroups = await fetch('/api/user-groups', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ names: newGroupNames }),
});
if (!resGroups.ok) {
const data = await resGroups.json().catch(() => null);
console.error(
'Fehler beim Bulk-Anlegen der Gruppen:',
resGroups.status,
data,
);
setImportError(
'Fehler beim Anlegen der Gruppen. Import abgebrochen.',
);
return;
}
const data = await resGroups.json();
const createdGroups = (data.groups ?? []) as SimpleGroup[];
for (const g of createdGroups) {
const name = g.name.trim();
if (name) {
groupCache.set(name, g.id);
knownGroupNames.add(name);
}
}
}
// 3) Benutzer-Payload für Bulk-Import bauen
setImportProgress('Bereite Benutzer-Daten für Import vor …');
type UserPayload = {
nwkennung: string;
firstName: string;
lastName: string;
arbeitsname: string;
groupId?: string | null;
};
const usersPayload: UserPayload[] = [];
let skippedByGroup = 0;
for (const row of parsedRows) {
let groupId: string | null = null;
if (row.groupName) {
const gName = row.groupName.trim();
if (gName) {
const id = groupCache.get(gName);
if (!id) {
console.warn(
'Zeile übersprungen (Gruppe nicht vorhanden):',
row.rawLine,
);
skippedByGroup++;
continue;
}
groupId = id;
}
}
usersPayload.push({
nwkennung: row.nwkennung,
firstName: row.firstName,
lastName: row.lastName,
arbeitsname: row.arbeitsname,
groupId: groupId ?? null,
});
}
if (usersPayload.length === 0) {
setImportError(
'Keine gültigen Benutzer-Datensätze nach Gruppenzuordnung. Import abgebrochen.',
);
return;
}
// 4) Bulk-Import der Benutzer
setImportProgress(
`Importiere ${usersPayload.length} Benutzer …`,
);
const resUsers = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ users: usersPayload }),
});
if (!resUsers.ok) {
const data = await resUsers.json().catch(() => null);
console.error(
'Fehler beim Bulk-Import der Benutzer:',
resUsers.status,
data,
);
setImportError(
'Fehler beim Import der Benutzer. Details siehe Konsole.',
);
return;
}
const result = await resUsers.json();
const createdCount = result.createdCount ?? 0;
const skippedServer = result.skippedCount ?? 0;
const totalSkipped =
skippedCount + skippedByGroup + skippedServer;
setImportSummary(
`Import abgeschlossen: ${createdCount} Personen importiert, ${totalSkipped} Zeilen übersprungen.`,
);
setImportProgress('Import abgeschlossen.');
router.refresh();
} catch (err: any) {
console.error('Fehler beim CSV-Import', err);
setImportError(
err instanceof Error
? err.message
: 'Fehler beim CSV-Import.',
);
} finally {
setImporting(false);
if (e.target) {
e.target.value = '';
}
}
}
return (
<div className="flex flex-col items-stretch gap-1">
<Button
type="button"
variant="soft"
tone="indigo"
size="lg"
disabled={importing}
onClick={() => fileInputRef.current?.click()}
>
{importing ? 'Importiere …' : 'Import aus CSV'}
</Button>
<input
ref={fileInputRef}
type="file"
accept=".csv,text/csv"
className="hidden"
onChange={handleImportCsv}
/>
{importProgress && (
<p className="text-[11px] text-gray-500 dark:text-gray-400">
{importProgress}
</p>
)}
{importSummary && (
<p className="text-[11px] text-gray-600 dark:text-gray-300">
{importSummary}
</p>
)}
{importError && (
<p className="text-[11px] text-red-600 dark:text-red-400">
{importError}
</p>
)}
</div>
);
}