32 Commits

Author SHA1 Message Date
195f270064 Merge branch 'dev' into debian12 2026-02-22 23:46:35 +01:00
c8a230979f changed version info 2026-02-22 23:45:38 +01:00
3d592c5c76 Merge branch 'dev' into debian12 2026-02-22 23:44:46 +01:00
581cd4a1fd changed link of help wiki 2026-02-22 23:41:25 +01:00
c53e6e095a updated api documentation 2026-02-22 23:33:30 +01:00
eaa325668c Implemented page header and added note column 2026-02-22 23:21:33 +01:00
08104d32db Merge branch 'dev' into debian12 2026-02-20 16:30:34 +01:00
1fa8b4a9a7 refactor: clean up layout components and improve footer styling 2026-02-20 16:30:20 +01:00
3ba3c1c0cb improved error logging for the api route to return or take loans 2026-02-20 16:22:13 +01:00
ee54d51f8b enhanced loan management: added note field to loan creation and email templates 2026-02-20 12:14:56 +01:00
a8dab549af fixed bug: cannot return loan 2026-02-20 12:02:33 +01:00
06976f7972 added pasword input to admin panel 2026-02-14 19:01:59 +01:00
977a6c1b16 deltedt mock data because its too old 2026-02-09 15:51:00 +01:00
38c647c62f Fixed bug/issue: #13 2026-02-09 15:49:51 +01:00
757b316b49 fixed translation bug 2026-02-09 13:42:19 +01:00
d05e9ab3ee deleted unused changelog 2026-02-09 13:42:06 +01:00
cc7c024892 Merge branch 'dev' into debian12 2026-02-07 17:48:44 +01:00
3f59ed6951 removed line 2026-02-07 17:48:23 +01:00
3eb452aeab fixed version 2026-02-07 17:41:09 +01:00
f46a654184 Merge branch 'dev' into debian12 2026-02-07 17:40:47 +01:00
6efb0fee80 updated modules 2026-02-07 17:39:31 +01:00
2e98fa50de refactor: update contact page message description and improve email logging 2026-02-07 17:33:16 +01:00
863409aed9 Merge branch 'dev' into debian12 2026-02-04 13:47:04 +01:00
7221ee1843 fixed message bug 2026-02-04 13:46:52 +01:00
052137a697 removed ports 2026-02-01 15:50:52 +01:00
ae0cb5af81 changed website title 2026-01-28 18:32:50 +01:00
2f3583ccd0 Merge branch 'dev' into debian12 2026-01-28 18:26:08 +01:00
80f38fcd3d fixed Admin Panel Bug: cannot change Password 2026-01-28 18:25:56 +01:00
9da72cc5bf Merge branch 'dev' into debian12 2026-01-28 13:06:19 +01:00
70f3d1fdcc added: User can return loan from web panel 2026-01-28 13:06:03 +01:00
c633627b7c Merge branch 'dev' into debian12 2026-01-28 12:44:25 +01:00
4b08a574d8 fixed Bug: redirecting
Also removed irrelavant console.logs
2026-01-28 12:43:58 +01:00
33 changed files with 644 additions and 871 deletions

View File

