diff --git a/backend/package-lock.json b/backend/package-lock.json index c466d41..4453be8 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,6 +13,7 @@ "dotenv": "^17.2.1", "ejs": "^3.1.10", "express": "^5.1.0", + "jose": "^6.0.12", "mysql2": "^3.14.3" } }, @@ -563,6 +564,15 @@ "node": ">=10" } }, + "node_modules/jose": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.12.tgz", + "integrity": "sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", diff --git a/backend/package.json b/backend/package.json index e1d502e..3614208 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,6 +15,7 @@ "dotenv": "^17.2.1", "ejs": "^3.1.10", "express": "^5.1.0", + "jose": "^6.0.12", "mysql2": "^3.14.3" } } diff --git a/backend/server.js b/backend/server.js index 17f2b59..28e85ed 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,7 +1,8 @@ import express from "express"; import cors from "cors"; import env from "dotenv"; -import { query } from "./services/database.js"; +import { query, loginAdmin, getTableData } from "./services/database.js"; +import { generateToken, authenticate } from "./services/tokenService.js"; env.config(); const app = express(); const port = 8002; @@ -24,6 +25,26 @@ app.post("/lose", async (req, res) => { } }); +app.get("/table-data", authenticate, async (req, res) => { + const result = await getTableData(); + if (result.success) { + res.status(200).json(result.data); + } else { + res.status(500); + } +}); + +app.post("/login", async (req, res) => { + const { username, password } = req.body; + const result = await loginAdmin(username, password); + if (result.success) { + const token = await generateToken({ username }); + res.status(200).json({ success: true, token }); + } else { + res.status(401).json({ success: false }); + } +}); + app.listen(port, () => { console.log(`Server is running on port: ${port}`); }); diff --git a/backend/services/database.js b/backend/services/database.js index 00385ac..a726f50 100644 --- a/backend/services/database.js +++ b/backend/services/database.js @@ -2,7 +2,7 @@ import mysql from "mysql2"; import dotenv from "dotenv"; dotenv.config(); -// Create a MySQL connection pool using environment variables for configuration +// Ein einzelner Pool reicht; der zweite Pool benutzte fälschlich DB_TABLE als Datenbank const pool = mysql .createPool({ host: process.env.DB_HOST, @@ -32,3 +32,21 @@ export async function query(params) { return { success: false }; } } + +export async function loginAdmin(username, password) { + const [rows] = await pool.query( + "SELECT * FROM admin_user WHERE username = ? AND password = ?", + [username, password] + ); + if (rows.length > 0) return { success: true }; + return { success: false }; +} + +export async function getTableData() { + const [result] = await pool.query("SELECT * FROM lose"); + + if (result.length > 0) { + return { success: true, data: result }; + } + return { success: false }; +} diff --git a/backend/services/tokenService.js b/backend/services/tokenService.js new file mode 100644 index 0000000..6c5c46f --- /dev/null +++ b/backend/services/tokenService.js @@ -0,0 +1,26 @@ +import { SignJWT, jwtVerify } from "jose"; +import env from "dotenv"; +env.config(); +const secret = new TextEncoder().encode(process.env.SECRET_KEY); + +export async function generateToken(payload) { + const newToken = await new SignJWT(payload) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime("2h") // Token valid for 2 hours + .sign(secret); + console.log("Generated token: ", newToken); + return newToken; +} + +export async function authenticate(req, res, next) { + const authHeader = req.headers["authorization"]; + const token = authHeader && authHeader.split(" ")[1]; // Bearer + + if (token == null) return res.sendStatus(401); // No token present + + const { payload } = await jwtVerify(token, secret); + req.user = payload; + + next(); +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1a44c8d..0d9d244 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,7 +9,10 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.1.11", + "js-cookie": "^3.0.5", "lucide-react": "^0.539.0", + "primeicons": "^7.0.0", + "primereact": "^10.9.6", "react": "^19.1.1", "react-dom": "^19.1.1", "react-router-dom": "^7.8.0", @@ -22,6 +25,7 @@ }, "devDependencies": { "@eslint/js": "^9.32.0", + "@types/js-cookie": "^3.0.6", "@types/react": "^19.1.9", "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^4.7.0", @@ -268,6 +272,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", + "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -1861,6 +1874,13 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1872,7 +1892,6 @@ "version": "19.1.9", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1888,6 +1907,15 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.39.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", @@ -2468,7 +2496,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -2504,6 +2531,16 @@ "node": ">=8" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", @@ -3089,6 +3126,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3439,6 +3485,18 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -3595,6 +3653,15 @@ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3761,6 +3828,46 @@ "node": ">= 0.8.0" } }, + "node_modules/primeicons": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", + "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==", + "license": "MIT" + }, + "node_modules/primereact": { + "version": "10.9.6", + "resolved": "https://registry.npmjs.org/primereact/-/primereact-10.9.6.tgz", + "integrity": "sha512-0Jjz/KzfUURSHaPTXJwjL2Dc7CDPnbO17MivyJz7T5smGAMLY5d+IqpQhV61R22G/rDmhMh3+32LCNva2M8fRw==", + "license": "MIT", + "dependencies": { + "@types/react-transition-group": "^4.4.1", + "react-transition-group": "^4.4.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3813,6 +3920,12 @@ "react": "^19.1.1" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -3874,6 +3987,22 @@ "react-dom": "^18 || ^19" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9531c43..9cf1365 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,10 @@ }, "dependencies": { "@tailwindcss/vite": "^4.1.11", + "js-cookie": "^3.0.5", "lucide-react": "^0.539.0", + "primeicons": "^7.0.0", + "primereact": "^10.9.6", "react": "^19.1.1", "react-dom": "^19.1.1", "react-router-dom": "^7.8.0", @@ -24,6 +27,7 @@ }, "devDependencies": { "@eslint/js": "^9.32.0", + "@types/js-cookie": "^3.0.6", "@types/react": "^19.1.9", "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^4.7.0", diff --git a/frontend/src/components/Admin.tsx b/frontend/src/components/Admin.tsx index 8f9ce85..2b9b5ee 100644 --- a/frontend/src/components/Admin.tsx +++ b/frontend/src/components/Admin.tsx @@ -1,13 +1,27 @@ import "../App.css"; -import Layout from "../layout/Layout"; -import React from "react"; +import React, { useState } from "react"; +import HeaderAdmin from "./HeaderAdmin"; +import Table from "./Table"; +import Cookies from "js-cookie"; const Admin: React.FC = () => { + // Keep token in state so UI updates immediately after login without reload + const [token, setToken] = useState( + () => Cookies.get("token") ?? null + ); + return ( <> - -

