"use client"; import { useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { AlertTriangle, ArrowRight, CheckCircle2, FileJson, Loader2, Upload, } from "lucide-react"; import { Modal } from "@/components/ui/Modal"; import { cn } from "@/lib/utils"; type Strategy = "skip" | "rename" | "overwrite"; interface ItemResult { name: string; slug: string; action: "create" | "rename" | "overwrite" | "skip" | "error"; newSlug?: string; notes: number; connection: "matched" | "missing" | "none"; connectionName?: string; message?: string; } interface ImportResponse { dryRun: boolean; strategy: Strategy; summary: { total: number; created: number; renamed: number; overwritten: number; skipped: number; errors: number; missingConnections: number; }; results: ItemResult[]; } const STRATEGIES: Array<{ value: Strategy; label: string; hint: string }> = [ { value: "skip", label: "Ueberspringen", hint: "Vorhandene bleiben unveraendert." }, { value: "rename", label: "Zusaetzlich anlegen", hint: "Doppelte bekommen einen freien Slug." }, { value: "overwrite", label: "Ersetzen", hint: "Stammdaten und Notizen werden ueberschrieben." }, ]; const ACTION_LABEL: Record = { create: { text: "neu", className: "bg-emerald-500/12 text-emerald-200" }, rename: { text: "zusaetzlich", className: "bg-cyan-500/12 text-cyan-200" }, overwrite: { text: "ersetzt", className: "bg-amber-500/12 text-amber-200" }, skip: { text: "uebersprungen", className: "bg-slate-500/12 text-slate-300" }, error: { text: "Fehler", className: "bg-rose-500/12 text-rose-200" }, }; export function ImportDialog({ open, onClose }: { open: boolean; onClose: () => void }) { const router = useRouter(); const fileRef = useRef(null); const [fileName, setFileName] = useState(null); const [payload, setPayload] = useState(null); const [strategy, setStrategy] = useState("skip"); const [preview, setPreview] = useState(null); const [done, setDone] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); function reset() { setFileName(null); setPayload(null); setPreview(null); setDone(null); setError(null); setStrategy("skip"); if (fileRef.current) fileRef.current.value = ""; } function close() { reset(); onClose(); } async function pickFile(event: React.ChangeEvent) { const file = event.target.files?.[0]; if (!file) return; setError(null); setPreview(null); setDone(null); setFileName(file.name); try { const text = await file.text(); const parsed = JSON.parse(text); setPayload(parsed); await run(parsed, strategy, true); } catch { setPayload(null); setError("Die Datei enthaelt kein gueltiges JSON."); } } async function run(data: unknown, chosen: Strategy, dryRun: boolean) { setBusy(true); setError(null); try { const response = await fetch("/api/projects/import", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ data, strategy: chosen, dryRun }), }); const body = await response.json().catch(() => ({})); if (!response.ok) throw new Error(body.error ?? "Import fehlgeschlagen."); if (dryRun) setPreview(body as ImportResponse); else { setDone(body as ImportResponse); router.refresh(); } } catch (caught) { setError(caught instanceof Error ? caught.message : String(caught)); } finally { setBusy(false); } } async function changeStrategy(next: Strategy) { setStrategy(next); if (payload) await run(payload, next, true); } const summary = done?.summary ?? preview?.summary; const results = done?.results ?? preview?.results ?? []; // Wuerde der Import ueberhaupt etwas veraendern? Sonst waere der Knopf ein // Angebot, das ins Leere laeuft. const wouldChange = preview ? preview.summary.created + preview.summary.renamed + preview.summary.overwritten > 0 : false; return (
{/* Datei */}
{/* Strategie */} {payload ? (
Bei gleichem Slug
{STRATEGIES.map((option) => ( ))}
) : null} {error ? (

{error}

) : null} {/* Vorschau oder Ergebnis */} {summary ? (
{done ? "Ergebnis:" : "Vorschau:"} {summary.total} Projekt(e) {summary.created > 0 ? {summary.created} neu : null} {summary.renamed > 0 ? {summary.renamed} zusaetzlich : null} {summary.overwritten > 0 ? ( {summary.overwritten} ersetzt ) : null} {summary.skipped > 0 ? {summary.skipped} uebersprungen : null} {summary.errors > 0 ? {summary.errors} Fehler : null}
{summary.missingConnections > 0 ? (

{summary.missingConnections} Projekt(e) verweisen auf eine Git-Verbindung, die hier nicht existiert. Sie werden ohne Verknuepfung angelegt – lege die Verbindung an und waehle sie im Projekt aus. Access Tokens sind in Exportdateien grundsaetzlich nicht enthalten.

) : null}
{results.map((item) => { const label = ACTION_LABEL[item.action]; return (

{item.name}

{item.slug} {item.newSlug ? ( <> {item.newSlug} ) : null} · {item.notes} Notizen {item.connection === "missing" ? ( · Verbindung „{item.connectionName}“ fehlt ) : null}

{item.message ? (

{item.message}

) : null}
{label.text}
); })}
) : null} {done ? (

Import abgeschlossen.

) : null}
{!done ? ( ) : null}
); } function Chip({ tone, children, }: { tone: "emerald" | "cyan" | "amber" | "slate" | "rose"; children: React.ReactNode; }) { const tones = { emerald: "bg-emerald-500/12 text-emerald-200", cyan: "bg-cyan-500/12 text-cyan-200", amber: "bg-amber-500/12 text-amber-200", slate: "bg-slate-500/12 text-slate-300", rose: "bg-rose-500/12 text-rose-200", }; return {children}; }