From e465e1a54e5d1e72b58d2bc46a0f89c8bfb71469 Mon Sep 17 00:00:00 2001 From: VIRUSPARTH Date: Tue, 29 Oct 2024 00:17:17 +0530 Subject: [PATCH] new changes --- backend/.env | 4 +- backend/Dockerfile | 15 + backend/package-lock.json | 109 ++- backend/package.json | 7 +- .../appControllers/clientController/create.js | 91 +++ .../appControllers/clientController/index.js | 17 +- .../clientController/listAll.js | 30 + .../clientController/migrate.js | 16 + .../clientController/paginatedList.js | 67 ++ .../appControllers/clientController/read.js | 29 + .../appControllers/clientController/remove.js | 84 ++ .../appControllers/clientController/search.js | 51 ++ .../appControllers/clientController/update.js | 12 + .../appControllers/companyController/index.js | 24 + .../companyController/remove.js | 57 ++ .../companyController/update.js | 44 + .../invoiceController/remove.js | 29 +- .../invoiceController/update.js | 69 +- .../appControllers/leadController/create.js | 51 ++ .../appControllers/leadController/index.js | 29 + .../appControllers/leadController/listAll.js | 30 + .../appControllers/leadController/migrate.js | 18 + .../leadController/paginatedList.js | 68 ++ .../appControllers/leadController/read.js | 29 + .../appControllers/leadController/remove.js | 10 + .../appControllers/leadController/search.js | 50 ++ .../appControllers/leadController/summary.js | 97 +++ .../appControllers/leadController/update.js | 13 + .../appControllers/offerController/create.js | 61 ++ .../appControllers/offerController/index.js | 18 + .../offerController/paginatedList.js | 68 ++ .../appControllers/offerController/read.js | 30 + .../offerController/sendMail.js | 9 + .../appControllers/offerController/summary.js | 123 +++ .../appControllers/offerController/update.js | 10 + .../paymentController/remove.js | 63 +- .../paymentController/update.js | 81 +- .../appControllers/peopleController/index.js | 21 + .../peopleController/paginatedList.js | 64 ++ .../appControllers/peopleController/read.js | 26 + .../appControllers/peopleController/remove.js | 8 + .../appControllers/peopleController/update.js | 13 + .../appControllers/quoteController/update.js | 57 +- .../appControllers/taxesController/index.js | 37 +- .../coreControllers/emailController/index.js | 14 + .../settingController/updateBySettingKey.js | 50 +- .../settingController/updateManySetting.js | 59 +- .../createAuthMiddleware/authUser.js | 50 +- .../createAuthMiddleware/forgetPassword.js | 7 + .../createAuthMiddleware/index.js | 6 + .../createAuthMiddleware/isValidAuthToken.js | 10 +- .../createAuthMiddleware/login.js | 15 +- .../createAuthMiddleware/logout.js | 48 +- .../createAuthMiddleware/register.js | 84 ++ .../createAuthMiddleware/resetPassword.js | 71 +- .../createAuthMiddleware/sendIdurarOffer.js | 18 + .../createAuthMiddleware/sendMail.js | 7 +- .../createCRUDController/remove.js | 33 +- .../createCRUDController/update.js | 31 +- .../createUserController/updatePassword.js | 8 - .../createUserController/updateProfile.js | 9 - .../updateProfilePassword.js | 7 - backend/src/emailTemplate/emailVerfication.js | 72 +- backend/src/locale/languages.js | 63 +- backend/src/locale/translation/en_us.js | 2 +- backend/src/middlewares/rateLimiter.js | 53 ++ backend/src/models/appModels/Client.js | 16 +- backend/src/models/appModels/Company.js | 217 +++++ backend/src/models/appModels/Employee.js | 186 +++++ backend/src/models/appModels/Expense.js | 98 +++ .../src/models/appModels/ExpenseCategory.js | 39 + backend/src/models/appModels/Lead.js | 92 +++ backend/src/models/appModels/Offer.js | 135 ++++ backend/src/models/appModels/Order.js | 140 ++++ backend/src/models/appModels/People.js | 203 +++++ backend/src/models/appModels/Product.js | 102 +++ .../src/models/appModels/ProductCategory.js | 76 ++ backend/src/models/appModels/Purchase.js | 153 ++++ backend/src/models/appModels/Shipment.js | 119 +++ backend/src/models/coreModels/Email.js | 47 ++ backend/src/routes/appRoutes/appApi.js | 2 +- backend/src/routes/coreRoutes/coreApi.js | 11 + backend/src/routes/coreRoutes/coreAuth.js | 5 +- .../src/routes/coreRoutes/corePublicRouter.js | 5 +- backend/src/server.js | 20 +- .../setup/defaultSettings/emailSettings.json | 20 + .../defaultSettings/inventorySettings.json | 38 + .../setup/defaultSettings/leadSettings.json | 85 ++ .../setup/defaultSettings/offerSettings.json | 34 + backend/src/setup/emailTemplate/index.json | 51 ++ backend/src/setup/reset.js | 4 - backend/src/utils/redis.js | 9 + frontend/Dockerfile | 16 + frontend/index.html | 7 +- frontend/package-lock.json | 753 +++++++++--------- frontend/package.json | 12 +- frontend/src/apps/ErpApp.jsx | 29 +- frontend/src/apps/Header/HeaderContainer.jsx | 9 +- frontend/src/apps/Header/UpgradeButton.jsx | 79 +- frontend/src/apps/Navigation/AppNav.jsx | 128 +++ frontend/src/apps/Navigation/ExpensesNav.jsx | 167 ++++ .../apps/Navigation/NavigationContainer.jsx | 94 ++- .../src/components/ChooseCurrency/index.jsx | 92 +++ .../src/components/DataTable/DataTable.jsx | 6 +- frontend/src/components/PageLoader/index.jsx | 5 +- .../components/PaypalButton/Subscription.jsx | 38 + frontend/src/components/SelectAsync/index.jsx | 4 +- frontend/src/components/SelectTag/index.jsx | 8 +- frontend/src/forms/DynamicForm/index.jsx | 5 +- frontend/src/forms/LoginForm.jsx | 19 +- frontend/src/forms/RegisterForm.jsx | 23 + frontend/src/layout/AuthLayout/index.jsx | 8 +- frontend/src/layout/ErpLayout/index.jsx | 4 + frontend/src/locale/Localization.jsx | 5 +- frontend/src/locale/translation/en_us.js | 4 +- frontend/src/locale/useLanguage.jsx | 63 +- .../modules/AdvancedCrudModule/CreateItem.jsx | 95 +++ .../modules/AdvancedCrudModule/DataTable.jsx | 194 +++++ .../modules/AdvancedCrudModule/DeleteItem.jsx | 59 ++ .../modules/AdvancedCrudModule/ItemRow.jsx | 121 +++ .../modules/AdvancedCrudModule/ReadItem.jsx | 315 ++++++++ .../modules/AdvancedCrudModule/SearchItem.jsx | 96 +++ .../modules/AdvancedCrudModule/UpdateItem.jsx | 175 ++++ .../src/modules/AdvancedCrudModule/index.jsx | 35 + .../src/modules/AuthModule/SideContent.jsx | 99 ++- .../components/CustomerPreviewCard.jsx | 28 +- .../components/PreviewCard.jsx | 17 +- .../src/modules/DashboardModule/index.jsx | 27 +- .../EmailDataTableModule/index.jsx | 10 + .../ReadEmailModule/components/ReadItem.jsx | 102 +++ .../EmailModule/ReadEmailModule/index.jsx | 39 + .../componenets/EmailForm.jsx | 55 ++ .../EmailModule/UpdateEmailModule/index.jsx | 49 ++ .../src/modules/ErpPanelModule/CreateItem.jsx | 5 +- .../src/modules/ErpPanelModule/DataTable.jsx | 5 +- .../src/modules/ErpPanelModule/ReadItem.jsx | 11 +- .../src/modules/ErpPanelModule/UpdateItem.jsx | 9 +- .../InvoiceModule/ReadInvoiceModule/index.jsx | 9 + .../components/Payment.jsx | 18 +- .../OfferModule/CreateOfferModule/index.jsx | 11 + .../modules/OfferModule/Forms/OfferForm.jsx | 290 +++++++ .../OfferDataTableModule/index.jsx | 10 + .../ReadOfferModule/ReadOfferItem.jsx | 318 ++++++++ .../OfferModule/ReadOfferModule/index.jsx | 40 + .../OfferModule/UpdateOfferModule/index.jsx | 50 ++ .../OrderModule/CreateOrderModule/index.jsx | 11 + .../modules/OrderModule/Forms/InvoiceForm.jsx | 284 +++++++ .../OrderDataTableModule/index.jsx | 22 + .../OrderModule/ReadOrderModule/index.jsx | 39 + .../components/Payment.jsx | 140 ++++ .../components/RecordPayment.jsx | 73 ++ .../OrderModule/RecordPaymentModule/index.jsx | 37 + .../OrderModule/UpdateInvoiceModule/index.jsx | 50 ++ .../ReadPaymentModule/components/ReadItem.jsx | 8 +- .../components/Payment.jsx | 12 +- .../modules/QuoteModule/Forms/QuoteForm.jsx | 9 +- .../CompanySettingsModule/SettingsForm.jsx | 4 +- .../FinanceSettingsModule/SettingsForm.jsx | 30 +- .../forms/GeneralSettingForm.jsx | 77 +- .../SettingsForm.jsx | 7 +- .../components/SetingsSection.jsx | 5 +- .../components/UpdateSettingModule.jsx | 7 + frontend/src/pages/AdvancedSettings/index.jsx | 102 +++ frontend/src/pages/Company/config.js | 173 ++++ frontend/src/pages/Company/index.jsx | 39 + frontend/src/pages/Customer/config.js | 43 +- frontend/src/pages/Email/EmailRead.jsx | 21 + frontend/src/pages/Email/EmailUpdate.jsx | 22 + frontend/src/pages/Email/index.jsx | 63 ++ frontend/src/pages/Employee/index.jsx | 125 +++ frontend/src/pages/Expense/config.js | 25 + frontend/src/pages/Expense/index.jsx | 39 + frontend/src/pages/ExpenseCategory/config.js | 21 + frontend/src/pages/ExpenseCategory/index.jsx | 39 + frontend/src/pages/Inventory/index.jsx | 71 ++ frontend/src/pages/Invoice/index.jsx | 24 + frontend/src/pages/Lead/config.js | 90 +++ frontend/src/pages/Lead/index.jsx | 39 + frontend/src/pages/Login.jsx | 67 +- frontend/src/pages/Offer/OfferCreate.jsx | 20 + frontend/src/pages/Offer/OfferRead.jsx | 20 + frontend/src/pages/Offer/OfferUpdate.jsx | 20 + frontend/src/pages/Offer/index.jsx | 102 +++ frontend/src/pages/Order/index.jsx | 97 +++ frontend/src/pages/People/config.js | 94 +++ frontend/src/pages/People/index.jsx | 39 + frontend/src/pages/Product/config.js | 25 + frontend/src/pages/Product/index.jsx | 39 + frontend/src/pages/ProductCategory/config.js | 21 + frontend/src/pages/ProductCategory/index.jsx | 39 + frontend/src/pages/Quote/index.jsx | 10 + frontend/src/pages/Register.jsx | 60 ++ frontend/src/redux/rootReducer.js | 2 + frontend/src/redux/store.js | 23 +- frontend/src/redux/translate/actions.js | 41 + frontend/src/redux/translate/index.js | 1 + frontend/src/redux/translate/reducer.js | 47 ++ frontend/src/redux/translate/selectors.js | 10 + frontend/src/redux/translate/types.js | 5 + frontend/src/request/errorHandler.js | 17 +- frontend/src/request/request.js | 38 +- frontend/src/router/AuthRouter.jsx | 2 + frontend/src/router/routes.jsx | 76 +- frontend/src/style/partials/collapseBox.css | 2 +- frontend/src/style/partials/core.css | 5 +- frontend/src/style/partials/customAntd.css | 27 +- frontend/src/style/partials/navigation.css | 8 +- frontend/src/style/partials/rest.css | 2 +- frontend/src/utils/countryList.js | 199 ++++- 209 files changed, 10003 insertions(+), 1239 deletions(-) create mode 100644 backend/Dockerfile create mode 100644 backend/src/controllers/appControllers/clientController/create.js create mode 100644 backend/src/controllers/appControllers/clientController/listAll.js create mode 100644 backend/src/controllers/appControllers/clientController/migrate.js create mode 100644 backend/src/controllers/appControllers/clientController/paginatedList.js create mode 100644 backend/src/controllers/appControllers/clientController/read.js create mode 100644 backend/src/controllers/appControllers/clientController/remove.js create mode 100644 backend/src/controllers/appControllers/clientController/search.js create mode 100644 backend/src/controllers/appControllers/clientController/update.js create mode 100644 backend/src/controllers/appControllers/companyController/index.js create mode 100644 backend/src/controllers/appControllers/companyController/remove.js create mode 100644 backend/src/controllers/appControllers/companyController/update.js create mode 100644 backend/src/controllers/appControllers/leadController/create.js create mode 100644 backend/src/controllers/appControllers/leadController/index.js create mode 100644 backend/src/controllers/appControllers/leadController/listAll.js create mode 100644 backend/src/controllers/appControllers/leadController/migrate.js create mode 100644 backend/src/controllers/appControllers/leadController/paginatedList.js create mode 100644 backend/src/controllers/appControllers/leadController/read.js create mode 100644 backend/src/controllers/appControllers/leadController/remove.js create mode 100644 backend/src/controllers/appControllers/leadController/search.js create mode 100644 backend/src/controllers/appControllers/leadController/summary.js create mode 100644 backend/src/controllers/appControllers/leadController/update.js create mode 100644 backend/src/controllers/appControllers/offerController/create.js create mode 100644 backend/src/controllers/appControllers/offerController/index.js create mode 100644 backend/src/controllers/appControllers/offerController/paginatedList.js create mode 100644 backend/src/controllers/appControllers/offerController/read.js create mode 100644 backend/src/controllers/appControllers/offerController/sendMail.js create mode 100644 backend/src/controllers/appControllers/offerController/summary.js create mode 100644 backend/src/controllers/appControllers/offerController/update.js create mode 100644 backend/src/controllers/appControllers/peopleController/index.js create mode 100644 backend/src/controllers/appControllers/peopleController/paginatedList.js create mode 100644 backend/src/controllers/appControllers/peopleController/read.js create mode 100644 backend/src/controllers/appControllers/peopleController/remove.js create mode 100644 backend/src/controllers/appControllers/peopleController/update.js create mode 100644 backend/src/controllers/coreControllers/emailController/index.js create mode 100644 backend/src/controllers/middlewaresControllers/createAuthMiddleware/register.js create mode 100644 backend/src/controllers/middlewaresControllers/createAuthMiddleware/sendIdurarOffer.js create mode 100644 backend/src/middlewares/rateLimiter.js create mode 100644 backend/src/models/appModels/Company.js create mode 100644 backend/src/models/appModels/Employee.js create mode 100644 backend/src/models/appModels/Expense.js create mode 100644 backend/src/models/appModels/ExpenseCategory.js create mode 100644 backend/src/models/appModels/Lead.js create mode 100644 backend/src/models/appModels/Offer.js create mode 100644 backend/src/models/appModels/Order.js create mode 100644 backend/src/models/appModels/People.js create mode 100644 backend/src/models/appModels/Product.js create mode 100644 backend/src/models/appModels/ProductCategory.js create mode 100644 backend/src/models/appModels/Purchase.js create mode 100644 backend/src/models/appModels/Shipment.js create mode 100644 backend/src/models/coreModels/Email.js create mode 100644 backend/src/setup/defaultSettings/emailSettings.json create mode 100644 backend/src/setup/defaultSettings/inventorySettings.json create mode 100644 backend/src/setup/defaultSettings/leadSettings.json create mode 100644 backend/src/setup/defaultSettings/offerSettings.json create mode 100644 backend/src/setup/emailTemplate/index.json create mode 100644 backend/src/utils/redis.js create mode 100644 frontend/Dockerfile create mode 100644 frontend/src/apps/Navigation/AppNav.jsx create mode 100644 frontend/src/apps/Navigation/ExpensesNav.jsx create mode 100644 frontend/src/components/ChooseCurrency/index.jsx create mode 100644 frontend/src/components/PaypalButton/Subscription.jsx create mode 100644 frontend/src/modules/AdvancedCrudModule/CreateItem.jsx create mode 100644 frontend/src/modules/AdvancedCrudModule/DataTable.jsx create mode 100644 frontend/src/modules/AdvancedCrudModule/DeleteItem.jsx create mode 100644 frontend/src/modules/AdvancedCrudModule/ItemRow.jsx create mode 100644 frontend/src/modules/AdvancedCrudModule/ReadItem.jsx create mode 100644 frontend/src/modules/AdvancedCrudModule/SearchItem.jsx create mode 100644 frontend/src/modules/AdvancedCrudModule/UpdateItem.jsx create mode 100644 frontend/src/modules/AdvancedCrudModule/index.jsx create mode 100644 frontend/src/modules/EmailModule/EmailDataTableModule/index.jsx create mode 100644 frontend/src/modules/EmailModule/ReadEmailModule/components/ReadItem.jsx create mode 100644 frontend/src/modules/EmailModule/ReadEmailModule/index.jsx create mode 100644 frontend/src/modules/EmailModule/UpdateEmailModule/componenets/EmailForm.jsx create mode 100644 frontend/src/modules/EmailModule/UpdateEmailModule/index.jsx create mode 100644 frontend/src/modules/OfferModule/CreateOfferModule/index.jsx create mode 100644 frontend/src/modules/OfferModule/Forms/OfferForm.jsx create mode 100644 frontend/src/modules/OfferModule/OfferDataTableModule/index.jsx create mode 100644 frontend/src/modules/OfferModule/ReadOfferModule/ReadOfferItem.jsx create mode 100644 frontend/src/modules/OfferModule/ReadOfferModule/index.jsx create mode 100644 frontend/src/modules/OfferModule/UpdateOfferModule/index.jsx create mode 100644 frontend/src/modules/OrderModule/CreateOrderModule/index.jsx create mode 100644 frontend/src/modules/OrderModule/Forms/InvoiceForm.jsx create mode 100644 frontend/src/modules/OrderModule/OrderDataTableModule/index.jsx create mode 100644 frontend/src/modules/OrderModule/ReadOrderModule/index.jsx create mode 100644 frontend/src/modules/OrderModule/RecordPaymentModule/components/Payment.jsx create mode 100644 frontend/src/modules/OrderModule/RecordPaymentModule/components/RecordPayment.jsx create mode 100644 frontend/src/modules/OrderModule/RecordPaymentModule/index.jsx create mode 100644 frontend/src/modules/OrderModule/UpdateInvoiceModule/index.jsx create mode 100644 frontend/src/pages/AdvancedSettings/index.jsx create mode 100644 frontend/src/pages/Company/config.js create mode 100644 frontend/src/pages/Company/index.jsx create mode 100644 frontend/src/pages/Email/EmailRead.jsx create mode 100644 frontend/src/pages/Email/EmailUpdate.jsx create mode 100644 frontend/src/pages/Email/index.jsx create mode 100644 frontend/src/pages/Employee/index.jsx create mode 100644 frontend/src/pages/Expense/config.js create mode 100644 frontend/src/pages/Expense/index.jsx create mode 100644 frontend/src/pages/ExpenseCategory/config.js create mode 100644 frontend/src/pages/ExpenseCategory/index.jsx create mode 100644 frontend/src/pages/Inventory/index.jsx create mode 100644 frontend/src/pages/Lead/config.js create mode 100644 frontend/src/pages/Lead/index.jsx create mode 100644 frontend/src/pages/Offer/OfferCreate.jsx create mode 100644 frontend/src/pages/Offer/OfferRead.jsx create mode 100644 frontend/src/pages/Offer/OfferUpdate.jsx create mode 100644 frontend/src/pages/Offer/index.jsx create mode 100644 frontend/src/pages/Order/index.jsx create mode 100644 frontend/src/pages/People/config.js create mode 100644 frontend/src/pages/People/index.jsx create mode 100644 frontend/src/pages/Product/config.js create mode 100644 frontend/src/pages/Product/index.jsx create mode 100644 frontend/src/pages/ProductCategory/config.js create mode 100644 frontend/src/pages/ProductCategory/index.jsx create mode 100644 frontend/src/pages/Register.jsx create mode 100644 frontend/src/redux/translate/actions.js create mode 100644 frontend/src/redux/translate/index.js create mode 100644 frontend/src/redux/translate/reducer.js create mode 100644 frontend/src/redux/translate/selectors.js create mode 100644 frontend/src/redux/translate/types.js diff --git a/backend/.env b/backend/.env index 360866b0c..ac2730e05 100644 --- a/backend/.env +++ b/backend/.env @@ -1,5 +1,5 @@ -#DATABASE = "mongodb://localhost:27017" -#RESEND_API = "your resend_api" +DATABASE = "mongodb+srv://192105adityashah:12QX651CYMCrGmT6@cluster0.akacd.mongodb.net/testdel?retryWrites=true&w=majority&appName=Cluster0" +RESEND_API = "re_ZB3uVo4u_DUGNmBHzp22NQXG8GUhE2jA7" #OPENAI_API_KEY = "your open_ai api key" JWT_SECRET= "your_private_jwt_secret_key" NODE_ENV = "production" diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 000000000..69b7179bd --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,15 @@ +FROM node:20.9.0-alpine + +WORKDIR /usr/src/app + +RUN npm install -g npm@10.2.4 + +COPY package*.json ./ + +COPY . . + +RUN npm install + +EXPOSE 8888 + +CMD ["npm", "run", "dev"] \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index 3635c8039..cb2c024df 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -18,7 +18,7 @@ "dotenv": "16.3.1", "express": "^4.18.2", "express-fileupload": "^1.4.3", - "express-rate-limit": "^7.1.5", + "express-rate-limit": "^7.4.1", "glob": "10.3.10", "html-pdf": "^3.0.1", "joi": "^17.11.0", @@ -32,6 +32,9 @@ "node-cache": "^5.1.2", "openai": "^4.27.0", "pug": "^3.0.2", + "rate-limit-redis": "^4.2.0", + "redis": "^4.7.0", + "request-ip": "^3.3.0", "resend": "^2.0.0", "shortid": "^2.2.16", "transliteration": "^2.3.5" @@ -948,6 +951,59 @@ "node": ">=18.0.0" } }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", + "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@selderee/plugin-htmlparser2": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", @@ -2112,6 +2168,14 @@ "node": ">=0.8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2706,9 +2770,9 @@ } }, "node_modules/express-rate-limit": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.1.5.tgz", - "integrity": "sha512-/iVogxu7ueadrepw1bS0X0kaRC/U0afwiYRSLg68Ts+p4Dc85Q5QKsOnPS/QUjPMHvOJQtBDrZgvkOzf8ejUYw==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz", + "integrity": "sha512-KS3efpnpIDVIXopMc65EMbWbUht7qvTCdtCR2dD/IZmi9MIkopYESwyRqLgv8Pfu589+KqDqOdzJWW7AHoACeg==", "engines": { "node": ">= 16" }, @@ -2958,6 +3022,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "engines": { + "node": ">= 4" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -4526,6 +4598,17 @@ "node": ">= 0.6" } }, + "node_modules/rate-limit-redis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/rate-limit-redis/-/rate-limit-redis-4.2.0.tgz", + "integrity": "sha512-wV450NQyKC24NmPosJb2131RoczLdfIJdKCReNwtVpm5998U8SgKrAZrIHaN/NfQgqOHaan8Uq++B4sa5REwjA==", + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "express-rate-limit": ">= 6" + } + }, "node_modules/raw-body": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", @@ -4594,6 +4677,19 @@ "node": ">=8.10.0" } }, + "node_modules/redis": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz", + "integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==", + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.6.0", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" + } + }, "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -4626,6 +4722,11 @@ "node": ">= 6" } }, + "node_modules/request-ip": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/request-ip/-/request-ip-3.3.0.tgz", + "integrity": "sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA==" + }, "node_modules/request-progress": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index ee2b151ea..0646e49c8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "idurar-erp-crm", - "version": "4.1.0", + "version": "4.0.0", "engines": { "npm": "10.2.4", "node": "20.9.0" @@ -23,7 +23,7 @@ "dotenv": "16.3.1", "express": "^4.18.2", "express-fileupload": "^1.4.3", - "express-rate-limit": "^7.1.5", + "express-rate-limit": "^7.4.1", "glob": "10.3.10", "html-pdf": "^3.0.1", "joi": "^17.11.0", @@ -37,6 +37,9 @@ "node-cache": "^5.1.2", "openai": "^4.27.0", "pug": "^3.0.2", + "rate-limit-redis": "^4.2.0", + "redis": "^4.7.0", + "request-ip": "^3.3.0", "resend": "^2.0.0", "shortid": "^2.2.16", "transliteration": "^2.3.5" diff --git a/backend/src/controllers/appControllers/clientController/create.js b/backend/src/controllers/appControllers/clientController/create.js new file mode 100644 index 000000000..fdb96dab3 --- /dev/null +++ b/backend/src/controllers/appControllers/clientController/create.js @@ -0,0 +1,91 @@ +const mongoose = require('mongoose'); + +const People = mongoose.model('People'); +const Company = mongoose.model('Company'); + +const create = async (Model, req, res) => { + // Creating a new document in the collection + + if (req.body.type === 'people') { + if (!req.body.people) { + return res.status(403).json({ + success: false, + message: 'Please select a people', + }); + } else { + let client = await Model.findOne({ + people: req.body.people, + removed: false, + }); + + if (client) { + return res.status(403).json({ + success: false, + result: null, + message: 'Client Already Exist', + }); + } + + let { firstname, lastname } = await People.findOneAndUpdate( + { + _id: req.body.people, + removed: false, + }, + { isClient: true }, + { + new: true, // return the new result instead of the old one + runValidators: true, + } + ).exec(); + req.body.name = firstname + ' ' + lastname; + req.body.company = undefined; + } + } else { + if (!req.body.company) { + return res.status(403).json({ + success: false, + message: 'Please select a company', + }); + } else { + let client = await Model.findOne({ + company: req.body.company, + removed: false, + }); + + if (client) { + return res.status(403).json({ + success: false, + result: null, + message: 'Client Already Exist', + }); + } + let { name } = await Company.findOneAndUpdate( + { + _id: req.body.company, + removed: false, + }, + { isClient: true }, + { + new: true, // return the new result instead of the old one + runValidators: true, + } + ).exec(); + req.body.name = name; + req.body.people = undefined; + } + } + + req.body.removed = false; + const result = await new Model({ + ...req.body, + }).save(); + + // Returning successfull response + return res.status(200).json({ + success: true, + result, + message: 'Successfully Created the document in Model ', + }); +}; + +module.exports = create; diff --git a/backend/src/controllers/appControllers/clientController/index.js b/backend/src/controllers/appControllers/clientController/index.js index 6411c1951..668727b75 100644 --- a/backend/src/controllers/appControllers/clientController/index.js +++ b/backend/src/controllers/appControllers/clientController/index.js @@ -1,13 +1,28 @@ const mongoose = require('mongoose'); const createCRUDController = require('@/controllers/middlewaresControllers/createCRUDController'); - +const remove = require('./remove'); const summary = require('./summary'); +const create = require('./create'); +const read = require('./read'); +const search = require('./search'); +const update = require('./update'); + +const listAll = require('./listAll'); +const paginatedList = require('./paginatedList'); + function modelController() { const Model = mongoose.model('Client'); const methods = createCRUDController('Client'); + methods.read = (req, res) => read(Model, req, res); + methods.delete = (req, res) => remove(Model, req, res); + methods.list = (req, res) => paginatedList(Model, req, res); methods.summary = (req, res) => summary(Model, req, res); + methods.create = (req, res) => create(Model, req, res); + methods.update = (req, res) => update(Model, req, res); + methods.search = (req, res) => search(Model, req, res); + methods.listAll = (req, res) => listAll(Model, req, res); return methods; } diff --git a/backend/src/controllers/appControllers/clientController/listAll.js b/backend/src/controllers/appControllers/clientController/listAll.js new file mode 100644 index 000000000..82a836d44 --- /dev/null +++ b/backend/src/controllers/appControllers/clientController/listAll.js @@ -0,0 +1,30 @@ +const { migrate } = require('./migrate'); + +const listAll = async (Model, req, res) => { + const sort = parseInt(req.query.sort) || 'desc'; + + // Query the database for a list of all results + const result = await Model.find({ + removed: false, + }) + .sort({ created: sort }) + .populate() + .exec(); + + const migratedData = result.map((x) => migrate(x)); + if (result.length > 0) { + return res.status(200).json({ + success: true, + result: migratedData, + message: 'Successfully found all documents', + }); + } else { + return res.status(203).json({ + success: true, + result: [], + message: 'Collection is Empty', + }); + } +}; + +module.exports = listAll; diff --git a/backend/src/controllers/appControllers/clientController/migrate.js b/backend/src/controllers/appControllers/clientController/migrate.js new file mode 100644 index 000000000..cc6659d9a --- /dev/null +++ b/backend/src/controllers/appControllers/clientController/migrate.js @@ -0,0 +1,16 @@ +exports.migrate = (result) => { + const client = result.type === 'people' ? result.people : result.company; + let newData = {}; + newData._id = result._id; + newData.type = result.type; + newData.name = result.name; + newData.phone = client.phone; + newData.email = client.email; + newData.website = client.website; + newData.country = client.country; + newData.address = client.address; + newData.people = result.people; + newData.company = result.company; + newData.notes = result.notes; + return newData; +}; diff --git a/backend/src/controllers/appControllers/clientController/paginatedList.js b/backend/src/controllers/appControllers/clientController/paginatedList.js new file mode 100644 index 000000000..9e121a66a --- /dev/null +++ b/backend/src/controllers/appControllers/clientController/paginatedList.js @@ -0,0 +1,67 @@ +const { migrate } = require('./migrate'); + +const paginatedList = async (Model, req, res) => { + const page = req.query.page || 1; + + const limit = parseInt(req.query.items) || 10; + const skip = page * limit - limit; + + const { sortBy = 'enabled', sortValue = -1, filter, equal } = req.query; + + const fieldsArray = req.query.fields ? req.query.fields.split(',') : []; + + let fields; + + fields = fieldsArray.length === 0 ? {} : { $or: [] }; + + for (const field of fieldsArray) { + fields.$or.push({ [field]: { $regex: new RegExp(req.query.q, 'i') } }); + } + + // Query the database for a list of all results + const resultsPromise = Model.find({ + removed: false, + [filter]: equal, + ...fields, + }) + .skip(skip) + .limit(limit) + .sort({ [sortBy]: sortValue }) + .populate() + .exec(); + + // Counting the total documents + const countPromise = Model.countDocuments({ + removed: false, + + [filter]: equal, + ...fields, + }); + // Resolving both promises + const [result, count] = await Promise.all([resultsPromise, countPromise]); + // console.log('๐Ÿš€ ~ file: paginatedList.js:23 ~ paginatedList ~ result:', result); + + // Calculating total pages + const pages = Math.ceil(count / limit); + + const pagination = { page, pages, count }; + if (count > 0) { + const migratedData = result.map((x) => migrate(x)); + // console.log('๐Ÿš€ ~ file: paginatedList.js:23 ~ paginatedList ~ migratedData:', migratedData); + return res.status(200).json({ + success: true, + result: migratedData, + pagination, + message: 'Successfully found all documents', + }); + } else { + return res.status(203).json({ + success: true, + result: [], + pagination, + message: 'Collection is Empty', + }); + } +}; + +module.exports = paginatedList; diff --git a/backend/src/controllers/appControllers/clientController/read.js b/backend/src/controllers/appControllers/clientController/read.js new file mode 100644 index 000000000..9f6c27db0 --- /dev/null +++ b/backend/src/controllers/appControllers/clientController/read.js @@ -0,0 +1,29 @@ +const { migrate } = require('./migrate'); + +const read = async (Model, req, res) => { + // Find document by id + let result = await Model.findOne({ + _id: req.params.id, + removed: false, + }).exec(); + // If no results found, return document not found + if (!result) { + return res.status(404).json({ + success: false, + result: null, + message: 'No document found ', + }); + } else { + // Return success resposne + + const migratedData = migrate(result); + + return res.status(200).json({ + success: true, + result: migratedData, + message: 'we found this document ', + }); + } +}; + +module.exports = read; diff --git a/backend/src/controllers/appControllers/clientController/remove.js b/backend/src/controllers/appControllers/clientController/remove.js new file mode 100644 index 000000000..64f301384 --- /dev/null +++ b/backend/src/controllers/appControllers/clientController/remove.js @@ -0,0 +1,84 @@ +const mongoose = require('mongoose'); + +const QuoteModel = mongoose.model('Quote'); +const InvoiceModel = mongoose.model('Invoice'); +const People = mongoose.model('People'); +const Company = mongoose.model('Company'); + +const remove = async (Model, req, res) => { + // cannot delete client it it have one invoice or quotes: + // check if client have invoice or quotes: + const { id } = req.params; + + // first find if there alt least one quote or invoice exist corresponding to the client + const resultQuotes = QuoteModel.findOne({ + client: id, + removed: false, + }).exec(); + const resultInvoice = InvoiceModel.findOne({ + client: id, + removed: false, + }).exec(); + + const [quotes, invoice] = await Promise.allSettled([resultQuotes, resultInvoice]); + if (quotes?.value) { + return res.status(400).json({ + success: false, + result: null, + message: 'Cannot delete client if client have any quote or invoice', + }); + } + if (invoice?.value) { + return res.status(400).json({ + success: false, + result: null, + message: 'Cannot delete client if client have any quote or invoice', + }); + } + + let result = await Model.findOneAndDelete({ + _id: id, + removed: false, + }).exec(); + + if (!result) { + return res.status(404).json({ + success: false, + result: null, + message: 'No client found by this id: ' + id, + }); + } + + if (result.type === 'people') { + await People.findOneAndUpdate( + { + _id: result.people._id, + removed: false, + }, + { isClient: false }, + { + new: true, // return the new result instead of the old one + runValidators: true, + } + ).exec(); + } else { + await Company.findOneAndUpdate( + { + _id: result.company._id, + removed: false, + }, + { isClient: false }, + { + new: true, // return the new result instead of the old one + runValidators: true, + } + ).exec(); + } + + return res.status(200).json({ + success: true, + result: null, + message: 'Successfully Deleted the client by id: ' + id, + }); +}; +module.exports = remove; diff --git a/backend/src/controllers/appControllers/clientController/search.js b/backend/src/controllers/appControllers/clientController/search.js new file mode 100644 index 000000000..343d09586 --- /dev/null +++ b/backend/src/controllers/appControllers/clientController/search.js @@ -0,0 +1,51 @@ +const { migrate } = require('./migrate'); + +const search = async (Model, req, res) => { + // console.log(req.query.fields) + // if (req.query.q === undefined || req.query.q.trim() === '') { + // return res + // .status(202) + // .json({ + // success: false, + // result: [], + // message: 'No document found by this request', + // }) + // .end(); + // } + const fieldsArray = req.query.fields ? req.query.fields.split(',') : ['name']; + + const fields = { $or: [] }; + + for (const field of fieldsArray) { + fields.$or.push({ [field]: { $regex: new RegExp(req.query.q, 'i') } }); + } + // console.log(fields) + + let results = await Model.find({ + ...fields, + }) + .where('removed', false) + .limit(20) + .exec(); + + const migratedData = results.map((x) => migrate(x)); + + if (results.length >= 1) { + return res.status(200).json({ + success: true, + result: migratedData, + message: 'Successfully found all documents', + }); + } else { + return res + .status(202) + .json({ + success: false, + result: [], + message: 'No document found by this request', + }) + .end(); + } +}; + +module.exports = search; diff --git a/backend/src/controllers/appControllers/clientController/update.js b/backend/src/controllers/appControllers/clientController/update.js new file mode 100644 index 000000000..3068e8a95 --- /dev/null +++ b/backend/src/controllers/appControllers/clientController/update.js @@ -0,0 +1,12 @@ +const mongoose = require('mongoose'); + +const update = async (Model, req, res) => { + // Find document by id and updates with the required fields + return res.status(200).json({ + success: false, + result: null, + message: 'You cant update client once is created', + }); +}; + +module.exports = update; diff --git a/backend/src/controllers/appControllers/companyController/index.js b/backend/src/controllers/appControllers/companyController/index.js new file mode 100644 index 000000000..3b3308320 --- /dev/null +++ b/backend/src/controllers/appControllers/companyController/index.js @@ -0,0 +1,24 @@ +const mongoose = require('mongoose'); +const { modelsFiles } = require('@/models/utils'); +const createCRUDController = require('@/controllers/middlewaresControllers/createCRUDController'); + +const remove = require('./remove'); +const update = require('./update'); + +function modelController() { + const modelName = 'Company'; + + if (!modelsFiles.includes(modelName)) { + throw new Error(`Model ${modelName} does not exist`); + } else { + const Model = mongoose.model(modelName); + const methods = createCRUDController(modelName); + + methods.delete = (req, res) => remove(Model, req, res); + methods.update = (req, res) => update(Model, req, res); + + return methods; + } +} + +module.exports = modelController(); diff --git a/backend/src/controllers/appControllers/companyController/remove.js b/backend/src/controllers/appControllers/companyController/remove.js new file mode 100644 index 000000000..f3257244e --- /dev/null +++ b/backend/src/controllers/appControllers/companyController/remove.js @@ -0,0 +1,57 @@ +const mongoose = require('mongoose'); + +const Client = mongoose.model('Client'); +const People = mongoose.model('People'); + +const remove = async (Model, req, res) => { + // cannot delete client it it have one invoice or Client: + // check if client have invoice or quotes: + const { id } = req.params; + + // first find if there alt least one quote or invoice exist corresponding to the client + const client = await Client.findOne({ + company: id, + removed: false, + }).exec(); + if (client) { + return res.status(400).json({ + success: false, + result: null, + message: 'Cannot delete company if company attached to any people or she is client', + }); + } + const people = await People.findOne({ + company: id, + removed: false, + }).exec(); + if (people) { + return res.status(400).json({ + success: false, + result: null, + message: 'Cannot delete company if company attached to any people or she is client', + }); + } + + // if no People or quote, delete the client + const result = await Model.findOneAndUpdate( + { _id: id, removed: false }, + { + $set: { + removed: true, + }, + } + ).exec(); + if (!result) { + return res.status(404).json({ + success: false, + result: null, + message: 'No people found by this id: ' + id, + }); + } + return res.status(200).json({ + success: true, + result, + message: 'Successfully Deleted the people by id: ' + id, + }); +}; +module.exports = remove; diff --git a/backend/src/controllers/appControllers/companyController/update.js b/backend/src/controllers/appControllers/companyController/update.js new file mode 100644 index 000000000..7dc04138d --- /dev/null +++ b/backend/src/controllers/appControllers/companyController/update.js @@ -0,0 +1,44 @@ +const mongoose = require('mongoose'); +const Client = mongoose.model('Client'); +const Lead = mongoose.model('People'); + +const update = async (Model, req, res) => { + // Find document by id and updates with the required fields + req.body.removed = false; + const result = await Model.findOneAndUpdate({ _id: req.params.id, removed: false }, req.body, { + new: true, // return the new result instead of the old one + runValidators: true, + }).exec(); + + if (!result) { + return res.status(404).json({ + success: false, + result: null, + message: 'No document found ', + }); + } else { + await Client.findOneAndUpdate( + { company: result._id }, + { name: result.name }, + { + new: true, // return the new result instead of the old one + } + ).exec(); + + await Lead.findOneAndUpdate( + { company: result._id }, + { name: result.name }, + { + new: true, // return the new result instead of the old one + } + ).exec(); + + return res.status(200).json({ + success: true, + result, + message: 'we update this document ', + }); + } +}; + +module.exports = update; diff --git a/backend/src/controllers/appControllers/invoiceController/remove.js b/backend/src/controllers/appControllers/invoiceController/remove.js index 94bc7bfa6..f4798289d 100644 --- a/backend/src/controllers/appControllers/invoiceController/remove.js +++ b/backend/src/controllers/appControllers/invoiceController/remove.js @@ -1,36 +1,13 @@ const mongoose = require('mongoose'); const Model = mongoose.model('Invoice'); -const ModelPayment = mongoose.model('Payment'); +const ModalPayment = mongoose.model('Payment'); const remove = async (req, res) => { - const deletedInvoice = await Model.findOneAndUpdate( - { - _id: req.params.id, - removed: false, - }, - { - $set: { - removed: true, - }, - } - ).exec(); - - if (!deletedInvoice) { - return res.status(404).json({ - success: false, - result: null, - message: 'Invoice not found', - }); - } - const paymentsInvoices = await ModelPayment.updateMany( - { invoice: deletedInvoice._id }, - { $set: { removed: true } } - ); return res.status(200).json({ success: true, - result: deletedInvoice, - message: 'Invoice deleted successfully', + result: null, + message: 'Please Upgrade to Premium Version to have full features', }); }; diff --git a/backend/src/controllers/appControllers/invoiceController/update.js b/backend/src/controllers/appControllers/invoiceController/update.js index 64bcbaa51..8fe7f9b31 100644 --- a/backend/src/controllers/appControllers/invoiceController/update.js +++ b/backend/src/controllers/appControllers/invoiceController/update.js @@ -8,75 +8,10 @@ const { calculate } = require('@/helpers'); const schema = require('./schemaValidate'); const update = async (req, res) => { - let body = req.body; - - const { error, value } = schema.validate(body); - if (error) { - const { details } = error; - return res.status(400).json({ - success: false, - result: null, - message: details[0]?.message, - }); - } - - const previousInvoice = await Model.findOne({ - _id: req.params.id, - removed: false, - }); - - const { credit } = previousInvoice; - - const { items = [], taxRate = 0, discount = 0 } = req.body; - - if (items.length === 0) { - return res.status(400).json({ - success: false, - result: null, - message: 'Items cannot be empty', - }); - } - - // default - let subTotal = 0; - let taxTotal = 0; - let total = 0; - - //Calculate the items array with subTotal, total, taxTotal - items.map((item) => { - let total = calculate.multiply(item['quantity'], item['price']); - //sub total - subTotal = calculate.add(subTotal, total); - //item total - item['total'] = total; - }); - taxTotal = calculate.multiply(subTotal, taxRate / 100); - total = calculate.add(subTotal, taxTotal); - - body['subTotal'] = subTotal; - body['taxTotal'] = taxTotal; - body['total'] = total; - body['items'] = items; - body['pdf'] = 'invoice-' + req.params.id + '.pdf'; - if (body.hasOwnProperty('currency')) { - delete body.currency; - } - // Find document by id and updates with the required fields - - let paymentStatus = - calculate.sub(total, discount) === credit ? 'paid' : credit > 0 ? 'partially' : 'unpaid'; - body['paymentStatus'] = paymentStatus; - - const result = await Model.findOneAndUpdate({ _id: req.params.id, removed: false }, body, { - new: true, // return the new result instead of the old one - }).exec(); - - // Returning successfull response - return res.status(200).json({ success: true, - result, - message: 'we update this document ', + result: null, + message: 'Please Upgrade to Premium Version to have full features', }); }; diff --git a/backend/src/controllers/appControllers/leadController/create.js b/backend/src/controllers/appControllers/leadController/create.js new file mode 100644 index 000000000..2b70d7ab0 --- /dev/null +++ b/backend/src/controllers/appControllers/leadController/create.js @@ -0,0 +1,51 @@ +const mongoose = require('mongoose'); +const People = mongoose.model('People'); +const Company = mongoose.model('Company'); + +const create = async (Model, req, res) => { + // Creating a new document in the collection + + if (req.body.type === 'people') { + if (!req.body.people) { + return res.status(403).json({ + success: false, + message: 'Please select a people', + }); + } else { + let { firstname, lastname } = await People.findOne({ + _id: req.body.people, + removed: false, + }).exec(); + req.body.name = firstname + ' ' + lastname; + req.body.company = null; + } + } else { + if (!req.body.company) { + return res.status(403).json({ + success: false, + message: 'Please select a company', + }); + } else { + let { name } = await Company.findOne({ + _id: req.body.company, + removed: false, + }).exec(); + req.body.name = name; + req.body.people = null; + } + } + + req.body.removed = false; + const result = await new Model({ + ...req.body, + }).save(); + + // Returning successfull response + return res.status(200).json({ + success: true, + result, + message: 'Successfully Created the document in Model ', + }); +}; + +module.exports = create; diff --git a/backend/src/controllers/appControllers/leadController/index.js b/backend/src/controllers/appControllers/leadController/index.js new file mode 100644 index 000000000..59f036bc8 --- /dev/null +++ b/backend/src/controllers/appControllers/leadController/index.js @@ -0,0 +1,29 @@ +const mongoose = require('mongoose'); +const createCRUDController = require('@/controllers/middlewaresControllers/createCRUDController'); +const remove = require('./remove'); +const summary = require('./summary'); + +const create = require('./create'); +const update = require('./update'); +const read = require('./read'); +const search = require('./search'); + +const listAll = require('./listAll'); +const paginatedList = require('./paginatedList'); + +function modelController() { + const modelName = 'Lead'; + const Model = mongoose.model(modelName); + const methods = createCRUDController(modelName); + methods.read = (req, res) => read(Model, req, res); + methods.delete = (req, res) => remove(Model, req, res); + methods.list = (req, res) => paginatedList(Model, req, res); + methods.summary = (req, res) => summary(Model, req, res); + methods.create = (req, res) => create(Model, req, res); + methods.update = (req, res) => update(Model, req, res); + methods.search = (req, res) => search(Model, req, res); + methods.listAll = (req, res) => listAll(Model, req, res); + return methods; +} + +module.exports = modelController(); diff --git a/backend/src/controllers/appControllers/leadController/listAll.js b/backend/src/controllers/appControllers/leadController/listAll.js new file mode 100644 index 000000000..82a836d44 --- /dev/null +++ b/backend/src/controllers/appControllers/leadController/listAll.js @@ -0,0 +1,30 @@ +const { migrate } = require('./migrate'); + +const listAll = async (Model, req, res) => { + const sort = parseInt(req.query.sort) || 'desc'; + + // Query the database for a list of all results + const result = await Model.find({ + removed: false, + }) + .sort({ created: sort }) + .populate() + .exec(); + + const migratedData = result.map((x) => migrate(x)); + if (result.length > 0) { + return res.status(200).json({ + success: true, + result: migratedData, + message: 'Successfully found all documents', + }); + } else { + return res.status(203).json({ + success: true, + result: [], + message: 'Collection is Empty', + }); + } +}; + +module.exports = listAll; diff --git a/backend/src/controllers/appControllers/leadController/migrate.js b/backend/src/controllers/appControllers/leadController/migrate.js new file mode 100644 index 000000000..69fb308f6 --- /dev/null +++ b/backend/src/controllers/appControllers/leadController/migrate.js @@ -0,0 +1,18 @@ +exports.migrate = (result) => { + let lead = result.type === 'people' ? result.people : result.company; + let newData = {}; + newData._id = result._id; + newData.type = result.type; + newData.status = result.status; + newData.source = result.source; + newData.name = result.name; + newData.phone = lead.phone; + newData.email = lead.email; + newData.website = lead.website; + newData.country = lead.country; + newData.address = lead.address; + newData.people = result.people; + newData.company = result.company; + newData.notes = result.notes; + return newData; +}; diff --git a/backend/src/controllers/appControllers/leadController/paginatedList.js b/backend/src/controllers/appControllers/leadController/paginatedList.js new file mode 100644 index 000000000..dc39e3001 --- /dev/null +++ b/backend/src/controllers/appControllers/leadController/paginatedList.js @@ -0,0 +1,68 @@ +const { migrate } = require('./migrate'); + +const paginatedList = async (Model, req, res) => { + const page = req.query.page || 1; + + const limit = parseInt(req.query.items) || 10; + const skip = page * limit - limit; + + const { sortBy = 'enabled', sortValue = -1, filter, equal } = req.query; + + const fieldsArray = req.query.fields ? req.query.fields.split(',') : []; + + let fields; + + fields = fieldsArray.length === 0 ? {} : { $or: [] }; + + for (const field of fieldsArray) { + fields.$or.push({ [field]: { $regex: new RegExp(req.query.q, 'i') } }); + } + + // Query the database for a list of all results + const resultsPromise = Model.find({ + removed: false, + + [filter]: equal, + ...fields, + }) + .skip(skip) + .limit(limit) + .sort({ [sortBy]: sortValue }) + .populate() + .exec(); + + // Counting the total documents + const countPromise = Model.countDocuments({ + removed: false, + + [filter]: equal, + ...fields, + }); + // Resolving both promises + const [result, count] = await Promise.all([resultsPromise, countPromise]); + // console.log('๐Ÿš€ ~ file: paginatedList.js:23 ~ paginatedList ~ result:', result); + + // Calculating total pages + const pages = Math.ceil(count / limit); + + const pagination = { page, pages, count }; + if (count > 0) { + const migratedData = result.map((x) => migrate(x)); + // console.log('๐Ÿš€ ~ file: paginatedList.js:23 ~ paginatedList ~ migratedData:', migratedData); + return res.status(200).json({ + success: true, + result: migratedData, + pagination, + message: 'Successfully found all documents', + }); + } else { + return res.status(203).json({ + success: true, + result: [], + pagination, + message: 'Collection is Empty', + }); + } +}; + +module.exports = paginatedList; diff --git a/backend/src/controllers/appControllers/leadController/read.js b/backend/src/controllers/appControllers/leadController/read.js new file mode 100644 index 000000000..9f6c27db0 --- /dev/null +++ b/backend/src/controllers/appControllers/leadController/read.js @@ -0,0 +1,29 @@ +const { migrate } = require('./migrate'); + +const read = async (Model, req, res) => { + // Find document by id + let result = await Model.findOne({ + _id: req.params.id, + removed: false, + }).exec(); + // If no results found, return document not found + if (!result) { + return res.status(404).json({ + success: false, + result: null, + message: 'No document found ', + }); + } else { + // Return success resposne + + const migratedData = migrate(result); + + return res.status(200).json({ + success: true, + result: migratedData, + message: 'we found this document ', + }); + } +}; + +module.exports = read; diff --git a/backend/src/controllers/appControllers/leadController/remove.js b/backend/src/controllers/appControllers/leadController/remove.js new file mode 100644 index 000000000..00021092d --- /dev/null +++ b/backend/src/controllers/appControllers/leadController/remove.js @@ -0,0 +1,10 @@ +const mongoose = require('mongoose'); + +const remove = async (Model, req, res) => { + return res.status(200).json({ + success: true, + result: null, + message: 'Please Upgrade to Premium Version to have full features', + }); +}; +module.exports = remove; diff --git a/backend/src/controllers/appControllers/leadController/search.js b/backend/src/controllers/appControllers/leadController/search.js new file mode 100644 index 000000000..e5ab7f765 --- /dev/null +++ b/backend/src/controllers/appControllers/leadController/search.js @@ -0,0 +1,50 @@ +const { migrate } = require('./migrate'); + +const search = async (Model, req, res) => { + // if (req.query.q === undefined || req.query.q.trim() === '') { + // return res + // .status(202) + // .json({ + // success: false, + // result: [], + // message: 'No document found by this request', + // }) + // .end(); + // } + const fieldsArray = req.query.fields ? req.query.fields.split(',') : ['name']; + + const fields = { $or: [] }; + + for (const field of fieldsArray) { + fields.$or.push({ [field]: { $regex: new RegExp(req.query.q, 'i') } }); + } + // console.log(fields) + + let results = await Model.find({ + ...fields, + }) + .where('removed', false) + .limit(20) + .exec(); + + const migratedData = results.map((x) => migrate(x)); + + if (results.length >= 1) { + return res.status(200).json({ + success: true, + result: migratedData, + message: 'Successfully found all documents', + }); + } else { + return res + .status(202) + .json({ + success: false, + result: [], + message: 'No document found by this request', + }) + .end(); + } +}; + +module.exports = search; diff --git a/backend/src/controllers/appControllers/leadController/summary.js b/backend/src/controllers/appControllers/leadController/summary.js new file mode 100644 index 000000000..8bc80d95a --- /dev/null +++ b/backend/src/controllers/appControllers/leadController/summary.js @@ -0,0 +1,97 @@ +const mongoose = require('mongoose'); +const moment = require('moment'); + +const OfferModel = mongoose.model('Offer'); + +const summary = async (Model, req, res) => { + let defaultType = 'month'; + const { type } = req.query; + + if (type && ['week', 'month', 'year'].includes(type)) { + defaultType = type; + } else if (type) { + return res.status(400).json({ + success: false, + result: null, + message: 'Invalid type', + }); + } + + const currentDate = moment(); + let startDate = currentDate.clone().startOf(defaultType); + let endDate = currentDate.clone().endOf(defaultType); + + const pipeline = [ + { + $facet: { + totalClients: [ + { + $match: { + removed: false, + enabled: true, + }, + }, + { + $count: 'count', + }, + ], + newClients: [ + { + $match: { + removed: false, + created: { $gte: startDate.toDate(), $lte: endDate.toDate() }, + enabled: true, + }, + }, + { + $count: 'count', + }, + ], + activeClients: [ + { + $lookup: { + from: OfferModel.collection.name, + localField: '_id', // Match _id from ClientModel + foreignField: 'lead', // Match client field in OfferModel + as: 'offer', + }, + }, + { + $match: { + 'offer.removed': false, + }, + }, + { + $group: { + _id: '$_id', + }, + }, + { + $count: 'count', + }, + ], + }, + }, + ]; + + const aggregationResult = await Model.aggregate(pipeline); + + const result = aggregationResult[0]; + const totalClients = result.totalClients[0] ? result.totalClients[0].count : 0; + const totalNewClients = result.newClients[0] ? result.newClients[0].count : 0; + const activeClients = result.activeClients[0] ? result.activeClients[0].count : 0; + + const totalActiveClientsPercentage = totalClients > 0 ? (activeClients / totalClients) * 100 : 0; + const totalNewClientsPercentage = totalClients > 0 ? (totalNewClients / totalClients) * 100 : 0; + + return res.status(200).json({ + success: true, + result: { + new: Math.round(totalNewClientsPercentage), + active: Math.round(totalActiveClientsPercentage), + }, + message: 'Successfully get summary of new clients', + }); +}; + +module.exports = summary; diff --git a/backend/src/controllers/appControllers/leadController/update.js b/backend/src/controllers/appControllers/leadController/update.js new file mode 100644 index 000000000..002d5023b --- /dev/null +++ b/backend/src/controllers/appControllers/leadController/update.js @@ -0,0 +1,13 @@ +const mongoose = require('mongoose'); +const People = mongoose.model('People'); +const Company = mongoose.model('Company'); + +const update = async (Model, req, res) => { + return res.status(200).json({ + success: true, + result: null, + message: 'Please Upgrade to Premium Version to have full features', + }); +}; + +module.exports = update; diff --git a/backend/src/controllers/appControllers/offerController/create.js b/backend/src/controllers/appControllers/offerController/create.js new file mode 100644 index 000000000..99ca822de --- /dev/null +++ b/backend/src/controllers/appControllers/offerController/create.js @@ -0,0 +1,61 @@ +const mongoose = require('mongoose'); + +const Model = mongoose.model('Offer'); + +const custom = require('@/controllers/pdfController'); + +const { calculate } = require('@/helpers'); +const { increaseBySettingKey } = require('@/middlewares/settings'); + +const create = async (req, res) => { + const { items = [], taxRate = 0, discount = 0 } = req.body; + + // default + let subTotal = 0; + let taxTotal = 0; + let total = 0; + // let credit = 0; + + //Calculate the items array with subTotal, total, taxTotal + items.map((item) => { + let total = calculate.multiply(item['quantity'], item['price']); + //sub total + subTotal = calculate.add(subTotal, total); + //item total + item['total'] = total; + }); + taxTotal = calculate.multiply(subTotal, taxRate / 100); + total = calculate.add(subTotal, taxTotal); + + let body = req.body; + + body['subTotal'] = subTotal; + body['taxTotal'] = taxTotal; + body['total'] = total; + body['items'] = items; + body['createdBy'] = req.admin._id; + + // Creating a new document in the collection + const result = await new Model(body).save(); + const fileId = 'offer-' + result._id + '.pdf'; + const updateResult = await Model.findOneAndUpdate( + { _id: result._id }, + { pdf: fileId }, + { + new: true, + } + ).exec(); + // Returning successfull response + + increaseBySettingKey({ + settingKey: 'last_offer_number', + }); + + // Returning successfull response + return res.status(200).json({ + success: true, + result: updateResult, + message: 'Offer created successfully', + }); +}; +module.exports = create; diff --git a/backend/src/controllers/appControllers/offerController/index.js b/backend/src/controllers/appControllers/offerController/index.js new file mode 100644 index 000000000..a9f7c2dd1 --- /dev/null +++ b/backend/src/controllers/appControllers/offerController/index.js @@ -0,0 +1,18 @@ +const createCRUDController = require('@/controllers/middlewaresControllers/createCRUDController'); +const methods = createCRUDController('Offer'); + +const create = require('./create'); +const summary = require('./summary'); +const update = require('./update'); +const paginatedList = require('./paginatedList'); +const read = require('./read'); +const sendMail = require('./sendMail'); + +methods.list = paginatedList; +methods.read = read; +methods.mail = sendMail; +methods.create = create; +methods.update = update; +methods.summary = summary; + +module.exports = methods; diff --git a/backend/src/controllers/appControllers/offerController/paginatedList.js b/backend/src/controllers/appControllers/offerController/paginatedList.js new file mode 100644 index 000000000..3fcae7c5a --- /dev/null +++ b/backend/src/controllers/appControllers/offerController/paginatedList.js @@ -0,0 +1,68 @@ +const mongoose = require('mongoose'); + +const Model = mongoose.model('Offer'); + +const paginatedList = async (req, res) => { + const page = req.query.page || 1; + const limit = parseInt(req.query.items) || 10; + const skip = page * limit - limit; + + // Query the database for a list of all results + const { sortBy = 'enabled', sortValue = -1, filter, equal } = req.query; + + const fieldsArray = req.query.fields ? req.query.fields.split(',') : []; + + let fields; + + fields = fieldsArray.length === 0 ? {} : { $or: [] }; + + for (const field of fieldsArray) { + fields.$or.push({ [field]: { $regex: new RegExp(req.query.q, 'i') } }); + } + + // Query the database for a list of all results + const resultsPromise = Model.find({ + removed: false, + + [filter]: equal, + ...fields, + }) + .skip(skip) + .limit(limit) + .sort({ [sortBy]: sortValue }) + .populate('createdBy', 'name') + .exec(); + + // Counting the total documents + const countPromise = Model.countDocuments({ + removed: false, + + [filter]: equal, + ...fields, + }); + + // Resolving both promises + const [result, count] = await Promise.all([resultsPromise, countPromise]); + // Calculating total pages + const pages = Math.ceil(count / limit); + + // Getting Pagination Object + const pagination = { page, pages, count }; + if (count > 0) { + return res.status(200).json({ + success: true, + result, + pagination, + message: 'Successfully found all documents', + }); + } else { + return res.status(203).json({ + success: true, + result: [], + pagination, + message: 'Collection is Empty', + }); + } +}; + +module.exports = paginatedList; diff --git a/backend/src/controllers/appControllers/offerController/read.js b/backend/src/controllers/appControllers/offerController/read.js new file mode 100644 index 000000000..c74e7dd8a --- /dev/null +++ b/backend/src/controllers/appControllers/offerController/read.js @@ -0,0 +1,30 @@ +const mongoose = require('mongoose'); + +const Model = mongoose.model('Offer'); + +const read = async (req, res) => { + // Find document by id + const result = await Model.findOne({ + _id: req.params.id, + removed: false, + }) + .populate('createdBy', 'name') + .exec(); + // If no results found, return document not found + if (!result) { + return res.status(404).json({ + success: false, + result: null, + message: 'No document found ', + }); + } else { + // Return success resposne + return res.status(200).json({ + success: true, + result, + message: 'we found this document ', + }); + } +}; + +module.exports = read; diff --git a/backend/src/controllers/appControllers/offerController/sendMail.js b/backend/src/controllers/appControllers/offerController/sendMail.js new file mode 100644 index 000000000..5cdabe784 --- /dev/null +++ b/backend/src/controllers/appControllers/offerController/sendMail.js @@ -0,0 +1,9 @@ +const mail = async (req, res) => { + return res.status(200).json({ + success: true, + result: null, + message: 'Please Upgrade to Premium Version to have full features', + }); +}; + +module.exports = mail; diff --git a/backend/src/controllers/appControllers/offerController/summary.js b/backend/src/controllers/appControllers/offerController/summary.js new file mode 100644 index 000000000..56f63b946 --- /dev/null +++ b/backend/src/controllers/appControllers/offerController/summary.js @@ -0,0 +1,123 @@ +const mongoose = require('mongoose'); +const moment = require('moment'); + +const Model = mongoose.model('Offer'); + +const summary = async (req, res) => { + let defaultType = 'month'; + + const { type } = req.query; + + if (type) { + if (['week', 'month', 'year'].includes(type)) { + defaultType = type; + } else { + return res.status(400).json({ + success: false, + result: null, + message: 'Invalid type', + }); + } + } + + const currentDate = moment(); + let startDate = currentDate.clone().startOf(defaultType); + let endDate = currentDate.clone().endOf(defaultType); + + const statuses = ['draft', 'pending', 'sent', 'expired', 'declined', 'accepted']; + + const response = await Model.aggregate([ + { + $match: { + removed: false, + + // date: { + // $gte: startDate.toDate(), + // $lte: endDate.toDate(), + // }, + }, + }, + { + $facet: { + totalOffer: [ + { + $group: { + _id: null, + total: { + $sum: '$total', + }, + count: { + $sum: 1, + }, + }, + }, + { + $project: { + _id: 0, + total: '$total', + count: '$count', + }, + }, + ], + statusCounts: [ + { + $group: { + _id: '$status', + count: { + $sum: 1, + }, + }, + }, + { + $project: { + _id: 0, + status: '$_id', + count: '$count', + }, + }, + ], + }, + }, + ]); + let result = []; + + const totalOffers = response[0].totalOffer ? response[0].totalOffer[0] : 0; + const statusResult = response[0].statusCounts || []; + // const overdueResult = response[0].overdueCounts || []; + + const statusResultMap = statusResult.map((item) => { + return { + ...item, + percentage: Math.round((item.count / totalOffers.count) * 100), + }; + }); + + // const overdueResultMap = overdueResult.map((item) => { + // return { + // ...item, + // status: 'expired', + // percentage: Math.round((item.count / totalOffers.count) * 100), + // }; + // }); + + statuses.forEach((status) => { + const found = [...statusResultMap].find((item) => item.status === status); + if (found) { + result.push(found); + } + }); + + const finalResult = { + total: totalOffers?.total, + type, + performance: result, + }; + + return res.status(200).json({ + success: true, + result: finalResult, + message: `Successfully found all invoices for the last ${defaultType}`, + }); +}; + +module.exports = summary; diff --git a/backend/src/controllers/appControllers/offerController/update.js b/backend/src/controllers/appControllers/offerController/update.js new file mode 100644 index 000000000..5d0265e4a --- /dev/null +++ b/backend/src/controllers/appControllers/offerController/update.js @@ -0,0 +1,10 @@ +const mongoose = require('mongoose'); + +const update = async (req, res) => { + return res.status(200).json({ + success: true, + result: null, + message: 'Please Upgrade to Premium Version to have full features', + }); +}; +module.exports = update; diff --git a/backend/src/controllers/appControllers/paymentController/remove.js b/backend/src/controllers/appControllers/paymentController/remove.js index 93939da85..5d329047e 100644 --- a/backend/src/controllers/appControllers/paymentController/remove.js +++ b/backend/src/controllers/appControllers/paymentController/remove.js @@ -1,67 +1,8 @@ -const mongoose = require('mongoose'); - -const Model = mongoose.model('Payment'); -const Invoice = mongoose.model('Invoice'); - const remove = async (req, res) => { - // Find document by id and updates with the required fields - const previousPayment = await Model.findOne({ - _id: req.params.id, - removed: false, - }); - - if (!previousPayment) { - return res.status(404).json({ - success: false, - result: null, - message: 'No document found ', - }); - } - - const { _id: paymentId, amount: previousAmount } = previousPayment; - const { id: invoiceId, total, discount, credit: previousCredit } = previousPayment.invoice; - - // Find the document by id and delete it - let updates = { - removed: true, - }; - // Find the document by id and delete it - const result = await Model.findOneAndUpdate( - { _id: req.params.id, removed: false }, - { $set: updates }, - { - new: true, // return the new result instead of the old one - } - ).exec(); - // If no results found, return document not found - - let paymentStatus = - total - discount === previousCredit - previousAmount - ? 'paid' - : previousCredit - previousAmount > 0 - ? 'partially' - : 'unpaid'; - - const updateInvoice = await Invoice.findOneAndUpdate( - { _id: invoiceId }, - { - $pull: { - payment: paymentId, - }, - $inc: { credit: -previousAmount }, - $set: { - paymentStatus: paymentStatus, - }, - }, - { - new: true, // return the new result instead of the old one - } - ).exec(); - return res.status(200).json({ success: true, - result, - message: 'Successfully Deleted the document ', + result: null, + message: 'Please Upgrade to Premium Version to have full features', }); }; module.exports = remove; diff --git a/backend/src/controllers/appControllers/paymentController/update.js b/backend/src/controllers/appControllers/paymentController/update.js index 0691048eb..ca4f0c3b7 100644 --- a/backend/src/controllers/appControllers/paymentController/update.js +++ b/backend/src/controllers/appControllers/paymentController/update.js @@ -1,85 +1,8 @@ -const mongoose = require('mongoose'); - -const Model = mongoose.model('Payment'); -const Invoice = mongoose.model('Invoice'); -const custom = require('@/controllers/pdfController'); - -const { calculate } = require('@/helpers'); - const update = async (req, res) => { - if (req.body.amount === 0) { - return res.status(202).json({ - success: false, - result: null, - message: `The Minimum Amount couldn't be 0`, - }); - } - // Find document by id and updates with the required fields - const previousPayment = await Model.findOne({ - _id: req.params.id, - removed: false, - }); - - const { amount: previousAmount } = previousPayment; - const { id: invoiceId, total, discount, credit: previousCredit } = previousPayment.invoice; - - const { amount: currentAmount } = req.body; - - const changedAmount = calculate.sub(currentAmount, previousAmount); - const maxAmount = calculate.sub(total, calculate.add(discount, previousCredit)); - - if (changedAmount > maxAmount) { - return res.status(202).json({ - success: false, - result: null, - message: `The Max Amount you can add is ${maxAmount + previousAmount}`, - error: `The Max Amount you can add is ${maxAmount + previousAmount}`, - }); - } - - let paymentStatus = - calculate.sub(total, discount) === calculate.add(previousCredit, changedAmount) - ? 'paid' - : calculate.add(previousCredit, changedAmount) > 0 - ? 'partially' - : 'unpaid'; - - const updatedDate = new Date(); - const updates = { - number: req.body.number, - date: req.body.date, - amount: req.body.amount, - paymentMode: req.body.paymentMode, - ref: req.body.ref, - description: req.body.description, - updated: updatedDate, - }; - - const result = await Model.findOneAndUpdate( - { _id: req.params.id, removed: false }, - { $set: updates }, - { - new: true, // return the new result instead of the old one - } - ).exec(); - - const updateInvoice = await Invoice.findOneAndUpdate( - { _id: result.invoice._id.toString() }, - { - $inc: { credit: changedAmount }, - $set: { - paymentStatus: paymentStatus, - }, - }, - { - new: true, // return the new result instead of the old one - } - ).exec(); - return res.status(200).json({ success: true, - result, - message: 'Successfully updated the Payment ', + result: null, + message: 'Please Upgrade to Premium Version to have full features', }); }; diff --git a/backend/src/controllers/appControllers/peopleController/index.js b/backend/src/controllers/appControllers/peopleController/index.js new file mode 100644 index 000000000..7adf66966 --- /dev/null +++ b/backend/src/controllers/appControllers/peopleController/index.js @@ -0,0 +1,21 @@ +const mongoose = require('mongoose'); + +const createCRUDController = require('@/controllers/middlewaresControllers/createCRUDController'); +const read = require('./read'); +const remove = require('./remove'); +const update = require('./update'); +const paginatedList = require('./paginatedList'); + +function modelController() { + const Model = mongoose.model('People'); + const methods = createCRUDController('People'); + + methods.read = (req, res) => read(Model, req, res); + methods.update = (req, res) => update(Model, req, res); + methods.delete = (req, res) => remove(Model, req, res); + methods.list = (req, res) => paginatedList(Model, req, res); + + return methods; +} + +module.exports = modelController(); diff --git a/backend/src/controllers/appControllers/peopleController/paginatedList.js b/backend/src/controllers/appControllers/peopleController/paginatedList.js new file mode 100644 index 000000000..43b6fe28c --- /dev/null +++ b/backend/src/controllers/appControllers/peopleController/paginatedList.js @@ -0,0 +1,64 @@ +const paginatedList = async (Model, req, res) => { + const page = req.query.page || 1; + const limit = parseInt(req.query.items) || 10; + const skip = page * limit - limit; + + const { sortBy = 'enabled', sortValue = -1, filter, equal } = req.query; + + const fieldsArray = req.query.fields ? req.query.fields.split(',') : []; + + let fields; + + fields = fieldsArray.length === 0 ? {} : { $or: [] }; + + for (const field of fieldsArray) { + fields.$or.push({ [field]: { $regex: new RegExp(req.query.q, 'i') } }); + } + + // Query the database for a list of all results + const resultsPromise = Model.find({ + removed: false, + + [filter]: equal, + ...fields, + }) + .skip(skip) + .limit(limit) + .sort({ [sortBy]: sortValue }) + .populate('company', 'name') + .exec(); + + // Counting the total documents + const countPromise = Model.countDocuments({ + removed: false, + + [filter]: equal, + ...fields, + }); + + // Resolving both promises + const [result, count] = await Promise.all([resultsPromise, countPromise]); + + // Calculating total pages + const pages = Math.ceil(count / limit); + + // Getting Pagination Object + const pagination = { page, pages, count }; + if (count > 0) { + return res.status(200).json({ + success: true, + result, + pagination, + message: 'Successfully found all documents', + }); + } else { + return res.status(203).json({ + success: true, + result: [], + pagination, + message: 'Collection is Empty', + }); + } +}; + +module.exports = paginatedList; diff --git a/backend/src/controllers/appControllers/peopleController/read.js b/backend/src/controllers/appControllers/peopleController/read.js new file mode 100644 index 000000000..8b0f39786 --- /dev/null +++ b/backend/src/controllers/appControllers/peopleController/read.js @@ -0,0 +1,26 @@ +const read = async (Model, req, res) => { + // Find document by id + const result = await Model.findOne({ + _id: req.params.id, + removed: false, + }) + .populate('company', 'name') + .exec(); + // If no results found, return document not found + if (!result) { + return res.status(404).json({ + success: false, + result: null, + message: 'No document found ', + }); + } else { + // Return success resposne + return res.status(200).json({ + success: true, + result, + message: 'we found this document ', + }); + } +}; + +module.exports = read; diff --git a/backend/src/controllers/appControllers/peopleController/remove.js b/backend/src/controllers/appControllers/peopleController/remove.js new file mode 100644 index 000000000..7e851a28e --- /dev/null +++ b/backend/src/controllers/appControllers/peopleController/remove.js @@ -0,0 +1,8 @@ +const remove = async (Model, req, res) => { + return res.status(200).json({ + success: true, + result: null, + message: 'Please Upgrade to Premium Version to have full features', + }); +}; +module.exports = remove; diff --git a/backend/src/controllers/appControllers/peopleController/update.js b/backend/src/controllers/appControllers/peopleController/update.js new file mode 100644 index 000000000..7a2511b4d --- /dev/null +++ b/backend/src/controllers/appControllers/peopleController/update.js @@ -0,0 +1,13 @@ +const mongoose = require('mongoose'); +const Client = mongoose.model('Client'); +const Lead = mongoose.model('People'); + +const update = async (Model, req, res) => { + return res.status(200).json({ + success: true, + result: null, + message: 'Please Upgrade to Premium Version to have full features', + }); +}; + +module.exports = update; diff --git a/backend/src/controllers/appControllers/quoteController/update.js b/backend/src/controllers/appControllers/quoteController/update.js index a4f32696d..a649e634d 100644 --- a/backend/src/controllers/appControllers/quoteController/update.js +++ b/backend/src/controllers/appControllers/quoteController/update.js @@ -1,61 +1,8 @@ -const mongoose = require('mongoose'); - -const Model = mongoose.model('Quote'); - -const custom = require('@/controllers/pdfController'); - -const { calculate } = require('@/helpers'); - const update = async (req, res) => { - const { items = [], taxRate = 0, discount = 0 } = req.body; - - if (items.length === 0) { - return res.status(400).json({ - success: false, - result: null, - message: 'Items cannot be empty', - }); - } - // default - let subTotal = 0; - let taxTotal = 0; - let total = 0; - // let credit = 0; - - //Calculate the items array with subTotal, total, taxTotal - items.map((item) => { - let total = calculate.multiply(item['quantity'], item['price']); - //sub total - subTotal = calculate.add(subTotal, total); - //item total - item['total'] = total; - }); - taxTotal = calculate.multiply(subTotal, taxRate / 100); - total = calculate.add(subTotal, taxTotal); - - let body = req.body; - - body['subTotal'] = subTotal; - body['taxTotal'] = taxTotal; - body['total'] = total; - body['items'] = items; - body['pdf'] = 'quote-' + req.params.id + '.pdf'; - - if (body.hasOwnProperty('currency')) { - delete body.currency; - } - // Find document by id and updates with the required fields - - const result = await Model.findOneAndUpdate({ _id: req.params.id, removed: false }, body, { - new: true, // return the new result instead of the old one - }).exec(); - - // Returning successfull response - return res.status(200).json({ success: true, - result, - message: 'we update this document ', + result: null, + message: 'Please Upgrade to Premium Version to have full features', }); }; module.exports = update; diff --git a/backend/src/controllers/appControllers/taxesController/index.js b/backend/src/controllers/appControllers/taxesController/index.js index ba31f817a..4f6f167dc 100644 --- a/backend/src/controllers/appControllers/taxesController/index.js +++ b/backend/src/controllers/appControllers/taxesController/index.js @@ -38,43 +38,10 @@ methods.delete = async (req, res) => { }; methods.update = async (req, res) => { - const { id } = req.params; - const tax = await Model.findOne({ - _id: req.params.id, - removed: false, - }).exec(); - const { isDefault = tax.isDefault, enabled = tax.enabled } = req.body; - - // if isDefault:false , we update first - isDefault:true - // if enabled:false and isDefault:true , we update first - isDefault:true - if (!isDefault || (!enabled && isDefault)) { - await Model.findOneAndUpdate({ _id: { $ne: id }, enabled: true }, { isDefault: true }); - } - - // if isDefault:true and enabled:true, we update other taxes and make is isDefault:false - if (isDefault && enabled) { - await Model.updateMany({ _id: { $ne: id } }, { isDefault: false }); - } - - const taxesCount = await Model.countDocuments({}); - - // if enabled:false and it's only one exist, we can't disable - if ((!enabled || !isDefault) && taxesCount <= 1) { - return res.status(422).json({ - success: false, - result: null, - message: 'You cannot disable the tax because it is the only existing one', - }); - } - - const result = await Model.findOneAndUpdate({ _id: id }, req.body, { - new: true, - }); - return res.status(200).json({ success: true, - message: 'Tax updated successfully', - result, + result: null, + message: 'Please Upgrade to Premium Version to have full features', }); }; diff --git a/backend/src/controllers/coreControllers/emailController/index.js b/backend/src/controllers/coreControllers/emailController/index.js new file mode 100644 index 000000000..98072d97c --- /dev/null +++ b/backend/src/controllers/coreControllers/emailController/index.js @@ -0,0 +1,14 @@ +const createCRUDController = require('@/controllers/middlewaresControllers/createCRUDController'); +const crudController = createCRUDController('Email'); + +const emailMethods = { + create:crudController.create, + read: crudController.read, + update: crudController.update, + list: crudController.list, + listAll: crudController.listAll, + filter: crudController.filter, + search: crudController.search, +}; + +module.exports = emailMethods; diff --git a/backend/src/controllers/coreControllers/settingController/updateBySettingKey.js b/backend/src/controllers/coreControllers/settingController/updateBySettingKey.js index f6e1aebc1..d93a0c317 100644 --- a/backend/src/controllers/coreControllers/settingController/updateBySettingKey.js +++ b/backend/src/controllers/coreControllers/settingController/updateBySettingKey.js @@ -1,49 +1,9 @@ -const mongoose = require('mongoose'); - -const Model = mongoose.model('Setting'); - const updateBySettingKey = async (req, res) => { - const settingKey = req.params.settingKey || undefined; - - if (!settingKey) { - return res.status(202).json({ - success: false, - result: null, - message: 'No settingKey provided ', - }); - } - const { settingValue } = req.body; - - if (!settingValue) { - return res.status(202).json({ - success: false, - result: null, - message: 'No settingValue provided ', - }); - } - const result = await Model.findOneAndUpdate( - { settingKey }, - { - settingValue, - }, - { - new: true, // return the new result instead of the old one - runValidators: true, - } - ).exec(); - if (!result) { - return res.status(404).json({ - success: false, - result: null, - message: 'No document found by this settingKey: ' + settingKey, - }); - } else { - return res.status(200).json({ - success: true, - result, - message: 'we update this document by this settingKey: ' + settingKey, - }); - } + return res.status(200).json({ + success: true, + result: null, + message: 'Please Upgrade to Premium Version to have full features', + }); }; module.exports = updateBySettingKey; diff --git a/backend/src/controllers/coreControllers/settingController/updateManySetting.js b/backend/src/controllers/coreControllers/settingController/updateManySetting.js index cdd7c41f2..39b99af8e 100644 --- a/backend/src/controllers/coreControllers/settingController/updateManySetting.js +++ b/backend/src/controllers/coreControllers/settingController/updateManySetting.js @@ -1,58 +1,9 @@ -const mongoose = require('mongoose'); - -const Model = mongoose.model('Setting'); - const updateManySetting = async (req, res) => { - // req/body = [{settingKey:"",settingValue}] - let settingsHasError = false; - const updateDataArray = []; - const { settings } = req.body; - - for (const setting of settings) { - if (!setting.hasOwnProperty('settingKey') || !setting.hasOwnProperty('settingValue')) { - settingsHasError = true; - break; - } - - const { settingKey, settingValue } = setting; - - updateDataArray.push({ - updateOne: { - filter: { settingKey: settingKey }, - update: { settingValue: settingValue }, - }, - }); - } - - if (updateDataArray.length === 0) { - return res.status(202).json({ - success: false, - result: null, - message: 'No settings provided ', - }); - } - if (settingsHasError) { - return res.status(202).json({ - success: false, - result: null, - message: 'Settings provided has Error', - }); - } - const result = await Model.bulkWrite(updateDataArray); - - if (!result || result.nMatched < 1) { - return res.status(404).json({ - success: false, - result: null, - message: 'No settings found by to update', - }); - } else { - return res.status(200).json({ - success: true, - result: [], - message: 'we update all settings', - }); - } + return res.status(200).json({ + success: true, + result: null, + message: 'Please Upgrade to Premium Version to have full features', + }); }; module.exports = updateManySetting; diff --git a/backend/src/controllers/middlewaresControllers/createAuthMiddleware/authUser.js b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/authUser.js index 546d56311..577c20e2e 100644 --- a/backend/src/controllers/middlewaresControllers/createAuthMiddleware/authUser.js +++ b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/authUser.js @@ -2,7 +2,7 @@ const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const authUser = async (req, res, { user, databasePassword, password, UserPasswordModel }) => { - const isMatch = await bcrypt.compare(databasePassword.salt + password, databasePassword.password); + const isMatch = await bcrypt.compare(password, databasePassword.password); if (!isMatch) return res.status(403).json({ @@ -28,29 +28,29 @@ const authUser = async (req, res, { user, databasePassword, password, UserPasswo } ).exec(); - // .cookie(`token_${user.cloud}`, token, { - // maxAge: req.body.remember ? 365 * 24 * 60 * 60 * 1000 : null, - // sameSite: 'None', - // httpOnly: true, - // secure: true, - // domain: req.hostname, - // path: '/', - // Partitioned: true, - // }) - res.status(200).json({ - success: true, - result: { - _id: user._id, - name: user.name, - surname: user.surname, - role: user.role, - email: user.email, - photo: user.photo, - token: token, - maxAge: req.body.remember ? 365 : null, - }, - message: 'Successfully login user', - }); + res + .status(200) + .cookie('token', token, { + maxAge: req.body.remember ? 365 * 24 * 60 * 60 * 1000 : null, + sameSite: 'Lax', + httpOnly: true, + secure: false, + domain: req.hostname, + path: '/', + Partitioned: true, + }) + .json({ + success: true, + result: { + _id: user._id, + name: user.name, + surname: user.surname, + role: user.role, + email: user.email, + photo: user.photo, + }, + message: 'Successfully login user', + }); } else { return res.status(403).json({ success: false, @@ -60,4 +60,4 @@ const authUser = async (req, res, { user, databasePassword, password, UserPasswo } }; -module.exports = authUser; +module.exports = authUser; \ No newline at end of file diff --git a/backend/src/controllers/middlewaresControllers/createAuthMiddleware/forgetPassword.js b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/forgetPassword.js index 176cdd1ce..ec90dfe84 100644 --- a/backend/src/controllers/middlewaresControllers/createAuthMiddleware/forgetPassword.js +++ b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/forgetPassword.js @@ -35,6 +35,13 @@ const forgetPassword = async (req, res, { userModel }) => { const user = await User.findOne({ email: email, removed: false }); const databasePassword = await UserPassword.findOne({ user: user._id, removed: false }); + if (!user.enabled) + return res.status(409).json({ + success: false, + result: null, + message: 'Your account is disabled, contact your account adminstrator', + }); + // console.log(user); if (!user) return res.status(404).json({ diff --git a/backend/src/controllers/middlewaresControllers/createAuthMiddleware/index.js b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/index.js index bf45d7b4a..993d8a9af 100644 --- a/backend/src/controllers/middlewaresControllers/createAuthMiddleware/index.js +++ b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/index.js @@ -1,6 +1,7 @@ const isValidAuthToken = require('./isValidAuthToken'); const login = require('./login'); const logout = require('./logout'); +const register = require('./register'); const forgetPassword = require('./forgetPassword'); const resetPassword = require('./resetPassword'); @@ -17,6 +18,11 @@ const createAuthMiddleware = (userModel) => { userModel, }); + authMethods.register = (req, res) => + register(req, res, { + userModel, + }); + authMethods.forgetPassword = (req, res) => forgetPassword(req, res, { userModel, diff --git a/backend/src/controllers/middlewaresControllers/createAuthMiddleware/isValidAuthToken.js b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/isValidAuthToken.js index 33a963f5e..ce79f4829 100644 --- a/backend/src/controllers/middlewaresControllers/createAuthMiddleware/isValidAuthToken.js +++ b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/isValidAuthToken.js @@ -6,11 +6,7 @@ const isValidAuthToken = async (req, res, next, { userModel, jwtSecret = 'JWT_SE try { const UserPassword = mongoose.model(userModel + 'Password'); const User = mongoose.model(userModel); - - // const token = req.cookies[`token_${cloud._id}`]; - const authHeader = req.headers['authorization']; - const token = authHeader && authHeader.split(' ')[1]; // Extract the token - + const token = req.cookies.token; if (!token) return res.status(401).json({ success: false, @@ -43,7 +39,6 @@ const isValidAuthToken = async (req, res, next, { userModel, jwtSecret = 'JWT_SE }); const { loggedSessions } = userPassword; - if (!loggedSessions.includes(token)) return res.status(401).json({ success: false, @@ -57,13 +52,12 @@ const isValidAuthToken = async (req, res, next, { userModel, jwtSecret = 'JWT_SE next(); } } catch (error) { - return res.status(500).json({ + return res.status(503).json({ success: false, result: null, message: error.message, error: error, controller: 'isValidAuthToken', - jwtExpired: true, }); } }; diff --git a/backend/src/controllers/middlewaresControllers/createAuthMiddleware/login.js b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/login.js index 7b1eccee8..7b57c615e 100644 --- a/backend/src/controllers/middlewaresControllers/createAuthMiddleware/login.js +++ b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/login.js @@ -1,7 +1,15 @@ +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); const Joi = require('joi'); const mongoose = require('mongoose'); +const checkAndCorrectURL = require('./checkAndCorrectURL'); +const sendMail = require('./sendMail'); + +const { loadSettings } = require('@/middlewares/settings'); +const { useAppSettings } = require('@/settings'); + const authUser = require('./authUser'); const login = async (req, res, { userModel }) => { @@ -48,12 +56,7 @@ const login = async (req, res, { userModel }) => { }); // authUser if your has correct password - authUser(req, res, { - user, - databasePassword, - password, - UserPasswordModel, - }); + authUser(req, res, { user, databasePassword, password, UserPasswordModel }); }; module.exports = login; diff --git a/backend/src/controllers/middlewaresControllers/createAuthMiddleware/logout.js b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/logout.js index 9eea8aedd..253bf753a 100644 --- a/backend/src/controllers/middlewaresControllers/createAuthMiddleware/logout.js +++ b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/logout.js @@ -3,33 +3,29 @@ const mongoose = require('mongoose'); const logout = async (req, res, { userModel }) => { const UserPassword = mongoose.model(userModel + 'Password'); - // const token = req.cookies[`token_${cloud._id}`]; + const token = req.cookies.token; + await UserPassword.findOneAndUpdate( + { user: req.admin._id }, + { $pull: { loggedSessions: token } }, + { + new: true, + } + ).exec(); - const authHeader = req.headers['authorization']; - const token = authHeader && authHeader.split(' ')[1]; // Extract the token - - if (token) - await UserPassword.findOneAndUpdate( - { user: req.admin._id }, - { $pull: { loggedSessions: token } }, - { - new: true, - } - ).exec(); - else - await UserPassword.findOneAndUpdate( - { user: req.admin._id }, - { loggedSessions: [] }, - { - new: true, - } - ).exec(); - - return res.json({ - success: true, - result: {}, - message: 'Successfully logout', - }); + res + .clearCookie('token', { + maxAge: null, + sameSite: 'none', + httpOnly: true, + secure: true, + domain: req.hostname, + Path: '/', + }) + .json({ + success: true, + result: {}, + message: 'Successfully logout', + }); }; module.exports = logout; diff --git a/backend/src/controllers/middlewaresControllers/createAuthMiddleware/register.js b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/register.js new file mode 100644 index 000000000..18e193467 --- /dev/null +++ b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/register.js @@ -0,0 +1,84 @@ +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); +const Joi = require('joi'); + +const mongoose = require('mongoose'); + +const checkAndCorrectURL = require('./checkAndCorrectURL'); +const sendMail = require('./sendMail'); + +const { loadSettings } = require('@/middlewares/settings'); +const { useAppSettings } = require('@/settings'); + +const register = async (req, res, { userModel }) => { + const UserPasswordModel = mongoose.model(userModel + 'Password'); + const UserModel = mongoose.model(userModel); + const { name, email, password, country } = req.body; + + // validate + const objectSchema = Joi.object({ + email: Joi.string() + .email({ tlds: { allow: true } }) + .required(), + name: Joi.string().required(), + country: Joi.string().required(), + password: Joi.string().required(), + }); + + const { error, value } = objectSchema.validate({ name, email, password, country }); + if (error) { + return res.status(409).json({ + success: false, + result: null, + error: error, + message: 'Invalid/Missing credentials.', + errorMessage: error.message, + }); + } + + const user = await UserModel.findOne({ email: email, removed: false }); + if (user) + return res.status(409).json({ + success: false, + result: null, + message: 'An account with this email already exists.', + }); + + // authUser if your has correct password + const salt = await bcrypt.genSalt(10); + console.log(salt) + const hashedPassword = await bcrypt.hash(password, salt); + const newUser = { + name: name, + email: email, + country: country, + enabled: true, + }; + + const createdUser = await UserModel.create(newUser); + const newUserPassword = { + removed: false, + user: createdUser, + password: hashedPassword, + salt: salt, + emailVerified: false, + authType: "email", + loggedSessions: [] + } + const databasePassword = await UserPasswordModel.create(newUserPassword); + if (!createdUser || !databasePassword) { + return res.status(500).json({ + success: false, + result: null, + message: 'Error creating your account.', + }); + } else { + const success = { + success: true + } + const newUser = {...createdUser, ...success} + return res.status(200).json(newUser); + } +}; + +module.exports = register; \ No newline at end of file diff --git a/backend/src/controllers/middlewaresControllers/createAuthMiddleware/resetPassword.js b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/resetPassword.js index d9bb0a811..de8eb1a3a 100644 --- a/backend/src/controllers/middlewaresControllers/createAuthMiddleware/resetPassword.js +++ b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/resetPassword.js @@ -13,6 +13,31 @@ const resetPassword = async (req, res, { userModel }) => { const databasePassword = await UserPassword.findOne({ user: userId, removed: false }); const user = await User.findOne({ _id: userId, removed: false }).exec(); + if (!user.enabled && user.role === 'owner') { + const settings = useAppSettings(); + const idurar_app_email = settings['idurar_app_email']; + const idurar_base_url = settings['idurar_base_url']; + + const url = checkAndCorrectURL(idurar_base_url); + + const link = url + '/verify/' + user._id + '/' + databasePassword.emailToken; + + await sendMail({ + email, + name: user.name, + link, + idurar_app_email, + emailToken: databasePassword.emailToken, + }); + + return res.status(403).json({ + success: false, + result: null, + message: + 'your email account is not verified , check your email inbox to activate your account', + }); + } + if (!user.enabled) return res.status(409).json({ success: false, @@ -85,29 +110,29 @@ const resetPassword = async (req, res, { userModel }) => { databasePassword.resetToken !== undefined && databasePassword.resetToken !== null ) - // .cookie(`token_${user.cloud}`, token, { - // maxAge: 24 * 60 * 60 * 1000, - // sameSite: 'None', - // httpOnly: true, - // secure: true, - // domain: req.hostname, - // path: '/', - // Partitioned: true, - // }) - return res.status(200).json({ - success: true, - result: { - _id: user._id, - name: user.name, - surname: user.surname, - role: user.role, - email: user.email, - photo: user.photo, - token: token, - maxAge: req.body.remember ? 365 : null, - }, - message: 'Successfully resetPassword user', - }); + return res + .status(200) + .cookie('token', token, { + maxAge: 24 * 60 * 60 * 1000, + sameSite: 'Lax', + httpOnly: true, + secure: false, + domain: req.hostname, + path: '/', + Partitioned: true, + }) + .json({ + success: true, + result: { + _id: user._id, + name: user.name, + surname: user.surname, + role: user.role, + email: user.email, + photo: user.photo, + }, + message: 'Successfully resetPassword user', + }); }; module.exports = resetPassword; diff --git a/backend/src/controllers/middlewaresControllers/createAuthMiddleware/sendIdurarOffer.js b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/sendIdurarOffer.js new file mode 100644 index 000000000..c2826e4fb --- /dev/null +++ b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/sendIdurarOffer.js @@ -0,0 +1,18 @@ +const { afterRegistrationSuccess } = require('@/emailTemplate/emailVerfication'); + +const { Resend } = require('resend'); + +const sendIdurarOffer = async ({ email, name }) => { + const resend = new Resend(process.env.RESEND_API); + + const { data } = await resend.emails.send({ + from: 'hello@idurarapp.com', + to: email, + subject: 'Customize IDURAR ERP CRM or build your own SaaS', + html: afterRegistrationSuccess({ name }), + }); + + return data; +}; + +module.exports = sendIdurarOffer; diff --git a/backend/src/controllers/middlewaresControllers/createAuthMiddleware/sendMail.js b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/sendMail.js index b16d2e487..c1d85fa0c 100644 --- a/backend/src/controllers/middlewaresControllers/createAuthMiddleware/sendMail.js +++ b/backend/src/controllers/middlewaresControllers/createAuthMiddleware/sendMail.js @@ -1,4 +1,4 @@ -const { passwordVerfication } = require('@/emailTemplate/emailVerfication'); +const { emailVerfication, passwordVerfication } = require('@/emailTemplate/emailVerfication'); const { Resend } = require('resend'); @@ -17,7 +17,10 @@ const sendMail = async ({ from: idurar_app_email, to: email, subject, - html: passwordVerfication({ name, link }), + html: + type === 'emailVerfication' + ? emailVerfication({ name, link, emailToken }) + : passwordVerfication({ name, link }), }); return data; diff --git a/backend/src/controllers/middlewaresControllers/createCRUDController/remove.js b/backend/src/controllers/middlewaresControllers/createCRUDController/remove.js index 5072eae18..c39be4395 100644 --- a/backend/src/controllers/middlewaresControllers/createCRUDController/remove.js +++ b/backend/src/controllers/middlewaresControllers/createCRUDController/remove.js @@ -1,32 +1,9 @@ const remove = async (Model, req, res) => { - // Find the document by id and delete it - let updates = { - removed: true, - }; - // Find the document by id and delete it - const result = await Model.findOneAndUpdate( - { - _id: req.params.id, - }, - { $set: updates }, - { - new: true, // return the new result instead of the old one - } - ).exec(); - // If no results found, return document not found - if (!result) { - return res.status(404).json({ - success: false, - result: null, - message: 'No document found ', - }); - } else { - return res.status(200).json({ - success: true, - result, - message: 'Successfully Deleted the document ', - }); - } + return res.status(200).json({ + success: true, + result: null, + message: 'Please Upgrade to Premium Version to have full features', + }); }; module.exports = remove; diff --git a/backend/src/controllers/middlewaresControllers/createCRUDController/update.js b/backend/src/controllers/middlewaresControllers/createCRUDController/update.js index 8438598d6..2e4e0f977 100644 --- a/backend/src/controllers/middlewaresControllers/createCRUDController/update.js +++ b/backend/src/controllers/middlewaresControllers/createCRUDController/update.js @@ -1,30 +1,9 @@ const update = async (Model, req, res) => { - // Find document by id and updates with the required fields - req.body.removed = false; - const result = await Model.findOneAndUpdate( - { - _id: req.params.id, - removed: false, - }, - req.body, - { - new: true, // return the new result instead of the old one - runValidators: true, - } - ).exec(); - if (!result) { - return res.status(404).json({ - success: false, - result: null, - message: 'No document found ', - }); - } else { - return res.status(200).json({ - success: true, - result, - message: 'we update this document ', - }); - } + return res.status(200).json({ + success: true, + result: null, + message: 'Please Upgrade to Premium Version to have full features', + }); }; module.exports = update; diff --git a/backend/src/controllers/middlewaresControllers/createUserController/updatePassword.js b/backend/src/controllers/middlewaresControllers/createUserController/updatePassword.js index f9b6d3fb8..df57cb545 100644 --- a/backend/src/controllers/middlewaresControllers/createUserController/updatePassword.js +++ b/backend/src/controllers/middlewaresControllers/createUserController/updatePassword.js @@ -17,14 +17,6 @@ const updatePassword = async (userModel, req, res) => { // Find document by id and updates with the required fields - if (userProfile.email === 'admin@demo.com') { - return res.status(403).json({ - success: false, - result: null, - message: "you couldn't update demo password", - }); - } - const salt = uniqueId(); const passwordHash = bcrypt.hashSync(salt + password); diff --git a/backend/src/controllers/middlewaresControllers/createUserController/updateProfile.js b/backend/src/controllers/middlewaresControllers/createUserController/updateProfile.js index 8842e71be..9a17247c8 100644 --- a/backend/src/controllers/middlewaresControllers/createUserController/updateProfile.js +++ b/backend/src/controllers/middlewaresControllers/createUserController/updateProfile.js @@ -5,15 +5,6 @@ const updateProfile = async (userModel, req, res) => { const reqUserName = userModel.toLowerCase(); const userProfile = req[reqUserName]; - - if (userProfile.email === 'admin@demo.com') { - return res.status(403).json({ - success: false, - result: null, - message: "you couldn't update demo informations", - }); - } - let updates = req.body.photo ? { email: req.body.email, diff --git a/backend/src/controllers/middlewaresControllers/createUserController/updateProfilePassword.js b/backend/src/controllers/middlewaresControllers/createUserController/updateProfilePassword.js index f8ce21dd9..c3c0c4130 100644 --- a/backend/src/controllers/middlewaresControllers/createUserController/updateProfilePassword.js +++ b/backend/src/controllers/middlewaresControllers/createUserController/updateProfilePassword.js @@ -32,13 +32,6 @@ const updateProfilePassword = async (userModel, req, res) => { salt: salt, }; - if (userProfile.email === 'admin@demo.com') { - return res.status(403).json({ - success: false, - result: null, - message: "you couldn't update demo password", - }); - } const resultPassword = await UserPassword.findOneAndUpdate( { user: userProfile._id, removed: false }, { $set: UserPasswordData }, diff --git a/backend/src/emailTemplate/emailVerfication.js b/backend/src/emailTemplate/emailVerfication.js index cae578940..393bbcdc5 100644 --- a/backend/src/emailTemplate/emailVerfication.js +++ b/backend/src/emailTemplate/emailVerfication.js @@ -1,3 +1,37 @@ +exports.emailVerfication = ({ + title = 'Verify your email', + name = '', + link = '', + time = new Date(), + emailToken, +}) => { + return ` +
+ + + + ${title} + + + + +

${title}

+
+

Hello ${name},

+

Code :
${emailToken}

+

Thank you for signing up for IDURAR ! Before we can activate your account, we kindly ask you to verify your email address by clicking on the link provided below:

+

${link}

+

Thank you for choosing IDURAR. We look forward to having you as a valued user!

+
+

Best regards,

+

Salah Eddine Lalami

+

Founder @ IDURAR

+ +
+ `; +}; + exports.passwordVerfication = ({ title = 'Reset your Password', name = '', @@ -20,9 +54,43 @@ exports.passwordVerfication = ({

Hello ${name},

We have received a request to reset the password for your account on IDURAR. To proceed with the password reset, please click on the link provided below:

${link}

- - +
+

Best regards,

+

Salah Eddine Lalami

+

Founder @ IDURAR

+ + + `; +}; + +exports.afterRegistrationSuccess = ({ + title = 'Customize IDURAR ERP CRM or build your own SaaS', + name = '', +}) => { + return ` +
+ + + + ${title} + + + + +

${title}

+
+

Hello ${name},

+

I would like to invite you to book a call if you need :

+

* Customize or adding new features to IDURAR ERP CRM.

+

* Build your own custom SaaS solution based on IDURAR ERP CRM , With IDURAR SaaS license , instead of investing in an uncertain developer team. This opportunity allows you to build a tailored SaaS platform that meets your specific business needs.

+

Book a call here https://calendly.com/lalami/meeting

+
+

Best regards,

+

Salah Eddine Lalami

+

Founder @ IDURAR

+ `; }; diff --git a/backend/src/locale/languages.js b/backend/src/locale/languages.js index e0a30c5df..71d48008e 100644 --- a/backend/src/locale/languages.js +++ b/backend/src/locale/languages.js @@ -1 +1,62 @@ -module.exports = []; +module.exports = [ + { icon: '๐Ÿ‡ฆ๐Ÿ‡ฑ ', label: 'Albanian', value: 'sq_al' }, + { icon: '๐Ÿ‡ฉ๐Ÿ‡ฟ ', label: 'Arabic', value: 'ar_eg', isRtl: true }, + { icon: '๐Ÿ‡ฆ๐Ÿ‡ฒ ', label: 'Armenian', value: 'hy_am' }, + { icon: '๐Ÿ‡ฆ๐Ÿ‡ฟ ', label: 'Azerbaijani', value: 'az_az' }, + { icon: '๐Ÿ‡ช๐Ÿ‡ฆ ', label: 'Basque', value: 'eu_es' }, + { icon: '๐Ÿ‡ง๐Ÿ‡พ ', label: 'Belarusian', value: 'by_by' }, + { icon: '๐Ÿ‡ท๐Ÿ‡ธ ', label: 'Serbian', value: 'sr_rs' }, + { icon: '๐Ÿ‡ง๐Ÿ‡ฉ ', label: 'Bengali', value: 'bn_bd' }, + { icon: '๐Ÿ‡ง๐Ÿ‡ฌ ', label: 'Bulgarian', value: 'bg_bg' }, + { icon: '๐Ÿ‡ช๐Ÿ‡ฆ ', label: 'Catalonian', value: 'ca_es' }, + { icon: '๐Ÿ‡จ๐Ÿ‡ณ ', label: 'Chinese', value: 'zh_cn' }, + { icon: '๐Ÿ‡ญ๐Ÿ‡ท ', label: 'Croatian', value: 'hr_hr' }, + { icon: '๐Ÿ‡จ๐Ÿ‡ฟ ', label: 'Czech', value: 'cs_cz' }, + { icon: '๐Ÿ‡ฉ๐Ÿ‡ฐ ', label: 'Danish', value: 'da_dk' }, + { icon: '๐Ÿ‡ณ๐Ÿ‡ฑ ', label: 'Dutch', value: 'nl_nl' }, + { icon: '๐Ÿ‡ช๐Ÿ‡ช ', label: 'Estonian', value: 'et_ee' }, + { icon: '๐Ÿ‡ต๐Ÿ‡ญ ', label: 'Filipino', value: 'fil_ph' }, + { icon: '๐Ÿ‡ซ๐Ÿ‡ฎ ', label: 'Finnish', value: 'fi_fi' }, + { icon: '๐Ÿ‡ซ๐Ÿ‡ท ', label: 'French', value: 'fr_fr' }, + { icon: '๐Ÿ‡ช๐Ÿ‡ธ ', label: 'Galician', value: 'gl_es' }, + { icon: '๐Ÿ‡ฌ๐Ÿ‡ช ', label: 'Georgian', value: 'ka_ge' }, + { icon: '๐Ÿ‡ฉ๐Ÿ‡ช ', label: 'German', value: 'de_de' }, + { icon: '๐Ÿ‡ฌ๐Ÿ‡ท ', label: 'Greek', value: 'el_gr' }, + { icon: '๐Ÿ‡ฎ๐Ÿ‡ฑ ', label: 'Hebrew', value: 'he_il', isRtl: true }, + { icon: '๐Ÿ‡ฎ๐Ÿ‡ณ ', label: 'Hindi', value: 'hi_in' }, + { icon: '๐Ÿ‡ญ๐Ÿ‡บ ', label: 'Hungarian', value: 'hu_hu' }, + { icon: '๐Ÿ‡ฎ๐Ÿ‡ฉ ', label: 'Indonesian', value: 'id_id' }, + { icon: '๐Ÿ‡ฎ๐Ÿ‡ธ ', label: 'Icelandic', value: 'is_is' }, + { icon: '๐Ÿ‡ฎ๐Ÿ‡น ', label: 'Italian', value: 'it_it' }, + { icon: '๐Ÿ‡ฏ๐Ÿ‡ต ', label: 'Japanese ', value: 'ja_jp' }, + { icon: '๐Ÿ‡ฉ๐Ÿ‡ฟ ', label: 'Kabyle', value: 'kb_dz' }, + { icon: '๐Ÿ‡ฎ๐Ÿ‡ถ ', label: 'Kurdish', value: 'kmr_iq' }, + { icon: '๐Ÿ‡ฐ๐Ÿ‡ฟ ', label: 'Kazakh', value: 'kk_kz' }, + { icon: '๐Ÿ‡ฐ๐Ÿ‡ท ', label: 'Korean', value: 'ko_kr' }, + { icon: '๐Ÿ‡ฑ๐Ÿ‡ป ', label: 'Latvian', value: 'lv_lv' }, + { icon: '๐Ÿ‡ฑ๐Ÿ‡น ', label: 'Lithuanian', value: 'lt_lt' }, + { icon: '๐Ÿ‡ฒ๐Ÿ‡ฐ ', label: 'Macedonian', value: 'mk_mk' }, + { icon: '๐Ÿ‡ฒ๐Ÿ‡พ ', label: 'Malay', value: 'ms_my' }, + { icon: '๐Ÿ‡ฒ๐Ÿ‡น ', label: 'Maltese', value: 'mt_mt' }, + { icon: '๐Ÿ‡ฒ๐Ÿ‡ณ ', label: 'Mongolian', value: 'mn_mn' }, + { icon: '๐Ÿ‡ณ๐Ÿ‡ต ', label: 'Nepali', value: 'ne_np' }, + { icon: '๐Ÿ‡ณ๐Ÿ‡ด ', label: 'Norwegian', value: 'nb_no' }, + { icon: '๐Ÿ‡ฎ๐Ÿ‡ท ', label: 'Persian', value: 'fa_ir', isRtl: true }, + { icon: '๐Ÿ‡ต๐Ÿ‡ฑ ', label: 'Polish', value: 'pl_pl' }, + { icon: '๐Ÿ‡ง๐Ÿ‡ท ', label: 'Portuguese Brazil', value: 'pt_br' }, + { icon: '๐Ÿ‡ต๐Ÿ‡น ', label: 'Portuguese Portugal', value: 'pt_pt' }, + { icon: '๐Ÿ‡ท๐Ÿ‡ด ', label: 'Romanian', value: 'ro_ro' }, + { icon: '๐Ÿ‡ท๐Ÿ‡บ ', label: 'Russian', value: 'ru_ru' }, + { icon: '๐Ÿ‡ธ๐Ÿ‡ฐ ', label: 'Slovak', value: 'sk_sk' }, + { icon: '๐Ÿ‡ธ๐Ÿ‡ฎ ', label: 'Slovenian', value: 'sl_si' }, + { icon: '๐Ÿ‡ช๐Ÿ‡ธ ', label: 'Spanish', value: 'es_es' }, + { icon: '๐Ÿ‡ฐ๐Ÿ‡ช ', label: 'Swahili', value: 'sw_ke' }, + { icon: '๐Ÿ‡ธ๐Ÿ‡ช ', label: 'Swedish', value: 'sv_se' }, + { icon: '๐Ÿ‡ฎ๐Ÿ‡ณ ', label: 'Tamil', value: 'ta_in' }, + { icon: '๐Ÿ‡น๐Ÿ‡ญ ', label: 'Thai', value: 'th_th' }, + { icon: '๐Ÿ‡น๐Ÿ‡ท ', label: 'Turkish', value: 'tr_tr' }, + { icon: '๐Ÿ‡บ๐Ÿ‡ฆ ', label: 'Ukrainian', value: 'uk_ua' }, + { icon: '๐Ÿ‡ต๐Ÿ‡ฐ ', label: 'Urdu', value: 'ur_pk', isRtl: true }, + { icon: '๐Ÿ‡บ๐Ÿ‡ฟ ', label: 'Uzbek', value: 'uz_uz' }, + { icon: '๐Ÿ‡ป๐Ÿ‡ณ ', label: 'Vietnamese', value: 'vi_vn' }, +]; diff --git a/backend/src/locale/translation/en_us.js b/backend/src/locale/translation/en_us.js index 27f9df51c..0ae0c617c 100644 --- a/backend/src/locale/translation/en_us.js +++ b/backend/src/locale/translation/en_us.js @@ -448,5 +448,5 @@ module.exports = { create_only: 'Create Only', enter_code: 'Enter Code', offers: 'Offers', - proforma_invoices: 'quote', + proforma_invoices: 'Proforma Invoices', }; diff --git a/backend/src/middlewares/rateLimiter.js b/backend/src/middlewares/rateLimiter.js new file mode 100644 index 000000000..4184c626f --- /dev/null +++ b/backend/src/middlewares/rateLimiter.js @@ -0,0 +1,53 @@ + +// const rateLimit = require('express-rate-limit'); +// const { RedisStore } = require('rate-limit-redis'); +// const { createClient } = require('redis'); +// const requestIp = require('request-ip'); + +// const redisClient = createClient({ +// url: 'redis://127.0.0.1:6379', +// }); + +// //docker run -d --name redis-container -p 6379:6379 redis + +// redisClient.on('error', (err) => { +// console.error('Redis Client Error', err); +// }); + +// const connectRedis = async () => { +// try { +// await redisClient.connect(); +// console.log('Redis connected'); +// } catch (err) { +// console.error('Redis connection error:', err); +// process.exit(1); +// } +// }; + +// connectRedis(); + +// const loginLimiter = rateLimit({ +// windowMs: 15 * 60 * 1000, +// max: 5, +// standardHeaders: true, +// legacyHeaders: false, +// store: new RedisStore({ +// sendCommand: (...args) => redisClient.sendCommand(args), +// }), +// handler: (req, res) => { +// console.log(`Rate limit exceeded for IP: ${req.ip}`); +// res.status(429).json({ +// success: false, +// message: 'Too many login attempts. Please try again after 15 minutes.', +// }); +// }, +// }); + +// const logRequest = (req, res, next) => { +// const clientIp = requestIp.getClientIp(req); +// console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl} - IP: ${clientIp}`); +// next(); +// }; + +// // Step 3: Export the rate limiter and logRequest middleware +// module.exports = { loginLimiter, logRequest }; diff --git a/backend/src/models/appModels/Client.js b/backend/src/models/appModels/Client.js index f8407a89f..70357766b 100644 --- a/backend/src/models/appModels/Client.js +++ b/backend/src/models/appModels/Client.js @@ -10,16 +10,24 @@ const schema = new mongoose.Schema({ default: true, }, + type: { + type: String, + default: 'company', + enum: ['company', 'people'], + required: true, + }, name: { type: String, required: true, }, - phone: String, - country: String, - address: String, - email: String, + company: { type: mongoose.Schema.ObjectId, ref: 'Company', autopopulate: true }, + people: { type: mongoose.Schema.ObjectId, ref: 'People', autopopulate: true }, + convertedFrom: { type: mongoose.Schema.ObjectId, ref: 'Lead' }, + interestedIn: [{ type: mongoose.Schema.ObjectId, ref: 'Product' }], createdBy: { type: mongoose.Schema.ObjectId, ref: 'Admin' }, assigned: { type: mongoose.Schema.ObjectId, ref: 'Admin' }, + source: String, + category: String, created: { type: Date, default: Date.now, diff --git a/backend/src/models/appModels/Company.js b/backend/src/models/appModels/Company.js new file mode 100644 index 000000000..dc98c565c --- /dev/null +++ b/backend/src/models/appModels/Company.js @@ -0,0 +1,217 @@ +const mongoose = require('mongoose'); + +const schema = new mongoose.Schema({ + removed: { + type: Boolean, + default: false, + }, + enabled: { + type: Boolean, + default: true, + }, + + name: { + type: String, + trim: true, + required: true, + }, + legalName: { + type: String, + trim: true, + }, + hasParentCompany: { + type: Boolean, + default: false, + }, + parentCompany: { + type: mongoose.Schema.ObjectId, + ref: 'Company', + }, + isClient: { + type: Boolean, + default: false, + }, + peoples: [{ type: mongoose.Schema.ObjectId, ref: 'People', autopopulate: true }], + mainContact: { type: mongoose.Schema.ObjectId, ref: 'People', autopopulate: true }, + icon: { + type: String, + trim: true, + }, + logo: { + type: String, + trim: true, + }, + imageHeader: String, + bankName: { + type: String, + trim: true, + }, + bankIban: { + type: String, + trim: true, + }, + bankSwift: { + type: String, + trim: true, + }, + bankNumber: { + type: String, + trim: true, + }, + bankRouting: { + type: String, + trim: true, + }, + bankCountry: { + type: String, + trim: true, + }, + companyRegNumber: { + type: String, + trim: true, + }, + companyTaxNumber: { + type: String, + trim: true, + }, + companyTaxId: { + type: String, + trim: true, + }, + companyRegId: { + type: String, + trim: true, + }, + securitySocialNbr: String, + customField: [ + { + fieldName: { + type: String, + trim: true, + lowercase: true, + }, + fieldType: { + type: String, + trim: true, + lowercase: true, + default: 'string', + }, + fieldValue: {}, + }, + ], + location: { + latitude: Number, + longitude: Number, + }, + address: { + type: String, + }, + city: { + type: String, + }, + State: { + type: String, + }, + postalCode: { + type: Number, + }, + country: { + type: String, + trim: true, + }, + phone: { + type: String, + trim: true, + }, + otherPhone: [ + { + type: String, + trim: true, + }, + ], + fax: { + type: String, + trim: true, + }, + email: { + type: String, + trim: true, + lowercase: true, + }, + otherEmail: [ + { + type: String, + trim: true, + lowercase: true, + }, + ], + website: { + type: String, + trim: true, + lowercase: true, + }, + socialMedia: { + facebook: String, + instagram: String, + twitter: String, + linkedin: String, + tiktok: String, + youtube: String, + snapchat: String, + }, + images: [ + { + id: String, + name: String, + path: String, + description: String, + isPublic: { + type: Boolean, + default: false, + }, + }, + ], + files: [ + { + id: String, + name: String, + path: String, + description: String, + isPublic: { + type: Boolean, + default: false, + }, + }, + ], + category: String, + approved: { + type: Boolean, + default: true, + }, + verified: { + type: Boolean, + }, + notes: String, + tags: [ + { + type: String, + trim: true, + lowercase: true, + }, + ], + created: { + type: Date, + default: Date.now, + }, + updated: { + type: Date, + default: Date.now, + }, + isPublic: { + type: Boolean, + default: false, + }, +}); + +schema.plugin(require('mongoose-autopopulate')); +module.exports = mongoose.model('Company', schema); diff --git a/backend/src/models/appModels/Employee.js b/backend/src/models/appModels/Employee.js new file mode 100644 index 000000000..645f3fa7a --- /dev/null +++ b/backend/src/models/appModels/Employee.js @@ -0,0 +1,186 @@ +const mongoose = require('mongoose'); + +const employeeSchema = new mongoose.Schema({ + removed: { + type: Boolean, + default: false, + }, + enabled: { + type: Boolean, + default: true, + }, + + isAdmin: { type: mongoose.Schema.ObjectId, ref: 'Admin' }, + firstname: { + type: String, + trim: true, + required: true, + }, + lastname: { + type: String, + trim: true, + required: true, + }, + birthplace: String, + gender: String, + idCardNumber: { + type: String, + trim: true, + }, + idCardType: String, + birthday: Date, + securitySocialNbr: String, + taxNumber: String, + nationality: { + type: String, + trim: true, + }, + photo: { + type: String, + trim: true, + }, + headerImage: { + type: String, + trim: true, + }, + + bankName: { + type: String, + trim: true, + }, + bankIban: { + type: String, + trim: true, + }, + bankSwift: { + type: String, + trim: true, + }, + bankNumber: { + type: String, + trim: true, + }, + bankRouting: { + type: String, + trim: true, + }, + customField: [ + { + fieldName: { + type: String, + trim: true, + lowercase: true, + }, + fieldType: { + type: String, + trim: true, + lowercase: true, + default: 'string', + }, + fieldValue: {}, + }, + ], + location: { + latitude: Number, + longitude: Number, + }, + address: { + type: String, + }, + city: { + type: String, + }, + State: { + type: String, + }, + postalCode: { + type: Number, + }, + country: { + type: String, + trim: true, + }, + phone: { + type: String, + trim: true, + }, + otherPhone: [ + { + type: String, + trim: true, + }, + ], + email: { + type: String, + trim: true, + lowercase: true, + }, + + otherEmail: [ + { + type: String, + trim: true, + lowercase: true, + }, + ], + socialMedia: { + facebook: String, + instagram: String, + twitter: String, + linkedin: String, + tiktok: String, + youtube: String, + snapchat: String, + }, + images: [ + { + id: String, + name: String, + path: String, + description: String, + isPublic: { + type: Boolean, + default: false, + }, + }, + ], + files: [ + { + id: String, + name: String, + path: String, + description: String, + isPublic: { + type: Boolean, + default: false, + }, + }, + ], + notes: String, + category: String, + status: String, + approved: { + type: Boolean, + }, + verified: { + type: Boolean, + }, + tags: [ + { + type: String, + trim: true, + lowercase: true, + }, + ], + created: { + type: Date, + default: Date.now, + }, + updated: { + type: Date, + default: Date.now, + }, +}); +employeeSchema.plugin(require('mongoose-autopopulate')); + +module.exports = mongoose.model('Employee', employeeSchema); diff --git a/backend/src/models/appModels/Expense.js b/backend/src/models/appModels/Expense.js new file mode 100644 index 000000000..e19f863da --- /dev/null +++ b/backend/src/models/appModels/Expense.js @@ -0,0 +1,98 @@ +const mongoose = require('mongoose'); + +const schema = new mongoose.Schema({ + removed: { + type: Boolean, + default: false, + }, + + date: { + type: Date, + default: Date.now, + }, + name: { + type: String, + trim: true, + required: true, + }, + description: { + type: String, + }, + ref: { + type: String, + trim: true, + }, + recurring: { + type: String, + enum: ['daily', 'weekly', 'monthly', 'annually', 'quarter'], + }, + supplier: { + type: mongoose.Schema.ObjectId, + autopopulate: true, + }, + expenseCategory: { + type: mongoose.Schema.ObjectId, + ref: 'ExpenseCategory', + autopopulate: true, + required: true, + }, + taxRate: { + type: Number, + }, + subTotal: { + type: Number, + }, + taxTotal: { + type: Number, + }, + total: { + type: Number, + }, + currency: { + type: String, + default: 'NA', + uppercase: true, + required: true, + }, + paymentMode: { + type: mongoose.Schema.ObjectId, + ref: 'PaymentMode', + autopopulate: true, + }, + receipt: String, + images: [ + { + id: String, + name: String, + path: String, + description: String, + isPublic: { + type: Boolean, + default: false, + }, + }, + ], + files: [ + { + id: String, + name: String, + path: String, + description: String, + isPublic: { + type: Boolean, + default: false, + }, + }, + ], + updated: { + type: Date, + default: Date.now, + }, + created: { + type: Date, + default: Date.now, + }, +}); + +schema.plugin(require('mongoose-autopopulate')); +module.exports = mongoose.model('Expense', schema); diff --git a/backend/src/models/appModels/ExpenseCategory.js b/backend/src/models/appModels/ExpenseCategory.js new file mode 100644 index 000000000..903b7a4de --- /dev/null +++ b/backend/src/models/appModels/ExpenseCategory.js @@ -0,0 +1,39 @@ +const mongoose = require('mongoose'); + +const expenseCategorySchema = new mongoose.Schema({ + removed: { + type: Boolean, + default: false, + }, + enabled: { + type: Boolean, + default: true, + }, + + name: { + type: String, + trim: true, + required: true, + }, + description: { + type: String, + trim: true, + required: true, + }, + color: { + type: String, + lowercase: true, + trim: true, + required: true, + }, + created: { + type: Date, + default: Date.now, + }, + updated: { + type: Date, + default: Date.now, + }, +}); + +module.exports = mongoose.model('ExpenseCategory', expenseCategorySchema); diff --git a/backend/src/models/appModels/Lead.js b/backend/src/models/appModels/Lead.js new file mode 100644 index 000000000..d8b16bab5 --- /dev/null +++ b/backend/src/models/appModels/Lead.js @@ -0,0 +1,92 @@ +const mongoose = require('mongoose'); + +const schema = new mongoose.Schema({ + removed: { + type: Boolean, + default: false, + }, + enabled: { + type: Boolean, + default: true, + }, + + type: { + type: String, + default: 'company', + enum: ['company', 'people'], + required: true, + }, + name: { + type: String, + required: true, + }, + company: { type: mongoose.Schema.ObjectId, ref: 'Company', autopopulate: true }, + people: { type: mongoose.Schema.ObjectId, ref: 'People', autopopulate: true }, + interestedIn: [{ type: mongoose.Schema.ObjectId, ref: 'Product' }], + offer: [{ type: mongoose.Schema.ObjectId, ref: 'Offer' }], + converted: { type: Boolean, default: false }, + createdBy: { type: mongoose.Schema.ObjectId, ref: 'Admin' }, + assigned: { type: mongoose.Schema.ObjectId, ref: 'Admin' }, + subTotal: { + type: Number, + }, + taxTotal: { + type: Number, + }, + total: { + type: Number, + }, + discount: { + type: Number, + }, + images: [ + { + id: String, + name: String, + path: String, + description: String, + isPublic: { + type: Boolean, + default: false, + }, + }, + ], + files: [ + { + id: String, + name: String, + path: String, + description: String, + isPublic: { + type: Boolean, + default: false, + }, + }, + ], + category: String, + status: String, + notes: String, + source: String, + approved: { + type: Boolean, + default: false, + }, + tags: [ + { + type: String, + trim: true, + lowercase: true, + }, + ], + created: { + type: Date, + default: Date.now, + }, + updated: { + type: Date, + default: Date.now, + }, +}); + +schema.plugin(require('mongoose-autopopulate')); +module.exports = mongoose.model('Lead', schema); diff --git a/backend/src/models/appModels/Offer.js b/backend/src/models/appModels/Offer.js new file mode 100644 index 000000000..e4c71a107 --- /dev/null +++ b/backend/src/models/appModels/Offer.js @@ -0,0 +1,135 @@ +const mongoose = require('mongoose'); + +const offerSchema = new mongoose.Schema({ + removed: { + type: Boolean, + default: false, + }, + + createdBy: { type: mongoose.Schema.ObjectId, ref: 'Admin', required: true }, + converted: { + type: Boolean, + default: false, + }, + number: { + type: Number, + required: true, + }, + year: { + type: Number, + required: true, + }, + content: String, + date: { + type: Date, + required: true, + }, + lead: { + type: mongoose.Schema.ObjectId, + ref: 'Lead', + required: true, + autopopulate: true, + }, + items: [ + { + itemName: { + type: String, + required: true, + }, + description: { + type: String, + }, + quantity: { + type: Number, + required: true, + }, + price: { + type: Number, + required: true, + }, + // taxRate: { + // type: Number, + // default: 0, + // }, + // subTotal: { + // type: Number, + // default: 0, + // }, + // taxTotal: { + // type: Number, + // default: 0, + // }, + total: { + type: Number, + required: true, + }, + }, + ], + currency: { + type: String, + default: 'NA', + uppercase: true, + required: true, + }, + taxRate: { + type: Number, + }, + subTotal: { + type: Number, + }, + subOfferTotal: { + type: Number, + }, + taxTotal: { + type: Number, + }, + total: { + type: Number, + }, + discount: { + type: Number, + default: 0, + }, + notes: { + type: String, + }, + status: { + type: String, + enum: ['draft', 'pending', 'sent', 'accepted', 'declined', 'cancelled', 'on hold'], + default: 'draft', + }, + approved: { + type: Boolean, + default: false, + }, + isExpired: { + type: Boolean, + default: false, + }, + pdf: { + type: String, + }, + files: [ + { + id: String, + name: String, + path: String, + description: String, + isPublic: { + type: Boolean, + default: true, + }, + }, + ], + updated: { + type: Date, + default: Date.now, + }, + created: { + type: Date, + default: Date.now, + }, +}); + +offerSchema.plugin(require('mongoose-autopopulate')); +module.exports = mongoose.model('Offer', offerSchema); diff --git a/backend/src/models/appModels/Order.js b/backend/src/models/appModels/Order.js new file mode 100644 index 000000000..6b145be9c --- /dev/null +++ b/backend/src/models/appModels/Order.js @@ -0,0 +1,140 @@ +const mongoose = require('mongoose'); + +const orderSchema = new mongoose.Schema({ + removed: { + type: Boolean, + default: false, + }, + enabled: { + type: Boolean, + default: true, + }, + + createdBy: { + type: mongoose.Schema.ObjectId, + ref: 'Admin', + }, + + assigned: { + type: mongoose.Schema.ObjectId, + ref: 'Employee', + }, + number: { + type: Number, + required: true, + }, + recurring: { + type: String, + enum: ['daily', 'weekly', 'monthly', 'annually', 'quarter'], + }, + date: { + type: Date, + default: Date.now, + required: true, + }, + client: { + type: mongoose.Schema.ObjectId, + ref: 'Client', + required: true, + autopopulate: true, + }, + invoice: { + type: mongoose.Schema.ObjectId, + ref: 'Ivoince', + autopopulate: true, + }, + items: [ + { + product: { + type: mongoose.Schema.ObjectId, + ref: 'Product', + required: true, + }, + itemName: { + type: String, + required: true, + }, + description: { + type: String, + }, + quantity: { + type: Number, + default: 1, + required: true, + }, + price: { + type: Number, + required: true, + }, + discount: { + type: Number, + default: 0, + }, + // taxRate: { + // type: Number, + // default: 0, + // }, + // subTotal: { + // type: Number, + // default: 0, + // }, + // taxTotal: { + // type: Number, + // default: 0, + // }, + total: { + type: Number, + }, + notes: { + type: String, + }, + }, + ], + shipment: { + type: mongoose.Schema.ObjectId, + ref: 'Shipment', + }, + approved: { + type: Boolean, + default: false, + }, + notes: { + type: String, + }, + fulfillment: { + type: String, + enum: ['pending', 'in review', 'processing', 'packing', 'shipped', 'on hold', 'cancelled'], + default: 'pending', + }, + status: { + type: String, + enum: [ + 'not started', + 'in progress', + 'delayed', + 'completed', + 'delivered', + 'returned', + 'cancelled', + 'on hold', + 'refunded', + ], + default: 'not started', + }, + processingStatus: String, + pdf: { + type: String, + }, + updated: { + type: Date, + default: Date.now, + }, + created: { + type: Date, + default: Date.now, + }, +}); + +orderSchema.plugin(require('mongoose-autopopulate')); + +module.exports = mongoose.model('Order', orderSchema); diff --git a/backend/src/models/appModels/People.js b/backend/src/models/appModels/People.js new file mode 100644 index 000000000..63136629f --- /dev/null +++ b/backend/src/models/appModels/People.js @@ -0,0 +1,203 @@ +const mongoose = require('mongoose'); + +const schema = new mongoose.Schema({ + removed: { + type: Boolean, + default: false, + }, + enabled: { + type: Boolean, + default: true, + }, + + firstname: { + type: String, + trim: true, + required: true, + }, + lastname: { + type: String, + trim: true, + required: true, + }, + isClient: { + type: Boolean, + default: false, + }, + company: { type: mongoose.Schema.ObjectId, ref: 'Company' }, + bio: String, + idCardNumber: { + type: String, + trim: true, + }, + idCardType: { + type: String, + }, + securitySocialNbr: { + type: String, + }, + taxNumber: { + type: String, + }, + birthday: { + type: Date, + }, + birthplace: { + type: String, + }, + gender: { + type: String, + enum: ['male', 'female'], + }, + photo: { + type: String, + }, + bankName: { + type: String, + trim: true, + }, + bankIban: { + type: String, + trim: true, + }, + bankSwift: { + type: String, + trim: true, + }, + bankNumber: { + type: String, + trim: true, + }, + bankRouting: { + type: String, + trim: true, + }, + customField: [ + { + fieldName: { + type: String, + trim: true, + lowercase: true, + }, + fieldType: { + type: String, + trim: true, + lowercase: true, + default: 'string', + }, + fieldValue: {}, + }, + ], + location: { + latitude: Number, + longitude: Number, + }, + address: { + type: String, + }, + city: { + type: String, + }, + State: { + type: String, + }, + postalCode: { + type: Number, + }, + country: { + type: String, + trim: true, + }, + phone: { + type: String, + trim: true, + }, + otherPhone: [ + { + type: String, + trim: true, + }, + ], + email: { + type: String, + trim: true, + lowercase: true, + }, + + otherEmail: [ + { + type: String, + trim: true, + lowercase: true, + }, + ], + socialMedia: { + facebook: String, + instagram: String, + twitter: String, + linkedin: String, + tiktok: String, + youtube: String, + snapchat: String, + }, + website: { + type: String, + trim: true, + lowercase: true, + }, + images: [ + { + id: String, + name: String, + path: String, + description: String, + isPublic: { + type: Boolean, + default: false, + }, + }, + ], + files: [ + { + id: String, + name: String, + path: String, + description: String, + isPublic: { + type: Boolean, + default: false, + }, + }, + ], + notes: String, + category: String, + status: String, + approved: { + type: Boolean, + }, + verified: { + type: Boolean, + }, + tags: [ + { + type: String, + trim: true, + lowercase: true, + }, + ], + created: { + type: Date, + default: Date.now, + }, + updated: { + type: Date, + default: Date.now, + }, + isPublic: { + type: Boolean, + default: false, + }, +}); + +schema.plugin(require('mongoose-autopopulate')); +module.exports = mongoose.model('People', schema); diff --git a/backend/src/models/appModels/Product.js b/backend/src/models/appModels/Product.js new file mode 100644 index 000000000..8e1f8fb19 --- /dev/null +++ b/backend/src/models/appModels/Product.js @@ -0,0 +1,102 @@ +const mongoose = require('mongoose'); + +const schema = new mongoose.Schema({ + removed: { + type: Boolean, + default: false, + }, + enabled: { + type: Boolean, + default: true, + }, + + productCategory: { + type: mongoose.Schema.ObjectId, + ref: 'ProductCategory', + required: true, + autopopulate: true, + }, + suppliers: [{ type: mongoose.Schema.ObjectId, ref: 'Supplier' }], + name: { + type: String, + required: true, + }, + description: String, + number: { + type: Number, + }, + title: String, + tags: [String], + headerImage: String, + photo: String, + images: [ + { + id: String, + name: String, + path: String, + description: String, + isPublic: { + type: Boolean, + default: false, + }, + }, + ], + files: [ + { + id: String, + name: String, + path: String, + description: String, + isPublic: { + type: Boolean, + default: false, + }, + }, + ], + priceBeforeTax: { + type: Number, + }, + taxRate: { type: Number, default: 0 }, + price: { + type: Number, + required: true, + }, + currency: { + type: String, + default: 'NA', + uppercase: true, + required: true, + }, + customField: [ + { + fieldName: { + type: String, + trim: true, + lowercase: true, + }, + fieldType: { + type: String, + trim: true, + lowercase: true, + default: 'string', + }, + fieldValue: {}, + }, + ], + created: { + type: Date, + default: Date.now, + }, + updated: { + type: Date, + default: Date.now, + }, + isPublic: { + type: Boolean, + default: true, + }, +}); + +schema.plugin(require('mongoose-autopopulate')); + +module.exports = mongoose.model('Product', schema); diff --git a/backend/src/models/appModels/ProductCategory.js b/backend/src/models/appModels/ProductCategory.js new file mode 100644 index 000000000..2e26c19b0 --- /dev/null +++ b/backend/src/models/appModels/ProductCategory.js @@ -0,0 +1,76 @@ +const mongoose = require('mongoose'); + +const productCategorySchema = new mongoose.Schema({ + removed: { + type: Boolean, + default: false, + }, + enabled: { + type: Boolean, + default: true, + }, + + name: { + type: String, + required: true, + }, + description: String, + color: { + type: String, + lowercase: true, + trim: true, + required: true, + }, + hasParentCategory: { + type: Boolean, + default: false, + }, + parentCategory: { + type: mongoose.Schema.ObjectId, + ref: 'ProductCategory', + }, + + title: String, + tags: [String], + icon: String, + headerImage: String, + photo: String, + images: [ + { + id: String, + name: String, + path: String, + description: String, + isPublic: { + type: Boolean, + default: false, + }, + }, + ], + files: [ + { + id: String, + name: String, + path: String, + description: String, + isPublic: { + type: Boolean, + default: false, + }, + }, + ], + created: { + type: Date, + default: Date.now, + }, + updated: { + type: Date, + default: Date.now, + }, + isPublic: { + type: Boolean, + default: true, + }, +}); + +module.exports = mongoose.model('ProductCategory', productCategorySchema); diff --git a/backend/src/models/appModels/Purchase.js b/backend/src/models/appModels/Purchase.js new file mode 100644 index 000000000..bfc8632d1 --- /dev/null +++ b/backend/src/models/appModels/Purchase.js @@ -0,0 +1,153 @@ +const mongoose = require('mongoose'); + +const purchaseSchema = new mongoose.Schema({ + removed: { + type: Boolean, + default: false, + }, + createdBy: { type: mongoose.Schema.ObjectId, ref: 'Admin', required: true }, + + number: { + type: Number, + required: true, + }, + year: { + type: Number, + required: true, + }, + content: String, + recurring: { + type: String, + enum: ['daily', 'weekly', 'monthly', 'annually', 'quarter'], + }, + date: { + type: Date, + required: true, + }, + expiredDate: { + type: Date, + required: true, + }, + supplier: { + type: mongoose.Schema.ObjectId, + ref: 'Supplier', + required: true, + autopopulate: true, + }, + items: [ + { + product: { + type: mongoose.Schema.ObjectId, + ref: 'Product', + }, + quantity: { + type: Number, + default: 1, + required: true, + }, + price: { + type: Number, + required: true, + }, + taxRate: { + type: Number, + default: 0, + }, + subTotal: { + type: Number, + default: 0, + }, + taxTotal: { + type: Number, + default: 0, + }, + total: { + type: Number, + required: true, + }, + }, + ], + currency: { + type: String, + default: 'NA', + uppercase: true, + required: true, + }, + taxRate: { + type: Number, + default: 0, + }, + subTotal: { + type: Number, + default: 0, + }, + taxTotal: { + type: Number, + default: 0, + }, + total: { + type: Number, + default: 0, + }, + credit: { + type: Number, + default: 0, + }, + discount: { + type: Number, + default: 0, + }, + expense: [ + { + type: mongoose.Schema.ObjectId, + ref: 'Expense', + }, + ], + paymentStatus: { + type: String, + default: 'unpaid', + enum: ['unpaid', 'paid', 'partially'], + }, + isOverdue: { + type: Boolean, + default: false, + }, + approved: { + type: Boolean, + default: false, + }, + notes: { + type: String, + }, + status: { + type: String, + enum: ['draft', 'pending', 'ordred', 'received', 'refunded', 'cancelled', 'on hold'], + default: 'draft', + }, + pdf: { + type: String, + }, + files: [ + { + id: String, + name: String, + path: String, + description: String, + isPublic: { + type: Boolean, + default: true, + }, + }, + ], + updated: { + type: Date, + default: Date.now, + }, + created: { + type: Date, + default: Date.now, + }, +}); + +purchaseSchema.plugin(require('mongoose-autopopulate')); +module.exports = mongoose.model('Purchase', purchaseSchema); diff --git a/backend/src/models/appModels/Shipment.js b/backend/src/models/appModels/Shipment.js new file mode 100644 index 000000000..6829f18d9 --- /dev/null +++ b/backend/src/models/appModels/Shipment.js @@ -0,0 +1,119 @@ +const mongoose = require('mongoose'); + +const ShipmentSchema = new mongoose.Schema({ + removed: { + type: Boolean, + default: false, + }, + + createdBy: { + type: mongoose.Schema.ObjectId, + ref: 'Admin', + required: true, + }, + assigned: { + type: mongoose.Schema.ObjectId, + ref: 'Employee', + }, + order: { + type: mongoose.Schema.ObjectId, + ref: 'Order', + }, + carrier: { + type: String, + required: true, + }, + trackingNmber: String, + trackingLink: String, + date: { + type: Date, + required: true, + }, + estimatedDeliveryDate: { + type: Date, + }, + client: { + type: mongoose.Schema.ObjectId, + ref: 'Client', + }, + invoice: { + type: mongoose.Schema.ObjectId, + ref: 'Ivoince', + }, + recipient: { + name: { + type: String, + required: true, + }, + address: { + type: String, + required: true, + }, + city: { + type: String, + required: true, + }, + state: { + type: String, + }, + country: { + type: String, + required: true, + }, + postalCode: { + type: String, + required: true, + }, + phone: { + type: String, + }, + }, + products: [ + { + productId: { + type: mongoose.Schema.ObjectId, + ref: 'Product', + }, + quantity: { + type: String, + required: true, + }, + }, + ], + approved: { + type: Boolean, + default: false, + }, + notes: { + type: String, + }, + status: { + type: String, + enum: [ + 'pending', + 'confirmed', + 'in transit', + 'out for delivery', + 'delivered', + 'returned', + 'failed', + 'cancelled', + ], + default: 'pending', + }, + pdf: { + type: String, + }, + updated: { + type: Date, + default: Date.now, + }, + created: { + type: Date, + default: Date.now, + }, +}); + +ShipmentSchema.plugin(require('mongoose-autopopulate')); + +module.exports = mongoose.model('Shipment', ShipmentSchema); diff --git a/backend/src/models/coreModels/Email.js b/backend/src/models/coreModels/Email.js new file mode 100644 index 000000000..56a9acac0 --- /dev/null +++ b/backend/src/models/coreModels/Email.js @@ -0,0 +1,47 @@ +const mongoose = require('mongoose'); + +const emailSchema = new mongoose.Schema({ + removed: { + type: Boolean, + default: false, + }, + enabled: { + type: Boolean, + default: true, + }, + + emailKey: { + type: String, + lowercase: true, + required: true, + }, + emailName: { + type: String, + required: true, + }, + emailVariables: { + type: Array, + }, + emailBody: { + type: String, + required: true, + }, + emailSubject: { + type: String, + required: true, + }, + language: { + type: String, + default: 'us_en', + }, + created: { + type: Date, + default: Date.now, + }, + updated: { + type: Date, + default: Date.now, + }, +}); + +module.exports = mongoose.model('Email', emailSchema); diff --git a/backend/src/routes/appRoutes/appApi.js b/backend/src/routes/appRoutes/appApi.js index 9d323c5c5..3c2c9d293 100644 --- a/backend/src/routes/appRoutes/appApi.js +++ b/backend/src/routes/appRoutes/appApi.js @@ -16,7 +16,7 @@ const routerApp = (entity, controller) => { router.route(`/${entity}/filter`).get(catchErrors(controller['filter'])); router.route(`/${entity}/summary`).get(catchErrors(controller['summary'])); - if (entity === 'invoice' || entity === 'quote' || entity === 'payment') { + if (entity === 'invoice' || entity === 'quote' || entity === 'offer' || entity === 'payment') { router.route(`/${entity}/mail`).post(catchErrors(controller['mail'])); } diff --git a/backend/src/routes/coreRoutes/coreApi.js b/backend/src/routes/coreRoutes/coreApi.js index f60afa2be..38276d418 100644 --- a/backend/src/routes/coreRoutes/coreApi.js +++ b/backend/src/routes/coreRoutes/coreApi.js @@ -6,6 +6,7 @@ const router = express.Router(); const adminController = require('@/controllers/coreControllers/adminController'); const settingController = require('@/controllers/coreControllers/settingController'); +const emailController = require('@/controllers/coreControllers/emailController'); const { singleStorageUpload } = require('@/middlewares/uploadMiddleware'); @@ -51,4 +52,14 @@ router catchErrors(settingController.updateBySettingKey) ); router.route('/setting/updateManySetting').patch(catchErrors(settingController.updateManySetting)); + +// //____________________________________________ API for Email Templates _________________ +router.route('/email/create').post(catchErrors(emailController.create)); +router.route('/email/read/:id').get(catchErrors(emailController.read)); +router.route('/email/update/:id').patch(catchErrors(emailController.update)); +router.route('/email/search').get(catchErrors(emailController.search)); +router.route('/email/list').get(catchErrors(emailController.list)); +router.route('/email/listAll').get(catchErrors(emailController.listAll)); +router.route('/email/filter').get(catchErrors(emailController.filter)); + module.exports = router; diff --git a/backend/src/routes/coreRoutes/coreAuth.js b/backend/src/routes/coreRoutes/coreAuth.js index 2bda27d4d..88a7e2881 100644 --- a/backend/src/routes/coreRoutes/coreAuth.js +++ b/backend/src/routes/coreRoutes/coreAuth.js @@ -4,9 +4,12 @@ const router = express.Router(); const { catchErrors } = require('@/handlers/errorHandlers'); const adminAuth = require('@/controllers/coreControllers/adminAuth'); +// const {loginLimiter, logRequest} = require('@/middlewares/rateLimiter'); -router.route('/login').post(catchErrors(adminAuth.login)); +// router.use(logRequest); +router.route('/login').post(catchErrors(adminAuth.login)); +router.route('/register').post(catchErrors(adminAuth.register)); router.route('/forgetpassword').post(catchErrors(adminAuth.forgetPassword)); router.route('/resetpassword').post(catchErrors(adminAuth.resetPassword)); diff --git a/backend/src/routes/coreRoutes/corePublicRouter.js b/backend/src/routes/coreRoutes/corePublicRouter.js index 22433b4ea..8b02f7fcb 100644 --- a/backend/src/routes/coreRoutes/corePublicRouter.js +++ b/backend/src/routes/coreRoutes/corePublicRouter.js @@ -8,10 +8,9 @@ const path = require('path'); router.route('/:subPath/:directory/:file').get(function (req, res) { try { const { subPath, directory, file } = req.params; - //subPath sanitization check - sanitizedPath = path.normalize(subPath).replace(/^(\.\.[\/\\])+/, ''); + const options = { - root: path.join(__dirname, `../../public/${sanitizedPath}/${directory}`), + root: path.join(__dirname, `../../public/${subPath}/${directory}`), }; const fileName = file; return res.sendFile(fileName, options, function (error) { diff --git a/backend/src/server.js b/backend/src/server.js index e7b62ba1e..99eed0278 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -3,35 +3,43 @@ const mongoose = require('mongoose'); const { globSync } = require('glob'); const path = require('path'); -// Make sure we are running node 7.6+ +// Make sure we are running Node.js 20 or greater const [major, minor] = process.versions.node.split('.').map(parseFloat); if (major < 20) { - console.log('Please upgrade your node.js version at least 20 or greater. ๐Ÿ‘Œ\n '); + console.log('Please upgrade your node.js version to at least 20 or greater. ๐Ÿ‘Œ\n '); process.exit(); } -// import environmental variables from our variables.env file +// Import environmental variables from .env files require('dotenv').config({ path: '.env' }); require('dotenv').config({ path: '.env.local' }); +// Connect to MongoDB mongoose.connect(process.env.DATABASE); +// Function to connect to Redis + + +// Define your OpenAI API key const OPENAI_API_KEY = process.env.OPENAI_API_KEY; +// MongoDB connection error handling mongoose.connection.on('error', (error) => { console.log( - `1. ๐Ÿ”ฅ Common Error caused issue โ†’ : check your .env file first and add your mongodb url` + `1. ๐Ÿ”ฅ Common Error caused issue โ†’ : check your .env file first and add your MongoDB URL` ); console.error(`2. ๐Ÿšซ Error โ†’ : ${error.message}`); }); -const modelsFiles = globSync('./src/models/**/*.js'); +// Connect to Redis +// Load all Mongoose models +const modelsFiles = globSync('./src/models/**/*.js'); for (const filePath of modelsFiles) { require(path.resolve(filePath)); } -// Start our app! +// Start the Express app const app = require('./app'); app.set('port', process.env.PORT || 8888); const server = app.listen(app.get('port'), () => { diff --git a/backend/src/setup/defaultSettings/emailSettings.json b/backend/src/setup/defaultSettings/emailSettings.json new file mode 100644 index 000000000..81fbaff88 --- /dev/null +++ b/backend/src/setup/defaultSettings/emailSettings.json @@ -0,0 +1,20 @@ +[ + { + "settingCategory": "email_settings", + "settingKey": "email_domain", + "settingValue": "idurarapp.com", + "valueType": "string" + }, + { + "settingCategory": "email_settings", + "settingKey": "email_reply_to", + "settingValue": "reply@idurarapp.com", + "valueType": "string" + }, + { + "settingCategory": "email_settings", + "settingKey": "email_from", + "settingValue": "IDURAR ERP CRM", + "valueType": "string" + } +] diff --git a/backend/src/setup/defaultSettings/inventorySettings.json b/backend/src/setup/defaultSettings/inventorySettings.json new file mode 100644 index 000000000..2715b79f7 --- /dev/null +++ b/backend/src/setup/defaultSettings/inventorySettings.json @@ -0,0 +1,38 @@ +[ + { + "settingCategory": "inventory_settings", + "settingKey": "last_order_number", + "settingValue": 0, + "valueType": "number" + }, + { + "settingCategory": "inventory_settings", + "settingKey": "order_number_length", + "settingValue": 13, + "valueType": "number" + }, + { + "settingCategory": "inventory_settings", + "settingKey": "order_number_type", + "settingValue": "date_uniqueid", + "valueType": "string" + }, + { + "settingCategory": "inventory_settings", + "settingKey": "product_number_type", + "settingValue": "barcode", + "valueType": "string" + }, + { + "settingCategory": "inventory_settings", + "settingKey": "last_product_number", + "settingValue": 0, + "valueType": "number" + }, + { + "settingCategory": "inventory_settings", + "settingKey": "generate_product_number", + "settingValue": false, + "valueType": "boolean" + } +] diff --git a/backend/src/setup/defaultSettings/leadSettings.json b/backend/src/setup/defaultSettings/leadSettings.json new file mode 100644 index 000000000..3026c4845 --- /dev/null +++ b/backend/src/setup/defaultSettings/leadSettings.json @@ -0,0 +1,85 @@ +[ + { + "settingCategory": "lead_settings", + "settingKey": "lead_type", + "settingValue": ["person", "company"], + "valueType": "array" + }, + { + "settingCategory": "lead_settings", + "settingKey": "lead_source", + "settingValue": [ + "self checking", + "sales lead", + "recomendation", + "facebook", + "instagram", + "tiktok", + "youtube", + "blog", + "linkedin", + "newsletter", + "website", + "twitter" + ], + "valueType": "array" + }, + { + "settingCategory": "lead_settings", + "settingKey": "lead_status", + "settingValue": ["draft", "new", "reached", "waiting", "in negosation", "won", "loose"], + "valueType": "array" + }, + { + "settingCategory": "lead_settings", + "settingKey": "lead_category", + "settingValue": [ + "Corporate", + "person", + "startup", + "small company", + "services business", + "retails", + "cafe & restaurant" + ], + "valueType": "array" + }, + { + "settingCategory": "lead_settings", + "settingKey": "offer_default_lead_type", + "settingValue": "company", + "valueType": "string" + }, + { + "settingCategory": "lead_settings", + "settingKey": "lead_email_imap_server", + "settingValue": "mail.idurarapp.com", + "valueType": "string", + + "isPrivate": true + }, + { + "settingCategory": "lead_settings", + "settingKey": "lead_email_imap_username", + "settingValue": "hello@idurarapp.com", + "valueType": "string", + + "isPrivate": true + }, + { + "settingCategory": "lead_settings", + "settingKey": "lead_email_imap_password", + "settingValue": "password", + "valueType": "string", + + "isPrivate": true + }, + { + "settingCategory": "lead_settings", + "settingKey": "lead_email_imap_port", + "settingValue": 993, + "valueType": "number", + + "isPrivate": true + } +] diff --git a/backend/src/setup/defaultSettings/offerSettings.json b/backend/src/setup/defaultSettings/offerSettings.json new file mode 100644 index 000000000..b1f44f456 --- /dev/null +++ b/backend/src/setup/defaultSettings/offerSettings.json @@ -0,0 +1,34 @@ +[ + { + "settingCategory": "offer_settings", + "settingKey": "offer_show_product_tax", + "settingValue": false, + "valueType": "boolean" + }, + { + "settingCategory": "offer_settings", + "settingKey": "offer_load_default_client", + "settingValue": false, + "valueType": "boolean" + }, + { + "settingCategory": "offer_settings", + "settingKey": "offer_status", + "settingValue": [ + "draft", + "pending", + "sent", + "negotiation", + "accepted", + "declined", + "cancelled" + ], + "valueType": "array" + }, + { + "settingCategory": "offer_settings", + "settingKey": "offer_pdf_footer", + "settingValue": "Offer was created on a computer and is valid without the signature and seal", + "valueType": "string" + } +] diff --git a/backend/src/setup/emailTemplate/index.json b/backend/src/setup/emailTemplate/index.json new file mode 100644 index 000000000..9e2219bb1 --- /dev/null +++ b/backend/src/setup/emailTemplate/index.json @@ -0,0 +1,51 @@ +[ + { + "emailKey": "email_invoice_default", + "emailName":"Invoice Email", + "emailSubject":"Invoice From Idurar", + "emailBody": "

email_invoice_default

", + "emailVariables": ["name","time"] + }, + { + "emailKey": "email_quote_default", + "emailName":"Quote Email", + "emailSubject":"Quote From Idurar", + "emailBody": "

email_quote_default

", + "emailVariables": ["name","time"] + }, + { + "emailKey": "email_offer_default", + "emailName":"Offer Email", + "emailSubject":"Invoice From Idurar", + "emailBody": "

email_offer_default

", + "emailVariables": [] + }, + { + "emailKey": "email_payment_receipt_default", + "emailName":"Payment Email", + "emailSubject":"Payment From Idurar", + "emailBody": "

email_payment_receipt_default

", + "emailVariables": ["name","time"] + }, + { + "emailKey": "email_signup_email_confirm_default", + "emailName":"Signup Confirmation Email", + "emailSubject":"Signup Confirmation From Idurar", + "emailBody": "

email_signup_email_confirm_default

", + "emailVariables": ["name"] + }, + { + "emailKey": "email_reset_password_default", + "emailName":"Password Reset Email", + "emailSubject":"Password Reset From Idurar", + "emailBody": "

email_reset_password_default

", + "emailVariables": ["name"] + }, + { + "emailKey": "welcome_email_default", + "emailName":"Welcome Email", + "emailSubject":"Welcom From Idurar", + "emailBody": "

welcome_email

", + "emailVariables":["name"] + } +] diff --git a/backend/src/setup/reset.js b/backend/src/setup/reset.js index ff9c8a507..ff5f77f5f 100644 --- a/backend/src/setup/reset.js +++ b/backend/src/setup/reset.js @@ -8,13 +8,9 @@ async function deleteData() { const Admin = require('../models/coreModels/Admin'); const AdminPassword = require('../models/coreModels/AdminPassword'); const Setting = require('../models/coreModels/Setting'); - const PaymentMode = require('../models/appModels/PaymentMode'); - const Taxes = require('../models/appModels/Taxes'); await Admin.deleteMany(); await AdminPassword.deleteMany(); - await PaymentMode.deleteMany(); - await Taxes.deleteMany(); console.log('๐Ÿ‘ Admin Deleted. To setup demo admin data, run\n\n\t npm run setup\n\n'); await Setting.deleteMany(); console.log('๐Ÿ‘ Setting Deleted. To setup Setting data, run\n\n\t npm run setup\n\n'); diff --git a/backend/src/utils/redis.js b/backend/src/utils/redis.js new file mode 100644 index 000000000..3c923a3e3 --- /dev/null +++ b/backend/src/utils/redis.js @@ -0,0 +1,9 @@ + +// const { createClient } = require('redis'); + +// const redisClient4 = createClient({ +// url: 'redis://127.0.0.1s:6379', +// }); + + +// module.exports = redisClient4; \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 000000000..6739192d7 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,16 @@ +FROM node:20.9.0-alpine + +WORKDIR /usr/src/app + +RUN npm install -g npm@10.2.4 + +COPY package*.json ./ +COPY vite.config.js ./ + +COPY . . + +RUN npm install + +EXPOSE 3000 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 0e3fdb85e..165630563 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,11 +5,8 @@ - IDURAR ERP CRM | Free Open Source Accounting Invoice Quote - + IDURAR ERP CRM | Open Code Source +
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cbb786e61..59f9678dd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,24 +1,24 @@ { "name": "idurar-erp-crm", - "version": "4.1.0", + "version": "4.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "idurar-erp-crm", - "version": "4.1.0", + "version": "4.0.0", "dependencies": { "@ant-design/icons": "^5.3.0", "@ant-design/pro-layout": "^7.17.19", "@reduxjs/toolkit": "^2.2.1", - "@vitejs/plugin-react": "^4.3.1", + "@vitejs/plugin-react": "^4.2.1", "antd": "^5.14.1", "axios": "^1.6.2", "cross-env": "7.0.3", "currency.js": "2.0.4", "dayjs": "^1.11.10", "just-compare": "^2.3.0", - "react": "^18.3.1", + "react": "^18.2.0", "react-dom": "^18.2.0", "react-quill": "^2.0.0", "react-redux": "^9.1.0", @@ -26,7 +26,7 @@ "redux": "^5.0.1", "reselect": "^5.1.0", "shortid": "^2.2.16", - "vite": "^5.4.8" + "vite": "^5.1.4" }, "devDependencies": { "@types/react": "^18.2.38", @@ -52,12 +52,12 @@ } }, "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { "node": ">=6.0.0" @@ -193,40 +193,40 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", - "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", + "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", - "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.5.tgz", + "integrity": "sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-module-transforms": "^7.25.2", - "@babel/helpers": "^7.25.0", - "@babel/parser": "^7.25.0", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.2", - "@babel/types": "^7.25.2", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.5", + "@babel/parser": "^7.23.5", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.5", + "@babel/types": "^7.23.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -242,13 +242,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", - "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz", + "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==", "dependencies": { - "@babel/types": "^7.25.6", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/types": "^7.23.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "engines": { @@ -256,13 +256,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", - "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", "dependencies": { - "@babel/compat-data": "^7.25.2", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -270,27 +270,58 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", - "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.2" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -300,82 +331,89 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", - "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz", + "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==", "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.6" + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.5", + "@babel/types": "^7.23.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", + "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "js-tokens": "^4.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", - "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", - "dependencies": { - "@babel/types": "^7.25.6" - }, + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", + "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", "bin": { "parser": "bin/babel-parser.js" }, @@ -384,11 +422,11 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", - "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", + "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -398,11 +436,11 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", - "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", + "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -423,29 +461,32 @@ } }, "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", - "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.6", - "@babel/parser": "^7.25.6", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.6", - "debug": "^4.3.1", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz", + "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.5", + "@babel/types": "^7.23.5", + "debug": "^4.1.0", "globals": "^11.1.0" }, "engines": { @@ -453,12 +494,12 @@ } }, "node_modules/@babel/types": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", - "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", + "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -484,9 +525,9 @@ "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", "cpu": [ "ppc64" ], @@ -499,9 +540,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "cpu": [ "arm" ], @@ -514,9 +555,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "cpu": [ "arm64" ], @@ -529,9 +570,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "cpu": [ "x64" ], @@ -544,9 +585,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "cpu": [ "arm64" ], @@ -559,9 +600,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "cpu": [ "x64" ], @@ -574,9 +615,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "cpu": [ "arm64" ], @@ -589,9 +630,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "cpu": [ "x64" ], @@ -604,9 +645,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "cpu": [ "arm" ], @@ -619,9 +660,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", "cpu": [ "arm64" ], @@ -634,9 +675,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "cpu": [ "ia32" ], @@ -649,9 +690,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "cpu": [ "loong64" ], @@ -664,9 +705,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "cpu": [ "mips64el" ], @@ -679,9 +720,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "cpu": [ "ppc64" ], @@ -694,9 +735,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "cpu": [ "riscv64" ], @@ -709,9 +750,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "cpu": [ "s390x" ], @@ -724,9 +765,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "cpu": [ "x64" ], @@ -739,9 +780,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "cpu": [ "x64" ], @@ -754,9 +795,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "cpu": [ "x64" ], @@ -769,9 +810,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "cpu": [ "x64" ], @@ -784,9 +825,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "cpu": [ "arm64" ], @@ -799,9 +840,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "cpu": [ "ia32" ], @@ -814,9 +855,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "cpu": [ "x64" ], @@ -933,43 +974,43 @@ "dev": true }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dependencies": { - "@jridgewell/set-array": "^1.2.1", + "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1154,9 +1195,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.5.tgz", - "integrity": "sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", + "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", "cpu": [ "arm" ], @@ -1166,9 +1207,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.5.tgz", - "integrity": "sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", + "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", "cpu": [ "arm64" ], @@ -1178,9 +1219,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.5.tgz", - "integrity": "sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", + "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", "cpu": [ "arm64" ], @@ -1190,9 +1231,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.5.tgz", - "integrity": "sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", + "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", "cpu": [ "x64" ], @@ -1202,9 +1243,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.5.tgz", - "integrity": "sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", + "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", "cpu": [ "arm" ], @@ -1214,9 +1255,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.5.tgz", - "integrity": "sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", + "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", "cpu": [ "arm" ], @@ -1226,9 +1267,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.5.tgz", - "integrity": "sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", + "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", "cpu": [ "arm64" ], @@ -1238,9 +1279,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.5.tgz", - "integrity": "sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", + "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", "cpu": [ "arm64" ], @@ -1250,9 +1291,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.5.tgz", - "integrity": "sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", + "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", "cpu": [ "ppc64" ], @@ -1262,9 +1303,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.5.tgz", - "integrity": "sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", + "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", "cpu": [ "riscv64" ], @@ -1274,9 +1315,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.5.tgz", - "integrity": "sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", + "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", "cpu": [ "s390x" ], @@ -1286,9 +1327,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz", - "integrity": "sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", "cpu": [ "x64" ], @@ -1298,9 +1339,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.5.tgz", - "integrity": "sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", + "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", "cpu": [ "x64" ], @@ -1310,9 +1351,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.5.tgz", - "integrity": "sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", + "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", "cpu": [ "arm64" ], @@ -1322,9 +1363,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.5.tgz", - "integrity": "sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", + "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", "cpu": [ "ia32" ], @@ -1334,9 +1375,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.5.tgz", - "integrity": "sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", + "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", "cpu": [ "x64" ], @@ -1383,9 +1424,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" }, "node_modules/@types/prop-types": { "version": "15.7.10", @@ -1452,15 +1493,15 @@ "dev": true }, "node_modules/@vitejs/plugin-react": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", - "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", + "integrity": "sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==", "dependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-transform-react-jsx-self": "^7.24.5", - "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@babel/core": "^7.23.5", + "@babel/plugin-transform-react-jsx-self": "^7.23.3", + "@babel/plugin-transform-react-jsx-source": "^7.23.3", "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.2" + "react-refresh": "^0.14.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -1759,9 +1800,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", - "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", "funding": [ { "type": "opencollective", @@ -1777,10 +1818,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001663", - "electron-to-chromium": "^1.5.28", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -1812,9 +1853,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001664", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz", - "integrity": "sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==", + "version": "1.0.30001563", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001563.tgz", + "integrity": "sha512-na2WUmOxnwIZtwnFI2CZ/3er0wdNzU7hN+cPYz/z2ajHThnkWjNBOpEPP4n+4r2WPM847JaMotaJE3bnfzjyKw==", "funding": [ { "type": "opencollective", @@ -2048,9 +2089,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.29", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.29.tgz", - "integrity": "sha512-PF8n2AlIhCKXQ+gTpiJi0VhcHDb69kYX4MtCiivctc2QD3XuNZ/XIOlbGzt7WAjjEev0TtaH6Cu3arZExm5DOw==" + "version": "1.4.588", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.588.tgz", + "integrity": "sha512-soytjxwbgcCu7nh5Pf4S2/4wa6UIu+A3p03U2yVr53qGxi1/VTR3ENI+p50v+UxqqZAfl48j3z55ud7VHIOr9w==" }, "node_modules/es-abstract": { "version": "1.22.3", @@ -2168,9 +2209,9 @@ } }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -2179,35 +2220,35 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" } }, "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "engines": { "node": ">=6" } @@ -3505,9 +3546,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" }, "node_modules/object-assign": { "version": "4.1.1", @@ -3745,14 +3786,14 @@ "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==" }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "funding": [ { "type": "opencollective", @@ -3769,8 +3810,8 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.1.0", - "source-map-js": "^1.2.1" + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -4474,9 +4515,9 @@ } }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -4543,9 +4584,9 @@ } }, "node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", "engines": { "node": ">=0.10.0" } @@ -4696,11 +4737,11 @@ } }, "node_modules/rollup": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.5.tgz", - "integrity": "sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", + "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.5" }, "bin": { "rollup": "dist/bin/rollup" @@ -4710,22 +4751,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.22.5", - "@rollup/rollup-android-arm64": "4.22.5", - "@rollup/rollup-darwin-arm64": "4.22.5", - "@rollup/rollup-darwin-x64": "4.22.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.22.5", - "@rollup/rollup-linux-arm-musleabihf": "4.22.5", - "@rollup/rollup-linux-arm64-gnu": "4.22.5", - "@rollup/rollup-linux-arm64-musl": "4.22.5", - "@rollup/rollup-linux-powerpc64le-gnu": "4.22.5", - "@rollup/rollup-linux-riscv64-gnu": "4.22.5", - "@rollup/rollup-linux-s390x-gnu": "4.22.5", - "@rollup/rollup-linux-x64-gnu": "4.22.5", - "@rollup/rollup-linux-x64-musl": "4.22.5", - "@rollup/rollup-win32-arm64-msvc": "4.22.5", - "@rollup/rollup-win32-ia32-msvc": "4.22.5", - "@rollup/rollup-win32-x64-msvc": "4.22.5", + "@rollup/rollup-android-arm-eabi": "4.18.0", + "@rollup/rollup-android-arm64": "4.18.0", + "@rollup/rollup-darwin-arm64": "4.18.0", + "@rollup/rollup-darwin-x64": "4.18.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", + "@rollup/rollup-linux-arm-musleabihf": "4.18.0", + "@rollup/rollup-linux-arm64-gnu": "4.18.0", + "@rollup/rollup-linux-arm64-musl": "4.18.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", + "@rollup/rollup-linux-riscv64-gnu": "4.18.0", + "@rollup/rollup-linux-s390x-gnu": "4.18.0", + "@rollup/rollup-linux-x64-gnu": "4.18.0", + "@rollup/rollup-linux-x64-musl": "4.18.0", + "@rollup/rollup-win32-arm64-msvc": "4.18.0", + "@rollup/rollup-win32-ia32-msvc": "4.18.0", + "@rollup/rollup-win32-x64-msvc": "4.18.0", "fsevents": "~2.3.2" } }, @@ -4886,9 +4927,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } @@ -5159,9 +5200,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "funding": [ { "type": "opencollective", @@ -5177,8 +5218,8 @@ } ], "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "escalade": "^3.1.1", + "picocolors": "^1.0.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -5205,13 +5246,13 @@ } }, "node_modules/vite": { - "version": "5.4.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", - "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "version": "5.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", + "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" }, "bin": { "vite": "bin/vite.js" @@ -5230,7 +5271,6 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", - "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -5248,9 +5288,6 @@ "sass": { "optional": true }, - "sass-embedded": { - "optional": true - }, "stylus": { "optional": true }, diff --git a/frontend/package.json b/frontend/package.json index 23e979115..f0a37ad0b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,23 +1,23 @@ { "name": "idurar-erp-crm", - "version": "4.1.0", + "version": "4.0.0", "engines": { - "node": "20.9.0", - "npm": "10.2.4" + "npm": "10.2.4", + "node": "20.9.0" }, "type": "module", "dependencies": { "@ant-design/icons": "^5.3.0", "@ant-design/pro-layout": "^7.17.19", "@reduxjs/toolkit": "^2.2.1", - "@vitejs/plugin-react": "^4.3.1", + "@vitejs/plugin-react": "^4.2.1", "antd": "^5.14.1", "axios": "^1.6.2", "cross-env": "7.0.3", "currency.js": "2.0.4", "dayjs": "^1.11.10", "just-compare": "^2.3.0", - "react": "^18.3.1", + "react": "^18.2.0", "react-dom": "^18.2.0", "react-quill": "^2.0.0", "react-redux": "^9.1.0", @@ -25,7 +25,7 @@ "redux": "^5.0.1", "reselect": "^5.1.0", "shortid": "^2.2.16", - "vite": "^5.4.8" + "vite": "^5.1.4" }, "scripts": { "dev": "vite", diff --git a/frontend/src/apps/ErpApp.jsx b/frontend/src/apps/ErpApp.jsx index ffb97de95..a197aee32 100644 --- a/frontend/src/apps/ErpApp.jsx +++ b/frontend/src/apps/ErpApp.jsx @@ -8,12 +8,13 @@ import { Layout } from 'antd'; import { useAppContext } from '@/context/appContext'; import Navigation from '@/apps/Navigation/NavigationContainer'; - +import ExpensesNav from '@/apps/Navigation/ExpensesNav'; import HeaderContent from '@/apps/Header/HeaderContainer'; import PageLoader from '@/components/PageLoader'; import { settingsAction } from '@/redux/settings/actions'; +import { translateAction } from '@/redux/translate/actions'; import { selectSettings } from '@/redux/settings/selectors'; import AppRouter from '@/router/AppRouter'; @@ -21,13 +22,14 @@ import AppRouter from '@/router/AppRouter'; import useResponsive from '@/hooks/useResponsive'; import storePersist from '@/redux/storePersist'; +import { selectLangDirection } from '@/redux/translate/selectors'; export default function ErpCrmApp() { const { Content } = Layout; - // const { state: stateApp, appContextAction } = useAppContext(); - // // const { app } = appContextAction; - // const { isNavMenuClose, currentApp } = stateApp; + const { state: stateApp, appContextAction } = useAppContext(); + const { app } = appContextAction; + const { isNavMenuClose, currentApp } = stateApp; const { isMobile } = useResponsive(); @@ -37,20 +39,23 @@ export default function ErpCrmApp() { dispatch(settingsAction.list({ entity: 'setting' })); }, []); - // const appSettings = useSelector(selectAppSettings); + const appSettings = useSelector(selectAppSettings); const { isSuccess: settingIsloaded } = useSelector(selectSettings); - // useEffect(() => { - // const { loadDefaultLang } = storePersist.get('firstVisit'); - // if (appSettings.idurar_app_language && !loadDefaultLang) { - // window.localStorage.setItem('firstVisit', JSON.stringify({ loadDefaultLang: true })); - // } - // }, [appSettings]); + useEffect(() => { + const { loadDefaultLang } = storePersist.get('firstVisit'); + if (appSettings.idurar_app_language && !loadDefaultLang) { + dispatch(translateAction.translate(appSettings.idurar_app_language)); + window.localStorage.setItem('firstVisit', JSON.stringify({ loadDefaultLang: true })); + } + }, [appSettings]); + const langDirection = useSelector(selectLangDirection); if (settingIsloaded) return ( - + + {/* {currentApp === 'default' ? : } */} {isMobile ? ( diff --git a/frontend/src/apps/Header/HeaderContainer.jsx b/frontend/src/apps/Header/HeaderContainer.jsx index d60568e6d..305672c72 100644 --- a/frontend/src/apps/Header/HeaderContainer.jsx +++ b/frontend/src/apps/Header/HeaderContainer.jsx @@ -1,6 +1,6 @@ import { useSelector } from 'react-redux'; import { Link, useNavigate } from 'react-router-dom'; -import { Avatar, Dropdown, Layout, Badge, Button } from 'antd'; +import { Avatar, Dropdown, Layout } from 'antd'; // import Notifications from '@/components/Notification'; @@ -14,6 +14,8 @@ import useLanguage from '@/locale/useLanguage'; import UpgradeButton from './UpgradeButton'; +import { selectLangDirection } from '@/redux/translate/selectors'; + export default function HeaderContent() { const currentAdmin = useSelector(selectCurrentAdmin); const { Header } = Layout; @@ -84,13 +86,14 @@ export default function HeaderContent() { }, ]; + const langDirection = useSelector(selectLangDirection); return (
{ + const features = [ + 'Self-Hosted Premium Version', + 'ulimited Users', + 'Multi-Currency - ulimited currency', + 'Multi-Branch - ulimited branch', + 'Free 1 year update', + '24/7 priority support', + ]; + + return ( + { + window.open('https://cloud.idurarapp.com/pricing'); + }} + > + Purchase Now + + } + // bordered + dataSource={features} + renderItem={(item) => {item}} + /> + ); +}; + export default function UpgradeButton() { const translate = useLanguage(); + const Content = () => { + return ( + + //

{translate('Do you need help on customize of this app')}

+ // + // + ); + }; return ( - - - + } trigger="click"> + + } + style={{ + color: '#f56a00', + backgroundColor: '#FFF', + float: 'right', + marginTop: '5px', + cursor: 'pointer', + }} + /> + + ); } diff --git a/frontend/src/apps/Navigation/AppNav.jsx b/frontend/src/apps/Navigation/AppNav.jsx new file mode 100644 index 000000000..2688d3324 --- /dev/null +++ b/frontend/src/apps/Navigation/AppNav.jsx @@ -0,0 +1,128 @@ +import { Link } from 'react-router-dom'; + +import { + SettingOutlined, + CustomerServiceOutlined, + ContainerOutlined, + FileSyncOutlined, + DashboardOutlined, + TagOutlined, + TagsOutlined, + UserOutlined, + CreditCardOutlined, + FileOutlined, + ShopOutlined, + FilterOutlined, + WalletOutlined, +} from '@ant-design/icons'; + +const AppNav = ({ translate }) => [ + { + key: 'dashboard', + icon: , + label: {translate('dashboard')}, + }, + { + key: 'customer', + icon: , + label: {translate('customer')}, + }, + { + key: 'people', + icon: , + label: {translate('people')}, + }, + { + key: 'company', + icon: , + label: {translate('company')}, + }, + { + key: 'lead', + icon: , + label: {translate('lead')}, + }, + { + key: 'offer', + icon: , + label: {translate('Offer Leads')}, + }, + { + key: 'invoice', + icon: , + label: {translate('invoice')}, + }, + { + key: 'quote', + icon: , + label: {translate('quote')}, + }, + { + key: 'payment', + icon: , + label: {translate('payment')}, + }, + { + key: 'expenses', + icon: , + label: {translate('expense')}, + }, + { + key: 'product', + icon: , + label: {translate('product')}, + }, + { + key: 'categoryproduct', + icon: , + label: {translate('product_category')}, + }, + // { + // key: 'employee', + // icon: , + // label: {translate('employee')}, + // }, + + { + label: translate('Settings'), + key: 'settings', + icon: , + children: [ + { + key: 'admin', + // icon: , + label: {translate('Staff')}, + }, + { + key: 'generalSettings', + label: {translate('general_settings')}, + }, + { + key: 'expensesCategory', + label: {translate('expenses_Category')}, + }, + // { + // key: 'emailTemplates', + // label: {translate('email_templates')}, + // }, + { + key: 'paymentMode', + label: {translate('payment_mode')}, + }, + { + key: 'taxes', + label: {translate('taxes')}, + }, + { + key: 'about', + label: {translate('about')}, + }, + // { + // key: 'advancedSettings', + // label: {translate('advanced_settings')}, + // }, + ], + }, +]; + +export default AppNav; diff --git a/frontend/src/apps/Navigation/ExpensesNav.jsx b/frontend/src/apps/Navigation/ExpensesNav.jsx new file mode 100644 index 000000000..8adeff057 --- /dev/null +++ b/frontend/src/apps/Navigation/ExpensesNav.jsx @@ -0,0 +1,167 @@ +import { useState, useEffect } from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import { Button, Drawer, Layout, Menu } from 'antd'; + +import { useAppContext } from '@/context/appContext'; + +import useLanguage from '@/locale/useLanguage'; +import logoIcon from '@/style/images/logo-icon.svg'; +import logoText from '@/style/images/logo-text.svg'; +import { useNavigate } from 'react-router-dom'; +import useResponsive from '@/hooks/useResponsive'; + +import { + SettingOutlined, + CustomerServiceOutlined, + ContainerOutlined, + FileSyncOutlined, + DashboardOutlined, + TagOutlined, + TagsOutlined, + UserOutlined, + CreditCardOutlined, + MenuOutlined, + FileOutlined, + ShopOutlined, + FilterOutlined, + WalletOutlined, +} from '@ant-design/icons'; + +const { Sider } = Layout; + +export default function Navigation() { + const { isMobile } = useResponsive(); + + return isMobile ? : ; +} + +function Sidebar({ collapsible, isMobile = false }) { + let location = useLocation(); + + const { state: stateApp, appContextAction } = useAppContext(); + const { isNavMenuClose } = stateApp; + const { navMenu } = appContextAction; + const [showLogoApp, setLogoApp] = useState(isNavMenuClose); + const [currentPath, setCurrentPath] = useState(location.pathname.slice(1)); + + const translate = useLanguage(); + const navigate = useNavigate(); + + const items = [ + { + key: 'expenses', + icon: , + label: {translate('expense')}, + }, + { + key: 'expensesCategory', + icon: , + label: {translate('expenses_Category')}, + }, + ]; + + useEffect(() => { + if (location) + if (currentPath !== location.pathname) { + if (location.pathname === '/') { + setCurrentPath('dashboard'); + } else setCurrentPath(location.pathname.slice(1)); + } + }, [location, currentPath]); + + useEffect(() => { + if (isNavMenuClose) { + setLogoApp(isNavMenuClose); + } + const timer = setTimeout(() => { + if (!isNavMenuClose) { + setLogoApp(isNavMenuClose); + } + }, 200); + return () => clearTimeout(timer); + }, [isNavMenuClose]); + const onCollapse = () => { + navMenu.collapse(); + }; + + return ( + +
navigate('/')} style={{ cursor: 'pointer' }}> + Logo + + {!showLogoApp && ( + Logo + )} +
+ + + ); +} + +function MobileSidebar() { + const [visible, setVisible] = useState(false); + const showDrawer = () => { + setVisible(true); + }; + const onClose = () => { + setVisible(false); + }; + return ( + <> + + + + + + ); +} diff --git a/frontend/src/apps/Navigation/NavigationContainer.jsx b/frontend/src/apps/Navigation/NavigationContainer.jsx index d90e5b3c9..fe7a663e5 100644 --- a/frontend/src/apps/Navigation/NavigationContainer.jsx +++ b/frontend/src/apps/Navigation/NavigationContainer.jsx @@ -27,6 +27,8 @@ import { WalletOutlined, ReconciliationOutlined, } from '@ant-design/icons'; +import { useSelector } from 'react-redux'; +import { selectLangDirection } from '@/redux/translate/selectors'; const { Sider } = Layout; @@ -59,7 +61,26 @@ function Sidebar({ collapsible, isMobile = false }) { icon: , label: {translate('customers')}, }, - + { + key: 'people', + icon: , + label: {translate('peoples')}, + }, + { + key: 'company', + icon: , + label: {translate('companies')}, + }, + { + key: 'lead', + icon: , + label: {translate('leads')}, + }, + { + key: 'offer', + icon: , + label: {translate('offers')}, + }, { key: 'invoice', icon: , @@ -68,7 +89,7 @@ function Sidebar({ collapsible, isMobile = false }) { { key: 'quote', icon: , - label: {translate('quote')}, + label: {translate('proforma invoices')}, }, { key: 'payment', @@ -77,24 +98,49 @@ function Sidebar({ collapsible, isMobile = false }) { }, { - key: 'paymentMode', - label: {translate('payments_mode')}, - icon: , + key: 'product', + icon: , + label: {translate('products')}, }, { - key: 'taxes', - label: {translate('taxes')}, - icon: , + key: 'categoryproduct', + icon: , + label: {translate('products_category')}, }, { - key: 'generalSettings', - label: {translate('settings')}, - icon: , + key: 'expenses', + icon: , + label: {translate('expenses')}, }, { - key: 'about', - label: {translate('about')}, + key: 'expensesCategory', icon: , + label: {translate('expenses_Category')}, + }, + + { + label: translate('Settings'), + key: 'settings', + icon: , + children: [ + { + key: 'generalSettings', + label: {translate('settings')}, + }, + + { + key: 'paymentMode', + label: {translate('payments_mode')}, + }, + { + key: 'taxes', + label: {translate('taxes')}, + }, + { + key: 'about', + label: {translate('about')}, + }, + ], }, ]; @@ -122,6 +168,7 @@ function Sidebar({ collapsible, isMobile = false }) { navMenu.collapse(); }; + const langDirection = useSelector(selectLangDirection); return ( @@ -187,6 +237,7 @@ function MobileSidebar() { setVisible(false); }; + const langDirection = useSelector(selectLangDirection); return ( <> { + const dispatch = useDispatch(); + + const { isMobile } = useResponsive(); + const [selectOptions, setOptions] = useState([]); + + const navigate = useNavigate(); + + const asyncList = () => { + return request.listAll({ entity: 'currency', options: { enabled: true } }); + }; + const { result, isLoading: fetchIsLoading, isSuccess } = useFetch(asyncList); + useEffect(() => { + if (isSuccess) { + setOptions(result); + } + }, [isSuccess]); + + const money_format_settings = useSelector(selectMoneyFormat); + + const handleSelectChange = (newValue) => { + if (newValue === 'redirectURL') { + navigate('/settings/currency'); + } + }; + + const optionsList = () => { + const list = []; + + const value = 'redirectURL'; + const label = `+ Add New Currency`; + + list.push(...currencyOptions(selectOptions)); + list.push({ value, label }); + + return list; + }; + + if (money_format_settings.default_currency_code) + return ( + + ); + else { + ; + } +}; + +export default ChooseCurrency; diff --git a/frontend/src/components/DataTable/DataTable.jsx b/frontend/src/components/DataTable/DataTable.jsx index b6f34b43d..f7860a927 100644 --- a/frontend/src/components/DataTable/DataTable.jsx +++ b/frontend/src/components/DataTable/DataTable.jsx @@ -22,6 +22,7 @@ import { useMoney, useDate } from '@/settings'; import { generate as uniqueId } from 'shortid'; import { useCrudContext } from '@/context/crud'; +import { selectLangDirection } from '@/redux/translate/selectors'; function AddNewItem({ config }) { const { crudContextAction } = useCrudContext(); @@ -175,11 +176,13 @@ export default function DataTable({ config, extra = [] }) { }; }, []); + const langDirection=useSelector(selectLangDirection) + return ( <> window.history.back()} - backIcon={} + backIcon={langDirection==="rtl"?:} title={DATATABLE_TITLE} ghost={false} extra={[ @@ -197,6 +200,7 @@ export default function DataTable({ config, extra = [] }) { ]} style={{ padding: '20px 0px', + direction:langDirection }} > diff --git a/frontend/src/components/PageLoader/index.jsx b/frontend/src/components/PageLoader/index.jsx index 3a95ba150..1598b0f16 100644 --- a/frontend/src/components/PageLoader/index.jsx +++ b/frontend/src/components/PageLoader/index.jsx @@ -1,13 +1,10 @@ import React from 'react'; import { Spin } from 'antd'; -import { LoadingOutlined } from '@ant-design/icons'; - const PageLoader = () => { - const antIcon = ; return (
- +
); }; diff --git a/frontend/src/components/PaypalButton/Subscription.jsx b/frontend/src/components/PaypalButton/Subscription.jsx new file mode 100644 index 000000000..2c05f4a53 --- /dev/null +++ b/frontend/src/components/PaypalButton/Subscription.jsx @@ -0,0 +1,38 @@ +const PaypalButton = () => { + useEffect(() => { + const script = document.createElement('script'); + script.src = + 'https://www.paypal.com/sdk/js?client-id=AXy1YZNZsMCdiYVhh_jyoYW9_HkylFwgkL75WNGw924gL4jHcW5myCTH5JGOyyMiuZSabMWpovoarBnQ&vault=true&intent=subscription'; + script.async = true; + + script.onload = () => { + paypal + .Buttons({ + style: { + shape: 'rect', + color: 'blue', + layout: 'vertical', + label: 'paypal', + }, + createSubscription: function (data, actions) { + return actions.subscription.create({ + /* Creates the subscription */ + plan_id: 'P-6NV451935K3609258MV3DRUQ', + }); + }, + onApprove: function (data, actions) { + alert(data.subscriptionID); // You can add optional success message for the subscriber here + }, + }) + .render('#paypal-button-container-P-6NV451935K3609258MV3DRUQ'); // Renders the PayPal button + }; + + document.body.appendChild(script); + + return () => { + document.body.removeChild(script); + }; + }, []); + + return
; +}; diff --git a/frontend/src/components/SelectAsync/index.jsx b/frontend/src/components/SelectAsync/index.jsx index f97fc15eb..d1491d0c5 100644 --- a/frontend/src/components/SelectAsync/index.jsx +++ b/frontend/src/components/SelectAsync/index.jsx @@ -37,7 +37,7 @@ const SelectAsync = ({ }; useEffect(() => { if (value !== undefined) { - const val = value?.[outputValue] ?? value; + const val = value[outputValue] ?? value; setCurrentValue(val); onChange(val); } @@ -47,7 +47,7 @@ const SelectAsync = ({ if (newValue === 'redirectURL') { navigate(urlToRedirect); } else { - const val = newValue?.[outputValue] ?? newValue; + const val = newValue[outputValue] ?? newValue; setCurrentValue(newValue); onChange(val); } diff --git a/frontend/src/components/SelectTag/index.jsx b/frontend/src/components/SelectTag/index.jsx index e983a18e8..d58b1d168 100644 --- a/frontend/src/components/SelectTag/index.jsx +++ b/frontend/src/components/SelectTag/index.jsx @@ -1,5 +1,6 @@ import { Select, Tag } from 'antd'; import { generate as uniqueId } from 'shortid'; +import { tagColor } from '@/utils/statusTagColor'; export default function SelectTag({ options, defaultValue }) { return ( @@ -10,16 +11,19 @@ export default function SelectTag({ options, defaultValue }) { }} > {options?.map((value) => { + const option = tagColor(value); if (option) return ( - {translate(option.label)} + + {translate(option.label)} + ); else return ( - {value} + {value} ); })} diff --git a/frontend/src/forms/DynamicForm/index.jsx b/frontend/src/forms/DynamicForm/index.jsx index 8cc5b2717..81e055420 100644 --- a/frontend/src/forms/DynamicForm/index.jsx +++ b/frontend/src/forms/DynamicForm/index.jsx @@ -9,12 +9,15 @@ import SelectAsync from '@/components/SelectAsync'; import { generate as uniqueId } from 'shortid'; import { countryList } from '@/utils/countryList'; +import { selectLangDirection } from '@/redux/translate/selectors'; +import { useSelector } from 'react-redux'; export default function DynamicForm({ fields, isUpdateForm = false }) { const [feedback, setFeedback] = useState(); + const langDirection = useSelector(selectLangDirection); return ( -
+
{Object.keys(fields).map((key) => { let field = fields[key]; diff --git a/frontend/src/forms/LoginForm.jsx b/frontend/src/forms/LoginForm.jsx index 258d5ae6b..24462d1c9 100644 --- a/frontend/src/forms/LoginForm.jsx +++ b/frontend/src/forms/LoginForm.jsx @@ -3,26 +3,32 @@ import { Form, Input, Checkbox } from 'antd'; import { UserOutlined, LockOutlined } from '@ant-design/icons'; import useLanguage from '@/locale/useLanguage'; +import { useSelector } from 'react-redux'; +import { selectLangDirection } from '@/redux/translate/selectors'; export default function LoginForm() { + const langDirection = useSelector(selectLangDirection) + const translate = useLanguage(); return ( -
+
+ +> } - placeholder={'admin@demo.com'} + placeholder={translate('email')} type="email" size="large" /> @@ -38,18 +44,19 @@ export default function LoginForm() { > } - placeholder={'admin123'} + placeholder={translate('password')} size="large" /> - + {translate('Remember me')} - + {translate('Forgot password')} +
); diff --git a/frontend/src/forms/RegisterForm.jsx b/frontend/src/forms/RegisterForm.jsx index e86693816..b8e63bfb8 100644 --- a/frontend/src/forms/RegisterForm.jsx +++ b/frontend/src/forms/RegisterForm.jsx @@ -8,6 +8,25 @@ import { countryList } from '@/utils/countryList'; export default function RegisterForm({ userLocation }) { const translate = useLanguage(); + const validatePassword = (_, value) => { + if (!value) { + return Promise.reject(new Error('Password is required')); + } + if (value.length < 8) { + return Promise.reject(new Error('Password must be at least 8 characters long')); + } + if (!/[a-zA-Z]/.test(value)) { + return Promise.reject(new Error('Password must contain at least one letter')); + } + if (!/[0-9]/.test(value)) { + return Promise.reject(new Error('Password must contain at least one number')); + } + if (!/[!@#$%^&*(),.?":{}|<>]/.test(value)) { + return Promise.reject(new Error('Password must contain at least one special character')); + } + return Promise.resolve(); + }; + return ( <> } size="large" /> diff --git a/frontend/src/layout/AuthLayout/index.jsx b/frontend/src/layout/AuthLayout/index.jsx index 588b4cb0a..ce6433cb7 100644 --- a/frontend/src/layout/AuthLayout/index.jsx +++ b/frontend/src/layout/AuthLayout/index.jsx @@ -1,12 +1,16 @@ import React from 'react'; import { Layout, Row, Col } from 'antd'; - +import { selectLangDirection } from '@/redux/translate/selectors'; import { useSelector } from 'react-redux'; import { Content } from 'antd/lib/layout/layout'; export default function AuthLayout({ sideContent, children }) { + const langDirection = useSelector(selectLangDirection); + return ( - + {children} diff --git a/frontend/src/locale/Localization.jsx b/frontend/src/locale/Localization.jsx index ba124dc6f..a629c7a15 100644 --- a/frontend/src/locale/Localization.jsx +++ b/frontend/src/locale/Localization.jsx @@ -4,10 +4,11 @@ export default function Localization({ children }) { return ( diff --git a/frontend/src/locale/translation/en_us.js b/frontend/src/locale/translation/en_us.js index 8127b6d89..5ff2676fd 100644 --- a/frontend/src/locale/translation/en_us.js +++ b/frontend/src/locale/translation/en_us.js @@ -448,8 +448,8 @@ const lang = { create_only: 'Create Only', enter_code: 'Enter Code', offers: 'Offers', - proforma_invoices: 'quote', - search: 'search', + proforma_invoices: 'Proforma Invoices', + search:"search" }; export default lang; diff --git a/frontend/src/locale/useLanguage.jsx b/frontend/src/locale/useLanguage.jsx index c632402da..9adbf31b7 100644 --- a/frontend/src/locale/useLanguage.jsx +++ b/frontend/src/locale/useLanguage.jsx @@ -1,41 +1,46 @@ -const getLabel = (key) => { +import { useSelector } from 'react-redux'; + +import { selectCurrentLang } from '@/redux/translate/selectors'; + +const getLabel = (lang, key) => { try { const lowerCaseKey = key .toLowerCase() .replace(/[^a-zA-Z0-9]/g, '_') .replace(/ /g, '_'); - // if (lang[lowerCaseKey]) return lang[lowerCaseKey]; + if (lang[lowerCaseKey]) return lang[lowerCaseKey]; + else { + // convert no found language label key to label - // convert no found language label key to label + const remove_underscore_fromKey = key.replace(/_/g, ' ').split(' '); - const remove_underscore_fromKey = key.replace(/_/g, ' ').split(' '); + const conversionOfAllFirstCharacterofEachWord = remove_underscore_fromKey.map( + (word) => word[0].toUpperCase() + word.substring(1) + ); - const conversionOfAllFirstCharacterofEachWord = remove_underscore_fromKey.map( - (word) => word[0].toUpperCase() + word.substring(1) - ); + const label = conversionOfAllFirstCharacterofEachWord.join(' '); - const label = conversionOfAllFirstCharacterofEachWord.join(' '); - - const result = window.localStorage.getItem('lang'); - if (!result) { - let list = {}; - list[lowerCaseKey] = label; - window.localStorage.setItem('lang', JSON.stringify(list)); - } else { - let list = { ...JSON.parse(result) }; - list[lowerCaseKey] = label; - window.localStorage.removeItem('lang'); - window.localStorage.setItem('lang', JSON.stringify(list)); + const result = window.localStorage.getItem('lang'); + if (!result) { + let list = {}; + list[lowerCaseKey] = label; + window.localStorage.setItem('lang', JSON.stringify(list)); + } else { + let list = { ...JSON.parse(result) }; + list[lowerCaseKey] = label; + window.localStorage.removeItem('lang'); + window.localStorage.setItem('lang', JSON.stringify(list)); + } + // console.error( + // '๐Ÿ‡ฉ๐Ÿ‡ฟ ๐Ÿ‡ง๐Ÿ‡ท ๐Ÿ‡ป๐Ÿ‡ณ ๐Ÿ‡ฎ๐Ÿ‡ฉ ๐Ÿ‡จ๐Ÿ‡ณ Language Label Warning : translate("' + + // lowerCaseKey + + // '") failed to get label for this key : ' + + // lowerCaseKey + + // ' please review your language config file and add this label' + // ); + return label; } - // console.error( - // '๐Ÿ‡ฉ๐Ÿ‡ฟ ๐Ÿ‡ง๐Ÿ‡ท ๐Ÿ‡ป๐Ÿ‡ณ ๐Ÿ‡ฎ๐Ÿ‡ฉ ๐Ÿ‡จ๐Ÿ‡ณ Language Label Warning : translate("' + - // lowerCaseKey + - // '") failed to get label for this key : ' + - // lowerCaseKey + - // ' please review your language config file and add this label' - // ); - return label; } catch (error) { // console.error( // '๐Ÿšจ error getting this label : translate("' + @@ -49,7 +54,9 @@ const getLabel = (key) => { }; const useLanguage = () => { - const translate = (value) => getLabel(value); + const lang = useSelector(selectCurrentLang); + + const translate = (value) => getLabel(lang, value); return translate; }; diff --git a/frontend/src/modules/AdvancedCrudModule/CreateItem.jsx b/frontend/src/modules/AdvancedCrudModule/CreateItem.jsx new file mode 100644 index 000000000..84e17003c --- /dev/null +++ b/frontend/src/modules/AdvancedCrudModule/CreateItem.jsx @@ -0,0 +1,95 @@ +import { useEffect } from 'react'; + +import { Button, Tag, Form, Divider } from 'antd'; +import { PageHeader } from '@ant-design/pro-layout'; + +import { useSelector, useDispatch } from 'react-redux'; + +import useLanguage from '@/locale/useLanguage'; + +import { settingsAction } from '@/redux/settings/actions'; + +import { adavancedCrud } from '@/redux/adavancedCrud/actions'; +import { selectCreatedItem } from '@/redux/adavancedCrud/selectors'; + +import { generate as uniqueId } from 'shortid'; + +import Loading from '@/components/Loading'; +import { CloseCircleOutlined, PlusOutlined } from '@ant-design/icons'; + +import { useNavigate } from 'react-router-dom'; + +function SaveForm({ form }) { + const translate = useLanguage(); + const handelClick = () => { + form.submit(); + }; + + return ( + + ); +} + +export default function CreateItem({ config, CreateForm }) { + const translate = useLanguage(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + + useEffect(() => { + dispatch(settingsAction.list({ entity: 'setting' })); + }, []); + let { entity } = config; + + const { isLoading, isSuccess, result } = useSelector(selectCreatedItem); + const [form] = Form.useForm(); + + useEffect(() => { + if (isSuccess) { + form.resetFields(); + dispatch(adavancedCrud.resetAction({ actionType: 'create' })); + navigate(`/${entity.toLowerCase()}/read/${result._id}`); + } + return () => {}; + }, [isSuccess]); + + const onSubmit = (fieldsValue) => { + if (fieldsValue) { + dispatch(adavancedCrud.create({ entity, jsonData: fieldsValue })); + } + }; + + return ( + <> + { + navigate(`/${entity.toLowerCase()}`); + }} + title={translate('New')} + ghost={false} + tags={{translate('Draft')}} + // subTitle="This is create page" + extra={[ + , + , + ]} + style={{ + padding: '20px 0px', + }} + > + + +
+ + +
+ + ); +} diff --git a/frontend/src/modules/AdvancedCrudModule/DataTable.jsx b/frontend/src/modules/AdvancedCrudModule/DataTable.jsx new file mode 100644 index 000000000..1564b6169 --- /dev/null +++ b/frontend/src/modules/AdvancedCrudModule/DataTable.jsx @@ -0,0 +1,194 @@ +import { useEffect } from 'react'; +import { + EyeOutlined, + EditOutlined, + DeleteOutlined, + FilePdfOutlined, + RedoOutlined, + PlusOutlined, + EllipsisOutlined, +} from '@ant-design/icons'; +import { Dropdown, Table, Button } from 'antd'; +import { PageHeader } from '@ant-design/pro-layout'; + +import { useSelector, useDispatch } from 'react-redux'; +import useLanguage from '@/locale/useLanguage'; +import { adavancedCrud } from '@/redux/adavancedCrud/actions'; +import { selectListItems } from '@/redux/adavancedCrud/selectors'; +import { useAdavancedCrudContext } from '@/context/adavancedCrud'; +import { generate as uniqueId } from 'shortid'; +import { useNavigate } from 'react-router-dom'; + +import { DOWNLOAD_BASE_URL } from '@/config/serverApiConfig'; + +function AddNewItem({ config }) { + const navigate = useNavigate(); + const { ADD_NEW_ENTITY, entity } = config; + + const handleClick = () => { + navigate(`/${entity.toLowerCase()}/create`); + }; + + return ( + + ); +} + +export default function DataTable({ config, extra = [] }) { + const translate = useLanguage(); + let { entity, dataTableColumns, disableAdd = false } = config; + + const { DATATABLE_TITLE } = config; + + const { result: listResult, isLoading: listIsLoading } = useSelector(selectListItems); + + const { pagination, items: dataSource } = listResult; + + const { adavancedCrudContextAction } = useAdavancedCrudContext(); + const { modal } = adavancedCrudContextAction; + + const items = [ + { + label: translate('Show'), + key: 'read', + icon: , + }, + { + label: translate('Edit'), + key: 'edit', + icon: , + }, + { + label: translate('Download'), + key: 'download', + icon: , + }, + ...extra, + { + type: 'divider', + }, + + { + label: translate('Delete'), + key: 'delete', + icon: , + }, + ]; + + const navigate = useNavigate(); + + const handleRead = (record) => { + dispatch(adavancedCrud.currentItem({ data: record })); + navigate(`/${entity}/read/${record._id}`); + }; + const handleEdit = (record) => { + const data = { ...record }; + dispatch(adavancedCrud.currentAction({ actionType: 'update', data })); + navigate(`/${entity}/update/${record._id}`); + }; + const handleDownload = (record) => { + window.open(`${DOWNLOAD_BASE_URL}${entity}/${entity}-${record._id}.pdf`, '_blank'); + }; + + const handleDelete = (record) => { + dispatch(adavancedCrud.currentAction({ actionType: 'delete', data: record })); + modal.open(); + }; + + const handleRecordPayment = (record) => { + dispatch(adavancedCrud.currentItem({ data: record })); + navigate(`/invoice/pay/${record.invoice._id}`); + }; + + dataTableColumns = [ + ...dataTableColumns, + { + title: '', + key: 'action', + fixed: 'right', + render: (_, record) => ( + { + switch (key) { + case 'read': + handleRead(record); + break; + case 'edit': + handleEdit(record); + break; + case 'download': + handleDownload(record); + break; + case 'delete': + handleDelete(record); + break; + case 'recordPayment': + handleRecordPayment(record); + break; + default: + break; + } + }, + }} + trigger={['click']} + > + e.preventDefault()} + /> + + ), + }, + ]; + + const dispatch = useDispatch(); + + const handelDataTableLoad = (pagination) => { + const options = { page: pagination.current || 1, items: pagination.pageSize || 10 }; + dispatch(adavancedCrud.list({ entity, options })); + }; + + const dispatcher = () => { + dispatch(adavancedCrud.list({ entity })); + }; + + useEffect(() => { + const controller = new AbortController(); + dispatcher(); + return () => { + controller.abort(); + }; + }, []); + + return ( + <> + }> + {translate('Refresh')} + , + !disableAdd && , + ]} + style={{ + padding: '20px 0px', + }} + > + + item._id} + dataSource={dataSource} + pagination={pagination} + loading={listIsLoading} + onChange={handelDataTableLoad} + scroll={{ x: true }} + /> + + ); +} diff --git a/frontend/src/modules/AdvancedCrudModule/DeleteItem.jsx b/frontend/src/modules/AdvancedCrudModule/DeleteItem.jsx new file mode 100644 index 000000000..0fe3d4d4c --- /dev/null +++ b/frontend/src/modules/AdvancedCrudModule/DeleteItem.jsx @@ -0,0 +1,59 @@ +import { useEffect, useState } from 'react'; +import { Modal } from 'antd'; + +import { useDispatch, useSelector } from 'react-redux'; +import { adavancedCrud } from '@/redux/adavancedCrud/actions'; +import { useAdavancedCrudContext } from '@/context/adavancedCrud'; +import { selectDeletedItem } from '@/redux/adavancedCrud/selectors'; +import { valueByString } from '@/utils/helpers'; + +export default function Delete({ config }) { + let { + entity, + deleteModalLabels, + deleteMessage = 'Do you want delete : ', + modalTitle = 'Remove Item', + } = config; + const dispatch = useDispatch(); + const { current, isLoading, isSuccess } = useSelector(selectDeletedItem); + const { state, adavancedCrudContextAction } = useAdavancedCrudContext(); + const { deleteModal } = state; + const { modal } = adavancedCrudContextAction; + const [displayItem, setDisplayItem] = useState(''); + + useEffect(() => { + if (isSuccess) { + modal.close(); + const options = { page: 1, items: 10 }; + dispatch(adavancedCrud.list({ entity, options })); + } + if (current) { + let labels = deleteModalLabels.map((x) => valueByString(current, x)).join(' '); + + setDisplayItem(labels); + } + }, [isSuccess, current]); + + const handleOk = () => { + const id = current._id; + dispatch(adavancedCrud.delete({ entity, id })); + modal.close(); + }; + const handleCancel = () => { + if (!isLoading) modal.close(); + }; + return ( + +

+ {deleteMessage} + {displayItem} +

+
+ ); +} diff --git a/frontend/src/modules/AdvancedCrudModule/ItemRow.jsx b/frontend/src/modules/AdvancedCrudModule/ItemRow.jsx new file mode 100644 index 000000000..8671be27c --- /dev/null +++ b/frontend/src/modules/AdvancedCrudModule/ItemRow.jsx @@ -0,0 +1,121 @@ +import { useState, useEffect } from 'react'; +import { Form, Input, InputNumber, Row, Col } from 'antd'; + +import { DeleteOutlined } from '@ant-design/icons'; +import { useMoney, useDate } from '@/settings'; +import calculate from '@/utils/calculate'; +import AutoCompleteAsync from '@/components/AutoCompleteAsync'; + +export default function ItemRow({ field, remove, current = null }) { + const [totalState, setTotal] = useState(undefined); + const [price, setPrice] = useState(0); + const [quantity, setQuantity] = useState(0); + + const money = useMoney(); + const updateQt = (value) => { + setQuantity(value); + }; + const updatePrice = (value) => { + setPrice(value); + }; + + useEffect(() => { + if (current) { + // When it accesses the /payment/ endpoint, + // it receives an invoice.item instead of just item + // and breaks the code, but now we can check if items exists, + // and if it doesn't we can access invoice.items. + + const { items, invoice } = current; + + if (invoice) { + const item = invoice[field.fieldKey]; + + if (item) { + setQuantity(item.quantity); + setPrice(item.price); + } + } else { + const item = items[field.fieldKey]; + + if (item) { + setQuantity(item.quantity); + setPrice(item.price); + } + } + } + }, [current]); + + useEffect(() => { + const currentTotal = calculate.multiply(price, quantity); + + setTotal(currentTotal); + }, [price, quantity]); + + return ( + + + + console.log(value)} + > + + + + + + + + + + + + + + + + + + + + + money.amountFormatter({ amount: value })} + /> + + + + +
+ remove(field.name)} /> +
+ + ); +} diff --git a/frontend/src/modules/AdvancedCrudModule/ReadItem.jsx b/frontend/src/modules/AdvancedCrudModule/ReadItem.jsx new file mode 100644 index 000000000..389515162 --- /dev/null +++ b/frontend/src/modules/AdvancedCrudModule/ReadItem.jsx @@ -0,0 +1,315 @@ +import { useState, useEffect } from 'react'; +import { Divider } from 'antd'; + +import { Button, Row, Col, Descriptions, Statistic, Tag } from 'antd'; +import { PageHeader } from '@ant-design/pro-layout'; +import { + EditOutlined, + FilePdfOutlined, + CloseCircleOutlined, + RetweetOutlined, + MailOutlined, +} from '@ant-design/icons'; + +import { useSelector, useDispatch } from 'react-redux'; +import useLanguage from '@/locale/useLanguage'; +import { adavancedCrud } from '@/redux/adavancedCrud/actions'; + +import { generate as uniqueId } from 'shortid'; + +import { selectCurrentItem } from '@/redux/adavancedCrud/selectors'; + +import { DOWNLOAD_BASE_URL } from '@/config/serverApiConfig'; +import { useMoney, useDate } from '@/settings'; +import useMail from '@/hooks/useMail'; +import { useNavigate } from 'react-router-dom'; +import { tagColor } from '@/utils/statusTagColor'; + +const Item = ({ item }) => { + const { moneyFormatter } = useMoney(); + return ( + + +

+ {item.itemName} +

+

{item.description}

+ + +

+ {moneyFormatter({ amount: item.price })} +

+ + +

+ {item.quantity} +

+ + +

+ {moneyFormatter({ amount: item.total })} +

+ + + + ); +}; + +export default function ReadItem({ config, selectedItem }) { + const translate = useLanguage(); + const { entity, ENTITY_NAME } = config; + const dispatch = useDispatch(); + const navigate = useNavigate(); + + const { moneyFormatter } = useMoney(); + const { send, isLoading: mailInProgress } = useMail({ entity }); + + const { result: currentResult } = useSelector(selectCurrentItem); + + const resetAdavancedCrud = { + status: '', + client: { + name: '', + email: '', + phone: '', + address: '', + }, + subTotal: 0, + taxTotal: 0, + taxRate: 0, + total: 0, + credit: 0, + number: 0, + year: 0, + }; + + const [itemslist, setItemsList] = useState([]); + const [currentAdavancedCrud, setCurrentAdavancedCrud] = useState( + selectedItem ?? resetAdavancedCrud + ); + const [client, setClient] = useState({}); + + useEffect(() => { + if (currentResult) { + const { items, invoice, ...others } = currentResult; + + if (items) { + setItemsList(items); + setCurrentAdavancedCrud(currentResult); + } else if (invoice.items) { + setItemsList(invoice.items); + setCurrentAdavancedCrud({ ...invoice.items, ...others, ...invoice }); + } + } + return () => { + setItemsList([]); + setCurrentAdavancedCrud(resetAdavancedCrud); + }; + }, [currentResult]); + + useEffect(() => { + if (currentAdavancedCrud?.client) { + setClient(currentAdavancedCrud.client[currentAdavancedCrud.client.type]); + } + }, [currentAdavancedCrud]); + + return ( + <> + { + navigate(`/${entity.toLowerCase()}`); + }} + title={`${ENTITY_NAME} # ${currentAdavancedCrud.number}/${currentAdavancedCrud.year || ''}`} + ghost={false} + tags={[ + + {currentAdavancedCrud.status && translate(currentAdavancedCrud.status)} + , + currentAdavancedCrud.paymentStatus && ( + + {currentAdavancedCrud.paymentStatus && translate(currentAdavancedCrud.paymentStatus)} + + ), + ]} + extra={[ + , + , + , + , + + , + ]} + style={{ + padding: '20px 0px', + }} + > + + + + + + + + + + {client.address} + {client.email} + {client.phone} + + + + +

+ {translate('Product')} +

+ + +

+ {translate('Price')} +

+ + +

+ {translate('Quantity')} +

+ + +

+ {translate('Total')} +

+ + + + {itemslist.map((item) => ( + + ))} +
+ +
+

{translate('Sub Total')} :

+ + + +

{moneyFormatter({ amount: currentAdavancedCrud.subTotal })}

+ + +

+ {translate('Tax Total')} ({currentAdavancedCrud.taxRate} %) : +

+ + +

{moneyFormatter({ amount: currentAdavancedCrud.taxTotal })}

+ + +

{translate('Total')} :

+ + +

{moneyFormatter({ amount: currentAdavancedCrud.total })}

+ + + + + ); +} diff --git a/frontend/src/modules/AdvancedCrudModule/SearchItem.jsx b/frontend/src/modules/AdvancedCrudModule/SearchItem.jsx new file mode 100644 index 000000000..04709abae --- /dev/null +++ b/frontend/src/modules/AdvancedCrudModule/SearchItem.jsx @@ -0,0 +1,96 @@ +import { useEffect, useState, useRef } from 'react'; + +import { AutoComplete, Input } from 'antd'; +import { SearchOutlined } from '@ant-design/icons'; +import { useSelector, useDispatch } from 'react-redux'; +import { adavancedCrud } from '@/redux/adavancedCrud/actions'; + +import { useAdavancedCrudContext } from '@/context/adavancedCrud'; +import { selectSearchedItems } from '@/redux/adavancedCrud/selectors'; + +import { Empty } from 'antd'; + +export default function Search({ config }) { + let { entity, searchConfig } = config; + + const { displayLabels, searchFields, outputValue = '_id' } = searchConfig; + const dispatch = useDispatch(); + const [value, setValue] = useState(''); + const [options, setOptions] = useState([]); + + const { adavancedCrudContextAction } = useAdavancedCrudContext(); + const { panel, collapsedBox, readBox } = adavancedCrudContextAction; + + const { result, isLoading, isSuccess } = useSelector(selectSearchedItems); + + const isTyping = useRef(false); + + let delayTimer = null; + useEffect(() => { + isLoading && setOptions([{ label: '... Searching' }]); + }, [isLoading]); + const onSearch = (searchText) => { + isTyping.current = true; + + clearTimeout(delayTimer); + delayTimer = setTimeout(function () { + if (isTyping.current && searchText !== '') { + dispatch( + adavancedCrud.search(entity, { + question: searchText, + fields: searchFields, + }) + ); + } + isTyping.current = false; + }, 500); + }; + + const onSelect = (data) => { + const currentItem = result.find((item) => { + return item[outputValue] === data; + }); + + dispatch(adavancedCrud.currentItem({ data: currentItem })); + panel.open(); + collapsedBox.open(); + readBox.open(); + }; + + const onChange = (data) => { + const currentItem = options.find((item) => { + return item.value === data; + }); + const currentValue = currentItem ? currentItem.label : data; + setValue(currentValue); + }; + + useEffect(() => { + let optionResults = []; + + result.map((item) => { + const labels = displayLabels.map((x) => item[x]).join(' '); + optionResults.push({ label: labels, value: item[outputValue] }); + }); + + setOptions(optionResults); + }, [result]); + + return ( + : ''} + allowClear={true} + placeholder="Your Search here" + > + } /> + + ); +} diff --git a/frontend/src/modules/AdvancedCrudModule/UpdateItem.jsx b/frontend/src/modules/AdvancedCrudModule/UpdateItem.jsx new file mode 100644 index 000000000..aaea8b396 --- /dev/null +++ b/frontend/src/modules/AdvancedCrudModule/UpdateItem.jsx @@ -0,0 +1,175 @@ +import { useState, useEffect } from 'react'; +import { Form, Divider } from 'antd'; +import dayjs from 'dayjs'; +import { Button, Tag } from 'antd'; +import { PageHeader } from '@ant-design/pro-layout'; + +import { useSelector, useDispatch } from 'react-redux'; +import useLanguage from '@/locale/useLanguage'; +import { adavancedCrud } from '@/redux/adavancedCrud/actions'; + +import calculate from '@/utils/calculate'; +import { generate as uniqueId } from 'shortid'; +import { selectUpdatedItem } from '@/redux/adavancedCrud/selectors'; +import Loading from '@/components/Loading'; +import { tagColor } from '@/utils/statusTagColor'; + +import { CloseCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { useNavigate, useParams } from 'react-router-dom'; +// import { StatusTag } from '@/components/Tag'; + +function SaveForm({ form, translate }) { + const handelClick = () => { + form.submit(); + }; + + return ( + + ); +} + +export default function UpdateItem({ config, UpdateForm }) { + const translate = useLanguage(); + let { entity } = config; + + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { current, isLoading, isSuccess } = useSelector(selectUpdatedItem); + const [form] = Form.useForm(); + const [subTotal, setSubTotal] = useState(0); + + const resetAdavancedCrud = { + status: '', + client: { + name: '', + email: '', + phone: '', + address: '', + }, + subTotal: 0, + taxTotal: 0, + taxRate: 0, + total: 0, + credit: 0, + number: 0, + year: 0, + }; + + const [currentAdavancedCrud, setCurrentAdavancedCrud] = useState(current ?? resetAdavancedCrud); + + const { id } = useParams(); + + const handelValuesChange = (changedValues, values) => { + const items = values['items']; + let subTotal = 0; + + if (items) { + items.map((item) => { + if (item) { + if (item.quantity && item.price) { + let total = calculate.multiply(item['quantity'], item['price']); + //sub total + subTotal = calculate.add(subTotal, total); + } + } + }); + setSubTotal(subTotal); + } + }; + + const onSubmit = (fieldsValue) => { + let dataToUpdate = { ...fieldsValue }; + if (fieldsValue) { + if (fieldsValue.date || fieldsValue.expiredDate) { + dataToUpdate.date = dayjs(fieldsValue.date).format('YYYY-MM-DDTHH:mm:ss.SSSZ'); + dataToUpdate.expiredDate = dayjs(fieldsValue.expiredDate).format( + 'YYYY-MM-DDTHH:mm:ss.SSSZ' + ); + } + if (fieldsValue.items) { + let newList = [...fieldsValue.items]; + newList.map((item) => { + item.total = item.quantity * item.price; + }); + dataToUpdate.items = newList; + } + } + + dispatch(adavancedCrud.update({ entity, id, jsonData: dataToUpdate })); + }; + useEffect(() => { + if (isSuccess) { + form.resetFields(); + setSubTotal(0); + dispatch(adavancedCrud.resetAction({ actionType: 'update' })); + navigate(`/${entity.toLowerCase()}/read/${id}`); + } + }, [isSuccess]); + + useEffect(() => { + if (current) { + setCurrentAdavancedCrud(current); + let formData = { ...current }; + if (formData.date) { + formData.date = dayjs(formData.date); + } + if (formData.expiredDate) { + formData.expiredDate = dayjs(formData.expiredDate); + } + if (!formData.taxRate) { + formData.taxRate = 0; + } + + const { subTotal } = formData; + + form.resetFields(); + form.setFieldsValue(formData); + setSubTotal(subTotal); + } + }, [current]); + + return ( + <> + { + navigate(`/${entity.toLowerCase()}`); + }} + title={translate('update')} + ghost={false} + tags={[ + + {currentAdavancedCrud.status && translate(currentAdavancedCrud.status)} + , + currentAdavancedCrud.paymentStatus && ( + + {currentAdavancedCrud.paymentStatus && translate(currentAdavancedCrud.paymentStatus)} + + ), + ]} + extra={[ + , + , + ]} + style={{ + padding: '20px 0px', + }} + > + + +
+ + +
+ + ); +} diff --git a/frontend/src/modules/AdvancedCrudModule/index.jsx b/frontend/src/modules/AdvancedCrudModule/index.jsx new file mode 100644 index 000000000..e2e00ee70 --- /dev/null +++ b/frontend/src/modules/AdvancedCrudModule/index.jsx @@ -0,0 +1,35 @@ +import { useLayoutEffect } from 'react'; + +import DataTable from './DataTable'; + +import Delete from './DeleteItem'; + +import { useDispatch } from 'react-redux'; +import { adavancedCrud } from '@/redux/adavancedCrud/actions'; + +import { useAdavancedCrudContext } from '@/context/adavancedCrud'; + +export default function AdavancedCrudPanel({ config, extra }) { + const dispatch = useDispatch(); + const { state } = useAdavancedCrudContext(); + const { deleteModal } = state; + + const dispatcher = () => { + dispatch(adavancedCrud.resetState()); + }; + + useLayoutEffect(() => { + const controller = new AbortController(); + dispatcher(); + return () => { + controller.abort(); + }; + }, []); + + return ( + <> + + + + ); +} diff --git a/frontend/src/modules/AuthModule/SideContent.jsx b/frontend/src/modules/AuthModule/SideContent.jsx index c1fcca01a..0e1596ec3 100644 --- a/frontend/src/modules/AuthModule/SideContent.jsx +++ b/frontend/src/modules/AuthModule/SideContent.jsx @@ -2,19 +2,21 @@ import { Space, Layout, Divider, Typography } from 'antd'; import logo from '@/style/images/idurar-crm-erp.svg'; import useLanguage from '@/locale/useLanguage'; import { useSelector } from 'react-redux'; +import { selectLangDirection } from '@/redux/translate/selectors'; const { Content } = Layout; const { Title, Text } = Typography; export default function SideContent() { const translate = useLanguage(); + const langDirection = useSelector(selectLangDirection) return ( - - - Free Open Source ERP / CRM - - - Accounting / Invoicing / Quote App based on Node.js React.js Ant Design - +
+ {translate('Manage your company with')} :
+
    +
  • + + {translate('All-in-one tool')} + + {translate('Run and scale your ERP CRM Apps')} + +
  • + +
  • + + {translate('Easily add and manage your services')} + {translate('It brings together your invoice clients and leads')} + +
  • +
+ +
+ {/* Logo1 + Logo2 + Logo3 + Logo4 */} +
); diff --git a/frontend/src/modules/DashboardModule/components/CustomerPreviewCard.jsx b/frontend/src/modules/DashboardModule/components/CustomerPreviewCard.jsx index 9acde89dc..2b87225ae 100644 --- a/frontend/src/modules/DashboardModule/components/CustomerPreviewCard.jsx +++ b/frontend/src/modules/DashboardModule/components/CustomerPreviewCard.jsx @@ -1,6 +1,6 @@ -import { Statistic, Progress, Divider, Row, Spin } from 'antd'; -import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'; -import useLanguage from '@/locale/useLanguage'; +import { Statistic, Progress, Divider, Row, Spin } from "antd"; +import { ArrowUpOutlined, ArrowDownOutlined } from "@ant-design/icons"; +import useLanguage from "@/locale/useLanguage"; export default function CustomerPreviewCard({ isLoading = false, @@ -14,12 +14,12 @@ export default function CustomerPreviewCard({
-

- {translate('Customers')} +

+ {translate("Customers")}

{isLoading ? ( @@ -27,23 +27,23 @@ export default function CustomerPreviewCard({ ) : (
-

{translate('New Customer this Month')}

+

{translate("New Customer this Month")}

0 - ? { color: '#333' } + ? { color: "#3f8600" } : activeCustomer < 0 - ? { color: '#333' } - : { color: '#000000' } + ? { color: "#cf1322" } + : { color: "#000000" } } prefix={ activeCustomer > 0 ? ( diff --git a/frontend/src/modules/DashboardModule/components/PreviewCard.jsx b/frontend/src/modules/DashboardModule/components/PreviewCard.jsx index d2ca4c90e..0429bd46a 100644 --- a/frontend/src/modules/DashboardModule/components/PreviewCard.jsx +++ b/frontend/src/modules/DashboardModule/components/PreviewCard.jsx @@ -71,7 +71,7 @@ const defaultInvoiceStatistics = [ }, ]; -const PreviewState = ({ tag, value }) => { +const PreviewState = ({ tag, color, value }) => { const translate = useLanguage(); return (
@@ -81,8 +81,8 @@ const PreviewState = ({ tag, value }) => { percent={value} showInfo={false} strokeColor={{ - '0%': '#333', - '100%': '#333', + '0%': color, + '100%': color, }} />
@@ -124,8 +124,8 @@ export default function PreviewCard({ className="gutter-row" xs={{ span: 24 }} sm={{ span: 24 }} - md={{ span: 12 }} - lg={{ span: 12 }} + md={{ span: 8 }} + lg={{ span: 8 }} >

( - + // sort by colours )) .sort(customSort) diff --git a/frontend/src/modules/DashboardModule/index.jsx b/frontend/src/modules/DashboardModule/index.jsx index 2dfbb450e..99f1c21a3 100644 --- a/frontend/src/modules/DashboardModule/index.jsx +++ b/frontend/src/modules/DashboardModule/index.jsx @@ -8,6 +8,7 @@ import { useMoney } from '@/settings'; import { request } from '@/request'; import useFetch from '@/hooks/useFetch'; import useOnFetch from '@/hooks/useOnFetch'; +import { tagColor } from '@/utils/statusTagColor'; import RecentTable from './components/RecentTable'; @@ -38,6 +39,8 @@ export default function DashboardModule() { const { result: quoteResult, isLoading: quoteLoading, onFetch: fetchQuotesStats } = useOnFetch(); + const { result: offerResult, isLoading: offerLoading, onFetch: fetchOffersStats } = useOnFetch(); + const { result: paymentResult, isLoading: paymentLoading, @@ -54,6 +57,7 @@ export default function DashboardModule() { if (currency) { fetchInvoicesStats(getStatsData({ entity: 'invoice', currency })); fetchQuotesStats(getStatsData({ entity: 'quote', currency })); + fetchOffersStats(getStatsData({ entity: 'offer', currency })); fetchPayemntsStats(getStatsData({ entity: 'payment', currency })); } }, [money_format_settings.default_currency_code]); @@ -85,6 +89,9 @@ export default function DashboardModule() { { title: translate('Status'), dataIndex: 'status', + render: (status) => { + return {translate(status)}; + }, }, ]; @@ -99,7 +106,13 @@ export default function DashboardModule() { result: quoteResult, isLoading: quoteLoading, entity: 'quote', - title: translate('quote'), + title: translate('proforma invoices'), + }, + { + result: offerResult, + isLoading: offerLoading, + entity: 'offer', + title: translate('offers'), }, ]; @@ -130,24 +143,28 @@ export default function DashboardModule() { + + + ); +} diff --git a/frontend/src/modules/EmailModule/ReadEmailModule/components/ReadItem.jsx b/frontend/src/modules/EmailModule/ReadEmailModule/components/ReadItem.jsx new file mode 100644 index 000000000..d076a7a4c --- /dev/null +++ b/frontend/src/modules/EmailModule/ReadEmailModule/components/ReadItem.jsx @@ -0,0 +1,102 @@ +import { useState, useEffect } from 'react'; + +import { Divider, Typography, Button } from 'antd'; +import { PageHeader } from '@ant-design/pro-layout'; +import { EditOutlined, CloseCircleOutlined } from '@ant-design/icons'; + +import { useSelector, useDispatch } from 'react-redux'; +import { erp } from '@/redux/erp/actions'; + +import { useErpContext } from '@/context/erp'; +import { generate as uniqueId } from 'shortid'; + +import { selectCurrentItem } from '@/redux/erp/selectors'; + +import { useNavigate } from 'react-router-dom'; +import useLanguage from '@/locale/useLanguage'; + +const { Title, Paragraph } = Typography; + +export default function ReadItem({ config, selectedItem }) { + const translate = useLanguage(); + const { entity, ENTITY_NAME } = config; + const dispatch = useDispatch(); + const { erpContextAction } = useErpContext(); + const navigate = useNavigate(); + + const { result: currentResult } = useSelector(selectCurrentItem); + + const { readPanel, updatePanel } = erpContextAction; + + const resetErp = { + emailName: '', + emailKey: '', + emailSubject: '', + emailBody: '', + emailVariables: [], + _id: '', + }; + + const [currentErp, setCurrentErp] = useState(selectedItem ?? resetErp); + + useEffect(() => { + const controller = new AbortController(); + if (currentResult) { + setCurrentErp(currentResult); + } + return () => controller.abort(); + }, [currentResult]); + + return ( + <> + { + readPanel.close(); + navigate(`/${entity.toLowerCase()}`);//navigate to previous page + }} + title={`${ENTITY_NAME} # ${currentErp?.emailName}`} + ghost={false} + extra={[ + , + + , + ]} + style={{ + padding: '20px 0px', + }} + > + +
+ {translate('Subject')} + {currentErp.emailSubject} + {translate('Body')} +
+
+ + ); +} diff --git a/frontend/src/modules/EmailModule/ReadEmailModule/index.jsx b/frontend/src/modules/EmailModule/ReadEmailModule/index.jsx new file mode 100644 index 000000000..e4f80954b --- /dev/null +++ b/frontend/src/modules/EmailModule/ReadEmailModule/index.jsx @@ -0,0 +1,39 @@ +import NotFound from '@/components/NotFound'; +import { ErpLayout } from '@/layout'; +import ReadItem from './components/ReadItem'; + +import PageLoader from '@/components/PageLoader'; +import { erp } from '@/redux/erp/actions'; +import { selectReadItem } from '@/redux/erp/selectors'; +import { useLayoutEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { useParams } from 'react-router-dom'; + +export default function ReadEmailModule({ config }) { + const dispatch = useDispatch(); + const { id } = useParams(); + + useLayoutEffect(() => { + dispatch(erp.read({ entity: config.entity, id })); + }, [id]); + + const { result: currentResult, isSuccess, isLoading = true } = useSelector(selectReadItem); + + if (isLoading) { + return ( + + + + ); + } else + return ( + + {isSuccess ? ( + + ) : ( + + )} + + ); +} diff --git a/frontend/src/modules/EmailModule/UpdateEmailModule/componenets/EmailForm.jsx b/frontend/src/modules/EmailModule/UpdateEmailModule/componenets/EmailForm.jsx new file mode 100644 index 000000000..b0a6be681 --- /dev/null +++ b/frontend/src/modules/EmailModule/UpdateEmailModule/componenets/EmailForm.jsx @@ -0,0 +1,55 @@ +import { useState } from 'react'; +import { Form, Input, Button, Row, Col, Typography, Tag } from 'antd'; +import ReactQuill from 'react-quill'; +import 'react-quill/dist/quill.snow.css'; +import { PlusOutlined } from '@ant-design/icons'; +import useLanguage from '@/locale/useLanguage'; + +const { Paragraph } = Typography; + +export default function EmailForm({ current = null }) { + const translate = useLanguage(); + const [body, setBody] = useState(current?.emailBody); + + const displayLabels = (labels = []) => ( + <> + {labels.map((label, index) => ( + setBody(body)} color="blue"> + {label} + + ))} + + ); + + const setBodyValue = (value) => { + setBody(value); + current.emailBody = value; + }; + + return ( + +

+ {translate('Available Variables')} : + {displayLabels(current?.emailVariables)} +
+ + + + + + + + + {translate('To write a variable name use the convention')} {`{{variable}}`} e.g. name -{' '} + {`{{name}}`} + + + + + + + + ); +} diff --git a/frontend/src/modules/EmailModule/UpdateEmailModule/index.jsx b/frontend/src/modules/EmailModule/UpdateEmailModule/index.jsx new file mode 100644 index 000000000..c771ac874 --- /dev/null +++ b/frontend/src/modules/EmailModule/UpdateEmailModule/index.jsx @@ -0,0 +1,49 @@ +import NotFound from '@/components/NotFound'; + +import { ErpLayout } from '@/layout'; +import UpdateItem from '@/modules/ErpPanelModule/UpdateItem'; +import EmailForm from './componenets/EmailForm'; + +import PageLoader from '@/components/PageLoader'; + +import { erp } from '@/redux/erp/actions'; +import { selectReadItem } from '@/redux/erp/selectors'; +import { useLayoutEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { useParams } from 'react-router-dom'; + +export default function UpdateEmailModule({ config }) { + const dispatch = useDispatch(); + + const { id } = useParams(); + + useLayoutEffect(() => { + dispatch(erp.read({ entity: config.entity, id })); + }, [id]); + + const { result: currentResult, isSuccess, isLoading = true } = useSelector(selectReadItem); + + useLayoutEffect(() => { + if (currentResult) { + dispatch(erp.currentAction({ actionType: 'update', data: currentResult })); + } + }, [currentResult]); + + if (isLoading) { + return ( + + + + ); + } else + return ( + + {isSuccess ? ( + + ) : ( + + )} + + ); +} diff --git a/frontend/src/modules/ErpPanelModule/CreateItem.jsx b/frontend/src/modules/ErpPanelModule/CreateItem.jsx index 0cb135440..5c91c6505 100644 --- a/frontend/src/modules/ErpPanelModule/CreateItem.jsx +++ b/frontend/src/modules/ErpPanelModule/CreateItem.jsx @@ -23,6 +23,7 @@ import { } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; +import { selectLangDirection } from '@/redux/translate/selectors'; function SaveForm({ form }) { const translate = useLanguage(); @@ -102,14 +103,14 @@ export default function CreateItem({ config, CreateForm }) { } dispatch(erp.create({ entity, jsonData: fieldsValue })); }; - + const langDirection = useSelector(selectLangDirection); return ( <> { navigate(`/${entity.toLowerCase()}`); }} - backIcon={} + backIcon={langDirection === 'rtl' ? : } title={translate('New')} ghost={false} tags={{translate('Draft')}} diff --git a/frontend/src/modules/ErpPanelModule/DataTable.jsx b/frontend/src/modules/ErpPanelModule/DataTable.jsx index 1404ba1e5..6daaa59c8 100644 --- a/frontend/src/modules/ErpPanelModule/DataTable.jsx +++ b/frontend/src/modules/ErpPanelModule/DataTable.jsx @@ -23,6 +23,7 @@ import { generate as uniqueId } from 'shortid'; import { useNavigate } from 'react-router-dom'; import { DOWNLOAD_BASE_URL } from '@/config/serverApiConfig'; +import { selectLangDirection } from '@/redux/translate/selectors'; function AddNewItem({ config }) { const navigate = useNavigate(); @@ -172,6 +173,7 @@ export default function DataTable({ config, extra = [] }) { const options = { equal: value, filter: searchConfig?.entity }; dispatch(erp.list({ entity, options })); }; + const langDirection=useSelector(selectLangDirection) return ( <> @@ -179,7 +181,7 @@ export default function DataTable({ config, extra = [] }) { title={DATATABLE_TITLE} ghost={true} onBack={() => window.history.back()} - backIcon={} + backIcon={langDirection==="rtl"?:} extra={[ diff --git a/frontend/src/modules/ErpPanelModule/ReadItem.jsx b/frontend/src/modules/ErpPanelModule/ReadItem.jsx index 364f3070f..fe9ada0b0 100644 --- a/frontend/src/modules/ErpPanelModule/ReadItem.jsx +++ b/frontend/src/modules/ErpPanelModule/ReadItem.jsx @@ -23,6 +23,7 @@ import { DOWNLOAD_BASE_URL } from '@/config/serverApiConfig'; import { useMoney, useDate } from '@/settings'; import useMail from '@/hooks/useMail'; import { useNavigate } from 'react-router-dom'; +import { tagColor } from '@/utils/statusTagColor'; const Item = ({ item, currentErp }) => { const { moneyFormatter } = useMoney(); @@ -119,7 +120,7 @@ export default function ReadItem({ config, selectedItem }) { useEffect(() => { if (currentErp?.client) { - setClient(currentErp.client); + setClient(currentErp.client[currentErp.client.type]); } }, [currentErp]); @@ -132,11 +133,13 @@ export default function ReadItem({ config, selectedItem }) { title={`${ENTITY_NAME} # ${currentErp.number}/${currentErp.year || ''}`} ghost={false} tags={[ - {currentErp.status && translate(currentErp.status)}, + + {currentErp.status && translate(currentErp.status)} + , currentErp.paymentStatus && ( - + {currentErp.paymentStatus && translate(currentErp.paymentStatus)} - + ), ]} extra={[ diff --git a/frontend/src/modules/ErpPanelModule/UpdateItem.jsx b/frontend/src/modules/ErpPanelModule/UpdateItem.jsx index 580bd444c..7e5ff02e2 100644 --- a/frontend/src/modules/ErpPanelModule/UpdateItem.jsx +++ b/frontend/src/modules/ErpPanelModule/UpdateItem.jsx @@ -12,6 +12,7 @@ import calculate from '@/utils/calculate'; import { generate as uniqueId } from 'shortid'; import { selectUpdatedItem } from '@/redux/erp/selectors'; import Loading from '@/components/Loading'; +import { tagColor } from '@/utils/statusTagColor'; import { CloseCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { useNavigate, useParams } from 'react-router-dom'; @@ -142,11 +143,13 @@ export default function UpdateItem({ config, UpdateForm }) { title={translate('update')} ghost={false} tags={[ - {currentErp.status && translate(currentErp.status)}, + + {currentErp.status && translate(currentErp.status)} + , currentErp.paymentStatus && ( - + {currentErp.paymentStatus && translate(currentErp.paymentStatus)} - + ), ]} extra={[ diff --git a/frontend/src/modules/InvoiceModule/ReadInvoiceModule/index.jsx b/frontend/src/modules/InvoiceModule/ReadInvoiceModule/index.jsx index 18c9c8d7b..a29ddde9a 100644 --- a/frontend/src/modules/InvoiceModule/ReadInvoiceModule/index.jsx +++ b/frontend/src/modules/InvoiceModule/ReadInvoiceModule/index.jsx @@ -20,6 +20,12 @@ export default function ReadInvoiceModule({ config }) { const { result: currentResult, isSuccess, isLoading = true } = useSelector(selectReadItem); + // Function to encrypt the invoice ID and make it shorter + const encryptId = (id) => { + const encoded = btoa(id); + return encoded.substring(0, 8); // Take only the first 8 characters + }; + if (isLoading) { return ( @@ -29,6 +35,9 @@ export default function ReadInvoiceModule({ config }) { } else return ( +
+

Invoice Id: {encryptId(id)}

+
{isSuccess ? ( ) : ( diff --git a/frontend/src/modules/InvoiceModule/RecordPaymentModule/components/Payment.jsx b/frontend/src/modules/InvoiceModule/RecordPaymentModule/components/Payment.jsx index 90ba819a5..4fb0b1fa4 100644 --- a/frontend/src/modules/InvoiceModule/RecordPaymentModule/components/Payment.jsx +++ b/frontend/src/modules/InvoiceModule/RecordPaymentModule/components/Payment.jsx @@ -10,7 +10,7 @@ import { useMoney } from '@/settings'; import RecordPayment from './RecordPayment'; import useLanguage from '@/locale/useLanguage'; - +import { tagColor } from '@/utils/statusTagColor'; import { useNavigate } from 'react-router-dom'; export default function Payment({ config, currentItem }) { @@ -26,7 +26,7 @@ export default function Payment({ config, currentItem }) { const [client, setClient] = useState({}); useEffect(() => { if (currentErp?.client) { - setClient(currentErp.client); + setClient(currentErp.client[currentErp.client.type]); } }, [currentErp]); @@ -41,6 +41,10 @@ export default function Payment({ config, currentItem }) { return () => controller.abort(); }, [currentItem]); + useEffect(() => { + console.info('itemslist', itemslist); + }, [itemslist]); + return ( <> @@ -57,7 +61,11 @@ export default function Payment({ config, currentItem }) { currentErp.year || '' }`} ghost={false} - tags={{currentErp.paymentStatus && translate(currentErp.paymentStatus)}} + tags={ + + {currentErp.paymentStatus && translate(currentErp.paymentStatus)} + + } // subTitle="This is cuurent erp page" extra={[
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

{translate('Item')}

+ +
+

{translate('Description')}

+ +
+

{translate('Quantity')}

{' '} + +
+

{translate('Price')}

+ +
+

{translate('Total')}

+ + + + {(fields, { add, remove }) => ( + <> + {fields.map((field) => ( + + ))} + + + + + )} + + +
+ +
+ + + + + +

+ {translate('Sub Total')} : +

+ +
+ + + + + + + + + + + + + + + +

+ {translate('Total')} : +

+ +
+ + + + + + ); +} diff --git a/frontend/src/modules/OfferModule/OfferDataTableModule/index.jsx b/frontend/src/modules/OfferModule/OfferDataTableModule/index.jsx new file mode 100644 index 000000000..26a7628ea --- /dev/null +++ b/frontend/src/modules/OfferModule/OfferDataTableModule/index.jsx @@ -0,0 +1,10 @@ +import { ErpLayout } from '@/layout'; +import ErpPanel from '@/modules/ErpPanelModule'; + +export default function OffereDataTableModule({ config }) { + return ( + + + + ); +} diff --git a/frontend/src/modules/OfferModule/ReadOfferModule/ReadOfferItem.jsx b/frontend/src/modules/OfferModule/ReadOfferModule/ReadOfferItem.jsx new file mode 100644 index 000000000..485f93cff --- /dev/null +++ b/frontend/src/modules/OfferModule/ReadOfferModule/ReadOfferItem.jsx @@ -0,0 +1,318 @@ +import { useState, useEffect } from 'react'; + +import { Button, Row, Col, Descriptions, Statistic, Tag, Divider } from 'antd'; +import { PageHeader } from '@ant-design/pro-layout'; +import { + EditOutlined, + FilePdfOutlined, + CloseCircleOutlined, + RetweetOutlined, + MailOutlined, +} from '@ant-design/icons'; + +import { useSelector, useDispatch } from 'react-redux'; +import useLanguage from '@/locale/useLanguage'; +import { erp } from '@/redux/erp/actions'; + +import { generate as uniqueId } from 'shortid'; + +import { selectCurrentItem } from '@/redux/erp/selectors'; + +import { DOWNLOAD_BASE_URL } from '@/config/serverApiConfig'; +import { useMoney, useDate } from '@/settings'; +import useMail from '@/hooks/useMail'; +import { useNavigate } from 'react-router-dom'; +import { tagColor } from '@/utils/statusTagColor'; + +const Item = ({ item, currentErp }) => { + const { moneyFormatter } = useMoney(); + return ( + + +

+ {item.itemName} +

+

{item.description}

+ +
+

+ {moneyFormatter({ amount: item.price, currency_code: currentErp.currency })} +

+ +
+

+ {item.quantity} +

+ +
+

+ {moneyFormatter({ amount: item.total, currency_code: currentErp.currency })} +

+ + + + ); +}; + +export default function ReadOfferItem({ config, selectedItem }) { + const translate = useLanguage(); + const { entity, ENTITY_NAME } = config; + const dispatch = useDispatch(); + + const { moneyFormatter } = useMoney(); + const { send, isLoading: mailInProgress } = useMail({ entity }); + const navigate = useNavigate(); + const [lead, setLead] = useState({}); + + const { result: currentResult } = useSelector(selectCurrentItem); + + const resetErp = { + status: '', + lead: { + name: '', + email: '', + phone: '', + address: '', + }, + subTotal: 0, + taxTotal: 0, + taxRate: 0, + total: 0, + credit: 0, + number: 0, + year: 0, + }; + + const [itemslist, setItemsList] = useState([]); + const [currentErp, setCurrentErp] = useState(selectedItem ?? resetErp); + + useEffect(() => { + const controller = new AbortController(); + if (currentResult) { + const { items, invoice, ...others } = currentResult; + + // When it accesses the /payment/ endpoint, + // it receives an invoice.item instead of just item + // and breaks the code, but now we can check if items exists, + // and if it doesn't we can access invoice.items and bring + // out the neccessary propery alongside other properties + + if (items) { + setItemsList(items); + setCurrentErp(currentResult); + } else if (invoice.items) { + setItemsList(invoice.items); + setCurrentErp({ ...invoice.items, ...others, ...invoice }); + } + } + return () => controller.abort(); + }, [currentResult]); + + useEffect(() => { + if (currentErp?.lead) { + setLead(currentErp.lead[currentErp.lead.type]); + } + }, [currentErp]); + + return ( + <> + { + navigate(`/${entity.toLowerCase()}`); + }} + title={`${ENTITY_NAME} # ${currentErp.number}/${currentErp.year || ''}`} + ghost={false} + tags={{translate(currentErp.status)}} + // subTitle="This is cuurent erp page" + extra={[ + , + , + , + , + + , + ]} + style={{ + padding: '20px 0px', + }} + > + + + + + + + + + + {lead.address} + {lead.email} + {lead.phone} + + + +
+

+ {translate('product')} +

+ +
+

+ {translate('PRICE')} +

+ +
+

+ {translate('QUANTITY')} +

+ +
+

+ {translate('TOTAL')} +

+ + + + {itemslist.map((item) => ( + + ))} +
+ +
+

{translate('Sub Total')} :

+ + +
+

+ {moneyFormatter({ amount: currentErp.subTotal, currency_code: currentErp.currency })} +

+ +
+

Tax Total ({currentErp.taxRate} %) :

+ +
+

+ {moneyFormatter({ amount: currentErp.taxTotal, currency_code: currentErp.currency })} +

+ +
+

{translate('Total')} :

+ +
+

+ {moneyFormatter({ amount: currentErp.total, currency_code: currentErp.currency })} +

+ + + + + ); +} diff --git a/frontend/src/modules/OfferModule/ReadOfferModule/index.jsx b/frontend/src/modules/OfferModule/ReadOfferModule/index.jsx new file mode 100644 index 000000000..8604a89b0 --- /dev/null +++ b/frontend/src/modules/OfferModule/ReadOfferModule/index.jsx @@ -0,0 +1,40 @@ +import NotFound from '@/components/NotFound'; +import { ErpLayout } from '@/layout'; +import ReadOfferItem from './ReadOfferItem'; + +import PageLoader from '@/components/PageLoader'; +import { erp } from '@/redux/erp/actions'; +import useLanguage from '@/locale/useLanguage'; +import { selectReadItem } from '@/redux/erp/selectors'; +import { useLayoutEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useParams, useNavigate } from 'react-router-dom'; + +export default function ReadOfferModule({ config }) { + const dispatch = useDispatch(); + const { id } = useParams(); + const navigate = useNavigate(); + + useLayoutEffect(() => { + dispatch(erp.read({ entity: config.entity, id })); + }, [id]); + + const { result: currentResult, isSuccess, isLoading = true } = useSelector(selectReadItem); + + if (isLoading) { + return ( + + + + ); + } else + return ( + + {isSuccess ? ( + + ) : ( + + )} + + ); +} diff --git a/frontend/src/modules/OfferModule/UpdateOfferModule/index.jsx b/frontend/src/modules/OfferModule/UpdateOfferModule/index.jsx new file mode 100644 index 000000000..ff48c87c9 --- /dev/null +++ b/frontend/src/modules/OfferModule/UpdateOfferModule/index.jsx @@ -0,0 +1,50 @@ +import NotFound from '@/components/NotFound'; + +import { ErpLayout } from '@/layout'; +import UpdateItem from '@/modules/ErpPanelModule/UpdateItem'; +import OfferForm from '@/modules/OfferModule/Forms/OfferForm'; + +import PageLoader from '@/components/PageLoader'; + +import { erp } from '@/redux/erp/actions'; +import useLanguage from '@/locale/useLanguage'; +import { selectReadItem } from '@/redux/erp/selectors'; +import { useLayoutEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useParams, useNavigate } from 'react-router-dom'; + +export default function UpdateOfferModule({ config }) { + const dispatch = useDispatch(); + + const { id } = useParams(); + const navigate = useNavigate(); + + useLayoutEffect(() => { + dispatch(erp.read({ entity: config.entity, id })); + }, [id]); + + const { result: currentResult, isSuccess, isLoading = true } = useSelector(selectReadItem); + + useLayoutEffect(() => { + if (currentResult) { + dispatch(erp.currentAction({ actionType: 'update', data: currentResult })); + } + }, [currentResult]); + + if (isLoading) { + return ( + + + + ); + } else + return ( + + {isSuccess ? ( + + ) : ( + + )} + + ); +} diff --git a/frontend/src/modules/OrderModule/CreateOrderModule/index.jsx b/frontend/src/modules/OrderModule/CreateOrderModule/index.jsx new file mode 100644 index 000000000..369298dbc --- /dev/null +++ b/frontend/src/modules/OrderModule/CreateOrderModule/index.jsx @@ -0,0 +1,11 @@ +import { ErpLayout } from '@/layout'; +import CreateItem from '@/modules/AdvancedCrudModule/CreateItem'; +import OrderForm from '@/modules/OrderModule/Forms/OrderForm'; + +export default function CreateInvoiceModule({ config }) { + return ( + + + + ); +} diff --git a/frontend/src/modules/OrderModule/Forms/InvoiceForm.jsx b/frontend/src/modules/OrderModule/Forms/InvoiceForm.jsx new file mode 100644 index 000000000..4b4cc663f --- /dev/null +++ b/frontend/src/modules/OrderModule/Forms/InvoiceForm.jsx @@ -0,0 +1,284 @@ +import { useState, useEffect, useRef } from 'react'; +import dayjs from 'dayjs'; +import { Form, Input, InputNumber, Button, Select, Divider, Row, Col } from 'antd'; + +import { PlusOutlined } from '@ant-design/icons'; + +import { DatePicker } from 'antd'; + +import AutoCompleteAsync from '@/components/AutoCompleteAsync'; + +import ItemRow from '@/modules/ErpPanelModule/ItemRow'; + +import MoneyInputFormItem from '@/components/MoneyInputFormItem'; +import { selectFinanceSettings } from '@/redux/settings/selectors'; +import { useDate } from '@/settings'; +import useLanguage from '@/locale/useLanguage'; + +import calculate from '@/utils/calculate'; +import { useSelector } from 'react-redux'; +import SelectAsync from '@/components/SelectAsync'; + +export default function InvoiceForm({ subTotal = 0, current = null }) { + const { last_invoice_number } = useSelector(selectFinanceSettings); + + if (!last_invoice_number) { + return <>; + } + + return ; +} + +function LoadInvoiceForm({ subTotal = 0, current = null }) { + const translate = useLanguage(); + const { dateFormat } = useDate(); + const { last_invoice_number } = useSelector(selectFinanceSettings); + const [total, setTotal] = useState(0); + const [taxRate, setTaxRate] = useState(0); + const [taxTotal, setTaxTotal] = useState(0); + const [currentYear, setCurrentYear] = useState(() => new Date().getFullYear()); + const [lastNumber, setLastNumber] = useState(() => last_invoice_number + 1); + + const handelTaxChange = (value) => { + setTaxRate(value / 100); + }; + + useEffect(() => { + if (current) { + const { taxRate = 0, year, number } = current; + setTaxRate(taxRate / 100); + setCurrentYear(year); + setLastNumber(number); + } + }, [current]); + useEffect(() => { + const currentTotal = calculate.add(calculate.multiply(subTotal, taxRate), subTotal); + setTaxTotal(Number.parseFloat(calculate.multiply(subTotal, taxRate))); + setTotal(Number.parseFloat(currentTotal)); + }, [subTotal, taxRate]); + + const addField = useRef(false); + + useEffect(() => { + addField.current.click(); + }, []); + + return ( + <> + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

{translate('Item')}

+ +
+

{translate('Description')}

+ +
+

{translate('Quantity')}

{' '} + +
+

{translate('Price')}

+ +
+

{translate('Total')}

+ + + + {(fields, { add, remove }) => ( + <> + {fields.map((field) => ( + + ))} + + + + + )} + + +
+ +
+ + + + + +

+ {translate('Sub Total')} : +

+ +
+ + + + + + + + + + + + + + + +

+ {translate('Total')} : +

+ +
+ + + + + + ); +} diff --git a/frontend/src/modules/OrderModule/OrderDataTableModule/index.jsx b/frontend/src/modules/OrderModule/OrderDataTableModule/index.jsx new file mode 100644 index 000000000..c0f55e2c8 --- /dev/null +++ b/frontend/src/modules/OrderModule/OrderDataTableModule/index.jsx @@ -0,0 +1,22 @@ +import { ErpLayout } from '@/layout'; +import AdvancedCrudModule from '@/modules/AdvancedCrudModule'; +import useLanguage from '@/locale/useLanguage'; +import { CreditCardOutlined } from '@ant-design/icons'; + +export default function InvoiceDataTableModule({ config }) { + const translate = useLanguage(); + return ( + + , + // }, + // ]} + > + + ); +} diff --git a/frontend/src/modules/OrderModule/ReadOrderModule/index.jsx b/frontend/src/modules/OrderModule/ReadOrderModule/index.jsx new file mode 100644 index 000000000..a578d9bfd --- /dev/null +++ b/frontend/src/modules/OrderModule/ReadOrderModule/index.jsx @@ -0,0 +1,39 @@ +import NotFound from '@/components/NotFound'; +import { ErpLayout } from '@/layout'; +import ReadItem from '@/modules/AdvancedCrudModule/ReadItem'; + +import PageLoader from '@/components/PageLoader'; +import { adavancedCrud } from '@/redux/adavancedCrud/actions'; +import { selectReadItem } from '@/redux/adavancedCrud/selectors'; +import { useLayoutEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { useParams } from 'react-router-dom'; + +export default function ReadInvoiceModule({ config }) { + const dispatch = useDispatch(); + const { id } = useParams(); + + useLayoutEffect(() => { + dispatch(adavancedCrud.read({ entity: config.entity, id })); + }, [id]); + + const { result: currentResult, isSuccess, isLoading = true } = useSelector(selectReadItem); + + if (isLoading) { + return ( + + + + ); + } else + return ( + + {isSuccess ? ( + + ) : ( + + )} + + ); +} diff --git a/frontend/src/modules/OrderModule/RecordPaymentModule/components/Payment.jsx b/frontend/src/modules/OrderModule/RecordPaymentModule/components/Payment.jsx new file mode 100644 index 000000000..f3789e6ab --- /dev/null +++ b/frontend/src/modules/OrderModule/RecordPaymentModule/components/Payment.jsx @@ -0,0 +1,140 @@ +import { useState, useEffect } from 'react'; + +import { Button, Row, Col, Descriptions, Tag, Divider } from 'antd'; +import { PageHeader } from '@ant-design/pro-layout'; +import { FileTextOutlined, CloseCircleOutlined } from '@ant-design/icons'; + +import { generate as uniqueId } from 'shortid'; + +import { useMoney } from '@/settings'; + +import RecordPayment from './RecordPayment'; +import useLanguage from '@/locale/useLanguage'; +import { tagColor } from '@/utils/statusTagColor'; +import { useNavigate } from 'react-router-dom'; + +export default function Payment({ config, currentItem }) { + const translate = useLanguage(); + const { entity, ENTITY_NAME } = config; + + const money = useMoney(); + const navigate = useNavigate(); + + const [itemslist, setItemsList] = useState([]); + const [currentErp, setCurrentErp] = useState(currentItem); + + const [client, setClient] = useState({}); + useEffect(() => { + if (currentErp?.client) { + setClient(currentErp.client[currentErp.client.type]); + } + }, [currentErp]); + + useEffect(() => { + const controller = new AbortController(); + if (currentItem) { + const { items } = currentItem; + + setItemsList(items); + setCurrentErp(currentItem); + } + return () => controller.abort(); + }, [currentItem]); + + useEffect(() => { + console.info('itemslist', itemslist); + }, [itemslist]); + + return ( + <> + + + navigate(`/${entity.toLowerCase()}`)} + title={`Record Payment for ${ENTITY_NAME} # ${currentErp.number}/${ + currentErp.year || '' + }`} + ghost={false} + tags={ + + {currentErp.paymentStatus && translate(currentErp.paymentStatus)} + + } + // subTitle="This is cuurent erp page" + extra={[ + , + , + ]} + style={{ + padding: '20px 0px', + }} + > + + + + + +
+ + {client.email} + {client.phone} + + + + {currentErp.paymentStatus && translate(currentErp.paymentStatus)} + + + + {money.amountFormatter({ amount: currentErp.subTotal })} + + + {money.amountFormatter({ amount: currentErp.total })} + + + {money.amountFormatter({ amount: currentErp.discount })} + + + {money.amountFormatter({ amount: currentErp.credit })} + + + + +
+ + + + + ); +} diff --git a/frontend/src/modules/OrderModule/RecordPaymentModule/components/RecordPayment.jsx b/frontend/src/modules/OrderModule/RecordPaymentModule/components/RecordPayment.jsx new file mode 100644 index 000000000..3fa37582c --- /dev/null +++ b/frontend/src/modules/OrderModule/RecordPaymentModule/components/RecordPayment.jsx @@ -0,0 +1,73 @@ +import { useState, useEffect } from 'react'; +import { Form, Button } from 'antd'; + +import { useSelector, useDispatch } from 'react-redux'; +import { erp } from '@/redux/erp/actions'; +import { selectRecordPaymentItem } from '@/redux/erp/selectors'; +import useLanguage from '@/locale/useLanguage'; + +import Loading from '@/components/Loading'; + +import PaymentForm from '@/forms/PaymentForm'; +import { useNavigate } from 'react-router-dom'; +import calculate from '@/utils/calculate'; + +export default function RecordPayment({ config }) { + const navigate = useNavigate(); + const translate = useLanguage(); + let { entity } = config; + + const dispatch = useDispatch(); + + const { isLoading, isSuccess, current: currentInvoice } = useSelector(selectRecordPaymentItem); + + const [form] = Form.useForm(); + + const [maxAmount, setMaxAmount] = useState(0); + useEffect(() => { + if (currentInvoice) { + const { credit, total, discount } = currentInvoice; + setMaxAmount(calculate.sub(calculate.sub(total, discount), credit)); + } + }, [currentInvoice]); + useEffect(() => { + if (isSuccess) { + form.resetFields(); + dispatch(erp.resetAction({ actionType: 'recordPayment' })); + dispatch(erp.list({ entity })); + navigate(`/${entity}/`); + } + }, [isSuccess]); + + const onSubmit = (fieldsValue) => { + if (currentInvoice) { + const { _id: invoice } = currentInvoice; + const client = currentInvoice.client && currentInvoice.client._id; + fieldsValue = { + ...fieldsValue, + invoice, + client, + }; + } + + dispatch( + erp.recordPayment({ + entity: 'payment', + jsonData: fieldsValue, + }) + ); + }; + + return ( + +
+ + + + + +
+ ); +} diff --git a/frontend/src/modules/OrderModule/RecordPaymentModule/index.jsx b/frontend/src/modules/OrderModule/RecordPaymentModule/index.jsx new file mode 100644 index 000000000..06332c03e --- /dev/null +++ b/frontend/src/modules/OrderModule/RecordPaymentModule/index.jsx @@ -0,0 +1,37 @@ +import { ErpLayout } from '@/layout'; + +import PageLoader from '@/components/PageLoader'; +import { erp } from '@/redux/erp/actions'; +import { selectItemById, selectCurrentItem, selectRecordPaymentItem } from '@/redux/erp/selectors'; +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; +import Payment from './components/Payment'; + +export default function RecordPaymentModule({ config }) { + const dispatch = useDispatch(); + const { id } = useParams(); + + let item = useSelector(selectItemById(id)); + + useEffect(() => { + if (item) { + dispatch(erp.currentItem({ data: item })); + } else { + dispatch(erp.read({ entity: config.entity, id })); + } + }, [item, id]); + + const { result: currentResult } = useSelector(selectCurrentItem); + item = currentResult; + + useEffect(() => { + dispatch(erp.currentAction({ actionType: 'recordPayment', data: item })); + }, [item]); + + return ( + + {item ? : } + + ); +} diff --git a/frontend/src/modules/OrderModule/UpdateInvoiceModule/index.jsx b/frontend/src/modules/OrderModule/UpdateInvoiceModule/index.jsx new file mode 100644 index 000000000..c7f1be758 --- /dev/null +++ b/frontend/src/modules/OrderModule/UpdateInvoiceModule/index.jsx @@ -0,0 +1,50 @@ +import NotFound from '@/components/NotFound'; + +import { ErpLayout } from '@/layout'; +import UpdateItem from '@/modules/ErpPanelModule/UpdateItem'; +import InvoiceForm from '@/modules/InvoiceModule/Forms/InvoiceForm'; + +import PageLoader from '@/components/PageLoader'; + +import { erp } from '@/redux/erp/actions'; + +import { selectReadItem } from '@/redux/erp/selectors'; +import { useLayoutEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; + +export default function UpdateInvoiceModule({ config }) { + const dispatch = useDispatch(); + + const { id } = useParams(); + + useLayoutEffect(() => { + dispatch(erp.read({ entity: config.entity, id })); + }, [id]); + + const { result: currentResult, isSuccess, isLoading = true } = useSelector(selectReadItem); + + useLayoutEffect(() => { + if (currentResult) { + const data = { ...currentResult }; + dispatch(erp.currentAction({ actionType: 'update', data })); + } + }, [currentResult]); + + if (isLoading) { + return ( + + + + ); + } else + return ( + + {isSuccess ? ( + + ) : ( + + )} + + ); +} diff --git a/frontend/src/modules/PaymentModule/ReadPaymentModule/components/ReadItem.jsx b/frontend/src/modules/PaymentModule/ReadPaymentModule/components/ReadItem.jsx index 0af86f18f..0e1e43b29 100644 --- a/frontend/src/modules/PaymentModule/ReadPaymentModule/components/ReadItem.jsx +++ b/frontend/src/modules/PaymentModule/ReadPaymentModule/components/ReadItem.jsx @@ -20,7 +20,7 @@ import { selectCurrentItem } from '@/redux/erp/selectors'; import { DOWNLOAD_BASE_URL } from '@/config/serverApiConfig'; import { useMoney } from '@/settings'; - +import { tagColor } from '@/utils/statusTagColor'; import useMail from '@/hooks/useMail'; import { useNavigate } from 'react-router-dom'; @@ -66,7 +66,7 @@ export default function ReadItem({ config, selectedItem }) { useEffect(() => { if (currentErp?.client) { - setClient(currentErp.client); + setClient(currentErp.client[currentErp.client.type]); } }, [currentErp]); @@ -78,7 +78,9 @@ export default function ReadItem({ config, selectedItem }) { }} title={`${ENTITY_NAME} # ${currentErp.number}/${currentErp.year || ''}`} ghost={false} - tags={{currentErp.paymentStatus}} + tags={ + {currentErp.paymentStatus} + } extra={[
+ - + - + + + +
{formItems.map((item) => { return ( +
{formItems.map((item) => { return ( +
+ + + + + + +
+
{title}{description} diff --git a/frontend/src/modules/SettingModule/components/UpdateSettingModule.jsx b/frontend/src/modules/SettingModule/components/UpdateSettingModule.jsx index a8bbf1d72..ea19a003b 100644 --- a/frontend/src/modules/SettingModule/components/UpdateSettingModule.jsx +++ b/frontend/src/modules/SettingModule/components/UpdateSettingModule.jsx @@ -3,6 +3,8 @@ import { Divider } from 'antd'; import { PageHeader } from '@ant-design/pro-layout'; import UpdateSettingForm from './UpdateSettingForm'; +import { useSelector } from 'react-redux'; +import { selectLangDirection } from '@/redux/translate/selectors'; export default function UpdateSettingModule({ config, @@ -10,10 +12,14 @@ export default function UpdateSettingModule({ withUpload = false, uploadSettingKey = null, }) { + + const langDirection=useSelector(selectLangDirection) + return ( <> }> @@ -22,6 +28,7 @@ export default function UpdateSettingModule({ // ]} style={{ padding: '20px 0px', + direction:langDirection }} > diff --git a/frontend/src/pages/AdvancedSettings/index.jsx b/frontend/src/pages/AdvancedSettings/index.jsx new file mode 100644 index 000000000..b0e73d194 --- /dev/null +++ b/frontend/src/pages/AdvancedSettings/index.jsx @@ -0,0 +1,102 @@ +import useLanguage from '@/locale/useLanguage'; + +import { Switch } from 'antd'; +import { CloseOutlined, CheckOutlined } from '@ant-design/icons'; +import CrudModule from '@/modules/CrudModule/CrudModule'; +import AdvancedSettingsForm from '@/forms/AdvancedSettingsForm'; + +export default function AdvancedSettings() { + const translate = useLanguage(); + const entity = 'setting'; + const searchConfig = { + displayLabels: ['name'], + searchFields: 'name', + outputValue: '_id', + }; + + const deleteModalLabels = ['name']; + + const readColumns = [ + { + title: translate('Setting'), + dataIndex: 'settingKey', + }, + { + title: translate('Value'), + dataIndex: 'settingValue', + }, + { + title: translate('enabled'), + dataIndex: 'enabled', + }, + { + title: translate('Core Setting'), + dataIndex: 'isCoreSetting', + }, + ]; + const dataTableColumns = [ + { + title: translate('Setting'), + dataIndex: 'settingKey', + }, + { + title: translate('Value'), + dataIndex: 'settingValue', + render: (text) => { + return `${text}`; + }, + }, + { + title: translate('enabled'), + dataIndex: 'enabled', + key: 'enabled', + onCell: () => { + return { + props: { + style: { + width: '60px', + }, + }, + }; + }, + render: (_, record) => { + return ( + } + unCheckedChildren={} + /> + ); + }, + }, + ]; + + const Labels = { + PANEL_TITLE: translate('settings'), + DATATABLE_TITLE: translate('settings_list'), + ADD_NEW_ENTITY: translate('add_new_settings'), + ENTITY_NAME: translate('setting'), + + RECORD_ENTITY: translate('record_payment'), + }; + + const configPage = { + entity, + ...Labels, + }; + const config = { + ...configPage, + readColumns, + dataTableColumns, + searchConfig, + deleteModalLabels, + }; + return ( + } + updateForm={} + config={config} + /> + ); +} diff --git a/frontend/src/pages/Company/config.js b/frontend/src/pages/Company/config.js new file mode 100644 index 000000000..ca28b22cc --- /dev/null +++ b/frontend/src/pages/Company/config.js @@ -0,0 +1,173 @@ +export const fields = { + name: { + type: 'string', + required: true, + }, + mainContact: { + type: 'search', + renderAsTag: true, + label: 'Contact', + entity: 'people', + redirectLabel: 'Add New Person', + withRedirect: true, + urlToRedirect: '/people', + displayLabels: ['firstname', 'lastname'], + searchFields: 'firstname,lastname', + dataIndex: ['mainContact', 'firstname'], + }, + country: { + type: 'country', + }, + phone: { + type: 'phone', + }, + email: { + type: 'email', + required: true, + }, + website: { + type: 'url', + }, + // legalName: { + // type: 'string', + // required: true, + // }, + // hasParentCompany: { + // type: 'boolean', + // default: false, + // }, + // parentCompany: { type: 'search', entity: 'company' }, + + // people: [{ type: 'search', entity: 'people', mutliple: true }], + + // icon: { + // type: 'image', + // }, + // logo: { + // type: 'image', + // }, + // imageHeader: 'image', + // bankName: { + // type: 'string', + // }, + // bankIban: { + // type: 'string', + // }, + // bankSwift: { + // type: 'string', + // }, + // bankNumber: { + // type: 'string', + // }, + // bankRouting: { + // type: 'string', + // }, + // bankCountry: { + // type: 'string', + // }, + // companyRegNumber: { + // type: 'string', + // }, + // companyTaxNumber: { + // type: 'string', + // }, + // companyTaxId: { + // type: 'string', + // }, + // companyRegId: { + // type: 'string', + // }, + // securitySocialNbr: 'string', + // customField: [ + // { + // fieldName: { + // type: 'string', + + // + // }, + // fieldType: { + // type: 'string', + + // + // default: 'string', + // }, + // fieldValue: {}, + // }, + // ], + // location: { + // latitude: Number, + // longitude: Number, + // }, + // address: { + // type: 'string', + // }, + // city: { + // type: 'string', + // }, + // State: { + // type: 'string', + // }, + // postalCode: { + // type: Number, + // }, + + // otherPhone: [ + // { + // type: 'string', + // }, + // ], + // fax: { + // type: 'string', + // }, + + // otherEmail: [ + // { + // type: 'string', + // }, + // ], + + // socialMedia: { + // facebook: 'string', + // instagram: 'string', + // twitter: 'string', + // linkedin: 'string', + // tiktok: 'string', + // youtube: 'string', + // snapchat: 'string', + // }, + // images: [ + // { + // id: 'string', + // name: 'string', + // path: 'string', + // description: 'string', + // isPublic: { + // type: 'boolean', + // default: false, + // }, + // }, + // ], + // files: [ + // { + // id: 'string', + // name: 'string', + // path: 'string', + // description: 'string', + // isPublic: { + // type: 'boolean', + // default: false, + // }, + // }, + // ], + // category: 'string', + // approved: { + // type: 'boolean', + // default: true, + // }, + // verified: { + // type: 'boolean', + // }, + // notes: { + // type: 'string', + // }, +}; diff --git a/frontend/src/pages/Company/index.jsx b/frontend/src/pages/Company/index.jsx new file mode 100644 index 000000000..e32e3552e --- /dev/null +++ b/frontend/src/pages/Company/index.jsx @@ -0,0 +1,39 @@ +import CrudModule from '@/modules/CrudModule/CrudModule'; +import DynamicForm from '@/forms/DynamicForm'; +import { fields } from './config'; + +import useLanguage from '@/locale/useLanguage'; + +export default function Company() { + const translate = useLanguage(); + const entity = 'company'; + const searchConfig = { + displayLabels: ['name'], + searchFields: 'name,phone,eamil', + }; + const deleteModalLabels = ['name']; + + const Labels = { + PANEL_TITLE: translate('company'), + DATATABLE_TITLE: translate('company_list'), + ADD_NEW_ENTITY: translate('add_new_company'), + ENTITY_NAME: translate('company'), + }; + const configPage = { + entity, + ...Labels, + }; + const config = { + ...configPage, + fields, + searchConfig, + deleteModalLabels, + }; + return ( + } + updateForm={} + config={config} + /> + ); +} diff --git a/frontend/src/pages/Customer/config.js b/frontend/src/pages/Customer/config.js index 0e75a84c4..91e196c78 100644 --- a/frontend/src/pages/Customer/config.js +++ b/frontend/src/pages/Customer/config.js @@ -1,18 +1,55 @@ export const fields = { + type: { + type: 'selectWithFeedback', + renderAsTag: true, + options: [ + { value: 'people', label: 'people', color: 'magenta' }, + { value: 'company', label: 'company', color: 'blue' }, + ], + required: true, + hasFeedback: true, + }, name: { type: 'string', + disableForForm: true, }, country: { type: 'country', // color: 'red', - }, - address: { - type: 'string', + disableForForm: true, }, phone: { type: 'phone', + disableForForm: true, }, email: { type: 'email', + disableForForm: true, + }, + people: { + type: 'search', + label: 'people', + entity: 'people', + redirectLabel: 'Add New Person', + withRedirect: true, + urlToRedirect: '/people', + displayLabels: ['firstname', 'lastname'], + searchFields: 'firstname,lastname', + dataIndex: ['people', 'firstname'], + disableForTable: true, + feedback: 'people', + }, + company: { + type: 'search', + label: 'company', + entity: 'company', + redirectLabel: 'Add New Company', + withRedirect: true, + urlToRedirect: '/company', + displayLabels: ['name'], + searchFields: 'name', + dataIndex: ['company', 'name'], + disableForTable: true, + feedback: 'company', }, }; diff --git a/frontend/src/pages/Email/EmailRead.jsx b/frontend/src/pages/Email/EmailRead.jsx new file mode 100644 index 000000000..e714a597a --- /dev/null +++ b/frontend/src/pages/Email/EmailRead.jsx @@ -0,0 +1,21 @@ +import useLanguage from '@/locale/useLanguage'; +import ReadEmailModule from '@/modules/EmailModule/ReadEmailModule'; + +export default function EmailRead() { + const entity = 'email'; + const translate = useLanguage(); + + const Labels = { + PANEL_TITLE: translate('email_template'), + DATATABLE_TITLE: translate('email_template_list'), + ADD_NEW_ENTITY: translate('add_new_email_template'), + ENTITY_NAME: translate('email_template'), + }; + + const configPage = { + entity, + create: false, + ...Labels, + }; + return ; +} diff --git a/frontend/src/pages/Email/EmailUpdate.jsx b/frontend/src/pages/Email/EmailUpdate.jsx new file mode 100644 index 000000000..9a1961c46 --- /dev/null +++ b/frontend/src/pages/Email/EmailUpdate.jsx @@ -0,0 +1,22 @@ +import useLanguage from '@/locale/useLanguage'; +import UpdateEmailModule from '@/modules/EmailModule/UpdateEmailModule'; + +export default function EmailUpdate() { + const entity = 'email'; + const translate = useLanguage(); + + const Labels = { + PANEL_TITLE: translate('email_template'), + DATATABLE_TITLE: translate('email_template_list'), + ADD_NEW_ENTITY: translate('add_new_email_template'), + ENTITY_NAME: translate('email_template'), + }; + + const configPage = { + entity, + create: false, + ...Labels, + }; + + return ; +} diff --git a/frontend/src/pages/Email/index.jsx b/frontend/src/pages/Email/index.jsx new file mode 100644 index 000000000..cb7b3d525 --- /dev/null +++ b/frontend/src/pages/Email/index.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import useLanguage from '@/locale/useLanguage'; +import EmailDataTableModule from '@/modules/EmailModule/EmailDataTableModule'; + +export default function Email() { + const translate = useLanguage(); + const entity = 'email'; + const searchConfig = { + displayLabels: ['name'], + searchFields: 'name', + outputValue: '_id', + }; + + const deleteModalLabels = ['name']; + + const readColumns = [ + { + title: translate('Template'), + dataIndex: 'emailName', + }, + { + title: translate('Subject'), + dataIndex: 'emailSubject', + }, + { + title: translate('email content'), + dataIndex: 'emailBody', + }, + ]; + const dataTableColumns = [ + { + title: translate('Template'), + dataIndex: 'emailName', + key: 'emailName', + }, + { + title: translate('Subject'), + dataIndex: 'emailSubject', + key: 'emailSubject', + }, + ]; + + const Labels = { + PANEL_TITLE: translate('email_template'), + DATATABLE_TITLE: translate('email_template_list'), + ADD_NEW_ENTITY: translate('add_new_email_template'), + ENTITY_NAME: translate('email_template'), + }; + + const configPage = { + entity, + create: false, + ...Labels, + }; + const config = { + ...configPage, + readColumns, + dataTableColumns, + searchConfig, + deleteModalLabels, + }; + return ; +} diff --git a/frontend/src/pages/Employee/index.jsx b/frontend/src/pages/Employee/index.jsx new file mode 100644 index 000000000..0e63a9fa7 --- /dev/null +++ b/frontend/src/pages/Employee/index.jsx @@ -0,0 +1,125 @@ +import useLanguage from '@/locale/useLanguage'; +import CrudModule from '@/modules/CrudModule/CrudModule'; +import EmployeeForm from '@/forms/EmployeeForm'; +import dayjs from 'dayjs'; +import { useDate } from '@/settings'; +export default function Employee() { + const translate = useLanguage(); + const { dateFormat } = useDate(); + const entity = 'employee'; + const searchConfig = { + displayLabels: ['name', 'surname'], + searchFields: 'name,surname,birthday', + outputValue: '_id', + }; + + const deleteModalLabels = ['name', 'surname']; + + const dataTableColumns = [ + { + title: translate('first name'), + dataIndex: 'name', + }, + { + title: translate('last name'), + dataIndex: 'surname', + }, + { + title: translate('Birthday'), + dataIndex: 'birthday', + render: (date) => { + return dayjs(date).format(dateFormat); + }, + }, + { + title: translate('Department'), + dataIndex: 'department', + }, + { + title: translate('Position'), + dataIndex: 'position', + }, + { + title: translate('Phone'), + dataIndex: 'phone', + }, + { + title: translate('Email'), + dataIndex: 'email', + }, + ]; + + const readColumns = [ + { + title: translate('first name'), + dataIndex: 'name', + }, + { + title: translate('last name'), + dataIndex: 'surname', + }, + { + title: translate('Birthday'), + dataIndex: 'birthday', + isDate: true, + }, + { + title: translate('birthplace'), + dataIndex: 'birthplace', + }, + { + title: translate('gender'), + dataIndex: 'gender', + }, + { + title: translate('Department'), + dataIndex: 'department', + }, + { + title: translate('Position'), + dataIndex: 'position', + }, + { + title: translate('address'), + dataIndex: 'address', + }, + { + title: translate('state'), + dataIndex: 'state', + }, + { + title: translate('Phone'), + dataIndex: 'phone', + }, + { + title: translate('Email'), + dataIndex: 'email', + }, + ]; + + const Labels = { + PANEL_TITLE: translate('employee'), + DATATABLE_TITLE: translate('employee_list'), + ADD_NEW_ENTITY: translate('add_new_employee'), + ENTITY_NAME: translate('employee'), + }; + + const configPage = { + entity, + ...Labels, + }; + const config = { + ...configPage, + readColumns, + dataTableColumns, + searchConfig, + deleteModalLabels, + }; + return ( + } + updateForm={} + config={config} + /> + ); +} diff --git a/frontend/src/pages/Expense/config.js b/frontend/src/pages/Expense/config.js new file mode 100644 index 000000000..8d347f183 --- /dev/null +++ b/frontend/src/pages/Expense/config.js @@ -0,0 +1,25 @@ +export const fields = { + name: { + type: 'string', + required: true, + }, + expenseCategory: { + type: 'async', + label: 'Expense Category', + displayLabels: ['expenseCategory', 'name'], + dataIndex: ['expenseCategory', 'name'], + entity: 'expensecategory', + required: true, + }, + + total: { + type: 'currency', + required: true, + }, + description: { + type: 'textarea', + }, + ref: { + type: 'string', + }, +}; diff --git a/frontend/src/pages/Expense/index.jsx b/frontend/src/pages/Expense/index.jsx new file mode 100644 index 000000000..3def15334 --- /dev/null +++ b/frontend/src/pages/Expense/index.jsx @@ -0,0 +1,39 @@ +import CrudModule from '@/modules/CrudModule/CrudModule'; +import DynamicForm from '@/forms/DynamicForm'; +import { fields } from './config'; + +import useLanguage from '@/locale/useLanguage'; + +export default function Expense() { + const translate = useLanguage(); + const entity = 'expense'; + const searchConfig = { + displayLabels: ['name'], + searchFields: 'name', + }; + const deleteModalLabels = ['name']; + + const Labels = { + PANEL_TITLE: translate('Expense'), + DATATABLE_TITLE: translate('Expense_list'), + ADD_NEW_ENTITY: translate('add_new_Expense'), + ENTITY_NAME: translate('Expense'), + }; + const configPage = { + entity, + ...Labels, + }; + const config = { + ...configPage, + fields, + searchConfig, + deleteModalLabels, + }; + return ( + } + updateForm={} + config={config} + /> + ); +} diff --git a/frontend/src/pages/ExpenseCategory/config.js b/frontend/src/pages/ExpenseCategory/config.js new file mode 100644 index 000000000..597a129d5 --- /dev/null +++ b/frontend/src/pages/ExpenseCategory/config.js @@ -0,0 +1,21 @@ +import color from '@/utils/color'; + +export const fields = { + name: { + type: 'stringWithColor', + required: true, + }, + description: { + type: 'textarea', + required: true, + }, + color: { + type: 'color', + options: [...color], + required: true, + }, + enabled: { + type: 'boolean', + required: true, + }, +}; diff --git a/frontend/src/pages/ExpenseCategory/index.jsx b/frontend/src/pages/ExpenseCategory/index.jsx new file mode 100644 index 000000000..5338f540a --- /dev/null +++ b/frontend/src/pages/ExpenseCategory/index.jsx @@ -0,0 +1,39 @@ +import CrudModule from '@/modules/CrudModule/CrudModule'; +import DynamicForm from '@/forms/DynamicForm'; +import { fields } from './config'; + +import useLanguage from '@/locale/useLanguage'; + +export default function ExpenseCategory() { + const translate = useLanguage(); + const entity = 'expensecategory'; + const searchConfig = { + displayLabels: ['name'], + searchFields: 'name', + }; + const deleteModalLabels = ['name']; + + const Labels = { + PANEL_TITLE: translate('Expense_Category'), + DATATABLE_TITLE: translate('Expense_Category_list'), + ADD_NEW_ENTITY: translate('add_new_Expense_Category'), + ENTITY_NAME: translate('Expense_Category'), + }; + const configPage = { + entity, + ...Labels, + }; + const config = { + ...configPage, + fields, + searchConfig, + deleteModalLabels, + }; + return ( + } + updateForm={} + config={config} + /> + ); +} diff --git a/frontend/src/pages/Inventory/index.jsx b/frontend/src/pages/Inventory/index.jsx new file mode 100644 index 000000000..398e0af13 --- /dev/null +++ b/frontend/src/pages/Inventory/index.jsx @@ -0,0 +1,71 @@ +import React from 'react'; +import CrudModule from '@/modules/CrudModule/CrudModule'; +import InventoryForm from '@/forms/InventoryForm'; // Retaining InventoryForm +import useLanguage from '@/locale/useLanguage'; + +export default function Inventory() { + const translate = useLanguage(); + const entity = 'inventory'; // Updated entity name + const searchConfig = { + displayLabels: ['product'], // Adjusted to search by product + searchFields: 'product', + outputValue: '_id', + }; + const deleteModalLabels = ['product', 'quantity', 'unitPrice']; // Adjusted to display inventory item labels + + const readColumns = [ + { + title: translate('Product'), + dataIndex: 'product', + }, + { + title: translate('Quantity'), + dataIndex: 'quantity', + }, + { + title: translate('Unit Price'), + dataIndex: 'unitPrice', + }, + ]; + + const dataTableColumns = [ + { + title: translate('Product'), + dataIndex: ['product'], + }, + { + title: translate('Quantity'), + dataIndex: ['quantity'], + }, + { + title: translate('Unit Price'), + dataIndex: ['unitPrice'], + }, + ]; + + const Labels = { + PANEL_TITLE: translate('product'), + DATATABLE_TITLE: translate('product_list'), + ADD_NEW_ENTITY: translate('add_new_product'), + ENTITY_NAME: translate('product'), + }; + + const configPage = { + entity, + ...Labels, + }; + const config = { + ...configPage, + readColumns, + dataTableColumns, + searchConfig, + deleteModalLabels, + }; + return ( + } // Retaining InventoryForm + updateForm={} // Retaining InventoryForm + config={config} + /> + ); +} diff --git a/frontend/src/pages/Invoice/index.jsx b/frontend/src/pages/Invoice/index.jsx index 94e89282a..f98bde7c9 100644 --- a/frontend/src/pages/Invoice/index.jsx +++ b/frontend/src/pages/Invoice/index.jsx @@ -74,10 +74,34 @@ export default function Invoice() { { title: translate('Status'), dataIndex: 'status', + render: (status) => { + let tagStatus = tagColor(status); + + return ( + + {/* {tagStatus.icon + ' '} */} + {status && translate(tagStatus.label)} + + ); + }, }, { title: translate('Payment'), dataIndex: 'paymentStatus', + render: (paymentStatus) => { + let tagStatus = tagColor(paymentStatus); + + return ( + + {/* {tagStatus.icon + ' '} */} + {paymentStatus && translate(paymentStatus)} + + ); + }, + }, + { + title: translate('Created By'), + dataIndex: ['createdBy', 'name'], }, ]; diff --git a/frontend/src/pages/Lead/config.js b/frontend/src/pages/Lead/config.js new file mode 100644 index 000000000..058098ec0 --- /dev/null +++ b/frontend/src/pages/Lead/config.js @@ -0,0 +1,90 @@ +import { selectColor } from '@/utils/color'; +export const fields = { + type: { + type: 'selectWithFeedback', + renderAsTag: true, + options: [ + { value: 'people', label: 'people', color: 'magenta' }, + { value: 'company', label: 'company', color: 'blue' }, + ], + required: true, + hasFeedback: true, + }, + name: { + type: 'string', + disableForForm: true, + }, + status: { + type: 'selectWithTranslation', + renderAsTag: true, + options: [ + { value: 'draft', label: 'draft' }, + { value: 'new', label: 'new', color: 'blue' }, + { value: 'in negociation', label: 'in negociation', color: 'purple' }, + { value: 'won', label: 'won', color: 'green' }, + { value: 'loose', label: 'loose', color: 'red' }, + { value: 'canceled', label: 'canceled', color: selectColor.crimson }, + { value: 'assigned', label: 'assigned', color: selectColor.mediumturquoise }, + { value: 'on hold', label: 'on hold', color: selectColor.burlywood }, + { value: 'waiting', label: 'waiting', color: 'orange' }, + ], + }, + + source: { + type: 'selectWithTranslation', + renderAsTag: true, + options: [ + { value: 'linkedin', label: 'linkedin', color: selectColor.royalblue }, + { value: 'socialmedia', label: 'social_media', color: selectColor.skyblue }, + { value: 'website', label: 'website', color: selectColor.coral }, + { value: 'advertising', label: 'advertising', color: selectColor.darkgreen }, + { value: 'friend', label: 'friend', color: selectColor.firebrick }, + { + value: 'professionals network', + label: 'professionals network', + color: selectColor.mediumvioletred, + }, + + { value: 'customer referral', label: 'customer referral', color: selectColor.violet }, + { value: 'sales', label: 'sales', color: selectColor.deeppink }, + { value: 'other', label: 'other', color: selectColor.darkgray }, + ], + }, + country: { + type: 'country', + color: null, + disableForForm: true, + }, + phone: { + type: 'phone', + disableForForm: true, + }, + email: { + type: 'email', + disableForForm: true, + }, + people: { + type: 'search', + label: 'people', + entity: 'people', + displayLabels: ['firstname', 'lastname'], + searchFields: 'firstname,lastname', + dataIndex: ['people', 'firstname'], + disableForTable: true, + feedback: 'people', + }, + company: { + type: 'search', + label: 'company', + entity: 'company', + displayLabels: ['name'], + searchFields: 'name', + dataIndex: ['company', 'name'], + disableForTable: true, + feedback: 'company', + }, + notes: { + type: 'textarea', + disableForTable: true, + }, +}; diff --git a/frontend/src/pages/Lead/index.jsx b/frontend/src/pages/Lead/index.jsx new file mode 100644 index 000000000..b3b8f6054 --- /dev/null +++ b/frontend/src/pages/Lead/index.jsx @@ -0,0 +1,39 @@ +import CrudModule from '@/modules/CrudModule/CrudModule'; +import DynamicForm from '@/forms/DynamicForm'; +import { fields } from './config'; + +import useLanguage from '@/locale/useLanguage'; + +export default function Lead() { + const translate = useLanguage(); + const entity = 'lead'; + const searchConfig = { + displayLabels: ['name'], + searchFields: 'name', + }; + const deleteModalLabels = ['name']; + + const Labels = { + PANEL_TITLE: translate('lead'), + DATATABLE_TITLE: translate('lead_list'), + ADD_NEW_ENTITY: translate('add_new_lead'), + ENTITY_NAME: translate('lead'), + }; + const configPage = { + entity, + ...Labels, + }; + const config = { + ...configPage, + fields, + searchConfig, + deleteModalLabels, + }; + return ( + } + updateForm={} + config={config} + /> + ); +} diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index c1f8a1f5d..d492df26e 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -1,11 +1,11 @@ -import { useEffect } from 'react'; +import { useEffect, useState, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import useLanguage from '@/locale/useLanguage'; -import { Form, Button } from 'antd'; +import { Form, Button, Input } from 'antd'; import { login } from '@/redux/auth/actions'; import { selectAuth } from '@/redux/auth/selectors'; @@ -17,11 +17,30 @@ const LoginPage = () => { const translate = useLanguage(); const { isLoading, isSuccess } = useSelector(selectAuth); const navigate = useNavigate(); - // const size = useSize(); - const dispatch = useDispatch(); + + // Add state for CAPTCHA + const [captcha, setCaptcha] = useState(''); + const [userCaptcha, setUserCaptcha] = useState(''); + + // Add ref for CAPTCHA input + const captchaInputRef = useRef(null); + + // Generate a random CAPTCHA on component mount + useEffect(() => { + const num1 = Math.floor(Math.random() * 5) + 1; // Random number between 1 and 5 + const num2 = Math.floor(Math.random() * 5) + 1; // Random number between 1 and 5 + setCaptcha(`${num1} + ${num2}`); + }, []); + const onFinish = (values) => { - dispatch(login({ loginData: values })); + const [num1, num2] = captcha.split(' + ').map(Number); + const correctAnswer = (num1 + num2) % 10; // Ensure the result is a single digit + if (parseInt(userCaptcha, 10) === correctAnswer) { + dispatch(login({ loginData: values })); + } else { + alert(translate('CAPTCHA verification failed. Please try again.')); + } }; useEffect(() => { @@ -41,6 +60,34 @@ const LoginPage = () => { onFinish={onFinish} > + + {/* Add CAPTCHA verification */} + +
+ {captcha} +
+ setUserCaptcha(e.target.value)} + placeholder={translate('Enter CAPTCHA')} + /> +
+ + diff --git a/frontend/src/pages/Offer/OfferCreate.jsx b/frontend/src/pages/Offer/OfferCreate.jsx new file mode 100644 index 000000000..27b95106e --- /dev/null +++ b/frontend/src/pages/Offer/OfferCreate.jsx @@ -0,0 +1,20 @@ +import useLanguage from '@/locale/useLanguage'; +import CreateOfferModule from '@/modules/OfferModule/CreateOfferModule'; + +export default function OfferCreate() { + const translate = useLanguage(); + + const entity = 'offer'; + const Labels = { + PANEL_TITLE: translate('Offer Leads'), + DATATABLE_TITLE: translate('offer_list'), + ADD_NEW_ENTITY: translate('add_new_offer'), + ENTITY_NAME: translate('Offer Leads'), + }; + + const configPage = { + entity, + ...Labels, + }; + return ; +} diff --git a/frontend/src/pages/Offer/OfferRead.jsx b/frontend/src/pages/Offer/OfferRead.jsx new file mode 100644 index 000000000..f324b084d --- /dev/null +++ b/frontend/src/pages/Offer/OfferRead.jsx @@ -0,0 +1,20 @@ +import useLanguage from '@/locale/useLanguage'; +import ReadOfferModule from '@/modules/OfferModule/ReadOfferModule'; + +export default function OfferRead() { + const translate = useLanguage(); + + const entity = 'offer'; + const Labels = { + PANEL_TITLE: translate('Offer Leads'), + DATATABLE_TITLE: translate('offer_list'), + ADD_NEW_ENTITY: translate('add_new_offer'), + ENTITY_NAME: translate('Offer Leads'), + }; + + const configPage = { + entity, + ...Labels, + }; + return ; +} diff --git a/frontend/src/pages/Offer/OfferUpdate.jsx b/frontend/src/pages/Offer/OfferUpdate.jsx new file mode 100644 index 000000000..f3e2ed2ad --- /dev/null +++ b/frontend/src/pages/Offer/OfferUpdate.jsx @@ -0,0 +1,20 @@ +import useLanguage from '@/locale/useLanguage'; +import UpdateOfferModule from '@/modules/OfferModule/UpdateOfferModule'; + +export default function OfferUpdate() { + const translate = useLanguage(); + + const entity = 'offer'; + const Labels = { + PANEL_TITLE: translate('Offer Leads'), + DATATABLE_TITLE: translate('offer_list'), + ADD_NEW_ENTITY: translate('add_new_offer'), + ENTITY_NAME: translate('Offer Leads'), + }; + + const configPage = { + entity, + ...Labels, + }; + return ; +} diff --git a/frontend/src/pages/Offer/index.jsx b/frontend/src/pages/Offer/index.jsx new file mode 100644 index 000000000..3e988fcbe --- /dev/null +++ b/frontend/src/pages/Offer/index.jsx @@ -0,0 +1,102 @@ +import dayjs from 'dayjs'; +import { Tag } from 'antd'; +import { tagColor } from '@/utils/statusTagColor'; + +import OfferDataTableModule from '@/modules/OfferModule/OfferDataTableModule'; +import { useMoney, useDate } from '@/settings'; +import useLanguage from '@/locale/useLanguage'; + +export default function Offer() { + const translate = useLanguage(); + const { moneyFormatter } = useMoney(); + const { dateFormat } = useDate(); + + const searchConfig = { + entity: 'lead', + displayLabels: ['name'], + searchFields: 'name', + }; + const deleteModalLabels = ['number', 'lead.name']; + const dataTableColumns = [ + { + title: translate('Number'), + dataIndex: 'number', + }, + { + title: translate('Company'), + dataIndex: ['lead', 'name'], + }, + { + title: translate('Date'), + dataIndex: 'date', + render: (date) => dayjs(date).format(dateFormat), + }, + { + title: translate('Sub Total'), + dataIndex: 'subTotal', + onCell: () => { + return { + style: { + textAlign: 'right', + whiteSpace: 'nowrap', + direction: 'ltr', + }, + }; + }, + render: (total, record) => moneyFormatter({ amount: total, currency_code: record.currency }), + }, + { + title: translate('Total'), + dataIndex: 'total', + onCell: () => { + return { + style: { + textAlign: 'right', + whiteSpace: 'nowrap', + direction: 'ltr', + }, + }; + }, + render: (total, record) => moneyFormatter({ amount: total, currency_code: record.currency }), + }, + + { + title: translate('Note'), + dataIndex: 'notes', + }, + { + title: translate('Status'), + dataIndex: 'status', + render: (status) => { + let tagStatus = tagColor(status); + + return ( + + {/* {tagStatus.icon + ' '} */} + {status && translate(tagStatus.label)} + + ); + }, + }, + ]; + + const entity = 'offer'; + const Labels = { + PANEL_TITLE: translate('Offer Leads'), + DATATABLE_TITLE: translate('offer_list'), + ADD_NEW_ENTITY: translate('add_new_offer'), + ENTITY_NAME: translate('Offer Leads'), + }; + + const configPage = { + entity, + ...Labels, + }; + const config = { + ...configPage, + dataTableColumns, + searchConfig, + deleteModalLabels, + }; + return ; +} diff --git a/frontend/src/pages/Order/index.jsx b/frontend/src/pages/Order/index.jsx new file mode 100644 index 000000000..485fccd2c --- /dev/null +++ b/frontend/src/pages/Order/index.jsx @@ -0,0 +1,97 @@ +import React from 'react'; + +import CrudModule from '@/modules/CrudModule/CrudModule'; +import OrderForm from '@/forms/OrderForm'; // Ensure to create this form +import useLanguage from '@/locale/useLanguage'; + +export default function Order() { + const translate = useLanguage(); + const entity = 'order'; + const searchConfig = { + displayLabels: ['orderId', 'status'], + searchFields: 'orderId,status', + outputValue: '_id', + }; + + const deleteModalLabels = ['orderId']; + + const readColumns = [ + { + title: translate('Order ID'), + dataIndex: 'orderId', + }, + { + title: translate('Product'), + dataIndex: 'products', + }, + { + title: translate('Quantity'), + dataIndex: 'quantity', + }, + { + title: translate('Price'), + dataIndex: 'price', + }, + { + title: translate('Status'), + dataIndex: 'status', + }, + { + title: translate('Note'), + dataIndex: 'notes', + }, + ]; + const dataTableColumns = [ + { + title: translate('Order ID'), + dataIndex: 'orderId', + }, + { + title: translate('Product'), + dataIndex: 'products', + }, + { + title: translate('Quantity'), + dataIndex: 'quantity', + }, + { + title: translate('Price'), + dataIndex: 'price', + }, + + { + title: translate('Status'), + dataIndex: 'status', + }, + { + title: translate('Note'), + dataIndex: 'notes', + }, + ]; + + const Labels = { + PANEL_TITLE: translate('order'), + DATATABLE_TITLE: translate('order_list'), + ADD_NEW_ENTITY: translate('add_new_order'), + ENTITY_NAME: translate('order'), + }; + + const configPage = { + entity, + ...Labels, + }; + const config = { + ...configPage, + dataTableColumns, + readColumns, + searchConfig, + deleteModalLabels, + }; + return ( + } + updateForm={} + config={config} + /> + ); +} diff --git a/frontend/src/pages/People/config.js b/frontend/src/pages/People/config.js new file mode 100644 index 000000000..157659dbc --- /dev/null +++ b/frontend/src/pages/People/config.js @@ -0,0 +1,94 @@ +export const fields = { + firstname: { + type: 'string', + required: true, + }, + lastname: { + type: 'string', + required: true, + }, + company: { + type: 'search', + entity: 'company', + renderAsTag: true, + redirectLabel: 'Add New Company', + withRedirect: true, + urlToRedirect: '/company', + displayLabels: ['name'], + searchFields: 'name', + dataIndex: ['company', 'name'], + }, + country: { + type: 'country', + }, + phone: { + type: 'phone', + }, + email: { + type: 'email', + }, + // bio: { + // type: 'string', + // }, + // idCardNumber: { + // type: 'string', + // }, + // idCardType: { + // type: 'string', + // }, + // securitySocialNbr: { + // type: 'string', + // }, + // taxNumber: { + // type: 'string', + // }, + // birthday: { + // type: 'date', + // }, + // birthplace: { + // type: 'string', + // }, + // gender: { + // type: 'select', + // options: [ + // { + // value: 'male', + // label: 'Male', + // }, + // { + // value: 'female', + // label: 'Female', + // }, + // ], + // }, + // bankName: { + // type: 'string', + // }, + // bankIban: { + // type: 'string', + // }, + // bankSwift: { + // type: 'string', + // }, + // bankNumber: { + // type: 'string', + // }, + // bankRouting: { + // type: 'string', + // }, + // address: { + // type: 'string', + // }, + // city: { + // type: 'string', + // }, + // State: { + // type: 'string', + // }, + // postalCode: { + // type: 'number', + // }, + // website: { + // type: 'string', + // }, +}; diff --git a/frontend/src/pages/People/index.jsx b/frontend/src/pages/People/index.jsx new file mode 100644 index 000000000..b44e2598e --- /dev/null +++ b/frontend/src/pages/People/index.jsx @@ -0,0 +1,39 @@ +import CrudModule from '@/modules/CrudModule/CrudModule'; +import DynamicForm from '@/forms/DynamicForm'; +import { fields } from './config'; + +import useLanguage from '@/locale/useLanguage'; + +export default function People() { + const translate = useLanguage(); + const entity = 'people'; + const searchConfig = { + displayLabels: ['firstname', 'lastname'], + searchFields: 'firstname,lastname,email', + }; + const deleteModalLabels = ['firstname', 'lastname']; + + const Labels = { + PANEL_TITLE: translate('person'), + DATATABLE_TITLE: translate('people_list'), + ADD_NEW_ENTITY: translate('add_new_person'), + ENTITY_NAME: translate('person'), + }; + const configPage = { + entity, + ...Labels, + }; + const config = { + ...configPage, + fields, + searchConfig, + deleteModalLabels, + }; + return ( + } + updateForm={} + config={config} + /> + ); +} diff --git a/frontend/src/pages/Product/config.js b/frontend/src/pages/Product/config.js new file mode 100644 index 000000000..c9f5c32c2 --- /dev/null +++ b/frontend/src/pages/Product/config.js @@ -0,0 +1,25 @@ +export const fields = { + name: { + type: 'string', + required: true, + }, + productCategory: { + type: 'async', + label: 'product Category', + displayLabels: ['productCategory', 'name'], + dataIndex: ['productCategory', 'name'], + entity: 'productcategory', + required: true, + }, + + price: { + type: 'currency', + required: true, + }, + description: { + type: 'textarea', + }, + ref: { + type: 'string', + }, +}; diff --git a/frontend/src/pages/Product/index.jsx b/frontend/src/pages/Product/index.jsx new file mode 100644 index 000000000..9c8c01070 --- /dev/null +++ b/frontend/src/pages/Product/index.jsx @@ -0,0 +1,39 @@ +import CrudModule from '@/modules/CrudModule/CrudModule'; +import DynamicForm from '@/forms/DynamicForm'; +import { fields } from './config'; + +import useLanguage from '@/locale/useLanguage'; + +export default function Product() { + const translate = useLanguage(); + const entity = 'product'; + const searchConfig = { + displayLabels: ['name'], + searchFields: 'name', + }; + const deleteModalLabels = ['name']; + + const Labels = { + PANEL_TITLE: translate('Product'), + DATATABLE_TITLE: translate('Product_list'), + ADD_NEW_ENTITY: translate('add_new_Product'), + ENTITY_NAME: translate('Product'), + }; + const configPage = { + entity, + ...Labels, + }; + const config = { + ...configPage, + fields, + searchConfig, + deleteModalLabels, + }; + return ( + } + updateForm={} + config={config} + /> + ); +} diff --git a/frontend/src/pages/ProductCategory/config.js b/frontend/src/pages/ProductCategory/config.js new file mode 100644 index 000000000..597a129d5 --- /dev/null +++ b/frontend/src/pages/ProductCategory/config.js @@ -0,0 +1,21 @@ +import color from '@/utils/color'; + +export const fields = { + name: { + type: 'stringWithColor', + required: true, + }, + description: { + type: 'textarea', + required: true, + }, + color: { + type: 'color', + options: [...color], + required: true, + }, + enabled: { + type: 'boolean', + required: true, + }, +}; diff --git a/frontend/src/pages/ProductCategory/index.jsx b/frontend/src/pages/ProductCategory/index.jsx new file mode 100644 index 000000000..2c841ba7e --- /dev/null +++ b/frontend/src/pages/ProductCategory/index.jsx @@ -0,0 +1,39 @@ +import CrudModule from '@/modules/CrudModule/CrudModule'; +import DynamicForm from '@/forms/DynamicForm'; +import { fields } from './config'; + +import useLanguage from '@/locale/useLanguage'; + +export default function ProductCategory() { + const translate = useLanguage(); + const entity = 'productcategory'; + const searchConfig = { + displayLabels: ['name'], + searchFields: 'name', + }; + const deleteModalLabels = ['name']; + + const Labels = { + PANEL_TITLE: translate('Product_Category'), + DATATABLE_TITLE: translate('Product_Category_list'), + ADD_NEW_ENTITY: translate('add_new_Product_Category'), + ENTITY_NAME: translate('Product_Category'), + }; + const configPage = { + entity, + ...Labels, + }; + const config = { + ...configPage, + fields, + searchConfig, + deleteModalLabels, + }; + return ( + } + updateForm={} + config={config} + /> + ); +} diff --git a/frontend/src/pages/Quote/index.jsx b/frontend/src/pages/Quote/index.jsx index ec668f817..e57981302 100644 --- a/frontend/src/pages/Quote/index.jsx +++ b/frontend/src/pages/Quote/index.jsx @@ -72,6 +72,16 @@ export default function Quote() { { title: translate('Status'), dataIndex: 'status', + render: (status) => { + let tagStatus = tagColor(status); + + return ( + + {/* {tagStatus.icon + ' '} */} + {status && translate(tagStatus.label)} + + ); + }, }, ]; diff --git a/frontend/src/pages/Register.jsx b/frontend/src/pages/Register.jsx new file mode 100644 index 000000000..78e9973cc --- /dev/null +++ b/frontend/src/pages/Register.jsx @@ -0,0 +1,60 @@ +import { useEffect } from 'react'; + +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; + +import useLanguage from '@/locale/useLanguage'; + +import { Form, Button } from 'antd'; + +import { selectAuth } from '@/redux/auth/selectors'; +import RegisterForm from '@/forms/RegisterForm'; +import Loading from '@/components/Loading'; +import AuthModule from '@/modules/AuthModule'; +import { register } from '@/redux/auth/actions'; +const RegisterPage = () => { + const translate = useLanguage(); + const { isLoading, isSuccess } = useSelector(selectAuth); + const navigate = useNavigate(); + // const size = useSize(); + + const dispatch = useDispatch(); + const onFinish = (values) => { + console.log("Finsihed") + dispatch(register({ registerData: values })); + navigate('/') + }; + + const FormContainer = () => { + return ( + +
+ + + + + +
+ ); + }; + + return } AUTH_TITLE="Sign Up" />; +}; + +export default RegisterPage; \ No newline at end of file diff --git a/frontend/src/redux/rootReducer.js b/frontend/src/redux/rootReducer.js index 8ba7ee88b..04c47da73 100644 --- a/frontend/src/redux/rootReducer.js +++ b/frontend/src/redux/rootReducer.js @@ -5,6 +5,7 @@ import { reducer as crudReducer } from './crud'; import { reducer as erpReducer } from './erp'; import { reducer as adavancedCrudReducer } from './adavancedCrud'; import { reducer as settingsReducer } from './settings'; +import { reducer as translateReducer } from './translate'; // Combine all reducers. @@ -14,6 +15,7 @@ const rootReducer = combineReducers({ erp: erpReducer, adavancedCrud: adavancedCrudReducer, settings: settingsReducer, + translate: translateReducer, }); export default rootReducer; diff --git a/frontend/src/redux/store.js b/frontend/src/redux/store.js index 2f88cb805..0fd4826d2 100644 --- a/frontend/src/redux/store.js +++ b/frontend/src/redux/store.js @@ -3,9 +3,20 @@ import { configureStore } from '@reduxjs/toolkit'; import lang from '@/locale/translation/en_us'; import rootReducer from './rootReducer'; -import storePersist from './storePersist'; +import storePersist, { localStorageHealthCheck } from './storePersist'; -// localStorageHealthCheck(); +localStorageHealthCheck(); + +const LANG_INITIAL_STATE = { + result: lang, + langCode: 'en_us', + isLoading: false, + isSuccess: false, +}; + +const lang_state = storePersist.get('translate') + ? storePersist.get('translate') + : LANG_INITIAL_STATE; const AUTH_INITIAL_STATE = { current: {}, @@ -16,7 +27,7 @@ const AUTH_INITIAL_STATE = { const auth_state = storePersist.get('auth') ? storePersist.get('auth') : AUTH_INITIAL_STATE; -const initialState = { auth: auth_state }; +const initialState = { translate: lang_state, auth: auth_state }; const store = configureStore({ reducer: rootReducer, @@ -24,8 +35,8 @@ const store = configureStore({ devTools: import.meta.env.PROD === false, // Enable Redux DevTools in development mode }); -console.log( - '๐Ÿš€ Welcome to IDURAR ERP CRM! Did you know that we also offer commercial customization services? Contact us at hello@idurarapp.com for more information.' -); +// console.log( +// '๐Ÿš€ Welcome to IDURAR ERP CRM! Did you know that we also offer commercial customization services? Contact us at hello@idurarapp.com for more information.' +// ); export default store; diff --git a/frontend/src/redux/translate/actions.js b/frontend/src/redux/translate/actions.js new file mode 100644 index 000000000..0c5db970d --- /dev/null +++ b/frontend/src/redux/translate/actions.js @@ -0,0 +1,41 @@ +import * as actionTypes from './types'; + +import translation from '@/locale/translation/translation'; + +export const translateAction = { + resetState: () => (dispatch) => { + dispatch({ + type: actionTypes.RESET_STATE, + }); + }, + translate: + (value = 'en_us') => + async (dispatch) => { + dispatch({ + type: actionTypes.REQUEST_LOADING, + }); + + let data = translation.en_us; + if (data) { + const LANG_STATE = { + result: data, + isRtl: isRtl, + langDirection: 'ltr', + langCode: value, + isLoading: false, + isSuccess: false, + }; + window.localStorage.setItem('translate', JSON.stringify(LANG_STATE)); + dispatch({ + type: actionTypes.REQUEST_SUCCESS, + payload: data, + langCode: value, + isRtl: isRtl, + }); + } else { + dispatch({ + type: actionTypes.REQUEST_FAILED, + }); + } + }, +}; diff --git a/frontend/src/redux/translate/index.js b/frontend/src/redux/translate/index.js new file mode 100644 index 000000000..cbc56ade5 --- /dev/null +++ b/frontend/src/redux/translate/index.js @@ -0,0 +1 @@ +export { default as reducer } from './reducer'; diff --git a/frontend/src/redux/translate/reducer.js b/frontend/src/redux/translate/reducer.js new file mode 100644 index 000000000..cb3ed7b2a --- /dev/null +++ b/frontend/src/redux/translate/reducer.js @@ -0,0 +1,47 @@ +import * as actionTypes from './types'; +import en_us from '@/locale/translation/en_us'; +import storePersist from '../storePersist'; + +const LANG_INITIAL_STATE = { + result: en_us, + langCode: 'en_us', + langDirection: 'ltr', + isLoading: false, + isSuccess: false, +}; + +const INITIAL_STATE = storePersist.get('translate') + ? storePersist.get('translate') + : LANG_INITIAL_STATE; + +const translateReducer = (state = INITIAL_STATE, action) => { + const { payload = null, langCode, isRtl = false } = action; + switch (action.type) { + case actionTypes.RESET_STATE: + return INITIAL_STATE; + case actionTypes.REQUEST_LOADING: + return { + ...state, + isLoading: true, + }; + case actionTypes.REQUEST_FAILED: + return { + ...state, + isLoading: false, + isSuccess: false, + }; + + case actionTypes.REQUEST_SUCCESS: + return { + result: payload, + langCode: langCode.toLowerCase(), + langDirection: isRtl ? 'rtl' : 'ltr', + isLoading: false, + isSuccess: true, + }; + default: + return state; + } +}; + +export default translateReducer; diff --git a/frontend/src/redux/translate/selectors.js b/frontend/src/redux/translate/selectors.js new file mode 100644 index 000000000..8ac59883c --- /dev/null +++ b/frontend/src/redux/translate/selectors.js @@ -0,0 +1,10 @@ +import { createSelector } from 'reselect'; + +export const selectLangState = (state) => state.translate; + +export const selectCurrentLang = createSelector([selectLangState], (translate) => translate.result); +export const selectLangCode = createSelector([selectLangState], (translate) => translate.langCode); +export const selectLangDirection = createSelector( + [selectLangState], + (translate) => translate.langDirection +); diff --git a/frontend/src/redux/translate/types.js b/frontend/src/redux/translate/types.js new file mode 100644 index 000000000..15bcf1785 --- /dev/null +++ b/frontend/src/redux/translate/types.js @@ -0,0 +1,5 @@ +export const RESET_STATE = 'TRANSLATE_RESET_STATE'; + +export const REQUEST_LOADING = 'TRANSLATE_REQUEST_LOADING'; +export const REQUEST_SUCCESS = 'TRANSLATE_REQUEST_SUCCESS'; +export const REQUEST_FAILED = 'TRANSLATE_REQUEST_FAILED'; diff --git a/frontend/src/request/errorHandler.js b/frontend/src/request/errorHandler.js index a580a0eca..8207317b7 100644 --- a/frontend/src/request/errorHandler.js +++ b/frontend/src/request/errorHandler.js @@ -27,10 +27,10 @@ const errorHandler = (error) => { maxCount: 1, }); // Code to execute when there is no internet connection - // notification.error({ - // message: 'Problem connecting to server', - // description: 'Cannot connect to the server, Try again later', - // }); + notification.error({ + message: 'Problem connecting to server', + description: 'Cannot connect to the server, Try again later', + }); return { success: false, result: null, @@ -53,7 +53,7 @@ const errorHandler = (error) => { const message = response.data && response.data.message; const errorText = message || codeMessage[response.status]; - const { status, error } = response; + const { status } = response; notification.config({ duration: 20, maxCount: 2, @@ -62,12 +62,7 @@ const errorHandler = (error) => { message: `Request error ${status}`, description: errorText, }); - - if (response?.data?.error?.name === 'JsonWebTokenError') { - window.localStorage.removeItem('auth'); - window.localStorage.removeItem('isLogout'); - window.location.href = '/logout'; - } else return response.data; + return response.data; } else { notification.config({ duration: 15, diff --git a/frontend/src/request/request.js b/frontend/src/request/request.js index 2679ade73..febf19028 100644 --- a/frontend/src/request/request.js +++ b/frontend/src/request/request.js @@ -3,31 +3,13 @@ import { API_BASE_URL } from '@/config/serverApiConfig'; import errorHandler from './errorHandler'; import successHandler from './successHandler'; -import storePersist from '@/redux/storePersist'; -function findKeyByPrefix(object, prefix) { - for (var property in object) { - if (object.hasOwnProperty(property) && property.toString().startsWith(prefix)) { - return property; - } - } -} - -function includeToken() { - axios.defaults.baseURL = API_BASE_URL; - - axios.defaults.withCredentials = true; - const auth = storePersist.get('auth'); - - if (auth) { - axios.defaults.headers.common['Authorization'] = `Bearer ${auth.current.token}`; - } -} +axios.defaults.baseURL = API_BASE_URL; +axios.defaults.withCredentials = true; const request = { create: async ({ entity, jsonData }) => { try { - includeToken(); const response = await axios.post(entity + '/create', jsonData); successHandler(response, { notifyOnSuccess: true, @@ -40,7 +22,6 @@ const request = { }, createAndUpload: async ({ entity, jsonData }) => { try { - includeToken(); const response = await axios.post(entity + '/create', jsonData, { headers: { 'Content-Type': 'multipart/form-data', @@ -57,7 +38,6 @@ const request = { }, read: async ({ entity, id }) => { try { - includeToken(); const response = await axios.get(entity + '/read/' + id); successHandler(response, { notifyOnSuccess: false, @@ -70,7 +50,6 @@ const request = { }, update: async ({ entity, id, jsonData }) => { try { - includeToken(); const response = await axios.patch(entity + '/update/' + id, jsonData); successHandler(response, { notifyOnSuccess: true, @@ -83,7 +62,6 @@ const request = { }, updateAndUpload: async ({ entity, id, jsonData }) => { try { - includeToken(); const response = await axios.patch(entity + '/update/' + id, jsonData, { headers: { 'Content-Type': 'multipart/form-data', @@ -101,7 +79,6 @@ const request = { delete: async ({ entity, id }) => { try { - includeToken(); const response = await axios.delete(entity + '/delete/' + id); successHandler(response, { notifyOnSuccess: true, @@ -115,7 +92,6 @@ const request = { filter: async ({ entity, options = {} }) => { try { - includeToken(); let filter = options.filter ? 'filter=' + options.filter : ''; let equal = options.equal ? '&equal=' + options.equal : ''; let query = `?${filter}${equal}`; @@ -133,7 +109,6 @@ const request = { search: async ({ entity, options = {} }) => { try { - includeToken(); let query = '?'; for (var key in options) { query += key + '=' + options[key] + '&'; @@ -154,7 +129,6 @@ const request = { list: async ({ entity, options = {} }) => { try { - includeToken(); let query = '?'; for (var key in options) { query += key + '=' + options[key] + '&'; @@ -174,7 +148,6 @@ const request = { }, listAll: async ({ entity, options = {} }) => { try { - includeToken(); let query = '?'; for (var key in options) { query += key + '=' + options[key] + '&'; @@ -195,7 +168,6 @@ const request = { post: async ({ entity, jsonData }) => { try { - includeToken(); const response = await axios.post(entity, jsonData); return response.data; @@ -205,7 +177,6 @@ const request = { }, get: async ({ entity }) => { try { - includeToken(); const response = await axios.get(entity); return response.data; } catch (error) { @@ -214,7 +185,6 @@ const request = { }, patch: async ({ entity, jsonData }) => { try { - includeToken(); const response = await axios.patch(entity, jsonData); successHandler(response, { notifyOnSuccess: true, @@ -228,7 +198,6 @@ const request = { upload: async ({ entity, id, jsonData }) => { try { - includeToken(); const response = await axios.patch(entity + '/upload/' + id, jsonData, { headers: { 'Content-Type': 'multipart/form-data', @@ -252,7 +221,6 @@ const request = { summary: async ({ entity, options = {} }) => { try { - includeToken(); let query = '?'; for (var key in options) { query += key + '=' + options[key] + '&'; @@ -273,7 +241,6 @@ const request = { mail: async ({ entity, jsonData }) => { try { - includeToken(); const response = await axios.post(entity + '/mail/', jsonData); successHandler(response, { notifyOnSuccess: true, @@ -287,7 +254,6 @@ const request = { convert: async ({ entity, id }) => { try { - includeToken(); const response = await axios.get(`${entity}/convert/${id}`); successHandler(response, { notifyOnSuccess: true, diff --git a/frontend/src/router/AuthRouter.jsx b/frontend/src/router/AuthRouter.jsx index 2416dd984..1dc3e1eed 100644 --- a/frontend/src/router/AuthRouter.jsx +++ b/frontend/src/router/AuthRouter.jsx @@ -5,6 +5,7 @@ import NotFound from '@/pages/NotFound'; import ForgetPassword from '@/pages/ForgetPassword'; import ResetPassword from '@/pages/ResetPassword'; +import Register from '@/pages/Register'; import { useDispatch } from 'react-redux'; @@ -15,6 +16,7 @@ export default function AuthRouter() { } path="/" /> } path="/login" /> + } path="/register"/> } path="/logout" /> } path="/forgetpassword" /> } path="/resetpassword/:userId/:resetToken" /> diff --git a/frontend/src/router/routes.jsx b/frontend/src/router/routes.jsx index 156b89c51..a2727d70d 100644 --- a/frontend/src/router/routes.jsx +++ b/frontend/src/router/routes.jsx @@ -4,6 +4,7 @@ import { Navigate } from 'react-router-dom'; const Logout = lazy(() => import('@/pages/Logout.jsx')); const NotFound = lazy(() => import('@/pages/NotFound.jsx')); +const Register = lazy(()=> import('@/pages/Register.jsx')); const Dashboard = lazy(() => import('@/pages/Dashboard')); const Customer = lazy(() => import('@/pages/Customer')); @@ -24,8 +25,21 @@ const PaymentUpdate = lazy(() => import('@/pages/Payment/PaymentUpdate')); const Settings = lazy(() => import('@/pages/Settings/Settings')); const PaymentMode = lazy(() => import('@/pages/PaymentMode')); const Taxes = lazy(() => import('@/pages/Taxes')); - +const AdvancedSettings = lazy(() => import('@/pages/AdvancedSettings')); const Profile = lazy(() => import('@/pages/Profile')); +const Lead = lazy(() => import('@/pages/Lead/index')); +const Offer = lazy(() => import('@/pages/Offer/index')); +const OfferCreate = lazy(() => import('@/pages/Offer/OfferCreate')); +const OfferRead = lazy(() => import('@/pages/Offer/OfferRead')); +const OfferUpdate = lazy(() => import('@/pages/Offer/OfferUpdate')); + +const ExpenseCategory = lazy(() => import('@/pages/ExpenseCategory')); +const Expense = lazy(() => import('@/pages/Expense')); +const ProductCategory = lazy(() => import('@/pages/ProductCategory')); +const Product = lazy(() => import('@/pages/Product')); + +const People = lazy(() => import('@/pages/People')); +const Company = lazy(() => import('@/pages/Company')); const About = lazy(() => import('@/pages/About')); @@ -36,10 +50,22 @@ let routes = { path: '/login', element: , }, + { + path: '/verify/*', + element: , + }, + { + path: '/resetpassword/*', + element: , + }, { path: '/logout', element: , }, + { + path: '/register', + element: + }, { path: '/about', element: , @@ -52,6 +78,22 @@ let routes = { path: '/customer', element: , }, + { + path: '/people', + element: , + }, + { + path: '/company', + element: , + }, + { + path: '/product', + element: , + }, + { + path: '/category/product', + element: , + }, { path: '/invoice', @@ -119,10 +161,42 @@ let routes = { element: , }, + { + path: '/settings/advanced', + element: , + }, { path: '/profile', element: , }, + { + path: '/lead', + element: , + }, + { + path: '/offer', + element: , + }, + { + path: '/offer/create', + element: , + }, + { + path: '/offer/read/:id', + element: , + }, + { + path: '/offer/update/:id', + element: , + }, + { + path: '/expenses', + element: , + }, + { + path: 'category/expenses', + element: , + }, { path: '*', element: , diff --git a/frontend/src/style/partials/collapseBox.css b/frontend/src/style/partials/collapseBox.css index c4dc5eab2..c3e6fc994 100644 --- a/frontend/src/style/partials/collapseBox.css +++ b/frontend/src/style/partials/collapseBox.css @@ -10,7 +10,7 @@ font-size: 14px; text-transform: uppercase; cursor: pointer; - background-color: #ffffff; + background-color: #f9fafc; border-top: 1px solid #edf0f5; border-bottom: 1px solid #edf0f5; } diff --git a/frontend/src/style/partials/core.css b/frontend/src/style/partials/core.css index de1cd89f5..600f0ef9a 100644 --- a/frontend/src/style/partials/core.css +++ b/frontend/src/style/partials/core.css @@ -13,16 +13,17 @@ } .whiteBox { background: #fff; + border-radius: 6px; width: 100%; min-height: 100px; overflow: hidden; transition: all 0.3s ease-in-out; } .shadow { - border: 1px solid #e0e0e0; + box-shadow: 0px 0px 20px 3px rgba(150, 190, 238, 0.15); } .shadow:hover { - border: 1px solid #bdbdbd; + box-shadow: 0px 0px 30px 8px rgba(150, 190, 238, 0.25); } .line { border-top: 1px solid #edf0f5; diff --git a/frontend/src/style/partials/customAntd.css b/frontend/src/style/partials/customAntd.css index 7e4837e9d..00110ff2b 100644 --- a/frontend/src/style/partials/customAntd.css +++ b/frontend/src/style/partials/customAntd.css @@ -1,5 +1,5 @@ .ant-layout { - background: #ffffff !important; + background: #f9fafc !important; } .site-layout .site-layout-background { background: #fff; @@ -7,6 +7,31 @@ [data-theme='dark'] .site-layout .site-layout-background { background: #141414; } +.ant-layout .ant-layout-sider-light .ant-layout-sider-trigger { + /* background: none !important; */ + border-radius: 6px !important; + border: none !important; + margin-top: -10px !important; +} +.ant-layout-sider-trigger { + background: #fff; + border-top: 1px solid #edf0f5; + border-right: 1px solid #edf0f5; + color: #4f5d75; +} +.ant-layout-sider-zero-width-trigger { + top: 5px; + right: 10px; + color: #001529; + background: none; + font-size: 20px; +} + +.ant-menu-inline, +.ant-menu-vertical, +.ant-menu-vertical-left { + border: none !important; +} .headerIcon .ant-dropdown-menu { border-radius: 6px; diff --git a/frontend/src/style/partials/navigation.css b/frontend/src/style/partials/navigation.css index d7f613c66..68596d98d 100644 --- a/frontend/src/style/partials/navigation.css +++ b/frontend/src/style/partials/navigation.css @@ -31,9 +31,9 @@ padding: 12px 0px !important; } -/* .ant-btn.mobile-sidebar-btn { +.ant-btn.mobile-sidebar-btn { display: none; -} */ +} .tabsNavigation span { background-color: transparent; @@ -53,11 +53,11 @@ height: 100%; } - /* .ant-btn.mobile-sidebar-btn { + .ant-btn.mobile-sidebar-btn { display: block; position: absolute; top: 21px; - } */ + } .mobile-sidebar-wraper { display: block; diff --git a/frontend/src/style/partials/rest.css b/frontend/src/style/partials/rest.css index a5ef192d7..a80015352 100644 --- a/frontend/src/style/partials/rest.css +++ b/frontend/src/style/partials/rest.css @@ -17,5 +17,5 @@ body { margin: 0; padding: 0; - background: #ffffff !important; + background: #f9fafc !important; } diff --git a/frontend/src/utils/countryList.js b/frontend/src/utils/countryList.js index 95631e626..17667046f 100644 --- a/frontend/src/utils/countryList.js +++ b/frontend/src/utils/countryList.js @@ -1,32 +1,39 @@ export const countryList = [ { + icon: '๐Ÿ‡ฆ๐Ÿ‡ซ', label: 'Afghanistan', value: 'AF', timeZone: ['Asia/Kabul'], }, { + icon: '๐Ÿ‡ฆ๐Ÿ‡ฑ', label: 'Albania', value: 'AL', timeZone: ['Europe/Tirane'], }, { + icon: '๐Ÿ‡ฉ๐Ÿ‡ฟ', label: 'Algeria', value: 'DZ', timeZone: ['Africa/Algiers'], }, { + icon: '๐Ÿ‡ฆ๐Ÿ‡ฉ', label: 'Andorra', value: 'AD', }, { + icon: '๐Ÿ‡ฆ๐Ÿ‡ด', label: 'Angola', value: 'AO', }, { + icon: '๐Ÿ‡ฆ๐Ÿ‡ฎ', label: 'Anguilla', value: 'AI', }, { + icon: '๐Ÿ‡ฆ๐Ÿ‡ท', label: 'Argentina', value: 'AR', timeZone: [ @@ -45,15 +52,18 @@ export const countryList = [ ], }, { + icon: '๐Ÿ‡ฆ๐Ÿ‡ฒ', label: 'Armenia', value: 'AM', timeZone: ['Asia/Yerevan'], }, { + icon: '๐Ÿ‡ฆ๐Ÿ‡ผ', label: 'Aruba', value: 'AW', }, { + icon: '๐Ÿ‡ฆ๐Ÿ‡บ', label: 'Australia', value: 'AU', timeZone: [ @@ -73,77 +83,93 @@ export const countryList = [ ], }, { + icon: '๐Ÿ‡ฆ๐Ÿ‡น', label: 'Austria', value: 'AT', timeZone: ['Europe/Vienna'], }, { + icon: '๐Ÿ‡ฆ๐Ÿ‡ฟ', label: 'Azerbaijan', value: 'AZ', timeZone: ['Asia/Baku'], }, { + icon: '๐Ÿ‡ง๐Ÿ‡ธ', label: 'Bahamas', value: 'BS', }, { + icon: '๐Ÿ‡ง๐Ÿ‡ญ', label: 'Bahrain', value: 'BH', timeZone: ['Asia/Bahrain'], }, { + icon: '๐Ÿ‡ง๐Ÿ‡ฉ', label: 'Bangladesh', value: 'BD', timeZone: ['Asia/Dhaka'], }, { + icon: '๐Ÿ‡ง๐Ÿ‡ง', label: 'Barbados', value: 'BB', }, { + icon: '๐Ÿ‡ง๐Ÿ‡พ', label: 'Belarus', value: 'BY', timeZone: ['Europe/Minsk'], }, { + icon: '๐Ÿ‡ง๐Ÿ‡ช', label: 'Belgium', value: 'BE', timeZone: ['Europe/Brussels'], }, { + icon: '๐Ÿ‡ง๐Ÿ‡ฟ', label: 'Belize', value: 'BZ', timeZone: ['America/Belize'], }, { + icon: '๐Ÿ‡ง๐Ÿ‡ฏ', label: 'Benin', value: 'BJ', }, { + icon: '๐Ÿ‡ง๐Ÿ‡ฒ', label: 'Bermuda', value: 'BM', }, { + icon: '๐Ÿ‡ง๐Ÿ‡น', label: 'Bhutan', value: 'BT', timeZone: ['Asia/Thimphu'], }, { + icon: '๐Ÿ‡ง๐Ÿ‡ด', label: 'Bolivia', value: 'BO', timeZone: ['America/La_Paz'], }, { + icon: '๐Ÿ‡ง๐Ÿ‡ฆ', label: 'Bosnia and Herzegovina', value: 'BA', timeZone: ['Europe/Sarajevo'], }, { + icon: '๐Ÿ‡ง๐Ÿ‡ผ', label: 'Botswana', value: 'BW', timeZone: ['Africa/Gaborone'], }, { + icon: '๐Ÿ‡ง๐Ÿ‡ท', label: 'Brazil', value: 'BR', timeZone: [ @@ -166,38 +192,46 @@ export const countryList = [ ], }, { + icon: '๐Ÿ‡ง๐Ÿ‡ณ', label: 'Brunei Darussalam', value: 'BN', timeZone: ['Asia/Brunei'], }, { + icon: '๐Ÿ‡ง๐Ÿ‡ฌ', label: 'Bulgaria', value: 'BG', timeZone: ['Europe/Sofia'], }, { + icon: '๐Ÿ‡ง๐Ÿ‡ซ', label: 'Burkina Faso', value: 'BF', }, { + icon: '๐Ÿ‡ง๐Ÿ‡ฎ', label: 'Burundi', value: 'BI', }, { + icon: '๐Ÿ‡จ๐Ÿ‡ป', label: 'Cabo Verde', value: 'CV', }, { + icon: '๐Ÿ‡ฐ๐Ÿ‡ญ', label: 'Cambodia', value: 'KH', timeZone: ['Asia/Phnom_Penh'], }, { + icon: '๐Ÿ‡จ๐Ÿ‡ฒ', label: 'Cameroon', value: 'CM', timeZone: ['Africa/Douala'], }, { + icon: '๐Ÿ‡จ๐Ÿ‡ฆ', label: 'Canada', value: 'CA', timeZone: [ @@ -232,385 +266,467 @@ export const countryList = [ ], }, { + icon: '๐Ÿ‡จ๐Ÿ‡ซ', label: 'Central African Republic', value: 'CF', }, { + icon: '๐Ÿ‡น๐Ÿ‡ฉ', label: 'Chad', value: 'TD', }, { + icon: '๐Ÿ‡จ๐Ÿ‡ฑ', label: 'Chile', value: 'CL', timeZone: ['America/Santiago', 'Pacific/Easter'], }, { + icon: '๐Ÿ‡จ๐Ÿ‡ณ', label: 'China', value: 'CN', timeZone: ['Asia/Shanghai', 'Asia/Urumqi'], }, { + icon: '๐Ÿ‡จ๐Ÿ‡ด', label: 'Colombia', value: 'CO', timeZone: ['America/Bogota'], }, { + icon: '๐Ÿ‡ฐ๐Ÿ‡ฒ', label: 'Comoros', value: 'KM', }, { + icon: '๐Ÿ‡จ๐Ÿ‡ฉ', label: 'Congo', value: 'CD', timeZone: ['Africa/Kinshasa', 'Africa/Lubumbashi'], }, { + icon: '๐Ÿ‡จ๐Ÿ‡ฌ', label: 'Congo', value: 'CG', }, { + icon: '๐Ÿ‡จ๐Ÿ‡ท', label: 'Costa Rica', value: 'CR', timeZone: ['America/Costa_Rica'], }, { + icon: '๐Ÿ‡ญ๐Ÿ‡ท', label: 'Croatia', value: 'HR', timeZone: ['Europe/Zagreb'], }, { + icon: '๐Ÿ‡จ๐Ÿ‡บ', label: 'Cuba', value: 'CU', timeZone: ['America/Havana'], }, { + icon: '๐Ÿ‡จ๐Ÿ‡พ', label: 'Cyprus', value: 'CY', }, { + icon: '๐Ÿ‡จ๐Ÿ‡ฟ', label: 'Czechia', value: 'CZ', timeZone: ['Europe/Prague'], }, { + icon: '๐Ÿ‡จ๐Ÿ‡ฎ', label: 'Cote d Ivoire', value: 'CI', timeZone: ['Africa/Abidjan'], }, { + icon: '๐Ÿ‡ฉ๐Ÿ‡ฐ', label: 'Denmark', value: 'DK', timeZone: ['Europe/Copenhagen'], }, { + icon: '๐Ÿ‡ฉ๐Ÿ‡ฏ', label: 'Djibouti', value: 'DJ', timeZone: ['Africa/Djibouti'], }, { + icon: '๐Ÿ‡ฉ๐Ÿ‡ฒ', label: 'Dominica', value: 'DM', }, { + icon: '๐Ÿ‡ฉ๐Ÿ‡ด', label: 'Dominican Republic', value: 'DO', timeZone: ['America/Santo_Domingo'], }, { + icon: '๐Ÿ‡ช๐Ÿ‡จ', label: 'Ecuador', value: 'EC', timeZone: ['America/Guayaquil', 'Pacific/Galapagos'], }, { + icon: '๐Ÿ‡ช๐Ÿ‡ฌ', label: 'Egypt', value: 'EG', timeZone: ['Africa/Cairo'], }, { + icon: '๐Ÿ‡ธ๐Ÿ‡ป', label: 'El Salvador', value: 'SV', timeZone: ['America/El_Salvador'], }, { + icon: '๐Ÿ‡ช๐Ÿ‡ท', label: 'Eritrea', value: 'ER', timeZone: ['Africa/Asmara'], }, { + icon: '๐Ÿ‡ช๐Ÿ‡ช', label: 'Estonia', value: 'EE', timeZone: ['Europe/Tallinn'], }, { + icon: '๐Ÿ‡ธ๐Ÿ‡ฟ', label: 'Eswatini', value: 'SZ', }, { + icon: '๐Ÿ‡ช๐Ÿ‡น', label: 'Ethiopia', value: 'ET', timeZone: ['Africa/Addis_Ababa'], }, { + icon: '๐Ÿ‡ซ๐Ÿ‡ฏ', label: 'Fiji', value: 'FJ', }, { + icon: '๐Ÿ‡ซ๐Ÿ‡ฎ', label: 'Finland', value: 'FI', timeZone: ['Europe/Helsinki'], }, { + icon: '๐Ÿ‡ซ๐Ÿ‡ท', label: 'France', value: 'FR', timeZone: ['Europe/Paris'], }, { + icon: '๐Ÿ‡ฌ๐Ÿ‡ฆ', label: 'Gabon', value: 'GA', }, { + icon: '๐Ÿ‡ฌ๐Ÿ‡ฒ', label: 'Gambia', value: 'GM', }, { + icon: '๐Ÿ‡ฌ๐Ÿ‡ช', label: 'Georgia', value: 'GE', timeZone: ['Asia/Tbilisi'], }, { + icon: '๐Ÿ‡ฉ๐Ÿ‡ช', label: 'Germany', value: 'DE', timeZone: ['Europe/Berlin', 'Europe/Busingen'], }, { + icon: '๐Ÿ‡ฌ๐Ÿ‡ญ', label: 'Ghana', value: 'GH', }, { + icon: '๐Ÿ‡ฌ๐Ÿ‡ฎ', label: 'Gibraltar', value: 'GI', }, { + icon: '๐Ÿ‡ฌ๐Ÿ‡ท', label: 'Greece', value: 'GR', timeZone: ['Europe/Athens'], }, { + icon: '๐Ÿ‡ฌ๐Ÿ‡ฑ', label: 'Greenland', value: 'GL', timeZone: ['America/Godthab', 'America/Danmarkshavn', 'America/Scoresbysund', 'America/Thule'], }, { + icon: '๐Ÿ‡ฌ๐Ÿ‡ต', label: 'Guadeloupe', value: 'GP', }, { + icon: '๐Ÿ‡ฌ๐Ÿ‡บ', label: 'Guam', value: 'GU', }, { + icon: '๐Ÿ‡ฌ๐Ÿ‡น', label: 'Guatemala', value: 'GT', timeZone: ['America/Guatemala'], }, { + icon: '๐Ÿ‡ฌ๐Ÿ‡ฌ', label: 'Guernsey', value: 'GG', }, { + icon: '๐Ÿ‡ฌ๐Ÿ‡ณ', label: 'Guinea', value: 'GN', }, { + icon: '๐Ÿ‡ฌ๐Ÿ‡ผ', label: 'Guinea-Bissau', value: 'GW', }, { + icon: '๐Ÿ‡ฌ๐Ÿ‡พ', label: 'Guyana', value: 'GY', }, { + icon: '๐Ÿ‡ญ๐Ÿ‡น', label: 'Haiti', value: 'HT', timeZone: ['America/Port-au-Prince'], }, { + icon: '๐Ÿ‡ญ๐Ÿ‡ณ', label: 'Honduras', value: 'HN', timeZone: ['America/Tegucigalpa'], }, { + icon: '๐Ÿ‡ญ๐Ÿ‡ฐ', label: 'Hong Kong', value: 'HK', timeZone: ['Asia/Hong_Kong'], }, { + icon: '๐Ÿ‡ญ๐Ÿ‡บ', label: 'Hungary', value: 'HU', timeZone: ['Europe/Budapest'], }, { + icon: '๐Ÿ‡ฎ๐Ÿ‡ธ', label: 'Iceland', value: 'IS', timeZone: ['Atlantic/Reykjavik'], }, { + icon: '๐Ÿ‡ฎ๐Ÿ‡ณ', value: 'IN', label: 'India', timeZone: ['Asia/Kolkata'], }, { + icon: '๐Ÿ‡ฎ๐Ÿ‡ฉ', value: 'ID', label: 'Indonesia', timeZone: ['Asia/Jakarta', 'Asia/Pontianak', 'Asia/Makassar', 'Asia/Jayapura'], }, { + icon: '๐Ÿ‡ฎ๐Ÿ‡ท', value: 'IR', label: 'Iran', timeZone: ['Asia/Tehran'], }, { + icon: '๐Ÿ‡ฎ๐Ÿ‡ถ', value: 'IQ', label: 'Iraq', timeZone: ['Asia/Baghdad'], }, { + icon: '๐Ÿ‡ฎ๐Ÿ‡ช', value: 'IE', label: 'Ireland', timeZone: ['Europe/Dublin'], }, { + icon: '๐Ÿ‡ฎ๐Ÿ‡ฑ', value: 'IL', label: 'Israel', timeZone: ['Asia/Jerusalem'], }, { + icon: '๐Ÿ‡ฎ๐Ÿ‡น', value: 'IT', label: 'Italy', timeZone: ['Europe/Rome'], }, { + icon: '๐Ÿ‡ฏ๐Ÿ‡ฒ', value: 'JM', label: 'Jamaica', timeZone: ['America/Jamaica'], }, { + icon: '๐Ÿ‡ฏ๐Ÿ‡ต', value: 'JP', label: 'Japan', timeZone: ['Asia/Tokyo'], }, { + icon: '๐Ÿ‡ฏ๐Ÿ‡ด', value: 'JO', label: 'Jordan', timeZone: ['Asia/Amman'], }, { + icon: '๐Ÿ‡ฐ๐Ÿ‡ฟ', value: 'KZ', label: 'Kazakhstan', timeZone: ['Asia/Almaty', 'Asia/Qyzylorda', 'Asia/Aqtobe', 'Asia/Aqtau', 'Asia/Oral'], }, { + icon: '๐Ÿ‡ฐ๐Ÿ‡ช', value: 'KE', label: 'Kenya', timeZone: ['Africa/Nairobi'], }, { + icon: '๐Ÿ‡ฐ๐Ÿ‡ต', value: 'KP', label: 'Korea', }, { + icon: '๐Ÿ‡ฐ๐Ÿ‡ท', value: 'KR', label: 'Korea', timeZone: ['Asia/Seoul'], }, { + icon: '๐Ÿ‡ฐ๐Ÿ‡ผ', value: 'KW', label: 'Kuwait', timeZone: ['Asia/Kuwait'], }, { + icon: '๐Ÿ‡ฐ๐Ÿ‡ฌ', value: 'KG', label: 'Kyrgyzstan', timeZone: ['Asia/Bishkek'], }, { + icon: '๐Ÿ‡ฑ๐Ÿ‡ป', value: 'LV', label: 'Latvia', timeZone: ['Europe/Riga'], }, { + icon: '๐Ÿ‡ฑ๐Ÿ‡ง', value: 'LB', label: 'Lebanon', timeZone: ['Asia/Beirut'], }, { + icon: '๐Ÿ‡ฑ๐Ÿ‡ธ', value: 'LS', label: 'Lesotho', }, { + icon: '๐Ÿ‡ฑ๐Ÿ‡ท', value: 'LR', label: 'Liberia', }, { + icon: '๐Ÿ‡ฑ๐Ÿ‡พ', value: 'LY', label: 'Libya', timeZone: ['Africa/Tripoli'], }, { + icon: '๐Ÿ‡ฑ๐Ÿ‡ฎ', value: 'LI', label: 'Liechtenstein', timeZone: ['Europe/Vaduz'], }, { + icon: '๐Ÿ‡ฑ๐Ÿ‡น', value: 'LT', label: 'Lithuania', timeZone: ['Europe/Vilnius'], }, { + icon: '๐Ÿ‡ฑ๐Ÿ‡บ', value: 'LU', label: 'Luxembourg', timeZone: ['Europe/Luxembourg'], }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡ฌ', value: 'MG', label: 'Madagascar', }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡ผ', value: 'MW', label: 'Malawi', }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡พ', value: 'MY', label: 'Malaysia', timeZone: ['Asia/Kuala_Lumpur', 'Asia/Kuching'], }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡ป', value: 'MV', label: 'Maldives', timeZone: ['Indian/Maldives'], }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡ฑ', value: 'ML', label: 'Mali', timeZone: ['Africa/Bamako'], }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡น', value: 'MT', label: 'Malta', timeZone: ['Europe/Malta'], }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡ถ', value: 'MQ', label: 'Martinique', }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡ท', value: 'MR', label: 'Mauritania', }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡บ', value: 'MU', label: 'Mauritius', }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡ฝ', value: 'MX', label: 'Mexico', timeZone: [ @@ -628,154 +744,186 @@ export const countryList = [ ], }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡ฉ', value: 'MD', label: 'Moldova', timeZone: ['Europe/Chisinau'], }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡จ', value: 'MC', label: 'Monaco', timeZone: ['Europe/Monaco'], }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡ณ', value: 'MN', label: 'Mongolia', timeZone: ['Asia/Ulaanbaatar', 'Asia/Hovd', 'Asia/Choibalsan'], }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡ช', value: 'ME', label: 'Montenegro', timeZone: ['Europe/Podgorica'], }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡ธ', value: 'MS', label: 'Montserrat', }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡ฆ', value: 'MA', label: 'Morocco', timeZone: ['Africa/Casablanca'], }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡ฟ', value: 'MZ', label: 'Mozambique', }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡ฒ', value: 'MM', label: 'Myanmar', timeZone: ['Asia/Rangoon'], }, { + icon: '๐Ÿ‡ณ๐Ÿ‡ฆ', value: 'NA', label: 'Namibia', }, { + icon: '๐Ÿ‡ณ๐Ÿ‡ต', value: 'NP', label: 'Nepal', timeZone: ['Asia/Kathmandu'], }, { + icon: '๐Ÿ‡ณ๐Ÿ‡ฑ', value: 'NL', label: 'Netherlands', timeZone: ['Europe/Amsterdam'], }, { + icon: '๐Ÿ‡ณ๐Ÿ‡จ', value: 'NC', label: 'New Caledonia', }, { + icon: '๐Ÿ‡ณ๐Ÿ‡ฟ', value: 'NZ', label: 'New Zealand', timeZone: ['Pacific/Auckland', 'Pacific/Chatham'], }, { + icon: '๐Ÿ‡ณ๐Ÿ‡ฎ', value: 'NI', label: 'Nicaragua', timeZone: ['America/Managua'], }, { + icon: '๐Ÿ‡ณ๐Ÿ‡ช', value: 'NE', label: 'Niger', }, { + icon: '๐Ÿ‡ณ๐Ÿ‡ฌ', value: 'NG', label: 'Nigeria', timeZone: ['Africa/Lagos'], }, { + icon: '๐Ÿ‡ณ๐Ÿ‡ด', value: 'NO', label: 'Norway', timeZone: ['Europe/Oslo'], }, { + icon: '๐Ÿ‡ด๐Ÿ‡ฒ', value: 'OM', label: 'Oman', timeZone: ['Asia/Muscat'], }, { + icon: '๐Ÿ‡ต๐Ÿ‡ฐ', value: 'PK', label: 'Pakistan', timeZone: ['Asia/Karachi'], }, { + icon: '๐Ÿ‡ต๐Ÿ‡ธ', value: 'PS', label: 'Palestine', }, { + icon: '๐Ÿ‡ต๐Ÿ‡ฆ', value: 'PA', label: 'Panama', timeZone: ['America/Panama'], }, { + icon: '๐Ÿ‡ต๐Ÿ‡ฌ', value: 'PG', label: 'Papua New Guinea', }, { + icon: '๐Ÿ‡ต๐Ÿ‡พ', value: 'PY', label: 'Paraguay', timeZone: ['America/Asuncion'], }, { + icon: '๐Ÿ‡ต๐Ÿ‡ช', value: 'PE', label: 'Peru', timeZone: ['America/Lima'], }, { + icon: '๐Ÿ‡ต๐Ÿ‡ญ', value: 'PH', label: 'Philippines', timeZone: ['Asia/Manila'], }, { + icon: '๐Ÿ‡ต๐Ÿ‡ฑ', value: 'PL', label: 'Poland', timeZone: ['Europe/Warsaw'], }, { + icon: '๐Ÿ‡ต๐Ÿ‡น', value: 'PT', label: 'Portugal', timeZone: ['Europe/Lisbon', 'Atlantic/Madeira', 'Atlantic/Azores'], }, { + icon: '๐Ÿ‡ต๐Ÿ‡ท', value: 'PR', label: 'Puerto Rico', timeZone: ['America/Puerto_Rico'], }, { + icon: '๐Ÿ‡ถ๐Ÿ‡ฆ', value: 'QA', label: 'Qatar', timeZone: ['Asia/Qatar'], }, { + icon: '๐Ÿ‡ฒ๐Ÿ‡ฐ', value: 'MK', label: 'Macedonia', timeZone: ['Europe/Skopje'], }, { + icon: '๐Ÿ‡ท๐Ÿ‡ด', value: 'RO', label: 'Romania', timeZone: ['Europe/Bucharest'], }, { + icon: '๐Ÿ‡ท๐Ÿ‡บ', value: 'RU', label: 'Russia', timeZone: [ @@ -806,129 +954,156 @@ export const countryList = [ ], }, { + icon: '๐Ÿ‡ท๐Ÿ‡ผ', value: 'RW', label: 'Rwanda', timeZone: ['Africa/Kigali'], }, { + icon: '๐Ÿ‡ท๐Ÿ‡ช', value: 'RE', label: 'Rรฉunion', timeZone: ['Indian/Reunion'], }, { + icon: '๐Ÿ‡ธ๐Ÿ‡ฆ', value: 'SA', label: 'Saudi Arabia', timeZone: ['Asia/Riyadh'], }, { + icon: '๐Ÿ‡ธ๐Ÿ‡ณ', value: 'SN', label: 'Senegal', timeZone: ['Africa/Dakar'], }, { + icon: '๐Ÿ‡ท๐Ÿ‡ธ', value: 'RS', label: 'Serbia', timeZone: ['Europe/Belgrade'], }, { + icon: '๐Ÿ‡ธ๐Ÿ‡ฌ', value: 'SG', label: 'Singapore', timeZone: ['Asia/Singapore'], }, { + icon: '๐Ÿ‡ธ๐Ÿ‡ฐ', value: 'SK', label: 'Slovakia', timeZone: ['Europe/Bratislava'], }, { + icon: '๐Ÿ‡ธ๐Ÿ‡ฎ', value: 'SI', label: 'Slovenia', timeZone: ['Europe/Ljubljana'], }, { + icon: '๐Ÿ‡ธ๐Ÿ‡ด', value: 'SO', label: 'Somalia', timeZone: ['Africa/Mogadishu'], }, { + icon: '๐Ÿ‡ฟ๐Ÿ‡ฆ', value: 'ZA', label: 'South Africa', timeZone: ['Africa/Johannesburg'], }, { + icon: '๐Ÿ‡ธ๐Ÿ‡ธ', value: 'SS', label: 'South Sudan', }, { + icon: '๐Ÿ‡ช๐Ÿ‡ธ', value: 'ES', label: 'Spain', timeZone: ['Europe/Madrid', 'Africa/Ceuta', 'Atlantic/Canary'], }, { + icon: '๐Ÿ‡ฑ๐Ÿ‡ฐ', value: 'LK', label: 'Sri Lanka', timeZone: ['Asia/Colombo'], }, { + icon: '๐Ÿ‡ธ๐Ÿ‡ฉ', value: 'SD', label: 'Sudan', }, { + icon: '๐Ÿ‡ธ๐Ÿ‡ท', value: 'SR', label: 'Suriname', }, { + icon: '๐Ÿ‡ธ๐Ÿ‡ช', value: 'SE', label: 'Sweden', timeZone: ['Europe/Stockholm'], }, { + icon: '๐Ÿ‡จ๐Ÿ‡ญ', value: 'CH', label: 'Switzerland', timeZone: ['Europe/Zurich'], }, { + icon: '๐Ÿ‡ธ๐Ÿ‡พ', value: 'SY', label: 'Syria', timeZone: ['Asia/Damascus'], }, { + icon: '๐Ÿ‡น๐Ÿ‡ผ', value: 'TW', label: 'Taiwan', timeZone: ['Asia/Taipei'], }, { + icon: '๐Ÿ‡น๐Ÿ‡ฏ', value: 'TJ', label: 'Tajikistan', timeZone: ['Asia/Dushanbe'], }, { + icon: '๐Ÿ‡น๐Ÿ‡ฟ', value: 'TZ', label: 'Tanzania', }, { + icon: '๐Ÿ‡น๐Ÿ‡ญ', value: 'TH', label: 'Thailand', timeZone: ['Asia/Bangkok'], }, { + icon: '๐Ÿ‡น๐Ÿ‡ฑ', value: 'TL', label: 'Timor-Leste', }, { + icon: '๐Ÿ‡น๐Ÿ‡ฌ', value: 'TG', label: 'Togo', }, { + icon: '๐Ÿ‡น๐Ÿ‡ด', value: 'TO', label: 'Tonga', }, { + icon: '๐Ÿ‡น๐Ÿ‡ณ', value: 'TN', label: 'Tunisia', timeZone: ['Africa/Tunis'], }, { + icon: '๐Ÿ‡น๐Ÿ‡ท', value: 'TR', label: 'Turkey', timeZone: ['Europe/Istanbul'], @@ -936,35 +1111,36 @@ export const countryList = [ { value: 'TM', label: 'Turkmenistan', - + icon: '๐Ÿ‡น๐Ÿ‡ฒ', timeZone: ['Asia/Ashgabat'], }, { value: 'UG', label: 'Uganda', + icon: '๐Ÿ‡บ๐Ÿ‡ฌ', }, { value: 'UA', label: 'Ukraine', - + icon: '๐Ÿ‡บ๐Ÿ‡ฆ', timeZone: ['Europe/Kiev', 'Europe/Uzhgorod', 'Europe/Zaporozhye'], }, { value: 'AE', label: 'United Arab Emirates', - + icon: '๐Ÿ‡ฆ๐Ÿ‡ช', timeZone: ['Asia/Dubai'], }, { value: 'GB', label: 'United Kingdom', - + icon: '๐Ÿ‡ฌ๐Ÿ‡ง', timeZone: ['Europe/London'], }, { value: 'US', label: 'United States', - + icon: '๐Ÿ‡บ๐Ÿ‡ธ', timeZone: [ 'America/New_York', 'America/Detroit', @@ -1000,41 +1176,42 @@ export const countryList = [ { value: 'UY', label: 'Uruguay', - + icon: '๐Ÿ‡บ๐Ÿ‡พ', timeZone: ['America/Montevideo'], }, { value: 'UZ', label: 'Uzbekistan', - + icon: '๐Ÿ‡บ๐Ÿ‡ฟ', timeZone: ['Asia/Samarkand', 'Asia/Tashkent'], }, { value: 'VE', label: 'Venezuela', - + icon: '๐Ÿ‡ป๐Ÿ‡ช', timeZone: ['America/Caracas'], }, { value: 'VN', label: 'Vietnam', - + icon: '๐Ÿ‡ป๐Ÿ‡ณ', timeZone: ['Asia/Ho_Chi_Minh'], }, { value: 'YE', label: 'Yemen', - + icon: '๐Ÿ‡พ๐Ÿ‡ช', timeZone: ['Asia/Aden'], }, { value: 'ZM', label: 'Zambia', + icon: '๐Ÿ‡ฟ๐Ÿ‡ฒ', }, { value: 'ZW', label: 'Zimbabwe', - + icon: '๐Ÿ‡ฟ๐Ÿ‡ผ', timeZone: ['Africa/Harare'], }, ];