316 lines
8.3 KiB
TypeScript
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>
|
|
);
|
|
}
|