feat: add entry removal and row saving functionality, enhance admin components with localization

This commit is contained in:
2025-08-13 21:48:19 +02:00
parent 5792ce154f
commit 3096e4ab83
8 changed files with 254 additions and 45 deletions

View File

@@ -6,6 +6,8 @@ import {
loginAdmin, loginAdmin,
getTableData, getTableData,
createEntry, createEntry,
removeEntries,
saveRow,
} from "./services/database.js"; } from "./services/database.js";
import { generateToken, authenticate } from "./services/tokenService.js"; import { generateToken, authenticate } from "./services/tokenService.js";
env.config(); env.config();
@@ -30,6 +32,22 @@ app.post("/lose", async (req, res) => {
} }
}); });
// !!!!!!! AUTHORISATION HINZUFÜGEN - DENN GEHT NICHT !!!!!!!!
app.get("/table-data", authenticate, async (req, res) => { app.get("/table-data", authenticate, async (req, res) => {
const result = await getTableData(); const result = await getTableData();
if (result.success) { if (result.success) {
@@ -48,6 +66,24 @@ app.post("/create-entry", async (req, res) => {
} }
}); });
app.delete("/remove-entries", async (req, res) => {
const result = await removeEntries(req.body.losnummern);
if (result) {
res.status(200).json({ success: true });
} else {
res.status(400).json({ success: false });
}
});
app.put("/save-row", async (req, res) => {
const result = await saveRow(req.body);
if (result.success) {
res.status(200).json({ success: true });
} else {
res.status(400).json({ success: false });
}
});
app.post("/login", async (req, res) => { app.post("/login", async (req, res) => {
const { username, password } = req.body; const { username, password } = req.body;
const result = await loginAdmin(username, password); const result = await loginAdmin(username, password);

View File

@@ -51,8 +51,6 @@ export async function getTableData() {
return { success: false }; return { success: false };
} }
// Create data from array not working !!!
export async function createEntry(data) { export async function createEntry(data) {
let { status } = { status: true }; let { status } = { status: true };
for (const item of data) { for (const item of data) {
@@ -71,3 +69,42 @@ export async function createEntry(data) {
return status; return status;
} }
export async function removeEntries(losnummern) {
let { status } = { status: true };
for (const losnummer of losnummern) {
const [result] = await pool.query("DELETE FROM lose WHERE losnummer = ?", [
losnummer,
]);
if (result.affectedRows > 0) {
status = true;
} else {
status = false;
break;
}
}
return status;
}
export async function saveRow(payload) {
const [result] = await pool.query(
"UPDATE lose SET vorname = ?, nachname = ?, adresse = ?, plz = ?, email = ? WHERE losnummer = ?",
[
payload.vorname,
payload.nachname,
payload.adresse,
payload.plz,
payload.email,
payload.losnummer,
]
);
if (result.affectedRows > 0) {
return { success: true };
} else {
return { success: false };
}
}

View File

@@ -22,7 +22,7 @@ const Admin: React.FC = () => {
{token ? ( {token ? (
<Table /> <Table />
) : ( ) : (
<div className="p-4">Please log in as an admin.</div> <div className="p-4">Bitte als Admin einloggen. Oder gehe <a className="text-blue-500 hover:underline" href="/">zurück</a>.</div>
)} )}
</> </>
); );

View File

@@ -125,7 +125,7 @@ const MainForm: React.FC = () => {
Los registrieren Los registrieren
</button> </button>
<p className="mt-1 text-xs text-zinc-500"> <p className="mt-1 text-xs text-zinc-500">
Wenn Sie die Daten eines Loses bearbeiten möchten,{" "} Wenn Sie die Daten eines bereits registrierten Loses bearbeiten möchten,{" "}
<a <a
className="text-blue-600 underline" className="text-blue-600 underline"
href="mailto:example@example.com" href="mailto:example@example.com"

View File

@@ -2,6 +2,7 @@ import React from "react";
import { Sheet, WholeWord } from "lucide-react"; import { Sheet, WholeWord } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import ImportGUI from "./ImportGUI"; import ImportGUI from "./ImportGUI";
import { removeSelection } from "../utils/tableActions";
type SubHeaderAdminProps = { type SubHeaderAdminProps = {
setFiles: (files: File[]) => void; setFiles: (files: File[]) => void;
@@ -41,6 +42,9 @@ const SubHeaderAdmin: React.FC<SubHeaderAdminProps> = ({ setFiles, files }) => {
<span className="whitespace-nowrap">Losnummern importieren</span> <span className="whitespace-nowrap">Losnummern importieren</span>
</button> </button>
<button <button
onClick={() => {
removeSelection();
}}
type="button" type="button"
className="group inline-flex items-center gap-2 rounded-md bg-rose-600 px-3.5 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-rose-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-500/60 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60" className="group inline-flex items-center gap-2 rounded-md bg-rose-600 px-3.5 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-rose-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-500/60 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60"
> >

View File

@@ -2,8 +2,9 @@ import React, { useEffect, useState } from "react";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { getTableData, readCachedTableData } from "../utils/userHandler"; import { getTableData, readCachedTableData } from "../utils/userHandler";
import { EllipsisVertical } from "lucide-react"; import { Save } from "lucide-react";
import SubHeaderAdmin from "./SubHeaderAdmin"; import SubHeaderAdmin from "./SubHeaderAdmin";
import { addToRemove, rmFromRemove, saveRow } from "../utils/tableActions";
interface DataPackage { interface DataPackage {
losnummer: string; losnummer: string;
@@ -17,9 +18,12 @@ interface DataPackage {
const Table: React.FC = () => { const Table: React.FC = () => {
const [rows, setRows] = useState<DataPackage[]>([]); // holds normalized cache view const [rows, setRows] = useState<DataPackage[]>([]); // holds normalized cache view
const [error, setError] = useState<string | null>(null);
const [files, setFiles] = useState<File[]>([]); const [files, setFiles] = useState<File[]>([]);
// Einheitliche Input-Styles (nur Tailwind)
const inputClasses =
"w-full h-10 px-3 rounded-md border border-gray-300 bg-white text-sm text-gray-900 placeholder-gray-400 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
// Hilfsfunktion zum Einlesen & Normalisieren der LocalStorage-Daten // Hilfsfunktion zum Einlesen & Normalisieren der LocalStorage-Daten
const loadFromCache = () => { const loadFromCache = () => {
const cached = readCachedTableData<any>(); const cached = readCachedTableData<any>();
@@ -48,15 +52,10 @@ const Table: React.FC = () => {
// Sync normalized cached data into local state whenever query succeeds or cache changes // Sync normalized cached data into local state whenever query succeeds or cache changes
useEffect(() => { useEffect(() => {
loadFromCache(); loadFromCache();
Cookies.remove("removeArr");
}, [tableQuery.data]); }, [tableQuery.data]);
useEffect(() => { // Fehleranzeige ist aktuell nicht sichtbar (auskommentierte UI).
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) // Reagieren auf LocalStorage-Änderungen (z.B. in anderen Tabs)
useEffect(() => { useEffect(() => {
@@ -69,13 +68,24 @@ const Table: React.FC = () => {
return () => window.removeEventListener("storage", handler); return () => window.removeEventListener("storage", handler);
}, []); }, []);
const formatValue = (v: any) => // Handles input changes for table rows
v === null || v === undefined || v === "" ? "-" : String(v); const handleInputChange = (
losnummer: string,
field: keyof DataPackage,
value: string
) => {
setRows((prevRows) =>
prevRows.map((row) =>
row.losnummer === losnummer ? { ...row, [field]: value } : row
)
);
};
return ( return (
<> <>
<SubHeaderAdmin setFiles={setFiles} files={files} /> <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">
{(tableQuery.isLoading || tableQuery.isFetching) && ( {(tableQuery.isLoading || tableQuery.isFetching) && (
<span className="text-xs text-blue-600 animate-pulse"> <span className="text-xs text-blue-600 animate-pulse">
@@ -83,6 +93,7 @@ const Table: React.FC = () => {
</span> </span>
)} )}
{error && <span className="text-xs text-red-600">{error}</span>} {error && <span className="text-xs text-red-600">{error}</span>}
<button <button
type="button" type="button"
onClick={() => tableQuery.refetch()} onClick={() => tableQuery.refetch()}
@@ -91,55 +102,56 @@ const Table: React.FC = () => {
Refresh Refresh
</button> </button>
</div> </div>
<div className="overflow-x-auto rounded-lg shadow ring-1 ring-black/5"> */}
<div className="overflow-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">
<thead className="bg-gray-50"> <thead className="bg-gray-50 sticky top-0">
<tr> <tr>
<th <th
scope="col" scope="col"
className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600" className="px-4 py-2 text-left invisible font-medium uppercase tracking-wide text-gray-600 w-10 min-w-[2.5rem]"
> >
<input type="checkbox" name="" id="" /> <input type="checkbox" name="" id="" />
</th> </th>
<th <th
scope="col" scope="col"
className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600" className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600 w-[10rem] min-w-[10rem]"
> >
Losnummer Losnummer
</th> </th>
<th <th
scope="col" scope="col"
className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600" className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600 w-[14rem] min-w-[14rem]"
> >
Vorname Vorname
</th> </th>
<th <th
scope="col" scope="col"
className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600" className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600 w-[14rem] min-w-[14rem]"
> >
Nachname Nachname
</th> </th>
<th <th
scope="col" scope="col"
className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600" className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600 w-[14rem] min-w-[14rem]"
> >
Adresse Adresse
</th> </th>
<th <th
scope="col" scope="col"
className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600" className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600 w-[14rem] min-w-[14rem]"
> >
PLZ PLZ
</th> </th>
<th <th
scope="col" scope="col"
className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600" className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600 w-[14rem] min-w-[14rem]"
> >
Email Email
</th> </th>
<th <th
scope="col" scope="col"
className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600" className="px-4 py-2 text-left font-medium uppercase tracking-wide text-gray-600 w-12 min-w-[3rem]"
></th> ></th>
</tr> </tr>
</thead> </thead>
@@ -147,7 +159,7 @@ const Table: React.FC = () => {
{rows.length === 0 && !tableQuery.isLoading && ( {rows.length === 0 && !tableQuery.isLoading && (
<tr> <tr>
<td <td
colSpan={6} colSpan={8}
className="px-4 py-6 text-center text-gray-500" className="px-4 py-6 text-center text-gray-500"
> >
Keine Daten vorhanden. Keine Daten vorhanden.
@@ -159,33 +171,95 @@ const Table: React.FC = () => {
key={row.losnummer ?? idx} key={row.losnummer ?? idx}
className="hover:bg-gray-50 transition-colors" className="hover:bg-gray-50 transition-colors"
> >
<td className="px-4 py-2 font-mono text-xs text-gray-900"> <td className="px-4 py-2 font-mono text-xs text-gray-900 w-10 min-w-[2.5rem]">
<input type="checkbox" name="" id={row.losnummer} /> <input
type="checkbox"
name=""
onChange={(e) => {
if (e.target.checked) {
addToRemove(row.losnummer);
} else {
rmFromRemove(row.losnummer);
}
}}
id={row.losnummer}
/>
</td> </td>
<td className="px-4 py-2 font-mono text-xs text-gray-900"> <td className="px-4 py-2 font-mono text-xs text-gray-900 w-[10rem] min-w-[10rem]">
{formatValue(row.losnummer)} {row.losnummer}
</td> </td>
<td className="px-4 py-2"> <td className="px-4 py-2 w-[14rem] min-w-[14rem]">
<input type="text" value={formatValue(row.vorname)} /> <input
type="text"
className={inputClasses}
onChange={(e) => {
handleInputChange(
row.losnummer,
"vorname",
e.target.value
);
}}
value={row.vorname ?? ""}
/>
</td> </td>
<td className="px-4 py-2"> <td className="px-4 py-2 w-[14rem] min-w-[14rem]">
<input type="text" value={formatValue(row.nachname)} /> <input
type="text"
className={inputClasses}
onChange={(e) => {
handleInputChange(
row.losnummer,
"nachname",
e.target.value
);
}}
value={row.nachname ?? ""}
/>
</td> </td>
<td <td
className="px-4 py-2 max-w-[16rem] truncate" className="px-4 py-2 w-[14rem] min-w-[14rem]"
title={formatValue(row.adresse)} title={row.adresse ?? ""}
> >
<input type="text" value={formatValue(row.adresse)} /> <input
type="text"
className={inputClasses}
onChange={(e) => {
handleInputChange(
row.losnummer,
"adresse",
e.target.value
);
}}
value={row.adresse ?? ""}
/>
</td> </td>
<td className="px-4 py-2"> <td className="px-4 py-2 w-[14rem] min-w-[14rem]">
<input type="text" value={formatValue(row.plz)} /> <input
type="text"
className={inputClasses}
onChange={(e) => {
handleInputChange(row.losnummer, "plz", e.target.value);
}}
value={row.plz ?? ""}
/>
</td> </td>
<td className="px-4 py-2"> <td className="px-4 py-2 w-[14rem] min-w-[14rem]">
<input type="text" value={formatValue(row.email)} /> <input
type="text"
className={inputClasses}
onChange={(e) => {
handleInputChange(
row.losnummer,
"email",
e.target.value
);
}}
value={row.email ?? ""}
/>
</td> </td>
<td className="px-4 py-2"> <td className="px-4 py-2 w-12 min-w-[3rem]">
<button> <button onClick={() => saveRow(row)}>
<EllipsisVertical /> <Save />
</button> </button>
</td> </td>
</tr> </tr>

View File

@@ -0,0 +1,58 @@
import Cookies from "js-cookie";
import { myToast } from "./toastify";
import { queryClient } from "../queryClient";
let removeArr: string[] = [];
export const addToRemove = (losnummer: string) => {
removeArr.push(losnummer);
const rawCookies = Cookies.withConverter({
write: (value: string, _name: string) => value,
});
rawCookies.set("removeArr", JSON.stringify(removeArr));
};
export const rmFromRemove = (losnummer: string) => {
removeArr = removeArr.filter((item) => item !== losnummer);
const rawCookies = Cookies.withConverter({
write: (value: string, _name: string) => value,
});
rawCookies.set("removeArr", JSON.stringify(removeArr));
};
const token = Cookies.get("token");
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
};
export const removeSelection = () => {
const selection = Cookies.get("removeArr");
if (selection && selection !== "[]") {
fetch("http://localhost:8002/remove-entries", {
method: "DELETE",
headers: headers,
body: `{
"losnummern": ${selection}
}`,
}).then((response) => {
if (response.ok) {
myToast("Einträge erfolgreich entfernt.", "success");
queryClient.invalidateQueries({ queryKey: ["table-data"] });
} else {
myToast("Fehler beim Entfernen der Einträge.", "error");
}
});
} else {
myToast("Keine Einträge zum Entfernen ausgewählt.", "info");
}
};
export const saveRow = (data: any) => {
fetch("http://localhost:8002/save-row", {
method: "PUT",
headers: headers,
body: JSON.stringify(data),
});
};

View File

@@ -7,6 +7,7 @@ export const logoutAdmin = () => {
myToast("Logged out successfully!", "success"); myToast("Logged out successfully!", "success");
}; };
// Fetch table data and store it in localStorage. Returns the parsed data or null on failure. // Fetch table data and store it in localStorage. Returns the parsed data or null on failure.
export const getTableData = async (token: string) => { export const getTableData = async (token: string) => {
try { try {
@@ -32,7 +33,6 @@ export const getTableData = async (token: string) => {
// Ensure we parse JSON // Ensure we parse JSON
const data = await response.json(); const data = await response.json();
localStorage.setItem("tableData", JSON.stringify(data)); localStorage.setItem("tableData", JSON.stringify(data));
myToast("Table data fetched successfully!", "success");
return data; return data;
} catch (error: any) { } catch (error: any) {
myToast(`Error fetching table data! ${error?.message || error}`, "error"); myToast(`Error fetching table data! ${error?.message || error}`, "error");