177 lines
5.8 KiB
TypeScript
177 lines
5.8 KiB
TypeScript
"use client";
|
|
|
|
import { Download, FileUp, Play, Upload } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { useState } from "react";
|
|
|
|
import ImportPreviewTable from "@/components/customers/ImportPreviewTable";
|
|
import ImportSummaryCard from "@/components/customers/ImportSummaryCard";
|
|
import { Button, buttonVariants } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Label } from "@/components/ui/label";
|
|
import { api } from "@/lib/api";
|
|
import type {
|
|
CustomerImportCommitResponse,
|
|
CustomerImportMode,
|
|
CustomerImportPreviewResponse,
|
|
} from "@/types/customer";
|
|
|
|
type Props = {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onCommitted: (result: CustomerImportCommitResponse) => void;
|
|
onError: (message: string) => void;
|
|
};
|
|
|
|
function errorMessage(error: unknown) {
|
|
if (typeof error === "object" && error !== null && "response" in error) {
|
|
const response = (error as { response?: { data?: { detail?: string } } }).response;
|
|
return response?.data?.detail ?? "Import konnte nicht ausgeführt werden";
|
|
}
|
|
return "Import konnte nicht ausgeführt werden";
|
|
}
|
|
|
|
function buildFormData(file: File, mode: CustomerImportMode) {
|
|
const formData = new FormData();
|
|
formData.append("mode", mode);
|
|
formData.append("file", file);
|
|
return formData;
|
|
}
|
|
|
|
export default function CustomerImportDialog({ open, onOpenChange, onCommitted, onError }: Props) {
|
|
const [file, setFile] = useState<File | null>(null);
|
|
const [mode, setMode] = useState<CustomerImportMode>("upsert");
|
|
const [preview, setPreview] = useState<CustomerImportPreviewResponse | null>(null);
|
|
const [pending, setPending] = useState(false);
|
|
const [serverError, setServerError] = useState("");
|
|
|
|
async function runPreview() {
|
|
if (!file) {
|
|
setServerError("Bitte CSV-Datei auswählen");
|
|
return;
|
|
}
|
|
|
|
setPending(true);
|
|
setServerError("");
|
|
try {
|
|
const response = await api.post<CustomerImportPreviewResponse>(
|
|
"/customers/import/preview",
|
|
buildFormData(file, mode),
|
|
);
|
|
setPreview(response.data);
|
|
} catch (error) {
|
|
const message = errorMessage(error);
|
|
setServerError(message);
|
|
onError(message);
|
|
} finally {
|
|
setPending(false);
|
|
}
|
|
}
|
|
|
|
async function commitImport() {
|
|
if (!file || !preview || preview.summary.valid_rows === 0) {
|
|
return;
|
|
}
|
|
|
|
setPending(true);
|
|
setServerError("");
|
|
try {
|
|
const response = await api.post<CustomerImportCommitResponse>(
|
|
"/customers/import/commit",
|
|
buildFormData(file, mode),
|
|
);
|
|
onCommitted(response.data);
|
|
setPreview(null);
|
|
setFile(null);
|
|
onOpenChange(false);
|
|
} catch (error) {
|
|
const message = errorMessage(error);
|
|
setServerError(message);
|
|
onError(message);
|
|
} finally {
|
|
setPending(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-5xl">
|
|
<DialogHeader>
|
|
<DialogTitle>Kunden importieren</DialogTitle>
|
|
<DialogDescription>CSV-Datei prüfen und anschließend gültige Zeilen übernehmen.</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="grid gap-4">
|
|
<div className="grid gap-4 lg:grid-cols-[1fr_14rem_auto] lg:items-end">
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="customer-import-file">CSV-Datei</Label>
|
|
<input
|
|
id="customer-import-file"
|
|
type="file"
|
|
accept=".csv,text/csv"
|
|
className="h-10 rounded-lg border border-input bg-white px-3 py-2 text-sm"
|
|
onChange={(event) => {
|
|
setFile(event.target.files?.[0] ?? null);
|
|
setPreview(null);
|
|
setServerError("");
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="customer-import-mode">Importmodus</Label>
|
|
<select
|
|
id="customer-import-mode"
|
|
value={mode}
|
|
className="h-10 rounded-lg border border-input bg-white px-3 text-sm"
|
|
onChange={(event) => {
|
|
setMode(event.target.value as CustomerImportMode);
|
|
setPreview(null);
|
|
}}
|
|
>
|
|
<option value="upsert">Upsert</option>
|
|
<option value="create_only">Nur erstellen</option>
|
|
<option value="update_existing">Bestehende aktualisieren</option>
|
|
</select>
|
|
</div>
|
|
|
|
<Link href="/api/customers/import/template" className={buttonVariants({ variant: "outline", size: "default" })}>
|
|
<Download size={16} />
|
|
Vorlage
|
|
</Link>
|
|
</div>
|
|
|
|
{serverError && <p className="rounded-lg bg-red-50 p-3 text-sm text-red-700">{serverError}</p>}
|
|
|
|
{preview && (
|
|
<div className="grid gap-4">
|
|
<ImportSummaryCard summary={preview.summary} />
|
|
<ImportPreviewTable rows={preview.rows} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={pending}>
|
|
Schließen
|
|
</Button>
|
|
<Button type="button" variant="outline" onClick={runPreview} disabled={pending || !file}>
|
|
<Play size={16} />
|
|
Preview starten
|
|
</Button>
|
|
<Button type="button" onClick={commitImport} disabled={pending || !preview || preview.summary.valid_rows === 0}>
|
|
{pending ? <FileUp size={16} /> : <Upload size={16} />}
|
|
Import übernehmen
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|