@@ -1,366 +1,177 @@
# Borrow System API Documentation # Borrow System API Documentation
**Frontend:** https://insta.the1s.de ## Overview
**Backend base URL:** `https://insta.the1s.de/backend/api`
--- The Borrow System API provides endpoints for managing items, loans, and door access for a borrowing/locker system. All endpoints require authentication via an 8-digit API key passed as a URL parameter.
## Authentication ## Authentication
All API endpoints require **either**: All requests must include a valid API key in the URL path as the `:key` parameter. API keys are 8-digit numeric strings.
### 1. Bearer Token (JWT)
Send an `Authorization` header:
```http
Authorization: Bearer <JWT_TOKEN>
```
- Used for user-based access.
- Token must be valid and not expired.
### 2. API Key (for devices / machine-to-machine)
Include an API key in the route as `:key` parameter:
```text
/api/.../:key/...
```
Example:
```http
GET /api/items/12345678
```
Where `12345678` is your API key.
The API key is validated server-side.
---
## Common Response Codes
- `200 OK` Request was successful.
- `401 Unauthorized` Missing or malformed credentials.
- `403 Forbidden` Credentials invalid or not allowed to access this resource.
- `404 Not Found` Resource (e.g., loan) not found.
- `500 Internal Server Error` Unexpected server error.
---
## Endpoints ## Endpoints
### 1. Get All Items The Base URL for all endpoints is: `https://insta.the1s.de/backend/api`
**GET** `/api/items/:key` ### Get All Items
Returns a list of all items. `GET /items/:key`
#### Path Parameters Returns all items in the system.
- `:key` API key (8-digit number) **Response 200:**
#### Authentication
- Either:
- Valid `Authorization: Bearer <token>`
- Or valid `:key` path parameter
#### Request Example
```http
GET /api/items/12345678 HTTP/1.1
Host: backend.insta.the1s.de
Authorization: Bearer <JWT_TOKEN>
```
#### Successful Response (200)
```json ```json
{ {
"data": [ "data": [
{ {
"id": 1, "id": 1,
"item_name": "DJI 1er Mikro", "item_name": "Laptop",
"can_borrow_role": 4, "can_borrow_role": 1,
"inSafe": 1, "in_safe": true,
"safe_nr": 3, "safe_nr": 3,
"door_key": "123", "door_key": 101,
"entry_created_at": "2025-08-19T22:02:16.000Z", "last_borrowed_person": "jdoe",
"entry_updated_at": "2025-08-19T22:02:16.000Z",
"last_borrowed_person": "alice",
"currently_borrowing": null "currently_borrowing": null
} }
] ]
} }
``` ```
#### Error Response (500) **Response 500:**
```json ```json
{ { "message": "Failed to fetch items" }
"message": "Failed to fetch items"
}
``` ```
--- ---
### 2. Toggle Item Safe State ### Change Item Safe State
Toggles `in_safe` between `0` and `1` for a given item. `POST /change-state/:key/:itemId`
**Keep in mind that when you return a loan by code, the item states are automatically updated.** Toggles the `in_safe` boolean state of an item.
**POST** `/api/change-state/:key/:itemId` **URL Parameters:**
#### Path Parameters - **key** - API key
- **itemId** - The item's ID
- `:key` API key (8-digit number) **Response 200:** Returns on successful toggle.
- `:itemId` Item ID (integer)
#### Authentication **Response 500:**
- Either Bearer token or `:key` API key.
#### Request Example
```http
POST /api/change-state/12345678/42 HTTP/1.1
Host: backend.insta.the1s.de
```
#### Successful Response (200)
```json ```json
{ { "message": "Failed to update item state" }
"data": {}
}
```
_(Implementation currently only returns `{ success: true }`, so `data` may be empty.)_
#### Error Response (500)
```json
{
"message": "Failed to update item state"
}
``` ```
--- ---
### 3. Get Loan by Code ### Get Loan by Code
Fetch loan information by `loan_code`. `GET /get-loan-by-code/:key/:loan_code`
**GET** `/api/get-loan-by-code/:key/:loan_code` Retrieves loan details by its 6-digit loan code.
#### Path Parameters **URL Parameters:**
- `:key` API key (8-digit number) - **key** - API key
- `:loan_code` Loan code (string) - **loan_code** - A 6-digit numeric loan code
#### Authentication **Response 200:**
- Either Bearer token or `:key` API key.
#### Request Example
```http
GET /api/get-loan-by-code/12345678/12345 HTTP/1.1
Host: backend.insta.the1s.de
```
#### Successful Response (200)
```json ```json
{ {
"data": { "data": {
"username": "john", "username": "jdoe",
"returned_date": null, "returned_date": null,
"take_date": "2025-01-01T10:00:00.000Z", "take_date": "2024-01-15T10:30:00.000Z",
"lockers": "[1, 2, 3]" "lockers": [1, 3]
} }
} }
``` ```
#### Error Response (404) **Response 404:**
```json ```json
{ { "message": "Loan not found" }
"message": "Loan not found"
}
``` ```
--- ---
### 4. Set Loan Return Date ### Set Take Date
Sets `returned_date = NOW()` on a loan and updates related items: `POST /set-take-date/:key/:loan_code`
- `in_safe = 1` Records when items are physically taken by setting `take_date` to the current timestamp. Updates associated items to `in_safe = false` and sets `currently_borrowing` to the loan's username.
- `currently_borrowing = NULL`
- `last_borrowed_person = username`
**POST** `/api/set-return-date/:key/:loan_code` **URL Parameters:**
#### Path Parameters - **key** - API key
- **loan_code** - A 6-digit numeric loan code
- `:key` API key (8-digit number) **Response 200:** Empty JSON object on success.
- `:loan_code` Loan code (string)
#### Authentication **Response 500:**
- Either Bearer token or `:key` API key.
#### Request Example
```http
POST /api/set-return-date/12345678/12345 HTTP/1.1
Host: backend.insta.the1s.de
```
#### Successful Response (200)
```json ```json
{ { "message": "Loan not found or already taken" }
"data": {}
}
``` ```
#### Error Response (500) > **Note:** This endpoint will fail if the loan has already been taken or does not exist.
```json
{
"message": "Failed to set return date"
}
```
--- ---
### 5. Set Loan Take Date ### Set Return Date
Sets `take_date = NOW()` on a loan and updates related items: `POST /set-return-date/:key/:loan_code`
- `in_safe = 0` Marks a loan as returned by setting `returned_date` to the current timestamp. Also updates all associated items to `in_safe = true`, clears `currently_borrowing`, and sets `last_borrowed_person`. Therefore, keep in mind that you must not call other endpoints that will change the safe state of an item after or before calling this endpoint, otherwise the state of the items will be inconsistent.
- `currently_borrowing = username`
**POST** `/api/set-take-date/:key/:loan_code` **URL Parameters:**
#### Path Parameters - **key** - API key
- **loan_code** - A 6-digit numeric loan code
- `:key` API key (8-digit number) **Response 200:** Empty JSON object on success.
- `:loan_code` Loan code (string)
#### Authentication **Response 500:**
- Either Bearer token or `:key` API key.
#### Request Example
```http
POST /api/set-take-date/12345678/LOAN-12345 HTTP/1.1
Host: backend.insta.the1s.de
```
#### Successful Response (200)
```json ```json
{ { "message": "Failed to set return date" }
"data": {}
}
``` ```
#### Error Response (500) > **Note:** This endpoint will fail if the loan has already been returned (i.e., `returned_date` is not `NULL`).
```json
{
"message": "Failed to set take date"
}
```
--- ---
### 6. Open Door by Door Key ### Open Door
Looks up an item by its `door_key`, toggles `in_safe`, and returns safe information. `GET /open-door/:key/:doorKey`
**GET** `/api/open-door/:key/:doorKey` Toggles the safe state of an item identified by its door key and returns the associated safe number.
#### Path Parameters **URL Parameters:**
- `:key` API key (8-digit number) - **key** - API key
- `:doorKey` Door key/token (string) used by hardware to identify the locker. - **doorKey** - The door key identifier assigned to an item
#### Authentication **Response 200:**
- Either Bearer token or `:key` API key.
#### Request Example
```http
GET /api/open-door/12345678/123 HTTP/1.1
Host: backend.insta.the1s.de
```
#### Successful Response (200)
```json ```json
{ {
"data": { "data": {
"safe_nr": 5, "safe_nr": 3,
"id": 42 "id": 1
} }
} }
``` ```
#### Error Response (500) **Response 500:**
```json ```json
{ { "message": "Failed to open door" }
"message": "Failed to open door"
}
``` ```
--- ## Error Handling
## Authentication Error Messages All endpoints return a `500` status code for server-side failures and a JSON body with a `message` field, except for **Get Loan by Code** which returns `404` when no matching loan is found.
### Missing credentials
Status: `401`
```json
{
"message": "Unauthorized"
}
```
### Invalid JWT
Status: `403`
```json
{
"message": "Present token invalid"
}
```
### Invalid API Key
Status: `403`
```json
{
"message": "API Key invalid"
}
```
---
## Notes
- All responses are JSON.
- Time fields like `take_date` and `returned_date` are in the format returned by MySQL (usually ISO-like strings).
- `loaned_items_id` in the database is stored as a JSON array string (e.g. `"[1,2,3]"`) and is parsed internally; clients do not interact with this field directly via current endpoints.

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontendv2</title> <title>Ausleihsystem</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -12,7 +12,7 @@ import { triggerLogoutAtom } from "@/states/Atoms";
import { MyLoansPage } from "./pages/MyLoansPage"; import { MyLoansPage } from "./pages/MyLoansPage";
import Landingpage from "./pages/Landingpage"; import Landingpage from "./pages/Landingpage";
import { changeLanguage } from "i18next"; import { changeLanguage } from "i18next";
import { Box, Flex } from "@chakra-ui/react"; import { Flex } from "@chakra-ui/react";
import { Footer } from "./components/footer/Footer"; import { Footer } from "./components/footer/Footer";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { API_BASE } from "@/config/api.config"; import { API_BASE } from "@/config/api.config";
@@ -72,8 +72,8 @@ function App() {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<Flex direction="column" minH="100vh"> <Flex direction="column" minH="100dvh">
<Box as="main" flex="1"> <Flex as="main" flex="1" direction="column">
<UserContext.Provider value={user}> <UserContext.Provider value={user}>
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
@@ -88,7 +88,7 @@ function App() {
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
</UserContext.Provider> </UserContext.Provider>
</Box> </Flex>
<Footer /> <Footer />
</Flex> </Flex>
</QueryClientProvider> </QueryClientProvider>

View File

@@ -1,50 +0,0 @@
{
"title": "Changelog",
"items": [
{
"version": "v2.1.0",
"date": "2025-10-24",
"changes": [
{
"type": "Hinzugefügt",
"text": [
"Neue Changelog-Komponente mit zentriertem Layout.",
"Unterstützung für mehrsprachige Einträge (Englisch und Deutsch)."
]
},
{
"type": "Verbessert",
"text": [
"Performance-Optimierungen beim Laden der Listenansichten.",
"Verbesserte Barrierefreiheit durch ARIA-Attribute."
]
},
{
"type": "Behoben",
"text": [
"Fehler bei der Datumsauswahl im Safari-Browser.",
"Anzeigeprobleme bei hohen DPI-Einstellungen."
]
}
]
},
{
"version": "v2.0.3",
"date": "2025-10-10",
"changes": [
{
"type": "Geändert",
"text": [
"Standard-Timeout für API-Requests auf 10s erhöht."
]
},
{
"type": "Sicherheit",
"text": [
"Abhängigkeiten aktualisiert (kritische CVEs behoben)."
]
}
]
}
]
}

View File

@@ -1,263 +0,0 @@
import { useEffect, useRef, useState } from "react";
const STORAGE_KEY = "changelog";
type ChangeType =
| "Hinzugefügt"
| "Geändert"
| "Behoben"
| "Entfernt"
| "Verbessert"
| "Sicherheit"
| "Veraltet"
| string;
type ChangeEntry = {
type: ChangeType;
text: string | string[]; // aus localStorage kann es eine Liste sein
};
type ChangelogItem = {
version?: string;
date: string;
changes: ChangeEntry[];
};
type StoredChangelog = {
title: string;
items: ChangelogItem[];
};
const typeStyles: Record<string, string> = {
Hinzugefügt:
"bg-emerald-500/15 text-emerald-300 ring-1 ring-inset ring-emerald-500/30",
Geändert: "bg-blue-500/15 text-blue-300 ring-1 ring-inset ring-blue-500/30",
Behoben: "bg-amber-500/15 text-amber-300 ring-1 ring-inset ring-amber-500/30",
Entfernt: "bg-rose-500/15 text-rose-300 ring-1 ring-inset ring-rose-500/30",
Verbessert:
"bg-indigo-500/15 text-indigo-300 ring-1 ring-inset ring-indigo-500/30",
Sicherheit: "bg-red-500/15 text-red-300 ring-1 ring-inset ring-red-500/30",
Veraltet: "bg-zinc-700/30 text-zinc-300 ring-1 ring-inset ring-zinc-600/40",
};
export default function Changelog() {
const [open, setOpen] = useState(true);
const [mounted, setMounted] = useState(false);
const [data, setData] = useState<StoredChangelog | null>(null);
const [error, setError] = useState<string | null>(null);
const cardRef = useRef<HTMLDivElement | null>(null);
useEffect(() => setMounted(true), []);
const loadFromStorage = () => {
try {
setError(null);
const raw =
typeof window !== "undefined"
? localStorage.getItem(STORAGE_KEY)
: null;
if (!raw) {
setData(null);
return;
}
const parsed = JSON.parse(raw) as StoredChangelog;
if (!parsed || !Array.isArray(parsed.items)) {
throw new Error("Ungültiges Format");
}
setData(parsed);
} catch (e) {
setError("Changelog konnte nicht aus localStorage geladen werden.");
setData(null);
}
};
useEffect(() => {
loadFromStorage();
}, []);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setOpen(false);
};
const onClickOutside = (e: MouseEvent) => {
if (cardRef.current && !cardRef.current.contains(e.target as Node)) {
setOpen(false);
}
};
const onStorage = (e: StorageEvent) => {
if (e.key === STORAGE_KEY) loadFromStorage();
};
window.addEventListener("keydown", onKey);
document.addEventListener("mousedown", onClickOutside);
window.addEventListener("storage", onStorage);
return () => {
window.removeEventListener("keydown", onKey);
document.removeEventListener("mousedown", onClickOutside);
window.removeEventListener("storage", onStorage);
};
}, []);
if (!open) return null;
const title = data?.title ?? "Changelog";
const items = data?.items ?? [];
return (
<div className="min-h-screen bg-zinc-950 bg-[radial-gradient(60%_60%_at_50%_0%,rgba(99,102,241,0.12),rgba(24,24,27,0))] flex items-center justify-center p-6">
<div
ref={cardRef}
className={[
"relative w-full max-w-6xl transition-all duration-300 ease-out",
mounted
? "opacity-100 translate-y-0 scale-100"
: "opacity-0 translate-y-1 scale-[0.99]",
].join(" ")}
aria-live="polite"
>
{/* Gradient border wrapper */}
<div className="rounded-2xl p-[1px] bg-gradient-to-b from-zinc-700/60 via-zinc-700/20 to-zinc-800/60 shadow-2xl">
{/* Card */}
<div className="relative rounded-[calc(theme(borderRadius.2xl)-1px)] border border-zinc-800/70 bg-zinc-900/70 supports-[backdrop-filter]:bg-zinc-900/60 backdrop-blur-xl ring-1 ring-white/10">
{/* Accent top line */}
<div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-indigo-500/40 to-transparent" />
{/* Close button */}
<button
aria-label="Changelog schließen"
onClick={() => setOpen(false)}
className="absolute right-3 top-3 inline-flex h-9 w-9 items-center justify-center rounded-md text-zinc-400 hover:text-zinc-100 hover:bg-zinc-800/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/70 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-900 transition"
>
<svg
viewBox="0 0 24 24"
className="h-5 w-5"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M6 6l12 12M18 6L6 18" />
</svg>
</button>
{/* Header */}
<header className="px-10 pt-8 pb-6 border-b border-zinc-800/70">
<div className="flex items-center gap-3">
<div className="inline-flex h-9 w-9 items-center justify-center rounded-lg bg-indigo-500/15 text-indigo-300 ring-1 ring-inset ring-indigo-500/30">
<svg
viewBox="0 0 24 24"
className="h-5 w-5"
fill="none"
stroke="currentColor"
strokeWidth={1.6}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1" />
</svg>
</div>
<div>
<h1 className="text-[30px] leading-8 font-semibold text-zinc-100 tracking-[-0.01em]">
{title}
</h1>
<p className="text-sm text-zinc-400">
Aktuelle Änderungen und Updates
</p>
</div>
</div>
</header>
{/* Body */}
<div className="relative max-h-[78vh] overflow-y-auto">
<div className="absolute pointer-events-none inset-x-0 top-0 h-8 bg-gradient-to-b from-zinc-900/70 to-transparent" />
<div className="absolute pointer-events-none inset-x-0 bottom-0 h-10 bg-gradient-to-t from-zinc-900/80 to-transparent" />
{error && (
<div className="px-10 py-8">
<div className="rounded-lg border border-red-900/40 bg-red-900/10 px-4 py-3 text-sm text-red-300">
{error}
</div>
</div>
)}
{!error && items.length === 0 && (
<div className="px-10 py-16 text-center">
<p className="text-zinc-400">
Kein Changelog im localStorage gefunden (Key: {STORAGE_KEY}
).
</p>
</div>
)}
<ul className="divide-y divide-zinc-800/70">
{items.map((entry, idx) => (
<li
key={`${entry.version ?? entry.date}-${idx}`}
className="px-10 py-8"
>
{/* Kopfzeile je Release */}
<div className="flex flex-wrap items-baseline gap-x-4 gap-y-2">
{entry.version && (
<span className="inline-flex items-center rounded-md bg-gradient-to-b from-zinc-100 to-zinc-300 text-zinc-900 px-3 py-0.5 text-sm font-semibold shadow-sm">
{entry.version}
</span>
)}
<time
className="text-sm text-zinc-400"
dateTime={entry.date}
>
{new Date(entry.date).toLocaleDateString("de-DE", {
year: "numeric",
month: "long",
day: "2-digit",
})}
</time>
</div>
{/* Zweispaltiges Layout: Typ links, Text rechts (mit schöner Leselänge) */}
<dl
role="list"
className="mt-6 grid grid-cols-1 gap-x-8 gap-y-3 md:grid-cols-[max-content_1fr]"
>
{entry.changes.map((c, i) => (
<div key={i} className="contents">
<dt className="md:w-44 md:justify-end md:text-right">
<span
className={`inline-flex items-center rounded-md px-2 py-0.5 text-[11px] font-medium ${
typeStyles[c.type] ??
"bg-zinc-700/30 text-zinc-300 ring-1 ring-inset ring-zinc-600/40"
}`}
>
{c.type}
</span>
</dt>
<dd className="max-w-[74ch] text-[15px] leading-7 text-zinc-200 tracking-[0.005em]">
{Array.isArray(c.text) ? (
<ul className="ml-4 list-disc marker:text-zinc-500/70 space-y-1.5">
{c.text.map((t, k) => (
<li key={k} className="break-words">
{t}
</li>
))}
</ul>
) : (
<p className="break-words">{c.text}</p>
)}
</dd>
</div>
))}
</dl>
</li>
))}
</ul>
</div>
{/* soft bottom glow */}
<div className="pointer-events-none absolute inset-x-12 -bottom-4 h-8 blur-2xl bg-indigo-600/20 rounded-full" />
</div>
</div>
</div>
</div>
);
}

