geraete/app/(app)/users/UsersCsvImportButton.tsx
2025-11-24 08:59:14 +01:00

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