Admin

-
+ setToken(t)} + onLogout={() => setToken(null)} + /> + {token ? ( + + ) : ( +
Please log in as an admin.
+ )} ); }; diff --git a/frontend/src/components/HeaderAdmin.tsx b/frontend/src/components/HeaderAdmin.tsx new file mode 100644 index 0000000..2bf4c91 --- /dev/null +++ b/frontend/src/components/HeaderAdmin.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import { Ticket, RectangleEllipsis } from "lucide-react"; +import { useState } from "react"; +import LoginForm from "./LoginForm"; +import { ToastContainer } from "react-toastify"; +import { logoutAdmin } from "../utils/userHandler"; + +interface HeaderAdminProps { + token?: string | null; + onLoginSuccess?: (token: string) => void; + onLogout?: () => void; +} + +const HeaderAdmin: React.FC = ({ + token, + onLoginSuccess, + onLogout, +}) => { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+
+
+ +

+ Admin Panel +

+
+ {!token && ( + + )} + {token && ( + + )} +
+ {isOpen && ( + setIsOpen(false)} + onLoginSuccess={(token) => { + onLoginSuccess?.(token); + }} + /> + )} + + +
+ ); +}; + +export default HeaderAdmin; diff --git a/frontend/src/components/Home.tsx b/frontend/src/components/Home.tsx index cef93a4..bd774ba 100644 --- a/frontend/src/components/Home.tsx +++ b/frontend/src/components/Home.tsx @@ -5,11 +5,9 @@ import React from "react"; const Home: React.FC = () => { return ( - <> - - - - + + + ); }; diff --git a/frontend/src/components/LoginForm.tsx b/frontend/src/components/LoginForm.tsx new file mode 100644 index 0000000..56951d3 --- /dev/null +++ b/frontend/src/components/LoginForm.tsx @@ -0,0 +1,89 @@ +import React, { useState } from "react"; +import { toast } from "react-toastify"; +import { handleSubmit } from "../utils/handleSubmit"; + +type LoginFormProps = { + onClose: () => void; + onLoginSuccess?: (token: string) => void; +}; + +const LoginForm: React.FC = ({ onClose, onLoginSuccess }) => { + const [loading, setLoading] = useState(false); + + const onSubmit = (e: React.FormEvent) => { + setLoading(true); + toast + .promise(handleSubmit(e), { + pending: "Logging in...", + }) + .then((res) => { + if (res?.token) { + onLoginSuccess?.(res.token); + } + onClose(); + }) + .finally(() => setLoading(false)); + }; + + return ( +
+
+ +
+

Login

+ +
+ + + + + +
+ + + + +
+ ); +}; + +export default LoginForm; diff --git a/frontend/src/components/SubHeaderAdmin.tsx b/frontend/src/components/SubHeaderAdmin.tsx new file mode 100644 index 0000000..b6a4399 --- /dev/null +++ b/frontend/src/components/SubHeaderAdmin.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { Sheet, WholeWord } from "lucide-react"; + +// Sub navigation bar for admin views: provides import + clear selection actions +const SubHeaderAdmin: React.FC = () => { + return ( +
+
+
+

+ Verwaltung +