View File

@@ -142,7 +142,7 @@ export const Header = () => {
value="help" value="help"
onSelect={() => onSelect={() =>
window.open( window.open(
"https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki", "https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki/?action=_pages",
"_blank", "_blank",
"noopener,noreferrer", "noopener,noreferrer",
) )
@@ -279,7 +279,7 @@ export const Header = () => {
</Button> </Button>
<a <a
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki" href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki/?action=_pages"
target="_blank" target="_blank"
> >
<Button variant="ghost"> <Button variant="ghost">

View File

@@ -9,10 +9,9 @@ export const Footer = () => {
as="footer" as="footer"
py={4} py={4}
textAlign="center" textAlign="center"
position="fixed" width="100%"
bottom="0" flexShrink={0}
left="0" fontSize="sm"
right="0"
> >
Made with by Theis Gaedigk - Class of 2019 at MCS-Bochum Made with by Theis Gaedigk - Class of 2019 at MCS-Bochum
<br /> <br />

View File

@@ -1,4 +1,11 @@
import { Field, Textarea, Button, Alert, Container } from "@chakra-ui/react"; import {
Field,
Textarea,
Button,
Alert,
Container,
Text,
} from "@chakra-ui/react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useState } from "react"; import { useState } from "react";
import { API_BASE } from "@/config/api.config"; import { API_BASE } from "@/config/api.config";
@@ -49,7 +56,7 @@ export const ContactPage = () => {
<Header /> <Header />
<Field.Root invalid={message === ""}> <Field.Root invalid={message === ""}>
<Field.Label> <Field.Label>
{t("contactPage_messageLabel")} <Text>{t("contactPage_messageDescription")}</Text>
<Field.RequiredIndicator /> <Field.RequiredIndicator />
</Field.Label> </Field.Label>
<Textarea <Textarea

View File

@@ -108,7 +108,6 @@ export const HomePage = () => {
} }
setBorrowableItems(response.data); setBorrowableItems(response.data);
setIsMsg(false); setIsMsg(false);
console.log(borrowableItems);
}); });
}} }}
> >

View File

@@ -9,12 +9,13 @@ import {
Card, Card,
SimpleGrid, SimpleGrid,
Button, Button,
Container,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import MyAlert from "@/components/myChakra/MyAlert"; import MyAlert from "@/components/myChakra/MyAlert";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { API_BASE } from "@/config/api.config"; import { API_BASE } from "@/config/api.config";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { useNavigate } from "react-router-dom"; import { Header } from "@/components/Header";
export const formatDateTime = (value: string | null | undefined) => { export const formatDateTime = (value: string | null | undefined) => {
if (!value) return "N/A"; if (!value) return "N/A";
@@ -32,6 +33,7 @@ type Loan = {
returned_date: string | null; returned_date: string | null;
take_date: string | null; take_date: string | null;
loaned_items_name: string[] | string; loaned_items_name: string[] | string;
note: string | null;
}; };
type Device = { type Device = {
@@ -46,7 +48,6 @@ type Device = {
const Landingpage: React.FC = () => { const Landingpage: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [loans, setLoans] = useState<Loan[]>([]); const [loans, setLoans] = useState<Loan[]>([]);
@@ -59,7 +60,7 @@ const Landingpage: React.FC = () => {
const setError = ( const setError = (
status: "error" | "success", status: "error" | "success",
message: string, message: string,
description: string description: string,
) => { ) => {
setIsError(false); setIsError(false);
setErrorStatus(status); setErrorStatus(status);
@@ -85,7 +86,7 @@ const Landingpage: React.FC = () => {
setError( setError(
"error", "error",
t("error-by-loading"), t("error-by-loading"),
t("unexpected-date-format_loan") t("unexpected-date-format_loan"),
); );
} }
@@ -102,7 +103,7 @@ const Landingpage: React.FC = () => {
setError( setError(
"error", "error",
t("error-by-loading"), t("error-by-loading"),
t("unexpected-date-format_device") t("unexpected-date-format_device"),
); );
} }
} catch (e) { } catch (e) {
@@ -115,14 +116,8 @@ const Landingpage: React.FC = () => {
}, []); }, []);
return ( return (
<> <Container className="px-6 sm:px-8 pt-10">
<Heading as="h1" size="lg" mb={2}> <Header />
Matthias-Claudius-Schule Technik
</Heading>
<Button onClick={() => navigate("/", { replace: true })}>
{t("back")}
</Button>
<Heading as="h2" size="md" mb={4}> <Heading as="h2" size="md" mb={4}>
{t("all-loans")} {t("all-loans")}
@@ -168,6 +163,9 @@ const Landingpage: React.FC = () => {
<Table.ColumnHeader> <Table.ColumnHeader>
<strong>{t("return-date")}</strong> <strong>{t("return-date")}</strong>
</Table.ColumnHeader> </Table.ColumnHeader>
<Table.ColumnHeader>
<strong>{t("note")}</strong>
</Table.ColumnHeader>
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
@@ -184,6 +182,7 @@ const Landingpage: React.FC = () => {
</Table.Cell> </Table.Cell>
<Table.Cell>{formatDateTime(loan.take_date)}</Table.Cell> <Table.Cell>{formatDateTime(loan.take_date)}</Table.Cell>
<Table.Cell>{formatDateTime(loan.returned_date)}</Table.Cell> <Table.Cell>{formatDateTime(loan.returned_date)}</Table.Cell>
<Table.Cell>{loan.note}</Table.Cell>
</Table.Row> </Table.Row>
))} ))}
</Table.Body> </Table.Body>
@@ -260,7 +259,7 @@ const Landingpage: React.FC = () => {
</HStack> </HStack>
</Button> </Button>
</HStack> </HStack>
</> </Container>
); );
}; };

