feat: enhance CSV upload handling with chunked requests and token support; improve form input limits
This commit is contained in:
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user