master
alex 2024-01-13 12:06:13 +01:00
parent 23af00a619
commit a3c979c5b7
17 changed files with 1721 additions and 453 deletions

View File

@ -6,14 +6,27 @@
"save": "Speichern", "save": "Speichern",
"delete": "Löschen", "delete": "Löschen",
"confirm": "Bestätigen", "confirm": "Bestätigen",
"create": "Erstellen" "create": "Erstellen",
"edit": "Bearbeiten"
}, },
"action": "Aktion", "action": "Aktion",
"contactAdmin": "Bitte kontaktieren Sie einen Administrator", "contactAdmin": "Bitte kontaktieren Sie einen Administrator",
"username": "Anzeigename", "username": "Anzeigename",
"usernamePlaceholder": "Geben Sie Ihren Anzeigename ein",
"accountName": "Benutzername", "accountName": "Benutzername",
"accountNamePlaceholder": "Geben Sie Ihren Benutzernamen ein",
"password": "Passwort", "password": "Passwort",
"noDataFound": "Keine Daten gefunden" "passwordPlaceholder": "Geben Sie Ihr Passwort ein",
"noDataFound": "Keine Einträge gefunden",
"inputRules": {
"usernameRequired": "Anzeigename ist erforderlich",
"usernameMinLength": "Anzeigename muss mindestens {{minLength}} Zeichen lang sein",
"accountNameRequired": "Benutzername ist erforderlich",
"accountNameMinLength": "Benutzername muss mindestens {{minLength}} Zeichen lang sein",
"accountNameTaken": "Benutzername ist bereits vergeben",
"passwordRequired": "Passwort ist erforderlich",
"passwordMinLength": "Passwort muss mindestens {{minLength}} Zeichen lang sein"
}
}, },
"pageNotFound": { "pageNotFound": {
"title": "Seite nicht gefunden", "title": "Seite nicht gefunden",
@ -28,17 +41,73 @@
"banner": "Banner", "banner": "Banner",
"socials": "Soziale Netzwerke" "socials": "Soziale Netzwerke"
}, },
"store": {
"titleSingular": "Geschäft",
"titlePlural": "Geschäfte",
"settings": "Einstellungen",
"employees": "Mitarbeiter", "employees": "Mitarbeiter",
"services": "Dienstleistungen", "services": "Dienstleistungen",
"calendar": "Kalender", "calendar": "Kalender"
},
"support": "Unterstützung", "support": "Unterstützung",
"feedback": "Feedback" "feedback": "Feedback"
}, },
"employees": { "employees": {
"pageTitle": "Mitarbeiter", "pageTitle": "Mitarbeiter",
"addEmployee": "Mitarbeiter anlegen", "addEmployee": "Mitarbeiter anlegen",
"editEmployee": "Mitarbeiter bearbeiten",
"modalAddEmployee": { "modalAddEmployee": {
"checkboxPasswordChange": "Mitarbeiter auffordern, das Passwort zu ändern (empfohlen)" "checkboxPasswordChange": "Mitarbeiter auffordern, das Passwort zu ändern (empfohlen)"
},
"popConfirmDeleteEmployee": {
"title": "Mitarbeiter löschen",
"description": "Möchten Sie den Mitarbeiter wirklich löschen?"
}
},
"login": {
"login": "Anmelden",
"signUp": "Registrieren"
},
"storeServices": {
"pageTitle": "Dienstleistungen",
"buttonAddService": "Dienstleistung hinzufügen",
"serviceName": "Name der Dienstleistung",
"serviceNamePlaceholder": "Geben Sie den Namen der Dienstleistung ein",
"serviceActivityName": "Name der Tätigkeit",
"serviceActivityNamePlaceholder": "Geben Sie den Namen der Tätigkeit ein",
"serviceActivityDescription": "Beschreibung der Tätigkeit",
"serviceActivityDescriptionPlaceholder": "Geben Sie die Beschreibung der Tätigkeit ein",
"serviceActivityPrice": "Preis der Tätigkeit",
"serviceActivityPricePlaceholder": "Geben Sie den Preis der Tätigkeit ein",
"serviceActivityPriceUnit": "€",
"serviceActivityDurationHours": "Dauer der Tätigkeit (Stunden)",
"serviceActivityDurationHoursPlaceholder": "Geben Sie die Dauer der Tätigkeit in Stunden ein",
"serviceActivityDurationHoursUnit": "Stunden",
"serviceActivityDurationMinutes": "Dauer der Tätigkeit (Minuten)",
"serviceActivityDurationMinutesPlaceholder": "Geben Sie die Dauer der Tätigkeit in Minuten ein",
"serviceActivityDurationMinutesUnit": "Minuten",
"inputRules": {
"serviceNameRequired": "Name der Dienstleistung ist erforderlich",
"serviceNameMinLength": "Name der Dienstleistung muss mindestens {{minLength}} Zeichen lang sein",
"serviceActivityNameRequired": "Name der Tätigkeit ist erforderlich",
"serviceActivityNameMinLength": "Name der Tätigkeit muss mindestens {{minLength}} Zeichen lang sein",
"serviceActivityDescriptionRequired": "Beschreibung der Tätigkeit ist erforderlich",
"serviceActivityDescriptionMinLength": "Beschreibung der Tätigkeit muss mindestens {{minLength}} Zeichen lang sein",
"serviceActivityPriceRequired": "Preis der Tätigkeit ist erforderlich",
"serviceActivityDurationHoursRequired": "Dauer der Tätigkeit (Stunden) ist erforderlich",
"serviceActivityDurationMinutesRequired": "Dauer der Tätigkeit (Minuten) ist erforderlich"
},
"modalAddService": {
"title": "Dienstleistung hinzufügen"
},
"modalEditService": {
"title": "Dienstleistung bearbeiten"
},
"modalAddServiceActivity": {
"title": "Tätigkeit hinzufügen"
},
"modalEditServiceActivity": {
"title": "Tätigkeit bearbeiten"
} }
} }
} }

View File

@ -6,14 +6,27 @@
"save": "Save", "save": "Save",
"delete": "Delete", "delete": "Delete",
"confirm": "Confirm", "confirm": "Confirm",
"create": "Create" "create": "Create",
"edit": "Edit"
}, },
"action": "Action", "action": "Action",
"contactAdmin": "Please contact an administrator", "contactAdmin": "Please contact an administrator",
"username": "Username", "username": "Username",
"usernamePlaceholder": "Enter your username",
"accountName": "Account name", "accountName": "Account name",
"accountNamePlaceholder": "Enter your account name",
"password": "Password", "password": "Password",
"noDataFound": "No data found" "passwordPlaceholder": "Enter your password",
"noDataFound": "No data found",
"inputRules": {
"usernameRequired": "Please enter your username",
"usernameMinLength": "Username must be at least {{minLength}} characters",
"accountNameRequired": "Please enter your account name",
"accountNameMinLength": "Account name must be at least {{minLength}} characters",
"accountNameTaken": "Account name already exists",
"passwordRequired": "Please enter your password",
"passwordMinLength": "Password must be at least {{minLength}} characters"
}
}, },
"pageNotFound": { "pageNotFound": {
"title": "Page Not Found", "title": "Page Not Found",
@ -28,17 +41,76 @@
"banner": "Banner", "banner": "Banner",
"socials": "Socials" "socials": "Socials"
}, },
"store": {
"titleSingular": "Store",
"titlePlural": "Stores",
"settings": "Settings",
"employees": "Employees", "employees": "Employees",
"services": "Services", "services": "Services",
"calendar": "Calendar", "calendar": "Calendar"
},
"support": "Support", "support": "Support",
"feedback": "Feedback" "feedback": "Feedback"
}, },
"employees": { "employees": {
"pageTitle": "Employees", "pageTitle": "Employees",
"addEmployee": "Add employee", "addEmployee": "Add employee",
"editEmployee": "Edit employee",
"modalAddEmployee": { "modalAddEmployee": {
"checkboxPasswordChange": "Require employee to change password (recommended)" "checkboxPasswordChange": "Require employee to change password (recommended)"
},
"popConfirmDeleteEmployee": {
"title": "Delete employee",
"description": "Are you sure you want to delete this employee?"
}
},
"login": {
"login": "Login",
"signUp": "Sign up"
},
"services": {
"pageTitle": "Services"
},
"storeServices": {
"pageTitle": "Services",
"buttonAddService": "Add Service",
"serviceName": "Name of the Service",
"serviceNamePlaceholder": "Enter the name of the service",
"serviceActivityName": "Name of the activity",
"serviceActivityNamePlaceholder": "Enter the name of the activity",
"serviceActivityDescription": "Description of the activity",
"serviceActivityDescriptionPlaceholder": "Enter the description of the activity",
"serviceActivityPrice": "Price of the activity",
"serviceActivityPricePlaceholder": "Enter the price of the activity",
"serviceActivityPriceUnit": "€",
"serviceActivityDurationHours": "Duration of activity (hours)",
"serviceActivityDurationHoursPlaceholder": "Enter the duration of the activity in hours",
"serviceActivityDurationHoursUnit": "Hours",
"serviceActivityDurationMinutes": "Duration of activity (minutes)",
"serviceActivityDurationMinutesPlaceholder": "Enter the duration of the activity in minutes",
"serviceActivityDurationMinutesUnit": "Minutes",
"inputRules": {
"serviceNameRequired": "Service name is required",
"serviceNameMinLength": "Service name must be at least {{minLength}} characters long",
"serviceActivityNameRequired": "Activity name is required",
"serviceActivityNameMinLength": "Activity name must be at least {{minLength}} characters long",
"serviceActivityDescriptionRequired": "Activity description is required",
"serviceActivityDescriptionMinLength": "Activity description must be at least {{minLength}} characters long",
"serviceActivityPriceRequired": "Activity price is required",
"serviceActivityDurationHoursRequired": "Activity duration (hours) is required",
"serviceActivityDurationMinutesRequired": "Activity duration (minutes) is required"
},
"modalAddService": {
"title": "Add Service"
},
"modalEditService": {
"title": "Edit Service"
},
"modalAddServiceActivity": {
"title": "Add activity"
},
"modalEditServiceActivity": {
"title": "Edit activity"
} }
} }
} }

