added settings page

This commit is contained in:
2026-05-29 22:16:18 +02:00
parent 3582e377f1
commit 344f0461b4
14 changed files with 275 additions and 42 deletions
+11 -1
View File
@@ -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
);
);
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);
@@ -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" };
}
};
+55 -1
View File
@@ -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;
+7 -6
View File
@@ -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") : ""}
</Typography>
</div>
@@ -60,12 +61,12 @@ export const Sidebar = () => {
{t("storages")}
</Button>
<Button
onClick={() => navigate({ to: "/app/profile" })}
variant={variant("/app/profile")}
startDecorator={<AccountBoxIcon />}
onClick={() => navigate({ to: "/app/app-settings" })}
variant={variant("/app/app-settings")}
startDecorator={<SettingsIcon />}
className={btnClass}
>
{t("profile")}
{t("settings")}
</Button>
</div>
+5
View File
@@ -40,3 +40,8 @@ export interface AlertInterface {
header: string;
text: string;
}
export interface SettingsIntf {
["app-name"]: string;
currency: string;
}
+2 -1
View File
@@ -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 = () => {
)}
</form.Field>
<Typography level="body-sm" className="text-slate-500">
{t("currency")}
{Cookies.get("currency")}
</Typography>
</div>
<div className="space-y-1">
+2 -1
View File
@@ -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 = () => {
<td className="px-6 py-5">
<Typography level="title-md">{row.price}</Typography>
<Typography level="body-sm" className="text-slate-400">
{t("currency")}
{Cookies.get("currency")}
</Typography>
</td>
<td className="px-6 py-5">
+77
View File
@@ -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 ? (
<CircularProgress />
) : (
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.Field name="app-name">
{(field) => (
<Input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
</form.Field>
<form.Field name="currency">
{(field) => (
<Input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
</form.Field>
<Button type="submit">{t("submit")}</Button>
</form>
)}
</>
);
};
+2 -1
View File
@@ -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) => {
)}
</form.Field>
<Typography level="body-sm" className="text-slate-500">
{t("currency")}
{Cookies.get("currency")}
</Typography>
</div>
<div className="space-y-1">
+22 -21
View File
@@ -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,
}
@@ -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 <Settings />;
}
@@ -1,9 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/app/_hiddenLayout/profile')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/app/_hiddenLayout/profile"!</div>
}
+6 -1
View File
@@ -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 };
}
+44
View File
@@ -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 };
}
};