addded error codes and improved error handling
This commit is contained in:
@@ -0,0 +1,107 @@
|
|||||||
|
# Errors
|
||||||
|
|
||||||
|
Here you will find all Error codes and its meaning. Error or status codes that are starting with an s are success codes.
|
||||||
|
|
||||||
|
## Error codes
|
||||||
|
|
||||||
|
### `eu006`
|
||||||
|
|
||||||
|
**Meaning:** There is an error while changing the password. _HTTP-Code: **406**_
|
||||||
|
|
||||||
|
**Solution:** Make sure that you have entered the correct previous password and that the double check passwords are matching.
|
||||||
|
|
||||||
|
### `eu004`
|
||||||
|
|
||||||
|
**Meaning:** There is an error while updating the settings. _HTTP-Code: **500**_
|
||||||
|
|
||||||
|
**Solution:** This error should not occur in the frontend. If so, please create an issue in this repository.
|
||||||
|
|
||||||
|
### `eu005`
|
||||||
|
|
||||||
|
**Meaning:** There is an error while fetching the app settings. _HTTP-Code: **500**_
|
||||||
|
|
||||||
|
**Solution:** This error should not occur in the frontend. If so, please create an issue in this repository.
|
||||||
|
|
||||||
|
### `eu001`
|
||||||
|
|
||||||
|
**Meaning:** Username or password is wrong. _HTTP-Code: **404**_
|
||||||
|
|
||||||
|
**Solution:** Check the username and password and try again.
|
||||||
|
|
||||||
|
### `eu002`
|
||||||
|
|
||||||
|
**Meaning:** The user is deactivated. _HTTP-Code: **403**_
|
||||||
|
|
||||||
|
**Solution:** Contact an admin to reactivate the account.
|
||||||
|
|
||||||
|
### `eu003`
|
||||||
|
|
||||||
|
**Meaning:** There is an error while updating the last login timestamp. _HTTP-Code: **500**_
|
||||||
|
|
||||||
|
**Solution:** This error should not occur in the frontend. If so, please create an issue in this repository.
|
||||||
|
|
||||||
|
### `ep001`
|
||||||
|
|
||||||
|
**Meaning:** There is an error while creating a product. _HTTP-Code: **406**_
|
||||||
|
|
||||||
|
**Solution:** Verify that all required fields are provided and valid, then try again.
|
||||||
|
|
||||||
|
### `ep002`
|
||||||
|
|
||||||
|
**Meaning:** There is an error while fetching products. _HTTP-Code: **406**_
|
||||||
|
|
||||||
|
**Solution:** Try again later. If the error persists, create an issue in this repository.
|
||||||
|
|
||||||
|
### `ep003`
|
||||||
|
|
||||||
|
**Meaning:** There is an error while fetching a product. _HTTP-Code: **406**_
|
||||||
|
|
||||||
|
**Solution:** Ensure the product exists and try again.
|
||||||
|
|
||||||
|
### `ep004`
|
||||||
|
|
||||||
|
**Meaning:** There is an error while updating the product amount. _HTTP-Code: **406**_
|
||||||
|
|
||||||
|
**Solution:** Check the amount value and try again.
|
||||||
|
|
||||||
|
### `ep005`
|
||||||
|
|
||||||
|
**Meaning:** There is an error while updating a product. _HTTP-Code: **406**_
|
||||||
|
|
||||||
|
**Solution:** Verify the product data and try again.
|
||||||
|
|
||||||
|
### `ep006`
|
||||||
|
|
||||||
|
**Meaning:** There is an error while deleting products. _HTTP-Code: **500**_
|
||||||
|
|
||||||
|
**Solution:** Try again later. If the error persists, create an issue in this repository.
|
||||||
|
|
||||||
|
### `es001`
|
||||||
|
|
||||||
|
**Meaning:** There is an error while fetching storage locations. _HTTP-Code: **500**_
|
||||||
|
|
||||||
|
**Solution:** Try again later. If the error persists, create an issue in this repository.
|
||||||
|
|
||||||
|
### `es000`
|
||||||
|
|
||||||
|
**Meaning:** The request body is invalid. _HTTP-Code: **400**_
|
||||||
|
|
||||||
|
**Solution:** Provide a storage name. The description is optional.
|
||||||
|
|
||||||
|
### `es002`
|
||||||
|
|
||||||
|
**Meaning:** There is an error while creating a storage location. _HTTP-Code: **500**_
|
||||||
|
|
||||||
|
**Solution:** Try again later. If the error persists, create an issue in this repository.
|
||||||
|
|
||||||
|
### `es003`
|
||||||
|
|
||||||
|
**Meaning:** There is an error while updating a storage location. _HTTP-Code: **500**_
|
||||||
|
|
||||||
|
**Solution:** Try again later. If the error persists, create an issue in this repository.
|
||||||
|
|
||||||
|
### `es004`
|
||||||
|
|
||||||
|
**Meaning:** There is an error while deleting a storage location. _HTTP-Code: **500**_
|
||||||
|
|
||||||
|
**Solution:** Try again later. If the error persists, create an issue in this repository.
|
||||||
@@ -72,3 +72,20 @@ export const getSettings = async () => {
|
|||||||
return { code: "eu005" };
|
return { code: "eu005" };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const changePassword = async (
|
||||||
|
username,
|
||||||
|
currentPasswordUser,
|
||||||
|
newPassword,
|
||||||
|
) => {
|
||||||
|
const [result] = await pool.query(
|
||||||
|
`UPDATE users SET password = ? WHERE username = ? AND password = ?;`,
|
||||||
|
[newPassword, username, currentPasswordUser],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.affectedRows > 0) {
|
||||||
|
return { code: "su005" };
|
||||||
|
} else {
|
||||||
|
return { code: "eu006" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
loginUser,
|
loginUser,
|
||||||
updateSettings,
|
updateSettings,
|
||||||
getSettings,
|
getSettings,
|
||||||
|
changePassword,
|
||||||
} from "./database/users.database.js";
|
} from "./database/users.database.js";
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -18,8 +19,6 @@ router.post("/update-app-settings", authenticate, async (req, res) => {
|
|||||||
const appName = req.body.appName;
|
const appName = req.body.appName;
|
||||||
const currency = req.body.currency;
|
const currency = req.body.currency;
|
||||||
|
|
||||||
console.log(req.body);
|
|
||||||
|
|
||||||
const result = await updateSettings(req.body);
|
const result = await updateSettings(req.body);
|
||||||
|
|
||||||
if (result.code === "su003") {
|
if (result.code === "su003") {
|
||||||
@@ -91,7 +90,7 @@ router.post("/login", async (req, res) => {
|
|||||||
const token = await generateToken(result.data);
|
const token = await generateToken(result.data);
|
||||||
const login = await loginUser(result.data.username);
|
const login = await loginUser(result.data.username);
|
||||||
|
|
||||||
if (login.code === "e003") {
|
if (login.code === "eu003") {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
code: "eu003",
|
code: "eu003",
|
||||||
@@ -111,4 +110,26 @@ router.post("/login", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post("/change-password", authenticate, async (req, res) => {
|
||||||
|
const currentPassword = req.body.currentPassword;
|
||||||
|
const newPassword = req.body.newPassword;
|
||||||
|
const username = req.user.username;
|
||||||
|
|
||||||
|
const result = await changePassword(username, currentPassword, newPassword);
|
||||||
|
|
||||||
|
if (result.code === "su005") {
|
||||||
|
res.status(202).json({
|
||||||
|
success: true,
|
||||||
|
code: result.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === "eu006") {
|
||||||
|
res.status(406).json({
|
||||||
|
success: false,
|
||||||
|
code: result.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
import { useForm } from "@tanstack/react-form";
|
import { useForm } from "@tanstack/react-form";
|
||||||
import { Input, Button } from "@mui/joy";
|
import { Input, Button, Alert } from "@mui/joy";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { signInUser } from "../utils/api/auth";
|
import { signInUser } from "../utils/api/auth";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { AlertInterface } from "../misc/interfaces";
|
||||||
|
|
||||||
export const LoginCard = () => {
|
export const LoginCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [alert, setAlert] = useState<AlertInterface>({
|
||||||
|
isAlert: false,
|
||||||
|
type: "neutral",
|
||||||
|
header: "",
|
||||||
|
text: "",
|
||||||
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -29,9 +37,24 @@ export const LoginCard = () => {
|
|||||||
}) => signInUser(username, password, t),
|
}) => signInUser(username, password, t),
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
|
setAlert({
|
||||||
|
isAlert: false,
|
||||||
|
type: "neutral",
|
||||||
|
header: "",
|
||||||
|
text: "",
|
||||||
|
});
|
||||||
navigate({ to: "/app/inventory" });
|
navigate({ to: "/app/inventory" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onError: (error: unknown) => {
|
||||||
|
const errorCode = (error as { code?: string })?.code;
|
||||||
|
setAlert({
|
||||||
|
isAlert: true,
|
||||||
|
type: "danger",
|
||||||
|
header: t("error"),
|
||||||
|
text: errorCode ? t(errorCode) : t("unknown-error"),
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -53,6 +76,17 @@ export const LoginCard = () => {
|
|||||||
form.handleSubmit();
|
form.handleSubmit();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{alert.isAlert && (
|
||||||
|
<Alert
|
||||||
|
variant="soft"
|
||||||
|
color={alert.type}
|
||||||
|
className="rounded-2xl border border-rose-200/70 bg-rose-50/80 text-rose-700 shadow-[0_12px_30px_rgba(220,38,38,0.12)]"
|
||||||
|
>
|
||||||
|
{alert.header}
|
||||||
|
<br />
|
||||||
|
{alert.text}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
<form.Field name="username">
|
<form.Field name="username">
|
||||||
{(field) => (
|
{(field) => (
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
interface StorageRowProps {
|
interface StorageRowProps {
|
||||||
storage: Storage;
|
storage: Storage;
|
||||||
|
onError: (error: unknown) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StorageRow = ({ storage }: StorageRowProps) => {
|
export const StorageRow = ({ storage, onError }: StorageRowProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -22,6 +23,7 @@ export const StorageRow = ({ storage }: StorageRowProps) => {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["storages"] });
|
queryClient.invalidateQueries({ queryKey: ["storages"] });
|
||||||
},
|
},
|
||||||
|
onError,
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
@@ -29,6 +31,7 @@ export const StorageRow = ({ storage }: StorageRowProps) => {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["storages"] });
|
queryClient.invalidateQueries({ queryKey: ["storages"] });
|
||||||
},
|
},
|
||||||
|
onError,
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const AddStorageModal = (props: AddStorageModalProps) => {
|
|||||||
isAlert: true,
|
isAlert: true,
|
||||||
type: "danger",
|
type: "danger",
|
||||||
header: t("error"),
|
header: t("error"),
|
||||||
text: error.code ? t(`errors.${error.code}`) : t("unknown-error"),
|
text: error.code ? t(error.code) : t("unknown-error"),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalDialog,
|
||||||
|
DialogTitle,
|
||||||
|
Stack,
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
Alert,
|
||||||
|
} from "@mui/joy";
|
||||||
|
import { useForm } from "@tanstack/react-form";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import type { ChangePasswordIntf, AlertInterface } from "../../misc/interfaces";
|
||||||
|
import { mutatePassword } from "../../utils/api/auth";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface ChangePasswordProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
setOpen: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChangePasswordModal = (props: ChangePasswordProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [alert, setAlert] = useState<AlertInterface>({
|
||||||
|
isAlert: false,
|
||||||
|
type: "neutral",
|
||||||
|
header: "",
|
||||||
|
text: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
currentPassword: "",
|
||||||
|
newPassword: "",
|
||||||
|
newPasswordRep: "",
|
||||||
|
},
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
mutate(value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate } = useMutation({
|
||||||
|
mutationFn: (values: ChangePasswordIntf) => mutatePassword(values),
|
||||||
|
onError: (error: { code?: string }) => {
|
||||||
|
setAlert({
|
||||||
|
isAlert: true,
|
||||||
|
type: "danger",
|
||||||
|
header: t("error"),
|
||||||
|
text: error.code ? t(error.code) : t("unknown-error"),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
props.setOpen(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal open={props.isOpen} onClose={() => props.setOpen(false)}>
|
||||||
|
<ModalDialog className="rounded-3xl border border-white/70 bg-white/90 p-6 shadow-[0_30px_70px_rgba(12,38,78,0.2)] backdrop-blur">
|
||||||
|
<DialogTitle className="text-slate-900">
|
||||||
|
{t("new-password-title")}
|
||||||
|
</DialogTitle>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={2} className="mt-4">
|
||||||
|
<form.Field name="currentPassword">
|
||||||
|
{(field) => (
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={field.state.value}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
placeholder={t("current-password")}
|
||||||
|
variant="outlined"
|
||||||
|
size="lg"
|
||||||
|
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
<form.Field name="newPassword">
|
||||||
|
{(field) => (
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={field.state.value}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
placeholder={t("new-password")}
|
||||||
|
variant="outlined"
|
||||||
|
size="lg"
|
||||||
|
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
<form.Field name="newPasswordRep">
|
||||||
|
{(field) => (
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={field.state.value}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
placeholder={t("new-password-rep")}
|
||||||
|
variant="outlined"
|
||||||
|
size="lg"
|
||||||
|
className="rounded-2xl bg-white/90 shadow-[0_10px_24px_rgba(15,23,42,0.08)]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="lg"
|
||||||
|
className="rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]"
|
||||||
|
>
|
||||||
|
{t("change")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
{alert.isAlert && (
|
||||||
|
<Alert
|
||||||
|
variant="soft"
|
||||||
|
color={alert.type}
|
||||||
|
className="mt-4 rounded-2xl border border-rose-200/70 bg-rose-50/80 text-rose-700 shadow-[0_12px_30px_rgba(220,38,38,0.12)]"
|
||||||
|
>
|
||||||
|
{alert.header}
|
||||||
|
<br />
|
||||||
|
{alert.text}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -61,3 +61,8 @@ export type ProductRow = {
|
|||||||
expiryDate: string;
|
expiryDate: string;
|
||||||
refillDate: string;
|
refillDate: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ChangePasswordIntf = {
|
||||||
|
currentPassword: string;
|
||||||
|
newPassword: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -11,22 +11,53 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from "@mui/joy";
|
} from "@mui/joy";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "@tanstack/react-form";
|
import { useForm } from "@tanstack/react-form";
|
||||||
import { createProduct } from "../utils/api/products";
|
import { createProduct } from "../utils/api/products";
|
||||||
import { getStorages } from "../utils/api/storages";
|
import { getStorages } from "../utils/api/storages";
|
||||||
import type { ProductFormValues } from "../misc/interfaces";
|
import type {
|
||||||
|
ProductFormValues,
|
||||||
|
AlertInterface,
|
||||||
|
Storage,
|
||||||
|
} from "../misc/interfaces";
|
||||||
|
import type { ApiError } from "../utils/api/apiError";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
export const AddProduct = () => {
|
export const AddProduct = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [success, setSuccess] = useState(false);
|
const [success, setSuccess] = useState(false);
|
||||||
|
const [alert, setAlert] = useState<AlertInterface>({
|
||||||
|
isAlert: false,
|
||||||
|
type: "neutral",
|
||||||
|
header: "",
|
||||||
|
text: "",
|
||||||
|
});
|
||||||
|
|
||||||
const { data: storages } = useQuery({
|
const showError = (error: unknown) => {
|
||||||
|
const errorCode = (error as { code?: string })?.code;
|
||||||
|
setAlert({
|
||||||
|
isAlert: true,
|
||||||
|
type: "danger",
|
||||||
|
header: t("error"),
|
||||||
|
text: errorCode ? t(errorCode) : t("unknown-error"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: storages,
|
||||||
|
isError: storagesError,
|
||||||
|
error: storagesErrorObj,
|
||||||
|
} = useQuery<Storage[], ApiError>({
|
||||||
queryKey: ["storages"],
|
queryKey: ["storages"],
|
||||||
queryFn: () => getStorages(),
|
queryFn: () => getStorages(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (storagesError && storagesErrorObj) {
|
||||||
|
showError(storagesErrorObj);
|
||||||
|
}
|
||||||
|
}, [storagesError, storagesErrorObj]);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
amount: 0,
|
amount: 0,
|
||||||
@@ -48,6 +79,7 @@ export const AddProduct = () => {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
},
|
},
|
||||||
|
onError: showError,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -227,13 +259,11 @@ export const AddProduct = () => {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
className="rounded-2xl bg-white/90"
|
className="rounded-2xl bg-white/90"
|
||||||
>
|
>
|
||||||
{storages?.map(
|
{storages?.map((storage) => (
|
||||||
(storage: { uuid: string; name: string }) => (
|
<Option key={storage.uuid} value={storage.uuid}>
|
||||||
<Option key={storage.uuid} value={storage.uuid}>
|
{storage.name}
|
||||||
{storage.name}
|
</Option>
|
||||||
</Option>
|
))}
|
||||||
),
|
|
||||||
)}
|
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
@@ -255,6 +285,17 @@ export const AddProduct = () => {
|
|||||||
{t("save")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{alert.isAlert && (
|
||||||
|
<Alert
|
||||||
|
color={alert.type}
|
||||||
|
variant="soft"
|
||||||
|
className="rounded-2xl border border-rose-200/70 bg-rose-50/80 text-rose-700 shadow-[0_12px_30px_rgba(220,38,38,0.12)]"
|
||||||
|
>
|
||||||
|
{alert.header}
|
||||||
|
<br />
|
||||||
|
{alert.text}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
{success && (
|
{success && (
|
||||||
<Alert
|
<Alert
|
||||||
color="success"
|
color="success"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
Button,
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
Chip,
|
Chip,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
Alert,
|
||||||
} from "@mui/joy";
|
} from "@mui/joy";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -17,17 +18,46 @@ import { deleteSelectedProducts, getProducts } from "../utils/api/products";
|
|||||||
import { formatDate } from "../utils/uxFncs";
|
import { formatDate } from "../utils/uxFncs";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import type { ProductRow } from "../misc/interfaces";
|
import type { ProductRow } from "../misc/interfaces";
|
||||||
|
import type { AlertInterface } from "../misc/interfaces";
|
||||||
|
import type { ApiError } from "../utils/api/apiError";
|
||||||
|
|
||||||
export const InventoryPage = () => {
|
export const InventoryPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const [alert, setAlert] = useState<AlertInterface>({
|
||||||
|
isAlert: false,
|
||||||
|
type: "neutral",
|
||||||
|
header: "",
|
||||||
|
text: "",
|
||||||
|
});
|
||||||
|
|
||||||
const { data: productsData, isLoading: productsIsLoading } = useQuery({
|
const showError = (error: unknown) => {
|
||||||
|
const errorCode = (error as { code?: string })?.code;
|
||||||
|
setAlert({
|
||||||
|
isAlert: true,
|
||||||
|
type: "danger",
|
||||||
|
header: t("error"),
|
||||||
|
text: errorCode ? t(errorCode) : t("unknown-error"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: productsData,
|
||||||
|
isLoading: productsIsLoading,
|
||||||
|
isError: productsError,
|
||||||
|
error: productsErrorObj,
|
||||||
|
} = useQuery<any[], ApiError>({
|
||||||
queryKey: ["products"],
|
queryKey: ["products"],
|
||||||
queryFn: getProducts,
|
queryFn: getProducts,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (productsError && productsErrorObj) {
|
||||||
|
showError(productsErrorObj);
|
||||||
|
}
|
||||||
|
}, [productsError, productsErrorObj]);
|
||||||
|
|
||||||
const rows: ProductRow[] = (productsData ?? []).map(
|
const rows: ProductRow[] = (productsData ?? []).map(
|
||||||
(product: any, index: number) => ({
|
(product: any, index: number) => ({
|
||||||
id: String(product?.uuid ?? index),
|
id: String(product?.uuid ?? index),
|
||||||
@@ -37,6 +67,8 @@ export const InventoryPage = () => {
|
|||||||
imageUrl: product?.picture ?? undefined,
|
imageUrl: product?.picture ?? undefined,
|
||||||
price: product?.price ? String(product.price) : "-",
|
price: product?.price ? String(product.price) : "-",
|
||||||
stock: `${product?.amount ?? 0} ${t("pcs")}`,
|
stock: `${product?.amount ?? 0} ${t("pcs")}`,
|
||||||
|
stockLabel: String(product?.amount ?? 0),
|
||||||
|
stockStatus: (product?.amount ?? 0) > 0 ? "ok" : "missing",
|
||||||
location: product?.storage_location_name ?? "-",
|
location: product?.storage_location_name ?? "-",
|
||||||
locationDetail: "",
|
locationDetail: "",
|
||||||
expiryDate: formatDate(product?.expiry_date),
|
expiryDate: formatDate(product?.expiry_date),
|
||||||
@@ -52,6 +84,7 @@ export const InventoryPage = () => {
|
|||||||
setSelected([]);
|
setSelected([]);
|
||||||
queryClient.invalidateQueries({ queryKey: ["products"] });
|
queryClient.invalidateQueries({ queryKey: ["products"] });
|
||||||
},
|
},
|
||||||
|
onError: showError,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -94,6 +127,17 @@ export const InventoryPage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
{productsIsLoading && <CircularProgress size="sm" />}
|
{productsIsLoading && <CircularProgress size="sm" />}
|
||||||
</div>
|
</div>
|
||||||
|
{alert.isAlert && (
|
||||||
|
<Alert
|
||||||
|
variant="soft"
|
||||||
|
color={alert.type}
|
||||||
|
className="mt-4 rounded-2xl border border-rose-200/70 bg-rose-50/80 text-rose-700 shadow-[0_12px_30px_rgba(220,38,38,0.12)]"
|
||||||
|
>
|
||||||
|
{alert.header}
|
||||||
|
<br />
|
||||||
|
{alert.text}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<Sheet
|
<Sheet
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|||||||
@@ -17,8 +17,13 @@ import { useEffect, useState } from "react";
|
|||||||
import { useForm } from "@tanstack/react-form";
|
import { useForm } from "@tanstack/react-form";
|
||||||
import { mutateProduct, getProductDetails } from "../utils/api/products.ts";
|
import { mutateProduct, getProductDetails } from "../utils/api/products.ts";
|
||||||
import { toInputDate } from "../utils/uxFncs";
|
import { toInputDate } from "../utils/uxFncs";
|
||||||
import type { ProductFormValues } from "../misc/interfaces";
|
import type {
|
||||||
import type { productDetailsInterface } from "../misc/interfaces";
|
ProductFormValues,
|
||||||
|
productDetailsInterface,
|
||||||
|
AlertInterface,
|
||||||
|
Storage,
|
||||||
|
} from "../misc/interfaces";
|
||||||
|
import type { ApiError } from "../utils/api/apiError";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import ElectricBoltIcon from "@mui/icons-material/ElectricBolt";
|
import ElectricBoltIcon from "@mui/icons-material/ElectricBolt";
|
||||||
|
|
||||||
@@ -27,21 +32,55 @@ export const ProductQuickView = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [success, setSuccess] = useState(false);
|
const [success, setSuccess] = useState(false);
|
||||||
|
const [alert, setAlert] = useState<AlertInterface>({
|
||||||
|
isAlert: false,
|
||||||
|
type: "neutral",
|
||||||
|
header: "",
|
||||||
|
text: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const showError = (error: unknown) => {
|
||||||
|
const errorCode = (error as { code?: string })?.code;
|
||||||
|
setAlert({
|
||||||
|
isAlert: true,
|
||||||
|
type: "danger",
|
||||||
|
header: t("error"),
|
||||||
|
text: errorCode ? t(errorCode) : t("unknown-error"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: productDetails,
|
data: productDetails,
|
||||||
isLoading: productDetailsLoading,
|
isLoading: productDetailsLoading,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
} = useQuery<productDetailsInterface>({
|
isError: productDetailsError,
|
||||||
|
error: productDetailsErrorObj,
|
||||||
|
} = useQuery<productDetailsInterface, ApiError>({
|
||||||
queryKey: ["product", uuid],
|
queryKey: ["product", uuid],
|
||||||
queryFn: () => getProductDetails(uuid),
|
queryFn: () => getProductDetails(uuid),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: storages } = useQuery({
|
const {
|
||||||
|
data: storages,
|
||||||
|
isError: storagesError,
|
||||||
|
error: storagesErrorObj,
|
||||||
|
} = useQuery<Storage[], ApiError>({
|
||||||
queryKey: ["storages"],
|
queryKey: ["storages"],
|
||||||
queryFn: () => getStorages(),
|
queryFn: () => getStorages(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (productDetailsError && productDetailsErrorObj) {
|
||||||
|
showError(productDetailsErrorObj);
|
||||||
|
}
|
||||||
|
}, [productDetailsError, productDetailsErrorObj]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (storagesError && storagesErrorObj) {
|
||||||
|
showError(storagesErrorObj);
|
||||||
|
}
|
||||||
|
}, [storagesError, storagesErrorObj]);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
amount: 0,
|
amount: 0,
|
||||||
@@ -73,6 +112,7 @@ export const ProductQuickView = () => {
|
|||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
queryClient.invalidateQueries({ queryKey: ["product", variables.uuid] });
|
queryClient.invalidateQueries({ queryKey: ["product", variables.uuid] });
|
||||||
},
|
},
|
||||||
|
onError: showError,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -271,13 +311,11 @@ export const ProductQuickView = () => {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
className="rounded-2xl bg-white/90"
|
className="rounded-2xl bg-white/90"
|
||||||
>
|
>
|
||||||
{storages?.map(
|
{storages?.map((storage) => (
|
||||||
(storage: { uuid: string; name: string }) => (
|
<Option key={storage.uuid} value={storage.uuid}>
|
||||||
<Option key={storage.uuid} value={storage.uuid}>
|
{storage.name}
|
||||||
{storage.name}
|
</Option>
|
||||||
</Option>
|
))}
|
||||||
),
|
|
||||||
)}
|
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
@@ -299,6 +337,17 @@ export const ProductQuickView = () => {
|
|||||||
{t("save")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{alert.isAlert && (
|
||||||
|
<Alert
|
||||||
|
color={alert.type}
|
||||||
|
variant="soft"
|
||||||
|
className="rounded-2xl border border-rose-200/70 bg-rose-50/80 text-rose-700 shadow-[0_12px_30px_rgba(220,38,38,0.12)]"
|
||||||
|
>
|
||||||
|
{alert.header}
|
||||||
|
<br />
|
||||||
|
{alert.text}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
{success && (
|
{success && (
|
||||||
<Alert
|
<Alert
|
||||||
color="success"
|
color="success"
|
||||||
|
|||||||
@@ -6,31 +6,68 @@ import {
|
|||||||
Sheet,
|
Sheet,
|
||||||
Chip,
|
Chip,
|
||||||
Divider,
|
Divider,
|
||||||
|
Alert,
|
||||||
} from "@mui/joy";
|
} from "@mui/joy";
|
||||||
import { useForm } from "@tanstack/react-form";
|
import { useForm } from "@tanstack/react-form";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { SettingsIntf } from "../misc/interfaces";
|
import type { SettingsIntf } from "../misc/interfaces";
|
||||||
|
import type { AlertInterface } from "../misc/interfaces";
|
||||||
|
import type { ApiError } from "../utils/api/apiError";
|
||||||
import { mutateSettings, fetchSettings } from "../utils/api/settings";
|
import { mutateSettings, fetchSettings } from "../utils/api/settings";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { ChangePasswordModal } from "../components/modals/ChangePasswordModal";
|
||||||
|
|
||||||
export const Settings = () => {
|
export const Settings = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const [modal, setModal] = useState(false);
|
||||||
|
const [alert, setAlert] = useState<AlertInterface>({
|
||||||
|
isAlert: false,
|
||||||
|
type: "neutral",
|
||||||
|
header: "",
|
||||||
|
text: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const showError = (error: unknown) => {
|
||||||
|
const errorCode = (error as { code?: string })?.code;
|
||||||
|
setAlert({
|
||||||
|
isAlert: true,
|
||||||
|
type: "danger",
|
||||||
|
header: t("error"),
|
||||||
|
text: errorCode ? t(errorCode) : t("unknown-error"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: settings,
|
data: settings,
|
||||||
isPending: settingsPending,
|
isPending: settingsPending,
|
||||||
isSuccess: settingsSuccess,
|
isSuccess: settingsSuccess,
|
||||||
} = useQuery({
|
isError: settingsError,
|
||||||
|
error: settingsErrorObj,
|
||||||
|
} = useQuery<{ data: { value: string }[] }, ApiError>({
|
||||||
queryKey: ["settings"],
|
queryKey: ["settings"],
|
||||||
queryFn: fetchSettings,
|
queryFn: fetchSettings,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Cookies.set("app-name", settings?.data[0].value);
|
if (settingsError && settingsErrorObj) {
|
||||||
Cookies.set("currency", settings?.data[1].value);
|
showError(settingsErrorObj);
|
||||||
|
}
|
||||||
|
}, [settingsError, settingsErrorObj]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const appName = settings?.data?.[0]?.value;
|
||||||
|
const currency = settings?.data?.[1]?.value;
|
||||||
|
|
||||||
|
if (appName) {
|
||||||
|
Cookies.set("app-name", appName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currency) {
|
||||||
|
Cookies.set("currency", currency);
|
||||||
|
}
|
||||||
}, [settingsSuccess]);
|
}, [settingsSuccess]);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
@@ -48,10 +85,12 @@ export const Settings = () => {
|
|||||||
onSuccess() {
|
onSuccess() {
|
||||||
queryClient.invalidateQueries({ queryKey: ["settings"] });
|
queryClient.invalidateQueries({ queryKey: ["settings"] });
|
||||||
},
|
},
|
||||||
|
onError: showError,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<ChangePasswordModal isOpen={modal} setOpen={setModal} />
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -73,6 +112,17 @@ export const Settings = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Sheet className="mt-6 rounded-3xl border border-white/70 bg-white/80 p-6 shadow-[0_24px_60px_rgba(12,38,78,0.12)] backdrop-blur">
|
<Sheet className="mt-6 rounded-3xl border border-white/70 bg-white/80 p-6 shadow-[0_24px_60px_rgba(12,38,78,0.12)] backdrop-blur">
|
||||||
|
{alert.isAlert && (
|
||||||
|
<Alert
|
||||||
|
variant="soft"
|
||||||
|
color={alert.type}
|
||||||
|
className="mb-6 rounded-2xl border border-rose-200/70 bg-rose-50/80 text-rose-700 shadow-[0_12px_30px_rgba(220,38,38,0.12)]"
|
||||||
|
>
|
||||||
|
{alert.header}
|
||||||
|
<br />
|
||||||
|
{alert.text}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
{settingsPending ? (
|
{settingsPending ? (
|
||||||
<div className="flex items-center justify-center py-16">
|
<div className="flex items-center justify-center py-16">
|
||||||
<CircularProgress size="lg" />
|
<CircularProgress size="lg" />
|
||||||
@@ -143,10 +193,19 @@ export const Settings = () => {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]"
|
color="primary"
|
||||||
|
className="rounded-2xl text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]"
|
||||||
>
|
>
|
||||||
{t("save")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => setModal(true)}
|
||||||
|
size="lg"
|
||||||
|
color="warning"
|
||||||
|
className="rounded-2xl text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]"
|
||||||
|
>
|
||||||
|
{t("change-password")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,22 +1,58 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { getStorages } from "../utils/api/storages";
|
import { getStorages } from "../utils/api/storages";
|
||||||
import { Sheet, Table, Button, CircularProgress, Typography } from "@mui/joy";
|
import {
|
||||||
|
Sheet,
|
||||||
|
Table,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
Typography,
|
||||||
|
Alert,
|
||||||
|
} from "@mui/joy";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { Storage } from "../misc/interfaces";
|
import type { Storage } from "../misc/interfaces";
|
||||||
|
import type { AlertInterface } from "../misc/interfaces";
|
||||||
|
import type { ApiError } from "../utils/api/apiError";
|
||||||
import { StorageRow } from "../components/StorageRow";
|
import { StorageRow } from "../components/StorageRow";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { AddStorageModal } from "../components/modals/AddStorageModal";
|
import { AddStorageModal } from "../components/modals/AddStorageModal";
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
|
||||||
export const Storages = () => {
|
export const Storages = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [modal, setModal] = useState(false);
|
const [modal, setModal] = useState(false);
|
||||||
|
const [alert, setAlert] = useState<AlertInterface>({
|
||||||
|
isAlert: false,
|
||||||
|
type: "neutral",
|
||||||
|
header: "",
|
||||||
|
text: "",
|
||||||
|
});
|
||||||
|
|
||||||
const { data: storages, isLoading } = useQuery({
|
const showError = (error: unknown) => {
|
||||||
|
const errorCode = (error as { code?: string })?.code;
|
||||||
|
setAlert({
|
||||||
|
isAlert: true,
|
||||||
|
type: "danger",
|
||||||
|
header: t("error"),
|
||||||
|
text: errorCode ? t(errorCode) : t("unknown-error"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: storages,
|
||||||
|
isLoading,
|
||||||
|
isError: storagesError,
|
||||||
|
error: storagesErrorObj,
|
||||||
|
} = useQuery<Storage[], ApiError>({
|
||||||
queryKey: ["storages"],
|
queryKey: ["storages"],
|
||||||
queryFn: () => getStorages(),
|
queryFn: () => getStorages(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (storagesError && storagesErrorObj) {
|
||||||
|
showError(storagesErrorObj);
|
||||||
|
}
|
||||||
|
}, [storagesError, storagesErrorObj]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
@@ -38,6 +74,17 @@ export const Storages = () => {
|
|||||||
{t("add")}
|
{t("add")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{alert.isAlert && (
|
||||||
|
<Alert
|
||||||
|
variant="soft"
|
||||||
|
color={alert.type}
|
||||||
|
className="rounded-2xl border border-rose-200/70 bg-rose-50/80 text-rose-700 shadow-[0_12px_30px_rgba(220,38,38,0.12)]"
|
||||||
|
>
|
||||||
|
{alert.header}
|
||||||
|
<br />
|
||||||
|
{alert.text}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Sheet
|
<Sheet
|
||||||
@@ -100,7 +147,11 @@ export const Storages = () => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{storages?.map((storage: Storage) => (
|
{storages?.map((storage: Storage) => (
|
||||||
<StorageRow key={storage.uuid} storage={storage} />
|
<StorageRow
|
||||||
|
key={storage.uuid}
|
||||||
|
storage={storage}
|
||||||
|
onError={showError}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|||||||
@@ -17,8 +17,13 @@ import { useEffect, useState } from "react";
|
|||||||
import { useForm } from "@tanstack/react-form";
|
import { useForm } from "@tanstack/react-form";
|
||||||
import { mutateProduct, getProductDetails } from "../utils/api/products.ts";
|
import { mutateProduct, getProductDetails } from "../utils/api/products.ts";
|
||||||
import { toInputDate } from "../utils/uxFncs";
|
import { toInputDate } from "../utils/uxFncs";
|
||||||
import type { ProductFormValues } from "../misc/interfaces";
|
import type {
|
||||||
import type { productDetailsInterface } from "../misc/interfaces";
|
ProductFormValues,
|
||||||
|
productDetailsInterface,
|
||||||
|
AlertInterface,
|
||||||
|
Storage,
|
||||||
|
} from "../misc/interfaces";
|
||||||
|
import type { ApiError } from "../utils/api/apiError";
|
||||||
import QrCodeIcon from "@mui/icons-material/QrCode";
|
import QrCodeIcon from "@mui/icons-material/QrCode";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import QRCode from "qrcode";
|
import QRCode from "qrcode";
|
||||||
@@ -32,21 +37,55 @@ export const ViewProduct = (props: ViewProductProps) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [success, setSuccess] = useState(false);
|
const [success, setSuccess] = useState(false);
|
||||||
|
const [alert, setAlert] = useState<AlertInterface>({
|
||||||
|
isAlert: false,
|
||||||
|
type: "neutral",
|
||||||
|
header: "",
|
||||||
|
text: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const showError = (error: unknown) => {
|
||||||
|
const errorCode = (error as { code?: string })?.code;
|
||||||
|
setAlert({
|
||||||
|
isAlert: true,
|
||||||
|
type: "danger",
|
||||||
|
header: t("error"),
|
||||||
|
text: errorCode ? t(errorCode) : t("unknown-error"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: productDetails,
|
data: productDetails,
|
||||||
isLoading: productDetailsLoading,
|
isLoading: productDetailsLoading,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
} = useQuery<productDetailsInterface>({
|
isError: productDetailsError,
|
||||||
|
error: productDetailsErrorObj,
|
||||||
|
} = useQuery<productDetailsInterface, ApiError>({
|
||||||
queryKey: ["product", uuid],
|
queryKey: ["product", uuid],
|
||||||
queryFn: () => getProductDetails(uuid),
|
queryFn: () => getProductDetails(uuid),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: storages } = useQuery({
|
const {
|
||||||
|
data: storages,
|
||||||
|
isError: storagesError,
|
||||||
|
error: storagesErrorObj,
|
||||||
|
} = useQuery<Storage[], ApiError>({
|
||||||
queryKey: ["storages"],
|
queryKey: ["storages"],
|
||||||
queryFn: () => getStorages(),
|
queryFn: () => getStorages(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (productDetailsError && productDetailsErrorObj) {
|
||||||
|
showError(productDetailsErrorObj);
|
||||||
|
}
|
||||||
|
}, [productDetailsError, productDetailsErrorObj]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (storagesError && storagesErrorObj) {
|
||||||
|
showError(storagesErrorObj);
|
||||||
|
}
|
||||||
|
}, [storagesError, storagesErrorObj]);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
amount: 0,
|
amount: 0,
|
||||||
@@ -78,6 +117,7 @@ export const ViewProduct = (props: ViewProductProps) => {
|
|||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
queryClient.invalidateQueries({ queryKey: ["product", variables.uuid] });
|
queryClient.invalidateQueries({ queryKey: ["product", variables.uuid] });
|
||||||
},
|
},
|
||||||
|
onError: showError,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -289,13 +329,11 @@ export const ViewProduct = (props: ViewProductProps) => {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
className="rounded-2xl bg-white/90"
|
className="rounded-2xl bg-white/90"
|
||||||
>
|
>
|
||||||
{storages?.map(
|
{storages?.map((storage) => (
|
||||||
(storage: { uuid: string; name: string }) => (
|
<Option key={storage.uuid} value={storage.uuid}>
|
||||||
<Option key={storage.uuid} value={storage.uuid}>
|
{storage.name}
|
||||||
{storage.name}
|
</Option>
|
||||||
</Option>
|
))}
|
||||||
),
|
|
||||||
)}
|
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
@@ -326,6 +364,17 @@ export const ViewProduct = (props: ViewProductProps) => {
|
|||||||
{t("save")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{alert.isAlert && (
|
||||||
|
<Alert
|
||||||
|
color={alert.type}
|
||||||
|
variant="soft"
|
||||||
|
className="rounded-2xl border border-rose-200/70 bg-rose-50/80 text-rose-700 shadow-[0_12px_30px_rgba(220,38,38,0.12)]"
|
||||||
|
>
|
||||||
|
{alert.header}
|
||||||
|
<br />
|
||||||
|
{alert.text}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
{success && (
|
{success && (
|
||||||
<Alert
|
<Alert
|
||||||
color="success"
|
color="success"
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export type ApiError = Error & { code?: string };
|
||||||
|
|
||||||
|
export const createApiError = (
|
||||||
|
code?: string,
|
||||||
|
message: string = "Request failed",
|
||||||
|
): ApiError => {
|
||||||
|
const error = new Error(message) as ApiError;
|
||||||
|
error.code = code || "unknown-error";
|
||||||
|
return error;
|
||||||
|
};
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
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 type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
import { toast } from "react-toastify";
|
|
||||||
import { fetchSettings } from "./settings";
|
import { fetchSettings } from "./settings";
|
||||||
|
import type { ChangePasswordIntf } from "../../misc/interfaces";
|
||||||
|
import { createApiError } from "./apiError";
|
||||||
|
|
||||||
export async function isAuthenticated() {
|
export async function isAuthenticated() {
|
||||||
if (Cookies.get("token")) {
|
if (Cookies.get("token")) {
|
||||||
@@ -52,11 +53,33 @@ export async function signInUser(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Cookies.remove("token");
|
Cookies.remove("token");
|
||||||
toast.error(t(response.code));
|
throw createApiError(response.code, t(response.code || "unknown-error"));
|
||||||
return { ok: false as const };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function signOutUser() {
|
export function signOutUser() {
|
||||||
Cookies.remove("token");
|
Cookies.remove("token");
|
||||||
return { ok: true as const };
|
return { ok: true as const };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const mutatePassword = async (payload: ChangePasswordIntf) => {
|
||||||
|
const result = await fetch(`${API_BASE}/users/change-password`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
currentPassword: payload.currentPassword,
|
||||||
|
newPassword: payload.newPassword,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await result.json();
|
||||||
|
|
||||||
|
if (response.code === "su005") {
|
||||||
|
return { code: response.code };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw createApiError(response.code, "Change password failed");
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 type { ProductFormValues } from "../../misc/interfaces";
|
import type { ProductFormValues } from "../../misc/interfaces";
|
||||||
|
import { createApiError } from "./apiError";
|
||||||
|
|
||||||
export const getProducts = async () => {
|
export const getProducts = async () => {
|
||||||
const result = await fetch(`${API_BASE}/products/all-products`, {
|
const result = await fetch(`${API_BASE}/products/all-products`, {
|
||||||
@@ -12,13 +13,11 @@ export const getProducts = async () => {
|
|||||||
});
|
});
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "ep002") {
|
|
||||||
return { success: false, code: response.code };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.code === "sp002") {
|
if (response.code === "sp002") {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw createApiError(response.code, "Get products failed");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProductDetails = async (uuid: string) => {
|
export const getProductDetails = async (uuid: string) => {
|
||||||
@@ -32,13 +31,11 @@ export const getProductDetails = async (uuid: string) => {
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "ep003") {
|
|
||||||
return { success: false, code: response.code };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.code === "sp003") {
|
if (response.code === "sp003") {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw createApiError(response.code, "Get product details failed");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mutateProduct = async (
|
export const mutateProduct = async (
|
||||||
@@ -66,13 +63,11 @@ export const mutateProduct = async (
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "ep004") {
|
if (response.code === "sp005") {
|
||||||
return { success: false, code: response.code };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.code === "sp004") {
|
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw createApiError(response.code, "Update product failed");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteSelectedProducts = async (uuids: string[]) => {
|
export const deleteSelectedProducts = async (uuids: string[]) => {
|
||||||
@@ -88,7 +83,11 @@ export const deleteSelectedProducts = async (uuids: string[]) => {
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
console.log(response);
|
if (response.code === "sp006") {
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw createApiError(response.code, "Delete products failed");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createProduct = async (values: ProductFormValues) => {
|
export const createProduct = async (values: ProductFormValues) => {
|
||||||
@@ -114,11 +113,9 @@ export const createProduct = async (values: ProductFormValues) => {
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "ep001") {
|
|
||||||
return { success: false, code: response.code };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.code === "sp001") {
|
if (response.code === "sp001") {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw createApiError(response.code, "Create product failed");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 type { SettingsIntf } from "../../misc/interfaces";
|
import type { SettingsIntf } from "../../misc/interfaces";
|
||||||
|
import { createApiError } from "./apiError";
|
||||||
|
|
||||||
export const fetchSettings = async () => {
|
export const fetchSettings = async () => {
|
||||||
const result = await fetch(`${API_BASE}/users/settings`, {
|
const result = await fetch(`${API_BASE}/users/settings`, {
|
||||||
@@ -14,13 +15,11 @@ export const fetchSettings = async () => {
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "eu005") {
|
|
||||||
return { success: false, code: response.code };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.code === "su004") {
|
if (response.code === "su004") {
|
||||||
return { success: true, data: response.data, code: response.code };
|
return { success: true, data: response.data, code: response.code };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw createApiError(response.code, "Fetch settings failed");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mutateSettings = async (payload: SettingsIntf) => {
|
export const mutateSettings = async (payload: SettingsIntf) => {
|
||||||
@@ -36,11 +35,9 @@ export const mutateSettings = async (payload: SettingsIntf) => {
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "eu004") {
|
|
||||||
return { success: false, code: response.code };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.code === "su003") {
|
if (response.code === "su003") {
|
||||||
return { success: true, code: response.code };
|
return { success: true, code: response.code };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw createApiError(response.code, "Update settings failed");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 type { NewStorage, Storage } from "../../misc/interfaces";
|
import type { NewStorage, Storage } from "../../misc/interfaces";
|
||||||
|
import { createApiError } from "./apiError";
|
||||||
|
|
||||||
export const getStorages = async () => {
|
export const getStorages = async () => {
|
||||||
const result = await fetch(`${API_BASE}/storage/all-storages`, {
|
const result = await fetch(`${API_BASE}/storage/all-storages`, {
|
||||||
@@ -13,13 +14,11 @@ export const getStorages = async () => {
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "es001") {
|
|
||||||
return { success: false, code: response.code };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.code === "ss001") {
|
if (response.code === "ss001") {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw createApiError(response.code, "Get storages failed");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mutateNewStorage = async (values: NewStorage) => {
|
export const mutateNewStorage = async (values: NewStorage) => {
|
||||||
@@ -35,13 +34,11 @@ export const mutateNewStorage = async (values: NewStorage) => {
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "es002") {
|
|
||||||
return { success: false, code: response.code };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.code === "ss002") {
|
if (response.code === "ss002") {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw createApiError(response.code, "Create storage failed");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateStorage = async (
|
export const updateStorage = async (
|
||||||
@@ -63,13 +60,11 @@ export const updateStorage = async (
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "ep001") {
|
if (response.code === "ss003") {
|
||||||
return { success: false, code: response.code };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.code === "sp001") {
|
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw createApiError(response.code, "Update storage failed");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteStorage = async (uuid: string) => {
|
export const deleteStorage = async (uuid: string) => {
|
||||||
@@ -84,11 +79,9 @@ export const deleteStorage = async (uuid: string) => {
|
|||||||
|
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
|
||||||
if (response.code === "es004") {
|
|
||||||
return { success: false, code: response.code };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.code === "ss004") {
|
if (response.code === "ss004") {
|
||||||
return { success: true, code: response.code };
|
return { success: true, code: response.code };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw createApiError(response.code, "Delete storage failed");
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user