From a3c979c5b7629adbf56b30b26c4e5587569d9ea0 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 13 Jan 2024 12:06:13 +0100 Subject: [PATCH] services --- public/locales/de/translation.json | 79 ++- public/locales/en/translation.json | 82 ++- src/App.js | 19 +- src/Components/AppRoutes/index.js | 28 +- src/Components/MyFormInputs/index.js | 227 +++++++ src/Components/MyModal/index.js | 8 +- src/Components/SideMenu/index.js | 97 +-- src/Contexts/SideBarContext.js | 26 +- src/Contexts/StoresContext.js | 20 + src/Pages/Employees/index.js | 217 ------- src/Pages/Login/index.js | 198 +++--- src/Pages/Services/index.js | 7 - src/Pages/{ => Store}/Calendar/index.js | 2 +- src/Pages/Store/Employees/index.js | 315 ++++++++++ src/Pages/Store/Services/index.js | 787 ++++++++++++++++++++++++ src/Pages/Store/Settings/index.js | 31 + src/utils.js | 31 +- 17 files changed, 1721 insertions(+), 453 deletions(-) create mode 100644 src/Components/MyFormInputs/index.js create mode 100644 src/Contexts/StoresContext.js delete mode 100644 src/Pages/Employees/index.js delete mode 100644 src/Pages/Services/index.js rename src/Pages/{ => Store}/Calendar/index.js (57%) create mode 100644 src/Pages/Store/Employees/index.js create mode 100644 src/Pages/Store/Services/index.js create mode 100644 src/Pages/Store/Settings/index.js diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index ca88ee6..2c0bec9 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -6,14 +6,27 @@ "save": "Speichern", "delete": "Löschen", "confirm": "Bestätigen", - "create": "Erstellen" + "create": "Erstellen", + "edit": "Bearbeiten" }, "action": "Aktion", "contactAdmin": "Bitte kontaktieren Sie einen Administrator", "username": "Anzeigename", + "usernamePlaceholder": "Geben Sie Ihren Anzeigename ein", "accountName": "Benutzername", + "accountNamePlaceholder": "Geben Sie Ihren Benutzernamen ein", "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": { "title": "Seite nicht gefunden", @@ -28,17 +41,73 @@ "banner": "Banner", "socials": "Soziale Netzwerke" }, - "employees": "Mitarbeiter", - "services": "Dienstleistungen", - "calendar": "Kalender", + "store": { + "titleSingular": "Geschäft", + "titlePlural": "Geschäfte", + "settings": "Einstellungen", + "employees": "Mitarbeiter", + "services": "Dienstleistungen", + "calendar": "Kalender" + }, "support": "Unterstützung", "feedback": "Feedback" }, "employees": { "pageTitle": "Mitarbeiter", "addEmployee": "Mitarbeiter anlegen", + "editEmployee": "Mitarbeiter bearbeiten", "modalAddEmployee": { "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" } } } diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 6e35f47..991be4a 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -6,14 +6,27 @@ "save": "Save", "delete": "Delete", "confirm": "Confirm", - "create": "Create" + "create": "Create", + "edit": "Edit" }, "action": "Action", "contactAdmin": "Please contact an administrator", "username": "Username", + "usernamePlaceholder": "Enter your username", "accountName": "Account name", + "accountNamePlaceholder": "Enter your account name", "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": { "title": "Page Not Found", @@ -28,17 +41,76 @@ "banner": "Banner", "socials": "Socials" }, - "employees": "Employees", - "services": "Services", - "calendar": "Calendar", + "store": { + "titleSingular": "Store", + "titlePlural": "Stores", + "settings": "Settings", + "employees": "Employees", + "services": "Services", + "calendar": "Calendar" + }, "support": "Support", "feedback": "Feedback" }, "employees": { "pageTitle": "Employees", "addEmployee": "Add employee", + "editEmployee": "Edit employee", "modalAddEmployee": { "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" } } } diff --git a/src/App.js b/src/App.js index 5debff8..10c357d 100644 --- a/src/App.js +++ b/src/App.js @@ -10,6 +10,7 @@ import { UserProfileProvider } from "./Contexts/UserProfileContext"; import { UsersProvider } from "./Contexts/UsersContext"; import HeaderProvider from "./Contexts/HeaderContext"; import { useEffect, useState } from "react"; +import StoresProvider from "./Contexts/StoresContext"; export default function App() { /*const [notificationApi, notificationContextHolder] = @@ -23,15 +24,11 @@ export default function App() { useEffect(() => { if (!userSession) return; - console.log("userprofile"); - myFetch("/user", "GET") .then((data) => { - console.log(data); - setAppUserData(data); }) - .catch((errStatus) => { + .catch(() => { setUserSession(); window.location.href = "/"; }); @@ -78,14 +75,16 @@ export default function App() { return ( - + - + + + diff --git a/src/Components/AppRoutes/index.js b/src/Components/AppRoutes/index.js index 6e19398..48c710c 100644 --- a/src/Components/AppRoutes/index.js +++ b/src/Components/AppRoutes/index.js @@ -7,9 +7,10 @@ import { MySupsenseFallback } from "../MySupsenseFallback"; // Lazy-loaded components const Dashboard = lazy(() => import("../../Pages/Dashboard")); const PageNotFound = lazy(() => import("../../Pages/PageNotFound")); -const Employees = lazy(() => import("../../Pages/Employees")); -const Services = lazy(() => import("../../Pages/Services")); -const Calendar = lazy(() => import("../../Pages/Calendar")); +const StoreSettings = lazy(() => import("../../Pages/Store/Settings")); +const StoreEmployees = lazy(() => import("../../Pages/Store/Employees")); +const StoreServices = lazy(() => import("../../Pages/Store/Services")); +const StoreCalendar = lazy(() => import("../../Pages/Store/Calendar")); const Support = lazy(() => import("../../Pages/Support")); const Feedback = lazy(() => import("../../Pages/Feedback")); const UserProfile = lazy(() => import("../../Pages/UserProfile")); @@ -29,28 +30,37 @@ export default function AppRoutes({ userSession, setUserSession }) { /> - + } /> - + } /> - + + + } + /> + + + } /> diff --git a/src/Components/MyFormInputs/index.js b/src/Components/MyFormInputs/index.js new file mode 100644 index 0000000..530f9ec --- /dev/null +++ b/src/Components/MyFormInputs/index.js @@ -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 ( + + ); +} + +export function MyAccountNameFormInput({ + propsFormItem, + propsInput, + disableAccountNameCheck, + hasFeedback, +}) { + const { t } = useTranslation(); + + return ( + + ); +} + +export function MyPasswordFormInput({ propsFormItem, propsInput }) { + const { t } = useTranslation(); + + return ( + + ); +} + +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 ( + ({ + 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: ( + + ), + number: , + password: , + default: , + }; + + return ( + + {inputComponents[inputType] || inputComponents.default} + + ); +} diff --git a/src/Components/MyModal/index.js b/src/Components/MyModal/index.js index 7ebad12..f0f98a5 100644 --- a/src/Components/MyModal/index.js +++ b/src/Components/MyModal/index.js @@ -108,13 +108,19 @@ export function MyModalCloseSaveButtonFooter({ onCancel, onSave, isSaveButtonLoading, + isSaveButtonDisabled, }) { const { t } = useTranslation(); return ( <> - diff --git a/src/Components/SideMenu/index.js b/src/Components/SideMenu/index.js index a7d9f83..22014c1 100644 --- a/src/Components/SideMenu/index.js +++ b/src/Components/SideMenu/index.js @@ -1,48 +1,33 @@ import { AppstoreOutlined, BgColorsOutlined, - BookOutlined, CalendarOutlined, ClusterOutlined, - ControlOutlined, - DesktopOutlined, EditOutlined, FileImageOutlined, - FileTextOutlined, - HistoryOutlined, MessageOutlined, QuestionCircleOutlined, - RobotOutlined, - ScanOutlined, ScissorOutlined, SettingOutlined, - SnippetsOutlined, + ShopOutlined, TeamOutlined, UserOutlined, - UsergroupAddOutlined, } from "@ant-design/icons"; -import { Badge, Divider, Menu } from "antd"; -import { useEffect, useRef, useState } from "react"; +import { Divider, Menu } from "antd"; +import { useEffect, useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; -import { - BreakpointLgWidth, - BrowserTabSession, - Constants, - hasOnePermission, - hasOneXYPermission, - hasPermission, - wsConnectionCustomEventName, -} from "../../utils"; +import { BreakpointLgWidth, Constants } from "../../utils"; import { useTranslation } from "react-i18next"; import { useSideBarContext } from "../../Contexts/SideBarContext"; import { useAppContext } from "../../Contexts/AppContext"; +import { useStoresContext } from "../../Contexts/StoresContext"; export function SideMenuContent({ setIsSideMenuCollapsed, contentFirstRender, }) { - const appContext = useAppContext(); const sideBarContext = useSideBarContext(); + const storesContext = useStoresContext(); const location = useLocation(); const [selectedKeys, setSelectedKeys] = useState("/"); const { t } = useTranslation(); @@ -88,29 +73,52 @@ export function SideMenuContent({ items.push(groupWebsite); - // employees + // stores - items.push({ - label: t("sideMenu.employees"), - icon: , - key: Constants.ROUTE_PATHS.EMPLOYEES, + let stores = { + 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: , + children: [], + }; + + groupStore.children.push({ + label: t("sideMenu.store.settings"), + icon: , + key: `${Constants.ROUTE_PATHS.STORE.SETTINGS}/${store.store_id}`, + }); + + groupStore.children.push({ + label: t("sideMenu.store.employees"), + icon: , + key: `${Constants.ROUTE_PATHS.STORE.EMPLOYEES}/${store.store_id}`, + }); + + groupStore.children.push({ + label: t("sideMenu.store.services"), + icon: , + key: `${Constants.ROUTE_PATHS.STORE.SERVICES}/${store.store_id}`, + }); + + groupStore.children.push({ + label: t("sideMenu.store.calendar"), + icon: , + key: `${Constants.ROUTE_PATHS.STORE.CALENDAR}/${store.store_id}`, + }); + + stores.children.push(groupStore); }); - // services - - items.push({ - label: t("sideMenu.services"), - icon: , - key: Constants.ROUTE_PATHS.SERVICES, - }); - - // calendar - - items.push({ - label: t("sideMenu.calendar"), - icon: , - key: Constants.ROUTE_PATHS.CALENDAR, - }); + items.push(stores); return items; }; @@ -118,20 +126,19 @@ export function SideMenuContent({ const getSecondMenuItems = () => { let items = []; - // connection status, userprofile, logout items.push( { - label: "Support", + label: t("sideMenu.support"), icon: , key: Constants.ROUTE_PATHS.SUPPORT, }, { - label: "Feedback", + label: t("sideMenu.feedback"), icon: , key: Constants.ROUTE_PATHS.FEEDBACK, }, { - label: ` ${sideBarContext.username}`, + label: sideBarContext.username, icon: , key: Constants.ROUTE_PATHS.USER_PROFILE, } diff --git a/src/Contexts/SideBarContext.js b/src/Contexts/SideBarContext.js index af17013..702fbae 100644 --- a/src/Contexts/SideBarContext.js +++ b/src/Contexts/SideBarContext.js @@ -1,42 +1,22 @@ import { createContext, useContext, useState } from "react"; -import { Constants } from "../utils"; const preview = { - connectionBadgeStatus: "", - connectedUsers: 0, - selectedScanner: "", username: "", - avatar: "", - availableCategories: [], + setUsername: () => {}, }; const SideBarContext = createContext(preview); export const useSideBarContext = () => useContext(SideBarContext); -export default function SideBarProvider({ _username, children }) { - const [connectionBadgeStatus, setConnectionBadgeStatus] = useState("error"); - const [connectedUsers, setConnectedUsers] = useState(0); - const [selectedScanner, setSelectedScanner] = useState(""); - const [username, setUsername] = useState(_username); // - const [avatar, setAvatar] = useState(""); - const [availableCategories, setAvailableCategories] = useState([]); +export default function SideBarProvider({ children, options }) { + const [username, setUsername] = useState(options.username); return ( {children} diff --git a/src/Contexts/StoresContext.js b/src/Contexts/StoresContext.js new file mode 100644 index 0000000..07c012b --- /dev/null +++ b/src/Contexts/StoresContext.js @@ -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 ( + + {children} + + ); +} diff --git a/src/Pages/Employees/index.js b/src/Pages/Employees/index.js deleted file mode 100644 index 7ab508b..0000000 --- a/src/Pages/Employees/index.js +++ /dev/null @@ -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: () => {t("common.button.delete")}, - }, - ]; - }; - - 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 ( - <> -
-

- {t("employees.pageTitle")} - {employees.length > 0 && ` (${employees.length})`} -

- -
- - - - { - setIsRequesting(true); - - myFetch("/users", "POST", { - username: username, - accountName: accountName, - password: EncodeStringToBase64(password), - }) - .then(() => { - setIsRequesting(false); - handleAddEmployeeModalClose(); - - fetchEmployees(); - }) - .catch((errStatus) => { - console.log(errStatus); - - setIsRequesting(false); - }); - }} - /> - } - > -
- - } - placeholder={t("common.username")} - value={username} - onChange={(e) => setUsername(e.target.value)} - minLength={Constants.GLOBALS.MIN_USERNAME_LENGTH} - maxLength={Constants.GLOBALS.MAX_USERNAME_LENGTH} - /> - - - - } - placeholder={t("common.accountName")} - value={accountName} - onChange={(e) => setAccountName(e.target.value)} - minLength={Constants.GLOBALS.MIN_USERNAME_LENGTH} - maxLength={Constants.GLOBALS.MAX_USERNAME_LENGTH} - /> - - - - } - placeholder={t("common.password")} - value={password} - onChange={(e) => setPassword(e.target.value)} - minLength={Constants.GLOBALS.MIN_PASSWORD_LENGTH} - maxLength={Constants.GLOBALS.MAX_PASSWORD_LENGTH} - /> - - - - - {t("employees.modalAddEmployee.checkboxPasswordChange")} - - -
-
- - ); -} diff --git a/src/Pages/Login/index.js b/src/Pages/Login/index.js index 380fb09..b178e0c 100644 --- a/src/Pages/Login/index.js +++ b/src/Pages/Login/index.js @@ -1,21 +1,27 @@ -import { LockOutlined, LoginOutlined, UserOutlined } from "@ant-design/icons"; -import { Button, Form, Input, Modal, Tabs, notification } from "antd"; +import { LoginOutlined } from "@ant-design/icons"; +import { Button, Form, Modal, Tabs, notification } from "antd"; import { - Constants, EncodeStringToBase64, myFetch, myFetchContentType, setUserSessionToLocalStorage, } from "../../utils"; import { useState } from "react"; +import { + MyAccountNameFormInput, + MyPasswordFormInput, + MyUsernameFormInput, +} from "../../Components/MyFormInputs"; +import { useTranslation } from "react-i18next"; export default function Login() { - const [username, setUsername] = useState(""); - const [accountName, setAccountName] = useState(""); - const [password, setPassword] = useState(""); + const { t } = useTranslation(); + const [form] = Form.useForm(); + const [api, contextHolder] = notification.useNotification(); const [selectedMethod, setSelectedMethod] = useState("1"); + const [isRequesting, setIsRequesting] = useState(false); const showErrorNotification = (errStatus) => { if (errStatus === 401) { @@ -32,44 +38,6 @@ export default function Login() { }); }; - const handleSubmit = () => { - if ( - accountName.length > Constants.GLOBALS.MAX_ACCOUNT_NAME_LENGTH || - accountName.length < Constants.GLOBALS.MIN_ACCOUNT_NAME_LENGTH || - password.length > Constants.GLOBALS.MAX_PASSWORD_LENGTH || - password.length < Constants.GLOBALS.MIN_PASSWORD_LENGTH - ) { - showErrorNotification(); - return; - } - - let body = { - accountName: accountName.toLocaleLowerCase(), - password: EncodeStringToBase64(password), - }; - - if (selectedMethod === "2") { - body.username = username; - } - - myFetch( - `/user/auth/${selectedMethod === "1" ? "login" : "signup"}`, - "POST", - body, - {}, - myFetchContentType.JSON, - "", - true - ) - .then((data) => { - console.log(data.XAuthorization); - - setUserSessionToLocalStorage(data.XAuthorization); - window.location.href = "/"; - }) - .catch((errStatus) => showErrorNotification(errStatus)); - }; - return ( <> {contextHolder} @@ -78,28 +46,18 @@ export default function Login() { closable={false} centered keyboard={false} - footer={ - - } + footer={null} > -
- {selectedMethod === "2" && ( - + {selectedMethod === "2" && } + + + + + +
+ +
diff --git a/src/Pages/Services/index.js b/src/Pages/Services/index.js deleted file mode 100644 index 60df5eb..0000000 --- a/src/Pages/Services/index.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function Services() { - return ( - <> -

Services

- - ); -} diff --git a/src/Pages/Calendar/index.js b/src/Pages/Store/Calendar/index.js similarity index 57% rename from src/Pages/Calendar/index.js rename to src/Pages/Store/Calendar/index.js index 9cffe98..3a9371b 100644 --- a/src/Pages/Calendar/index.js +++ b/src/Pages/Store/Calendar/index.js @@ -1,4 +1,4 @@ -export default function Calendar() { +export default function StoreCalendar() { return ( <>

Calendar

diff --git a/src/Pages/Store/Employees/index.js b/src/Pages/Store/Employees/index.js new file mode 100644 index 0000000..ae055f6 --- /dev/null +++ b/src/Pages/Store/Employees/index.js @@ -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) => ( + + { + setAddEditEmployeeModalOptions({ + ...addEditEmployeeModalOptions, + mode: "edit", + isOpen: true, + selectedEmployee: record, + }); + }} + > + {t("common.button.edit")} + + { + setIsRequesting(true); + + myFetch("/users", "DELETE", { + userId: record.key, + }) + .then(() => fetchEmployees()) + .catch((errStatus) => { + console.log(errStatus); + + setIsRequesting(false); + }); + }} + > + {t("common.button.delete")} + + + ), + }, + ]; + }; + + 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 ( + <> +
+

+ {t("employees.pageTitle")} + {employees.length > 0 && ` (${employees.length})`} +

+ + +
+ + + + ); +} + +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 ( + <> + + + { + 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); + }); + }} + /> + ) : ( + { + // 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); + }); + }} + /> + ) + } + > +
+ + + + + {modalOptions.mode === "add" && ( + <> + + + + {t("employees.modalAddEmployee.checkboxPasswordChange")} + + + + )} + +
+ + ); +} diff --git a/src/Pages/Store/Services/index.js b/src/Pages/Store/Services/index.js new file mode 100644 index 0000000..a4af356 --- /dev/null +++ b/src/Pages/Store/Services/index.js @@ -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 ( + <> +
+

{t("storeServices.pageTitle")}

+ + +
+ + {isRequestingServices ? ( + <> + + + + + ) : ( + <> + {servicesData.services.map((service) => ( + + ))} + + )} + + + + + ); +} + +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 ( + setIsOpen(e.length !== 0)} + items={[ + { + key: "1", + label: ( +
+ {service.name} + + + e.stopPropagation()} + /> + e.stopPropagation()} + /> + { + e.stopPropagation(); + + setAddEditServiceActivityModalOptions({ + mode: "add", + isOpen: true, + service: service, + }); + }} + /> + { + e.stopPropagation(); + + setAddEditServiceModalOptions({ + mode: "edit", + isOpen: true, + service: service, + }); + }} + /> + e.stopPropagation()} /> + +
+ ), + children: ( + + {isRequestingActivities ? ( + + +

Desc

+

Preis: €

+

Dauer: Minuten

+
+
+ ) : ( + <> + {serviceActivities.length === 0 ? ( + + ) : ( + serviceActivities.map((activity) => ( + + + {users.map((user) => ( + + + {user.username.charAt(0)} + + + ))} + + + + + { + setAddEditServiceActivityModalOptions({ + mode: "edit", + isOpen: true, + activity: activity, + }); + }} + /> + +
+ } + > +

{activity.description}

+

Preis: {activity.price} €

+

Dauer: {activity.duration} Minuten

+ + )) + )} + + )} + + ), + }, + ]} + /> + ); +} + +// 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 ( + <> + + + { + 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); + }); + }} + /> + ) : ( + { + // 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); + }); + }} + /> + ) + } + > +
+ + +
+ + ); +} + +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 ( + { + 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); + }); + }} + /> + ) : ( + { + // 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); + }); + }} + /> + ) + } + > +
+ + setIsFormValid({ + ...isFormValid, + serviceActivityName: isValid, + }) + } + /> + + + setIsFormValid({ + ...isFormValid, + serviceActivityDescription: isValid, + }) + } + /> + + + setIsFormValid({ + ...isFormValid, + serviceActivityPrice: isValid, + }) + } + /> + + + setIsFormValid({ + ...isFormValid, + serviceActivityDurationHours: isValid, + }) + } + /> + + + setIsFormValid({ + ...isFormValid, + serviceActivityDurationMinutes: isValid, + }) + } + /> + +
+ ); +} + +function ServiceNameFormInput({ formItemName }) { + const { t } = useTranslation(); + + return ( + + ); +} + +function ServiceActivityNameFormInput({ formItemName, setIsInputValid }) { + const { t } = useTranslation(); + + return ( + + ); +} + +function ServiceActivityDescriptionFormInput({ + formItemName, + setIsInputValid, +}) { + const { t } = useTranslation(); + + return ( + + ); +} + +function ServiceActivityPriceFormInput({ formItemName, setIsInputValid }) { + const { t } = useTranslation(); + + return ( + + ); +} + +function ServiceActivityDurationHoursFormInput({ + formItemName, + setIsInputValid, +}) { + const { t } = useTranslation(); + + return ( + + ); +} + +function ServiceActivityDurationMinutesFormInput({ + formItemName, + setIsInputValid, +}) { + const { t } = useTranslation(); + + return ( + + ); +} diff --git a/src/Pages/Store/Settings/index.js b/src/Pages/Store/Settings/index.js new file mode 100644 index 0000000..b18470c --- /dev/null +++ b/src/Pages/Store/Settings/index.js @@ -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 ( + <> +

Store

+ +

{storeData.name}

+ +

Name ändern

+

Firmensitz angeben - Addresse

+

Telefonnummer, Email (oder in Website einstellungen)

+ + ); +} diff --git a/src/utils.js b/src/utils.js index 60b039e..cb6d9bf 100644 --- a/src/utils.js +++ b/src/utils.js @@ -44,22 +44,38 @@ export const Constants = { WEBSITE_COLOR_PALETTE: "/website/color-palette", WEBSITE_SOCIALS: "/website/socials", WEBSITE_BANNER: "/website/banner", - EMPLOYEES: "/employees", - SERVICES: "/services", - CALENDAR: "/calendar", + STORE: { + SETTINGS: "/store/settings", + EMPLOYEES: "/store/employees", + SERVICES: "/store/services", + CALENDAR: "/store/calendar", + }, FEEDBACK: "/feedback", SUPPORT: "/support", USER_PROFILE: "/user-profile", }, GLOBALS: { - MIN_USERNAME_LENGTH: 2, + MIN_USERNAME_LENGTH: 3, MAX_USERNAME_LENGTH: 20, - MIN_ACCOUNT_NAME_LENGTH: 2, + MIN_ACCOUNT_NAME_LENGTH: 3, MAX_ACCOUNT_NAME_LENGTH: 20, MIN_PASSWORD_LENGTH: 8, 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, ACCEPTED_AVATAR_FILE_TYPES: [ "image/png", @@ -1375,8 +1391,9 @@ export function myFetch( // if status is not in range 200-299 if (!response.ok) { if (!ignoreUnauthorized && response.status === 401) { - setUserSessionToLocalStorage(""); - window.location.href = "/"; + console.error("Unauthorized"); + //setUserSessionToLocalStorage(""); + //window.location.href = "/"; } throw response.status;