View File

@@ -4,10 +4,9 @@ import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms"; import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { Navigate, useNavigate } from "react-router-dom"; import { Navigate, useNavigate, useLocation } from "react-router-dom";
import { PasswordInput } from "@/components/ui/password-input"; import { PasswordInput } from "@/components/ui/password-input";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Footer } from "@/components/footer/Footer";
import { API_BASE } from "@/config/api.config"; import { API_BASE } from "@/config/api.config";
export const LoginPage = () => { export const LoginPage = () => {
@@ -16,13 +15,15 @@ export const LoginPage = () => {
const [isLoggedIn, setIsLoggedIn] = useAtom(setIsLoggedInAtom); const [isLoggedIn, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
const [triggerLogout, setTriggerLogout] = useAtom(triggerLogoutAtom); const [triggerLogout, setTriggerLogout] = useAtom(triggerLogoutAtom);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from?.pathname || "/";
useEffect(() => { useEffect(() => {
if (isLoggedIn) { if (isLoggedIn) {
navigate("/", { replace: true }); navigate(from, { replace: true });
window.location.reload(); // Wenn entfernt: Seite bleibt schwarz und muss manuell neu geladen werden window.location.reload(); // if deleted, the user context is not updated in time
} }
}, [isLoggedIn, navigate]); }, [isLoggedIn, navigate, from]);
const loginFnc = async (username: string, password: string) => { const loginFnc = async (username: string, password: string) => {
const response = await fetch(`${API_BASE}/api/users/login`, { const response = await fetch(`${API_BASE}/api/users/login`, {
@@ -61,15 +62,15 @@ export const LoginPage = () => {
return; return;
} }
setTriggerLogout(false); setTriggerLogout(false);
navigate("/", { replace: true }); navigate(from, { replace: true });
}; };
if (isLoggedIn) { if (isLoggedIn) {
return <Navigate to="/" replace />; return <Navigate to={from} replace />;
} }
return ( return (
<div className="min-h-screen flex items-center justify-center p-4"> <div className="flex flex-1 items-center justify-center p-4">
<form onSubmit={(e) => e.preventDefault()}> <form onSubmit={(e) => e.preventDefault()}>
<Card.Root maxW="sm"> <Card.Root maxW="sm">
<Card.Header> <Card.Header>
@@ -113,7 +114,6 @@ export const LoginPage = () => {
</Card.Footer> </Card.Footer>
</Card.Root> </Card.Root>
</form> </form>
<Footer />
</div> </div>
); );
}; };

View File

@@ -112,6 +112,86 @@ export const MyLoansPage = () => {
return `${d}.${M}.${y} ${h}:${min}`; return `${d}.${M}.${y} ${h}:${min}`;
}; };
const handleTakeAction = async (loanCode: string) => {
try {
const res = await fetch(
`${API_BASE}/api/loans/set-take-date/${loanCode}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
},
);
if (!res.ok) {
setMsgStatus("error");
setMsgTitle(t("error"));
setMsgDescription(t("error-take-loan"));
setIsMsg(true);
return;
}
// Update the loan in state
setLoans((prev) =>
prev.map((loan) =>
loan.loan_code === loanCode
? { ...loan, take_date: new Date().toISOString() }
: loan,
),
);
setMsgStatus("success");
setMsgTitle(t("success"));
setMsgDescription(t("take-loan-success"));
setIsMsg(true);
} catch (e) {
setMsgStatus("error");
setMsgTitle(t("error"));
setMsgDescription(t("network-error"));
setIsMsg(true);
}
};
const handleReturnAction = async (loanCode: string) => {
try {
const res = await fetch(
`${API_BASE}/api/loans/set-return-date/${loanCode}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
},
);
if (!res.ok) {
setMsgStatus("error");
setMsgTitle(t("error"));
setMsgDescription(t("error-return-loan"));
setIsMsg(true);
return;
}
// Update the loan in state
setLoans((prev) =>
prev.map((loan) =>
loan.loan_code === loanCode
? { ...loan, returned_date: new Date().toISOString() }
: loan,
),
);
setMsgStatus("success");
setMsgTitle(t("success"));
setMsgDescription(t("return-loan-success"));
setIsMsg(true);
} catch (e) {
setMsgStatus("error");
setMsgTitle(t("error"));
setMsgDescription(t("network-error"));
setIsMsg(true);
}
};
return ( return (
<> <>
<Container className="px-6 sm:px-8 pt-10"> <Container className="px-6 sm:px-8 pt-10">
@@ -190,8 +270,33 @@ export const MyLoansPage = () => {
: "-"} : "-"}
</Text> </Text>
</Table.Cell> </Table.Cell>
<Table.Cell>{formatDate(loan.take_date)}</Table.Cell> <Table.Cell>
<Table.Cell>{formatDate(loan.returned_date)}</Table.Cell> {loan.take_date ? (
formatDate(loan.take_date)
) : (
<Button
size="xs"
colorPalette="teal"
onClick={() => handleTakeAction(loan.loan_code)}
>
{t("take")}
</Button>
)}
</Table.Cell>
<Table.Cell>
{loan.returned_date ? (
formatDate(loan.returned_date)
) : (
<Button
size="xs"
colorPalette="blue"
onClick={() => handleReturnAction(loan.loan_code)}
disabled={!loan.take_date}
>
{t("return")}
</Button>
)}
</Table.Cell>
<Table.Cell>{loan.note}</Table.Cell> <Table.Cell>{loan.note}</Table.Cell>
<Table.Cell> <Table.Cell>
<Dialog.Root role="alertdialog"> <Dialog.Root role="alertdialog">

View File

@@ -3,7 +3,7 @@ import { API_BASE } from "@/config/api.config";
export const getBorrowableItems = async ( export const getBorrowableItems = async (
startDate: string, startDate: string,
endDate: string endDate: string,
) => { ) => {
try { try {
const response = await fetch(`${API_BASE}/api/loans/borrowable-items`, { const response = await fetch(`${API_BASE}/api/loans/borrowable-items`, {
@@ -22,7 +22,7 @@ export const getBorrowableItems = async (
status: "error", status: "error",
title: "Server error", title: "Server error",
description: description:
"Ein Fehler ist auf dem Server aufgetreten. Manchmal hilft es, die Seite neu zu laden.", "An error occurred on the server. Sometimes reloading the page helps. Otherwise, please contact the administrator.",
}; };
} }
@@ -48,7 +48,7 @@ export const createLoan = async (
itemIds: number[], itemIds: number[],
startDate: string, startDate: string,
endDate: string, endDate: string,
note: string | null note: string | null,
) => { ) => {
const response = await fetch(`${API_BASE}/api/loans/createLoan`, { const response = await fetch(`${API_BASE}/api/loans/createLoan`, {
method: "POST", method: "POST",

View File

@@ -81,5 +81,12 @@
"contactPage_messageLabel": "Nachricht", "contactPage_messageLabel": "Nachricht",
"contactPage_messagePlaceholder": "Geben Sie hier Ihre Nachricht ein...", "contactPage_messagePlaceholder": "Geben Sie hier Ihre Nachricht ein...",
"contactPage_messageErrorText": "Dieses Feld darf nicht leer sein.", "contactPage_messageErrorText": "Dieses Feld darf nicht leer sein.",
"contact": "Kontakt" "contact": "Kontakt",
"take": "Abholen",
"return": "Zurückgeben",
"serverError": "Serverfehler. Bitte versuchen Sie es später erneut, oder laden Sie die Seite neu.",
"take-loan-success": "Ausleihe erfolgreich abgeholt",
"return-loan-success": "Ausleihe erfolgreich zurückgegeben",
"network-error": "Netzwerkfehler. Kontaktieren Sie den Administrator.",
"contactPage_messageDescription": "Bitte geben Sie hier Ihre Nachricht ein. Der Systemadministrator (Theis Gaedigk) wird sich so schnell wie möglich bei Ihnen melden."
} }

View File

@@ -81,5 +81,12 @@
"contactPage_messageLabel": "Message", "contactPage_messageLabel": "Message",
"contactPage_messagePlaceholder": "Enter your message here...", "contactPage_messagePlaceholder": "Enter your message here...",
"contactPage_messageErrorText": "This field cannot be empty.", "contactPage_messageErrorText": "This field cannot be empty.",
"contact": "Contact" "contact": "Contact",
"serverError": "Server error. Please try again later, or refresh the page.",
"take": "Take",
"return": "Return",
"take-loan-success": "Loan taken successfully",
"return-loan-success": "Loan returned successfully",
"network-error": "Network error. Please contact the administrator.",
"contactPage_messageDescription": "Please enter your message here. The system administrator (Theis Gaedigk) will get back to you as soon as possible."
} }

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />

View File

@@ -3675,12 +3675,16 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie": { "node_modules/cookie": {
"version": "1.0.2", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/cosmiconfig": { "node_modules/cosmiconfig": {
@@ -4466,9 +4470,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
@@ -4904,9 +4908,9 @@
} }
}, },
"node_modules/minizlib": { "node_modules/minizlib": {
"version": "3.0.2", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"minipass": "^7.1.2" "minipass": "^7.1.2"
@@ -4915,21 +4919,6 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/mkdirp": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
"license": "MIT",
"bin": {
"mkdirp": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -5307,9 +5296,9 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "7.8.2", "version": "7.13.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.2.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz",
"integrity": "sha512-7M2fR1JbIZ/jFWqelpvSZx+7vd7UlBTfdZqf6OSdF9g6+sfdqJDAWcak6ervbHph200ePlu+7G8LdoiC3ReyAQ==", "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cookie": "^1.0.1", "cookie": "^1.0.1",
@@ -5329,12 +5318,12 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "7.8.2", "version": "7.13.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.2.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz",
"integrity": "sha512-Z4VM5mKDipal2jQ385H6UBhiiEDlnJPx6jyWsTYoZQdl5TrjxEV2a9yl3Fi60NBJxYzOTGTTHXPi0pdizvTwow==", "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"react-router": "7.8.2" "react-router": "7.13.0"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
@@ -5492,9 +5481,9 @@
} }
}, },
"node_modules/set-cookie-parser": { "node_modules/set-cookie-parser": {
"version": "2.7.1", "version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/shebang-command": { "node_modules/shebang-command": {
@@ -5649,16 +5638,15 @@
} }
}, },
"node_modules/tar": { "node_modules/tar": {
"version": "7.4.3", "version": "7.5.7",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
"license": "ISC", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"@isaacs/fs-minipass": "^4.0.0", "@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0", "chownr": "^3.0.0",
"minipass": "^7.1.2", "minipass": "^7.1.2",
"minizlib": "^3.0.1", "minizlib": "^3.1.0",
"mkdirp": "^3.0.1",
"yallist": "^5.0.0" "yallist": "^5.0.0"
}, },
"engines": { "engines": {

View File

@@ -3,6 +3,7 @@ import { useState } from "react";
import { loginFunc } from "@/utils/loginUser"; import { loginFunc } from "@/utils/loginUser";
import MyAlert from "../components/myChakra/MyAlert"; import MyAlert from "../components/myChakra/MyAlert";
import { Button, Card, Field, Input, Stack } from "@chakra-ui/react"; import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
import { PasswordInput } from "@/components/ui/password-input";
const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => { const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
@@ -43,8 +44,7 @@ const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
</Field.Root> </Field.Root>
<Field.Root> <Field.Root>
<Field.Label>password</Field.Label> <Field.Label>password</Field.Label>
<Input <PasswordInput
type="password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
/> />

View File

@@ -63,7 +63,6 @@ const APIKeyTable: React.FC = () => {
} }
); );
const data = await response.json(); const data = await response.json();
console.log(data);
return data; return data;
} catch (error) { } catch (error) {
setError("error", "Failed to fetch items", "There is an error"); setError("error", "Failed to fetch items", "There is an error");

View File

@@ -85,7 +85,6 @@ const UserTable: React.FC = () => {
setIsLoading(true); setIsLoading(true);
try { try {
const data = await fetchUserData(); const data = await fetchUserData();
console.log(data);
if (Array.isArray(data)) { if (Array.isArray(data)) {
setUsers(data); setUsers(data);
} else { } else {

View File

@@ -0,0 +1,159 @@
"use client"
import type {
ButtonProps,
GroupProps,
InputProps,
StackProps,
} from "@chakra-ui/react"
import {
Box,
HStack,
IconButton,
Input,
InputGroup,
Stack,
mergeRefs,
useControllableState,
} from "@chakra-ui/react"
import * as React from "react"
import { LuEye, LuEyeOff } from "react-icons/lu"
export interface PasswordVisibilityProps {
/**
* The default visibility state of the password input.
*/
defaultVisible?: boolean
/**
* The controlled visibility state of the password input.
*/
visible?: boolean
/**
* Callback invoked when the visibility state changes.
*/
onVisibleChange?: (visible: boolean) => void
/**
* Custom icons for the visibility toggle button.
*/
visibilityIcon?: { on: React.ReactNode; off: React.ReactNode }
}
export interface PasswordInputProps
extends InputProps,
PasswordVisibilityProps {
rootProps?: GroupProps
}
export const PasswordInput = React.forwardRef<
HTMLInputElement,
PasswordInputProps
>(function PasswordInput(props, ref) {
const {
rootProps,
defaultVisible,
visible: visibleProp,
onVisibleChange,
visibilityIcon = { on: <LuEye />, off: <LuEyeOff /> },
...rest
} = props
const [visible, setVisible] = useControllableState({
value: visibleProp,
defaultValue: defaultVisible || false,
onChange: onVisibleChange,
})
const inputRef = React.useRef<HTMLInputElement>(null)
return (
<InputGroup
endElement={
<VisibilityTrigger
disabled={rest.disabled}
onPointerDown={(e) => {
if (rest.disabled) return
if (e.button !== 0) return
e.preventDefault()
setVisible(!visible)
}}
>
{visible ? visibilityIcon.off : visibilityIcon.on}
</VisibilityTrigger>
}
{...rootProps}
>
<Input
{...rest}
ref={mergeRefs(ref, inputRef)}
type={visible ? "text" : "password"}
/>
</InputGroup>
)
})
const VisibilityTrigger = React.forwardRef<HTMLButtonElement, ButtonProps>(
function VisibilityTrigger(props, ref) {
return (
<IconButton
tabIndex={-1}
ref={ref}
me="-2"
aspectRatio="square"
size="sm"
variant="ghost"
height="calc(100% - {spacing.2})"
aria-label="Toggle password visibility"
{...props}
/>
)
},
)
interface PasswordStrengthMeterProps extends StackProps {
max?: number
value: number
}
export const PasswordStrengthMeter = React.forwardRef<
HTMLDivElement,
PasswordStrengthMeterProps
>(function PasswordStrengthMeter(props, ref) {
const { max = 4, value, ...rest } = props
const percent = (value / max) * 100
const { label, colorPalette } = getColorPalette(percent)
return (
<Stack align="flex-end" gap="1" ref={ref} {...rest}>
<HStack width="full" {...rest}>
{Array.from({ length: max }).map((_, index) => (
<Box
key={index}
height="1"
flex="1"
rounded="sm"
data-selected={index < value ? "" : undefined}
layerStyle="fill.subtle"
colorPalette="gray"
_selected={{
colorPalette,
layerStyle: "fill.solid",
}}
/>
))}
</HStack>
{label && <HStack textStyle="xs">{label}</HStack>}
</Stack>
)
})
function getColorPalette(percent: number) {
switch (true) {
case percent < 33:
return { label: "Low", colorPalette: "red" }
case percent < 66:
return { label: "Medium", colorPalette: "orange" }
default:
return { label: "High", colorPalette: "green" }
}
}

View File

@@ -167,7 +167,6 @@ export const createItem = async (
can_borrow_role: number, can_borrow_role: number,
lockerNumber: string | null lockerNumber: string | null
) => { ) => {
console.log(JSON.stringify({ item_name, can_borrow_role, lockerNumber }));
try { try {
const response = await fetch( const response = await fetch(
`${API_BASE}/api/admin/item-data/create-item`, `${API_BASE}/api/admin/item-data/create-item`,

View File

@@ -1,10 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ESNext", "target": "ES2022",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"], "lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext", "module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
@@ -23,14 +24,10 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true, "noUncheckedSideEffectImports": true,
/* Chakra / Pfad Aliases */ /* Path aliases */
"baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
}, }
"forceConsistentCasingInFileNames": true,
"ignoreDeprecations": "5.0"
}, },
"include": ["src"] "include": ["src"]
} }

View File

@@ -1,11 +1,11 @@
{ {
"backend-info": { "backend-info": {
"version": "v2.0.1" "version": "v2.1.1"
}, },
"frontend-info": { "frontend-info": {
"version": "v2.0" "version": "v2.1.2"
}, },
"admin-panel-info": { "admin-panel-info": {
"version": "v1.3" "version": "v1.3.2"
} }
} }

View File

@@ -18,11 +18,11 @@ export const createUser = async (
isAdmin, isAdmin,
email, email,
first_name, first_name,
last_name last_name,
) => { ) => {
const [result] = await pool.query( const [result] = await pool.query(
"INSERT INTO users (username, role, password, is_admin, email, first_name, last_name) VALUES (?, ?, ?, ?, ?, ?, ?)", "INSERT INTO users (username, role, password, is_admin, email, first_name, last_name) VALUES (?, ?, ?, ?, ?, ?, ?)",
[username, role, password, isAdmin, email, first_name, last_name] [username, role, password, isAdmin, email, first_name, last_name],
); );
if (result.affectedRows > 0) return { success: true }; if (result.affectedRows > 0) return { success: true };
return { success: false }; return { success: false };
@@ -34,10 +34,10 @@ export const deleteUserById = async (userId) => {
return { success: false }; return { success: false };
}; };
export const changePassword = async (userId, newPassword) => { export const changePassword = async (username, newPassword) => {
const [result] = await pool.query( const [result] = await pool.query(
"UPDATE users SET password = ?, entry_updated_at = NOW() WHERE id = ?", "UPDATE users SET password = ?, entry_updated_at = NOW() WHERE username = ?",
[newPassword, userId] [newPassword, username],
); );
if (result.affectedRows > 0) return { success: true }; if (result.affectedRows > 0) return { success: true };
return { success: false }; return { success: false };
@@ -49,11 +49,11 @@ export const editUserById = async (
last_name, last_name,
role, role,
email, email,
is_admin is_admin,
) => { ) => {
const [result] = await pool.query( const [result] = await pool.query(
"UPDATE users SET first_name = ?, last_name = ?, role = ?, email = ?, is_admin = ?, entry_updated_at = NOW() WHERE id = ?", "UPDATE users SET first_name = ?, last_name = ?, role = ?, email = ?, is_admin = ?, entry_updated_at = NOW() WHERE id = ?",
[first_name, last_name, role, email, is_admin, userId] [first_name, last_name, role, email, is_admin, userId],
); );
if (result.affectedRows > 0) return { success: true }; if (result.affectedRows > 0) return { success: true };
return { success: false }; return { success: false };
@@ -61,7 +61,7 @@ export const editUserById = async (
export const getAllUsers = async () => { export const getAllUsers = async () => {
const [result] = await pool.query( const [result] = await pool.query(
"SELECT id, username, first_name, last_name, role, email, is_admin, entry_created_at, entry_updated_at FROM users" "SELECT id, username, first_name, last_name, role, email, is_admin, entry_created_at, entry_updated_at FROM users",
); );
if (result.length > 0) return { success: true, data: result }; if (result.length > 0) return { success: true, data: result };
return { success: false }; return { success: false };
@@ -70,7 +70,7 @@ export const getAllUsers = async () => {
export const getUserById = async (userId) => { export const getUserById = async (userId) => {
const [rows] = await pool.query( const [rows] = await pool.query(
"SELECT id, username, first_name, last_name, role, email, is_admin FROM users WHERE id = ?", "SELECT id, username, first_name, last_name, role, email, is_admin FROM users WHERE id = ?",
[userId] [userId],
); );
if (rows.length === 0) { if (rows.length === 0) {
return { success: false }; return { success: false };

View File

@@ -22,7 +22,7 @@ export const getItemsFromDatabaseV2 = async () => {
export const getLoanByCodeV2 = async (loan_code) => { export const getLoanByCodeV2 = async (loan_code) => {
const [result] = await pool.query( const [result] = await pool.query(
"SELECT username, returned_date, take_date, lockers FROM loans WHERE loan_code = ?;", "SELECT username, returned_date, take_date, lockers FROM loans WHERE loan_code = ?;",
[loan_code] [loan_code],
); );
if (result.length > 0) { if (result.length > 0) {
return { success: true, data: result[0] }; return { success: true, data: result[0] };
@@ -33,7 +33,7 @@ export const getLoanByCodeV2 = async (loan_code) => {
export const changeInSafeStateV2 = async (itemId) => { export const changeInSafeStateV2 = async (itemId) => {
const [result] = await pool.query( const [result] = await pool.query(
"UPDATE items SET in_safe = NOT in_safe WHERE id = ?", "UPDATE items SET in_safe = NOT in_safe WHERE id = ?",
[itemId] [itemId],
); );
if (result.affectedRows > 0) { if (result.affectedRows > 0) {
return { success: true }; return { success: true };
@@ -42,50 +42,62 @@ export const changeInSafeStateV2 = async (itemId) => {
}; };
export const setReturnDateV2 = async (loanCode) => { export const setReturnDateV2 = async (loanCode) => {
try {
const [items] = await pool.query( const [items] = await pool.query(
"SELECT loaned_items_id FROM loans WHERE loan_code = ?", "SELECT loaned_items_id, username FROM loans WHERE loan_code = ?",
[loanCode] [loanCode],
); );
const [owner] = await pool.query( if (items.length === 0)
"SELECT username FROM loans WHERE loan_code = ?", return { success: false, message: "No items found for loan" };
[loanCode]
);
if (items.length === 0) return { success: false };
const itemIds = Array.isArray(items[0].loaned_items_id) const itemIds = Array.isArray(items[0].loaned_items_id)
? items[0].loaned_items_id ? items[0].loaned_items_id
: JSON.parse(items[0].loaned_items_id || "[]"); : JSON.parse(items[0].loaned_items_id || "[]");
const [setItemStates] = await pool.query(
"UPDATE items SET in_safe = 1, currently_borrowing = NULL, last_borrowed_person = (?) WHERE id IN (?)",
[owner[0].username, itemIds]
);
const [result] = await pool.query( const [result] = await pool.query(
"UPDATE loans SET returned_date = NOW() WHERE loan_code = ?", "UPDATE loans SET returned_date = NOW() WHERE loan_code = ? AND returned_date IS NULL",
[loanCode] [loanCode],
); );
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) { if (result.affectedRows === 0) return { success: false };
return { success: true };
if (itemIds.length > 0) {
await pool.query(
"UPDATE items SET in_safe = 1, currently_borrowing = NULL, last_borrowed_person = ? WHERE id IN (?)",
[items[0].username, itemIds],
);
}
return { success: true, data: { returned: true } };
} catch (error) {
console.error("setReturnDateV2 error:", error);
return { success: false, message: "Failed to set return date" };
} }
return { success: false };
}; };
export const setTakeDateV2 = async (loanCode) => { export const setTakeDateV2 = async (loanCode) => {
const [isTaken] = await pool.query(
"SELECT take_date FROM loans WHERE loan_code = ?",
[loanCode],
);
if (isTaken.length === 0 || isTaken[0].take_date !== null) {
return { success: false, message: "Loan not found or already taken" };
}
const [items] = await pool.query( const [items] = await pool.query(
"SELECT loaned_items_id FROM loans WHERE loan_code = ?", "SELECT loaned_items_id FROM loans WHERE loan_code = ?",
[loanCode] [loanCode],
); );
const [owner] = await pool.query( const [owner] = await pool.query(
"SELECT username FROM loans WHERE loan_code = ?", "SELECT username FROM loans WHERE loan_code = ?",
[loanCode] [loanCode],
); );
if (items.length === 0) return { success: false }; if (items.length === 0)
return { success: false, message: "No items found for loan" };
const itemIds = Array.isArray(items[0].loaned_items_id) const itemIds = Array.isArray(items[0].loaned_items_id)
? items[0].loaned_items_id ? items[0].loaned_items_id
@@ -93,18 +105,18 @@ export const setTakeDateV2 = async (loanCode) => {
const [setItemStates] = await pool.query( const [setItemStates] = await pool.query(
"UPDATE items SET in_safe = 0, currently_borrowing = (?) WHERE id IN (?)", "UPDATE items SET in_safe = 0, currently_borrowing = (?) WHERE id IN (?)",
[owner[0].username, itemIds] [owner[0].username, itemIds],
); );
const [result] = await pool.query( const [result] = await pool.query(
"UPDATE loans SET take_date = NOW() WHERE loan_code = ?", "UPDATE loans SET take_date = NOW() WHERE loan_code = ? AND take_date IS NULL",
[loanCode] [loanCode],
); );
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) { if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
return { success: true }; return { success: true };
} }
return { success: false }; return { message: "Failed to set take date", success: false };
}; };
export const getAllLoansV2 = async () => { export const getAllLoansV2 = async () => {
@@ -118,12 +130,12 @@ export const getAllLoansV2 = async () => {
export const openDoor = async (doorKey) => { export const openDoor = async (doorKey) => {
const [result] = await pool.query( const [result] = await pool.query(
"SELECT safe_nr, id FROM items WHERE door_key = ?;", "SELECT safe_nr, id FROM items WHERE door_key = ?;",
[doorKey] [doorKey],
); );
if (result.length > 0) { if (result.length > 0) {
const [changeItemSate] = await pool.query( const [changeItemSate] = await pool.query(
"UPDATE items SET in_safe = NOT in_safe WHERE id = ?", "UPDATE items SET in_safe = NOT in_safe WHERE id = ?",
[result[0].id] [result[0].id],
); );
if (changeItemSate.affectedRows > 0) { if (changeItemSate.affectedRows > 0) {
return { success: true, data: result[0] }; return { success: true, data: result[0] };

View File

@@ -47,7 +47,7 @@ router.get(
} else { } else {
res.status(404).json({ message: "Loan not found" }); res.status(404).json({ message: "Loan not found" });
} }
} },
); );
// Route for API to set the return date by the loan code // Route for API to set the return date by the loan code
@@ -58,11 +58,11 @@ router.post(
const loanCode = req.params.loan_code; const loanCode = req.params.loan_code;
const result = await setReturnDateV2(loanCode); const result = await setReturnDateV2(loanCode);
if (result.success) { if (result.success) {
res.status(200).json({ data: result.data }); res.status(200).json({});
} else { } else {
res.status(500).json({ message: "Failed to set return date" }); res.status(500).json({ message: "Failed to set return date" });
} }
} },
); );
// Route for API to set the take away date by the loan code // Route for API to set the take away date by the loan code
@@ -73,11 +73,11 @@ router.post(
const loanCode = req.params.loan_code; const loanCode = req.params.loan_code;
const result = await setTakeDateV2(loanCode); const result = await setTakeDateV2(loanCode);
if (result.success) { if (result.success) {
res.status(200).json({ data: result.data }); res.status(200).json({});
} else { } else {
res.status(500).json({ message: "Failed to set take date" }); res.status(500).json({ message: result.message });
}
} }
},
); );
// Route for API to open a door // Route for API to open a door

View File

@@ -16,7 +16,7 @@ export const createLoanInDatabase = async (
startDate, startDate,
endDate, endDate,
note, note,
itemIds itemIds,
) => { ) => {
if (!username) if (!username)
return { success: false, code: "BAD_REQUEST", message: "Missing username" }; return { success: false, code: "BAD_REQUEST", message: "Missing username" };
@@ -52,7 +52,7 @@ export const createLoanInDatabase = async (
// Ensure all items exist and collect names + lockers // Ensure all items exist and collect names + lockers
const [itemsRows] = await conn.query( const [itemsRows] = await conn.query(
"SELECT id, item_name, safe_nr FROM items WHERE id IN (?)", "SELECT id, item_name, safe_nr FROM items WHERE id IN (?)",
[itemIds] [itemIds],
); );
if (!itemsRows || itemsRows.length !== itemIds.length) { if (!itemsRows || itemsRows.length !== itemIds.length) {
await conn.rollback(); await conn.rollback();
@@ -65,7 +65,7 @@ export const createLoanInDatabase = async (
const itemNames = itemIds const itemNames = itemIds
.map( .map(
(id) => itemsRows.find((r) => Number(r.id) === Number(id))?.item_name (id) => itemsRows.find((r) => Number(r.id) === Number(id))?.item_name,
) )
.filter(Boolean); .filter(Boolean);
@@ -80,9 +80,9 @@ export const createLoanInDatabase = async (
sn !== undefined && sn !== undefined &&
Number.isInteger(Number(sn)) && Number.isInteger(Number(sn)) &&
Number(sn) >= 0 && Number(sn) >= 0 &&
Number(sn) <= 99 Number(sn) <= 99,
) )
.map((sn) => Number(sn)) .map((sn) => Number(sn)),
), ),
]; ];
@@ -98,7 +98,7 @@ export const createLoanInDatabase = async (
AND l.start_date < ? AND l.start_date < ?
AND COALESCE(l.returned_date, l.end_date) > ? AND COALESCE(l.returned_date, l.end_date) > ?
`, `,
[itemIds, end, start] [itemIds, end, start],
); );
if (confRows?.[0]?.conflicts > 0) { if (confRows?.[0]?.conflicts > 0) {
await conn.rollback(); await conn.rollback();
@@ -115,7 +115,7 @@ export const createLoanInDatabase = async (
const candidate = Math.floor(100000 + Math.random() * 899999); // 6 digits const candidate = Math.floor(100000 + Math.random() * 899999); // 6 digits
const [exists] = await conn.query( const [exists] = await conn.query(
"SELECT 1 FROM loans WHERE loan_code = ? LIMIT 1", "SELECT 1 FROM loans WHERE loan_code = ? LIMIT 1",
[candidate] [candidate],
); );
if (exists.length === 0) { if (exists.length === 0) {
loanCode = candidate; loanCode = candidate;
@@ -146,7 +146,7 @@ export const createLoanInDatabase = async (
JSON.stringify(itemIds.map((n) => Number(n))), JSON.stringify(itemIds.map((n) => Number(n))),
JSON.stringify(itemNames), JSON.stringify(itemNames),
note, note,
] ],
); );
await conn.commit(); await conn.commit();
@@ -189,7 +189,7 @@ export const getLoanInfoWithID = async (loanId) => {
export const getLoansFromDatabase = async (username) => { export const getLoansFromDatabase = async (username) => {
const [result] = await pool.query( const [result] = await pool.query(
"SELECT * FROM loans WHERE username = ? AND deleted = 0;", "SELECT * FROM loans WHERE username = ? AND deleted = 0;",
[username] [username],
); );
if (result.length > 0) { if (result.length > 0) {
return { success: true, status: true, data: result }; return { success: true, status: true, data: result };
@@ -202,7 +202,7 @@ export const getLoansFromDatabase = async (username) => {
export const getBorrowableItemsFromDatabase = async ( export const getBorrowableItemsFromDatabase = async (
startDate, startDate,
endDate, endDate,
role = 0 role = 0,
) => { ) => {
// Overlap if: loan.start < end AND effective_end > start // Overlap if: loan.start < end AND effective_end > start
// effective_end is returned_date if set, otherwise end_date // effective_end is returned_date if set, otherwise end_date
@@ -236,7 +236,7 @@ export const getBorrowableItemsFromDatabase = async (
export const SETdeleteLoanFromDatabase = async (loanId) => { export const SETdeleteLoanFromDatabase = async (loanId) => {
const [result] = await pool.query( const [result] = await pool.query(
"UPDATE loans SET deleted = 1 WHERE id = ?;", "UPDATE loans SET deleted = 1 WHERE id = ?;",
[loanId] [loanId],
); );
if (result.affectedRows > 0) { if (result.affectedRows > 0) {
return { success: true }; return { success: true };
@@ -260,3 +260,69 @@ export const getItems = async () => {
} }
return { success: false }; return { success: false };
}; };
export const setReturnDate = async (loanCode) => {
const [items] = await pool.query(
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
[loanCode],
);
const [owner] = await pool.query(
"SELECT username FROM loans WHERE loan_code = ?",
[loanCode],
);
if (items.length === 0) return { success: false };
const itemIds = Array.isArray(items[0].loaned_items_id)
? items[0].loaned_items_id
: JSON.parse(items[0].loaned_items_id || "[]");
const [setItemStates] = await pool.query(
"UPDATE items SET in_safe = 1, currently_borrowing = NULL, last_borrowed_person = (?) WHERE id IN (?)",
[owner[0].username, itemIds],
);
const [result] = await pool.query(
"UPDATE loans SET returned_date = NOW() WHERE loan_code = ?",
[loanCode],
);
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
return { success: true };
}
return { success: false };
};
export const setTakeDate = async (loanCode) => {
const [items] = await pool.query(
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
[loanCode],
);
const [owner] = await pool.query(
"SELECT username FROM loans WHERE loan_code = ?",
[loanCode],
);
if (items.length === 0) return { success: false };
const itemIds = Array.isArray(items[0].loaned_items_id)
? items[0].loaned_items_id
: JSON.parse(items[0].loaned_items_id || "[]");
const [setItemStates] = await pool.query(
"UPDATE items SET in_safe = 0, currently_borrowing = (?) WHERE id IN (?)",
[owner[0].username, itemIds],
);
const [result] = await pool.query(
"UPDATE loans SET take_date = NOW() WHERE loan_code = ?",
[loanCode],
);
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
return { success: true };
}
return { success: false };
};

View File

@@ -13,6 +13,8 @@ import {
getALLLoans, getALLLoans,
getItems, getItems,
SETdeleteLoanFromDatabase, SETdeleteLoanFromDatabase,
setReturnDate,
setTakeDate,
} from "./database/loansMgmt.database.js"; } from "./database/loansMgmt.database.js";
import { sendMailLoan } from "./services/mailer.js"; import { sendMailLoan } from "./services/mailer.js";
@@ -48,7 +50,7 @@ router.post("/createLoan", authenticate, async (req, res) => {
start, start,
end, end,
note, note,
itemIds itemIds,
); );
if (result.success) { if (result.success) {
@@ -59,7 +61,8 @@ router.post("/createLoan", authenticate, async (req, res) => {
mailInfo.data.loaned_items_name, mailInfo.data.loaned_items_name,
mailInfo.data.start_date, mailInfo.data.start_date,
mailInfo.data.end_date, mailInfo.data.end_date,
mailInfo.data.created_at mailInfo.data.created_at,
mailInfo.data.note,
); );
return res.status(201).json({ return res.status(201).json({
message: "Loan created successfully", message: "Loan created successfully",
@@ -96,6 +99,26 @@ router.get("/loans", authenticate, async (req, res) => {
} }
}); });
router.post("/set-return-date/:loan_code", authenticate, async (req, res) => {
const loanCode = req.params.loan_code;
const result = await setReturnDate(loanCode);
if (result.success) {
res.status(200).json({ data: result.data });
} else {
res.status(500).json({ message: "Failed to set return date" });
}
});
router.post("/set-take-date/:loan_code", authenticate, async (req, res) => {
const loanCode = req.params.loan_code;
const result = await setTakeDate(loanCode);
if (result.success) {
res.status(200).json({ data: result.data });
} else {
res.status(500).json({ message: "Failed to set take date" });
}
});
router.get("/all-items", authenticate, async (req, res) => { router.get("/all-items", authenticate, async (req, res) => {
const result = await getItems(); const result = await getItems();
if (result.success) { if (result.success) {
@@ -135,7 +158,7 @@ router.post("/borrowable-items", authenticate, async (req, res) => {
const result = await getBorrowableItemsFromDatabase( const result = await getBorrowableItemsFromDatabase(
startDate, startDate,
endDate, endDate,
req.user.role req.user.role,
); );
if (result.success) { if (result.success) {
// return the array directly for consistency with /items // return the array directly for consistency with /items

View File

@@ -34,14 +34,21 @@ const formatDateTime = (value) => {
return "N/A"; return "N/A";
}; };
function buildLoanEmail({ user, items, startDate, endDate, createdDate }) { function buildLoanEmail({
user,
items,
startDate,
endDate,
createdDate,
note,
}) {
const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9"; const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9";
const itemsList = const itemsList =
Array.isArray(items) && items.length Array.isArray(items) && items.length
? `<ul style="margin:4px 0 0 18px; padding:0;">${items ? `<ul style="margin:4px 0 0 18px; padding:0;">${items
.map( .map(
(i) => (i) =>
`<li style="margin:2px 0; color:#111827; line-height:1.3;">${i}</li>` `<li style="margin:2px 0; color:#111827; line-height:1.3;">${i}</li>`,
) )
.join("")}</ul>` .join("")}</ul>`
: "<span style='color:#111827;'>N/A</span>"; : "<span style='color:#111827;'>N/A</span>";
@@ -101,21 +108,27 @@ function buildLoanEmail({ user, items, startDate, endDate, createdDate }) {
<tr> <tr>
<td style="padding:10px 14px; color:#6b7280; border-bottom:1px solid #ececec;">Startdatum</td> <td style="padding:10px 14px; color:#6b7280; border-bottom:1px solid #ececec;">Startdatum</td>
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${formatDateTime( <td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${formatDateTime(
startDate startDate,
)}</td> )}</td>
</tr> </tr>
<tr> <tr>
<td style="padding:10px 14px; color:#6b7280; border-bottom:1px solid #ececec;">Enddatum</td> <td style="padding:10px 14px; color:#6b7280; border-bottom:1px solid #ececec;">Enddatum</td>
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${formatDateTime( <td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${formatDateTime(
endDate endDate,
)}</td> )}</td>
</tr> </tr>
<tr> <tr>
<td style="padding:10px 14px; color:#6b7280;">Erstellt am</td> <td style="padding:10px 14px; color:#6b7280;">Erstellt am</td>
<td style="padding:10px 14px; font-weight:600; color:#111827;">${formatDateTime( <td style="padding:10px 14px; font-weight:600; color:#111827;">${formatDateTime(
createdDate createdDate,
)}</td> )}</td>
</tr> </tr>
<tr>
<td style="padding:10px 14px; color:#6b7280; vertical-align:top;">Notiz</td>
<td style="padding:10px 14px; font-weight:600; color:#111827;">${
note || "Keine Notiz"
}</td>
</tr>
</tbody> </tbody>
</table> </table>
<p style="margin:22px 0 0 0; font-size:14px;"> <p style="margin:22px 0 0 0; font-size:14px;">
@@ -134,7 +147,14 @@ function buildLoanEmail({ user, items, startDate, endDate, createdDate }) {
</html>`; </html>`;
} }
function buildLoanEmailText({ user, items, startDate, endDate, createdDate }) { function buildLoanEmailText({
user,
items,
startDate,
endDate,
createdDate,
note,
}) {
const itemsText = const itemsText =
Array.isArray(items) && items.length ? items.join(", ") : "N/A"; Array.isArray(items) && items.length ? items.join(", ") : "N/A";
return [ return [
@@ -145,10 +165,18 @@ function buildLoanEmailText({ user, items, startDate, endDate, createdDate }) {
`Start: ${formatDateTime(startDate)}`, `Start: ${formatDateTime(startDate)}`,
`Ende: ${formatDateTime(endDate)}`, `Ende: ${formatDateTime(endDate)}`,
`Erstellt am: ${formatDateTime(createdDate)}`, `Erstellt am: ${formatDateTime(createdDate)}`,
`Notiz: ${note || "Keine Notiz"}`,
].join("\n"); ].join("\n");
} }
export function sendMailLoan(user, items, startDate, endDate, createdDate) { export function sendMailLoan(
user,
items,
startDate,
endDate,
createdDate,
note,
) {
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: process.env.MAIL_HOST, host: process.env.MAIL_HOST,
port: process.env.MAIL_PORT, port: process.env.MAIL_PORT,
@@ -170,12 +198,18 @@ export function sendMailLoan(user, items, startDate, endDate, createdDate) {
startDate, startDate,
endDate, endDate,
createdDate, createdDate,
note,
}),
html: buildLoanEmail({
user,
items,
startDate,
endDate,
createdDate,
note,
}), }),
html: buildLoanEmail({ user, items, startDate, endDate, createdDate }),
}); });
// debugging logs console.log("Loan message sent:", info.messageId);
// console.log("Message sent:", info.messageId);
})(); })();
// console.log("sendMailLoan called");
} }

View File

@@ -38,8 +38,6 @@ export function sendMail(username, message) {
html: mailHtml, html: mailHtml,
}); });
// debugging logs console.log("Contact message sent: %s", info.messageId);
// console.log("Message sent:", info.messageId);
})(); })();
// console.log("sendMailLoan called");
} }

View File

@@ -1,120 +0,0 @@
USE borrow_system_new;
-- Reset tables (no FKs defined, so order is safe)
SET FOREIGN_KEY_CHECKS = 0;
TRUNCATE TABLE loans;
TRUNCATE TABLE apiKeys;
TRUNCATE TABLE items;
TRUNCATE TABLE users;
SET FOREIGN_KEY_CHECKS = 1;
-- Users (roles 16, plain-text passwords; is_admin is BOOL)
INSERT INTO users (username, password, email, first_name, last_name, role, is_admin) VALUES
('admin', 'adminpass', 'admin@example.com', 'System', 'Admin', 6, TRUE),
('alice', 'alice123', 'alice@example.com', 'Alice', 'Andersen',1, FALSE),
('bob', 'bob12345', 'bob@example.com', 'Bob', 'Berg', 2, FALSE),
('carol', 'carol123', 'carol@example.com', 'Carol', 'Christensen', 3, FALSE),
('dave', 'dave123', 'dave@example.com', 'Dave', 'Dahl', 4, FALSE),
('erin', 'erin123', 'erin@example.com', 'Erin', 'Enevoldsen', 5, FALSE),
('frank', 'frank123', 'frank@example.com', 'Frank', 'Fisher', 2, FALSE),
('grace', 'grace123', 'grace@example.com', 'Grace', 'Gundersen',1, FALSE),
('heidi', 'heidi123', 'heidi@example.com', 'Heidi', 'Hansen', 4, FALSE),
('tech', 'techpass', 'tech@example.com', 'Tech', 'User', 5, TRUE);
-- Items (safe_nr is two digits or NULL; matches CHECK and UNIQUE constraint)
INSERT INTO items (item_name, can_borrow_role, in_safe, safe_nr, last_borrowed_person, currently_borrowing) VALUES
('Laptop A', 2, FALSE, NULL, 'grace', 'bob'),
('Laptop B', 2, TRUE, '01', NULL, NULL),
('Camera Canon', 3, TRUE, '02', 'erin', NULL),
('Microphone Rode', 1, TRUE, '03', 'grace', NULL),
('Tripod Manfrotto', 1, TRUE, '04', 'frank', NULL),
('Oscilloscope Tek', 4, TRUE, '05', NULL, NULL),
('VR Headset', 3, FALSE, NULL, 'heidi', 'carol'),
('Keycard Programmer', 6, TRUE, '06', 'admin', NULL);
-- Loans (JSON strings, 6-digit numeric loan_code per CHECK)
-- Assumes the items above have ids 1..8 in insert order
INSERT INTO loans (
username,
lockers,
loan_code,
start_date,
end_date,
take_date,
returned_date,
loaned_items_id,
loaned_items_name,
deleted,
note
) VALUES
-- Active loan: bob has Laptop A (item id 1, locker "01")
('bob',
'["01"]',
'123456',
'2025-11-15 09:00:00',
'2025-11-22 17:00:00',
'2025-11-15 09:15:00',
NULL,
'[1]',
'["Laptop A"]',
FALSE,
'Active loan - Laptop A'
),
-- Returned loan: frank had Tripod Manfrotto (item id 5, locker "04")
('frank',
'["04"]',
'234567',
'2025-10-01 10:00:00',
'2025-10-07 16:00:00',
'2025-10-01 10:05:00',
'2025-10-05 15:30:00',
'[5]',
'["Tripod Manfrotto"]',
FALSE,
'Completed loan'
),
-- Future reservation: dave will take Oscilloscope Tek (item id 6, locker "05")
('dave',
'["05"]',
'345678',
'2025-12-10 09:00:00',
'2025-12-12 17:00:00',
NULL,
NULL,
'[6]',
'["Oscilloscope Tek"]',
FALSE,
'Reserved'
),
-- Active loan: carol has VR Headset (item id 7, locker "02")
('carol',
'["02"]',
'456789',
'2025-11-10 13:00:00',
'2025-11-20 12:00:00',
'2025-11-10 13:10:00',
NULL,
'[7]',
'["VR Headset"]',
FALSE,
'Active loan - VR Headset'
),
-- Soft-deleted historic loan: grace had Microphone + Tripod (item ids 4,5; lockers "03","04")
('grace',
'["03","04"]',
'567890',
'2025-09-01 09:00:00',
'2025-09-03 17:00:00',
'2025-09-01 09:10:00',
'2025-09-03 16:45:00',
'[4,5]',
'["Microphone Rode","Tripod Manfrotto"]',
TRUE,
'Canceled/soft-deleted record'
);
-- API keys (8-digit numeric keys per CHECK)
INSERT INTO apiKeys (api_key, entry_name, last_used_at) VALUES
('12345678', 'CI token', '2025-11-15 08:00:00'),
('87654321', 'Local dev', NULL),
('00000001', 'Monitoring', '2025-11-10 12:30:00');

View File

@@ -18,8 +18,6 @@ services:
networks: networks:
- proxynet - proxynet
build: ./backendV2 build: ./backendV2
ports:
- "8004:8004"
environment: environment:
NODE_ENV: production NODE_ENV: production
DB_HOST: mysql_v2 DB_HOST: mysql_v2