From 344f0461b4f6d1b76bfadbbd866956591be270d4 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Fri, 29 May 2026 22:16:18 +0200 Subject: [PATCH] added settings page --- backend/database.scheme.sql | 12 ++- backend/routes/app/database/users.database.js | 32 ++++++++ backend/routes/app/users.route.js | 56 +++++++++++++- frontend/src/components/Sidebar.tsx | 13 ++-- frontend/src/misc/interfaces.ts | 5 ++ frontend/src/pages/AddProduct.tsx | 3 +- frontend/src/pages/Inventory.tsx | 3 +- frontend/src/pages/Settings.tsx | 77 +++++++++++++++++++ frontend/src/pages/ViewProduct.tsx | 3 +- frontend/src/routeTree.gen.ts | 43 ++++++----- .../routes/app/_hiddenLayout/app-settings.tsx | 10 +++ .../src/routes/app/_hiddenLayout/profile.tsx | 9 --- frontend/src/utils/auth.ts | 7 +- frontend/src/utils/uxFncs.ts | 44 +++++++++++ 14 files changed, 275 insertions(+), 42 deletions(-) create mode 100644 frontend/src/pages/Settings.tsx create mode 100644 frontend/src/routes/app/_hiddenLayout/app-settings.tsx delete mode 100644 frontend/src/routes/app/_hiddenLayout/profile.tsx diff --git a/backend/database.scheme.sql b/backend/database.scheme.sql index eab27ea..ecc6552 100644 --- a/backend/database.scheme.sql +++ b/backend/database.scheme.sql @@ -36,4 +36,14 @@ CREATE TABLE IF NOT EXISTS products ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (storage_location) REFERENCES storage_locations(uuid) ON DELETE CASCADE -); \ No newline at end of file +); + +CREATE TABLE IF NOT EXISTS app_settings ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + value VARCHAR(500) DEFAULT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +INSERT INTO app_settings (name, value) VALUES ("app-name", null), ("currency", null); \ No newline at end of file diff --git a/backend/routes/app/database/users.database.js b/backend/routes/app/database/users.database.js index ddc1668..fb8b6ff 100644 --- a/backend/routes/app/database/users.database.js +++ b/backend/routes/app/database/users.database.js @@ -40,3 +40,35 @@ export const loginUser = async (username) => { return { code: "eu003" }; } }; + +export const updateSettings = async (payload) => { + const appName = payload["app-name"]; + const currency = payload.currency; + + const [result] = await pool.query( + `UPDATE app_settings + SET value = CASE name + WHEN "app-name" THEN ? + WHEN "currency" THEN ? + ELSE value + END + WHERE name IN ("app-name", "currency");`, + [appName, currency], + ); + + if (result.affectedRows > 0) { + return { code: "su003" }; + } else { + return { code: "eu004" }; + } +}; + +export const getSettings = async () => { + const [result] = await pool.query(`SELECT * FROM app_settings;`); + + if (result.length > 0) { + return { code: "su004", result }; + } else { + return { code: "eu005" }; + } +}; diff --git a/backend/routes/app/users.route.js b/backend/routes/app/users.route.js index 019f85b..f4899f9 100644 --- a/backend/routes/app/users.route.js +++ b/backend/routes/app/users.route.js @@ -1,7 +1,12 @@ import express from "express"; import dotenv from "dotenv"; import { authenticate, generateToken } from "../../services/tokenService.js"; -import { findUser, loginUser } from "./database/users.database.js"; +import { + findUser, + loginUser, + updateSettings, + getSettings, +} from "./database/users.database.js"; dotenv.config(); const router = express.Router(); @@ -9,6 +14,55 @@ router.post("/verify-token", authenticate, async (req, res) => { res.sendStatus(200); }); +router.post("/update-app-settings", authenticate, async (req, res) => { + const appName = req.body.appName; + const currency = req.body.currency; + + console.log(req.body); + + const result = await updateSettings(req.body); + + if (result.code === "su003") { + res.status(201).json({ + success: true, + code: "su003", + data: result.data, + message: null, + }); + } + + if (result.code === "eu004") { + res.status(500).json({ + success: false, + code: "eu004", + data: null, + message: "Unexpected server error", + }); + } +}); + +router.get("/settings", authenticate, async (req, res) => { + const result = await getSettings(); + + if (result.code === "su004") { + res.status(201).json({ + success: true, + code: "su004", + data: result.result, + message: null, + }); + } + + if (result.code === "eu005") { + res.status(500).json({ + success: false, + code: "eu005", + data: null, + message: "Unexpected server error", + }); + } +}); + router.post("/login", async (req, res) => { const username = req.body.username; const password = req.body.password; diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 15e203e..488c3ab 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -3,8 +3,9 @@ import { Button, Typography } from "@mui/joy"; import InventoryIcon from "@mui/icons-material/Inventory"; import AddBoxIcon from "@mui/icons-material/AddBox"; import StorageIcon from "@mui/icons-material/Storage"; -import AccountBoxIcon from "@mui/icons-material/AccountBox"; +import SettingsIcon from "@mui/icons-material/Settings"; import { useNavigate, useMatchRoute } from "@tanstack/react-router"; +import Cookies from "js-cookie"; export const Sidebar = () => { const { t } = useTranslation(); @@ -30,7 +31,7 @@ export const Sidebar = () => { level="body-lg" className="text-sm font-medium text-slate-500" > - {t("app-subtitle")} + {Cookies.get("app-name") ? Cookies.get("app-name") : ""} @@ -60,12 +61,12 @@ export const Sidebar = () => { {t("storages")} diff --git a/frontend/src/misc/interfaces.ts b/frontend/src/misc/interfaces.ts index fc13ddb..8bd9859 100644 --- a/frontend/src/misc/interfaces.ts +++ b/frontend/src/misc/interfaces.ts @@ -40,3 +40,8 @@ export interface AlertInterface { header: string; text: string; } + +export interface SettingsIntf { + ["app-name"]: string; + currency: string; +} diff --git a/frontend/src/pages/AddProduct.tsx b/frontend/src/pages/AddProduct.tsx index 154a4f4..532d088 100644 --- a/frontend/src/pages/AddProduct.tsx +++ b/frontend/src/pages/AddProduct.tsx @@ -15,6 +15,7 @@ import { useState } from "react"; import { useForm } from "@tanstack/react-form"; import { createProduct, getStorages } from "../utils/uxFncs"; import type { ProductFormValues } from "../misc/interfaces"; +import Cookies from "js-cookie"; export const AddProduct = () => { const { t } = useTranslation(); @@ -205,7 +206,7 @@ export const AddProduct = () => { )} - {t("currency")} + {Cookies.get("currency")}
diff --git a/frontend/src/pages/Inventory.tsx b/frontend/src/pages/Inventory.tsx index 36afa23..2ee72aa 100644 --- a/frontend/src/pages/Inventory.tsx +++ b/frontend/src/pages/Inventory.tsx @@ -29,6 +29,7 @@ import { useQuery } from "@tanstack/react-query"; import { getProducts } from "../utils/uxFncs"; import { visuallyHidden } from "@mui/utils"; import { formatDate } from "../utils/uxFncs"; +import Cookies from "js-cookie"; type Order = "asc" | "desc"; @@ -427,7 +428,7 @@ export const InventoryPage = () => { {row.price} - {t("currency")} + {Cookies.get("currency")} diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx new file mode 100644 index 0000000..f3c84e1 --- /dev/null +++ b/frontend/src/pages/Settings.tsx @@ -0,0 +1,77 @@ +import { Input, Button, CircularProgress } from "@mui/joy"; +import { useForm } from "@tanstack/react-form"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import Cookies from "js-cookie"; +import { useTranslation } from "react-i18next"; +import type { SettingsIntf } from "../misc/interfaces"; +import { mutateSettings, fetchSettings } from "../utils/uxFncs"; +import { useEffect } from "react"; + +export const Settings = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + const { + data: settings, + isPending: settingsPending, + isSuccess: settingsSuccess, + } = useQuery({ + queryKey: ["settings"], + queryFn: fetchSettings, + }); + + useEffect(() => { + Cookies.set("app-name", settings?.data[0].value); + Cookies.set("currency", settings?.data[1].value); + }, [settingsSuccess]); + + const form = useForm({ + defaultValues: { + "app-name": settings?.data[0].value ?? "", + currency: settings?.data[1].value ?? "", + }, + onSubmit: async ({ value }) => { + mutate(value); + }, + }); + + const { mutate } = useMutation({ + mutationFn: (values: SettingsIntf) => mutateSettings(values), + onSuccess() { + queryClient.invalidateQueries({ queryKey: ["settings"] }); + }, + }); + + return ( + <> + {settingsPending ? ( + + ) : ( +
{ + e.preventDefault(); + form.handleSubmit(); + }} + > + + {(field) => ( + field.handleChange(e.target.value)} + /> + )} + + + {(field) => ( + field.handleChange(e.target.value)} + /> + )} + + +
+ )} + + ); +}; diff --git a/frontend/src/pages/ViewProduct.tsx b/frontend/src/pages/ViewProduct.tsx index 5acaa1a..975b823 100644 --- a/frontend/src/pages/ViewProduct.tsx +++ b/frontend/src/pages/ViewProduct.tsx @@ -19,6 +19,7 @@ import { mutateProduct } from "../utils/uxFncs"; import { toInputDate } from "../utils/uxFncs"; import type { ProductFormValues } from "../misc/interfaces"; import type { productDetailsInterface } from "../misc/interfaces"; +import Cookies from "js-cookie"; interface ViewProductProps { uuid: string; @@ -256,7 +257,7 @@ export const ViewProduct = (props: ViewProductProps) => { )} - {t("currency")} + {Cookies.get("currency")}
diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 1e6db25..870569d 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -14,8 +14,8 @@ import { Route as IndexRouteImport } from './routes/index' import { Route as AppHiddenLayoutRouteImport } from './routes/app/_hiddenLayout' import { Route as AppHiddenLayoutViewProductRouteImport } from './routes/app/_hiddenLayout/view-product' import { Route as AppHiddenLayoutStoragesRouteImport } from './routes/app/_hiddenLayout/storages' -import { Route as AppHiddenLayoutProfileRouteImport } from './routes/app/_hiddenLayout/profile' import { Route as AppHiddenLayoutInventoryRouteImport } from './routes/app/_hiddenLayout/inventory' +import { Route as AppHiddenLayoutAppSettingsRouteImport } from './routes/app/_hiddenLayout/app-settings' import { Route as AppHiddenLayoutAddProductRouteImport } from './routes/app/_hiddenLayout/add-product' const LoginRoute = LoginRouteImport.update({ @@ -44,17 +44,18 @@ const AppHiddenLayoutStoragesRoute = AppHiddenLayoutStoragesRouteImport.update({ path: '/storages', getParentRoute: () => AppHiddenLayoutRoute, } as any) -const AppHiddenLayoutProfileRoute = AppHiddenLayoutProfileRouteImport.update({ - id: '/profile', - path: '/profile', - getParentRoute: () => AppHiddenLayoutRoute, -} as any) const AppHiddenLayoutInventoryRoute = AppHiddenLayoutInventoryRouteImport.update({ id: '/inventory', path: '/inventory', getParentRoute: () => AppHiddenLayoutRoute, } as any) +const AppHiddenLayoutAppSettingsRoute = + AppHiddenLayoutAppSettingsRouteImport.update({ + id: '/app-settings', + path: '/app-settings', + getParentRoute: () => AppHiddenLayoutRoute, + } as any) const AppHiddenLayoutAddProductRoute = AppHiddenLayoutAddProductRouteImport.update({ id: '/add-product', @@ -67,8 +68,8 @@ export interface FileRoutesByFullPath { '/login': typeof LoginRoute '/app': typeof AppHiddenLayoutRouteWithChildren '/app/add-product': typeof AppHiddenLayoutAddProductRoute + '/app/app-settings': typeof AppHiddenLayoutAppSettingsRoute '/app/inventory': typeof AppHiddenLayoutInventoryRoute - '/app/profile': typeof AppHiddenLayoutProfileRoute '/app/storages': typeof AppHiddenLayoutStoragesRoute '/app/view-product': typeof AppHiddenLayoutViewProductRoute } @@ -77,8 +78,8 @@ export interface FileRoutesByTo { '/login': typeof LoginRoute '/app': typeof AppHiddenLayoutRouteWithChildren '/app/add-product': typeof AppHiddenLayoutAddProductRoute + '/app/app-settings': typeof AppHiddenLayoutAppSettingsRoute '/app/inventory': typeof AppHiddenLayoutInventoryRoute - '/app/profile': typeof AppHiddenLayoutProfileRoute '/app/storages': typeof AppHiddenLayoutStoragesRoute '/app/view-product': typeof AppHiddenLayoutViewProductRoute } @@ -88,8 +89,8 @@ export interface FileRoutesById { '/login': typeof LoginRoute '/app/_hiddenLayout': typeof AppHiddenLayoutRouteWithChildren '/app/_hiddenLayout/add-product': typeof AppHiddenLayoutAddProductRoute + '/app/_hiddenLayout/app-settings': typeof AppHiddenLayoutAppSettingsRoute '/app/_hiddenLayout/inventory': typeof AppHiddenLayoutInventoryRoute - '/app/_hiddenLayout/profile': typeof AppHiddenLayoutProfileRoute '/app/_hiddenLayout/storages': typeof AppHiddenLayoutStoragesRoute '/app/_hiddenLayout/view-product': typeof AppHiddenLayoutViewProductRoute } @@ -100,8 +101,8 @@ export interface FileRouteTypes { | '/login' | '/app' | '/app/add-product' + | '/app/app-settings' | '/app/inventory' - | '/app/profile' | '/app/storages' | '/app/view-product' fileRoutesByTo: FileRoutesByTo @@ -110,8 +111,8 @@ export interface FileRouteTypes { | '/login' | '/app' | '/app/add-product' + | '/app/app-settings' | '/app/inventory' - | '/app/profile' | '/app/storages' | '/app/view-product' id: @@ -120,8 +121,8 @@ export interface FileRouteTypes { | '/login' | '/app/_hiddenLayout' | '/app/_hiddenLayout/add-product' + | '/app/_hiddenLayout/app-settings' | '/app/_hiddenLayout/inventory' - | '/app/_hiddenLayout/profile' | '/app/_hiddenLayout/storages' | '/app/_hiddenLayout/view-product' fileRoutesById: FileRoutesById @@ -169,13 +170,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AppHiddenLayoutStoragesRouteImport parentRoute: typeof AppHiddenLayoutRoute } - '/app/_hiddenLayout/profile': { - id: '/app/_hiddenLayout/profile' - path: '/profile' - fullPath: '/app/profile' - preLoaderRoute: typeof AppHiddenLayoutProfileRouteImport - parentRoute: typeof AppHiddenLayoutRoute - } '/app/_hiddenLayout/inventory': { id: '/app/_hiddenLayout/inventory' path: '/inventory' @@ -183,6 +177,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AppHiddenLayoutInventoryRouteImport parentRoute: typeof AppHiddenLayoutRoute } + '/app/_hiddenLayout/app-settings': { + id: '/app/_hiddenLayout/app-settings' + path: '/app-settings' + fullPath: '/app/app-settings' + preLoaderRoute: typeof AppHiddenLayoutAppSettingsRouteImport + parentRoute: typeof AppHiddenLayoutRoute + } '/app/_hiddenLayout/add-product': { id: '/app/_hiddenLayout/add-product' path: '/add-product' @@ -195,16 +196,16 @@ declare module '@tanstack/react-router' { interface AppHiddenLayoutRouteChildren { AppHiddenLayoutAddProductRoute: typeof AppHiddenLayoutAddProductRoute + AppHiddenLayoutAppSettingsRoute: typeof AppHiddenLayoutAppSettingsRoute AppHiddenLayoutInventoryRoute: typeof AppHiddenLayoutInventoryRoute - AppHiddenLayoutProfileRoute: typeof AppHiddenLayoutProfileRoute AppHiddenLayoutStoragesRoute: typeof AppHiddenLayoutStoragesRoute AppHiddenLayoutViewProductRoute: typeof AppHiddenLayoutViewProductRoute } const AppHiddenLayoutRouteChildren: AppHiddenLayoutRouteChildren = { AppHiddenLayoutAddProductRoute: AppHiddenLayoutAddProductRoute, + AppHiddenLayoutAppSettingsRoute: AppHiddenLayoutAppSettingsRoute, AppHiddenLayoutInventoryRoute: AppHiddenLayoutInventoryRoute, - AppHiddenLayoutProfileRoute: AppHiddenLayoutProfileRoute, AppHiddenLayoutStoragesRoute: AppHiddenLayoutStoragesRoute, AppHiddenLayoutViewProductRoute: AppHiddenLayoutViewProductRoute, } diff --git a/frontend/src/routes/app/_hiddenLayout/app-settings.tsx b/frontend/src/routes/app/_hiddenLayout/app-settings.tsx new file mode 100644 index 0000000..d5a542e --- /dev/null +++ b/frontend/src/routes/app/_hiddenLayout/app-settings.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { Settings } from "../../../pages/Settings"; + +export const Route = createFileRoute("/app/_hiddenLayout/app-settings")({ + component: RouteComponent, +}); + +function RouteComponent() { + return ; +} diff --git a/frontend/src/routes/app/_hiddenLayout/profile.tsx b/frontend/src/routes/app/_hiddenLayout/profile.tsx deleted file mode 100644 index 0ebec38..0000000 --- a/frontend/src/routes/app/_hiddenLayout/profile.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' - -export const Route = createFileRoute('/app/_hiddenLayout/profile')({ - component: RouteComponent, -}) - -function RouteComponent() { - return
Hello "/app/_hiddenLayout/profile"!
-} diff --git a/frontend/src/utils/auth.ts b/frontend/src/utils/auth.ts index 6ed64c5..9c2b70a 100644 --- a/frontend/src/utils/auth.ts +++ b/frontend/src/utils/auth.ts @@ -2,6 +2,7 @@ import { API_BASE } from "../config/api.config"; import Cookies from "js-cookie"; import type { TFunction } from "i18next"; import { toast } from "react-toastify"; +import { fetchSettings } from "./uxFncs"; export async function isAuthenticated() { if (Cookies.get("token")) { @@ -39,10 +40,14 @@ export async function signInUser( }); const response = await result.json(); - console.log(response); if (result.status === 202) { Cookies.set("token", response.data.token); + + const settings = await fetchSettings(); + Cookies.set("app-name", settings?.data[0].value); + Cookies.set("currency", settings?.data[1].value); + return { ok: true as const }; } diff --git a/frontend/src/utils/uxFncs.ts b/frontend/src/utils/uxFncs.ts index 2b75f51..e3cba07 100644 --- a/frontend/src/utils/uxFncs.ts +++ b/frontend/src/utils/uxFncs.ts @@ -3,6 +3,7 @@ import Cookies from "js-cookie"; import type { NewStorage, ProductFormValues, + SettingsIntf, Storage, } from "../misc/interfaces"; @@ -229,3 +230,46 @@ export const deleteStorage = async (uuid: string) => { return { success: true, code: response.code }; } }; + +export const mutateSettings = async (payload: SettingsIntf) => { + const result = await fetch(`${API_BASE}/users/update-app-settings`, { + method: "POST", + body: JSON.stringify(payload), + headers: { + Authorization: `Bearer ${Cookies.get("token") || ""}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + }); + + const response = await result.json(); + + if (response.code === "eu004") { + return { success: false, code: response.code }; + } + + if (response.code === "su003") { + return { success: true, code: response.code }; + } +}; + +export const fetchSettings = async () => { + const result = await fetch(`${API_BASE}/users/settings`, { + method: "GET", + headers: { + Authorization: `Bearer ${Cookies.get("token") || ""}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + }); + + const response = await result.json(); + + if (response.code === "eu005") { + return { success: false, code: response.code }; + } + + if (response.code === "su004") { + return { success: true, data: response.data, code: response.code }; + } +};