+
+
+ + +
+
+
+ ); +}; + +export default SubHeaderAdmin; diff --git a/frontend/src/components/Table.tsx b/frontend/src/components/Table.tsx new file mode 100644 index 0000000..987c8f6 --- /dev/null +++ b/frontend/src/components/Table.tsx @@ -0,0 +1,190 @@ +import React, { useEffect, useState } from "react"; +import Cookies from "js-cookie"; +import { getTableData, readCachedTableData } from "../utils/userHandler"; +import { EllipsisVertical } from "lucide-react"; +import SubHeaderAdmin from "./SubHeaderAdmin"; + +interface DataPackage { + losnummer: string; + vorname: string | null; + nachname: string | null; + adresse: string | null; + plz: string | null; + email: string | null; + [key: string]: any; +} + +const Table: React.FC = () => { + const [rows, setRows] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Hilfsfunktion zum Einlesen & Normalisieren der LocalStorage-Daten + const loadFromCache = () => { + const cached = readCachedTableData(); + if (!cached) { + setRows([]); + return; + } + // Server könnte entweder ein Objekt oder ein Array liefern + const normalized: DataPackage[] = Array.isArray(cached) + ? cached + : [cached]; + setRows(normalized); + }; + + useEffect(() => { + // Initial lokale Daten laden (falls schon vorhanden) + loadFromCache(); + + // 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)); + }, []); + + // Reagieren auf LocalStorage-Änderungen (z.B. in anderen Tabs) + useEffect(() => { + const handler = (e: StorageEvent) => { + if (e.key === "tableData") { + loadFromCache(); + } + }; + window.addEventListener("storage", handler); + return () => window.removeEventListener("storage", handler); + }, []); + + const formatValue = (v: any) => + v === null || v === undefined || v === "" ? "-" : String(v); + + return ( + <> + +
+
+ {loading && ( + + Laden... + + )} + {error && {error}} +
+
+
+ + + + + + + + + + + + + + {rows.length === 0 && !loading && ( + + + + )} + {rows.map((row, idx) => ( + + + + + + + + + + + ))} + +
+ + + Losnummer + + Vorname + + Nachname + + Adresse + + PLZ + + Email +
+ Keine Daten vorhanden. +
+ + + {formatValue(row.losnummer)} + + + + + + + + + + + + +
+ + + + ); +}; + +export default Table; diff --git a/frontend/src/utils/handleSubmit.ts b/frontend/src/utils/handleSubmit.ts new file mode 100644 index 0000000..16e7a12 --- /dev/null +++ b/frontend/src/utils/handleSubmit.ts @@ -0,0 +1,49 @@ +import Cookies from "js-cookie"; +import type React from "react"; +import { myToast } from "./toastify"; + +interface LoginResponse { + success: boolean; + token?: string; + message?: string; +} + +// Performs login; resolves with data on success, throws Error on failure. +export const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const form = e.currentTarget; + const username = (form.elements.namedItem("username") as HTMLInputElement) + ?.value; + const password = (form.elements.namedItem("password") as HTMLInputElement) + ?.value; + + let response: Response; + try { + response = await fetch("http://localhost:8002/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password }), + }); + } catch (networkErr) { + myToast("Network error. Please try again.", "error"); + return; + } + + let data: LoginResponse; + try { + data = await response.json(); + } catch { + myToast("Invalid server response!", "error"); + return; + } + + if (!response.ok || !data.success || !data.token) { + myToast("Login failed!", "error"); + return; + } + + Cookies.set("token", data.token, { sameSite: "strict" }); + myToast("Login successful", "success"); + return data; +}; diff --git a/frontend/src/utils/userHandler.ts b/frontend/src/utils/userHandler.ts new file mode 100644 index 0000000..e77f3c2 --- /dev/null +++ b/frontend/src/utils/userHandler.ts @@ -0,0 +1,54 @@ +import Cookies from "js-cookie"; +import { myToast } from "./toastify"; + +export const logoutAdmin = () => { + Cookies.remove("token"); + localStorage.removeItem("tableData"); + myToast("Logged out successfully!", "success"); +}; + +// Fetch table data and store it in localStorage. Returns the parsed data or null on failure. +export const getTableData = async (token: string) => { + try { + const response = await fetch("http://localhost:8002/table-data", { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (response.status === 401) { + myToast("Session expired. Please log in again.", "error"); + logoutAdmin(); + return null; + } + + if (!response.ok) { + const text = await response.text().catch(() => ""); + myToast(`Error fetching table data! ${text}`, "error"); + return null; + } + + // Ensure we parse JSON + const data = await response.json(); + localStorage.setItem("tableData", JSON.stringify(data)); + myToast("Table data fetched successfully!", "success"); + return data; + } catch (error: any) { + myToast(`Error fetching table data! ${error?.message || error}`, "error"); + return null; + } +}; + +// Helper to read cached table data safely +export const readCachedTableData = (): T | null => { + const raw = localStorage.getItem("tableData"); + if (!raw) return null; + try { + return JSON.parse(raw) as T; + } catch { + // Corrupted cache -> clear it + localStorage.removeItem("tableData"); + return null; + } +};