diff --git a/backend/server.js b/backend/server.js index 9b7e314..9a88522 100644 --- a/backend/server.js +++ b/backend/server.js @@ -40,8 +40,8 @@ app.get("/table-data", authenticate, async (req, res) => { }); app.post("/create-entry", async (req, res) => { - const result = await createEntry(req.body); - if (result.success) { + const result = await createEntry(req.body.losnummer); + if (result) { res.status(201).json({ success: true }); } else { res.status(400).json({ success: false }); diff --git a/backend/services/database.js b/backend/services/database.js index 7deb016..6b91a1f 100644 --- a/backend/services/database.js +++ b/backend/services/database.js @@ -51,18 +51,23 @@ export async function getTableData() { return { success: false }; } -export async function createEntryCSV(file) { - // Implement CSV creation logic here -} +// Create data from array not working !!! export async function createEntry(data) { - const [result] = await pool.query("INSERT INTO lose (losnummer) VALUES (?)", [ - data.losnummer, - ]); + let { status } = { status: true }; + for (const item of data) { + const [result] = await pool.query( + "INSERT INTO lose (losnummer) VALUES (?)", + [item] + ); - if (result.affectedRows > 0) { - return { success: true }; - } else { - return { success: false }; + if (result.affectedRows > 0) { + status = true; + } else { + status = false; + return status; + } } + + return status; } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0d9d244..415e629 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.1.11", + "@tanstack/react-query": "^5.85.0", "js-cookie": "^3.0.5", "lucide-react": "^0.539.0", "primeicons": "^7.0.0", @@ -17,6 +18,7 @@ "react-dom": "^19.1.1", "react-router-dom": "^7.8.0", "react-toastify": "^11.0.5", + "split-lines": "^3.0.0", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11", "tailwindcss-animate": "^1.0.7", @@ -1823,6 +1825,32 @@ "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": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4149,6 +4177,18 @@ "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": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9cf1365..33652b8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.1.11", + "@tanstack/react-query": "^5.85.0", "js-cookie": "^3.0.5", "lucide-react": "^0.539.0", "primeicons": "^7.0.0", @@ -19,6 +20,7 @@ "react-dom": "^19.1.1", "react-router-dom": "^7.8.0", "react-toastify": "^11.0.5", + "split-lines": "^3.0.0", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11", "tailwindcss-animate": "^1.0.7", diff --git a/frontend/public/Beispiel.csv b/frontend/public/Beispiel.csv index 6f6fa2d..633202a 100644 --- a/frontend/public/Beispiel.csv +++ b/frontend/public/Beispiel.csv @@ -1,40 +1,3 @@ -a7B9-kD3f-92Lm-Q1z -Z1x9-3kLp-a8D2-mN7 -pQ4d-T7v2-bK9X-f5G -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 \ No newline at end of file +asdgerwr +asdgfwadf +asdfqasdf \ No newline at end of file diff --git a/frontend/src/components/Admin.tsx b/frontend/src/components/Admin.tsx index 2b9b5ee..c476288 100644 --- a/frontend/src/components/Admin.tsx +++ b/frontend/src/components/Admin.tsx @@ -10,6 +10,8 @@ const Admin: React.FC = () => { () => Cookies.get("token") ?? null ); + + return ( <> void; accept?: string; multiple?: boolean; + setFiles: (files: File[]) => void; + files?: File[]; }; const CircleUpload: React.FC = ({ onFiles, accept = "", multiple = true, + setFiles, + files = [], }) => { const [isDragging, setIsDragging] = useState(false); - const [files, setFiles] = useState([]); const inputRef = useRef(null); const processFiles = useCallback( diff --git a/frontend/src/components/HeaderAdmin.tsx b/frontend/src/components/HeaderAdmin.tsx index 2bf4c91..30b1982 100644 --- a/frontend/src/components/HeaderAdmin.tsx +++ b/frontend/src/components/HeaderAdmin.tsx @@ -23,6 +23,7 @@ const HeaderAdmin: React.FC = ({
+

Admin Panel

diff --git a/frontend/src/components/ImportGUI.tsx b/frontend/src/components/ImportGUI.tsx index 5f76bab..308bfd0 100644 --- a/frontend/src/components/ImportGUI.tsx +++ b/frontend/src/components/ImportGUI.tsx @@ -1,12 +1,15 @@ -import React from "react"; +import React, { useState } from "react"; import CircleUpload from "./CircleUpload"; import { CircleX } from "lucide-react"; +import { postCSV } from "../utils/fileHandler"; type ImportGUIProps = { onClose: () => void; + setFiles: (files: File[]) => void; + files?: File[]; }; -const ImportGUI: React.FC = ({ onClose }) => { +const ImportGUI: React.FC = ({ onClose, setFiles, files }) => { return (
{/* Backdrop */} @@ -50,7 +53,7 @@ const ImportGUI: React.FC = ({ onClose }) => {
- +
{/* Placeholder for optional file summary / mapping UI */} @@ -70,7 +73,12 @@ const ImportGUI: React.FC = ({ onClose }) => { diff --git a/frontend/src/components/SubHeaderAdmin.tsx b/frontend/src/components/SubHeaderAdmin.tsx index 736e410..5e06ae6 100644 --- a/frontend/src/components/SubHeaderAdmin.tsx +++ b/frontend/src/components/SubHeaderAdmin.tsx @@ -3,8 +3,13 @@ import { Sheet, WholeWord } from "lucide-react"; import { useState } from "react"; import ImportGUI from "./ImportGUI"; +type SubHeaderAdminProps = { + setFiles: (files: File[]) => void; + files?: File[]; +}; + // Sub navigation bar for admin views: provides import + clear selection actions -const SubHeaderAdmin: React.FC = () => { +const SubHeaderAdmin: React.FC = ({ setFiles, files }) => { const [showImport, setShowImport] = useState(false); return ( @@ -48,7 +53,13 @@ const SubHeaderAdmin: React.FC = () => {
- {showImport && setShowImport(false)} />} + {showImport && ( + setShowImport(false)} + setFiles={setFiles} + files={files} + /> + )} ); }; diff --git a/frontend/src/components/Table.tsx b/frontend/src/components/Table.tsx index 987c8f6..41f73f0 100644 --- a/frontend/src/components/Table.tsx +++ b/frontend/src/components/Table.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from "react"; import Cookies from "js-cookie"; +import { useQuery } from "@tanstack/react-query"; import { getTableData, readCachedTableData } from "../utils/userHandler"; import { EllipsisVertical } from "lucide-react"; import SubHeaderAdmin from "./SubHeaderAdmin"; @@ -15,9 +16,9 @@ interface DataPackage { } const Table: React.FC = () => { - const [rows, setRows] = useState([]); - const [loading, setLoading] = useState(false); + const [rows, setRows] = useState([]); // holds normalized cache view const [error, setError] = useState(null); + const [files, setFiles] = useState([]); // Hilfsfunktion zum Einlesen & Normalisieren der LocalStorage-Daten const loadFromCache = () => { @@ -27,32 +28,35 @@ const Table: React.FC = () => { return; } // Server könnte entweder ein Objekt oder ein Array liefern - const normalized: DataPackage[] = Array.isArray(cached) - ? cached - : [cached]; + const normalized: DataPackage[] = Array.isArray(cached) ? cached : [cached]; setRows(normalized); }; + const token = Cookies.get("token") || ""; + + const tableQuery = useQuery({ + queryKey: ["table-data", token], + enabled: !!token, + queryFn: async () => { + 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(() => { - // Initial lokale Daten laden (falls schon vorhanden) loadFromCache(); + }, [tableQuery.data]); - // Frische Daten vom Backend holen - const token = Cookies.get("token") || ""; - if (!token) return; // Kein Token => nur Cache anzeigen - - setLoading(true); - getTableData(token) - .then((data) => { - if (data === null) { - setError("Fehler beim Laden der Daten."); - } else { - setError(null); - } - loadFromCache(); - }) - .finally(() => setLoading(false)); - }, []); + useEffect(() => { + if (tableQuery.isError) { + setError((tableQuery.error as Error).message); + } else { + setError(null); + } + }, [tableQuery.isError, tableQuery.error]); // Reagieren auf LocalStorage-Änderungen (z.B. in anderen Tabs) useEffect(() => { @@ -70,15 +74,22 @@ const Table: React.FC = () => { return ( <> - +
- {loading && ( + {(tableQuery.isLoading || tableQuery.isFetching) && ( Laden... )} {error && {error}} +
@@ -133,7 +144,7 @@ const Table: React.FC = () => { - {rows.length === 0 && !loading && ( + {rows.length === 0 && !tableQuery.isLoading && (
- - , -) + + + + +); diff --git a/frontend/src/queryClient.ts b/frontend/src/queryClient.ts new file mode 100644 index 0000000..6bbd7d3 --- /dev/null +++ b/frontend/src/queryClient.ts @@ -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, + }, + }, +}); diff --git a/frontend/src/utils/fileHandler.ts b/frontend/src/utils/fileHandler.ts index ae41ee8..0824228 100644 --- a/frontend/src/utils/fileHandler.ts +++ b/frontend/src/utils/fileHandler.ts @@ -1,3 +1,6 @@ +import { myToast } from "./toastify"; +import { queryClient } from "../queryClient"; + export function downloadHandler(filename: string, content: string) { const blob = new Blob([content], { type: "text/plain" }); const url = URL.createObjectURL(blob); @@ -9,3 +12,55 @@ export function downloadHandler(filename: string, content: string) { document.body.removeChild(a); URL.revokeObjectURL(url); } + +export async function postCSV(file: File): Promise { + // Reads a CSV file client-side and returns a Promise 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((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; +}