Compare commits
11 Commits
8589971dc8
...
demoDev
| Author | SHA1 | Date | |
|---|---|---|---|
| 98834a9270 | |||
| c8a230979f | |||
| 581cd4a1fd | |||
| c53e6e095a | |||
| eaa325668c | |||
| 8d04465705 | |||
| 1fa8b4a9a7 | |||
| 3ba3c1c0cb | |||
| ee54d51f8b | |||
| a8dab549af | |||
| 06976f7972 |
@@ -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.
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ export const Footer = () => {
|
|||||||
const { data: info } = useVersionInfoQuery();
|
const { data: info } = useVersionInfoQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box as="footer" py={4} textAlign="center" width="100%">
|
<Box
|
||||||
|
as="footer"
|
||||||
|
py={4}
|
||||||
|
textAlign="center"
|
||||||
|
width="100%"
|
||||||
|
flexShrink={0}
|
||||||
|
fontSize="sm"
|
||||||
|
>
|
||||||
Made with ❤️ by Theis Gaedigk - Class of 2019 at MCS-Bochum
|
Made with ❤️ by Theis Gaedigk - Class of 2019 at MCS-Bochum
|
||||||
<br />
|
<br />
|
||||||
Frontend-Version: {info ? info["frontend-info"].version : "N/A"} |
|
Frontend-Version: {info ? info["frontend-info"].version : "N/A"} |
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import Cookies from "js-cookie";
|
|||||||
import { Navigate, useNavigate, useLocation } 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 = () => {
|
||||||
@@ -71,7 +70,7 @@ export const LoginPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
@@ -115,7 +114,6 @@ export const LoginPage = () => {
|
|||||||
</Card.Footer>
|
</Card.Footer>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
</form>
|
</form>
|
||||||
<Footer />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { API_BASE } from "@/config/api.config";
|
import { API_BASE } from "@/config/api.config";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export const getBorrowableItems = async (
|
export const getBorrowableItems = async (
|
||||||
startDate: string,
|
startDate: string,
|
||||||
endDate: string,
|
endDate: string,
|
||||||
) => {
|
) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/api/loans/borrowable-items`, {
|
const response = await fetch(`${API_BASE}/api/loans/borrowable-items`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -24,7 +21,8 @@ export const getBorrowableItems = async (
|
|||||||
data: null,
|
data: null,
|
||||||
status: "error",
|
status: "error",
|
||||||
title: "Server error",
|
title: "Server error",
|
||||||
description: t("serverError"),
|
description:
|
||||||
|
"An error occurred on the server. Sometimes reloading the page helps. Otherwise, please contact the administrator.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
159
admin/src/components/ui/password-input.tsx
Normal file
159
admin/src/components/ui/password-input.tsx
Normal 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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,13 +24,10 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true,
|
"noUncheckedSideEffectImports": true,
|
||||||
|
|
||||||
/* Chakra / Pfad Aliases */
|
/* Path aliases */
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
},
|
}
|
||||||
|
|
||||||
"forceConsistentCasingInFileNames": true
|
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"backend-info": {
|
"backend-info": {
|
||||||
"version": "v2.1 (demo)"
|
"version": "v2.1.1 (demo)"
|
||||||
},
|
},
|
||||||
"frontend-info": {
|
"frontend-info": {
|
||||||
"version": "v2.1 (demo)"
|
"version": "v2.1.2 (demo)"
|
||||||
},
|
},
|
||||||
"admin-panel-info": {
|
"admin-panel-info": {
|
||||||
"version": "v1.3.2 (demo)"
|
"version": "v1.3.2 (demo)"
|
||||||
|
|||||||
@@ -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] };
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ router.post("/createLoan", authenticate, async (req, res) => {
|
|||||||
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",
|
||||||
|
|||||||
@@ -34,7 +34,14 @@ 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
|
||||||
@@ -116,6 +123,12 @@ function buildLoanEmail({ user, items, startDate, endDate, createdDate }) {
|
|||||||
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,8 +198,16 @@ 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 }),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Loan message sent:", info.messageId);
|
console.log("Loan message sent:", info.messageId);
|
||||||
|
|||||||
Reference in New Issue
Block a user