feat: enhance CSV upload handling with chunked requests and token support; improve form input limits

This commit is contained in:
2025-08-14 10:38:35 +02:00
parent bb63388986
commit 214a3cb3c8
6 changed files with 961 additions and 61 deletions

View File

@@ -1,5 +1,5 @@
import React from "react";
import { Sheet, WholeWord } from "lucide-react";
import { Sheet, WholeWord, Search } from "lucide-react";
import { useState } from "react";
import ImportGUI from "./ImportGUI";
import { removeSelection } from "../utils/tableActions";
@@ -7,10 +7,17 @@ import { removeSelection } from "../utils/tableActions";
type SubHeaderAdminProps = {
setFiles: (files: File[]) => void;
files?: File[];
search: string;
setSearch: (value: string) => void;
};
// Sub navigation bar for admin views: provides import + clear selection actions
const SubHeaderAdmin: React.FC<SubHeaderAdminProps> = ({ setFiles, files }) => {
const SubHeaderAdmin: React.FC<SubHeaderAdminProps> = ({
setFiles,
files,
search,
setSearch,
}) => {
const [showImport, setShowImport] = useState(false);
return (
@@ -30,6 +37,17 @@ const SubHeaderAdmin: React.FC<SubHeaderAdminProps> = ({ setFiles, files }) => {
</p>
</div>
<div className="flex items-center gap-2">
{/* Search */}
<div className="relative hidden sm:block">
<Search className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Suchen… (Losnummer, Name, Adresse, PLZ, Email)"
className="w-72 rounded-md border border-gray-300 bg-white pl-9 pr-3 py-2 text-sm text-gray-900 placeholder-gray-400 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<button
onClick={() => setShowImport(true)}
type="button"

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import Cookies from "js-cookie";
import { useQuery } from "@tanstack/react-query";
import { getTableData, readCachedTableData } from "../utils/userHandler";
@@ -19,6 +19,7 @@ interface DataPackage {
const Table: React.FC = () => {
const [rows, setRows] = useState<DataPackage[]>([]); // holds normalized cache view
const [files, setFiles] = useState<File[]>([]);
const [search, setSearch] = useState("");
// Einheitliche Input-Styles (nur Tailwind)
const inputClasses =
@@ -81,9 +82,33 @@ const Table: React.FC = () => {
);
};
// Filter rows by search query (case-insensitive)
const filteredRows = useMemo(() => {
const q = search.trim().toLowerCase();
if (!q) return rows;
return rows.filter((r) => {
const values = [
r.losnummer,
r.vorname ?? "",
r.nachname ?? "",
r.adresse ?? "",
r.plz ?? "",
r.email ?? "",
]
.join(" ")
.toLowerCase();
return values.includes(q);
});
}, [rows, search]);
return (
<>
<SubHeaderAdmin setFiles={setFiles} files={files} />
<SubHeaderAdmin
setFiles={setFiles}
files={files}
search={search}
setSearch={setSearch}
/>
<div className="w-full">
{/*
<div className="mb-4 flex items-center gap-3">
@@ -156,7 +181,7 @@ const Table: React.FC = () => {
</tr>
</thead>
<tbody className="divide-y divide-gray-100 bg-white">
{rows.length === 0 && !tableQuery.isLoading && (
{filteredRows.length === 0 && !tableQuery.isLoading && (
<tr>
<td
colSpan={8}
@@ -166,7 +191,7 @@ const Table: React.FC = () => {
</td>
</tr>
)}
{rows.map((row, idx) => (
{filteredRows.map((row, idx) => (
<tr
key={row.losnummer ?? idx}
className="hover:bg-gray-50 transition-colors"

View File

@@ -46,21 +46,31 @@ export async function postCSV(file: File): Promise<boolean> {
reader.readAsText(file, "utf-8");
});
const res = await fetch("http://localhost:8002/create-entry", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
losnummer: await readerLines,
}),
});
const lines = await readerLines;
if (res.ok) {
myToast("CSV Datei erfolgreich importiert!", "success");
queryClient.invalidateQueries({ queryKey: ["table-data"] });
return true;
// Send token if available
const token = (await import("js-cookie")).default.get("token");
// Chunk uploads to avoid huge single payloads
const chunkSize = 2000; // ~2k per request => 25 requests for 50k
for (let i = 0; i < lines.length; i += chunkSize) {
const chunk = lines.slice(i, i + chunkSize);
const res = await fetch("http://localhost:8002/create-entry", {
method: "POST",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ losnummer: chunk }),
});
if (!res.ok) {
myToast(`Fehler beim Importieren (Batch ${i / chunkSize + 1}).`, "error");
return false;
}
}
myToast("Fehler beim Importieren der CSV Datei.", "error");
return false;
myToast("CSV Datei erfolgreich importiert!", "success");
queryClient.invalidateQueries({ queryKey: ["table-data"] });
return true;
}