fetch error message handling

master
alex 2024-01-25 20:38:59 +01:00
parent 260bebf8dc
commit a8f6f75d28
18 changed files with 766 additions and 1768 deletions

View File

@ -0,0 +1 @@
# Zeit Adler Dashboard

View File

@ -54,6 +54,20 @@
"passwordMinLength": "Passwort muss mindestens {{minLength}} Zeichen lang sein",
"calendarMaxFutureBookingDaysRequired": "Maximaler Buchungszeitraum ist erforderlich",
"calendarMinEarliestBookingTimeRequired": "Minimaler frühester Buchungszeitpunkt ist erforderlich"
},
"request": {
"inputsInvalid": {
"title": "Eingaben ungültig",
"description": "Bitte überprüfen Sie Ihre Eingaben."
},
"failed": {
"title": "Ein Fehler ist aufgetreten",
"description": "Bitte versuchen Sie es erneut."
},
"failedInternetProblem": {
"title": "Anfrage fehlgeschlagen",
"description": "Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut."
}
}
},
"pageNotFound": {
@ -98,7 +112,13 @@
},
"login": {
"login": "Anmelden",
"signUp": "Registrieren"
"signUp": "Registrieren",
"request": {
"400": {
"title": "Anmeldung fehlgeschlagen",
"description": "Bitte überprüfen Sie Ihre Eingaben."
}
}
},
"storeServices": {
"pageTitle": "Dienstleistungen",

View File

@ -54,6 +54,20 @@
"passwordMinLength": "Password must be at least {{minLength}} characters",
"calendarMaxFutureBookingDaysRequired": "Please enter the max. future booking days",
"calendarMinEarliestBookingTimeRequired": "Please enter the min. earliest booking time"
},
"request": {
"inputsInvalid": {
"title": "Invalid inputs",
"description": "Please check your inputs and try again."
},
"failed": {
"title": "An error has occurred",
"description": "The request failed. Please try again."
},
"failedInternetProblem": {
"title": "Request failed",
"description": "The request failed. Please check your internet connection and try again."
}
}
},
"pageNotFound": {
@ -98,7 +112,13 @@
},
"login": {
"login": "Login",
"signUp": "Sign up"
"signUp": "Sign up",
"request": {
"400": {
"title": "Login failed",
"description": "Please check your inputs and try again."
}
}
},
"services": {
"pageTitle": "Services"

View File

@ -24,7 +24,10 @@ export default function App() {
useEffect(() => {
if (!userSession) return;
myFetch("/user", "GET")
myFetch({
url: "/user",
method: "GET",
})
.then((data) => {
setAppUserData(data);
})

View File

@ -17,9 +17,7 @@ const StoreWebsite = lazy(() => import("../../Pages/Store/Website"));
//const Feedback = lazy(() => import("../../Pages/Feedback"));
const UserProfile = lazy(() => import("../../Pages/UserProfile"));
export default function AppRoutes({ userSession, setUserSession }) {
//const appContext = useAppContext();
export default function AppRoutes({ setUserSession }) {
return (
<Routes>
<Route
@ -111,10 +109,7 @@ export default function AppRoutes({ userSession, setUserSession }) {
path={Constants.ROUTE_PATHS.USER_PROFILE}
element={
<MySupsenseFallback>
<UserProfile
userSession={userSession}
setUserSession={setUserSession}
/>
<UserProfile setUserSession={setUserSession} />
</MySupsenseFallback>
}
/>

View File

@ -1,65 +1,11 @@
import {
BellOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
CloseOutlined,
DeleteOutlined,
ExclamationCircleOutlined,
InboxOutlined,
InfoCircleOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
QuestionCircleOutlined,
} from "@ant-design/icons";
import { Badge, Button, Drawer, List, Popconfirm, Typography } from "antd";
import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons";
import { Button } from "antd";
import { Header } from "antd/es/layout/layout";
import { useEffect, useState } from "react";
import { useHeaderContext } from "../../Contexts/HeaderContext";
import { myFetch } from "../../utils";
import { useWebSocketContext } from "../../Contexts/WebSocketContext";
import { SentMessagesCommands } from "../../Handlers/WebSocketMessageHandler";
import { useTranslation } from "react-i18next";
import LiveTimeAgo from "../LiveTimeAgo";
import MyPagination from "../MyPagination";
export default function HeaderMenu({
isSideMenuCollapsed,
setIsSideMenuCollapsed,
}) {
//const webSocketContext = useWebSocketContext();
//const headerContext = useHeaderContext();
//const { t } = useTranslation();
const [isNotificationDrawerOpen, setIsNotificationDrawerOpen] =
useState(false);
/*
const fetchNotifications = (page = 1) => {
myFetch(`/notifications?page=${page}`, "GET").then((data) =>
headerContext.setNotificationResponse(data)
);
};
const onPaginationChange = (page) => {
headerContext.setPaginationPage(page);
headerContext.paginationPageRef.current = page;
}; */
/*
useEffect(() => {
// fetch will only be called if the drawer is open and there are no notifications
// further notifications will be fetched by the websocket
if (!isNotificationDrawerOpen || headerContext.notficationResponse !== null)
return;
fetchNotifications(1);
}, [isNotificationDrawerOpen]);
useEffect(() => {
if (!isNotificationDrawerOpen) return;
fetchNotifications(headerContext.paginationPage);
}, [headerContext.paginationPage]); */
return (
<Header
style={{
@ -85,112 +31,3 @@ export default function HeaderMenu({
</Header>
);
}
/*
<Button
type="text"
icon={
<Badge count={headerContext.totalNotifications} offset={[2, -2]}>
<BellOutlined style={{ fontSize: "16px" }} />
</Badge>
}
onClick={() => setIsNotificationDrawerOpen(true)}
style={{ fontSize: "16px", width: 64, height: 64 }}
/>
<Drawer
title={t("header.notificationDrawer.title")}
placement="right"
open={isNotificationDrawerOpen}
onClose={() => setIsNotificationDrawerOpen(false)}
extra={
headerContext.totalNotifications > 0 && (
<Popconfirm
title={t("header.notificationDrawer.deleteAllPopconfirm.title")}
okText={t("common.button.confirm")}
cancelText={t("common.button.cancel")}
onConfirm={() => {
webSocketContext.SendSocketMessage(
SentMessagesCommands.DeleteAllNotifications,
{}
);
setIsNotificationDrawerOpen(false);
}}
>
<Button type="link" icon={<DeleteOutlined />}>
{t("header.notificationDrawer.deleteAllButtonText")}
</Button>
</Popconfirm>
)
}
>
{isNotificationDrawerOpen && (
<>
{headerContext.totalNotifications === 0 ||
headerContext.notficationResponse === null ? (
<div style={{ textAlign: "center" }}>
<InboxOutlined style={{ fontSize: 32, marginBottom: 10 }} />
<Typography.Title level={5}>
{t("header.notificationDrawer.noNotifications")}
</Typography.Title>
</div>
) : (
<List
dataSource={headerContext.notficationResponse.Notifications.sort(
(a, b) => {
return new Date(b.CreatedAt) - new Date(a.CreatedAt);
}
)}
footer={
<MyPagination
paginationPage={headerContext.paginationPage}
setPaginationPage={(page) => onPaginationChange(page)}
totalPages={headerContext.notficationResponse.TotalPages}
size="small"
/>
}
renderItem={(item) => (
<List.Item>
<List.Item.Meta
avatar={<NotificationTypeIcon type={item.Type} />}
title={item.Title}
description={<LiveTimeAgo startTime={item.CreatedAt} />}
/>
<CloseOutlined
onClick={() => {
/*webSocketContext.SendSocketMessage(
SentMessagesCommands.DeleteOneNotification,
{
notificationId: item.Id,
}
)
}}
/>
</List.Item>
)}
/>
)}
</>
)}
</Drawer>
*/
function NotificationTypeIcon({ type }) {
switch (type) {
case 1:
return <CheckCircleOutlined color="#33a834" style={{ fontSize: 16 }} />;
case 2:
return <InfoCircleOutlined color="#0c69d7" style={{ fontSize: 16 }} />;
case 3:
return (
<ExclamationCircleOutlined color="#dd9433" style={{ fontSize: 16 }} />
);
case 4:
return <CloseCircleOutlined color="#e5444b" style={{ fontSize: 16 }} />;
default:
return <QuestionCircleOutlined color="#fff" style={{ fontSize: 16 }} />;
}
}

View File

@ -187,7 +187,11 @@ export function MyAvailableCheckFormInput({
body[fetchParameter] = value; // like accountName: value
myFetch(fetchUrl, "POST", body)
myFetch({
url: fetchUrl,
method: "POST",
body: body,
})
.then(() => {
resolve();
})

View File

@ -4,7 +4,7 @@ import {
PlusOutlined,
QuestionCircleOutlined,
} from "@ant-design/icons";
import { Popconfirm, Tooltip } from "antd";
import { Popconfirm } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
@ -79,14 +79,14 @@ export function MyPlusIcon({ onClick }) {
export function MyIcon({
popconfirmDisabled,
propsPopconfirm,
propsTooltip,
// propsTooltip,
onConfirm,
onFetchSuccess,
onCancel,
popConfirmTitle,
popConfirmDescription,
popConfirmOkText,
tooltipTitle,
//tooltipTitle,
icon,
}) {
const { t } = useTranslation();

View File

@ -3,8 +3,8 @@ import { Button, Form, Modal, Tabs, notification } from "antd";
import {
EncodeStringToBase64,
myFetch,
myFetchContentType,
setUserSessionToLocalStorage,
showInputsInvalidNotification,
} from "../../utils";
import { useState } from "react";
import {
@ -19,31 +19,27 @@ export default function Login() {
const { t } = useTranslation();
const [form] = Form.useForm();
const [api, contextHolder] = notification.useNotification();
const [notificationApi, notificationContextHolder] =
notification.useNotification();
const [selectedMethod, setSelectedMethod] = useState("1");
const [isRequesting, setIsRequesting] = useState(false);
const showErrorNotification = (errStatus) => {
if (errStatus === 401) {
api["error"]({
message: "Account deactivated",
description: "Please contact an administrator",
if (errStatus === 400) {
notificationApi["error"]({
message: t("login.request.400.title"),
description: t("login.request.400.description"),
});
return;
}
api["error"]({
message: "Login failed",
description: "Please check your accountName and password!",
});
};
return (
<>
{contextHolder}
{notificationContextHolder}
<Modal
open={true}
mask={false}
closable={false}
centered
keyboard={false}
@ -68,18 +64,16 @@ export default function Login() {
body.username = values.username;
}
myFetch(
`/user/auth/${selectedMethod === "1" ? "login" : "signup"}`,
"POST",
body,
{},
myFetchContentType.JSON,
"",
true
)
myFetch({
url: `/user/auth/${
selectedMethod === "1" ? "login" : "signup"
}`,
method: "POST",
body: body,
notificationApi: notificationApi,
t: t,
})
.then((data) => {
console.log(data.XAuthorization);
setUserSessionToLocalStorage(data.XAuthorization);
window.location.href = "/";
})
@ -88,9 +82,7 @@ export default function Login() {
setIsRequesting(false);
});
})
.catch((info) => {
console.log("Validate Failed:", info);
});
.catch(() => showInputsInvalidNotification(notificationApi, t));
}}
>
{selectedMethod === "1" ? t("login.login") : t("login.signUp")}
@ -119,9 +111,7 @@ export default function Login() {
},
]}
centered
onChange={(activeKey) => {
setSelectedMethod(activeKey);
}}
onChange={(activeKey) => setSelectedMethod(activeKey)}
/>
<Form form={form} layout="vertical" requiredMark={false}>

View File

@ -1,20 +1,27 @@
import { Button, Result, Spin } from "antd";
import { Button, Result, Spin, notification } from "antd";
import { Link, useParams, useNavigate } from "react-router-dom";
import { Constants, myFetch } from "../../../../utils";
import { useEffect, useState } from "react";
import MyCenteredContainer from "../../../../Components/MyContainer";
import { useTranslation } from "react-i18next";
import { MySupsenseFallback } from "../../../../Components/MySupsenseFallback";
export default function StoreCalendarAuth() {
const { t } = useTranslation();
const [notificationApi, notificationContextHolder] =
notification.useNotification();
const { status } = useParams();
const [isRequesting, setIsRequesting] = useState(true);
const [storeId, setStoreId] = useState("");
useEffect(() => {
myFetch("/calendar/store", "GET")
myFetch({
url: "/calendar/store",
method: "GET",
notificationApi: notificationApi,
t: t,
})
.then((res) => {
setIsRequesting(false);
setStoreId(res.storeId);
@ -33,32 +40,36 @@ export default function StoreCalendarAuth() {
}
return (
<Result
status={status === "finish" ? "success" : "error"}
title={
status === "finish"
? t("calendar.authFinish.title")
: t("calendar.authFailed.title")
}
subTitle={
status === "finish"
? t("calendar.authFinish.description")
: t("calendar.authFailed.description")
}
extra={[
<div key="1">
<Link to={`${Constants.ROUTE_PATHS.STORE.CALENDAR}/${storeId}`}>
<Button>
{status === "finish"
? t("calendar.authFinish.button")
: t("calendar.authFailed.button")}
</Button>
</Link>
<>
{notificationContextHolder}
{status === "finish" && <CountdownRedirect storeId={storeId} />}
</div>,
]}
/>
<Result
status={status === "finish" ? "success" : "error"}
title={
status === "finish"
? t("calendar.authFinish.title")
: t("calendar.authFailed.title")
}
subTitle={
status === "finish"
? t("calendar.authFinish.description")
: t("calendar.authFailed.description")
}
extra={[
<div key="1">
<Link to={`${Constants.ROUTE_PATHS.STORE.CALENDAR}/${storeId}`}>
<Button>
{status === "finish"
? t("calendar.authFinish.button")
: t("calendar.authFailed.button")}
</Button>
</Link>
{status === "finish" && <CountdownRedirect storeId={storeId} />}
</div>,
]}
/>
</>
);
}

View File

@ -10,6 +10,7 @@ import {
Spin,
Switch,
Typography,
notification,
} from "antd";
import { useTranslation } from "react-i18next";
import {
@ -38,6 +39,9 @@ const { useBreakpoint } = Grid;
export default function StoreCalendar() {
const { t } = useTranslation();
const [notificationApi, notificationContextHolder] =
notification.useNotification();
const { storeId } = useParams();
const [calendarSettings, setCalendarSettings] = useState({});
@ -47,7 +51,12 @@ export default function StoreCalendar() {
// delete session cookie
document.cookie = `session=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
myFetch("/calendar/settings", "GET")
myFetch({
url: "/calendar/settings",
method: "GET",
notificationApi: notificationApi,
t: t,
})
.then((res) => {
setIsRequesting(false);
setCalendarSettings(res);
@ -67,6 +76,8 @@ export default function StoreCalendar() {
return (
<>
{notificationContextHolder}
{calendarSettings.connected === false ? (
<MyCenteredContainer>
<Result
@ -135,6 +146,7 @@ function CalendarFrame({ storeId }) {
</div>
)}
<iframe
title="calendar"
onLoad={() => setIsLoading(false)}
style={{ border: 0, borderRadius: 12 }}
width="100%"
@ -147,6 +159,9 @@ function CalendarFrame({ storeId }) {
function CardPersonalCalendarSettings({ settings }) {
const { t } = useTranslation();
const [notificationApi, notificationContextHolder] =
notification.useNotification();
const [form] = Form.useForm();
const [formUnlinkCalendar] = Form.useForm();
const screenBreakpoint = useBreakpoint();
@ -189,8 +204,14 @@ function CardPersonalCalendarSettings({ settings }) {
}
delayTimeout.current = setTimeout(() => {
myFetch("/calendar/settings/personal", "POST", {
calendarUsingPrimaryCalendar: usingPrimaryCalendar,
myFetch({
url: "/calendar/settings/personal",
method: "POST",
body: {
calendarUsingPrimaryCalendar: usingPrimaryCalendar,
},
notificationApi: notificationApi,
t: t,
})
.then(() => setRequestState(RequestState.SUCCESS))
.catch((errStatus) => {
@ -239,6 +260,8 @@ function CardPersonalCalendarSettings({ settings }) {
return (
<>
{notificationContextHolder}
<Card
title={
screenBreakpoint.xl ? (
@ -290,8 +313,14 @@ function CardPersonalCalendarSettings({ settings }) {
formUnlinkCalendar.validateFields().then((values) => {
setIsRequesting(true);
myFetch("/calendar/settings/personal/unlink", "POST", {
password: EncodeStringToBase64(values.password),
myFetch({
url: "/calendar/settings/personal/unlink",
method: "POST",
body: {
password: EncodeStringToBase64(values.password),
},
notificationApi: notificationApi,
t: t,
})
.then(() => window.location.reload())
.catch((errStatus) => {
@ -321,6 +350,9 @@ function CardPersonalCalendarSettings({ settings }) {
function CardStoreCalendarSettings({ settings }) {
const { t } = useTranslation();
const [notificationApi, notificationContextHolder] =
notification.useNotification();
const [form] = Form.useForm();
const [requestState, setRequestState] = useState(RequestState.INIT);
@ -369,9 +401,15 @@ function CardStoreCalendarSettings({ settings }) {
}
delayTimeout.current = setTimeout(() => {
myFetch("/calendar/settings/store", "POST", {
calendarMaxFutureBookingDays,
calendarMinEarliestBookingTime,
myFetch({
url: "/calendar/settings/store",
method: "POST",
body: {
calendarMaxFutureBookingDays,
calendarMinEarliestBookingTime,
},
notificationApi: notificationApi,
t: t,
})
.then(() => setRequestState(RequestState.SUCCESS))
.catch((errStatus) => {
@ -382,22 +420,26 @@ function CardStoreCalendarSettings({ settings }) {
}, [calendarMaxFutureBookingDays, calendarMinEarliestBookingTime]);
return (
<Card
title={t("calendar.cardStoreCalendarSettings.title")}
extra={
<RequestStateItem
state={requestState}
setRequestState={setRequestState}
/>
}
>
<Form form={form} requiredMark={false}>
<>
<MyCalendarMaxFutureBookingDaysFormInput />
<>
{notificationContextHolder}
<MyCalendarMinEarliestBookingTimeFormInput />
</>
</Form>
</Card>
<Card
title={t("calendar.cardStoreCalendarSettings.title")}
extra={
<RequestStateItem
state={requestState}
setRequestState={setRequestState}
/>
}
>
<Form form={form} requiredMark={false}>
<>
<MyCalendarMaxFutureBookingDaysFormInput />
<MyCalendarMinEarliestBookingTimeFormInput />
</>
</Form>
</Card>
</>
);
}

View File

@ -1,5 +1,13 @@
import { PlusOutlined } from "@ant-design/icons";
import { Button, Checkbox, Form, Grid, Popconfirm, Space } from "antd";
import {
Button,
Checkbox,
Form,
Grid,
Popconfirm,
Space,
notification,
} from "antd";
import MyModal, {
MyModalCloseCreateButtonFooter,
MyModalCloseSaveButtonFooter,
@ -21,6 +29,9 @@ const { useBreakpoint } = Grid;
export default function StoreEmployees() {
const { t } = useTranslation();
const [notificationApi, notificationContextHolder] =
notification.useNotification();
const screenBreakpoint = useBreakpoint();
const { storeId } = useParams();
@ -85,8 +96,12 @@ export default function StoreEmployees() {
onConfirm={() => {
setIsRequesting(true);
myFetch("/users", "DELETE", {
userId: record.key,
myFetch({
url: "/users",
method: "DELETE",
body: { userId: record.key },
notificationApi: notificationApi,
t: t,
})
.then(() => fetchEmployees())
.catch((errStatus) => {
@ -123,7 +138,12 @@ export default function StoreEmployees() {
const fetchEmployees = () => {
setIsRequesting(true);
myFetch(`/users/${storeId}`, "GET")
myFetch({
url: `/users/${storeId}`,
method: "GET",
notificationApi: notification,
t: t,
})
.then((data) => {
setIsRequesting(false);
setRequestData(data);
@ -137,6 +157,8 @@ export default function StoreEmployees() {
return (
<>
{notificationContextHolder}
<div
style={{
display: "flex",
@ -271,7 +293,13 @@ function ModalAddEditEmployee({
values.calendarMinEarliestBookingTime;
}
myFetch("/users", "POST", body)
myFetch({
url: "/users",
method: "POST",
body: body,
notificationApi: notification,
t: t,
})
.then(() => {
setIsRequesting(false);
handleModalClose();
@ -357,7 +385,13 @@ function ModalAddEditEmployee({
.then(() => {
setIsRequesting(true);
myFetch("/users/update", "POST", body)
myFetch({
url: "/users/update",
method: "POST",
body: body,
notificationApi: notification,
t: t,
})
.then(() => {
setIsRequesting(false);
handleModalClose();

View File

@ -11,6 +11,7 @@ import {
Spin,
Tooltip,
Typography,
notification,
} from "antd";
import { useTranslation } from "react-i18next";
import { useEffect, useState } from "react";
@ -33,6 +34,8 @@ const { useBreakpoint } = Grid;
export default function StoreServices() {
const { t } = useTranslation();
const [notificationApi, notificationContextHolder] =
notification.useNotification();
const screenBreakpoint = useBreakpoint();
const { storeId } = useParams();
@ -58,7 +61,12 @@ export default function StoreServices() {
const fetchServices = () => {
setIsRequestingServices(true);
myFetch(`/store/services/${storeId}`, "GET")
myFetch({
url: `/store/services/${storeId}`,
method: "GET",
notificationApi: notificationApi,
t: t,
})
.then((data) => {
setIsRequestingServices(false);
setServicesData(data);
@ -72,6 +80,8 @@ export default function StoreServices() {
return (
<>
{notificationContextHolder}
<div
style={{
display: "flex",
@ -148,6 +158,8 @@ function Service({
fetchServices,
}) {
const { t } = useTranslation();
const [notificationApi, notificationContextHolder] =
notification.useNotification();
const [isRequestingActivities, setIsRequestingActivities] = useState(false);
const [isOpen, setIsOpen] = useState(false);
@ -159,10 +171,12 @@ function Service({
setIsRequestingActivities(true);
myFetch(
`/store/services/activities/${storeId}/${service.service_id}`,
"GET"
)
myFetch({
url: `/store/services/activities/${storeId}/${service.service_id}`,
method: "GET",
notificationApi: notificationApi,
t: t,
})
.then((data) => {
setIsRequestingActivities(false);
setServiceActivities(data.activities);
@ -175,24 +189,27 @@ function Service({
useEffect(() => fetchServiceActivities(), [isOpen]);
return (
<Collapse
key={service.service_id}
onChange={(e) => setIsOpen(e.length !== 0)}
items={[
{
key: "1",
label: (
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<span>{service.name}</span>
<>
{notificationContextHolder}
<Space>
{/*
<Collapse
key={service.service_id}
onChange={(e) => setIsOpen(e.length !== 0)}
items={[
{
key: "1",
label: (
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<span>{service.name}</span>
<Space>
{/*
<ArrowUpOutlined
disabled
onClick={(e) => e.stopPropagation()}
@ -202,192 +219,202 @@ function Service({
onClick={(e) => e.stopPropagation()}
/>
*/}
<MyPlusIcon
onClick={(e) => {
e.stopPropagation();
<MyPlusIcon
onClick={(e) => {
e.stopPropagation();
setAddEditServiceActivityModalOptions({
mode: "add",
isOpen: true,
service: service,
});
}}
/>
setAddEditServiceActivityModalOptions({
mode: "add",
isOpen: true,
service: service,
});
}}
/>
<MyEditIcon
onClick={(e) => {
e.stopPropagation();
<MyEditIcon
onClick={(e) => {
e.stopPropagation();
setAddEditServiceModalOptions({
mode: "edit",
isOpen: true,
service: service,
});
}}
/>
<MyDeleteIcon
onClick={(e) => e.stopPropagation()}
propsPopconfirm={{
placement: "left",
}}
onConfirm={() => {
return myFetch(
`/store/services/${service.service_id}`,
"DELETE"
);
}}
onFetchSuccess={fetchServices}
popConfirmTitle={t(
"storeServices.popConfirmDeleteService.title"
)}
popConfirmDescription={t(
"storeServices.popConfirmDeleteService.description"
)}
/>
</Space>
</div>
),
children: (
<Space
key={`space-${service.service_id}`}
direction="vertical"
style={{ width: "100%" }}
>
{isRequestingActivities ? (
<Skeleton active>
<Card title="loading">
<p>loading</p>
<p>loading</p>
<p>loading</p>
</Card>
</Skeleton>
) : (
<>
{serviceActivities.length === 0 ? (
<MyEmpty />
) : (
serviceActivities.map((activity) => {
let userList = [];
setAddEditServiceModalOptions({
mode: "edit",
isOpen: true,
service: service,
});
}}
/>
<MyDeleteIcon
onClick={(e) => e.stopPropagation()}
propsPopconfirm={{
placement: "left",
}}
onConfirm={() => {
return myFetch({
url: `/store/services/${service.service_id}`,
method: "DELETE",
notificationApi: notificationApi,
t: t,
});
}}
onFetchSuccess={fetchServices}
popConfirmTitle={t(
"storeServices.popConfirmDeleteService.title"
)}
popConfirmDescription={t(
"storeServices.popConfirmDeleteService.description"
)}
/>
</Space>
</div>
),
children: (
<Space
key={`space-${service.service_id}`}
direction="vertical"
style={{ width: "100%" }}
>
{isRequestingActivities ? (
<Skeleton active>
<Card title="loading">
<p>loading</p>
<p>loading</p>
<p>loading</p>
</Card>
</Skeleton>
) : (
<>
{serviceActivities.length === 0 ? (
<MyEmpty />
) : (
serviceActivities.map((activity) => {
let userList = [];
if (activity.StoreServiceActivityUsers.length > 0) {
// StoreServiceActivityUsers is only an array of user_ids
// we need to get the user object from the users array
if (activity.StoreServiceActivityUsers.length > 0) {
// StoreServiceActivityUsers is only an array of user_ids
// we need to get the user object from the users array
for (let i = 0; i < users.length; i++) {
for (
let j = 0;
j < activity.StoreServiceActivityUsers.length;
j++
) {
if (
users[i].user_id ===
activity.StoreServiceActivityUsers[j].user_id
for (let i = 0; i < users.length; i++) {
for (
let j = 0;
j < activity.StoreServiceActivityUsers.length;
j++
) {
userList.push(users[i]);
if (
users[i].user_id ===
activity.StoreServiceActivityUsers[j].user_id
) {
userList.push(users[i]);
}
}
}
} else {
// if there are no users assigned to this activity, we just use the whole users array
userList = users;
}
} else {
// if there are no users assigned to this activity, we just use the whole users array
userList = users;
}
return (
<Card
key={activity.activity_id}
title={activity.name}
extra={
<Space>
<Avatar.Group maxCount={2} size="small">
{userList.map((user) => (
<Tooltip
key={user.user_id}
title={user.username}
>
<Avatar
size="small"
style={{ backgroundColor: "#6878d6" }}
return (
<Card
key={activity.activity_id}
title={activity.name}
extra={
<Space>
<Avatar.Group maxCount={2} size="small">
{userList.map((user) => (
<Tooltip
key={user.user_id}
title={user.username}
>
{user.username.charAt(0)}
</Avatar>
</Tooltip>
))}
</Avatar.Group>
<Avatar
size="small"
style={{ backgroundColor: "#6878d6" }}
>
{user.username.charAt(0)}
</Avatar>
</Tooltip>
))}
</Avatar.Group>
{/*
{/*
<ArrowUpOutlined disabled />
<ArrowDownOutlined disabled />
*/}
<MyEditIcon
onClick={() => {
setAddEditServiceActivityModalOptions({
mode: "edit",
isOpen: true,
activity: activity,
});
}}
/>
<MyEditIcon
onClick={() => {
setAddEditServiceActivityModalOptions({
mode: "edit",
isOpen: true,
activity: activity,
});
}}
/>
<MyDeleteIcon
propsPopconfirm={{
placement: "left",
}}
onConfirm={() => {
return myFetch(
`/store/services/activity/${activity.activity_id}`,
"DELETE"
);
}}
onFetchSuccess={fetchServiceActivities}
popConfirmTitle={t(
"storeServices.popConfirmDeleteServiceActivity.title"
<MyDeleteIcon
propsPopconfirm={{
placement: "left",
}}
onConfirm={() => {
return myFetch({
url: `/store/services/activity/${activity.activity_id}`,
method: "DELETE",
notificationApi: notificationApi,
t: t,
});
}}
onFetchSuccess={fetchServiceActivities}
popConfirmTitle={t(
"storeServices.popConfirmDeleteServiceActivity.title"
)}
popConfirmDescription={t(
"storeServices.popConfirmDeleteServiceActivity.description"
)}
/>
</Space>
}
>
<Typography.Title level={5}>
{t("storeServices.serviceActivityDescription")}
</Typography.Title>
<Typography.Paragraph>
{activity.description}
</Typography.Paragraph>
<Typography.Title level={5}>
{t("storeServices.serviceActivityPrice")}
</Typography.Title>
<p>{activity.price} </p>
<Typography.Title level={5}>
{t(
"storeServices.serviceActivityDurationMinutes"
)}
</Typography.Title>
<Typography.Paragraph>
{activity.duration}{" "}
{activity.duration === 1
? t("common.unit.minute")
: t("common.unit.minutes")}{" "}
<Typography.Text type="secondary">
{durationToHoursAndMinutes(
t,
activity.duration
)}
popConfirmDescription={t(
"storeServices.popConfirmDeleteServiceActivity.description"
)}
/>
</Space>
}
>
<Typography.Title level={5}>
{t("storeServices.serviceActivityDescription")}
</Typography.Title>
<Typography.Paragraph>
{activity.description}
</Typography.Paragraph>
<Typography.Title level={5}>
{t("storeServices.serviceActivityPrice")}
</Typography.Title>
<p>{activity.price} </p>
<Typography.Title level={5}>
{t("storeServices.serviceActivityDurationMinutes")}
</Typography.Title>
<Typography.Paragraph>
{activity.duration}{" "}
{activity.duration === 1
? t("common.unit.minute")
: t("common.unit.minutes")}{" "}
<Typography.Text type="secondary">
{durationToHoursAndMinutes(t, activity.duration)}
</Typography.Text>
</Typography.Paragraph>
</Card>
);
})
)}
</>
)}
</Space>
),
},
]}
/>
</Typography.Text>
</Typography.Paragraph>
</Card>
);
})
)}
</>
)}
</Space>
),
},
]}
/>
</>
);
}
@ -430,6 +457,8 @@ function ModalAddEditService({
setAddEditServiceModalOptions,
}) {
const { t } = useTranslation();
const [notificationApi, notificationContextHolder] =
notification.useNotification();
const screenBreakpoint = useBreakpoint();
const [form] = Form.useForm();
@ -456,6 +485,8 @@ function ModalAddEditService({
return (
<>
{notificationContextHolder}
<Button
type="primary"
icon={<PlusOutlined />}
@ -489,9 +520,15 @@ function ModalAddEditService({
.then((values) => {
setIsRequesting(true);
myFetch("/store/services", "POST", {
storeId: storeId,
name: values.serviceName,
myFetch({
url: "/store/services",
method: "POST",
body: {
storeId: storeId,
name: values.serviceName,
},
notificationApi: notificationApi,
t: t,
})
.then(() => {
setIsRequesting(false);
@ -527,9 +564,16 @@ function ModalAddEditService({
.then(() => {
setIsRequesting(true);
myFetch("/store/services/update", "POST", {
serviceId: addEditServiceModalOptions.service.service_id,
name: formServiceName,
myFetch({
url: "/store/services/update",
method: "POST",
body: {
serviceId:
addEditServiceModalOptions.service.service_id,
name: formServiceName,
},
notificationApi: notificationApi,
t: t,
})
.then(() => {
setIsRequesting(false);
@ -564,7 +608,8 @@ function ModalAddEditServiceActivity({
users,
}) {
const { t } = useTranslation();
const [notificationApi, notificationContextHolder] =
notification.useNotification();
const [form] = Form.useForm();
const [isRequesting, setIsRequesting] = useState(false);
const [selectedEmployeesRowKeys, setSelectedEmployeesRowKeys] = useState([]);
@ -629,186 +674,202 @@ function ModalAddEditServiceActivity({
}, [addEditServiceActivityModalOptions.isOpen]);
return (
<MyModal
title={
addEditServiceActivityModalOptions.mode === "add"
? t("storeServices.modalAddServiceActivity.title")
: t("storeServices.modalEditServiceActivity.title")
}
isOpen={addEditServiceActivityModalOptions.isOpen}
onCancel={handleModalClose}
footer={
addEditServiceActivityModalOptions.mode === "add" ? (
<MyModalCloseCreateButtonFooter
onCancel={handleModalClose}
isCreateButtonLoading={isRequesting}
onCreate={() => {
form
.validateFields()
.then((values) => {
setIsRequesting(true);
<>
{notificationContextHolder}
myFetch("/store/services/activity", "POST", {
serviceId:
addEditServiceActivityModalOptions.service.service_id,
name: values.serviceActivityName,
description: values.serviceActivityDescription,
price: values.serviceActivityPrice,
duration: values.serviceActivityDurationMinutes,
userIds:
selectedEmployeesRowKeys.length === users.length
? []
: selectedEmployeesRowKeys,
<MyModal
title={
addEditServiceActivityModalOptions.mode === "add"
? t("storeServices.modalAddServiceActivity.title")
: t("storeServices.modalEditServiceActivity.title")
}
isOpen={addEditServiceActivityModalOptions.isOpen}
onCancel={handleModalClose}
footer={
addEditServiceActivityModalOptions.mode === "add" ? (
<MyModalCloseCreateButtonFooter
onCancel={handleModalClose}
isCreateButtonLoading={isRequesting}
onCreate={() => {
form
.validateFields()
.then((values) => {
setIsRequesting(true);
myFetch({
url: "/store/services/activity",
method: "POST",
body: {
serviceId:
addEditServiceActivityModalOptions.service.service_id,
name: values.serviceActivityName,
description: values.serviceActivityDescription,
price: values.serviceActivityPrice,
duration: values.serviceActivityDurationMinutes,
userIds:
selectedEmployeesRowKeys.length === users.length
? []
: selectedEmployeesRowKeys,
},
notificationApi: notificationApi,
t: t,
})
.then(() => {
setIsRequesting(false);
handleModalClose();
fetchServices();
})
.catch((errStatus) => {
console.log(errStatus);
});
})
.then(() => {
setIsRequesting(false);
handleModalClose();
.catch((info) => {
console.log("Validate Failed:", info);
});
}}
/>
) : (
<MyModalCloseSaveButtonFooter
onCancel={handleModalClose}
isSaveButtonLoading={isRequesting}
onSave={() => {
const formServiceActivityName = form.getFieldValue(
"serviceActivityName"
);
const formServiceActivityDescription = form.getFieldValue(
"serviceActivityDescription"
);
const formServiceActivityPrice = form.getFieldValue(
"serviceActivityPrice"
);
const formServiceActivityDurationMinutes = form.getFieldValue(
"serviceActivityDurationMinutes"
);
fetchServices();
// if the service didn't change, don't send a request
if (
addEditServiceActivityModalOptions.activity.name ===
formServiceActivityName &&
addEditServiceActivityModalOptions.activity.description ===
formServiceActivityDescription &&
addEditServiceActivityModalOptions.activity.price ===
formServiceActivityPrice &&
addEditServiceActivityModalOptions.activity.duration ===
formServiceActivityDurationMinutes &&
(selectedEmployeesRowKeys.length === users.length ||
addEditServiceActivityModalOptions.activity
.StoreServiceActivityUsers.length ===
selectedEmployeesRowKeys.length)
) {
handleModalClose();
return;
}
let validateFields = [];
let body = {
activityId:
addEditServiceActivityModalOptions.activity.activity_id,
};
if (
formServiceActivityName !==
addEditServiceActivityModalOptions.activity.name
) {
validateFields.push("serviceActivityName");
body.name = formServiceActivityName;
}
if (
formServiceActivityDescription !==
addEditServiceActivityModalOptions.activity.description
) {
validateFields.push("serviceActivityDescription");
body.description = formServiceActivityDescription;
}
if (
formServiceActivityPrice !==
addEditServiceActivityModalOptions.activity.price
) {
validateFields.push("serviceActivityPrice");
body.price = formServiceActivityPrice;
}
let formDuration = formServiceActivityDurationMinutes;
if (
formDuration !==
addEditServiceActivityModalOptions.activity.duration
) {
validateFields.push("serviceActivityDurationMinutes");
body.duration = formDuration;
}
body.userIds = selectedEmployeesRowKeys;
form
.validateFields()
.then(() => {
setIsRequesting(true);
myFetch({
url: "/store/services/activity/update",
method: "POST",
body: body,
notificationApi: notificationApi,
t: t,
})
.catch((errStatus) => {
console.log(errStatus);
});
})
.catch((info) => {
console.log("Validate Failed:", info);
});
}}
/>
) : (
<MyModalCloseSaveButtonFooter
onCancel={handleModalClose}
isSaveButtonLoading={isRequesting}
onSave={() => {
const formServiceActivityName = form.getFieldValue(
"serviceActivityName"
);
const formServiceActivityDescription = form.getFieldValue(
"serviceActivityDescription"
);
const formServiceActivityPrice = form.getFieldValue(
"serviceActivityPrice"
);
const formServiceActivityDurationMinutes = form.getFieldValue(
"serviceActivityDurationMinutes"
);
.then(() => {
setIsRequesting(false);
handleModalClose();
// if the service didn't change, don't send a request
if (
addEditServiceActivityModalOptions.activity.name ===
formServiceActivityName &&
addEditServiceActivityModalOptions.activity.description ===
formServiceActivityDescription &&
addEditServiceActivityModalOptions.activity.price ===
formServiceActivityPrice &&
addEditServiceActivityModalOptions.activity.duration ===
formServiceActivityDurationMinutes &&
(selectedEmployeesRowKeys.length === users.length ||
addEditServiceActivityModalOptions.activity
.StoreServiceActivityUsers.length ===
selectedEmployeesRowKeys.length)
) {
handleModalClose();
return;
}
fetchServices();
})
.catch((errStatus) => {
console.log(errStatus);
});
})
.catch((info) => {
console.log("Validate Failed:", info);
});
}}
/>
)
}
>
<Form form={form} layout="vertical" requiredMark={false}>
<ServiceActivityNameFormInput formItemName="serviceActivityName" />
let validateFields = [];
let body = {
activityId:
addEditServiceActivityModalOptions.activity.activity_id,
};
<ServiceActivityDescriptionFormInput formItemName="serviceActivityDescription" />
if (
formServiceActivityName !==
addEditServiceActivityModalOptions.activity.name
) {
validateFields.push("serviceActivityName");
body.name = formServiceActivityName;
}
<ServiceActivityPriceFormInput formItemName="serviceActivityPrice" />
if (
formServiceActivityDescription !==
addEditServiceActivityModalOptions.activity.description
) {
validateFields.push("serviceActivityDescription");
body.description = formServiceActivityDescription;
}
<ServiceActivityDurationMinutesFormInput formItemName="serviceActivityDurationMinutes" />
if (
formServiceActivityPrice !==
addEditServiceActivityModalOptions.activity.price
) {
validateFields.push("serviceActivityPrice");
body.price = formServiceActivityPrice;
}
<Space direction="vertical" style={{ width: "100%" }}>
<Typography.Text>
{t("storeServices.serviceActivityResponsible")}
</Typography.Text>
let formDuration = formServiceActivityDurationMinutes;
if (
formDuration !==
addEditServiceActivityModalOptions.activity.duration
) {
validateFields.push("serviceActivityDurationMinutes");
body.duration = formDuration;
}
body.userIds = selectedEmployeesRowKeys;
form
.validateFields()
.then(() => {
setIsRequesting(true);
myFetch("/store/services/activity/update", "POST", body)
.then(() => {
setIsRequesting(false);
handleModalClose();
fetchServices();
})
.catch((errStatus) => {
console.log(errStatus);
});
})
.catch((info) => {
console.log("Validate Failed:", info);
});
}}
/>
)
}
>
<Form form={form} layout="vertical" requiredMark={false}>
<ServiceActivityNameFormInput formItemName="serviceActivityName" />
<ServiceActivityDescriptionFormInput formItemName="serviceActivityDescription" />
<ServiceActivityPriceFormInput formItemName="serviceActivityPrice" />
<ServiceActivityDurationMinutesFormInput formItemName="serviceActivityDurationMinutes" />
<Space direction="vertical" style={{ width: "100%" }}>
<Typography.Text>
{t("storeServices.serviceActivityResponsible")}
</Typography.Text>
<MyTable
props={{
rowSelection: {
selectedRowKeys: selectedEmployeesRowKeys,
onChange: (newSelectedRowKeys) =>
setSelectedEmployeesRowKeys(newSelectedRowKeys),
},
loading: isRequesting,
columns: getTableColumns(),
dataSource: getTableItems(),
size: "small",
pagination: false,
}}
/>
</Space>
</Form>
</MyModal>
<MyTable
props={{
rowSelection: {
selectedRowKeys: selectedEmployeesRowKeys,
onChange: (newSelectedRowKeys) =>
setSelectedEmployeesRowKeys(newSelectedRowKeys),
},
loading: isRequesting,
columns: getTableColumns(),
dataSource: getTableItems(),
size: "small",
pagination: false,
}}
/>
</Space>
</Form>
</MyModal>
</>
);
}

View File

@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from "react";
import { myFetch } from "../../../utils";
import { useParams } from "react-router-dom";
import { Card, Form } from "antd";
import { Card, Form, notification } from "antd";
import { MyFormInput } from "../../../Components/MyFormInputs";
import { useTranslation } from "react-i18next";
import {
@ -11,6 +11,8 @@ import {
export default function StoreSettings() {
const { t } = useTranslation();
const [notificationApi, notificationContextHolder] =
notification.useNotification();
const { storeId } = useParams();
const [form] = Form.useForm();
@ -26,7 +28,12 @@ export default function StoreSettings() {
const address = Form.useWatch("address", form);
useEffect(() => {
myFetch(`/store/${storeId}`, "GET")
myFetch({
url: `/store/${storeId}`,
method: "GET",
notificationApi: notificationApi,
t: t,
})
.then((data) => {
setStoreData(data.store);
@ -65,11 +72,17 @@ export default function StoreSettings() {
}
delayTimeout.current = setTimeout(() => {
myFetch(`/store/${storeId}`, "POST", {
name: companyName,
phoneNumber,
email,
address,
myFetch({
url: `/store/${storeId}`,
method: "POST",
body: {
name: companyName,
phoneNumber,
email,
address,
},
notificationApi: notificationApi,
t: t,
})
.then(() => setRequestState(RequestState.SUCCESS))
.catch((errStatus) => {
@ -81,6 +94,8 @@ export default function StoreSettings() {
return (
<>
{notificationContextHolder}
<Card
title={t("storeSettings.pageTitle")}
loading={isRequesting}

View File

@ -2,7 +2,7 @@ import { lazy, useEffect, useState } from "react";
import { isDevelopmentEnv, myFetch } from "../../../utils";
import { useParams } from "react-router-dom";
import MyCenteredContainer from "../../../Components/MyContainer";
import { Button, Card, Result, Spin, Tabs } from "antd";
import { Button, Card, Result, Spin, Tabs, notification } from "antd";
import { useTranslation } from "react-i18next";
import { MySupsenseFallback } from "../../../Components/MySupsenseFallback";
import PageInDevelopment from "../../PageInDevelopment";
@ -20,6 +20,8 @@ function SuspenseFallback({ children }) {
export default function Website() {
const { storeId } = useParams();
const { t } = useTranslation();
const [notificationApi, notificationContextHolder] =
notification.useNotification();
const [isRequesting, setIsRequesting] = useState(true);
const [website, setWebsite] = useState({});
@ -48,10 +50,13 @@ export default function Website() {
});
useEffect(() => {
myFetch(`/website/${storeId}`, "GET")
.then((res) => {
setIsRequesting(false);
})
myFetch({
url: `/website/${storeId}`,
method: "GET",
notificationApi: notificationApi,
t: t,
})
.then(() => setIsRequesting(false))
.catch((err) => {
setIsRequesting(false);
@ -76,7 +81,9 @@ export default function Website() {
}
return (
<div>
<>
{notificationContextHolder}
<Card>
<Tabs
type="card"
@ -86,12 +93,15 @@ export default function Website() {
onChange={(key) => setActiveTab(key)}
/>
</Card>
</div>
</>
);
}
function NoWebsiteCreateOne({ setWebsite }) {
const { t } = useTranslation();
const [notificationApi, notificationContextHolder] =
notification.useNotification();
const { storeId } = useParams();
const [isRequesting, setIsRequesting] = useState(false);
@ -99,8 +109,14 @@ function NoWebsiteCreateOne({ setWebsite }) {
const handleCreateWebsite = () => {
setIsRequesting(true);
myFetch("/website", "POST", {
storeId,
myFetch({
url: "/website",
method: "POST",
body: {
storeId,
},
notificationApi: notificationApi,
t: t,
})
.then((res) => {
console.log(res);
@ -116,6 +132,8 @@ function NoWebsiteCreateOne({ setWebsite }) {
return (
<MyCenteredContainer>
{notificationContextHolder}
<Result
status="404"
title={t("storeWebsite.noWebsite.title")}

View File

@ -1,28 +1,39 @@
import { Button, Card, Select, Typography } from "antd";
import { Constants } from "../../utils";
import { Button, Card, Select, Typography, notification } from "antd";
import { myFetch } from "../../utils";
import { useTranslation } from "react-i18next";
import { useState } from "react";
export default function UserProfile({ userSession, setUserSession }) {
export default function UserProfile({ setUserSession }) {
const { t, i18n } = useTranslation();
const [notificationApi, notificationContextHolder] =
notification.useNotification();
const [isRequestingLogout, setIsRequestingLogout] = useState(false);
return (
<>
{notificationContextHolder}
<Card
title={t("userProfile.title")}
extra={
<Button
type="primary"
loading={isRequestingLogout}
onClick={() => {
setUserSession();
window.location.href = "/";
setIsRequestingLogout(true);
fetch(`${Constants.API_ADDRESS}/user/auth/logout`, {
myFetch({
url: "/user/auth/logout",
method: "DELETE",
headers: {
"Content-Type": "application/json",
"X-Authorization": userSession,
},
}).catch(console.error);
notificationApi: notificationApi,
t: t,
})
.then(() => {
setUserSession();
window.location.href = "/";
})
.catch(() => setIsRequestingLogout(false));
}}
>
{t("common.button.logout")}

View File

@ -6,6 +6,7 @@ body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f5f5f5;
}
code {

File diff suppressed because it is too large Load Diff