feat: enhance CSV import functionality, integrate react-query for data fetching, and refactor admin components
This commit is contained in:
@@ -40,8 +40,8 @@ app.get("/table-data", authenticate, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post("/create-entry", async (req, res) => {
|
app.post("/create-entry", async (req, res) => {
|
||||||
const result = await createEntry(req.body);
|
const result = await createEntry(req.body.losnummer);
|
||||||
if (result.success) {
|
if (result) {
|
||||||
res.status(201).json({ success: true });
|
res.status(201).json({ success: true });
|
||||||
} else {
|
} else {
|
||||||
res.status(400).json({ success: false });
|
res.status(400).json({ success: false });
|
||||||
|
@@ -51,18 +51,23 @@ export async function getTableData() {
|
|||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createEntryCSV(file) {
|
// Create data from array not working !!!
|
||||||
// Implement CSV creation logic here
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createEntry(data) {
|
export async function createEntry(data) {
|
||||||
const [result] = await pool.query("INSERT INTO lose (losnummer) VALUES (?)", [
|
let { status } = { status: true };
|
||||||
data.losnummer,
|
for (const item of data) {
|
||||||
]);
|
const [result] = await pool.query(
|
||||||
|
"INSERT INTO lose (losnummer) VALUES (?)",
|
||||||
|
[item]
|
||||||
|
);
|
||||||
|
|
||||||
if (result.affectedRows > 0) {
|
if (result.affectedRows > 0) {
|
||||||
return { success: true };
|
status = true;
|
||||||
} else {
|
} else {
|
||||||
return { success: false };
|
status = false;
|
||||||
|
return status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
40
frontend/package-lock.json
generated
40
frontend/package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@tanstack/react-query": "^5.85.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-router-dom": "^7.8.0",
|
"react-router-dom": "^7.8.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
|
"split-lines": "^3.0.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
@@ -1823,6 +1825,32 @@
|
|||||||
"vite": "^5.2.0 || ^6 || ^7"
|
"vite": "^5.2.0 || ^6 || ^7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "5.83.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.1.tgz",
|
||||||
|
"integrity": "sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "5.85.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.0.tgz",
|
||||||
|
"integrity": "sha512-t1HMfToVMGfwEJRya6GG7gbK0luZJd+9IySFNePL1BforU1F3LqQ3tBC2Rpvr88bOrlU6PXyMLgJD0Yzn4ztUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.83.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -4149,6 +4177,18 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/split-lines": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/split-lines/-/split-lines-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-d0TpRBL/VfKDXsk8JxPF7zgF5pCUDdBMSlEL36xBgVeaX448t+yGXcJaikUyzkoKOJ0l6KpMfygzJU9naIuivw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-json-comments": {
|
"node_modules/strip-json-comments": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||||
|
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@tanstack/react-query": "^5.85.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-router-dom": "^7.8.0",
|
"react-router-dom": "^7.8.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
|
"split-lines": "^3.0.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
@@ -1,40 +1,3 @@
|
|||||||
a7B9-kD3f-92Lm-Q1z
|
asdgerwr
|
||||||
Z1x9-3kLp-a8D2-mN7
|
asdgfwadf
|
||||||
pQ4d-T7v2-bK9X-f5G
|
asdfqasdf
|
||||||
m3N8-wQ2z-V6bP-tR4
|
|
||||||
gH7k-Lp2Q-xD5f-sE9
|
|
||||||
R8tV-c2Bq-J7mZ-yK1
|
|
||||||
nK5w-D9sP-T3vL-rU6
|
|
||||||
Y4dE-q7Wm-hP2k-Js9
|
|
||||||
c2Fh-X8nR-L5vq-tG7
|
|
||||||
bM6x-pQ3Z-s7Kf-U1h
|
|
||||||
V9cQ-h3Tn-2KxF-z6P
|
|
||||||
k5Lr-M8pQ-w2Tg-Y9d
|
|
||||||
H2sN-v7Qb-c4XK-p8J
|
|
||||||
q8Wk-R5nD-1zLp-G6y
|
|
||||||
T7bH-y2Kq-M9vF-c3R
|
|
||||||
j4Dq-P6sL-t8XG-r1V
|
|
||||||
F6kP-q1Xv-3TzB-n7H
|
|
||||||
x9Nf-L2dQ-5vHk-S8p
|
|
||||||
G3rH-t9Kc-Q4sV-m1Z
|
|
||||||
s1Vx-B7mQ-8kDt-P5g
|
|
||||||
P5kZ-w4Sg-2HnV-y9M
|
|
||||||
L8cJ-q3Vt-7pXh-D2f
|
|
||||||
d7Xv-T1kL-6mQp-R8s
|
|
||||||
K2nP-z6Hq-9vBd-J3t
|
|
||||||
u9Hc-R4sV-5kLm-Q2x
|
|
||||||
N3vG-y8Kp-1TqD-s6F
|
|
||||||
h5Qm-C9tB-4vXn-Z7k
|
|
||||||
W6zF-p2Jc-8KqH-d1L
|
|
||||||
r2Kp-G5sN-7tVh-M4q
|
|
||||||
J4mT-x9Hc-2QvP-k8S
|
|
||||||
f8Pq-D3kV-6zLn-R1y
|
|
||||||
Q7zD-h1Sg-9KpM-t4V
|
|
||||||
z6Kx-P5vH-1cQn-Y8d
|
|
||||||
C9tB-u4Hq-7mKs-p2J
|
|
||||||
y1Vd-G7nP-3kLq-S6h
|
|
||||||
M2cL-t5Xh-9vQp-r8K
|
|
||||||
b4Sg-N6kP-2HqV-J9t
|
|
||||||
X5nV-q8Tz-1pKd-L3h
|
|
||||||
e3Kq-R7vH-5nXc-P2m
|
|
||||||
H1pM-w9Dk-4sQv-T7g
|
|
|
@@ -10,6 +10,8 @@ const Admin: React.FC = () => {
|
|||||||
() => Cookies.get("token") ?? null
|
() => Cookies.get("token") ?? null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeaderAdmin
|
<HeaderAdmin
|
||||||
|
@@ -4,15 +4,18 @@ type CircleUploadProps = {
|
|||||||
onFiles?: (files: File[]) => void;
|
onFiles?: (files: File[]) => void;
|
||||||
accept?: string;
|
accept?: string;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
|
setFiles: (files: File[]) => void;
|
||||||
|
files?: File[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const CircleUpload: React.FC<CircleUploadProps> = ({
|
const CircleUpload: React.FC<CircleUploadProps> = ({
|
||||||
onFiles,
|
onFiles,
|
||||||
accept = "",
|
accept = "",
|
||||||
multiple = true,
|
multiple = true,
|
||||||
|
setFiles,
|
||||||
|
files = [],
|
||||||
}) => {
|
}) => {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
const processFiles = useCallback(
|
const processFiles = useCallback(
|
||||||
|
@@ -23,6 +23,7 @@ const HeaderAdmin: React.FC<HeaderAdminProps> = ({
|
|||||||
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-3 md:px-6">
|
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-3 md:px-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Ticket className="h-8 w-8 text-black" strokeWidth={2.6} />
|
<Ticket className="h-8 w-8 text-black" strokeWidth={2.6} />
|
||||||
|
|
||||||
<h1 className="text-2xl font-black tracking-tight text-neutral-900 md:text-3xl">
|
<h1 className="text-2xl font-black tracking-tight text-neutral-900 md:text-3xl">
|
||||||
Admin Panel
|
Admin Panel
|
||||||
</h1>
|
</h1>
|
||||||
|
@@ -1,12 +1,15 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import CircleUpload from "./CircleUpload";
|
import CircleUpload from "./CircleUpload";
|
||||||
import { CircleX } from "lucide-react";
|
import { CircleX } from "lucide-react";
|
||||||
|
import { postCSV } from "../utils/fileHandler";
|
||||||
|
|
||||||
type ImportGUIProps = {
|
type ImportGUIProps = {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
setFiles: (files: File[]) => void;
|
||||||
|
files?: File[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const ImportGUI: React.FC<ImportGUIProps> = ({ onClose }) => {
|
const ImportGUI: React.FC<ImportGUIProps> = ({ onClose, setFiles, files }) => {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-start justify-center pt-24">
|
<div className="fixed inset-0 z-50 flex items-start justify-center pt-24">
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
@@ -50,7 +53,7 @@ const ImportGUI: React.FC<ImportGUIProps> = ({ onClose }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<CircleUpload accept=".csv" />
|
<CircleUpload accept=".csv" setFiles={setFiles} files={files} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Placeholder for optional file summary / mapping UI */}
|
{/* Placeholder for optional file summary / mapping UI */}
|
||||||
@@ -70,7 +73,12 @@ const ImportGUI: React.FC<ImportGUIProps> = ({ onClose }) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex justify-center rounded-xl bg-blue-600 px-6 py-3 text-sm font-bold text-white shadow transition hover:bg-blue-700 active:bg-blue-800 disabled:opacity-60 disabled:cursor-not-allowed"
|
className="inline-flex justify-center rounded-xl bg-blue-600 px-6 py-3 text-sm font-bold text-white shadow transition hover:bg-blue-700 active:bg-blue-800 disabled:opacity-60 disabled:cursor-not-allowed"
|
||||||
disabled
|
disabled={!files || files.length === 0}
|
||||||
|
onClick={() => {
|
||||||
|
if (files && files.length) {
|
||||||
|
postCSV(files[0]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Importieren
|
Importieren
|
||||||
</button>
|
</button>
|
||||||
|
@@ -3,8 +3,13 @@ import { Sheet, WholeWord } from "lucide-react";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import ImportGUI from "./ImportGUI";
|
import ImportGUI from "./ImportGUI";
|
||||||
|
|
||||||
|
type SubHeaderAdminProps = {
|
||||||
|
setFiles: (files: File[]) => void;
|
||||||
|
files?: File[];
|
||||||
|
};
|
||||||
|
|
||||||
// Sub navigation bar for admin views: provides import + clear selection actions
|
// Sub navigation bar for admin views: provides import + clear selection actions
|
||||||
const SubHeaderAdmin: React.FC = () => {
|
const SubHeaderAdmin: React.FC<SubHeaderAdminProps> = ({ setFiles, files }) => {
|
||||||
const [showImport, setShowImport] = useState(false);
|
const [showImport, setShowImport] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -48,7 +53,13 @@ const SubHeaderAdmin: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{showImport && <ImportGUI onClose={() => setShowImport(false)} />}
|
{showImport && (
|
||||||
|
<ImportGUI
|
||||||
|
onClose={() => setShowImport(false)}
|
||||||
|
setFiles={setFiles}
|
||||||
|
files={files}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { getTableData, readCachedTableData } from "../utils/userHandler";
|
import { getTableData, readCachedTableData } from "../utils/userHandler";
|
||||||
import { EllipsisVertical } from "lucide-react";
|
import { EllipsisVertical } from "lucide-react";
|
||||||
import SubHeaderAdmin from "./SubHeaderAdmin";
|
import SubHeaderAdmin from "./SubHeaderAdmin";
|
||||||
@@ -15,9 +16,9 @@ interface DataPackage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Table: React.FC = () => {
|
const Table: React.FC = () => {
|
||||||
const [rows, setRows] = useState<DataPackage[]>([]);
|
const [rows, setRows] = useState<DataPackage[]>([]); // holds normalized cache view
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
|
|
||||||
// Hilfsfunktion zum Einlesen & Normalisieren der LocalStorage-Daten
|
// Hilfsfunktion zum Einlesen & Normalisieren der LocalStorage-Daten
|
||||||
const loadFromCache = () => {
|
const loadFromCache = () => {
|
||||||
@@ -27,32 +28,35 @@ const Table: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Server könnte entweder ein Objekt oder ein Array liefern
|
// Server könnte entweder ein Objekt oder ein Array liefern
|
||||||
const normalized: DataPackage[] = Array.isArray(cached)
|
const normalized: DataPackage[] = Array.isArray(cached) ? cached : [cached];
|
||||||
? cached
|
|
||||||
: [cached];
|
|
||||||
setRows(normalized);
|
setRows(normalized);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Initial lokale Daten laden (falls schon vorhanden)
|
|
||||||
loadFromCache();
|
|
||||||
|
|
||||||
// Frische Daten vom Backend holen
|
|
||||||
const token = Cookies.get("token") || "";
|
const token = Cookies.get("token") || "";
|
||||||
if (!token) return; // Kein Token => nur Cache anzeigen
|
|
||||||
|
|
||||||
setLoading(true);
|
const tableQuery = useQuery({
|
||||||
getTableData(token)
|
queryKey: ["table-data", token],
|
||||||
.then((data) => {
|
enabled: !!token,
|
||||||
if (data === null) {
|
queryFn: async () => {
|
||||||
setError("Fehler beim Laden der Daten.");
|
const data = await getTableData(token);
|
||||||
|
if (data === null) throw new Error("Fehler beim Laden der Daten.");
|
||||||
|
return data as unknown as DataPackage[] | DataPackage; // server may send single object
|
||||||
|
},
|
||||||
|
refetchOnMount: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync normalized cached data into local state whenever query succeeds or cache changes
|
||||||
|
useEffect(() => {
|
||||||
|
loadFromCache();
|
||||||
|
}, [tableQuery.data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tableQuery.isError) {
|
||||||
|
setError((tableQuery.error as Error).message);
|
||||||
} else {
|
} else {
|
||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
loadFromCache();
|
}, [tableQuery.isError, tableQuery.error]);
|
||||||
})
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Reagieren auf LocalStorage-Änderungen (z.B. in anderen Tabs)
|
// Reagieren auf LocalStorage-Änderungen (z.B. in anderen Tabs)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -70,15 +74,22 @@ const Table: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SubHeaderAdmin />
|
<SubHeaderAdmin setFiles={setFiles} files={files} />
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="mb-4 flex items-center gap-3">
|
<div className="mb-4 flex items-center gap-3">
|
||||||
{loading && (
|
{(tableQuery.isLoading || tableQuery.isFetching) && (
|
||||||
<span className="text-xs text-blue-600 animate-pulse">
|
<span className="text-xs text-blue-600 animate-pulse">
|
||||||
Laden...
|
Laden...
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{error && <span className="text-xs text-red-600">{error}</span>}
|
{error && <span className="text-xs text-red-600">{error}</span>}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => tableQuery.refetch()}
|
||||||
|
className="text-xs rounded border px-2 py-1 text-gray-600 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto rounded-lg shadow ring-1 ring-black/5">
|
<div className="overflow-x-auto rounded-lg shadow ring-1 ring-black/5">
|
||||||
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
||||||
@@ -133,7 +144,7 @@ const Table: React.FC = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100 bg-white">
|
<tbody className="divide-y divide-gray-100 bg-white">
|
||||||
{rows.length === 0 && !loading && (
|
{rows.length === 0 && !tableQuery.isLoading && (
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
colSpan={6}
|
colSpan={6}
|
||||||
|
@@ -1,10 +1,14 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from "react-dom/client";
|
||||||
import './index.css'
|
import "./index.css";
|
||||||
import App from './App.tsx'
|
import App from "./App.tsx";
|
||||||
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { queryClient } from "./queryClient";
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>,
|
</QueryClientProvider>
|
||||||
)
|
</StrictMode>
|
||||||
|
);
|
||||||
|
11
frontend/src/queryClient.ts
Normal file
11
frontend/src/queryClient.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
// Central QueryClient instance so utilities (e.g. file upload) can invalidate queries.
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
@@ -1,3 +1,6 @@
|
|||||||
|
import { myToast } from "./toastify";
|
||||||
|
import { queryClient } from "../queryClient";
|
||||||
|
|
||||||
export function downloadHandler(filename: string, content: string) {
|
export function downloadHandler(filename: string, content: string) {
|
||||||
const blob = new Blob([content], { type: "text/plain" });
|
const blob = new Blob([content], { type: "text/plain" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -9,3 +12,55 @@ export function downloadHandler(filename: string, content: string) {
|
|||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function postCSV(file: File): Promise<boolean> {
|
||||||
|
// Reads a CSV file client-side and returns a Promise<string[]> where each
|
||||||
|
// entry is one non-empty trimmed line from the file. Uses the lightweight
|
||||||
|
// split-lines library to normalize different newline styles.
|
||||||
|
const readerLines = new Promise<string[]>((resolve, reject) => {
|
||||||
|
if (!file) {
|
||||||
|
resolve([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onerror = () =>
|
||||||
|
reject(reader.error ?? new Error("Failed to read file"));
|
||||||
|
reader.onload = () => {
|
||||||
|
try {
|
||||||
|
// Lazy import to avoid bundling if unused elsewhere
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||||
|
import("split-lines")
|
||||||
|
.then(({ default: splitLines }) => {
|
||||||
|
const text = String(reader.result ?? "");
|
||||||
|
// split-lines preserves empty lines if keepNewlines = false; we filter afterwards
|
||||||
|
const lines = splitLines(text, { preserveNewlines: false })
|
||||||
|
.map((l) => l.trim())
|
||||||
|
.filter((l) => l.length > 0);
|
||||||
|
resolve(lines);
|
||||||
|
})
|
||||||
|
.catch((err) => reject(err));
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
myToast("CSV Datei erfolgreich importiert!", "success");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["table-data"] });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
myToast("Fehler beim Importieren der CSV Datei.", "error");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user