Refactor loan and user management components and backend routes

- Updated LoanTable component to fetch loan data from new API endpoint and display notes.
- Enhanced UserTable component to include additional user fields (first name, last name, email, admin status) and updated input handling.
- Modified fetcher utility to use new user data API endpoint.
- Adjusted login functionality to point to the new admin login endpoint and handle unauthorized access.
- Refactored user actions utility to align with updated API endpoints for user management.
- Updated backend routes for user and loan data management to reflect new structure and naming conventions.
- Revised SQL schema and mock data to accommodate new fields and constraints.
- Changed Docker configuration to use the new database name.
This commit is contained in:
2025-11-11 17:08:45 +01:00
parent 974a5a75d8
commit a8b4ac3d60
26 changed files with 605 additions and 347 deletions

View File

@@ -17,17 +17,14 @@ import { useState, useEffect } from "react";
import { deleteAPKey } from "@/utils/userActions";
import AddAPIKey from "./AddAPIKey";
import { formatDateTime } from "@/utils/userFuncs";
const API_BASE =
(import.meta as any).env?.VITE_BACKEND_URL ||
import.meta.env.VITE_BACKEND_URL ||
"http://localhost:8002";
import { API_BASE } from "@/config/api.config";
type Items = {
id: number;
apiKey: string;
user: string;
api_key: string;
entry_name: string;
entry_created_at: string;
last_used_at: string | null;
};
const APIKeyTable: React.FC = () => {
@@ -56,13 +53,17 @@ const APIKeyTable: React.FC = () => {
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch(`${API_BASE}/api/apiKeys`, {
method: "GET",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
});
const response = await fetch(
`${API_BASE}/api/admin/api-data/get-api-keys`,
{
method: "GET",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
}
);
const data = await response.json();
console.log(data);
return data;
} catch (error) {
setError("error", "Failed to fetch items", "There is an error");
@@ -159,29 +160,37 @@ const APIKeyTable: React.FC = () => {
<strong>API Key</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Benutzer</strong>
<strong>Name</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Eintrag erstellt am</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Zuletzt benutzt am</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Aktionen</strong>
</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{items.map((apiKey) => (
<Table.Row key={apiKey.id}>
<Table.Cell>{apiKey.id}</Table.Cell>
<Table.Cell>{apiKey.apiKey}</Table.Cell>
<Table.Cell>{apiKey.user}</Table.Cell>
<Table.Cell>{formatDateTime(apiKey.entry_created_at)}</Table.Cell>
{items.map((item) => (
<Table.Row key={item.id}>
<Table.Cell>{item.id}</Table.Cell>
<Table.Cell>{item.api_key}</Table.Cell>
<Table.Cell>{item.entry_name}</Table.Cell>
<Table.Cell>{formatDateTime(item.entry_created_at)}</Table.Cell>
<Table.Cell>
{!item.last_used_at
? "Nie benutzt"
: formatDateTime(item.last_used_at)}
</Table.Cell>
<Table.Cell>
<Button
onClick={() =>
deleteAPKey(apiKey.id).then((response) => {
deleteAPKey(item.id).then((response) => {
if (response.success) {
setItems(items.filter((i) => i.id !== apiKey.id));
setItems(items.filter((i) => i.id !== item.id));
setError(
"success",
"Gegenstand gelöscht",

View File

@@ -1,6 +1,15 @@
import React from "react";
import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
import {
Button,
Card,
Field,
Input,
Stack,
InputGroup,
Span,
} from "@chakra-ui/react";
import { createAPIentry } from "@/utils/userActions";
import { useState } from "react";
type AddAPIKeyProps = {
onClose: () => void;
@@ -12,6 +21,8 @@ type AddAPIKeyProps = {
};
const AddAPIKey: React.FC<AddAPIKeyProps> = ({ onClose, alert }) => {
const [value, setValue] = useState("");
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<Card.Root maxW="sm">
@@ -23,13 +34,26 @@ const AddAPIKey: React.FC<AddAPIKeyProps> = ({ onClose, alert }) => {
</Card.Header>
<Card.Body>
<Stack gap="4" w="full">
<InputGroup
endElement={
<Span color="fg.muted" textStyle="xs">
{value.length} / {15}
</Span>
}
>
<Input
placeholder="Er muss 15 Zeichen lang sein"
value={value}
id="apiKey"
maxLength={15}
onChange={(e) => {
setValue(e.currentTarget.value.slice(0, 15));
}}
/>
</InputGroup>
<Field.Root>
<Field.Label>API key</Field.Label>
<Input type="number" id="apiKey" />
</Field.Root>
<Field.Root>
<Field.Label>Benutzer</Field.Label>
<Input id="user" type="text" />
<Field.Label>Name</Field.Label>
<Input id="name" type="text" />
</Field.Root>
</Stack>
</Card.Body>
@@ -44,14 +68,14 @@ const AddAPIKey: React.FC<AddAPIKeyProps> = ({ onClose, alert }) => {
(
document.getElementById("apiKey") as HTMLInputElement
)?.value.trim() || "";
const user =
const name =
(
document.getElementById("user") as HTMLInputElement
document.getElementById("name") as HTMLInputElement
)?.value.trim() || "";
if (!apiKey || !user) return;
if (!apiKey || !name) return;
const res = await createAPIentry(apiKey, user);
const res = await createAPIentry(apiKey, name);
if (res.success) {
alert(
"success",

View File

@@ -1,5 +1,13 @@
import React from "react";
import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
import {
Button,
Card,
Field,
Input,
Stack,
Text,
Checkbox,
} from "@chakra-ui/react";
import { createUser } from "@/utils/userActions";
type AddFormProps = {
@@ -12,73 +20,128 @@ type AddFormProps = {
};
const AddForm: React.FC<AddFormProps> = ({ onClose, alert }) => {
const [admin, setAdmin] = React.useState(false);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<Card.Root maxW="sm">
<Card.Header>
<Card.Title>Neuen Nutzer erstellen</Card.Title>
<Card.Description>
Füllen Sie das folgende Formular aus, um einen Nutzer zu erstellen.
</Card.Description>
</Card.Header>
<Card.Body>
<Stack gap="4" w="full">
<Field.Root>
<Field.Label>Username</Field.Label>
<Input id="username" />
</Field.Root>
<Field.Root>
<Field.Label>Password</Field.Label>
<Input id="password" type="password" />
</Field.Root>
<Field.Root>
<Field.Label>Role</Field.Label>
<Input id="role" type="number" />
</Field.Root>
</Stack>
</Card.Body>
<Card.Footer justifyContent="flex-end">
<Button variant="outline" onClick={onClose}>
Abbrechen
</Button>
<Button
variant="solid"
onClick={async () => {
const username =
(
document.getElementById("username") as HTMLInputElement
)?.value.trim() || "";
const password =
(document.getElementById("password") as HTMLInputElement)
?.value || "";
const role = Number(
(document.getElementById("role") as HTMLInputElement)?.value
);
<form
onSubmit={(e) => {
e.preventDefault();
}}
>
<Card.Root maxW="sm">
<Card.Header>
<Card.Title>Neuen Nutzer erstellen</Card.Title>
<Card.Description>
Füllen Sie das folgende Formular aus, um einen Nutzer zu
erstellen.
</Card.Description>
</Card.Header>
if (!username || !password || Number.isNaN(role)) return;
<Card.Body>
<Stack gap="4" w="full">
<Field.Root>
<Field.Label>Benutzername</Field.Label>
<Input id="username" />
</Field.Root>
<Field.Root>
<Field.Label>Passwort</Field.Label>
<Input id="password" type="password" />
</Field.Root>
<Field.Root>
<Field.Label>Vorname</Field.Label>
<Input id="firstname" />
</Field.Root>
<Field.Root>
<Field.Label>Nachname</Field.Label>
<Input id="lastname" />
</Field.Root>
<Field.Root>
<Field.Label>E-Mail</Field.Label>
<Input id="email" type="email" />
</Field.Root>
const res = await createUser(username, role, password);
if (res.success) {
alert(
"success",
"Nutzer erstellt",
"Der Nutzer wurde erfolgreich erstellt."
{/* Kontrollierte Checkbox */}
<Checkbox.Root
checked={admin}
onCheckedChange={(e: any) => setAdmin(Boolean(e?.checked ?? e))}
>
<Checkbox.HiddenInput />
<Checkbox.Control />
<Checkbox.Label>Admin</Checkbox.Label>
</Checkbox.Root>
<Field.Root>
<Field.Label>Rolle</Field.Label>
<Input id="role" type="number" />
</Field.Root>
</Stack>
</Card.Body>
<Card.Footer justifyContent="flex-end">
<Text>Der Benutzername kann nicht mehr geändert werden.</Text>
<Button variant="outline" onClick={onClose}>
Abbrechen
</Button>
<Button
variant="solid"
type="submit"
onClick={async () => {
const username =
(
document.getElementById("username") as HTMLInputElement
)?.value.trim() || "";
const password =
(document.getElementById("password") as HTMLInputElement)
?.value || "";
const role = Number(
(document.getElementById("role") as HTMLInputElement)?.value
);
onClose();
} else {
alert(
"error",
"Fehler beim Erstellen des Nutzers",
"Es gab einen Fehler beim Erstellen des Nutzers. Vielleicht gibt es bereits einen Nutzer mit diesem Benutzernamen."
const firstname =
(
document.getElementById("firstname") as HTMLInputElement
)?.value.trim() || "";
const lastname =
(
document.getElementById("lastname") as HTMLInputElement
)?.value.trim() || "";
const email =
(
document.getElementById("email") as HTMLInputElement
)?.value.trim() || "";
// admin kommt jetzt zuverlässig aus dem State
const res = await createUser(
username,
role,
password,
firstname,
lastname,
email,
admin
);
onClose();
}
}}
>
Erstellen
</Button>
</Card.Footer>
</Card.Root>
if (res.success) {
alert(
"success",
"Nutzer erstellt",
"Der Nutzer wurde erfolgreich erstellt."
);
onClose();
} else {
alert(
"error",
"Fehler beim Erstellen des Nutzers",
"Es gab einen Fehler beim Erstellen des Nutzers. Vielleicht gibt es bereits einen Nutzer mit diesem Benutzernamen."
);
onClose();
}
}}
>
Erstellen
</Button>
</Card.Footer>
</Card.Root>
</form>
</div>
);
};

View File

@@ -30,18 +30,17 @@ import {
} from "@/utils/userActions";
import AddItemForm from "./AddItemForm";
import { formatDateTime } from "@/utils/userFuncs";
const API_BASE =
(import.meta as any).env?.VITE_BACKEND_URL ||
import.meta.env.VITE_BACKEND_URL ||
"http://localhost:8002";
import { API_BASE } from "@/config/api.config";
type Items = {
id: number;
item_name: string;
can_borrow_role: string;
inSafe: boolean;
in_safe: boolean;
entry_created_at: string;
entry_updated_at: string;
last_borrowed_person: string | null;
currently_borrowing: string | null;
};
const ItemTable: React.FC = () => {
@@ -82,12 +81,15 @@ const ItemTable: React.FC = () => {
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch(`${API_BASE}/api/allItems`, {
method: "GET",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
});
const response = await fetch(
`${API_BASE}/api/admin/item-data/all-items`,
{
method: "GET",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
}
);
const data = await response.json();
return data;
} catch (error) {
@@ -193,6 +195,15 @@ const ItemTable: React.FC = () => {
<Table.ColumnHeader>
<strong>Eintrag erstellt am</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Eintrag aktualisiert am</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Letzte ausleihende Person</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Derzeit ausgeliehen von</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Aktionen</strong>
</Table.ColumnHeader>
@@ -229,31 +240,34 @@ const ItemTable: React.FC = () => {
py={1}
gap={2}
variant="ghost"
color={item.inSafe ? "green.600" : "red.600"}
color={item.in_safe ? "green.600" : "red.600"}
borderWidth="1px"
borderColor={item.inSafe ? "green.300" : "red.300"}
borderColor={item.in_safe ? "green.300" : "red.300"}
_hover={{
bg: item.inSafe ? "green.50" : "red.50",
borderColor: item.inSafe ? "green.400" : "red.400",
bg: item.in_safe ? "green.50" : "red.50",
borderColor: item.in_safe ? "green.400" : "red.400",
transform: "translateY(-1px)",
shadow: "sm",
}}
_active={{ transform: "translateY(0)" }}
aria-label={
item.inSafe ? "Mark as not in safe" : "Mark as in safe"
item.in_safe ? "Mark as not in safe" : "Mark as in safe"
}
>
<Icon
as={item.inSafe ? CheckCircle2 : XCircle}
as={item.in_safe ? CheckCircle2 : XCircle}
boxSize={3.5}
mr={2}
/>
<Text as="span" fontSize="xs" fontWeight="semibold">
{item.inSafe ? "Yes" : "No"}
{item.in_safe ? "Yes" : "No"}
</Text>
</Button>
</Table.Cell>
<Table.Cell>{formatDateTime(item.entry_created_at)}</Table.Cell>
<Table.Cell>{formatDateTime(item.entry_updated_at)}</Table.Cell>
<Table.Cell>{item.last_borrowed_person}</Table.Cell>
<Table.Cell>{item.currently_borrowing}</Table.Cell>
<Table.Cell>
<Button
onClick={() =>

View File

@@ -17,11 +17,7 @@ import MyAlert from "./myChakra/MyAlert";
import { formatDateTime } from "@/utils/userFuncs";
import { Trash2, RefreshCcwDot } from "lucide-react";
import { deleteLoan } from "@/utils/userActions";
const API_BASE =
(import.meta as any).env?.VITE_BACKEND_URL ||
import.meta.env.VITE_BACKEND_URL ||
"http://localhost:8002";
import { API_BASE } from "@/config/api.config";
const LoanTable: React.FC = () => {
const [items, setItems] = useState<Loan[]>([]);
@@ -55,18 +51,22 @@ const LoanTable: React.FC = () => {
created_at: string;
loaned_items_name: string[];
deleted: boolean;
note: string;
};
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch(`${API_BASE}/api/allLoans`, {
method: "GET",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
});
const response = await fetch(
`${API_BASE}/api/admin/loan-data/all-loans`,
{
method: "GET",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
}
);
const data = await response.json();
return data;
} catch (error) {
@@ -161,6 +161,9 @@ const LoanTable: React.FC = () => {
<Table.ColumnHeader>
<strong>Ausgeliehene Artikel</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Notiz</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Aktionen</strong>
</Table.ColumnHeader>
@@ -180,6 +183,7 @@ const LoanTable: React.FC = () => {
<Table.Cell>{formatDateTime(item.returned_date)}</Table.Cell>
<Table.Cell>{formatDateTime(item.created_at)}</Table.Cell>
<Table.Cell>{item.loaned_items_name.join(", ")}</Table.Cell>
<Table.Cell>{item.note}</Table.Cell>
<Table.Cell>
<Button
onClick={() =>

View File

@@ -10,6 +10,7 @@ import {
HStack,
IconButton,
Heading,
Switch, // neu
} from "@chakra-ui/react";
import { Tooltip } from "@/components/ui/tooltip";
import { fetchUserData } from "@/utils/fetcher";
@@ -23,9 +24,13 @@ import ChangePWform from "./ChangePWform";
type User = {
id: number;
username: string;
password: string;
role: string;
first_name: string;
last_name: string;
email: string;
is_admin: boolean;
role: number;
entry_created_at: string;
entry_updated_at: string;
};
const UserTable: React.FC = () => {
@@ -52,10 +57,20 @@ const UserTable: React.FC = () => {
setIsError(true);
};
const handleInputChange = (userId: number, field: string, value: string) => {
const handleInputChange = (userId: number, field: string, value: any) => {
setUsers((prevUsers) =>
prevUsers.map((user) =>
user.id === userId ? { ...user, [field]: value } : user
user.id === userId
? {
...user,
[field]:
field === "role"
? Number(value)
: field === "is_admin"
? value === true || value === "true" || value === 1
: value,
}
: user
)
);
};
@@ -70,7 +85,7 @@ const UserTable: React.FC = () => {
setIsLoading(true);
try {
const data = await fetchUserData();
console.log("user api response", data);
console.log(data);
if (Array.isArray(data)) {
setUsers(data);
} else {
@@ -189,6 +204,18 @@ const UserTable: React.FC = () => {
<Table.ColumnHeader>
<strong>Benutzername</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Vorname</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Nachname</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>E-Mail</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Admin</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Passwort ändern</strong>
</Table.ColumnHeader>
@@ -198,6 +225,9 @@ const UserTable: React.FC = () => {
<Table.ColumnHeader>
<strong>Eintrag erstellt am</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Eintrag aktualisiert am</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Aktionen</strong>
</Table.ColumnHeader>
@@ -207,14 +237,58 @@ const UserTable: React.FC = () => {
{users.map((user) => (
<Table.Row key={user.id}>
<Table.Cell>{user.id}</Table.Cell>
<Table.Cell>{user.username}</Table.Cell>
{/* Vorname */}
<Table.Cell>
<Input
size="sm"
value={user.first_name ?? ""}
onChange={(e) =>
handleInputChange(user.id, "username", e.target.value)
handleInputChange(user.id, "first_name", e.target.value)
}
value={user.username}
/>
</Table.Cell>
{/* Nachname */}
<Table.Cell>
<Input
size="sm"
value={user.last_name ?? ""}
onChange={(e) =>
handleInputChange(user.id, "last_name", e.target.value)
}
/>
</Table.Cell>
{/* E-Mail */}
<Table.Cell>
<Input
type="email"
size="sm"
value={user.email ?? ""}
onChange={(e) =>
handleInputChange(user.id, "email", e.target.value)
}
/>
</Table.Cell>
{/* Admin */}
<Table.Cell>
<Switch.Root
size="sm"
checked={!!user.is_admin}
onCheckedChange={(details) =>
handleInputChange(user.id, "is_admin", details.checked)
}
aria-label="Adminrechte umschalten"
>
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
<Switch.HiddenInput />
</Switch.Root>
</Table.Cell>
<Table.Cell>
<Button onClick={() => handlePasswordChange(user.username)}>
Passwort ändern
@@ -230,13 +304,17 @@ const UserTable: React.FC = () => {
/>
</Table.Cell>
<Table.Cell>{formatDateTime(user.entry_created_at)}</Table.Cell>
<Table.Cell>{formatDateTime(user.entry_updated_at)}</Table.Cell>
<Table.Cell>
<Button
onClick={() =>
handleEdit(
user.id,
user.username,
user.role,
user.first_name,
user.last_name,
user.email,
user.is_admin,
Number(user.role)
).then((response) => {
if (response.success) {
setError(