View File

@ -10,6 +10,7 @@ import { UserProfileProvider } from "./Contexts/UserProfileContext";
import { UsersProvider } from "./Contexts/UsersContext"; import { UsersProvider } from "./Contexts/UsersContext";
import HeaderProvider from "./Contexts/HeaderContext"; import HeaderProvider from "./Contexts/HeaderContext";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import StoresProvider from "./Contexts/StoresContext";
export default function App() { export default function App() {
/*const [notificationApi, notificationContextHolder] = /*const [notificationApi, notificationContextHolder] =
@ -23,15 +24,11 @@ export default function App() {
useEffect(() => { useEffect(() => {
if (!userSession) return; if (!userSession) return;
console.log("userprofile");
myFetch("/user", "GET") myFetch("/user", "GET")
.then((data) => { .then((data) => {
console.log(data);
setAppUserData(data); setAppUserData(data);
}) })
.catch((errStatus) => { .catch(() => {
setUserSession(); setUserSession();
window.location.href = "/"; window.location.href = "/";
}); });
@ -78,14 +75,16 @@ export default function App() {
return ( return (
<Layout style={{ minHeight: "100vh" }}> <Layout style={{ minHeight: "100vh" }}>
<HeaderProvider> <HeaderProvider>
<SideBarProvider _username={appUserData.username}> <SideBarProvider options={{ username: appUserData.user.username }}>
<UserProfileProvider> <UserProfileProvider>
<UsersProvider> <UsersProvider>
<AppProvider> <AppProvider>
<StoresProvider options={{ stores: appUserData.stores }}>
<DashboardLayout <DashboardLayout
userSession={userSession} userSession={userSession}
setUserSession={setUserSession} setUserSession={setUserSession}
/> />
</StoresProvider>
</AppProvider> </AppProvider>
</UsersProvider> </UsersProvider>
</UserProfileProvider> </UserProfileProvider>

View File

@ -7,9 +7,10 @@ import { MySupsenseFallback } from "../MySupsenseFallback";
// Lazy-loaded components // Lazy-loaded components
const Dashboard = lazy(() => import("../../Pages/Dashboard")); const Dashboard = lazy(() => import("../../Pages/Dashboard"));
const PageNotFound = lazy(() => import("../../Pages/PageNotFound")); const PageNotFound = lazy(() => import("../../Pages/PageNotFound"));
const Employees = lazy(() => import("../../Pages/Employees")); const StoreSettings = lazy(() => import("../../Pages/Store/Settings"));
const Services = lazy(() => import("../../Pages/Services")); const StoreEmployees = lazy(() => import("../../Pages/Store/Employees"));
const Calendar = lazy(() => import("../../Pages/Calendar")); const StoreServices = lazy(() => import("../../Pages/Store/Services"));
const StoreCalendar = lazy(() => import("../../Pages/Store/Calendar"));
const Support = lazy(() => import("../../Pages/Support")); const Support = lazy(() => import("../../Pages/Support"));
const Feedback = lazy(() => import("../../Pages/Feedback")); const Feedback = lazy(() => import("../../Pages/Feedback"));
const UserProfile = lazy(() => import("../../Pages/UserProfile")); const UserProfile = lazy(() => import("../../Pages/UserProfile"));
@ -29,28 +30,37 @@ export default function AppRoutes({ userSession, setUserSession }) {
/> />
<Route <Route
path={Constants.ROUTE_PATHS.EMPLOYEES} path={`${Constants.ROUTE_PATHS.STORE.SETTINGS}/:storeId`}
element={ element={
<MySupsenseFallback> <MySupsenseFallback>
<Employees /> <StoreSettings />
</MySupsenseFallback> </MySupsenseFallback>
} }
/> />
<Route <Route
path={Constants.ROUTE_PATHS.SERVICES} path={`${Constants.ROUTE_PATHS.STORE.EMPLOYEES}/:storeId`}
element={ element={
<MySupsenseFallback> <MySupsenseFallback>
<Services /> <StoreEmployees />
</MySupsenseFallback> </MySupsenseFallback>
} }
/> />
<Route <Route
path={Constants.ROUTE_PATHS.CALENDAR} path={`${Constants.ROUTE_PATHS.STORE.SERVICES}/:storeId`}
element={ element={
<MySupsenseFallback> <MySupsenseFallback>
<Calendar /> <StoreServices />
</MySupsenseFallback>
}
/>
<Route
path={`${Constants.ROUTE_PATHS.STORE.CALENDAR}/:storeId`}
element={
<MySupsenseFallback>
<StoreCalendar />
</MySupsenseFallback> </MySupsenseFallback>
} }
/> />

View File

@ -0,0 +1,227 @@
import { Form, Input, InputNumber } from "antd";
import { Constants, myFetch } from "../../utils";
import { useRef } from "react";
import { useTranslation } from "react-i18next";
export function MyUsernameFormInput({ propsFormItem, propsInput }) {
const { t } = useTranslation();
return (
<MyFormInput
propsFormItem={propsFormItem}
propsInput={propsInput}
formItemName="username"
minLength={Constants.GLOBALS.MIN_USERNAME_LENGTH}
maxLength={Constants.GLOBALS.MAX_USERNAME_LENGTH}
label={t("common.username")}
ruleMessageValueRequired={t("common.inputRules.usernameRequired")}
ruleMessageValueMinLengthRequired={t(
"common.inputRules.usernameMinLength",
{
minLength: Constants.GLOBALS.MIN_USERNAME_LENGTH,
}
)}
inputPlaceholder={t("common.usernamePlaceholder")}
/>
);
}
export function MyAccountNameFormInput({
propsFormItem,
propsInput,
disableAccountNameCheck,
hasFeedback,
}) {
const { t } = useTranslation();
return (
<MyAvailableCheckFormInput
propsFormItem={propsFormItem}
propsInput={propsInput}
hasFeedback={hasFeedback}
formItemName="accountName"
minLength={Constants.GLOBALS.MIN_ACCOUNT_NAME_LENGTH}
maxLength={Constants.GLOBALS.MAX_ACCOUNT_NAME_LENGTH}
label={t("common.accountName")}
ruleMessageValueRequired={t("common.inputRules.accountNameRequired")}
ruleMessageValueMinLengthRequired={t(
"common.inputRules.accountNameMinLength",
{
minLength: Constants.GLOBALS.MIN_ACCOUNT_NAME_LENGTH,
}
)}
ruleMessageValueNotAvailable={t("common.inputRules.accountNameTaken")}
inputPlaceholder={t("common.accountNamePlaceholder")}
inputType="text"
disableAvailableCheck={disableAccountNameCheck}
fetchUrl="/user/auth/check/accountname"
fetchParameter="accountName"
fetchDelay={Constants.DELAY_ACCOUNT_NAME_CHECK}
/>
);
}
export function MyPasswordFormInput({ propsFormItem, propsInput }) {
const { t } = useTranslation();
return (
<MyFormInput
propsFormItem={propsFormItem}
propsInput={propsInput}
formItemName="password"
minLength={Constants.GLOBALS.MIN_PASSWORD_LENGTH}
maxLength={Constants.GLOBALS.MAX_PASSWORD_LENGTH}
label={t("common.password")}
ruleMessageValueRequired={t("common.inputRules.passwordRequired")}
ruleMessageValueMinLengthRequired={t(
"common.inputRules.passwordMinLength",
{
minLength: Constants.GLOBALS.MIN_PASSWORD_LENGTH,
}
)}
inputPlaceholder={t("common.passwordPlaceholder")}
inputType="password"
/>
);
}
export function MyAvailableCheckFormInput({
propsFormItem,
propsInput,
formItemName,
minLength,
maxLength,
label,
ruleMessageValueRequired,
ruleMessageValueMinLengthRequired,
ruleMessageValueNotAvailable,
inputPlaceholder,
inputType,
disableAvailableCheck = false,
fetchUrl,
fetchParameter,
fetchDelay,
hasFeedback,
}) {
const delayTimeout = useRef();
const isValid = (value) => {
return value.length >= minLength && value.length <= maxLength;
};
return (
<MyFormInput
propsFormItem={{
...propsFormItem,
hasFeedback: hasFeedback,
}}
propsInput={propsInput}
formItemName={formItemName}
minLength={minLength}
maxLength={maxLength}
label={label}
ruleMessageValueRequired={ruleMessageValueRequired}
ruleMessageValueMinLengthRequired={ruleMessageValueMinLengthRequired}
inputPlaceholder={inputPlaceholder}
inputType={inputType}
formItemRules={[
() => ({
validator(_, value) {
if (!value || !isValid(value)) {
return Promise.reject("");
}
if (disableAvailableCheck) {
return Promise.resolve();
}
if (delayTimeout.current) {
clearTimeout(delayTimeout.current);
}
return new Promise((resolve, reject) => {
delayTimeout.current = setTimeout(() => {
let body = {};
body[fetchParameter] = value; // like accountName: value
myFetch(fetchUrl, "POST", body)
.then(() => {
resolve();
})
.catch((errStatus) => {
console.log(errStatus);
reject(ruleMessageValueNotAvailable);
});
}, fetchDelay);
});
},
}),
]}
/>
);
}
export function MyFormInput({
propsFormItem,
propsInput,
formItemName,
formItemRules,
minLength,
maxLength,
label,
ruleMessageValueRequired,
ruleMessageValueMinLengthRequired,
inputPlaceholder,
inputType,
}) {
const commonProps = {
...propsInput,
placeholder: inputPlaceholder,
};
const myFormItemRules = [
{
required: true,
message: ruleMessageValueRequired,
},
];
if (formItemRules) {
myFormItemRules.push(...formItemRules);
}
if (inputType === "number") {
commonProps.min = minLength;
commonProps.max = maxLength;
} else {
commonProps.minLength = minLength;
commonProps.maxLength = maxLength;
myFormItemRules.push({
min: minLength,
message: ruleMessageValueMinLengthRequired,
});
}
const inputComponents = {
textarea: (
<Input.TextArea {...commonProps} autoSize={{ minRows: 2, maxRows: 6 }} />
),
number: <InputNumber {...commonProps} />,
password: <Input.Password {...commonProps} />,
default: <Input {...commonProps} />,
};
return (
<Form.Item
{...propsFormItem}
label={label}
name={formItemName}
required
rules={myFormItemRules}
>
{inputComponents[inputType] || inputComponents.default}
</Form.Item>
);
}

View File

@ -108,13 +108,19 @@ export function MyModalCloseSaveButtonFooter({
onCancel, onCancel,
onSave, onSave,
isSaveButtonLoading, isSaveButtonLoading,
isSaveButtonDisabled,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<> <>
<Button onClick={onCancel}>{t("common.button.close")}</Button> <Button onClick={onCancel}>{t("common.button.close")}</Button>
<Button onClick={onSave} type="primary" loading={isSaveButtonLoading}> <Button
onClick={onSave}
type="primary"
disabled={isSaveButtonDisabled}
loading={isSaveButtonLoading}
>
{t("common.button.save")} {t("common.button.save")}
</Button> </Button>
</> </>

View File

@ -1,48 +1,33 @@
import { import {
AppstoreOutlined, AppstoreOutlined,
BgColorsOutlined, BgColorsOutlined,
BookOutlined,
CalendarOutlined, CalendarOutlined,
ClusterOutlined, ClusterOutlined,
ControlOutlined,
DesktopOutlined,
EditOutlined, EditOutlined,
FileImageOutlined, FileImageOutlined,
FileTextOutlined,
HistoryOutlined,
MessageOutlined, MessageOutlined,
QuestionCircleOutlined, QuestionCircleOutlined,
RobotOutlined,
ScanOutlined,
ScissorOutlined, ScissorOutlined,
SettingOutlined, SettingOutlined,
SnippetsOutlined, ShopOutlined,
TeamOutlined, TeamOutlined,
UserOutlined, UserOutlined,
UsergroupAddOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Badge, Divider, Menu } from "antd"; import { Divider, Menu } from "antd";
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { import { BreakpointLgWidth, Constants } from "../../utils";
BreakpointLgWidth,
BrowserTabSession,
Constants,
hasOnePermission,
hasOneXYPermission,
hasPermission,
wsConnectionCustomEventName,
} from "../../utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useSideBarContext } from "../../Contexts/SideBarContext"; import { useSideBarContext } from "../../Contexts/SideBarContext";
import { useAppContext } from "../../Contexts/AppContext"; import { useAppContext } from "../../Contexts/AppContext";
import { useStoresContext } from "../../Contexts/StoresContext";
export function SideMenuContent({ export function SideMenuContent({
setIsSideMenuCollapsed, setIsSideMenuCollapsed,
contentFirstRender, contentFirstRender,
}) { }) {
const appContext = useAppContext();
const sideBarContext = useSideBarContext(); const sideBarContext = useSideBarContext();
const storesContext = useStoresContext();
const location = useLocation(); const location = useLocation();
const [selectedKeys, setSelectedKeys] = useState("/"); const [selectedKeys, setSelectedKeys] = useState("/");
const { t } = useTranslation(); const { t } = useTranslation();
@ -88,50 +73,72 @@ export function SideMenuContent({
items.push(groupWebsite); items.push(groupWebsite);
// employees // stores
items.push({ let stores = {
label: t("sideMenu.employees"), label:
storesContext.stores.length > 1
? t("sideMenu.store.titlePlural")
: t("sideMenu.store.titleSingular"),
children: [],
type: "group",
};
storesContext.stores.forEach((store) => {
let groupStore = {
label: store.name,
icon: <ShopOutlined />,
children: [],
};
groupStore.children.push({
label: t("sideMenu.store.settings"),
icon: <SettingOutlined />,
key: `${Constants.ROUTE_PATHS.STORE.SETTINGS}/${store.store_id}`,
});
groupStore.children.push({
label: t("sideMenu.store.employees"),
icon: <TeamOutlined />, icon: <TeamOutlined />,
key: Constants.ROUTE_PATHS.EMPLOYEES, key: `${Constants.ROUTE_PATHS.STORE.EMPLOYEES}/${store.store_id}`,
}); });
// services groupStore.children.push({
label: t("sideMenu.store.services"),
items.push({
label: t("sideMenu.services"),
icon: <ScissorOutlined />, icon: <ScissorOutlined />,
key: Constants.ROUTE_PATHS.SERVICES, key: `${Constants.ROUTE_PATHS.STORE.SERVICES}/${store.store_id}`,
}); });
// calendar groupStore.children.push({
label: t("sideMenu.store.calendar"),
items.push({
label: t("sideMenu.calendar"),
icon: <CalendarOutlined />, icon: <CalendarOutlined />,
key: Constants.ROUTE_PATHS.CALENDAR, key: `${Constants.ROUTE_PATHS.STORE.CALENDAR}/${store.store_id}`,
}); });
stores.children.push(groupStore);
});
items.push(stores);
return items; return items;
}; };
const getSecondMenuItems = () => { const getSecondMenuItems = () => {
let items = []; let items = [];
// connection status, userprofile, logout
items.push( items.push(
{ {
label: "Support", label: t("sideMenu.support"),
icon: <QuestionCircleOutlined />, icon: <QuestionCircleOutlined />,
key: Constants.ROUTE_PATHS.SUPPORT, key: Constants.ROUTE_PATHS.SUPPORT,
}, },
{ {
label: "Feedback", label: t("sideMenu.feedback"),
icon: <MessageOutlined />, icon: <MessageOutlined />,
key: Constants.ROUTE_PATHS.FEEDBACK, key: Constants.ROUTE_PATHS.FEEDBACK,
}, },
{ {
label: ` ${sideBarContext.username}`, label: sideBarContext.username,
icon: <UserOutlined />, icon: <UserOutlined />,
key: Constants.ROUTE_PATHS.USER_PROFILE, key: Constants.ROUTE_PATHS.USER_PROFILE,
} }

View File

@ -1,42 +1,22 @@
import { createContext, useContext, useState } from "react"; import { createContext, useContext, useState } from "react";
import { Constants } from "../utils";
const preview = { const preview = {
connectionBadgeStatus: "",
connectedUsers: 0,
selectedScanner: "",
username: "", username: "",
avatar: "", setUsername: () => {},
availableCategories: [],
}; };
const SideBarContext = createContext(preview); const SideBarContext = createContext(preview);
export const useSideBarContext = () => useContext(SideBarContext); export const useSideBarContext = () => useContext(SideBarContext);
export default function SideBarProvider({ _username, children }) { export default function SideBarProvider({ children, options }) {
const [connectionBadgeStatus, setConnectionBadgeStatus] = useState("error"); const [username, setUsername] = useState(options.username);
const [connectedUsers, setConnectedUsers] = useState(0);
const [selectedScanner, setSelectedScanner] = useState("");
const [username, setUsername] = useState(_username); //
const [avatar, setAvatar] = useState("");
const [availableCategories, setAvailableCategories] = useState([]);
return ( return (
<SideBarContext.Provider <SideBarContext.Provider
value={{ value={{
connectionBadgeStatus,
setConnectionBadgeStatus,
connectedUsers,
setConnectedUsers,
selectedScanner,
setSelectedScanner,
username, username,
setUsername, setUsername,
avatar,
setAvatar,
availableCategories,
setAvailableCategories,
}} }}
> >
{children} {children}

View File

@ -0,0 +1,20 @@
import { createContext, useContext, useState } from "react";
const preview = {
stores: [],
setStores: () => {},
};
const StoresContext = createContext(preview);
export const useStoresContext = () => useContext(StoresContext);
export default function StoresProvider({ children, options }) {
const [stores, setStores] = useState(options.stores);
return (
<StoresContext.Provider value={{ stores, setStores }}>
{children}
</StoresContext.Provider>
);
}

View File

@ -1,217 +0,0 @@
import { LockOutlined, PlusOutlined, UserOutlined } from "@ant-design/icons";
import { Button, Checkbox, Col, Form, Grid, Input, Row, Table } from "antd";
import MyModal, {
MyModalCloseCreateButtonFooter,
} from "../../Components/MyModal";
import { useEffect, useState } from "react";
import { Constants, myFetch, EncodeStringToBase64 } from "../../utils";
import { useTranslation } from "react-i18next";
import MyTable from "../../Components/MyTable";
const { useBreakpoint } = Grid;
export default function Employees() {
const { t } = useTranslation();
const screenBreakpoint = useBreakpoint();
const [employees, setEmployees] = useState([]);
const [isAddEmployeeModalOpen, setIsAddEmployeeModalOpen] = useState(false);
const [username, setUsername] = useState("");
const [accountName, setAccountName] = useState("");
const [password, setPassword] = useState("");
const [isRequesting, setIsRequesting] = useState(false);
const getTableColumns = () => {
return [
{
title: t("common.accountName"),
dataIndex: "account_name",
key: "account_name",
},
{
title: t("common.username"),
dataIndex: "username",
key: "username",
},
{
title: t("common.action"),
dataIndex: "action",
key: "actions",
render: () => <a>{t("common.button.delete")}</a>,
},
];
};
const getTableItems = () => {
return employees.map((employee) => {
return {
key: employee.account_name,
account_name: employee.account_name,
username: employee.username,
};
});
};
const handleAddEmployeeModalClose = () => {
setIsAddEmployeeModalOpen(false);
setUsername("");
setAccountName("");
setPassword("");
};
const fetchEmployees = () => {
myFetch("/users", "GET")
.then((data) => {
setIsRequesting(false);
setEmployees(data.employees);
})
.catch((errStatus) => {
console.log(errStatus);
});
};
useEffect(() => {
setIsRequesting(true);
fetchEmployees();
}, []);
return (
<>
<div
style={{
display: "flex",
justifyContent: "space-between",
flexDirection: screenBreakpoint.xs ? "column" : "row",
}}
>
<h1>
{t("employees.pageTitle")}
{employees.length > 0 && ` (${employees.length})`}
</h1>
<Button
type="primary"
block={screenBreakpoint.xs}
icon={<PlusOutlined />}
onClick={() => setIsAddEmployeeModalOpen(true)}
>
{t("employees.addEmployee")}
</Button>
</div>
<MyTable
props={{
loading: isRequesting,
columns: getTableColumns(),
dataSource: getTableItems(),
}}
/>
<MyModal
title={t("employees.addEmployee")}
isOpen={isAddEmployeeModalOpen}
onCancel={handleAddEmployeeModalClose}
footer={
<MyModalCloseCreateButtonFooter
onCancel={handleAddEmployeeModalClose}
isCreateButtonLoading={isRequesting}
onCreate={() => {
setIsRequesting(true);
myFetch("/users", "POST", {
username: username,
accountName: accountName,
password: EncodeStringToBase64(password),
})
.then(() => {
setIsRequesting(false);
handleAddEmployeeModalClose();
fetchEmployees();
})
.catch((errStatus) => {
console.log(errStatus);
setIsRequesting(false);
});
}}
/>
}
>
<Form>
<Form.Item
name="username"
required
rules={[
{ required: true, message: "Please enter your username!" },
{
min: Constants.GLOBALS.MIN_USERNAME_LENGTH,
message: `Please enter a username length of at least ${Constants.GLOBALS.MIN_USERNAME_LENGTH}!`,
},
]}
>
<Input
prefix={<UserOutlined />}
placeholder={t("common.username")}
value={username}
onChange={(e) => setUsername(e.target.value)}
minLength={Constants.GLOBALS.MIN_USERNAME_LENGTH}
maxLength={Constants.GLOBALS.MAX_USERNAME_LENGTH}
/>
</Form.Item>
<Form.Item
name="accountName"
required
rules={[
{ required: true, message: "Please enter an account name!" },
{
min: Constants.GLOBALS.MIN_USERNAME_LENGTH,
message: `Please enter an account name length of at least ${Constants.GLOBALS.MIN_ACCOUNT_NAME_LENGTH}!`,
},
]}
>
<Input
prefix={<UserOutlined />}
placeholder={t("common.accountName")}
value={accountName}
onChange={(e) => setAccountName(e.target.value)}
minLength={Constants.GLOBALS.MIN_USERNAME_LENGTH}
maxLength={Constants.GLOBALS.MAX_USERNAME_LENGTH}
/>
</Form.Item>
<Form.Item
name="password"
required
rules={[
{
required: true,
message: "Please enter your Password!",
},
{
min: Constants.GLOBALS.MIN_PASSWORD_LENGTH,
message: `Please enter a password length of at least ${Constants.GLOBALS.MIN_PASSWORD_LENGTH}!`,
},
]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder={t("common.password")}
value={password}
onChange={(e) => setPassword(e.target.value)}
minLength={Constants.GLOBALS.MIN_PASSWORD_LENGTH}
maxLength={Constants.GLOBALS.MAX_PASSWORD_LENGTH}
/>
</Form.Item>
<Form.Item>
<Checkbox defaultChecked>
{t("employees.modalAddEmployee.checkboxPasswordChange")}
</Checkbox>
</Form.Item>
</Form>
</MyModal>
</>
);
}

View File

@ -1,21 +1,27 @@
import { LockOutlined, LoginOutlined, UserOutlined } from "@ant-design/icons"; import { LoginOutlined } from "@ant-design/icons";
import { Button, Form, Input, Modal, Tabs, notification } from "antd"; import { Button, Form, Modal, Tabs, notification } from "antd";
import { import {
Constants,
EncodeStringToBase64, EncodeStringToBase64,
myFetch, myFetch,
myFetchContentType, myFetchContentType,
setUserSessionToLocalStorage, setUserSessionToLocalStorage,
} from "../../utils"; } from "../../utils";
import { useState } from "react"; import { useState } from "react";
import {
MyAccountNameFormInput,
MyPasswordFormInput,
MyUsernameFormInput,
} from "../../Components/MyFormInputs";
import { useTranslation } from "react-i18next";
export default function Login() { export default function Login() {
const [username, setUsername] = useState(""); const { t } = useTranslation();
const [accountName, setAccountName] = useState(""); const [form] = Form.useForm();
const [password, setPassword] = useState("");
const [api, contextHolder] = notification.useNotification(); const [api, contextHolder] = notification.useNotification();
const [selectedMethod, setSelectedMethod] = useState("1"); const [selectedMethod, setSelectedMethod] = useState("1");
const [isRequesting, setIsRequesting] = useState(false);
const showErrorNotification = (errStatus) => { const showErrorNotification = (errStatus) => {
if (errStatus === 401) { if (errStatus === 401) {
@ -32,28 +38,69 @@ export default function Login() {
}); });
}; };
const handleSubmit = () => { return (
if ( <>
accountName.length > Constants.GLOBALS.MAX_ACCOUNT_NAME_LENGTH || {contextHolder}
accountName.length < Constants.GLOBALS.MIN_ACCOUNT_NAME_LENGTH || <Modal
password.length > Constants.GLOBALS.MAX_PASSWORD_LENGTH || open={true}
password.length < Constants.GLOBALS.MIN_PASSWORD_LENGTH closable={false}
) { centered
showErrorNotification(); keyboard={false}
return; footer={null}
} >
<Tabs
defaultActiveKey="1"
items={[
{
key: "1",
label: t("login.login"),
},
{
key: "2",
label: t("login.signUp"),
},
]}
centered
onChange={(activeKey) => {
setSelectedMethod(activeKey);
}}
/>
<Form form={form} layout="vertical" requiredMark={false}>
{selectedMethod === "2" && <MyUsernameFormInput />}
<MyAccountNameFormInput
disableAccountNameCheck={selectedMethod === "1"}
hasFeedback={selectedMethod === "2"}
/>
<MyPasswordFormInput />
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<Button
type="primary"
htmlType="submit"
icon={<LoginOutlined />}
loading={isRequesting}
onClick={() => {
form
.validateFields()
.then((values) => {
setIsRequesting(true);
let body = { let body = {
accountName: accountName.toLocaleLowerCase(), accountName: values.accountName.toLocaleLowerCase(),
password: EncodeStringToBase64(password), password: EncodeStringToBase64(values.password),
}; };
if (selectedMethod === "2") { if (selectedMethod === "2") {
body.username = username; body.username = values.username;
} }
myFetch( myFetch(
`/user/auth/${selectedMethod === "1" ? "login" : "signup"}`, `/user/auth/${
selectedMethod === "1" ? "login" : "signup"
}`,
"POST", "POST",
body, body,
{}, {},
@ -67,114 +114,19 @@ export default function Login() {
setUserSessionToLocalStorage(data.XAuthorization); setUserSessionToLocalStorage(data.XAuthorization);
window.location.href = "/"; window.location.href = "/";
}) })
.catch((errStatus) => showErrorNotification(errStatus)); .catch((errStatus) => {
}; showErrorNotification(errStatus);
setIsRequesting(false);
return ( });
<> })
{contextHolder} .catch((info) => {
<Modal console.log("Validate Failed:", info);
open={true} });
closable={false}
centered
keyboard={false}
footer={
<Button
type="primary"
htmlType="submit"
icon={<LoginOutlined />}
className="login-form-button"
onClick={() => handleSubmit()}
>
{selectedMethod === "1" ? "Anmelden" : "Registrieren"}
</Button>
}
>
<Tabs
defaultActiveKey="1"
items={[
{
key: "1",
label: "Anmelden",
},
{
key: "2",
label: "Registrieren",
},
]}
centered
onChange={(activeKey) => {
setSelectedMethod(activeKey);
}} }}
/>
<Form>
{selectedMethod === "2" && (
<Form.Item
name="username"
required
rules={[
{ required: true, message: "Please enter your username!" },
{
min: Constants.GLOBALS.MIN_USERNAME_LENGTH,
message: `Please enter a username length of at least ${Constants.GLOBALS.MIN_USERNAME_LENGTH}!`,
},
]}
> >
<Input {selectedMethod === "1" ? t("login.login") : t("login.signUp")}
prefix={<UserOutlined />} </Button>
placeholder="Anzeigename" </div>
onChange={(e) => setUsername(e.target.value)}
minLength={Constants.GLOBALS.MIN_USERNAME_LENGTH}
maxLength={Constants.GLOBALS.MAX_USERNAME_LENGTH}
/>
</Form.Item>
)}
<Form.Item
hasFeedback
name="accountName"
validateStatus="validating"
required
rules={[
{ required: true, message: "Please enter your accountName!" },
{
min: Constants.GLOBALS.MIN_ACCOUNT_NAME_LENGTH,
message: `Please enter a accountName length of at least ${Constants.GLOBALS.MIN_ACCOUNT_NAME_LENGTH}!`,
},
]}
>
<Input
prefix={<UserOutlined />}
placeholder="Benutzername"
onChange={(e) => setAccountName(e.target.value)}
minLength={Constants.GLOBALS.MIN_ACCOUNT_NAME_LENGTH}
maxLength={Constants.GLOBALS.MAX_ACCOUNT_NAME_LENGTH}
/>
</Form.Item>
<Form.Item
name="password"
required
rules={[
{
required: true,
message: "Please enter your Password!",
},
{
min: Constants.GLOBALS.MIN_PASSWORD_LENGTH,
message: `Please enter a password length of at least ${Constants.GLOBALS.MIN_PASSWORD_LENGTH}!`,
},
]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="Passwort"
onChange={(e) => setPassword(e.target.value)}
minLength={Constants.GLOBALS.MIN_PASSWORD_LENGTH}
maxLength={Constants.GLOBALS.MAX_PASSWORD_LENGTH}
/>
</Form.Item>
</Form> </Form>
</Modal> </Modal>
</> </>

View File

@ -1,7 +0,0 @@
export default function Services() {
return (
<>
<h1>Services</h1>
</>
);
}

View File

@ -1,4 +1,4 @@
export default function Calendar() { export default function StoreCalendar() {
return ( return (
<> <>
<h1>Calendar</h1> <h1>Calendar</h1>

View File

@ -0,0 +1,315 @@
import { PlusOutlined } from "@ant-design/icons";
import { Button, Checkbox, Form, Grid, Popconfirm, Space } from "antd";
import MyModal, {
MyModalCloseCreateButtonFooter,
MyModalCloseSaveButtonFooter,
} from "../../../Components/MyModal";
import { useEffect, useState } from "react";
import { myFetch, EncodeStringToBase64 } from "../../../utils";
import { useTranslation } from "react-i18next";
import MyTable from "../../../Components/MyTable";
import {
MyAccountNameFormInput,
MyPasswordFormInput,
MyUsernameFormInput,
} from "../../../Components/MyFormInputs";
import { Link, useParams } from "react-router-dom";
const { useBreakpoint } = Grid;
export default function StoreEmployees() {
const { t } = useTranslation();
const screenBreakpoint = useBreakpoint();
const { storeId } = useParams();
const [employees, setEmployees] = useState([]);
const [isRequesting, setIsRequesting] = useState(false);
const [addEditEmployeeModalOptions, setAddEditEmployeeModalOptions] =
useState({
mode: "add",
isOpen: false,
selectedEmployee: null,
});
const getTableColumns = () => {
return [
{
title: t("common.accountName"),
dataIndex: "account_name",
key: "account_name",
},
{
title: t("common.username"),
dataIndex: "username",
key: "username",
},
{
title: t("common.action"),
dataIndex: "action",
key: "actions",
render: (_, record) => (
<Space>
<Link
to="#"
onClick={() => {
setAddEditEmployeeModalOptions({
...addEditEmployeeModalOptions,
mode: "edit",
isOpen: true,
selectedEmployee: record,
});
}}
>
{t("common.button.edit")}
</Link>
<Popconfirm
title={t("employees.popConfirmDeleteEmployee.title")}
description={t("employees.popConfirmDeleteEmployee.description")}
okText={t("common.button.delete")}
onConfirm={() => {
setIsRequesting(true);
myFetch("/users", "DELETE", {
userId: record.key,
})
.then(() => fetchEmployees())
.catch((errStatus) => {
console.log(errStatus);
setIsRequesting(false);
});
}}
>
<Link to="#">{t("common.button.delete")}</Link>
</Popconfirm>
</Space>
),
},
];
};
const getTableItems = () => {
return employees.map((employee) => {
return {
key: employee.user_id,
account_name: employee.account_name,
username: employee.username,
};
});
};
const fetchEmployees = () => {
setIsRequesting(true);
myFetch(`/users/${storeId}`, "GET")
.then((data) => {
setIsRequesting(false);
setEmployees(data.employees);
})
.catch((errStatus) => {
console.log(errStatus);
});
};
useEffect(() => fetchEmployees(), []);
return (
<>
<div
style={{
display: "flex",
justifyContent: "space-between",
flexDirection: screenBreakpoint.xs ? "column" : "row",
}}
>
<h1>
{t("employees.pageTitle")}
{employees.length > 0 && ` (${employees.length})`}
</h1>
<ModalAddEditEmployee
modalOptions={addEditEmployeeModalOptions}
setModalOptions={setAddEditEmployeeModalOptions}
fetchEmployees={fetchEmployees}
/>
</div>
<MyTable
props={{
loading: isRequesting,
columns: getTableColumns(),
dataSource: getTableItems(),
}}
/>
</>
);
}
function ModalAddEditEmployee({
modalOptions,
setModalOptions,
fetchEmployees,
}) {
const { t } = useTranslation();
const { storeId } = useParams();
const screenBreakpoint = useBreakpoint();
const [form] = Form.useForm();
const [isRequesting, setIsRequesting] = useState(false);
const handleModalClose = () => {
setModalOptions({
...modalOptions,
isOpen: false,
});
form.resetFields();
};
useEffect(() => {
if (!modalOptions.isOpen) return;
if (modalOptions.mode === "edit") {
form.setFieldsValue({
username: modalOptions.selectedEmployee.username,
accountName: modalOptions.selectedEmployee.account_name,
});
}
}, [modalOptions.isOpen]);
return (
<>
<Button
type="primary"
block={screenBreakpoint.xs}
icon={<PlusOutlined />}
onClick={() =>
setModalOptions({ ...modalOptions, mode: "add", isOpen: true })
}
>
{t("employees.addEmployee")}
</Button>
<MyModal
title={
modalOptions.mode === "add"
? t("employees.addEmployee")
: t("employees.editEmployee")
}
isOpen={modalOptions.isOpen}
onCancel={handleModalClose}
footer={
modalOptions.mode === "add" ? (
<MyModalCloseCreateButtonFooter
onCancel={handleModalClose}
isCreateButtonLoading={isRequesting}
onCreate={() => {
form
.validateFields()
.then((values) => {
setIsRequesting(true);
myFetch("/users", "POST", {
storeId: storeId,
username: values.username,
accountName: values.accountName,
password: EncodeStringToBase64(values.password),
})
.then(() => {
setIsRequesting(false);
handleModalClose();
fetchEmployees();
})
.catch((errStatus) => {
console.log(errStatus);
setIsRequesting(false);
});
})
.catch((info) => {
console.log("Validate Failed:", info);
});
}}
/>
) : (
<MyModalCloseSaveButtonFooter
onCancel={handleModalClose}
isSaveButtonLoading={isRequesting}
onSave={() => {
// only validate if something has changed
let formUsername = form.getFieldValue("username");
let formAccountName = form.getFieldValue("accountName");
if (
formUsername === modalOptions.selectedEmployee.username &&
formAccountName === modalOptions.selectedEmployee.account_name
) {
handleModalClose();
return;
}
let validateFields = [];
let body = {
userId: modalOptions.selectedEmployee.key,
};
if (formUsername !== modalOptions.selectedEmployee.username) {
validateFields.push("username");
body.username = formUsername;
}
if (
formAccountName !== modalOptions.selectedEmployee.account_name
) {
validateFields.push("accountName");
body.accountName = formAccountName;
}
form
.validateFields(validateFields)
.then(() => {
setIsRequesting(true);
myFetch("/users/update", "POST", body)
.then(() => {
setIsRequesting(false);
handleModalClose();
fetchEmployees();
})
.catch((errStatus) => {
console.log(errStatus);
setIsRequesting(false);
});
})
.catch((info) => {
console.log("Validate Failed:", info);
});
}}
/>
)
}
>
<Form form={form} layout="vertical" requiredMark={false}>
<MyUsernameFormInput />
<MyAccountNameFormInput hasFeedback />
{modalOptions.mode === "add" && (
<>
<MyPasswordFormInput />
<Form.Item>
<Checkbox defaultChecked disabled>
{t("employees.modalAddEmployee.checkboxPasswordChange")}
</Checkbox>
</Form.Item>
</>
)}
</Form>
</MyModal>
</>
);
}

View File

@ -0,0 +1,787 @@
import {
ArrowDownOutlined,
ArrowUpOutlined,
DeleteOutlined,
EditOutlined,
PlusOutlined,
} from "@ant-design/icons";
import {
Avatar,
Button,
Card,
Collapse,
Empty,
Form,
Grid,
Skeleton,
Space,
Tooltip,
} from "antd";
import { useTranslation } from "react-i18next";
import { useEffect, useState } from "react";
import { Constants, myFetch } from "../../../utils";
import { useParams } from "react-router-dom";
import MyModal, {
MyModalCloseCreateButtonFooter,
MyModalCloseSaveButtonFooter,
} from "../../../Components/MyModal";
import { MyFormInput } from "../../../Components/MyFormInputs";
const { useBreakpoint } = Grid;
export default function StoreServices() {
const { t } = useTranslation();
const screenBreakpoint = useBreakpoint();
const { storeId } = useParams();
const [isRequestingServices, setIsRequestingServices] = useState(false);
const [addEditServiceModalOptions, setAddEditServiceModalOptions] = useState({
mode: "add", // add | edit
isOpen: false,
service: null,
});
const [
addEditServiceActivityModalOptions,
setAddEditServiceActivityModalOptions,
] = useState({
mode: "add", // add | edit
isOpen: false,
activity: null,
});
const [servicesData, setServicesData] = useState({
services: [],
users: [],
});
const fetchServices = () => {
setIsRequestingServices(true);
myFetch(`/store/services/${storeId}`, "GET")
.then((data) => {
setIsRequestingServices(false);
setServicesData(data);
})
.catch((errStatus) => {
console.log(errStatus);
});
};
useEffect(() => fetchServices(), []);
return (
<>
<div
style={{
display: "flex",
justifyContent: "space-between",
flexDirection: screenBreakpoint.xs ? "column" : "row",
}}
>
<h1>{t("storeServices.pageTitle")}</h1>
<ModalAddEditService
storeId={storeId}
fetchServices={fetchServices}
addEditServiceModalOptions={addEditServiceModalOptions}
setAddEditServiceModalOptions={setAddEditServiceModalOptions}
/>
</div>
<Space direction="vertical" style={{ width: "100%" }}>
{isRequestingServices ? (
<>
<Skeleton.Input block active size="large" />
<Skeleton.Input block active size="large" />
<Skeleton.Input block active size="large" />
</>
) : (
<>
{servicesData.services.map((service) => (
<Service
key={service.service_id}
service={service}
users={servicesData.users}
storeId={storeId}
setAddEditServiceActivityModalOptions={
setAddEditServiceActivityModalOptions
}
setAddEditServiceModalOptions={setAddEditServiceModalOptions}
/>
))}
</>
)}
</Space>
<ModalAddEditServiceActivity
fetchServices={fetchServices}
addEditServiceActivityModalOptions={addEditServiceActivityModalOptions}
setAddEditServiceActivityModalOptions={
setAddEditServiceActivityModalOptions
}
/>
</>
);
}
function Service({
service,
users,
storeId,
setAddEditServiceActivityModalOptions,
setAddEditServiceModalOptions,
}) {
const [isRequestingActivities, setIsRequestingActivities] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [serviceActivities, setServiceActivities] = useState([]);
const fetchServiceActivities = () => {
if (!isOpen) return;
setIsRequestingActivities(true);
myFetch(
`/store/services/activities/${storeId}/${service.service_id}`,
"GET"
)
.then((data) => {
setIsRequestingActivities(false);
setServiceActivities(data.activities);
})
.catch((errStatus) => {
console.log(errStatus);
});
};
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>
<Space>
<ArrowUpOutlined
disabled
onClick={(e) => e.stopPropagation()}
/>
<ArrowDownOutlined
disabled
onClick={(e) => e.stopPropagation()}
/>
<PlusOutlined
onClick={(e) => {
e.stopPropagation();
setAddEditServiceActivityModalOptions({
mode: "add",
isOpen: true,
service: service,
});
}}
/>
<EditOutlined
onClick={(e) => {
e.stopPropagation();
setAddEditServiceModalOptions({
mode: "edit",
isOpen: true,
service: service,
});
}}
/>
<DeleteOutlined onClick={(e) => e.stopPropagation()} />
</Space>
</div>
),
children: (
<Space
key={`space-${service.service_id}`}
direction="vertical"
style={{ width: "100%" }}
>
{isRequestingActivities ? (
<Skeleton active>
<Card title="Test">
<p>Desc</p>
<p>Preis: </p>
<p>Dauer: Minuten</p>
</Card>
</Skeleton>
) : (
<>
{serviceActivities.length === 0 ? (
<Empty />
) : (
serviceActivities.map((activity) => (
<Card
key={activity.activity_id}
title={activity.name}
extra={
<Space>
<Avatar.Group maxCount={1} size="small">
{users.map((user) => (
<Tooltip
key={user.user_id}
title={user.username}
>
<Avatar
size="small"
style={{ backgroundColor: "#87d068" }}
>
{user.username.charAt(0)}
</Avatar>
</Tooltip>
))}
</Avatar.Group>
<ArrowUpOutlined disabled />
<ArrowDownOutlined disabled />
<EditOutlined
onClick={() => {
setAddEditServiceActivityModalOptions({
mode: "edit",
isOpen: true,
activity: activity,
});
}}
/>
<DeleteOutlined />
</Space>
}
>
<p>{activity.description}</p>
<p>Preis: {activity.price} </p>
<p>Dauer: {activity.duration} Minuten</p>
</Card>
))
)}
</>
)}
</Space>
),
},
]}
/>
);
}
// this modal is used to create and edit services
function ModalAddEditService({
storeId,
fetchServices,
addEditServiceModalOptions,
setAddEditServiceModalOptions,
}) {
const { t } = useTranslation();
const screenBreakpoint = useBreakpoint();
const [form] = Form.useForm();
const [isRequesting, setIsRequesting] = useState(false);
const handleModalClose = () => {
setAddEditServiceModalOptions({
...addEditServiceModalOptions,
isOpen: false,
});
form.resetFields();
};
useEffect(() => {
if (!addEditServiceModalOptions.isOpen) return;
if (addEditServiceModalOptions.mode === "edit") {
form.setFieldsValue({
serviceName: addEditServiceModalOptions.service.name,
});
}
}, [addEditServiceModalOptions.isOpen]);
return (
<>
<Button
type="primary"
icon={<PlusOutlined />}
block={screenBreakpoint.xs}
onClick={() =>
setAddEditServiceModalOptions({
...addEditServiceModalOptions,
isOpen: true,
})
}
>
{t("storeServices.buttonAddService")}
</Button>
<MyModal
title={
addEditServiceModalOptions.mode === "add"
? t("storeServices.modalAddService.title")
: t("storeServices.modalEditService.title")
}
isOpen={addEditServiceModalOptions.isOpen}
onCancel={handleModalClose}
footer={
addEditServiceModalOptions.mode === "add" ? (
<MyModalCloseCreateButtonFooter
onCancel={handleModalClose}
isCreateButtonLoading={isRequesting}
onCreate={() => {
form
.validateFields()
.then((values) => {
setIsRequesting(true);
myFetch("/store/services", "POST", {
storeId: storeId,
name: values.serviceName,
})
.then(() => {
setIsRequesting(false);
handleModalClose();
fetchServices();
})
.catch((errStatus) => {
console.log(errStatus);
});
})
.catch((info) => {
console.log("Validate Failed:", info);
});
}}
/>
) : (
<MyModalCloseSaveButtonFooter
onCancel={handleModalClose}
isSaveButtonLoading={isRequesting}
onSave={() => {
// if the service name didn't change, don't send a request
let formServiceName = form.getFieldValue("serviceName");
if (
addEditServiceModalOptions.service.name === formServiceName
) {
handleModalClose();
return;
}
form
.validateFields()
.then(() => {
setIsRequesting(true);
myFetch("/store/services/update", "POST", {
serviceId: addEditServiceModalOptions.service.service_id,
name: formServiceName,
})
.then(() => {
setIsRequesting(false);
handleModalClose();
fetchServices();
})
.catch((errStatus) => {
console.log(errStatus);
});
})
.catch((info) => {
console.log("Validate Failed:", info);
});
}}
/>
)
}
>
<Form form={form} layout="vertical" requiredMark={false}>
<ServiceNameFormInput formItemName="serviceName" />
</Form>
</MyModal>
</>
);
}
let defaultModalAddServiceFormValidOptions = {
serviceActivityName: false,
serviceActivityDescription: false,
serviceActivityPrice: false,
serviceActivityDurationHours: false,
serviceActivityDurationMinutes: false,
};
function ModalAddEditServiceActivity({
fetchServices,
addEditServiceActivityModalOptions,
setAddEditServiceActivityModalOptions,
}) {
const { t } = useTranslation();
const [isFormValid, setIsFormValid] = useState(
defaultModalAddServiceFormValidOptions
);
const [form] = Form.useForm();
const [isRequesting, setIsRequesting] = useState(false);
const handleModalClose = () => {
setAddEditServiceActivityModalOptions({
...addEditServiceActivityModalOptions,
isOpen: false,
});
form.resetFields();
};
useEffect(() => {
if (!addEditServiceActivityModalOptions.isOpen) return;
setIsFormValid(
addEditServiceActivityModalOptions.mode === "add"
? defaultModalAddServiceFormValidOptions
: {
serviceActivityName: true,
serviceActivityDescription: true,
serviceActivityPrice: true,
serviceActivityDurationHours: true,
serviceActivityDurationMinutes: true,
}
);
if (addEditServiceActivityModalOptions.mode === "edit") {
form.setFieldsValue({
serviceActivityName: addEditServiceActivityModalOptions.activity.name,
serviceActivityDescription:
addEditServiceActivityModalOptions.activity.description,
serviceActivityPrice: addEditServiceActivityModalOptions.activity.price,
serviceActivityDurationHours: Math.floor(
addEditServiceActivityModalOptions.activity.duration / 60
),
serviceActivityDurationMinutes:
addEditServiceActivityModalOptions.activity.duration % 60,
});
}
}, [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}
isCreateButtonDisabled={
!isFormValid.serviceActivityName ||
!isFormValid.serviceActivityDescription ||
!isFormValid.serviceActivityPrice ||
!isFormValid.serviceActivityDurationHours ||
!isFormValid.serviceActivityDurationMinutes
}
isCreateButtonLoading={isRequesting}
onCreate={() => {
setIsRequesting(true);
myFetch("/store/services/activity", "POST", {
serviceId:
addEditServiceActivityModalOptions.service.service_id,
name: form.getFieldValue("serviceActivityName"),
description: form.getFieldValue("serviceActivityDescription"),
price: form.getFieldValue("serviceActivityPrice"),
duration:
form.getFieldValue("serviceActivityDurationHours") * 60 +
form.getFieldValue("serviceActivityDurationMinutes"),
})
.then(() => {
setIsRequesting(false);
handleModalClose();
fetchServices();
})
.catch((errStatus) => {
console.log(errStatus);
});
}}
/>
) : (
<MyModalCloseSaveButtonFooter
onCancel={handleModalClose}
isSaveButtonDisabled={
!isFormValid.serviceActivityName ||
!isFormValid.serviceActivityDescription ||
!isFormValid.serviceActivityPrice ||
!isFormValid.serviceActivityDurationHours ||
!isFormValid.serviceActivityDurationMinutes
}
isSaveButtonLoading={isRequesting}
onSave={() => {
// if the service name didn't change, don't send a request
if (
addEditServiceActivityModalOptions.activity.name ===
form.getFieldValue("serviceActivityName") &&
addEditServiceActivityModalOptions.activity.description ===
form.getFieldValue("serviceActivityDescription") &&
addEditServiceActivityModalOptions.activity.price ===
form.getFieldValue("serviceActivityPrice") &&
addEditServiceActivityModalOptions.activity.duration ===
form.getFieldValue("serviceActivityDurationHours") * 60 +
form.getFieldValue("serviceActivityDurationMinutes")
) {
handleModalClose();
console.log("same");
return;
}
setIsRequesting(true);
myFetch("/store/services/activity/update", "POST", {
activityId:
addEditServiceActivityModalOptions.activity.activity_id,
name: form.getFieldValue("serviceActivityName"),
description: form.getFieldValue("serviceActivityDescription"),
price: form.getFieldValue("serviceActivityPrice"),
duration:
form.getFieldValue("serviceActivityDurationHours") * 60 +
form.getFieldValue("serviceActivityDurationMinutes"),
})
.then(() => {
setIsRequesting(false);
handleModalClose();
fetchServices();
})
.catch((errStatus) => {
console.log(errStatus);
});
}}
/>
)
}
>
<Form form={form} layout="vertical" requiredMark={false}>
<ServiceActivityNameFormInput
formItemName="serviceActivityName"
setIsInputValid={(isValid) =>
setIsFormValid({
...isFormValid,
serviceActivityName: isValid,
})
}
/>
<ServiceActivityDescriptionFormInput
formItemName="serviceActivityDescription"
setIsInputValid={(isValid) =>
setIsFormValid({
...isFormValid,
serviceActivityDescription: isValid,
})
}
/>
<ServiceActivityPriceFormInput
formItemName="serviceActivityPrice"
setIsInputValid={(isValid) =>
setIsFormValid({
...isFormValid,
serviceActivityPrice: isValid,
})
}
/>
<ServiceActivityDurationHoursFormInput
formItemName="serviceActivityDurationHours"
setIsInputValid={(isValid) =>
setIsFormValid({
...isFormValid,
serviceActivityDurationHours: isValid,
})
}
/>
<ServiceActivityDurationMinutesFormInput
formItemName="serviceActivityDurationMinutes"
setIsInputValid={(isValid) =>
setIsFormValid({
...isFormValid,
serviceActivityDurationMinutes: isValid,
})
}
/>
</Form>
</MyModal>
);
}
function ServiceNameFormInput({ formItemName }) {
const { t } = useTranslation();
return (
<MyFormInput
formItemName={formItemName}
minLength={Constants.GLOBALS.MIN_STORE_SERVICE_NAME_LENGTH}
maxLength={Constants.GLOBALS.MAX_STORE_SERVICE_NAME_LENGTH}
label={t("storeServices.serviceName")}
ruleMessageValueRequired={t(
"storeServices.inputRules.serviceNameRequired"
)}
ruleMessageValueMinLengthRequired={t(
"storeServices.inputRules.serviceNameMinLength",
{
minLength: Constants.GLOBALS.MIN_STORE_SERVICE_NAME_LENGTH,
}
)}
inputPlaceholder={t("storeServices.serviceNamePlaceholder")}
/>
);
}
function ServiceActivityNameFormInput({ formItemName, setIsInputValid }) {
const { t } = useTranslation();
return (
<MyFormInput
formItemName={formItemName}
setIsInputValid={setIsInputValid}
minLength={Constants.GLOBALS.MIN_STORE_SERVICE_ACTIVITY_NAME_LENGTH}
maxLength={Constants.GLOBALS.MAX_STORE_SERVICE_ACTIVITY_NAME_LENGTH}
label={t("storeServices.serviceActivityName")}
ruleMessageValueRequired={t(
"storeServices.inputRules.serviceActivityNameRequired"
)}
ruleMessageValueMinLengthRequired={t(
"storeServices.inputRules.serviceActivityNameMinLength",
{
minLength: Constants.GLOBALS.MIN_STORE_SERVICE_ACTIVITY_NAME_LENGTH,
}
)}
inputPlaceholder={t("storeServices.serviceActivityNamePlaceholder")}
/>
);
}
function ServiceActivityDescriptionFormInput({
formItemName,
setIsInputValid,
}) {
const { t } = useTranslation();
return (
<MyFormInput
inputType="textarea"
formItemName={formItemName}
setIsInputValid={setIsInputValid}
minLength={
Constants.GLOBALS.MIN_STORE_SERVICE_ACTIVITY_DESCRIPTION_LENGTH
}
maxLength={
Constants.GLOBALS.MAX_STORE_SERVICE_ACTIVITY_DESCRIPTION_LENGTH
}
label={t("storeServices.serviceActivityDescription")}
ruleMessageValueRequired={t(
"storeServices.inputRules.serviceActivityDescriptionRequired"
)}
ruleMessageValueMinLengthRequired={t(
"storeServices.inputRules.serviceActivityDescriptionMinLength",
{
minLength:
Constants.GLOBALS.MIN_STORE_SERVICE_ACTIVITY_DESCRIPTION_LENGTH,
}
)}
inputPlaceholder={t(
"storeServices.serviceActivityDescriptionPlaceholder"
)}
/>
);
}
function ServiceActivityPriceFormInput({ formItemName, setIsInputValid }) {
const { t } = useTranslation();
return (
<MyFormInput
propsInput={{
addonAfter: t("storeServices.serviceActivityPriceUnit"),
}}
inputType="number"
formItemName={formItemName}
setIsInputValid={setIsInputValid}
minLength={Constants.GLOBALS.MIN_STORE_SERVICE_ACTIVITY_PRICE}
maxLength={Constants.GLOBALS.MAX_STORE_SERVICE_ACTIVITY_PRICE}
label={t("storeServices.serviceActivityPrice")}
ruleMessageValueRequired={t(
"storeServices.inputRules.serviceActivityPriceRequired"
)}
inputPlaceholder={t("storeServices.serviceActivityPricePlaceholder")}
/>
);
}
function ServiceActivityDurationHoursFormInput({
formItemName,
setIsInputValid,
}) {
const { t } = useTranslation();
return (
<MyFormInput
propsInput={{
addonAfter: t("storeServices.serviceActivityDurationHoursUnit"),
}}
inputType="number"
formItemName={formItemName}
setIsInputValid={setIsInputValid}
minLength={Constants.GLOBALS.MIN_STORE_SERVICE_ACTIVITY_DURATION_HOURS}
maxLength={Constants.GLOBALS.MAX_STORE_SERVICE_ACTIVITY_DURATION_HOURS}
label={t("storeServices.serviceActivityDurationHours")}
ruleMessageValueRequired={t(
"storeServices.inputRules.serviceActivityDurationHoursRequired"
)}
inputPlaceholder={t(
"storeServices.serviceActivityDurationHoursPlaceholder"
)}
/>
);
}
function ServiceActivityDurationMinutesFormInput({
formItemName,
setIsInputValid,
}) {
const { t } = useTranslation();
return (
<MyFormInput
propsInput={{
addonAfter: t("storeServices.serviceActivityDurationMinutesUnit"),
}}
inputType="number"
formItemName={formItemName}
setIsInputValid={setIsInputValid}
minLength={Constants.GLOBALS.MIN_STORE_SERVICE_ACTIVITY_DURATION_MINUTES}
maxLength={Constants.GLOBALS.MAX_STORE_SERVICE_ACTIVITY_DURATION_MINUTES}
label={t("storeServices.serviceActivityDurationMinutes")}
ruleMessageValueRequired={t(
"storeServices.inputRules.serviceActivityDurationMinutesRequired"
)}
inputPlaceholder={t(
"storeServices.serviceActivityDurationMinutesPlaceholder"
)}
/>
);
}

View File

@ -0,0 +1,31 @@
import { useEffect, useState } from "react";
import { myFetch } from "../../../utils";
import { useParams } from "react-router-dom";
export default function StoreSettings() {
const { storeId } = useParams();
const [storeData, setStoreData] = useState([]);
useEffect(() => {
myFetch(`/store/${storeId}`, "GET")
.then((data) => {
setStoreData(data.store);
})
.catch((errStatus) => {
console.log(errStatus);
});
}, []);
return (
<>
<h1>Store</h1>
<p>{storeData.name}</p>
<p>Name ändern</p>
<p>Firmensitz angeben - Addresse</p>
<p>Telefonnummer, Email (oder in Website einstellungen)</p>
</>
);
}

View File

@ -44,22 +44,38 @@ export const Constants = {
WEBSITE_COLOR_PALETTE: "/website/color-palette", WEBSITE_COLOR_PALETTE: "/website/color-palette",
WEBSITE_SOCIALS: "/website/socials", WEBSITE_SOCIALS: "/website/socials",
WEBSITE_BANNER: "/website/banner", WEBSITE_BANNER: "/website/banner",
EMPLOYEES: "/employees", STORE: {
SERVICES: "/services", SETTINGS: "/store/settings",
CALENDAR: "/calendar", EMPLOYEES: "/store/employees",
SERVICES: "/store/services",
CALENDAR: "/store/calendar",
},
FEEDBACK: "/feedback", FEEDBACK: "/feedback",
SUPPORT: "/support", SUPPORT: "/support",
USER_PROFILE: "/user-profile", USER_PROFILE: "/user-profile",
}, },
GLOBALS: { GLOBALS: {
MIN_USERNAME_LENGTH: 2, MIN_USERNAME_LENGTH: 3,
MAX_USERNAME_LENGTH: 20, MAX_USERNAME_LENGTH: 20,
MIN_ACCOUNT_NAME_LENGTH: 2, MIN_ACCOUNT_NAME_LENGTH: 3,
MAX_ACCOUNT_NAME_LENGTH: 20, MAX_ACCOUNT_NAME_LENGTH: 20,
MIN_PASSWORD_LENGTH: 8, MIN_PASSWORD_LENGTH: 8,
MAX_PASSWORD_LENGTH: 64, MAX_PASSWORD_LENGTH: 64,
MIN_STORE_SERVICE_NAME_LENGTH: 3,
MAX_STORE_SERVICE_NAME_LENGTH: 32,
MIN_STORE_SERVICE_ACTIVITY_NAME_LENGTH: 3,
MAX_STORE_SERVICE_ACTIVITY_NAME_LENGTH: 32,
MIN_STORE_SERVICE_ACTIVITY_DESCRIPTION_LENGTH: 3,
MAX_STORE_SERVICE_ACTIVITY_DESCRIPTION_LENGTH: 1024,
MIN_STORE_SERVICE_ACTIVITY_PRICE: 0,
MAX_STORE_SERVICE_ACTIVITY_PRICE: 10000000,
MIN_STORE_SERVICE_ACTIVITY_DURATION_HOURS: 0,
MAX_STORE_SERVICE_ACTIVITY_DURATION_HOURS: 23,
MIN_STORE_SERVICE_ACTIVITY_DURATION_MINUTES: 0,
MAX_STORE_SERVICE_ACTIVITY_DURATION_MINUTES: 60,
}, },
DELAY_ACCOUNT_NAME_CHECK: 250,
MAX_AVATAR_SIZE: 5 * 1024 * 1024, MAX_AVATAR_SIZE: 5 * 1024 * 1024,
ACCEPTED_AVATAR_FILE_TYPES: [ ACCEPTED_AVATAR_FILE_TYPES: [
"image/png", "image/png",
@ -1375,8 +1391,9 @@ export function myFetch(
// if status is not in range 200-299 // if status is not in range 200-299
if (!response.ok) { if (!response.ok) {
if (!ignoreUnauthorized && response.status === 401) { if (!ignoreUnauthorized && response.status === 401) {
setUserSessionToLocalStorage(""); console.error("Unauthorized");
window.location.href = "/"; //setUserSessionToLocalStorage("");
//window.location.href = "/";
} }
throw response.status; throw response.status;