215 lines
5.5 KiB
TypeScript
215 lines
5.5 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[];
|
|
};
|
|
|
|
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);
|
|
|
|
// Hilfsfunktion: Gruppe sicherstellen (existiert oder neu anlegen)
|
|
async function ensureGroupId(
|
|
name: string,
|
|
cache: Map<string, string>,
|
|
): Promise<string | null> {
|
|
const trimmed = name.trim();
|
|
if (!trimmed) return null;
|
|
|
|
const cached = cache.get(trimmed);
|
|
if (cached) return cached;
|
|
|
|
const res = await fetch('/api/user-groups', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: trimmed }),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => null);
|
|
console.error('Fehler beim Anlegen der Gruppe', res.status, data);
|
|
return null;
|
|
}
|
|
|
|
const data = await res.json();
|
|
const id = data.id as string;
|
|
cache.set(trimmed, id);
|
|
return id;
|
|
}
|
|
|
|
async function handleImportCsv(
|
|
e: React.ChangeEvent<HTMLInputElement>,
|
|
): Promise<void> {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
setImporting(true);
|
|
setImportError(null);
|
|
setImportSummary(null);
|
|
|
|
try {
|
|
const text = await file.text();
|
|
|
|
// Gruppen-Cache (Name -> ID), Start mit bestehenden Gruppen
|
|
const groupCache = new Map<string, string>();
|
|
for (const g of groups) {
|
|
groupCache.set(g.name.trim(), g.id);
|
|
}
|
|
|
|
let createdCount = 0;
|
|
let skippedCount = 0;
|
|
|
|
const lines = text
|
|
.split(/\r?\n/)
|
|
.map((l) => l.trim())
|
|
.filter((l) => l.length > 0);
|
|
|
|
for (let index = 0; index < lines.length; index++) {
|
|
const line = lines[index];
|
|
|
|
// ⬅️ Erste Zeile immer ignorieren (Header)
|
|
if (index === 0) {
|
|
continue;
|
|
}
|
|
|
|
// Format: NwKennung;Nachname;Vorname;Arbeitsname;Gruppe
|
|
const parts = line.split(';');
|
|
if (parts.length < 4) {
|
|
console.warn('Zeile übersprungen (falsches Format):', line);
|
|
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 groupName = (groupRaw ?? '').trim();
|
|
|
|
// NwKennung + Name + Arbeitsname als Pflichtfelder
|
|
if (!nwkennung || !lastName || !firstName || !arbeitsname) {
|
|
console.warn(
|
|
'Zeile übersprungen (Pflichtfelder leer):',
|
|
line,
|
|
);
|
|
skippedCount++;
|
|
continue;
|
|
}
|
|
|
|
let groupId: string | null = null;
|
|
if (groupName) {
|
|
groupId = await ensureGroupId(groupName, groupCache);
|
|
if (!groupId) {
|
|
console.warn(
|
|
'Zeile übersprungen (Gruppe konnte nicht angelegt werden):',
|
|
line,
|
|
);
|
|
skippedCount++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const res = await fetch('/api/users', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
nwkennung, // ⬅ NEU
|
|
arbeitsname,
|
|
firstName,
|
|
lastName,
|
|
groupId,
|
|
}),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => null);
|
|
console.error(
|
|
'Fehler beim Anlegen der Person aus CSV:',
|
|
res.status,
|
|
data,
|
|
'Zeile:',
|
|
line,
|
|
);
|
|
skippedCount++;
|
|
continue;
|
|
}
|
|
|
|
createdCount++;
|
|
}
|
|
|
|
setImportSummary(
|
|
`Import abgeschlossen: ${createdCount} Personen importiert, ${skippedCount} Zeilen übersprungen.`,
|
|
);
|
|
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}
|
|
/>
|
|
|
|
{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>
|
|
);
|
|
}
|