Compare commits
6 Commits
8589971dc8
...
8d04465705
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d04465705 | |||
| 1fa8b4a9a7 | |||
| 3ba3c1c0cb | |||
| ee54d51f8b | |||
| a8dab549af | |||
| 06976f7972 |
@@ -12,7 +12,7 @@ import { triggerLogoutAtom } from "@/states/Atoms";
|
||||
import { MyLoansPage } from "./pages/MyLoansPage";
|
||||
import Landingpage from "./pages/Landingpage";
|
||||
import { changeLanguage } from "i18next";
|
||||
import { Box, Flex } from "@chakra-ui/react";
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
import { Footer } from "./components/footer/Footer";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
@@ -72,8 +72,8 @@ function App() {
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Flex direction="column" minH="100vh">
|
||||
<Box as="main" flex="1">
|
||||
<Flex direction="column" minH="100dvh">
|
||||
<Flex as="main" flex="1" direction="column">
|
||||
<UserContext.Provider value={user}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
@@ -88,7 +88,7 @@ function App() {
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</UserContext.Provider>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Footer />
|
||||
</Flex>
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -5,7 +5,14 @@ export const Footer = () => {
|
||||
const { data: info } = useVersionInfoQuery();
|
||||
|
||||
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
|
||||
<br />
|
||||
Frontend-Version: {info ? info["frontend-info"].version : "N/A"} |
|
||||
|
||||
@@ -7,7 +7,6 @@ import Cookies from "js-cookie";
|
||||
import { Navigate, useNavigate, useLocation } from "react-router-dom";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Footer } from "@/components/footer/Footer";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
|
||||
export const LoginPage = () => {
|
||||
@@ -71,7 +70,7 @@ export const LoginPage = () => {
|
||||
}
|
||||
|
||||
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()}>
|
||||
<Card.Root maxW="sm">
|
||||
<Card.Header>
|
||||
@@ -115,7 +114,6 @@ export const LoginPage = () => {
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
</form>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import Cookies from "js-cookie";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const getBorrowableItems = async (
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/loans/borrowable-items`, {
|
||||
method: "POST",
|
||||
@@ -24,7 +21,8 @@ export const getBorrowableItems = async (
|
||||
data: null,
|
||||
status: "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 MyAlert from "../components/myChakra/MyAlert";
|
||||
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 [username, setUsername] = useState("");
|
||||
@@ -43,8 +44,7 @@ const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
|
||||
</Field.Root>
|
||||
<Field.Root>
|
||||
<Field.Label>password</Field.Label>
|
||||
<Input
|
||||
type="password"
|
||||
<PasswordInput
|
||||
value={password}
|
||||
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": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ESNext",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
@@ -23,13 +24,10 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
|
||||
/* Chakra / Pfad Aliases */
|
||||
"baseUrl": ".",
|
||||
/* Path aliases */
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
"forceConsistentCasingInFileNames": true
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const getItemsFromDatabaseV2 = async () => {
|
||||
export const getLoanByCodeV2 = async (loan_code) => {
|
||||
const [result] = await pool.query(
|
||||
"SELECT username, returned_date, take_date, lockers FROM loans WHERE loan_code = ?;",
|
||||
[loan_code]
|
||||
[loan_code],
|
||||
);
|
||||
if (result.length > 0) {
|
||||
return { success: true, data: result[0] };
|
||||
@@ -33,7 +33,7 @@ export const getLoanByCodeV2 = async (loan_code) => {
|
||||
export const changeInSafeStateV2 = async (itemId) => {
|
||||
const [result] = await pool.query(
|
||||
"UPDATE items SET in_safe = NOT in_safe WHERE id = ?",
|
||||
[itemId]
|
||||
[itemId],
|
||||
);
|
||||
if (result.affectedRows > 0) {
|
||||
return { success: true };
|
||||
@@ -42,50 +42,62 @@ export const changeInSafeStateV2 = async (itemId) => {
|
||||
};
|
||||
|
||||
export const setReturnDateV2 = async (loanCode) => {
|
||||
const [items] = await pool.query(
|
||||
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
|
||||
[loanCode]
|
||||
);
|
||||
try {
|
||||
const [items] = await pool.query(
|
||||
"SELECT loaned_items_id, username 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, message: "No items found for loan" };
|
||||
|
||||
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 itemIds = Array.isArray(items[0].loaned_items_id)
|
||||
? items[0].loaned_items_id
|
||||
: JSON.parse(items[0].loaned_items_id || "[]");
|
||||
const [result] = await pool.query(
|
||||
"UPDATE loans SET returned_date = NOW() WHERE loan_code = ? AND returned_date IS NULL",
|
||||
[loanCode],
|
||||
);
|
||||
|
||||
const [setItemStates] = await pool.query(
|
||||
"UPDATE items SET in_safe = 1, currently_borrowing = NULL, last_borrowed_person = (?) WHERE id IN (?)",
|
||||
[owner[0].username, itemIds]
|
||||
);
|
||||
if (result.affectedRows === 0) return { success: false };
|
||||
|
||||
const [result] = await pool.query(
|
||||
"UPDATE loans SET returned_date = NOW() WHERE loan_code = ?",
|
||||
[loanCode]
|
||||
);
|
||||
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],
|
||||
);
|
||||
}
|
||||
|
||||
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
||||
return { success: true };
|
||||
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) => {
|
||||
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(
|
||||
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
|
||||
[loanCode]
|
||||
[loanCode],
|
||||
);
|
||||
|
||||
const [owner] = await pool.query(
|
||||
"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)
|
||||
? items[0].loaned_items_id
|
||||
@@ -93,18 +105,18 @@ export const setTakeDateV2 = async (loanCode) => {
|
||||
|
||||
const [setItemStates] = await pool.query(
|
||||
"UPDATE items SET in_safe = 0, currently_borrowing = (?) WHERE id IN (?)",
|
||||
[owner[0].username, itemIds]
|
||||
[owner[0].username, itemIds],
|
||||
);
|
||||
|
||||
const [result] = await pool.query(
|
||||
"UPDATE loans SET take_date = NOW() WHERE loan_code = ?",
|
||||
[loanCode]
|
||||
"UPDATE loans SET take_date = NOW() WHERE loan_code = ? AND take_date IS NULL",
|
||||
[loanCode],
|
||||
);
|
||||
|
||||
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false };
|
||||
return { message: "Failed to set take date", success: false };
|
||||
};
|
||||
|
||||
export const getAllLoansV2 = async () => {
|
||||
@@ -118,12 +130,12 @@ export const getAllLoansV2 = async () => {
|
||||
export const openDoor = async (doorKey) => {
|
||||
const [result] = await pool.query(
|
||||
"SELECT safe_nr, id FROM items WHERE door_key = ?;",
|
||||
[doorKey]
|
||||
[doorKey],
|
||||
);
|
||||
if (result.length > 0) {
|
||||
const [changeItemSate] = await pool.query(
|
||||
"UPDATE items SET in_safe = NOT in_safe WHERE id = ?",
|
||||
[result[0].id]
|
||||
[result[0].id],
|
||||
);
|
||||
if (changeItemSate.affectedRows > 0) {
|
||||
return { success: true, data: result[0] };
|
||||
|
||||
@@ -47,7 +47,7 @@ router.get(
|
||||
} else {
|
||||
res.status(404).json({ message: "Loan not found" });
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 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 result = await setReturnDateV2(loanCode);
|
||||
if (result.success) {
|
||||
res.status(200).json({ data: result.data });
|
||||
res.status(200).json({});
|
||||
} else {
|
||||
res.status(500).json({ message: "Failed to set return date" });
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 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 result = await setTakeDateV2(loanCode);
|
||||
if (result.success) {
|
||||
res.status(200).json({ data: result.data });
|
||||
res.status(200).json({});
|
||||
} 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
|
||||
|
||||
@@ -62,6 +62,7 @@ router.post("/createLoan", authenticate, async (req, res) => {
|
||||
mailInfo.data.start_date,
|
||||
mailInfo.data.end_date,
|
||||
mailInfo.data.created_at,
|
||||
mailInfo.data.note,
|
||||
);
|
||||
return res.status(201).json({
|
||||
message: "Loan created successfully",
|
||||
|
||||
@@ -34,7 +34,14 @@ const formatDateTime = (value) => {
|
||||
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 itemsList =
|
||||
Array.isArray(items) && items.length
|
||||
@@ -116,6 +123,12 @@ function buildLoanEmail({ user, items, startDate, endDate, createdDate }) {
|
||||
createdDate,
|
||||
)}</td>
|
||||
</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>
|
||||
</table>
|
||||
<p style="margin:22px 0 0 0; font-size:14px;">
|
||||
@@ -134,7 +147,14 @@ function buildLoanEmail({ user, items, startDate, endDate, createdDate }) {
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function buildLoanEmailText({ user, items, startDate, endDate, createdDate }) {
|
||||
function buildLoanEmailText({
|
||||
user,
|
||||
items,
|
||||
startDate,
|
||||
endDate,
|
||||
createdDate,
|
||||
note,
|
||||
}) {
|
||||
const itemsText =
|
||||
Array.isArray(items) && items.length ? items.join(", ") : "N/A";
|
||||
return [
|
||||
@@ -145,10 +165,18 @@ function buildLoanEmailText({ user, items, startDate, endDate, createdDate }) {
|
||||
`Start: ${formatDateTime(startDate)}`,
|
||||
`Ende: ${formatDateTime(endDate)}`,
|
||||
`Erstellt am: ${formatDateTime(createdDate)}`,
|
||||
`Notiz: ${note || "Keine Notiz"}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function sendMailLoan(user, items, startDate, endDate, createdDate) {
|
||||
export function sendMailLoan(
|
||||
user,
|
||||
items,
|
||||
startDate,
|
||||
endDate,
|
||||
createdDate,
|
||||
note,
|
||||
) {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.MAIL_HOST,
|
||||
port: process.env.MAIL_PORT,
|
||||
@@ -170,8 +198,16 @@ export function sendMailLoan(user, items, startDate, endDate, createdDate) {
|
||||
startDate,
|
||||
endDate,
|
||||
createdDate,
|
||||
note,
|
||||
}),
|
||||
html: buildLoanEmail({
|
||||
user,
|
||||
items,
|
||||
startDate,
|
||||
endDate,
|
||||
createdDate,
|
||||
note,
|
||||
}),
|
||||
html: buildLoanEmail({ user, items, startDate, endDate, createdDate }),
|
||||
});
|
||||
|
||||
console.log("Loan message sent:", info.messageId);
|
||||
|
||||
Reference in New Issue
Block a user