From 41282cbfbcaf42178bb057addfc0c11d694b0896 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 26 Sep 2025 05:53:08 +0000 Subject: [PATCH] Initial version --- .gitignore | 3 + 502.html | 187 + Dockerfile | 17 + Dockerfile.dev | 85 + LICENSE | 1 + README.md | 200 + app-shell/.eslintrc.cjs | 26 + app-shell/.prettierrc | 11 + app-shell/.sequelizerc | 7 + app-shell/Dockerfile | 23 + app-shell/README.md | 13 + app-shell/package.json | 43 + app-shell/src/_schema.json | 5 + app-shell/src/config.js | 18 + app-shell/src/helpers.js | 23 + app-shell/src/index.js | 54 + .../src/middlewares/check-permissions.js | 17 + app-shell/src/middlewares/modify-path.js | 8 + app-shell/src/routes/executor.js | 346 + app-shell/src/routes/vcs.js | 40 + app-shell/src/services/database.js | 88 + app-shell/src/services/executor.js | 1206 ++++ .../notifications/errors/forbidden.js | 16 + .../notifications/errors/validation.js | 16 + .../src/services/notifications/helpers.js | 30 + app-shell/src/services/notifications/list.js | 100 + app-shell/src/services/project-events.js | 67 + app-shell/src/services/screenshot_service.js | 83 + app-shell/src/services/vcs.js | 1205 ++++ app-shell/yarn.lock | 3044 +++++++++ backend/.prettierrc | 11 + backend/.sequelizerc | 7 + backend/Dockerfile | 23 + backend/README.md | 67 + backend/package.json | 53 + backend/src/auth/auth.js | 79 + backend/src/config.js | 77 + backend/src/db/api/audit_logs.js | 428 ++ backend/src/db/api/data_connections.js | 413 ++ backend/src/db/api/file.js | 73 + backend/src/db/api/fin_accounts.js | 435 ++ backend/src/db/api/fin_ap.js | 443 ++ backend/src/db/api/fin_ar.js | 444 ++ backend/src/db/api/fin_budgets.js | 473 ++ backend/src/db/api/fin_customers.js | 380 ++ backend/src/db/api/fin_forecasts.js | 477 ++ backend/src/db/api/fin_transactions.js | 614 ++ backend/src/db/api/fin_vendors.js | 372 + backend/src/db/api/hr_applicants.js | 467 ++ backend/src/db/api/hr_attendance.js | 463 ++ backend/src/db/api/hr_employees.js | 529 ++ backend/src/db/api/hr_payroll.js | 491 ++ backend/src/db/api/hr_positions.js | 398 ++ backend/src/db/api/hr_reqs.js | 458 ++ backend/src/db/api/ops_incidents.js | 514 ++ backend/src/db/api/ops_inventory.js | 469 ++ backend/src/db/api/ops_lines.js | 388 ++ backend/src/db/api/ops_orders.js | 445 ++ backend/src/db/api/ops_plants.js | 366 + backend/src/db/api/ops_production_log.js | 523 ++ backend/src/db/api/ops_shipments.js | 425 ++ backend/src/db/api/organizations.js | 492 ++ backend/src/db/api/permissions.js | 257 + backend/src/db/api/roles.js | 344 + backend/src/db/api/users.js | 804 +++ backend/src/db/db.config.js | 31 + backend/src/db/migrations/1758865238752.js | 3771 ++++++++++ backend/src/db/models/audit_logs.js | 85 + backend/src/db/models/data_connections.js | 85 + backend/src/db/models/file.js | 53 + backend/src/db/models/fin_accounts.js | 114 + backend/src/db/models/fin_ap.js | 87 + backend/src/db/models/fin_ar.js | 87 + backend/src/db/models/fin_budgets.js | 85 + backend/src/db/models/fin_customers.js | 97 + backend/src/db/models/fin_forecasts.js | 85 + backend/src/db/models/fin_transactions.js | 117 + backend/src/db/models/fin_vendors.js | 89 + backend/src/db/models/hr_applicants.js | 91 + backend/src/db/models/hr_attendance.js | 83 + backend/src/db/models/hr_employees.js | 123 + backend/src/db/models/hr_payroll.js | 89 + backend/src/db/models/hr_positions.js | 91 + backend/src/db/models/hr_reqs.js | 101 + backend/src/db/models/index.js | 47 + backend/src/db/models/ops_incidents.js | 99 + backend/src/db/models/ops_inventory.js | 85 + backend/src/db/models/ops_lines.js | 89 + backend/src/db/models/ops_orders.js | 87 + backend/src/db/models/ops_plants.js | 89 + backend/src/db/models/ops_production_log.js | 89 + backend/src/db/models/ops_shipments.js | 83 + backend/src/db/models/organizations.js | 425 ++ backend/src/db/models/permissions.js | 49 + backend/src/db/models/roles.js | 86 + backend/src/db/models/users.js | 187 + backend/src/db/reset.js | 16 + .../db/seeders/20200430130759-admin-user.js | 84 + .../db/seeders/20200430130760-user-roles.js | 3131 +++++++++ .../db/seeders/20231127130745-sample-data.js | 6042 +++++++++++++++++ backend/src/db/utils.js | 24 + backend/src/helpers.js | 23 + backend/src/index.js | 339 + backend/src/middlewares/check-permissions.js | 176 + backend/src/middlewares/upload.js | 11 + backend/src/routes/audit_logs.js | 462 ++ backend/src/routes/auth.js | 270 + backend/src/routes/contactForm.js | 33 + backend/src/routes/data_connections.js | 464 ++ backend/src/routes/file.js | 40 + backend/src/routes/fin_accounts.js | 460 ++ backend/src/routes/fin_ap.js | 452 ++ backend/src/routes/fin_ar.js | 452 ++ backend/src/routes/fin_budgets.js | 467 ++ backend/src/routes/fin_customers.js | 462 ++ backend/src/routes/fin_forecasts.js | 467 ++ backend/src/routes/fin_transactions.js | 489 ++ backend/src/routes/fin_vendors.js | 462 ++ backend/src/routes/hr_applicants.js | 467 ++ backend/src/routes/hr_attendance.js | 457 ++ backend/src/routes/hr_employees.js | 483 ++ backend/src/routes/hr_payroll.js | 470 ++ backend/src/routes/hr_positions.js | 466 ++ backend/src/routes/hr_reqs.js | 454 ++ backend/src/routes/openai.js | 244 + backend/src/routes/ops_incidents.js | 471 ++ backend/src/routes/ops_inventory.js | 481 ++ backend/src/routes/ops_lines.js | 452 ++ backend/src/routes/ops_orders.js | 457 ++ backend/src/routes/ops_plants.js | 458 ++ backend/src/routes/ops_production_log.js | 476 ++ backend/src/routes/ops_shipments.js | 467 ++ backend/src/routes/organizationLogin.js | 46 + backend/src/routes/organizations.js | 456 ++ backend/src/routes/permissions.js | 442 ++ backend/src/routes/pexels.js | 106 + backend/src/routes/roles.js | 444 ++ backend/src/routes/search.js | 60 + backend/src/routes/users.js | 458 ++ backend/src/services/audit_logs.js | 114 + backend/src/services/auth.js | 228 + backend/src/services/data_connections.js | 121 + .../emailAddressVerification.html | 52 + .../invitation/invitationTemplate.html | 56 + .../passwordReset/passwordResetEmail.html | 55 + backend/src/services/email/index.js | 41 + .../email/list/addressVerification.js | 41 + backend/src/services/email/list/invitation.js | 41 + .../src/services/email/list/passwordReset.js | 42 + backend/src/services/file.js | 202 + backend/src/services/fin_accounts.js | 117 + backend/src/services/fin_ap.js | 114 + backend/src/services/fin_ar.js | 114 + backend/src/services/fin_budgets.js | 114 + backend/src/services/fin_customers.js | 117 + backend/src/services/fin_forecasts.js | 117 + backend/src/services/fin_transactions.js | 121 + backend/src/services/fin_vendors.js | 114 + backend/src/services/hr_applicants.js | 117 + backend/src/services/hr_attendance.js | 117 + backend/src/services/hr_employees.js | 117 + backend/src/services/hr_payroll.js | 114 + backend/src/services/hr_positions.js | 117 + backend/src/services/hr_reqs.js | 114 + .../notifications/errors/forbidden.js | 16 + .../notifications/errors/validation.js | 16 + backend/src/services/notifications/helpers.js | 30 + backend/src/services/notifications/list.js | 100 + backend/src/services/openai.js | 68 + backend/src/services/ops_incidents.js | 117 + backend/src/services/ops_inventory.js | 117 + backend/src/services/ops_lines.js | 114 + backend/src/services/ops_orders.js | 114 + backend/src/services/ops_plants.js | 114 + backend/src/services/ops_production_log.js | 121 + backend/src/services/ops_shipments.js | 117 + backend/src/services/organizations.js | 117 + backend/src/services/permissions.js | 114 + backend/src/services/roles.js | 389 ++ backend/src/services/search.js | 235 + backend/src/services/users.js | 163 + backend/watcher.js | 45 + backend/yarn.lock | 4470 ++++++++++++ cloudbuild.yaml | 14 + docker/.gitignore | 1 + docker/README.md | 46 + docker/docker-compose.yml | 56 + docker/start-backend.sh | 2 + docker/wait-for-it.sh | 182 + frontend/.eslintrc.cjs | 10 + frontend/.gitignore | 33 + frontend/.prettierrc | 10 + frontend/Dockerfile | 19 + frontend/LICENSE-justboil | 21 + frontend/README.md | 91 + frontend/next-env.d.ts | 6 + frontend/next.config.mjs | 50 + frontend/package.json | 77 + frontend/postcss.config.js | 9 + frontend/prettier.config.js | 13 + frontend/public/data-sources/clients.json | 224 + frontend/public/data-sources/history.json | 40 + frontend/public/favicon.svg | 27 + frontend/public/locales/de/common.json | 55 + frontend/public/locales/en/common.json | 52 + frontend/public/locales/es/common.json | 55 + frontend/public/locales/fr/common.json | 55 + frontend/src/colors.ts | 150 + frontend/src/components/AsideMenu.tsx | 32 + frontend/src/components/AsideMenuItem.tsx | 116 + frontend/src/components/AsideMenuLayer.tsx | 103 + frontend/src/components/AsideMenuList.tsx | 35 + .../components/Audit_logs/CardAudit_logs.tsx | 162 + .../components/Audit_logs/ListAudit_logs.tsx | 118 + .../components/Audit_logs/TableAudit_logs.tsx | 500 ++ .../Audit_logs/configureAudit_logsCols.tsx | 152 + frontend/src/components/BaseButton.tsx | 106 + frontend/src/components/BaseButtons.tsx | 40 + frontend/src/components/BaseDivider.tsx | 14 + frontend/src/components/BaseIcon.tsx | 39 + frontend/src/components/BigCalendar.tsx | 171 + frontend/src/components/CardBox.tsx | 70 + .../src/components/CardBoxComponentBody.tsx | 21 + .../src/components/CardBoxComponentEmpty.tsx | 11 + .../src/components/CardBoxComponentFooter.tsx | 10 + .../src/components/CardBoxComponentTitle.tsx | 17 + frontend/src/components/CardBoxModal.tsx | 75 + .../src/components/ChartLineSample/config.ts | 54 + .../src/components/ChartLineSample/index.tsx | 44 + frontend/src/components/ClickOutside.tsx | 45 + .../src/components/DataGridMultiSelect.tsx | 55 + .../Data_connections/CardData_connections.tsx | 159 + .../Data_connections/ListData_connections.tsx | 121 + .../TableData_connections.tsx | 500 ++ .../configureData_connectionsCols.tsx | 146 + frontend/src/components/DevModeBadge.tsx | 150 + .../src/components/DragDropFilePicker.tsx | 124 + frontend/src/components/ErrorBoundary.tsx | 218 + .../Fin_accounts/CardFin_accounts.tsx | 152 + .../Fin_accounts/ListFin_accounts.tsx | 124 + .../Fin_accounts/TableFin_accounts.tsx | 500 ++ .../configureFin_accountsCols.tsx | 152 + frontend/src/components/Fin_ap/CardFin_ap.tsx | 164 + frontend/src/components/Fin_ap/ListFin_ap.tsx | 120 + .../src/components/Fin_ap/TableFin_ap.tsx | 497 ++ .../components/Fin_ap/configureFin_apCols.tsx | 156 + frontend/src/components/Fin_ar/CardFin_ar.tsx | 166 + frontend/src/components/Fin_ar/ListFin_ar.tsx | 124 + .../src/components/Fin_ar/TableFin_ar.tsx | 497 ++ .../components/Fin_ar/configureFin_arCols.tsx | 156 + .../Fin_budgets/CardFin_budgets.tsx | 160 + .../Fin_budgets/ListFin_budgets.tsx | 120 + .../Fin_budgets/TableFin_budgets.tsx | 500 ++ .../Fin_budgets/configureFin_budgetsCols.tsx | 156 + .../Fin_customers/CardFin_customers.tsx | 141 + .../Fin_customers/ListFin_customers.tsx | 109 + .../Fin_customers/TableFin_customers.tsx | 500 ++ .../configureFin_customersCols.tsx | 118 + .../Fin_forecasts/CardFin_forecasts.tsx | 163 + .../Fin_forecasts/ListFin_forecasts.tsx | 123 + .../Fin_forecasts/TableFin_forecasts.tsx | 500 ++ .../configureFin_forecastsCols.tsx | 156 + .../Fin_transactions/CardFin_transactions.tsx | 231 + .../Fin_transactions/ListFin_transactions.tsx | 163 + .../TableFin_transactions.tsx | 500 ++ .../configureFin_transactionsCols.tsx | 246 + .../Fin_vendors/CardFin_vendors.tsx | 136 + .../Fin_vendors/ListFin_vendors.tsx | 106 + .../Fin_vendors/TableFin_vendors.tsx | 500 ++ .../Fin_vendors/configureFin_vendorsCols.tsx | 118 + frontend/src/components/FooterBar.tsx | 37 + frontend/src/components/FormCheckRadio.tsx | 20 + .../src/components/FormCheckRadioGroup.tsx | 26 + frontend/src/components/FormField.tsx | 92 + frontend/src/components/FormFilePicker.tsx | 101 + frontend/src/components/FormImagePicker.tsx | 102 + .../Hr_applicants/CardHr_applicants.tsx | 170 + .../Hr_applicants/ListHr_applicants.tsx | 138 + .../Hr_applicants/TableHr_applicants.tsx | 500 ++ .../configureHr_applicantsCols.tsx | 170 + .../Hr_attendance/CardHr_attendance.tsx | 156 + .../Hr_attendance/ListHr_attendance.tsx | 120 + .../Hr_attendance/TableHr_attendance.tsx | 492 ++ .../configureHr_attendanceCols.tsx | 143 + .../Hr_employees/CardHr_employees.tsx | 207 + .../Hr_employees/ListHr_employees.tsx | 153 + .../Hr_employees/TableHr_employees.tsx | 500 ++ .../configureHr_employeesCols.tsx | 218 + .../components/Hr_payroll/CardHr_payroll.tsx | 173 + .../components/Hr_payroll/ListHr_payroll.tsx | 129 + .../components/Hr_payroll/TableHr_payroll.tsx | 500 ++ .../Hr_payroll/configureHr_payrollCols.tsx | 172 + .../Hr_positions/CardHr_positions.tsx | 156 + .../Hr_positions/ListHr_positions.tsx | 116 + .../Hr_positions/TableHr_positions.tsx | 500 ++ .../configureHr_positionsCols.tsx | 142 + .../src/components/Hr_reqs/CardHr_reqs.tsx | 177 + .../src/components/Hr_reqs/ListHr_reqs.tsx | 129 + .../src/components/Hr_reqs/TableHr_reqs.tsx | 497 ++ .../Hr_reqs/configureHr_reqsCols.tsx | 170 + frontend/src/components/IconRounded.tsx | 36 + frontend/src/components/ImageField.tsx | 51 + frontend/src/components/IntroGuide.tsx | 54 + .../components/KanbanBoard/KanbanBoard.tsx | 51 + .../src/components/KanbanBoard/KanbanCard.tsx | 64 + .../components/KanbanBoard/KanbanColumn.tsx | 215 + frontend/src/components/LanguageSwitcher.tsx | 104 + .../src/components/ListActionsPopover.tsx | 114 + frontend/src/components/LoadingSpinner.tsx | 14 + frontend/src/components/Logo/index.tsx | 15 + frontend/src/components/NavBar.tsx | 65 + frontend/src/components/NavBarItem.tsx | 149 + frontend/src/components/NavBarItemPlain.tsx | 35 + frontend/src/components/NavBarMenuList.tsx | 19 + frontend/src/components/NotificationBar.tsx | 65 + .../Ops_incidents/CardOps_incidents.tsx | 185 + .../Ops_incidents/ListOps_incidents.tsx | 137 + .../Ops_incidents/TableOps_incidents.tsx | 500 ++ .../configureOps_incidentsCols.tsx | 188 + .../Ops_inventory/CardOps_inventory.tsx | 174 + .../Ops_inventory/ListOps_inventory.tsx | 124 + .../Ops_inventory/TableOps_inventory.tsx | 500 ++ .../configureOps_inventoryCols.tsx | 162 + .../components/Ops_lines/CardOps_lines.tsx | 125 + .../components/Ops_lines/ListOps_lines.tsx | 103 + .../components/Ops_lines/TableOps_lines.tsx | 500 ++ .../Ops_lines/configureOps_linesCols.tsx | 114 + .../components/Ops_orders/CardOps_orders.tsx | 164 + .../components/Ops_orders/ListOps_orders.tsx | 122 + .../components/Ops_orders/TableOps_orders.tsx | 500 ++ .../Ops_orders/configureOps_ordersCols.tsx | 156 + .../components/Ops_plants/CardOps_plants.tsx | 138 + .../components/Ops_plants/ListOps_plants.tsx | 106 + .../components/Ops_plants/TableOps_plants.tsx | 500 ++ .../Ops_plants/configureOps_plantsCols.tsx | 118 + .../CardOps_production_log.tsx | 176 + .../ListOps_production_log.tsx | 132 + .../TableOps_production_log.tsx | 502 ++ .../configureOps_production_logCols.tsx | 173 + .../Ops_shipments/CardOps_shipments.tsx | 167 + .../Ops_shipments/ListOps_shipments.tsx | 123 + .../Ops_shipments/TableOps_shipments.tsx | 500 ++ .../configureOps_shipmentsCols.tsx | 150 + .../Organizations/CardOrganizations.tsx | 108 + .../Organizations/ListOrganizations.tsx | 92 + .../Organizations/TableOrganizations.tsx | 487 ++ .../configureOrganizationsCols.tsx | 74 + frontend/src/components/OverlayLayer.tsx | 41 + frontend/src/components/Pagination.tsx | 85 + .../src/components/PasswordSetOrReset.tsx | 111 + .../Permissions/CardPermissions.tsx | 105 + .../Permissions/ListPermissions.tsx | 89 + .../Permissions/TablePermissions.tsx | 487 ++ .../Permissions/configurePermissionsCols.tsx | 74 + frontend/src/components/RichTextField.tsx | 41 + frontend/src/components/Roles/CardRoles.tsx | 129 + frontend/src/components/Roles/ListRoles.tsx | 107 + frontend/src/components/Roles/TableRoles.tsx | 484 ++ .../components/Roles/configureRolesCols.tsx | 107 + frontend/src/components/Search.tsx | 56 + frontend/src/components/SearchResults.tsx | 83 + frontend/src/components/SectionFullScreen.tsx | 32 + frontend/src/components/SectionMain.tsx | 10 + frontend/src/components/SectionTitle.tsx | 38 + .../components/SectionTitleLineWithButton.tsx | 40 + frontend/src/components/SelectField.tsx | 60 + frontend/src/components/SelectFieldMany.tsx | 75 + .../components/SmartWidget/SmartWidget.tsx | 101 + .../SmartWidget/components/AreaChart.tsx | 18 + .../components/AreaChart/ApexAreaChart.tsx | 135 + .../components/AreaChart/ChartJSAreaChart.tsx | 96 + .../SmartWidget/components/BarChart.tsx | 18 + .../components/BarChart/ApexBarChart.tsx | 131 + .../components/BarChart/ChartJSBarChart.tsx | 92 + .../SmartWidget/components/FunnelChart.tsx | 134 + .../SmartWidget/components/LineChart.tsx | 18 + .../components/LineChart/ApexLineChart.tsx | 135 + .../components/LineChart/ChartJSLineChart.tsx | 97 + .../SmartWidget/components/PieChart.tsx | 18 + .../components/PieChart/ApexPieChart.tsx | 106 + .../components/PieChart/ChartJSPieChart.tsx | 81 + .../SmartWidget/models/widget.model.ts | 35 + .../components/SmartWidget/widgetHelpers.tsx | 38 + frontend/src/components/SwitchField.tsx | 29 + .../src/components/TableSampleClients.tsx | 149 + .../src/components/Uploaders/FilesUploader.js | 133 + .../components/Uploaders/ImagesUploader.js | 227 + .../src/components/Uploaders/UploadService.js | 82 + frontend/src/components/UserAvatar.tsx | 48 + .../src/components/UserAvatarCurrentUser.tsx | 48 + frontend/src/components/UserCard.tsx | 47 + frontend/src/components/Users/CardUsers.tsx | 209 + frontend/src/components/Users/ListUsers.tsx | 158 + frontend/src/components/Users/TableUsers.tsx | 485 ++ .../components/Users/configureUsersCols.tsx | 203 + .../AboutUsComponent/designs/ImageLeft.tsx | 54 + .../AboutUsComponent/designs/ImageRight.tsx | 53 + .../AboutUsComponent/index.tsx | 85 + .../designs/FormWithImage.tsx | 95 + .../designs/HighlightedForm.tsx | 126 + .../designs/SimpleAndCleanForm.tsx | 166 + .../ContactFormComponent/index.tsx | 89 + .../FaqComponent/designs/FAQAccordion.tsx | 78 + .../FaqComponent/designs/FAQSplitList.tsx | 67 + .../FaqComponent/designs/FAQTwoColumn.tsx | 30 + .../WebPageComponents/FaqComponent/index.tsx | 75 + .../designs/CardsGridWithIcons.tsx | 80 + .../FeaturesComponent/designs/IconsTop.tsx | 49 + .../designs/IconsWithImage.tsx | 81 + .../designs/LargeNumbers.tsx | 54 + .../FeaturesComponent/index.tsx | 136 + .../components/WebPageComponents/Footer.tsx | 75 + .../designs/HorizontalGalleryWithButtons.tsx | 106 + .../designs/OverlappingCentralImage.tsx | 128 + .../GalleryPortfolioComponent/index.tsx | 75 + .../components/WebPageComponents/Header.tsx | 81 + .../HeroComponent/designs/HeroImageBg.tsx | 38 + .../HeroComponent/designs/HeroImageLeft.tsx | 48 + .../HeroComponent/designs/HeroImageRight.tsx | 48 + .../HeroComponent/designs/HeroTextCenter.tsx | 22 + .../WebPageComponents/HeroComponent/index.tsx | 73 + .../PricingComponent/index.tsx | 242 + .../designs/HorizontalCarousel.tsx | 93 + .../designs/MultiCardDisplay.tsx | 76 + .../TestimonialsComponent/index.tsx | 112 + .../components/WebPageComponents/designs.ts | 77 + .../components/WidgetCreator/RoleSelect.tsx | 65 + .../WidgetCreator/WidgetCreator.tsx | 156 + frontend/src/config.ts | 22 + frontend/src/css/_app.css | 34 + frontend/src/css/_calendar.css | 62 + frontend/src/css/_checkbox-radio-switch.css | 73 + frontend/src/css/_helper.css | 24 + frontend/src/css/_progress.css | 21 + frontend/src/css/_rich-text.css | 63 + frontend/src/css/_scrollbars.css | 41 + frontend/src/css/_select-dropdown.css | 32 + frontend/src/css/_table.css | 123 + frontend/src/css/_theme.css | 105 + frontend/src/css/main.css | 62 + frontend/src/css/tailwind/_base.css | 1 + frontend/src/css/tailwind/_components.css | 1 + frontend/src/css/tailwind/_utilities.css | 1 + frontend/src/helpers/dataFormatter.js | 269 + frontend/src/helpers/fileSaver.ts | 6 + frontend/src/helpers/humanize.ts | 12 + frontend/src/helpers/notifyStateHandler.ts | 32 + frontend/src/helpers/pexels.ts | 75 + frontend/src/helpers/userPermissions.ts | 16 + frontend/src/hooks/sampleData.ts | 22 + frontend/src/hooks/useDevCompilationStatus.ts | 44 + frontend/src/i18n.ts | 21 + frontend/src/interfaces/index.ts | 122 + frontend/src/layouts/Authenticated.tsx | 132 + frontend/src/layouts/Guest.tsx | 19 + frontend/src/menuAside.ts | 317 + frontend/src/menuNavBar.ts | 64 + frontend/src/pages/_app.tsx | 194 + frontend/src/pages/api/hello.js | 5 + frontend/src/pages/api/logError.ts | 83 + .../src/pages/audit_logs/[audit_logsId].tsx | 187 + .../src/pages/audit_logs/audit_logs-edit.tsx | 185 + .../src/pages/audit_logs/audit_logs-list.tsx | 173 + .../src/pages/audit_logs/audit_logs-new.tsx | 152 + .../src/pages/audit_logs/audit_logs-table.tsx | 172 + .../src/pages/audit_logs/audit_logs-view.tsx | 120 + frontend/src/pages/dashboard.tsx | 1211 ++++ .../data_connections/[data_connectionsId].tsx | 214 + .../data_connections-edit.tsx | 212 + .../data_connections-list.tsx | 189 + .../data_connections/data_connections-new.tsx | 164 + .../data_connections-table.tsx | 186 + .../data_connections-view.tsx | 139 + frontend/src/pages/error.tsx | 35 + .../pages/fin_accounts/[fin_accountsId].tsx | 201 + .../pages/fin_accounts/fin_accounts-edit.tsx | 199 + .../pages/fin_accounts/fin_accounts-list.tsx | 183 + .../pages/fin_accounts/fin_accounts-new.tsx | 166 + .../pages/fin_accounts/fin_accounts-table.tsx | 180 + .../pages/fin_accounts/fin_accounts-view.tsx | 288 + frontend/src/pages/fin_ap/[fin_apId].tsx | 206 + frontend/src/pages/fin_ap/fin_ap-edit.tsx | 204 + frontend/src/pages/fin_ap/fin_ap-list.tsx | 178 + frontend/src/pages/fin_ap/fin_ap-new.tsx | 162 + frontend/src/pages/fin_ap/fin_ap-table.tsx | 177 + frontend/src/pages/fin_ap/fin_ap-view.tsx | 132 + frontend/src/pages/fin_ar/[fin_arId].tsx | 206 + frontend/src/pages/fin_ar/fin_ar-edit.tsx | 204 + frontend/src/pages/fin_ar/fin_ar-list.tsx | 178 + frontend/src/pages/fin_ar/fin_ar-new.tsx | 162 + frontend/src/pages/fin_ar/fin_ar-table.tsx | 177 + frontend/src/pages/fin_ar/fin_ar-view.tsx | 132 + .../src/pages/fin_budgets/[fin_budgetsId].tsx | 191 + .../pages/fin_budgets/fin_budgets-edit.tsx | 189 + .../pages/fin_budgets/fin_budgets-list.tsx | 176 + .../src/pages/fin_budgets/fin_budgets-new.tsx | 156 + .../pages/fin_budgets/fin_budgets-table.tsx | 175 + .../pages/fin_budgets/fin_budgets-view.tsx | 120 + .../pages/fin_customers/[fin_customersId].tsx | 170 + .../fin_customers/fin_customers-edit.tsx | 168 + .../fin_customers/fin_customers-list.tsx | 175 + .../pages/fin_customers/fin_customers-new.tsx | 136 + .../fin_customers/fin_customers-table.tsx | 172 + .../fin_customers/fin_customers-view.tsx | 280 + .../pages/fin_forecasts/[fin_forecastsId].tsx | 193 + .../fin_forecasts/fin_forecasts-edit.tsx | 191 + .../fin_forecasts/fin_forecasts-list.tsx | 178 + .../pages/fin_forecasts/fin_forecasts-new.tsx | 158 + .../fin_forecasts/fin_forecasts-table.tsx | 175 + .../fin_forecasts/fin_forecasts-view.tsx | 120 + .../fin_transactions/[fin_transactionsId].tsx | 257 + .../fin_transactions-edit.tsx | 255 + .../fin_transactions-list.tsx | 187 + .../fin_transactions/fin_transactions-new.tsx | 206 + .../fin_transactions-table.tsx | 184 + .../fin_transactions-view.tsx | 172 + .../src/pages/fin_vendors/[fin_vendorsId].tsx | 168 + .../pages/fin_vendors/fin_vendors-edit.tsx | 166 + .../pages/fin_vendors/fin_vendors-list.tsx | 173 + .../src/pages/fin_vendors/fin_vendors-new.tsx | 134 + .../pages/fin_vendors/fin_vendors-table.tsx | 172 + .../pages/fin_vendors/fin_vendors-view.tsx | 225 + frontend/src/pages/forgot.tsx | 81 + frontend/src/pages/forms.tsx | 162 + .../pages/hr_applicants/[hr_applicantsId].tsx | 239 + .../hr_applicants/hr_applicants-edit.tsx | 237 + .../hr_applicants/hr_applicants-list.tsx | 186 + .../pages/hr_applicants/hr_applicants-new.tsx | 176 + .../hr_applicants/hr_applicants-table.tsx | 183 + .../hr_applicants/hr_applicants-view.tsx | 157 + .../pages/hr_attendance/[hr_attendanceId].tsx | 206 + .../hr_attendance/hr_attendance-edit.tsx | 204 + .../hr_attendance/hr_attendance-list.tsx | 183 + .../pages/hr_attendance/hr_attendance-new.tsx | 158 + .../hr_attendance/hr_attendance-table.tsx | 180 + .../hr_attendance/hr_attendance-view.tsx | 129 + .../pages/hr_employees/[hr_employeesId].tsx | 254 + .../pages/hr_employees/hr_employees-edit.tsx | 252 + .../pages/hr_employees/hr_employees-list.tsx | 190 + .../pages/hr_employees/hr_employees-new.tsx | 194 + .../pages/hr_employees/hr_employees-table.tsx | 187 + .../pages/hr_employees/hr_employees-view.tsx | 281 + .../src/pages/hr_payroll/[hr_payrollId].tsx | 212 + .../src/pages/hr_payroll/hr_payroll-edit.tsx | 210 + .../src/pages/hr_payroll/hr_payroll-list.tsx | 175 + .../src/pages/hr_payroll/hr_payroll-new.tsx | 166 + .../src/pages/hr_payroll/hr_payroll-table.tsx | 174 + .../src/pages/hr_payroll/hr_payroll-view.tsx | 139 + .../pages/hr_positions/[hr_positionsId].tsx | 184 + .../pages/hr_positions/hr_positions-edit.tsx | 182 + .../pages/hr_positions/hr_positions-list.tsx | 183 + .../pages/hr_positions/hr_positions-new.tsx | 150 + .../pages/hr_positions/hr_positions-table.tsx | 180 + .../pages/hr_positions/hr_positions-view.tsx | 174 + frontend/src/pages/hr_reqs/[hr_reqsId].tsx | 233 + frontend/src/pages/hr_reqs/hr_reqs-edit.tsx | 231 + frontend/src/pages/hr_reqs/hr_reqs-list.tsx | 186 + frontend/src/pages/hr_reqs/hr_reqs-new.tsx | 178 + frontend/src/pages/hr_reqs/hr_reqs-table.tsx | 185 + frontend/src/pages/hr_reqs/hr_reqs-view.tsx | 214 + frontend/src/pages/index.tsx | 206 + frontend/src/pages/login.tsx | 382 ++ .../pages/ops_incidents/[ops_incidentsId].tsx | 233 + .../ops_incidents/ops_incidents-edit.tsx | 231 + .../ops_incidents/ops_incidents-list.tsx | 188 + .../pages/ops_incidents/ops_incidents-new.tsx | 186 + .../ops_incidents/ops_incidents-table.tsx | 185 + .../ops_incidents/ops_incidents-view.tsx | 147 + .../pages/ops_inventory/[ops_inventoryId].tsx | 192 + .../ops_inventory/ops_inventory-edit.tsx | 190 + .../ops_inventory/ops_inventory-list.tsx | 178 + .../pages/ops_inventory/ops_inventory-new.tsx | 158 + .../ops_inventory/ops_inventory-table.tsx | 175 + .../ops_inventory/ops_inventory-view.tsx | 124 + .../src/pages/ops_lines/[ops_linesId].tsx | 169 + .../src/pages/ops_lines/ops_lines-edit.tsx | 167 + .../src/pages/ops_lines/ops_lines-list.tsx | 170 + .../src/pages/ops_lines/ops_lines-new.tsx | 134 + .../src/pages/ops_lines/ops_lines-table.tsx | 169 + .../src/pages/ops_lines/ops_lines-view.tsx | 219 + .../src/pages/ops_orders/[ops_ordersId].tsx | 208 + .../src/pages/ops_orders/ops_orders-edit.tsx | 206 + .../src/pages/ops_orders/ops_orders-list.tsx | 180 + .../src/pages/ops_orders/ops_orders-new.tsx | 162 + .../src/pages/ops_orders/ops_orders-table.tsx | 179 + .../src/pages/ops_orders/ops_orders-view.tsx | 134 + .../src/pages/ops_plants/[ops_plantsId].tsx | 168 + .../src/pages/ops_plants/ops_plants-edit.tsx | 166 + .../src/pages/ops_plants/ops_plants-list.tsx | 170 + .../src/pages/ops_plants/ops_plants-new.tsx | 134 + .../src/pages/ops_plants/ops_plants-table.tsx | 169 + .../src/pages/ops_plants/ops_plants-view.tsx | 201 + .../[ops_production_logId].tsx | 225 + .../ops_production_log-edit.tsx | 223 + .../ops_production_log-list.tsx | 182 + .../ops_production_log-new.tsx | 176 + .../ops_production_log-table.tsx | 179 + .../ops_production_log-view.tsx | 143 + .../pages/ops_shipments/[ops_shipmentsId].tsx | 218 + .../ops_shipments/ops_shipments-edit.tsx | 216 + .../ops_shipments/ops_shipments-list.tsx | 184 + .../pages/ops_shipments/ops_shipments-new.tsx | 162 + .../ops_shipments/ops_shipments-table.tsx | 181 + .../ops_shipments/ops_shipments-view.tsx | 151 + .../pages/organizations/[organizationsId].tsx | 132 + .../organizations/organizations-edit.tsx | 130 + .../organizations/organizations-list.tsx | 165 + .../pages/organizations/organizations-new.tsx | 100 + .../organizations/organizations-table.tsx | 164 + .../organizations/organizations-view.tsx | 2674 ++++++++ frontend/src/pages/password-reset.tsx | 12 + .../src/pages/permissions/[permissionsId].tsx | 130 + .../pages/permissions/permissions-edit.tsx | 128 + .../pages/permissions/permissions-list.tsx | 165 + .../src/pages/permissions/permissions-new.tsx | 98 + .../pages/permissions/permissions-table.tsx | 164 + .../pages/permissions/permissions-view.tsx | 87 + frontend/src/pages/privacy-policy.tsx | 292 + frontend/src/pages/profile.tsx | 178 + frontend/src/pages/register.tsx | 127 + frontend/src/pages/roles/[rolesId].tsx | 151 + frontend/src/pages/roles/roles-edit.tsx | 149 + frontend/src/pages/roles/roles-list.tsx | 164 + frontend/src/pages/roles/roles-new.tsx | 120 + frontend/src/pages/roles/roles-table.tsx | 163 + frontend/src/pages/roles/roles-view.tsx | 183 + frontend/src/pages/search.tsx | 101 + frontend/src/pages/tables.tsx | 37 + frontend/src/pages/terms-of-use.tsx | 205 + frontend/src/pages/users/[usersId].tsx | 222 + frontend/src/pages/users/users-edit.tsx | 220 + frontend/src/pages/users/users-list.tsx | 173 + frontend/src/pages/users/users-new.tsx | 183 + frontend/src/pages/users/users-table.tsx | 168 + frontend/src/pages/users/users-view.tsx | 219 + frontend/src/pages/verify-email.tsx | 65 + frontend/src/pages/web_pages/contact.tsx | 112 + frontend/src/pages/web_pages/faq.tsx | 101 + frontend/src/pages/web_pages/services.tsx | 188 + .../src/stores/audit_logs/audit_logsSlice.ts | 236 + frontend/src/stores/authSlice.ts | 125 + .../data_connections/data_connectionsSlice.ts | 250 + .../stores/fin_accounts/fin_accountsSlice.ts | 241 + frontend/src/stores/fin_ap/fin_apSlice.ts | 236 + frontend/src/stores/fin_ar/fin_arSlice.ts | 236 + .../stores/fin_budgets/fin_budgetsSlice.ts | 241 + .../fin_customers/fin_customersSlice.ts | 250 + .../fin_forecasts/fin_forecastsSlice.ts | 250 + .../fin_transactions/fin_transactionsSlice.ts | 250 + .../stores/fin_vendors/fin_vendorsSlice.ts | 241 + frontend/src/stores/hooks.ts | 6 + .../hr_applicants/hr_applicantsSlice.ts | 250 + .../hr_attendance/hr_attendanceSlice.ts | 250 + .../stores/hr_employees/hr_employeesSlice.ts | 241 + .../src/stores/hr_payroll/hr_payrollSlice.ts | 236 + .../stores/hr_positions/hr_positionsSlice.ts | 241 + frontend/src/stores/hr_reqs/hr_reqsSlice.ts | 236 + frontend/src/stores/introSteps.ts | 152 + frontend/src/stores/mainSlice.ts | 36 + frontend/src/stores/openAiSlice.ts | 116 + .../ops_incidents/ops_incidentsSlice.ts | 250 + .../ops_inventory/ops_inventorySlice.ts | 250 + .../src/stores/ops_lines/ops_linesSlice.ts | 236 + .../src/stores/ops_orders/ops_ordersSlice.ts | 236 + .../src/stores/ops_plants/ops_plantsSlice.ts | 236 + .../ops_production_logSlice.ts | 250 + .../ops_shipments/ops_shipmentsSlice.ts | 250 + .../organizations/organizationsSlice.ts | 250 + .../stores/permissions/permissionsSlice.ts | 241 + frontend/src/stores/roles/rolesSlice.ts | 283 + frontend/src/stores/store.ts | 75 + frontend/src/stores/styleSlice.ts | 108 + frontend/src/stores/users/usersSlice.ts | 236 + frontend/src/stores/usersSlice.ts | 119 + frontend/src/styles.ts | 136 + frontend/tailwind.config.js | 127 + frontend/tsconfig.json | 20 + frontend/yarn.lock | 3608 ++++++++++ nginx.conf | 96 + package.json | 9 + pids/backend.pid | 1 + pids/frontend.pid | 1 + 682 files changed, 142290 insertions(+) create mode 100644 .gitignore create mode 100644 502.html create mode 100644 Dockerfile create mode 100644 Dockerfile.dev create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app-shell/.eslintrc.cjs create mode 100644 app-shell/.prettierrc create mode 100644 app-shell/.sequelizerc create mode 100644 app-shell/Dockerfile create mode 100644 app-shell/README.md create mode 100644 app-shell/package.json create mode 100644 app-shell/src/_schema.json create mode 100644 app-shell/src/config.js create mode 100644 app-shell/src/helpers.js create mode 100644 app-shell/src/index.js create mode 100644 app-shell/src/middlewares/check-permissions.js create mode 100644 app-shell/src/middlewares/modify-path.js create mode 100644 app-shell/src/routes/executor.js create mode 100644 app-shell/src/routes/vcs.js create mode 100644 app-shell/src/services/database.js create mode 100644 app-shell/src/services/executor.js create mode 100644 app-shell/src/services/notifications/errors/forbidden.js create mode 100644 app-shell/src/services/notifications/errors/validation.js create mode 100644 app-shell/src/services/notifications/helpers.js create mode 100644 app-shell/src/services/notifications/list.js create mode 100644 app-shell/src/services/project-events.js create mode 100644 app-shell/src/services/screenshot_service.js create mode 100644 app-shell/src/services/vcs.js create mode 100644 app-shell/yarn.lock create mode 100644 backend/.prettierrc create mode 100644 backend/.sequelizerc create mode 100644 backend/Dockerfile create mode 100644 backend/README.md create mode 100644 backend/package.json create mode 100644 backend/src/auth/auth.js create mode 100644 backend/src/config.js create mode 100644 backend/src/db/api/audit_logs.js create mode 100644 backend/src/db/api/data_connections.js create mode 100644 backend/src/db/api/file.js create mode 100644 backend/src/db/api/fin_accounts.js create mode 100644 backend/src/db/api/fin_ap.js create mode 100644 backend/src/db/api/fin_ar.js create mode 100644 backend/src/db/api/fin_budgets.js create mode 100644 backend/src/db/api/fin_customers.js create mode 100644 backend/src/db/api/fin_forecasts.js create mode 100644 backend/src/db/api/fin_transactions.js create mode 100644 backend/src/db/api/fin_vendors.js create mode 100644 backend/src/db/api/hr_applicants.js create mode 100644 backend/src/db/api/hr_attendance.js create mode 100644 backend/src/db/api/hr_employees.js create mode 100644 backend/src/db/api/hr_payroll.js create mode 100644 backend/src/db/api/hr_positions.js create mode 100644 backend/src/db/api/hr_reqs.js create mode 100644 backend/src/db/api/ops_incidents.js create mode 100644 backend/src/db/api/ops_inventory.js create mode 100644 backend/src/db/api/ops_lines.js create mode 100644 backend/src/db/api/ops_orders.js create mode 100644 backend/src/db/api/ops_plants.js create mode 100644 backend/src/db/api/ops_production_log.js create mode 100644 backend/src/db/api/ops_shipments.js create mode 100644 backend/src/db/api/organizations.js create mode 100644 backend/src/db/api/permissions.js create mode 100644 backend/src/db/api/roles.js create mode 100644 backend/src/db/api/users.js create mode 100644 backend/src/db/db.config.js create mode 100644 backend/src/db/migrations/1758865238752.js create mode 100644 backend/src/db/models/audit_logs.js create mode 100644 backend/src/db/models/data_connections.js create mode 100644 backend/src/db/models/file.js create mode 100644 backend/src/db/models/fin_accounts.js create mode 100644 backend/src/db/models/fin_ap.js create mode 100644 backend/src/db/models/fin_ar.js create mode 100644 backend/src/db/models/fin_budgets.js create mode 100644 backend/src/db/models/fin_customers.js create mode 100644 backend/src/db/models/fin_forecasts.js create mode 100644 backend/src/db/models/fin_transactions.js create mode 100644 backend/src/db/models/fin_vendors.js create mode 100644 backend/src/db/models/hr_applicants.js create mode 100644 backend/src/db/models/hr_attendance.js create mode 100644 backend/src/db/models/hr_employees.js create mode 100644 backend/src/db/models/hr_payroll.js create mode 100644 backend/src/db/models/hr_positions.js create mode 100644 backend/src/db/models/hr_reqs.js create mode 100644 backend/src/db/models/index.js create mode 100644 backend/src/db/models/ops_incidents.js create mode 100644 backend/src/db/models/ops_inventory.js create mode 100644 backend/src/db/models/ops_lines.js create mode 100644 backend/src/db/models/ops_orders.js create mode 100644 backend/src/db/models/ops_plants.js create mode 100644 backend/src/db/models/ops_production_log.js create mode 100644 backend/src/db/models/ops_shipments.js create mode 100644 backend/src/db/models/organizations.js create mode 100644 backend/src/db/models/permissions.js create mode 100644 backend/src/db/models/roles.js create mode 100644 backend/src/db/models/users.js create mode 100644 backend/src/db/reset.js create mode 100644 backend/src/db/seeders/20200430130759-admin-user.js create mode 100644 backend/src/db/seeders/20200430130760-user-roles.js create mode 100644 backend/src/db/seeders/20231127130745-sample-data.js create mode 100644 backend/src/db/utils.js create mode 100644 backend/src/helpers.js create mode 100644 backend/src/index.js create mode 100644 backend/src/middlewares/check-permissions.js create mode 100644 backend/src/middlewares/upload.js create mode 100644 backend/src/routes/audit_logs.js create mode 100644 backend/src/routes/auth.js create mode 100644 backend/src/routes/contactForm.js create mode 100644 backend/src/routes/data_connections.js create mode 100644 backend/src/routes/file.js create mode 100644 backend/src/routes/fin_accounts.js create mode 100644 backend/src/routes/fin_ap.js create mode 100644 backend/src/routes/fin_ar.js create mode 100644 backend/src/routes/fin_budgets.js create mode 100644 backend/src/routes/fin_customers.js create mode 100644 backend/src/routes/fin_forecasts.js create mode 100644 backend/src/routes/fin_transactions.js create mode 100644 backend/src/routes/fin_vendors.js create mode 100644 backend/src/routes/hr_applicants.js create mode 100644 backend/src/routes/hr_attendance.js create mode 100644 backend/src/routes/hr_employees.js create mode 100644 backend/src/routes/hr_payroll.js create mode 100644 backend/src/routes/hr_positions.js create mode 100644 backend/src/routes/hr_reqs.js create mode 100644 backend/src/routes/openai.js create mode 100644 backend/src/routes/ops_incidents.js create mode 100644 backend/src/routes/ops_inventory.js create mode 100644 backend/src/routes/ops_lines.js create mode 100644 backend/src/routes/ops_orders.js create mode 100644 backend/src/routes/ops_plants.js create mode 100644 backend/src/routes/ops_production_log.js create mode 100644 backend/src/routes/ops_shipments.js create mode 100644 backend/src/routes/organizationLogin.js create mode 100644 backend/src/routes/organizations.js create mode 100644 backend/src/routes/permissions.js create mode 100644 backend/src/routes/pexels.js create mode 100644 backend/src/routes/roles.js create mode 100644 backend/src/routes/search.js create mode 100644 backend/src/routes/users.js create mode 100644 backend/src/services/audit_logs.js create mode 100644 backend/src/services/auth.js create mode 100644 backend/src/services/data_connections.js create mode 100644 backend/src/services/email/htmlTemplates/addressVerification/emailAddressVerification.html create mode 100644 backend/src/services/email/htmlTemplates/invitation/invitationTemplate.html create mode 100644 backend/src/services/email/htmlTemplates/passwordReset/passwordResetEmail.html create mode 100644 backend/src/services/email/index.js create mode 100644 backend/src/services/email/list/addressVerification.js create mode 100644 backend/src/services/email/list/invitation.js create mode 100644 backend/src/services/email/list/passwordReset.js create mode 100644 backend/src/services/file.js create mode 100644 backend/src/services/fin_accounts.js create mode 100644 backend/src/services/fin_ap.js create mode 100644 backend/src/services/fin_ar.js create mode 100644 backend/src/services/fin_budgets.js create mode 100644 backend/src/services/fin_customers.js create mode 100644 backend/src/services/fin_forecasts.js create mode 100644 backend/src/services/fin_transactions.js create mode 100644 backend/src/services/fin_vendors.js create mode 100644 backend/src/services/hr_applicants.js create mode 100644 backend/src/services/hr_attendance.js create mode 100644 backend/src/services/hr_employees.js create mode 100644 backend/src/services/hr_payroll.js create mode 100644 backend/src/services/hr_positions.js create mode 100644 backend/src/services/hr_reqs.js create mode 100644 backend/src/services/notifications/errors/forbidden.js create mode 100644 backend/src/services/notifications/errors/validation.js create mode 100644 backend/src/services/notifications/helpers.js create mode 100644 backend/src/services/notifications/list.js create mode 100644 backend/src/services/openai.js create mode 100644 backend/src/services/ops_incidents.js create mode 100644 backend/src/services/ops_inventory.js create mode 100644 backend/src/services/ops_lines.js create mode 100644 backend/src/services/ops_orders.js create mode 100644 backend/src/services/ops_plants.js create mode 100644 backend/src/services/ops_production_log.js create mode 100644 backend/src/services/ops_shipments.js create mode 100644 backend/src/services/organizations.js create mode 100644 backend/src/services/permissions.js create mode 100644 backend/src/services/roles.js create mode 100644 backend/src/services/search.js create mode 100644 backend/src/services/users.js create mode 100644 backend/watcher.js create mode 100644 backend/yarn.lock create mode 100644 cloudbuild.yaml create mode 100644 docker/.gitignore create mode 100644 docker/README.md create mode 100644 docker/docker-compose.yml create mode 100644 docker/start-backend.sh create mode 100644 docker/wait-for-it.sh create mode 100644 frontend/.eslintrc.cjs create mode 100644 frontend/.gitignore create mode 100644 frontend/.prettierrc create mode 100644 frontend/Dockerfile create mode 100644 frontend/LICENSE-justboil create mode 100644 frontend/README.md create mode 100644 frontend/next-env.d.ts create mode 100644 frontend/next.config.mjs create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/prettier.config.js create mode 100644 frontend/public/data-sources/clients.json create mode 100644 frontend/public/data-sources/history.json create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/public/locales/de/common.json create mode 100644 frontend/public/locales/en/common.json create mode 100644 frontend/public/locales/es/common.json create mode 100644 frontend/public/locales/fr/common.json create mode 100644 frontend/src/colors.ts create mode 100644 frontend/src/components/AsideMenu.tsx create mode 100644 frontend/src/components/AsideMenuItem.tsx create mode 100644 frontend/src/components/AsideMenuLayer.tsx create mode 100644 frontend/src/components/AsideMenuList.tsx create mode 100644 frontend/src/components/Audit_logs/CardAudit_logs.tsx create mode 100644 frontend/src/components/Audit_logs/ListAudit_logs.tsx create mode 100644 frontend/src/components/Audit_logs/TableAudit_logs.tsx create mode 100644 frontend/src/components/Audit_logs/configureAudit_logsCols.tsx create mode 100644 frontend/src/components/BaseButton.tsx create mode 100644 frontend/src/components/BaseButtons.tsx create mode 100644 frontend/src/components/BaseDivider.tsx create mode 100644 frontend/src/components/BaseIcon.tsx create mode 100644 frontend/src/components/BigCalendar.tsx create mode 100644 frontend/src/components/CardBox.tsx create mode 100644 frontend/src/components/CardBoxComponentBody.tsx create mode 100644 frontend/src/components/CardBoxComponentEmpty.tsx create mode 100644 frontend/src/components/CardBoxComponentFooter.tsx create mode 100644 frontend/src/components/CardBoxComponentTitle.tsx create mode 100644 frontend/src/components/CardBoxModal.tsx create mode 100644 frontend/src/components/ChartLineSample/config.ts create mode 100644 frontend/src/components/ChartLineSample/index.tsx create mode 100644 frontend/src/components/ClickOutside.tsx create mode 100644 frontend/src/components/DataGridMultiSelect.tsx create mode 100644 frontend/src/components/Data_connections/CardData_connections.tsx create mode 100644 frontend/src/components/Data_connections/ListData_connections.tsx create mode 100644 frontend/src/components/Data_connections/TableData_connections.tsx create mode 100644 frontend/src/components/Data_connections/configureData_connectionsCols.tsx create mode 100644 frontend/src/components/DevModeBadge.tsx create mode 100644 frontend/src/components/DragDropFilePicker.tsx create mode 100644 frontend/src/components/ErrorBoundary.tsx create mode 100644 frontend/src/components/Fin_accounts/CardFin_accounts.tsx create mode 100644 frontend/src/components/Fin_accounts/ListFin_accounts.tsx create mode 100644 frontend/src/components/Fin_accounts/TableFin_accounts.tsx create mode 100644 frontend/src/components/Fin_accounts/configureFin_accountsCols.tsx create mode 100644 frontend/src/components/Fin_ap/CardFin_ap.tsx create mode 100644 frontend/src/components/Fin_ap/ListFin_ap.tsx create mode 100644 frontend/src/components/Fin_ap/TableFin_ap.tsx create mode 100644 frontend/src/components/Fin_ap/configureFin_apCols.tsx create mode 100644 frontend/src/components/Fin_ar/CardFin_ar.tsx create mode 100644 frontend/src/components/Fin_ar/ListFin_ar.tsx create mode 100644 frontend/src/components/Fin_ar/TableFin_ar.tsx create mode 100644 frontend/src/components/Fin_ar/configureFin_arCols.tsx create mode 100644 frontend/src/components/Fin_budgets/CardFin_budgets.tsx create mode 100644 frontend/src/components/Fin_budgets/ListFin_budgets.tsx create mode 100644 frontend/src/components/Fin_budgets/TableFin_budgets.tsx create mode 100644 frontend/src/components/Fin_budgets/configureFin_budgetsCols.tsx create mode 100644 frontend/src/components/Fin_customers/CardFin_customers.tsx create mode 100644 frontend/src/components/Fin_customers/ListFin_customers.tsx create mode 100644 frontend/src/components/Fin_customers/TableFin_customers.tsx create mode 100644 frontend/src/components/Fin_customers/configureFin_customersCols.tsx create mode 100644 frontend/src/components/Fin_forecasts/CardFin_forecasts.tsx create mode 100644 frontend/src/components/Fin_forecasts/ListFin_forecasts.tsx create mode 100644 frontend/src/components/Fin_forecasts/TableFin_forecasts.tsx create mode 100644 frontend/src/components/Fin_forecasts/configureFin_forecastsCols.tsx create mode 100644 frontend/src/components/Fin_transactions/CardFin_transactions.tsx create mode 100644 frontend/src/components/Fin_transactions/ListFin_transactions.tsx create mode 100644 frontend/src/components/Fin_transactions/TableFin_transactions.tsx create mode 100644 frontend/src/components/Fin_transactions/configureFin_transactionsCols.tsx create mode 100644 frontend/src/components/Fin_vendors/CardFin_vendors.tsx create mode 100644 frontend/src/components/Fin_vendors/ListFin_vendors.tsx create mode 100644 frontend/src/components/Fin_vendors/TableFin_vendors.tsx create mode 100644 frontend/src/components/Fin_vendors/configureFin_vendorsCols.tsx create mode 100644 frontend/src/components/FooterBar.tsx create mode 100644 frontend/src/components/FormCheckRadio.tsx create mode 100644 frontend/src/components/FormCheckRadioGroup.tsx create mode 100644 frontend/src/components/FormField.tsx create mode 100644 frontend/src/components/FormFilePicker.tsx create mode 100644 frontend/src/components/FormImagePicker.tsx create mode 100644 frontend/src/components/Hr_applicants/CardHr_applicants.tsx create mode 100644 frontend/src/components/Hr_applicants/ListHr_applicants.tsx create mode 100644 frontend/src/components/Hr_applicants/TableHr_applicants.tsx create mode 100644 frontend/src/components/Hr_applicants/configureHr_applicantsCols.tsx create mode 100644 frontend/src/components/Hr_attendance/CardHr_attendance.tsx create mode 100644 frontend/src/components/Hr_attendance/ListHr_attendance.tsx create mode 100644 frontend/src/components/Hr_attendance/TableHr_attendance.tsx create mode 100644 frontend/src/components/Hr_attendance/configureHr_attendanceCols.tsx create mode 100644 frontend/src/components/Hr_employees/CardHr_employees.tsx create mode 100644 frontend/src/components/Hr_employees/ListHr_employees.tsx create mode 100644 frontend/src/components/Hr_employees/TableHr_employees.tsx create mode 100644 frontend/src/components/Hr_employees/configureHr_employeesCols.tsx create mode 100644 frontend/src/components/Hr_payroll/CardHr_payroll.tsx create mode 100644 frontend/src/components/Hr_payroll/ListHr_payroll.tsx create mode 100644 frontend/src/components/Hr_payroll/TableHr_payroll.tsx create mode 100644 frontend/src/components/Hr_payroll/configureHr_payrollCols.tsx create mode 100644 frontend/src/components/Hr_positions/CardHr_positions.tsx create mode 100644 frontend/src/components/Hr_positions/ListHr_positions.tsx create mode 100644 frontend/src/components/Hr_positions/TableHr_positions.tsx create mode 100644 frontend/src/components/Hr_positions/configureHr_positionsCols.tsx create mode 100644 frontend/src/components/Hr_reqs/CardHr_reqs.tsx create mode 100644 frontend/src/components/Hr_reqs/ListHr_reqs.tsx create mode 100644 frontend/src/components/Hr_reqs/TableHr_reqs.tsx create mode 100644 frontend/src/components/Hr_reqs/configureHr_reqsCols.tsx create mode 100644 frontend/src/components/IconRounded.tsx create mode 100644 frontend/src/components/ImageField.tsx create mode 100644 frontend/src/components/IntroGuide.tsx create mode 100644 frontend/src/components/KanbanBoard/KanbanBoard.tsx create mode 100644 frontend/src/components/KanbanBoard/KanbanCard.tsx create mode 100644 frontend/src/components/KanbanBoard/KanbanColumn.tsx create mode 100644 frontend/src/components/LanguageSwitcher.tsx create mode 100644 frontend/src/components/ListActionsPopover.tsx create mode 100644 frontend/src/components/LoadingSpinner.tsx create mode 100644 frontend/src/components/Logo/index.tsx create mode 100644 frontend/src/components/NavBar.tsx create mode 100644 frontend/src/components/NavBarItem.tsx create mode 100644 frontend/src/components/NavBarItemPlain.tsx create mode 100644 frontend/src/components/NavBarMenuList.tsx create mode 100644 frontend/src/components/NotificationBar.tsx create mode 100644 frontend/src/components/Ops_incidents/CardOps_incidents.tsx create mode 100644 frontend/src/components/Ops_incidents/ListOps_incidents.tsx create mode 100644 frontend/src/components/Ops_incidents/TableOps_incidents.tsx create mode 100644 frontend/src/components/Ops_incidents/configureOps_incidentsCols.tsx create mode 100644 frontend/src/components/Ops_inventory/CardOps_inventory.tsx create mode 100644 frontend/src/components/Ops_inventory/ListOps_inventory.tsx create mode 100644 frontend/src/components/Ops_inventory/TableOps_inventory.tsx create mode 100644 frontend/src/components/Ops_inventory/configureOps_inventoryCols.tsx create mode 100644 frontend/src/components/Ops_lines/CardOps_lines.tsx create mode 100644 frontend/src/components/Ops_lines/ListOps_lines.tsx create mode 100644 frontend/src/components/Ops_lines/TableOps_lines.tsx create mode 100644 frontend/src/components/Ops_lines/configureOps_linesCols.tsx create mode 100644 frontend/src/components/Ops_orders/CardOps_orders.tsx create mode 100644 frontend/src/components/Ops_orders/ListOps_orders.tsx create mode 100644 frontend/src/components/Ops_orders/TableOps_orders.tsx create mode 100644 frontend/src/components/Ops_orders/configureOps_ordersCols.tsx create mode 100644 frontend/src/components/Ops_plants/CardOps_plants.tsx create mode 100644 frontend/src/components/Ops_plants/ListOps_plants.tsx create mode 100644 frontend/src/components/Ops_plants/TableOps_plants.tsx create mode 100644 frontend/src/components/Ops_plants/configureOps_plantsCols.tsx create mode 100644 frontend/src/components/Ops_production_log/CardOps_production_log.tsx create mode 100644 frontend/src/components/Ops_production_log/ListOps_production_log.tsx create mode 100644 frontend/src/components/Ops_production_log/TableOps_production_log.tsx create mode 100644 frontend/src/components/Ops_production_log/configureOps_production_logCols.tsx create mode 100644 frontend/src/components/Ops_shipments/CardOps_shipments.tsx create mode 100644 frontend/src/components/Ops_shipments/ListOps_shipments.tsx create mode 100644 frontend/src/components/Ops_shipments/TableOps_shipments.tsx create mode 100644 frontend/src/components/Ops_shipments/configureOps_shipmentsCols.tsx create mode 100644 frontend/src/components/Organizations/CardOrganizations.tsx create mode 100644 frontend/src/components/Organizations/ListOrganizations.tsx create mode 100644 frontend/src/components/Organizations/TableOrganizations.tsx create mode 100644 frontend/src/components/Organizations/configureOrganizationsCols.tsx create mode 100644 frontend/src/components/OverlayLayer.tsx create mode 100644 frontend/src/components/Pagination.tsx create mode 100644 frontend/src/components/PasswordSetOrReset.tsx create mode 100644 frontend/src/components/Permissions/CardPermissions.tsx create mode 100644 frontend/src/components/Permissions/ListPermissions.tsx create mode 100644 frontend/src/components/Permissions/TablePermissions.tsx create mode 100644 frontend/src/components/Permissions/configurePermissionsCols.tsx create mode 100644 frontend/src/components/RichTextField.tsx create mode 100644 frontend/src/components/Roles/CardRoles.tsx create mode 100644 frontend/src/components/Roles/ListRoles.tsx create mode 100644 frontend/src/components/Roles/TableRoles.tsx create mode 100644 frontend/src/components/Roles/configureRolesCols.tsx create mode 100644 frontend/src/components/Search.tsx create mode 100644 frontend/src/components/SearchResults.tsx create mode 100644 frontend/src/components/SectionFullScreen.tsx create mode 100644 frontend/src/components/SectionMain.tsx create mode 100644 frontend/src/components/SectionTitle.tsx create mode 100644 frontend/src/components/SectionTitleLineWithButton.tsx create mode 100644 frontend/src/components/SelectField.tsx create mode 100644 frontend/src/components/SelectFieldMany.tsx create mode 100644 frontend/src/components/SmartWidget/SmartWidget.tsx create mode 100644 frontend/src/components/SmartWidget/components/AreaChart.tsx create mode 100644 frontend/src/components/SmartWidget/components/AreaChart/ApexAreaChart.tsx create mode 100644 frontend/src/components/SmartWidget/components/AreaChart/ChartJSAreaChart.tsx create mode 100644 frontend/src/components/SmartWidget/components/BarChart.tsx create mode 100644 frontend/src/components/SmartWidget/components/BarChart/ApexBarChart.tsx create mode 100644 frontend/src/components/SmartWidget/components/BarChart/ChartJSBarChart.tsx create mode 100644 frontend/src/components/SmartWidget/components/FunnelChart.tsx create mode 100644 frontend/src/components/SmartWidget/components/LineChart.tsx create mode 100644 frontend/src/components/SmartWidget/components/LineChart/ApexLineChart.tsx create mode 100644 frontend/src/components/SmartWidget/components/LineChart/ChartJSLineChart.tsx create mode 100644 frontend/src/components/SmartWidget/components/PieChart.tsx create mode 100644 frontend/src/components/SmartWidget/components/PieChart/ApexPieChart.tsx create mode 100644 frontend/src/components/SmartWidget/components/PieChart/ChartJSPieChart.tsx create mode 100644 frontend/src/components/SmartWidget/models/widget.model.ts create mode 100644 frontend/src/components/SmartWidget/widgetHelpers.tsx create mode 100644 frontend/src/components/SwitchField.tsx create mode 100644 frontend/src/components/TableSampleClients.tsx create mode 100644 frontend/src/components/Uploaders/FilesUploader.js create mode 100644 frontend/src/components/Uploaders/ImagesUploader.js create mode 100644 frontend/src/components/Uploaders/UploadService.js create mode 100644 frontend/src/components/UserAvatar.tsx create mode 100644 frontend/src/components/UserAvatarCurrentUser.tsx create mode 100644 frontend/src/components/UserCard.tsx create mode 100644 frontend/src/components/Users/CardUsers.tsx create mode 100644 frontend/src/components/Users/ListUsers.tsx create mode 100644 frontend/src/components/Users/TableUsers.tsx create mode 100644 frontend/src/components/Users/configureUsersCols.tsx create mode 100644 frontend/src/components/WebPageComponents/AboutUsComponent/designs/ImageLeft.tsx create mode 100644 frontend/src/components/WebPageComponents/AboutUsComponent/designs/ImageRight.tsx create mode 100644 frontend/src/components/WebPageComponents/AboutUsComponent/index.tsx create mode 100644 frontend/src/components/WebPageComponents/ContactFormComponent/designs/FormWithImage.tsx create mode 100644 frontend/src/components/WebPageComponents/ContactFormComponent/designs/HighlightedForm.tsx create mode 100644 frontend/src/components/WebPageComponents/ContactFormComponent/designs/SimpleAndCleanForm.tsx create mode 100644 frontend/src/components/WebPageComponents/ContactFormComponent/index.tsx create mode 100644 frontend/src/components/WebPageComponents/FaqComponent/designs/FAQAccordion.tsx create mode 100644 frontend/src/components/WebPageComponents/FaqComponent/designs/FAQSplitList.tsx create mode 100644 frontend/src/components/WebPageComponents/FaqComponent/designs/FAQTwoColumn.tsx create mode 100644 frontend/src/components/WebPageComponents/FaqComponent/index.tsx create mode 100644 frontend/src/components/WebPageComponents/FeaturesComponent/designs/CardsGridWithIcons.tsx create mode 100644 frontend/src/components/WebPageComponents/FeaturesComponent/designs/IconsTop.tsx create mode 100644 frontend/src/components/WebPageComponents/FeaturesComponent/designs/IconsWithImage.tsx create mode 100644 frontend/src/components/WebPageComponents/FeaturesComponent/designs/LargeNumbers.tsx create mode 100644 frontend/src/components/WebPageComponents/FeaturesComponent/index.tsx create mode 100644 frontend/src/components/WebPageComponents/Footer.tsx create mode 100644 frontend/src/components/WebPageComponents/GalleryPortfolioComponent/designs/HorizontalGalleryWithButtons.tsx create mode 100644 frontend/src/components/WebPageComponents/GalleryPortfolioComponent/designs/OverlappingCentralImage.tsx create mode 100644 frontend/src/components/WebPageComponents/GalleryPortfolioComponent/index.tsx create mode 100644 frontend/src/components/WebPageComponents/Header.tsx create mode 100644 frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageBg.tsx create mode 100644 frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageLeft.tsx create mode 100644 frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageRight.tsx create mode 100644 frontend/src/components/WebPageComponents/HeroComponent/designs/HeroTextCenter.tsx create mode 100644 frontend/src/components/WebPageComponents/HeroComponent/index.tsx create mode 100644 frontend/src/components/WebPageComponents/PricingComponent/index.tsx create mode 100644 frontend/src/components/WebPageComponents/TestimonialsComponent/designs/HorizontalCarousel.tsx create mode 100644 frontend/src/components/WebPageComponents/TestimonialsComponent/designs/MultiCardDisplay.tsx create mode 100644 frontend/src/components/WebPageComponents/TestimonialsComponent/index.tsx create mode 100644 frontend/src/components/WebPageComponents/designs.ts create mode 100644 frontend/src/components/WidgetCreator/RoleSelect.tsx create mode 100644 frontend/src/components/WidgetCreator/WidgetCreator.tsx create mode 100644 frontend/src/config.ts create mode 100644 frontend/src/css/_app.css create mode 100644 frontend/src/css/_calendar.css create mode 100644 frontend/src/css/_checkbox-radio-switch.css create mode 100644 frontend/src/css/_helper.css create mode 100644 frontend/src/css/_progress.css create mode 100644 frontend/src/css/_rich-text.css create mode 100644 frontend/src/css/_scrollbars.css create mode 100644 frontend/src/css/_select-dropdown.css create mode 100644 frontend/src/css/_table.css create mode 100644 frontend/src/css/_theme.css create mode 100644 frontend/src/css/main.css create mode 100644 frontend/src/css/tailwind/_base.css create mode 100644 frontend/src/css/tailwind/_components.css create mode 100644 frontend/src/css/tailwind/_utilities.css create mode 100644 frontend/src/helpers/dataFormatter.js create mode 100644 frontend/src/helpers/fileSaver.ts create mode 100644 frontend/src/helpers/humanize.ts create mode 100644 frontend/src/helpers/notifyStateHandler.ts create mode 100644 frontend/src/helpers/pexels.ts create mode 100644 frontend/src/helpers/userPermissions.ts create mode 100644 frontend/src/hooks/sampleData.ts create mode 100644 frontend/src/hooks/useDevCompilationStatus.ts create mode 100644 frontend/src/i18n.ts create mode 100644 frontend/src/interfaces/index.ts create mode 100644 frontend/src/layouts/Authenticated.tsx create mode 100644 frontend/src/layouts/Guest.tsx create mode 100644 frontend/src/menuAside.ts create mode 100644 frontend/src/menuNavBar.ts create mode 100644 frontend/src/pages/_app.tsx create mode 100644 frontend/src/pages/api/hello.js create mode 100644 frontend/src/pages/api/logError.ts create mode 100644 frontend/src/pages/audit_logs/[audit_logsId].tsx create mode 100644 frontend/src/pages/audit_logs/audit_logs-edit.tsx create mode 100644 frontend/src/pages/audit_logs/audit_logs-list.tsx create mode 100644 frontend/src/pages/audit_logs/audit_logs-new.tsx create mode 100644 frontend/src/pages/audit_logs/audit_logs-table.tsx create mode 100644 frontend/src/pages/audit_logs/audit_logs-view.tsx create mode 100644 frontend/src/pages/dashboard.tsx create mode 100644 frontend/src/pages/data_connections/[data_connectionsId].tsx create mode 100644 frontend/src/pages/data_connections/data_connections-edit.tsx create mode 100644 frontend/src/pages/data_connections/data_connections-list.tsx create mode 100644 frontend/src/pages/data_connections/data_connections-new.tsx create mode 100644 frontend/src/pages/data_connections/data_connections-table.tsx create mode 100644 frontend/src/pages/data_connections/data_connections-view.tsx create mode 100644 frontend/src/pages/error.tsx create mode 100644 frontend/src/pages/fin_accounts/[fin_accountsId].tsx create mode 100644 frontend/src/pages/fin_accounts/fin_accounts-edit.tsx create mode 100644 frontend/src/pages/fin_accounts/fin_accounts-list.tsx create mode 100644 frontend/src/pages/fin_accounts/fin_accounts-new.tsx create mode 100644 frontend/src/pages/fin_accounts/fin_accounts-table.tsx create mode 100644 frontend/src/pages/fin_accounts/fin_accounts-view.tsx create mode 100644 frontend/src/pages/fin_ap/[fin_apId].tsx create mode 100644 frontend/src/pages/fin_ap/fin_ap-edit.tsx create mode 100644 frontend/src/pages/fin_ap/fin_ap-list.tsx create mode 100644 frontend/src/pages/fin_ap/fin_ap-new.tsx create mode 100644 frontend/src/pages/fin_ap/fin_ap-table.tsx create mode 100644 frontend/src/pages/fin_ap/fin_ap-view.tsx create mode 100644 frontend/src/pages/fin_ar/[fin_arId].tsx create mode 100644 frontend/src/pages/fin_ar/fin_ar-edit.tsx create mode 100644 frontend/src/pages/fin_ar/fin_ar-list.tsx create mode 100644 frontend/src/pages/fin_ar/fin_ar-new.tsx create mode 100644 frontend/src/pages/fin_ar/fin_ar-table.tsx create mode 100644 frontend/src/pages/fin_ar/fin_ar-view.tsx create mode 100644 frontend/src/pages/fin_budgets/[fin_budgetsId].tsx create mode 100644 frontend/src/pages/fin_budgets/fin_budgets-edit.tsx create mode 100644 frontend/src/pages/fin_budgets/fin_budgets-list.tsx create mode 100644 frontend/src/pages/fin_budgets/fin_budgets-new.tsx create mode 100644 frontend/src/pages/fin_budgets/fin_budgets-table.tsx create mode 100644 frontend/src/pages/fin_budgets/fin_budgets-view.tsx create mode 100644 frontend/src/pages/fin_customers/[fin_customersId].tsx create mode 100644 frontend/src/pages/fin_customers/fin_customers-edit.tsx create mode 100644 frontend/src/pages/fin_customers/fin_customers-list.tsx create mode 100644 frontend/src/pages/fin_customers/fin_customers-new.tsx create mode 100644 frontend/src/pages/fin_customers/fin_customers-table.tsx create mode 100644 frontend/src/pages/fin_customers/fin_customers-view.tsx create mode 100644 frontend/src/pages/fin_forecasts/[fin_forecastsId].tsx create mode 100644 frontend/src/pages/fin_forecasts/fin_forecasts-edit.tsx create mode 100644 frontend/src/pages/fin_forecasts/fin_forecasts-list.tsx create mode 100644 frontend/src/pages/fin_forecasts/fin_forecasts-new.tsx create mode 100644 frontend/src/pages/fin_forecasts/fin_forecasts-table.tsx create mode 100644 frontend/src/pages/fin_forecasts/fin_forecasts-view.tsx create mode 100644 frontend/src/pages/fin_transactions/[fin_transactionsId].tsx create mode 100644 frontend/src/pages/fin_transactions/fin_transactions-edit.tsx create mode 100644 frontend/src/pages/fin_transactions/fin_transactions-list.tsx create mode 100644 frontend/src/pages/fin_transactions/fin_transactions-new.tsx create mode 100644 frontend/src/pages/fin_transactions/fin_transactions-table.tsx create mode 100644 frontend/src/pages/fin_transactions/fin_transactions-view.tsx create mode 100644 frontend/src/pages/fin_vendors/[fin_vendorsId].tsx create mode 100644 frontend/src/pages/fin_vendors/fin_vendors-edit.tsx create mode 100644 frontend/src/pages/fin_vendors/fin_vendors-list.tsx create mode 100644 frontend/src/pages/fin_vendors/fin_vendors-new.tsx create mode 100644 frontend/src/pages/fin_vendors/fin_vendors-table.tsx create mode 100644 frontend/src/pages/fin_vendors/fin_vendors-view.tsx create mode 100644 frontend/src/pages/forgot.tsx create mode 100644 frontend/src/pages/forms.tsx create mode 100644 frontend/src/pages/hr_applicants/[hr_applicantsId].tsx create mode 100644 frontend/src/pages/hr_applicants/hr_applicants-edit.tsx create mode 100644 frontend/src/pages/hr_applicants/hr_applicants-list.tsx create mode 100644 frontend/src/pages/hr_applicants/hr_applicants-new.tsx create mode 100644 frontend/src/pages/hr_applicants/hr_applicants-table.tsx create mode 100644 frontend/src/pages/hr_applicants/hr_applicants-view.tsx create mode 100644 frontend/src/pages/hr_attendance/[hr_attendanceId].tsx create mode 100644 frontend/src/pages/hr_attendance/hr_attendance-edit.tsx create mode 100644 frontend/src/pages/hr_attendance/hr_attendance-list.tsx create mode 100644 frontend/src/pages/hr_attendance/hr_attendance-new.tsx create mode 100644 frontend/src/pages/hr_attendance/hr_attendance-table.tsx create mode 100644 frontend/src/pages/hr_attendance/hr_attendance-view.tsx create mode 100644 frontend/src/pages/hr_employees/[hr_employeesId].tsx create mode 100644 frontend/src/pages/hr_employees/hr_employees-edit.tsx create mode 100644 frontend/src/pages/hr_employees/hr_employees-list.tsx create mode 100644 frontend/src/pages/hr_employees/hr_employees-new.tsx create mode 100644 frontend/src/pages/hr_employees/hr_employees-table.tsx create mode 100644 frontend/src/pages/hr_employees/hr_employees-view.tsx create mode 100644 frontend/src/pages/hr_payroll/[hr_payrollId].tsx create mode 100644 frontend/src/pages/hr_payroll/hr_payroll-edit.tsx create mode 100644 frontend/src/pages/hr_payroll/hr_payroll-list.tsx create mode 100644 frontend/src/pages/hr_payroll/hr_payroll-new.tsx create mode 100644 frontend/src/pages/hr_payroll/hr_payroll-table.tsx create mode 100644 frontend/src/pages/hr_payroll/hr_payroll-view.tsx create mode 100644 frontend/src/pages/hr_positions/[hr_positionsId].tsx create mode 100644 frontend/src/pages/hr_positions/hr_positions-edit.tsx create mode 100644 frontend/src/pages/hr_positions/hr_positions-list.tsx create mode 100644 frontend/src/pages/hr_positions/hr_positions-new.tsx create mode 100644 frontend/src/pages/hr_positions/hr_positions-table.tsx create mode 100644 frontend/src/pages/hr_positions/hr_positions-view.tsx create mode 100644 frontend/src/pages/hr_reqs/[hr_reqsId].tsx create mode 100644 frontend/src/pages/hr_reqs/hr_reqs-edit.tsx create mode 100644 frontend/src/pages/hr_reqs/hr_reqs-list.tsx create mode 100644 frontend/src/pages/hr_reqs/hr_reqs-new.tsx create mode 100644 frontend/src/pages/hr_reqs/hr_reqs-table.tsx create mode 100644 frontend/src/pages/hr_reqs/hr_reqs-view.tsx create mode 100644 frontend/src/pages/index.tsx create mode 100644 frontend/src/pages/login.tsx create mode 100644 frontend/src/pages/ops_incidents/[ops_incidentsId].tsx create mode 100644 frontend/src/pages/ops_incidents/ops_incidents-edit.tsx create mode 100644 frontend/src/pages/ops_incidents/ops_incidents-list.tsx create mode 100644 frontend/src/pages/ops_incidents/ops_incidents-new.tsx create mode 100644 frontend/src/pages/ops_incidents/ops_incidents-table.tsx create mode 100644 frontend/src/pages/ops_incidents/ops_incidents-view.tsx create mode 100644 frontend/src/pages/ops_inventory/[ops_inventoryId].tsx create mode 100644 frontend/src/pages/ops_inventory/ops_inventory-edit.tsx create mode 100644 frontend/src/pages/ops_inventory/ops_inventory-list.tsx create mode 100644 frontend/src/pages/ops_inventory/ops_inventory-new.tsx create mode 100644 frontend/src/pages/ops_inventory/ops_inventory-table.tsx create mode 100644 frontend/src/pages/ops_inventory/ops_inventory-view.tsx create mode 100644 frontend/src/pages/ops_lines/[ops_linesId].tsx create mode 100644 frontend/src/pages/ops_lines/ops_lines-edit.tsx create mode 100644 frontend/src/pages/ops_lines/ops_lines-list.tsx create mode 100644 frontend/src/pages/ops_lines/ops_lines-new.tsx create mode 100644 frontend/src/pages/ops_lines/ops_lines-table.tsx create mode 100644 frontend/src/pages/ops_lines/ops_lines-view.tsx create mode 100644 frontend/src/pages/ops_orders/[ops_ordersId].tsx create mode 100644 frontend/src/pages/ops_orders/ops_orders-edit.tsx create mode 100644 frontend/src/pages/ops_orders/ops_orders-list.tsx create mode 100644 frontend/src/pages/ops_orders/ops_orders-new.tsx create mode 100644 frontend/src/pages/ops_orders/ops_orders-table.tsx create mode 100644 frontend/src/pages/ops_orders/ops_orders-view.tsx create mode 100644 frontend/src/pages/ops_plants/[ops_plantsId].tsx create mode 100644 frontend/src/pages/ops_plants/ops_plants-edit.tsx create mode 100644 frontend/src/pages/ops_plants/ops_plants-list.tsx create mode 100644 frontend/src/pages/ops_plants/ops_plants-new.tsx create mode 100644 frontend/src/pages/ops_plants/ops_plants-table.tsx create mode 100644 frontend/src/pages/ops_plants/ops_plants-view.tsx create mode 100644 frontend/src/pages/ops_production_log/[ops_production_logId].tsx create mode 100644 frontend/src/pages/ops_production_log/ops_production_log-edit.tsx create mode 100644 frontend/src/pages/ops_production_log/ops_production_log-list.tsx create mode 100644 frontend/src/pages/ops_production_log/ops_production_log-new.tsx create mode 100644 frontend/src/pages/ops_production_log/ops_production_log-table.tsx create mode 100644 frontend/src/pages/ops_production_log/ops_production_log-view.tsx create mode 100644 frontend/src/pages/ops_shipments/[ops_shipmentsId].tsx create mode 100644 frontend/src/pages/ops_shipments/ops_shipments-edit.tsx create mode 100644 frontend/src/pages/ops_shipments/ops_shipments-list.tsx create mode 100644 frontend/src/pages/ops_shipments/ops_shipments-new.tsx create mode 100644 frontend/src/pages/ops_shipments/ops_shipments-table.tsx create mode 100644 frontend/src/pages/ops_shipments/ops_shipments-view.tsx create mode 100644 frontend/src/pages/organizations/[organizationsId].tsx create mode 100644 frontend/src/pages/organizations/organizations-edit.tsx create mode 100644 frontend/src/pages/organizations/organizations-list.tsx create mode 100644 frontend/src/pages/organizations/organizations-new.tsx create mode 100644 frontend/src/pages/organizations/organizations-table.tsx create mode 100644 frontend/src/pages/organizations/organizations-view.tsx create mode 100644 frontend/src/pages/password-reset.tsx create mode 100644 frontend/src/pages/permissions/[permissionsId].tsx create mode 100644 frontend/src/pages/permissions/permissions-edit.tsx create mode 100644 frontend/src/pages/permissions/permissions-list.tsx create mode 100644 frontend/src/pages/permissions/permissions-new.tsx create mode 100644 frontend/src/pages/permissions/permissions-table.tsx create mode 100644 frontend/src/pages/permissions/permissions-view.tsx create mode 100644 frontend/src/pages/privacy-policy.tsx create mode 100644 frontend/src/pages/profile.tsx create mode 100644 frontend/src/pages/register.tsx create mode 100644 frontend/src/pages/roles/[rolesId].tsx create mode 100644 frontend/src/pages/roles/roles-edit.tsx create mode 100644 frontend/src/pages/roles/roles-list.tsx create mode 100644 frontend/src/pages/roles/roles-new.tsx create mode 100644 frontend/src/pages/roles/roles-table.tsx create mode 100644 frontend/src/pages/roles/roles-view.tsx create mode 100644 frontend/src/pages/search.tsx create mode 100644 frontend/src/pages/tables.tsx create mode 100644 frontend/src/pages/terms-of-use.tsx create mode 100644 frontend/src/pages/users/[usersId].tsx create mode 100644 frontend/src/pages/users/users-edit.tsx create mode 100644 frontend/src/pages/users/users-list.tsx create mode 100644 frontend/src/pages/users/users-new.tsx create mode 100644 frontend/src/pages/users/users-table.tsx create mode 100644 frontend/src/pages/users/users-view.tsx create mode 100644 frontend/src/pages/verify-email.tsx create mode 100644 frontend/src/pages/web_pages/contact.tsx create mode 100644 frontend/src/pages/web_pages/faq.tsx create mode 100644 frontend/src/pages/web_pages/services.tsx create mode 100644 frontend/src/stores/audit_logs/audit_logsSlice.ts create mode 100644 frontend/src/stores/authSlice.ts create mode 100644 frontend/src/stores/data_connections/data_connectionsSlice.ts create mode 100644 frontend/src/stores/fin_accounts/fin_accountsSlice.ts create mode 100644 frontend/src/stores/fin_ap/fin_apSlice.ts create mode 100644 frontend/src/stores/fin_ar/fin_arSlice.ts create mode 100644 frontend/src/stores/fin_budgets/fin_budgetsSlice.ts create mode 100644 frontend/src/stores/fin_customers/fin_customersSlice.ts create mode 100644 frontend/src/stores/fin_forecasts/fin_forecastsSlice.ts create mode 100644 frontend/src/stores/fin_transactions/fin_transactionsSlice.ts create mode 100644 frontend/src/stores/fin_vendors/fin_vendorsSlice.ts create mode 100644 frontend/src/stores/hooks.ts create mode 100644 frontend/src/stores/hr_applicants/hr_applicantsSlice.ts create mode 100644 frontend/src/stores/hr_attendance/hr_attendanceSlice.ts create mode 100644 frontend/src/stores/hr_employees/hr_employeesSlice.ts create mode 100644 frontend/src/stores/hr_payroll/hr_payrollSlice.ts create mode 100644 frontend/src/stores/hr_positions/hr_positionsSlice.ts create mode 100644 frontend/src/stores/hr_reqs/hr_reqsSlice.ts create mode 100644 frontend/src/stores/introSteps.ts create mode 100644 frontend/src/stores/mainSlice.ts create mode 100644 frontend/src/stores/openAiSlice.ts create mode 100644 frontend/src/stores/ops_incidents/ops_incidentsSlice.ts create mode 100644 frontend/src/stores/ops_inventory/ops_inventorySlice.ts create mode 100644 frontend/src/stores/ops_lines/ops_linesSlice.ts create mode 100644 frontend/src/stores/ops_orders/ops_ordersSlice.ts create mode 100644 frontend/src/stores/ops_plants/ops_plantsSlice.ts create mode 100644 frontend/src/stores/ops_production_log/ops_production_logSlice.ts create mode 100644 frontend/src/stores/ops_shipments/ops_shipmentsSlice.ts create mode 100644 frontend/src/stores/organizations/organizationsSlice.ts create mode 100644 frontend/src/stores/permissions/permissionsSlice.ts create mode 100644 frontend/src/stores/roles/rolesSlice.ts create mode 100644 frontend/src/stores/store.ts create mode 100644 frontend/src/stores/styleSlice.ts create mode 100644 frontend/src/stores/users/usersSlice.ts create mode 100644 frontend/src/stores/usersSlice.ts create mode 100644 frontend/src/styles.ts create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json create mode 100644 frontend/yarn.lock create mode 100644 nginx.conf create mode 100644 package.json create mode 100644 pids/backend.pid create mode 100644 pids/frontend.pid diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e427ff3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +*/node_modules/ +*/build/ diff --git a/502.html b/502.html new file mode 100644 index 0000000..5d45a7e --- /dev/null +++ b/502.html @@ -0,0 +1,187 @@ + + + + + + + Service Starting + + + + +
+

Loading the app, just a moment…

+

The application is currently launching. The page will automatically refresh once site is + available.

+
+

Title Enterprise Dashboards Finance HR Ops

+

Enterprise dashboards for Finance, HR, and Operations with multi-tenant support.

+
+
+ +
+
+
+ +
+
+ + + + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..affcee8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM node:20.15.1-alpine AS builder +RUN apk add --no-cache git +WORKDIR /app +COPY frontend/package.json frontend/yarn.lock ./ +RUN yarn install --pure-lockfile +COPY frontend . +RUN yarn build + +FROM node:20.15.1-alpine +WORKDIR /app +COPY backend/package.json backend/yarn.lock ./ +RUN yarn install --pure-lockfile +COPY backend . + +COPY --from=builder /app/build /app/public +CMD ["yarn", "start"] + diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..a8353d5 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,85 @@ +# Base image for Node.js dependencies +FROM node:20.15.1-alpine AS frontend-deps +RUN apk add --no-cache git +WORKDIR /app/frontend +COPY frontend/package.json frontend/yarn.lock ./ +RUN yarn install --pure-lockfile + +FROM node:20.15.1-alpine AS backend-deps +RUN apk add --no-cache git +WORKDIR /app/backend +COPY backend/package.json backend/yarn.lock ./ +RUN yarn install --pure-lockfile + +FROM node:20.15.1-alpine AS app-shell-deps +RUN apk add --no-cache git +WORKDIR /app/app-shell +COPY app-shell/package.json app-shell/yarn.lock ./ +RUN yarn install --pure-lockfile + +# Nginx setup and application build +FROM node:20.15.1-alpine AS build +RUN apk add --no-cache git nginx curl +RUN apk add --no-cache lsof procps +RUN yarn global add concurrently + +RUN apk add --no-cache \ + chromium \ + nss \ + freetype \ + harfbuzz \ + ttf-freefont \ + fontconfig + +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true +ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser + +RUN mkdir -p /app/pids + +# Make sure to add yarn global bin to PATH +ENV PATH /root/.yarn/bin:/root/.config/yarn/global/node_modules/.bin:$PATH + +# Copy dependencies +WORKDIR /app +COPY --from=frontend-deps /app/frontend /app/frontend +COPY --from=backend-deps /app/backend /app/backend +COPY --from=app-shell-deps /app/app-shell /app/app-shell + +COPY frontend /app/frontend +COPY backend /app/backend +COPY app-shell /app/app-shell +COPY docker /app/docker + +# Copy all files from root to /app +COPY . /app + +# Copy Nginx configuration +COPY nginx.conf /etc/nginx/nginx.conf + +# Copy custom error page +COPY 502.html /usr/share/nginx/html/502.html + +# Change owner and permissions of the error page +RUN chown nginx:nginx /usr/share/nginx/html/502.html && \ + chmod 644 /usr/share/nginx/html/502.html + +# Expose the port the app runs on +EXPOSE 8080 +ENV NODE_ENV=dev_stage +ENV FRONT_PORT=3001 +ENV BACKEND_PORT=3000 +ENV APP_SHELL_PORT=4000 + + +CMD ["sh", "-c", "\ + yarn --cwd /app/frontend dev & echo $! > /app/pids/frontend.pid && \ + yarn --cwd /app/backend start & echo $! > /app/pids/backend.pid && \ + sleep 10 && nginx -g 'daemon off;' & \ + NGINX_PID=$! && \ + echo 'Waiting for backend (port 3000) to be available...' && \ + while ! nc -z localhost ${BACKEND_PORT}; do \ + sleep 2; \ + done && \ + echo 'Backend is up. Starting app_shell for Git check...' && \ + yarn --cwd /app/app-shell start && \ + wait $NGINX_PID"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5d69ed7 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +https://flatlogic.com/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..2de50fd --- /dev/null +++ b/README.md @@ -0,0 +1,200 @@ + + +# Title Enterprise Dashboards Finance HR Ops + +## This project was generated by [Flatlogic Platform](https://flatlogic.com). + + - Frontend: [React.js](https://flatlogic.com/templates?framework%5B%5D=react&sort=default) + + - Backend: [NodeJS](https://flatlogic.com/templates?backend%5B%5D=nodejs&sort=default) + +
Backend Folder Structure + + The generated application has the following backend folder structure: + + `src` folder which contains your working files that will be used later to create the build. The src folder contains folders as: + + - `auth` - config the library for authentication and authorization; + + - `db` - contains such folders as: + + - `api` - documentation that is automatically generated by jsdoc or other tools; + + - `migrations` - is a skeleton of the database or all the actions that users do with the database; + + - `models`- what will represent the database for the backend; + + - `seeders` - the entity that creates the data for the database. + + - `routes` - this folder would contain all the routes that you have created using Express Router and what they do would be exported from a Controller file; + + - `services` - contains such folders as `emails` and `notifications`. +
+ + - Database: PostgreSQL + + - app-shel: Core application framework that provides essential infrastructure services +for the entire application. + ----------------------- +### We offer 2 ways how to start the project locally: by running Frontend and Backend or with Docker. +----------------------- + +## To start the project: + +### Backend: + +> Please change current folder: `cd backend` + +#### Install local dependencies: +`yarn install` + + ------------ + +#### Adjust local db: +##### 1. Install postgres: + +MacOS: + +`brew install postgres` + + > if you don’t have ‘brew‘ please install it (https://brew.sh) and repeat step `brew install postgres`. + +Ubuntu: + +`sudo apt update` + +`sudo apt install postgresql postgresql-contrib` + +##### 2. Create db and admin user: +Before run and test connection, make sure you have created a database as described in the above configuration. You can use the `psql` command to create a user and database. + +`psql postgres --u postgres` + +Next, type this command for creating a new user with password then give access for creating the database. + +`postgres-# CREATE ROLE admin WITH LOGIN PASSWORD 'admin_pass';` + +`postgres-# ALTER ROLE admin CREATEDB;` + +Quit `psql` then log in again using the new user that previously created. + +`postgres-# \q` + +`psql postgres -U admin` + +Type this command to creating a new database. + +`postgres=> CREATE DATABASE db_{your_project_name};` + +Then give that new user privileges to the new database then quit the `psql`. + +`postgres=> GRANT ALL PRIVILEGES ON DATABASE db_{your_project_name} TO admin;` + +`postgres=> \q` + + ------------ + +#### Create database: +`yarn db:create` + +#### Start production build: +`yarn start` + +### Frontend: + +> Please change current folder: `cd frontend` + +## To start the project with Docker: +### Description: + +The project contains the **docker folder** and the `Dockerfile`. + +The `Dockerfile` is used to Deploy the project to Google Cloud. + +The **docker folder** contains a couple of helper scripts: + +- `docker-compose.yml` (all our services: web, backend, db are described here) +- `start-backend.sh` (starts backend, but only after the database) +- `wait-for-it.sh` (imported from https://github.com/vishnubob/wait-for-it) + + > To avoid breaking the application, we recommend you don't edit the following files: everything that includes the **docker folder** and `Dokerfile`. + +## Run services: + +1. Install docker compose (https://docs.docker.com/compose/install/) + +2. Move to `docker` folder. All next steps should be done from this folder. + + ``` cd docker ``` + +3. Make executables from `wait-for-it.sh` and `start-backend.sh`: + + ``` chmod +x start-backend.sh && chmod +x wait-for-it.sh ``` + +4. Download dependend projects for services. + +5. Review the docker-compose.yml file. Make sure that all services have Dockerfiles. Only db service doesn't require a Dockerfile. + +6. Make sure you have needed ports (see them in `ports`) available on your local machine. + +7. Start services: + + 7.1. With an empty database `rm -rf data && docker-compose up` + + 7.2. With a stored (from previus runs) database data `docker-compose up` + +8. Check http://localhost:3000 + +9. Stop services: + + 9.1. Just press `Ctr+C` + +## Most common errors: + +1. `connection refused` + + There could be many reasons, but the most common are: + + - The port is not open on the destination machine. + + - The port is open on the destination machine, but its backlog of pending connections is full. + + - A firewall between the client and server is blocking access (also check local firewalls). + + After checking for firewalls and that the port is open, use telnet to connect to the IP/port to test connectivity. This removes any potential issues from your application. + + ***MacOS:*** + + If you suspect that your SSH service might be down, you can run this command to find out: + + `sudo service ssh status` + + If the command line returns a status of down, then you’ve likely found the reason behind your connectivity error. + + ***Ubuntu:*** + + Sometimes a connection refused error can also indicate that there is an IP address conflict on your network. You can search for possible IP conflicts by running: + + `arp-scan -I eth0 -l | grep ` + + `arp-scan -I eth0 -l | grep ` + + and + + `arping ` + +2. `yarn db:create` creates database with the assembled tables (on MacOS with Postgres database) + + The workaround - put the next commands to your Postgres database terminal: + + `DROP SCHEMA public CASCADE;` + + `CREATE SCHEMA public;` + + `GRANT ALL ON SCHEMA public TO postgres;` + + `GRANT ALL ON SCHEMA public TO public;` + + Afterwards, continue to start your project in the backend directory by running: + + `yarn start` diff --git a/app-shell/.eslintrc.cjs b/app-shell/.eslintrc.cjs new file mode 100644 index 0000000..563d159 --- /dev/null +++ b/app-shell/.eslintrc.cjs @@ -0,0 +1,26 @@ +const globals = require('globals'); + +module.exports = [ + { + files: ['**/*.js', '**/*.ts', '**/*.tsx'], + languageOptions: { + ecmaVersion: 2021, + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.node, + }, + parser: '@typescript-eslint/parser', + }, + plugins: ['@typescript-eslint'], + rules: { + 'no-unused-vars': 'warn', + 'no-console': 'off', + 'indent': ['error', 2], + 'quotes': ['error', 'single'], + 'semi': ['error', 'always'], + + '@typescript-eslint/no-unused-vars': 'warn', + }, + }, +]; \ No newline at end of file diff --git a/app-shell/.prettierrc b/app-shell/.prettierrc new file mode 100644 index 0000000..bb087f2 --- /dev/null +++ b/app-shell/.prettierrc @@ -0,0 +1,11 @@ +{ + "singleQuote": true, + "tabWidth": 2, + "printWidth": 80, + "trailingComma": "all", + "quoteProps": "as-needed", + "jsxSingleQuote": true, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always" +} diff --git a/app-shell/.sequelizerc b/app-shell/.sequelizerc new file mode 100644 index 0000000..fe89188 --- /dev/null +++ b/app-shell/.sequelizerc @@ -0,0 +1,7 @@ +const path = require('path'); +module.exports = { + "config": path.resolve("src", "db", "db.config.js"), + "models-path": path.resolve("src", "db", "models"), + "seeders-path": path.resolve("src", "db", "seeders"), + "migrations-path": path.resolve("src", "db", "migrations") +}; \ No newline at end of file diff --git a/app-shell/Dockerfile b/app-shell/Dockerfile new file mode 100644 index 0000000..eb79c5d --- /dev/null +++ b/app-shell/Dockerfile @@ -0,0 +1,23 @@ +FROM node:20.15.1-alpine + +RUN apk update && apk add bash +# Create app directory +WORKDIR /usr/src/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ + +RUN yarn install +# If you are building your code for production +# RUN npm ci --only=production + + +# Bundle app source +COPY . . + + +EXPOSE 4000 + +CMD [ "yarn", "start" ] diff --git a/app-shell/README.md b/app-shell/README.md new file mode 100644 index 0000000..c53191f --- /dev/null +++ b/app-shell/README.md @@ -0,0 +1,13 @@ +#test - template backend, + +#### Run App on local machine: + +##### Install local dependencies: + +- `yarn install` + +--- + +##### Start build: + +- `yarn start` diff --git a/app-shell/package.json b/app-shell/package.json new file mode 100644 index 0000000..12c9700 --- /dev/null +++ b/app-shell/package.json @@ -0,0 +1,43 @@ +{ + "name": "app-shell", + "description": "app-shell", + "scripts": { + "start": "node ./src/index.js" + }, + "dependencies": { + "@babel/parser": "^7.26.7", + "adm-zip": "^0.5.16", + "axios": "^1.6.7", + "bcrypt": "5.1.1", + "cors": "2.8.5", + "eslint": "^9.13.0", + "express": "4.18.2", + "formidable": "1.2.2", + "helmet": "4.1.1", + "json2csv": "^5.0.7", + "jsonwebtoken": "8.5.1", + "lodash": "4.17.21", + "moment": "2.30.1", + "multer": "^1.4.4", + "passport": "^0.7.0", + "passport-google-oauth2": "^0.2.0", + "passport-jwt": "^4.0.1", + "passport-microsoft": "^0.1.0", + "postcss": "^8.5.1", + "puppeteer": "^24.10.0", + "sequelize-json-schema": "^2.1.1", + "pg": "^8.13.3" + }, + "engines": { + "node": ">=18" + }, + "private": true, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^8.12.2", + "@typescript-eslint/parser": "^8.12.2", + "cross-env": "7.0.3", + "mocha": "8.1.3", + "nodemon": "^3.1.7", + "sequelize-cli": "6.6.2" + } +} diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json new file mode 100644 index 0000000..98d22a1 --- /dev/null +++ b/app-shell/src/_schema.json @@ -0,0 +1,5 @@ + + +{ + "Initial version": "{\"iv\":\"bbZKQ0NZuyqCFzZH\",\"encryptedData\":\"FgUDWFoczSONnbMe7wP98jsQN7KxCMNsvxxIOpzpAuDXce83kmIDKHkremdCYOubFbMPzkMrGkGLV6Hj52nk6cfxBh6VTrHwUYB/6UvAxnvJLzVaDGWe7eraelDsDqsCeJTHaMvaa3Do5GbhZ8KVnpEUnA4rDP0SxDFGS9nk0h616KfrI7ShnTgdy5I1hcfy/Oo1hx6doQysyQ8BZNsTJp9VUE2xtyFZiOreFyp486LZBVLRDU865nxqnS1DrtWigcjakZlicYyvxgEzuJaj/hYlArfFqPb/lhZYEDcleWFM0wyQGYQYSSUuepMPr+HWauvcgMcQifJfBvG+SEmuF96uNLBIxaT7pVlBqrWyqm4fj/VivvSaVUu41IZN8c3m81cd6yqtVhz1rQonzm1+ZmKcUArGbS1aD8uTRuro/73Y1hlo7Q7gDRct91DF6TY8S792f1KU+loStMLzE9YpTsPz4ZKGtLJBaS6uiPobNxLDIKpabKcD+IY1lYPLq5hzS1zHY0P4uV2mm9Q8PKYasbYYjbYsR8DSekM7aW6VoZMKesKeANP6VBgcwMf/YWyR9S9FPsMh8zvQx4IZUmf8Vi8K1Ah5tQ+6m6zG5n8Jifrd6Jbn8B+/yuS16hDSwR59B2DrivYQN5gtXSjknx9UVEcMRhUFRQG9d9HMm1yyiM0k3rWdSklHNKgHv2csirBP042yv/SKOcjjIdNjSQ3+Pg93xJowUydK4+PVEzNi5HVskweBC//Z0ObJIN3Deo8OKD/thEjXzMsBdzoZRrunptHHyGlKDqGToOsbqKTlzDBxPMt20bOUiyYYMPejNi2k6cr7XS6kuvf3cWjKRiDmrVUmffCYEYljeThle58hmlau/UOqGo6ufwvU3uHC0fMIT3i3IdYLN2lQS/nxwv50HfrLYajLGwjrXt+3R7q+lp11tKXRjPwKGLaZ8ZxjsvuxYKw/EgQn1pSMIo2ZkSrx+YqOzVlLbttgDIvlDyNy/sJWAeHu5BKx6gEAKftuHuQSuDP9smIvwR7viOtnM38dEwz0axC8cRXfrxS2+TePB5sKGIKUlAsoBMXkv37mC0qKm0OnWaNtqei28ggqgdB8DnYugmyBLjOSlcVYZAKH4Zz+QPWaMFkU/7yBL9fOQ/LxDG60cU/PHQglqVY6zK7wShLmWHCRxCV2tXKptQoG6ySuqiYs4Y2LlTBrF+lN9W/W01yi2euOAPntgABFXM+lEgdsunGvwCABCQ0GqBLtzFl/s99wUP4jLiaOXemib/WVQV+hvt25pcZIE0l7TFEgbj2QWhOcYagQTinA321FiP3hIxK5IcjvDh5oO0MoqoExwTj0gm9A1GHt7uwVH5JSDejTNKK5LN6k+2aFBGVwNHcYIkSunCepbLitIYnKzueCtrvvlcHG85nKirzkmWCTf9fYlztZXfyiM2RKCXF/wxPaXe6hs2vEDPe5RtO6wBC2plG2w0pDbMkALGvIiZm3M/EdYtwOjPG3M/QL7LO/wOGsKpgs4UtICltco459m4fu6Hv1V8DLnBMHjn3RwS1oz75HJYsMDbXV+ikR60QIl4aVSynVEYS0fdNQHLZvgVooKlOk17/3vyH31ZxapxqoKXkjsnPxrkNTmxj9bExmfe/Jir9svRY+bNWM/BJo+k+ct17YEmYYF1SushXd3jZGNZl2G/uFvF6XO4RJSPEab+dGK4fBF2TxTiN2LWym+B5upktOeAy883vtynr2VeGuEx7cUMfN5hyxrrxRYFAEfvbfaGH3zW5QNotFeWuS8DYLTs4e2nWwEjDq2M7VjGd2UZxvQDFUDVra6ibT05NKF12xegUF7pS1HNPZugXbeEu9qscv7FyKBH9kcuw34s3kH9AKrJmrUO0icPw3bQFrw4FEkxe5noE+hqy1uqQjgjkSe13EyCV2u3FQEMosSm/NvuY/BEe78mb6SCz/kebUFsKeShKLv87/cLvQ/8+FEYtMjQgPjXPca5UYz7+PC/ZDLet97Hd2jKAMexhnKDqVQaXjOVsqeFZW2YOoBPW0RsXk9fVBgqszJyp+ylLvK7bO9VAVjI87HbDCHaXnILgC8qYQ0tqpOFW4DQoYIe6REa+ErKoc3YfPQCIbt4KTsVNMJhSkq9s17r/gWfglVrS442xpXwhyN/riVobOMc+unuUlieQTrVgnhw/wH/tcQ3AHvGtitxHp/43leo82L6egZpuYGoQRs4u6GKxp2bRwKArxnn3IddKWKfdGueHBO4JKoOYlnxou8SxDcsFoHj+8P4egIFHn3+c2cSPWrSxYsRGWSNxQ3Vkbp+ADnF1QF7ZOyD6cYLTCGQZx+Xtl/C7A7GlUz7qmiQa1majbpDMQrq1VO6srNvBtAXiTXeM9QwDezUdERHbz6tTVVOJfvPlU/7OJbd5IK9KkwBCF1QkhPGyCLFyubLDYfKdXsRdB/l3YdkGk1sYZi9aD4hpqXBHC1SsP57Sa6n2ddBiMlm0PWhC13UFbK1QAxwKHHIRWvuMwwiWu836ASBkSLEdy87SsVY6tnFxpbnXuTU2Pa5ktYIvPt7iqclnUOXjOfgCp5Z4SBudBCjExVhbZPeq8ThCf6kZX65cqowVNkU6+lgnUZXXdT+lDus08EBgWBG6DYY50GqxV/hrqxlMwfg0WzvFnV/72AxV8WIDCcPbFZXalsKcjdoYd0PtaGEwF4d/r860Qg+G/RiCPYhfpJqu029gZ4ej4j9S6tiNEKhZY6/2R7lEC7zTmj2deNSwTXIH8jJU6XuyFOk5MqcqNWgCC5k+TA/8RyAkvLJdtLuu57pG368nKW1uEk6nuDIbz9aTkYFwXZb4X6WH1SQH95iESw54EgTfA2hCVkftfSqGme0NaNPHnFsaAIsdeJWfmX3uqaArFrJCJHPOdyrsk64sfzemUjEwCnW2jif/7RADuNElHOVskqDIDHggvI0oFYXV1LaryDhLg2nb6R/vXGnqUWkFrfTYTMXFT//sdMj4Tg77J1ltloutLYXzjGt2+wB73XmE62RcS7ILWSk5aK0MFOXMFwG7dRhveQiEgEuUlUmgfdCJypmqpAaoMNvrjWrCCEf5hcRAqA5nWECEpF6gX9cygSVrvC/mrUZqTKUj1Itrf4GyGPM9NwE+H3TWrvH8B2rwh/2Sc37EUY9Y5X0FblljLOexe9m6u5cCVao/nqMbEnhhJyVpllTg8iVOFxxGqw5R+FqPUXXOVVS6MLBgbL/g+Qi9+eDNZm1DOYpa2omCVTtNQwbVtWlsQKaIuSpDn7i/6q8nXWo5v/51MR5hOGfOjKJgOxGBz8Pw7cQLOblMWcNxremqja3ZtwT9crqU7T/NHD0Hn0OuJyL6REYSzxPbq0ho1yB4d/M+l9mwwEpEXnRvMovEJ6fEwA4+KnXOXCYCcFdk1BFRXclOQ+QjMDsq2eHkiA2z1Yjnr+XSJrByUVotLTruMrAf/4+e/lSxTS2xdaKihiZpNGu+BYbzq78vJ5AGbon60Vm80e4cmFVU0Eg6EBGJxqfacHIJPptnTBJly5nV7fPQd+BMAI4ZCDPCl1EeZrIhbFu61naPeC4NE0FRBeuLrBIxJTRx+c5IeJty54uW8Vb700UERIpu3VGIazGhyfypeCtdNE89gFRH1PIWUKNHzAik5+yCkP7TQ22LuvJIbbhJDYYKFDSgsUv6bTmr+vhv/wJbU8NAdDF3+ECTOQeNLPLUIINoOIiNEzfjIb4Hjar38vyUtV0G8v/H552G87GWEzcTh60HbT1I6lhiSVXaZLGMY8DEnjhhFc39p8SjK2+XyT3zPMpvwhjCJoGiuA71BfLWkvNTstBGrvf4z9Z2lJZM6c/uiRyib0ADEtPC4TmYId7HEgHkG/Y/Yq+1sT0o6ZFv2D7stZrhIRb+x7r7OjUCbss5A2nvpLFH+dzXOOl5Kt7ln4JWp+wdkewTLyh3Megq8cFmveSu3H9Oh0bXMggnVUwIXQs+Hf5vxze0BTvgQ8lAtF67Y8X+B3Ou8+tyRRWcCoHZtsHAE1OJ4sg7CoKlS5Vt2vvTY90G+uG3tbZyUzTmLjwY3mMDIYpRe1BPNY3M0uOjbCQAwTbJr6JFhj17ydlgBksuGiOtxZ7aysDvjIILdAz1VonWrwZ+GbmSDAwvbsU8IYnRXaVNY5P9aXfHj4AHi9/+CphNzSlw+lvMi4+w2Dmp8dCMhmq45VyTNyeB8APnekdvnIarINLARYKTXYrjP6ZxSzCLt83rEaJz7hOt5m0QykNxx8uzrRV+dpZssDte6jOgxqEY2dZEuTGKLBlCFAQ6ScMxWVOLVk/Et8GM0kWTDBpDhnrZQ8a0NaVtOgGZK1nFmzDrcrrA7zojmYnVGe8grm7DDHGVnNcFupfJptUvDy/gUniQh2O8nVlGjcnuVYAaD7SsVb4TQkiHBLcbfoYQKOe187ras35I/eIzTSDsfoy2Rr17Y+xt6sylZndLAFumlvurSsP78Fm3+uiiGl4JOVTkUl+YnWm5ccv+AAxWIddHhBwlRSRTfbEV/ydZu/GqZpLzHNScz5N2ZfIOI7XMlYm6EIYQutOuAhLt/O41zediC6DUTjzYPUSQrOEvbQOYSzhQs225E496fBa9EZHw4WZU//rvEKx/Vd6FH6S3X5qGVkOrjPDZ4WJDH9OXr10pzgmwqqKWga0TwGhybibOlXj3Jy4DG+X+WjplGzNz2Ay7nR9LlzJJfS7Ko42WCosm8O85hbtVB1HJGq6WjRj+2FpJjkQYMHee0bNkBHHXFOuyh2Si8lKEPw3Yh7uTPQfuNfNeWUwLrGntplGjJ2wsrrzQ+jH34Q9YJI77LR0DbOMckdf6L4xtOFi8mRB6Gv8Ca/8jnFQJ0jo+Xdz213IZHQBz+CE2ve9dIH/OlE11IgSV7aZqEQs1ELx7qBNZ9oldHJ7zy13d9rPyR+8sz6OKHwVdh57930ObFyIKRMFd2RE5p0Lx+bO+tftrFhkktRAXJAbE8r9eTlirZCH+nKJ3UgDLylzZJuzORbzGKjOqfS/mnS8gwN+VqTPwpoTLG+HgGSMXl3ck9dQcksqYCT18FTQM2V+vC3oTCPRcdDoVg4vYQZlFyRXm+8RjgygWSJdTf5gy8FuzDXsJSPnenPBId92wH4wieokDOCHUruqKMppl7SOj6WiOhQhKlXJ/c5qJ2oJwcCVWb6n4wRbuGZ+RlssdgsGO2k8TFoJSUCfnGi+K/+56iedAoR61ErgnSiHu2yOAuQkBPF+5GazvthLl1VP6hga98EkfXkscDeW+aWYRwmm1LHLxCb/ur9pWbtI3i+fUvbOV7PUuf9/5PKgKNZVzVm3yI1lxVleWcmdVNikGM2uxDUeU3q+1Ve3cennJr6n+BBAClcXQWUIMPsM7SGYw0EUMJpAZMveEOaAXyaQVBP1VLqXlLLvQZJ9Mf9elSQDSKVBMqkdYzRUhO2Q3w5Xag4Lqq8OpHB1vyRqB9wbyo6NYmrKnR+S/0sb6ah0xBmSfW0c3KWqYgrW+RuJyu6Gr6rw9gB4aveOLqxGIvFyxfsd2NvPzQR3m/kQ7FiXoj3NSBtsLXjhAm2q+ymyrNIhtond9LTOFGLGUfQkwpc1EaIf85Fzstz4+vSRpEK6nMYmbhVYcNNENiWZ75IPUTqQEz43NoEa7eK7kp29x59/N2ICbNpWhdGTK+AA/hf08KIn7ThLqw1reJzCPjxc3nimvEeE01wIc0ez1i6JU0H1qUbvuKBrwynYmu8bg9EGZcCBctplc17IWND9DqpSYxlgAJOzZfTjZHL//ytWer/qkxNBwhKnxNZy5nxYjSoDHh/1UJexwoFtEzTD71QMqzJmZs1le8RbY7Oy3FQcdunudHud8hHVw+kd//3LAZUB6WJKxQRjfHHO4DVQ0GPAzlQyru92yG31Q3e4gO7v18JRmwT89MMT8yQ8xdDdw77TM1bSngdWV1jmCVoJquBk2z8WENtLFy9sS6DhWrdNhMbJHfhj3rhzzMcZ2TGugt/T8mlAquop9vHBrUcosuKrstwHLa7v9om23J3ULOI2MmB0jDqyBgMkfZCwvTVQCOKMY3DraIaSuJsfVVHuj0F+hGMqjzswD5+H7KzAsHTG3WmjCB5ywKz2H7izRUCM0h9BQw8yUXLBHxBLGnVp6GB6A/UccVhgMrZZ7lV2Ohr7s6106EWAdlLETv8GDmzV/X4Gcx//IE3ZpMQZqdSeMqnnFtqQWbGIte6Ih2WZDYVZ9Zajnowz9ordWaQqWhVG11Naq3CR5otOCKTcfWEtmxrJGGn7s9pn+37CAC7jO6TWqsU3M4YGoKq4qffOMPRHvpo7r1XSH5vUqMcZtRTgmCZk7iHRWH5r/UmOsjoOySIBwFWpnZgk5VvRka7lGecVSmHhGZeedjrACxbh2Dnp3gAzqu1fIBcivGVcQSBfOrDJnG5SXXrPk38sQJt1q3Aq3Q7/1qu02IK+kg7hau9Ti8OJdwWDacrghbdMbRH7Eblk1nayb+boXzSQuKviWcg+eAhmZEfet92pcvDckDcx04MTtlDfqwxbpYFYv2F20gI0cPQndlePnLPktUHtJPhOmroLNzfGSIF6RZWLcUAsYVU9nqHwegoZt06gutiYG8WJ95f0lFhj+Z1G/sXLIfLCYlg2bdihN+qg1UnAKffrA842GHv9mOABkxVw77D8S5FHr4US+Msss37zRhG5t9cVXAH8LtUBEsfCaNTex5/nJN2fK5bVWJIegE6w3VBJRug+y0KiVA709bRNc2xm+kbG8FJwkgjz4F9k/7wAoVnIxJzaBjKdqj6kGx7nipTkDAA94W9OErNYWNr/9buSDyg6zFIxlLMkW+bRBM2UApB5iZdl6FukJVNdl9qBwogrPTNpMWSDYZmU1fXBZ4Zfs1bAci79EgxTxxNG/X8NMFgcgsixV2Hn49/ARTSKPdm03LrYB5ip5zVzXIfhj3qWC9dYXvXnxyWznFVPYHks4VGU+EHDUTDOG6vBMtTuJFH79kEz0ogu6LJidHJNJBsAqjdSgFOub2HeI9EQHHe90ogfN9MC3BszX3xN4RRheSbflcEoUHnL9bfXi/ML2mwyh6t5HMETmqI84Dnjsfk4+w/mtXoV+SvTZRsrrEM5Cc30hK9SvgXwTtvtJ0okVCsLxsqJLFQsDeWzqRRUFmOCVZAeRIyIWMSTW/TIv4vYcOkaDt9gQ9JuClwIKNQJ/nYsjsTb5/1KX+pw3wiMwpPdm4SxplJd1HI0SvgqCdvr5hX96xUvODqHMBQNFnH4pG/EHjEQxo6sCUeU9qebA9ow+9mjmQ1h5HhyZv1gxiHyXHRU/YhTd1u7I97oH0gsm463IZbi3KRvHjI3t9KvPoMD+W5H6OKn6lF8rq4W2E48qV4Q+FuxqWLpOwAJQLbpK4zb3JAhgqQ1ZIrL6DKzvtUa/w7FrSPhyUgj3oXC4Xb7urDrd2mm5/MmbmyKOUXz6TEQBD3pIs3ZofBGuoua+rLT3kS3OP2j3ITFgi00/Pwwt2VhooVn3ZYZ5lw6uRSuZHRRyTeCUb9coYbAEU8Ii7KK3qX1C3cTRFAL0sDgZ3S5C+qpSf7HHanSowzhUU1kycMzzEGP4L3hgfsOyPx5DVOHaEcZUCB4AFN+TboxetN7Tq8t7tpiHHBaXPCPWfDYtUmxvK+91PfSidSce7cPaTWOvXW8pascx/X7g18uRsw0Lp6Z/Dbns2NmbUOEn9WZKkhztwkILHJFdnYxOXFaknahnHmQBW6uaRrP3TB6Bbc00caEsaMTpSO3k5gxkkgZ0B1N/fe609l2JrMG5xIT1ELU9yoKzkT6jVvYe0GQk8D4pfC8bsOi/K2SVT6hd+1+kf7deD1hW9Tejz57sWlexa18u2C/fE4hxt4ff4RVvr8lkHqjVxzoe1ikge4EPevyduxe1S22Sjk75WLfGgP1ANwjNXmVhafpsT0TADy4AmPqtruQ0dHlICyH9Z47SYvXOGiIl4cD3+no89cOx8GVsx1TNz2NLtI9eTxC1zB/zwncJS2efxqUr07SbAaNNcI1h3+0vzJWQPqIL8PbrYLUPQRG57out7p6Pea1Mq3U5AQ+mW9RjRTWRxt8kxR0nnssXXfB/yZ461uXErm/tfv8s1LjS8DV+9Ueyz1i/Wfu6Hm5B98atfMHpE/cK1+m8KeS1YIiqk+cjkRAseqfZTDAIXMEy4t/UPslvMNaoU7Wvr+QINa8w4R7YJZeXCo5iDq6Ptt5/ylfBTnvaT1i+dWB+tYFIMRj5P5QfRv01CcOPtkn4wcS+BpSF3Atbz+dqAZccrfiTO95GSi/jrIE6wHNjVeGsOe5LixPsjAL45ESW0qPRv0KAR+wLhuglR0BUILI/KMtIaZ6fBXZruqRmhCfeqq74i98DvArkQUbC25YoE9eso56uzT6C5wpvA4JnR5ythTpeiyXmc/H0HTVSTWfAJd8qqTI+HAQCjE3kY8a8BaFFq5B9w8CfEGb1bRqtQa4KomqgBflWPsb9RyGkzC4xw+KrKMOqypglCyRBilMF0KpfZpciXdLBcenKpgQVprboDrf1FCH5fqcMBaweWPotDNg2BEBwP0VdKvzbHIGBrLX8KBF8R/UFgjrn8qCA/ryGKl3FcgWvRW90llNZgxiOYS03MR/hS0YtGGJGWpfWz7vzV5tbhiPRtUVe4qN2mGEXlD8N928mRfUdNs4YSHDiR0HmcI8boY1zlC9cO+WiUYNPeWmHL9F4mP4bkKZedqH8ATJ0WrIKW9Py51DtsCcBXfA1yz1xQTP3nG68SWbV3/48ERHW0Ns/fl1u6RTRmMcxBJ39YVXv4V0Lclr3agd2dA8OXidS34d8XIVj83UCgcxQFh6lFpdJ2JBJm8CmzmBZi6PtqVLwsApfiaGctfp7MQ9epVe+ncMhSXxLazRLe2ofIy3J34+grN5ijeLUiimssVkcufQTP062hPtIB1cYHLHRWvCluxUsoQ6F7DQnbe6MIBehFfP/5SGP/Etsnd05RVhfYK1Tw2gC4lV15Y6fJ1HxZ+ExlB6jCTY04djf4iXdKHOJtwwSIdqWZPyx/Bwb+ltNYwdTe0OXNT6BEhVtRCOTYgeBVxWQN0EvO2JN7udv3I48zLmr669WKDt3aDTwaVS8K175H7VoZQAlNUpAwSePhoLKpo8f8JwamRgsl34jf5FO5CmRheIp+JnETI8SOkCeiVdS/Wo9zhcPrQOCGH8l9cwi7hTYQdckdb7VYFnshHXbjgHf5IJcp5IX/fPAHbFp5k4imwS74ibVzLfagd0gFd0dv3Lkh+FC3eBOGnjgxWz1oHK181mmTFkIlJjpol2t34GBxXWtFxSJ6dQBDGcQcvT8soY8epd7nkPhwApi9ouwbel/8MvP/pTj617u/E9+c9TrYWh8+pNvfGrekbFIxaEKGOVtSsPVGRZaToE6RITJU2VZFx+b0VGPkGYGW8wPcpZlNdfxVTxQTdsrBCCQn2x6AbearP6ImPR4RRnPUf6UwEXmKMckGXNLnEGMgsWtI6Op3ihRu76/Z1kTbsCKTstW9dRwu11SiT92VVdlFuxn9CNoXfWnzm/z41CLjAi2WqnTdT1Sr1BlJnJI11I+l8oslb6GwwWq00fgOdPLda4qn3btkO6sKIEN0X3OBkyO+KJHhCSVFmphMnVieMgcbgXfSoLnUt1lwvBbeJ0xQiHyw0KpIKQEdcls4t2hp00P33a/pvkuKYAlVvVm3nuiS/APTVMWgCVM5PN8TZRp83OatcApAJIBRVSTcnHBNsKJQDxzi0qdu0nHpHkVkO/EZEZbu48prA4h5TKiDvdZYr/Q7whi/AWhwtMipmZAwWEusFXjxVLrEyMuAli4jdd+h2ubhhuy73pPRKkp5clY98IVzCIei8g6ha/oySF/7GjZixYPRnxH/0WJuMAPph30x/3Y6VpK5cdhA/e91tO/ATwMJu7E13CPB89PCkI/Y0bQXgiOikLiS1CmZGNqz2skJKUuduMnCDfaZBDYA35UrJqdYsWfBvH2ykACbG5GZwYe6o3yiCH26pQNrw9LuYHqiIXf7Zyj3nu1kiX6tmb/TS+AwBeAFQJ+c/miVI81DU7TuNoXOt/in4+lThrTEYOmaT0QaIrT47QcKWGs6exgAkzaYJ7nlr8ASOkTicRn0TkW9+ScIS+TtliBwZs7eQ0ngjQdGevOtxS1n9Rw2Pa6noaS6y9FhtQqhmFG96gtmngIcOQ74YODvE6W6Hfk51LTsIbEw4W9I1WsncJITD5F7HKH0njbpaECuEgmbj2dm3qQxlNvoqv8itsIoeL5JL296/hmJaL4Glm77eSws5bwY6pitQWv7h7KZ0ByDfpDQfmwfV64TPjZ6qKqQXNJ5Iv46C4gXc/tmWdhHdBxfhjEMbFLLFMrO09oN832Ltm8Y6WwT3ubEOCIxCctm3yYtBZxjT+tI/1yuWzv7O0vbubt0nLiS7jrZFF1lnRoVpFKJILP1zYl4VC0kHegFfW9vM3fSQqAlPHEbhK8ESwyB7Hg+ghE8BDPm6DRE8N7TcjEhgghhwM8J/2//wex8ylv8DnpLAbZgMcvpesm2F8+/hCELKT/mwHTnPu4Vb7/yJ7mPt/SaxSCM9Mm/M3BPiw4y7pqiJhEa5O0GxFSAq4E5B8lk0XstkzD2t1/X6PiEOg1a+wHaWmMpF2H9z1NTh7DnXDLCvw5TpiNJJLlFZg88OeK/ngb1Px+LMMeHHQ9hRhfNjiLLy6S5eeo/Z0HxVyLvdX9f03H0WJmO6AD4Hu9tF/+CbKjkOCPQjEOes+sHLHdwKtcUzsM2rxjZhOS5PKGno9EtA8/wU49jMYWjYNPjNsiIQ6XzhMVHSqXvoTSA3SMIedg4QhDgWffSq2cNwoDr7tb0hjSROfobvO7n+YYqpuRbmuRkT+PLo9f1AfCQoS3u2WkTcq85Dpq3neSKxV3M+PMXGn5S0Uv8SAnKt7prnCkiX7CEp9qxjXF4p+UTC1B5Ii26vJbWojrgKoKeoFd/aKgD2386wpP5tvacR+Joh0cmsPxq0hMo/nvr9vUIYmq4Hj8TOCi76UIQgdRcTdvw5zrggKg4UtLVGgaVxpZJeSukLzg9ZoOVWmZYkAtibEKvzbLgZFmrfmYWiohVLpxK/oY0FjYYlQco2MCmiV/IINPW1j50RkkXMU6VNrKj9RoIpoQxkWaTPrGF7jlDgK5bfcw9MSRmy5BCBXecA3Wn0NlUnm9qFvUvsnA6LMs0FQLX269oPGpOaO5sFmTxlStlbm48uXnU+frhE1frboUpzXuFRNxsEWWDGujGhXB5+08KAFcMX0cgYXu/y1p0hShcQscO9P3Z0qc+CZzsh1sMMcVfiNOyhRto0h+rUPz+CYgdDMYWVwWbjEaV2YXxt/tN6nXU/r9Q2aSGmbdq60uJwBN9A/lKkqTo3CBq7BmFSQOu1pJkaFzM3ep7ucV9Fmt4LrVvpYLHratS7g5gVbMOHdr82EN88YOtgDiM8Ri5CW6l/fblFzz81MhJ0CqtHHh2y+c+g0wyi+n+fh3KYoGsWJPv8aBahxRWB7B97mYkpMd8kL1dGNeoNEhF5EruCincEpUjKuQujVQYsARpGgSchOCyBbNi93STpOR0QswZW9qGuqePATA39nvd1QdoLCOVbfl2YMFzktyjdaSWoBz3pftI+E38nMSqSY/eNXjwtyqFRnqNP+3gJ4EDXLX4mgN4NNdbOCb1oeQjb/6415dFmKa9sN2eG58hnXLWsbQPjJ9Cylmkz7qBUh7a8yvJhyddQN/7LJAfnWQCAxMLqO+IQixRCVm3LKg7B4BIF89JonX2yeG0CrXlgWuuXAwCqW4X/sLVFS99EWLkhMXheRXJj7mRtWia2eyP+JrzcugUZpQ5dhJja7YS6Mc7TxVkGLAl7x367HDMM/fMfxp9/cWNGEXJjLesl3NIbjoWyt1w3+2BDw6OMmaRUntGQBFl2itoX41pS+geEjQLn5N5nE/OykK4VC582fxaqjIByPoqv4BkninZXgAo7oHwdu5JYG7X0i89qwLFRasvVBP9tr6i4csCtLAf9ywnydYB9B90liL9RosklC4uQMUQvMoMQTHytEN/HyRGmLYqc1v2L3P0QxL0PREQx7GlJ+In+HHolZr5wiWnoQiVTjkLWrWrof3J69D6VjMIXlCLLmGy/FPo4KCmvBzZ8wYDJseyL7em6AbPiuT1QeNFelJctD0TTDRWyGu9+L7mupAbzwV7DeNG//ti4i6HeAXU1HUAVhBfL97XpSLIjJ0KpLB4PdQXrw5Tw4T/pI5VfT+WOfG61ve1SzJynEDcoBqK1H2kKSDk2o2+lpXz1JGGuZDWwPW4OGEJjNzJ1AkZfRyQbxQF9CFTs0QIuPxJUXGYTlZUTpxZ9u6nUDgL51cF2N1mg8bOFZhqsjvVbawWsBzEVoJE/KnoFnQy477X6DiAlc71oD2Gy3kDTNEDRslT6gVVdNEJniDk79x8q+rPU8f/ufVrdJkxDps3Lj9GwFKBSdN7uUekhtC2uzVywlAovlSQqR0eU+JRb+xVNtdoFvd/mtgTYzdiEPkr+JCECW5rz/MsbzimCzgR6ZdZieJR57M5ad1urvnrgyKfkDMBcZaLmoLwpf8Z5yJrzaUyFeS7/ad/urPeJb8Wd4RoDunGdUrAphc4cTkLhlh0bSWWzI+rPmxPQPI6u56wNXBaf5zwrNUQO7DdP9JgLOY+I9Ao2i9a2DSl+ZemWjToLAoJqnZIP5hxUqu7fRcHvSdvPZnOhQXU79BoxelCZ/yUBD/AAfGc/WlzPDu3VKUABdpgRPLPFu41UY3XpzEED49cBp7LwPYpliA31qhXihUfQFW9KW7KruOQi7BSlPnm1VfsIkLaWpFDWYI5GKwiedl0a6OBvLPjENjNL6pa4sQWlxjB2j4YsxMKSwC0cxDAAHYHrEQCAhmLlnY65LL9lPSKL7iBpEgNjOQ1cYo93VQfcFau5zT66mcbJPkrAQccxLdofGKrY6u/p1uwi0v7z0R37dFUas+k5g5eLuPzMGUY/3aTXrN+8vrW/yucbXHxZ+kBgiN99IEqax+mzaljOKbcaQUfsNSua1OONtOpkVGaobkrNRHdJs5NaB9y8E48iNZi7bga6aULD9aENZRB++K6+Mu0nHDOEjKVuNr0YRBuS5fz6qF/vDl8MciFnSccqmdncoKXPg6o+BuTHTyOpMAkBZXszG+JHIzGsSaIwgIyv5/AW16mRxGrtgXagz0+kCGNGF6kL5/R9Q9E/7VvcQc5q2zhPLjlXiqB5tVNFAOZiPU4YWLZpW1tAxQKLwUm3toUduaA6cvdcheuQKjBJityz1mzdoxTZOpbkcYXNP4T1FtrxMztR+eImz49zeBWilSf43cQjTnkQUv3HctyL4WJviak1dVqiXYqLVf99EFDG8/R2bwhsmjOpyLXOhYNaEVaM5/pG4QbgBrT18R76ex4gF62Ix1hPH1FgyXRldQn4q+pJ9+hSadhgly4+4REL5aPtc6J+Qga9FQg9Dd6xv8tOQ7mrsg//zwpPU1eGIVSUEWhmacCsx9AjHrGa0/TUe4GgeTzFQQJ+7L/ILANdjrWGGqi8Hqh3/NMuGL/oGVS3a4X0w4uICSnvjS20KsdiyyeFQiNU3p9/Kj9oMFr2oL/zp4xhGUFEYmDnOB5T2dC11OWPcGR9HXab3xk9KeNJ2/6E5uciRHYWP7wlgIysvt1lDaNBsSwputZ4Hjuv+Xkc7T3x/F4woJTCvbpCE4GXQNdCUUxbsuaaKgtGeN4xGQWnUQ4beWzhRaTII/1oIZM3fZyezsv1diSRU6YkEseqhbuWOqL6yaXT8wkh7yIcipZ4sSJI7q3OremGP4WWH5NT8K0BKyLVFjwBSvq1p9yI0gE0XVmYZSji10be1oO3ZsdOIhP0vamjK3o5+YFPeJiamb/Z0qU3amqrbL562+gKoZtdHFZz1wuJKqbohlsP4z+mCirBNJU6U7zpsKxyM/f2f9HIciqSmM3ZUDVQanYUwtk8/TyJUNDgCHOPOyqvFAfipm/ftIc8FMxWcRmKjdLCijGWO1Q86Yymjfg+UdpLCX3AtZqrTMmgeOa3flBigNnhBFZB8ABwdT1CGvHaNF86w1xgS9HWjWY/i7KbGpqRLhLTKqwr3BVWUsW1gXdpQI1/e89e1r6fVAPE/V7y1KoTz06bMpXnXkzHU5WSRbZLcVv3BJvrwBl1qRoqTG+GVEi88lJNLNvW3slohMjeU5Eih5V8pOzC3gK4pUzJc1pPiLcWGgPhPmJP7PzsxHe85uEPv1zsfsAfcuUTUQATJ4LzxF5zSlO9xTSf/lFL8KhjJ9Ae1eAX00N2IzVFIUUwrERA+GAUbJbw6aZZDMDMuEP2PDDcgw3do3ZT5HQoeId6UrMtr5qj+eNH4B7XVZwjORHL8VEShhUP/FFq168wA8wKb8TGT9/kTUPsp0Xijksz9w6lhc0DVIVD1592KugFon6wqSu8tHN7XE/ebS2GRXKNO2q04oRzttdFPnT6wR32sRCsHtJ5S+tPxKo3D33H1r9lzYYLJGiiH4M/CgRn0TrGeQutgFpLYCDKDODZSyQ+al8aTSRF9E3ZVb08plEMiSWy4BSOh5vVMdYrhW9fCr8Xj1bIiFhtMlB07v6svd2hLuVY3FKeBXBw0AI222XF+FJh5ERauAK/bhZ/3Sq/LDCOKRRaCvpXyXKkrJbMKHsxpF/xohYb3iYXaD50hR2GWbJ+Lo/eZcr0cJ11oDtGPTGDLU8sm9iPQVAGdb9CV7ZM5CJX+dtiaeJInbWdtJdM//Hplj7pR1nqTr4Y94Qi3uk/WjaUtfOJ8pugyLUomysPK8Z64qpAKRuKzpi39YlPaj9yFaXjSdVepz3rvGFnHA1Wp5ygpuiKJ00Mj/edv8ctPtcDi16emBxx9hp6S9T9E5LoNP3tnhzvWJON2Aae2YC39I1FMvT8YcsFVN7JF5S3a+Capu43rfA51c/GxRui/rJrMJBW0x4IAdlrKuOdtfIPoTUxeIb0K/Oq5fTE0v4A01mjBBA9iOuSZH78jXMU92zvaIbF8ELUdsV6bNFmvmhHDzEKRYJ83dZEbHIu4EdE6YGjZitjtVzbE1SeFAq96Md7DfbGYgGS1Y/ma63jmbQcWvrm0kKmHSkwmDTo9rBEEUxijMoVWTOydz73M7PvwN4TGkT/iBeGarRMOFp8Uv+fSDng+JsGT59Txuieyfx8F58JSajE141nzGtnzVp7xV+KcdyN2R8j8jzxLZfOJ+iEcB6mlOPgUsfYI69rr5f+LQN5gjNL99cLqe7qLYkPpKLQyfY8K6kKfvWnE32qXA3kTmgb4TrMCAZ09E2R/MrLgwxh5eUvRgFdOFCNBURrpHUQ77RUmrpkmmoKLBk4ij6e8h931eTT7QEoGhridykGEoXarAyp9GBVO2Od0NuYXzD+lXk2+DvP0yLMzed7jbn8dzh2Blf7AGmDSNvBWBWMSc0mgmSa0nhjwHKMv6Qe65Zxqd+bj20cxqY5Ez1s68lyckT776B9KUgfjbQ8YAVKc/+UH/PmTumF7/thCSx7Ud9bOoaNHXgvRj/JeaxyqFMhnukdlB15lJkOGJc/c7hNnRWSGVi3dRZdtViPbxZIMI6uqQ+NSFbXRU68JKJPJ/b3XpX6PtEIHwyY4KkmwLmZOA7xqhhM0SDBS5savoAsAyp9o2YoQRzjdbAYujf7yM/5OSY0PDqt2FXQnT4+5a9snyLFmI2ImwVdvMBeK3jFvKOOz5vwIj+0Q5p6wpL1o2bgKYjLs1QJTxgT10eIoOAcYRHw/7QQ3bJk+MR4ccyZ3QIO5JvH0Ej+ocEiGDMDIVtyIrwYSIh81cZaFFHdKa3EYnowNoXvTrgyYNhZb5QihMCJC2bJzke1GHIpkgrJb0Lb3I4SOQOgH+cDwos6WvJAL0k9N78M//W9c/FvJuXPgqqstzYv9Jcuprw+9G42/O0WfH0j1AqPrk8iWjXoU7tZOxm3Vze6ZfP1CFDLfyc3tiq3edx7kacVaOXrooCUDSmKC6c6jjN2OeSR8HdZuq/DPvFfONPwRJ59qoaRxUO7AjFG8uUBg0jQ2Q9Ep+l3fErPd28UELukd+F898WXGgzHyAyzmARWn51wTDxa55UtXOG+vY1qCBuhjCveVChTPMSDClcelMJHSPjYKuA/73ExNEk6+jCMqdR3DC9T9WNvQ6l8PRy8j5FxyZ6gZggxl4BqlBziiGUBeaDiNsOOGPOVHGnuGBW07ddmVUqe+9EP5yMV5VTFNkevCMWjrqp78mF3lVoLn+UJSDztqDrjfVijyW0AOtTznmw3dYqP0oNHzPfQdys/7BmJHVidh6taGQfaXtK1xUBDhUK12BiqaUePZ/Z0Xi1BtEKxGCtnBedN6Ap/MVIzTmnmUiB71PDAtusOftGiYJ7KzHgZYlLoP93L9w2fw54WnQ+5Cei/gi6RBR1hk6RdPnf2jF9V86AqFmUUhU5LH9tn+SmZ9sJGbisj4ZKpbEVwX1UnUODO/tn+fZRVDIhnwnIOP7mtvm5crfE29BzcGwgePt65OLWR3YrtseMcdFXV7gJp4don3RR3UCYJCQ+Dh53kycoumb+7WKwhQ8hXChDox7Pw8W7mKeU9S5wDn8Xt6R4W+D6HQnZ9FleGCl36OZqGJeixeeHVsv71Y05PtsmCJ+5DKiHro+bvmNfBH2wggViFuFv+pONuJoBumHpa9IcgFFkzy53MIgZGv2skfyhsikIPhXXpTkcmtrvDwlL6/XULx6Cg1aI/4PjoBI0LJP4Ja9IKwvElN3BqJ2P6kZ3u3EK3MNds6KwEwavVlJLbb+Oas8TQ/zEvYC6x9KbdicUC/Etf2Y3caUzjDwohmJUubBtOTYakeJrpstHngmw+V0ffSp69tmqnXZPMrjukmdJ+HaNUpCt3JUWs9gyXbAfh3HXjyZMMQ9cQ+833XN+eIL9ToUa5d8nHnzdC+KmO+94a67En25RDlBMEPF3VikOQ3hqNnMXYyT7TIz37SCuGk0UDecj3Ef69P9XBWaq/eaUAVVO330fRyT1BH3PGw9gF9TkG+1StpuQc4gpcV9J27UIcgWu7E9jdmjC5ob04cXjMfg84MifPK+hx6hY9JQjvqPncSUoE2exspLFtuBdNFiqWu/X3LA9xxdSxxU9vxkgJPcpaaaQxVp+FFJxu33lR1Z1vB/Uqm6hxQt4FmBbtKclxbaHXdC/bovGJ8aIgakd/bEEX6MYyX6hmq1kM5jnYkFzmfhOFEeslTPHZX0MzcAXVkPNgw2Zyd8Wn2HLmSgPnokmxJoxz9Tq8DrR4MAYKwEuUxvXFlZygUlvHL+u13NNwoq5YwzeELMiKNVaAjqfCtvPpiv4NAH3Itr6EY3+aTA7jIrHZFZ8suWwU5Psd+aIfEQHPAnfqFQ09dh3OJ6MUCgxeuLuOFs/msNLOalry07RTPCYTkE+O2jVpDUGTNePmyU6KZJplMykl5nB4QdiGSNDu+8np5hk404AkjZAOUpRt2YHEklvXbjk7MYp4Ne6iusaCqfiO+AUklkalpQDVxVcdhLng/ZFHChkJdDiVWGReofRlgJ9aLSpf9oENUQujK6CJKbUCn+3GdAApIgqgl+IXzhrMKBgdo4uCoA8iJf5DbytdjlvYDq2jjCrXtkzngTLEsLsSq3XO1recgfntXmiELMFSTtC0sYJYoCSKxs3lgH1B0hZKj8KlfMVQ7KhSyHolwLEgFziUGxEwYcIVdalzpceS6MIa4dIeIOAJ41Pvih2breHH+x4MlmkwoTNSN9ELnSDTEKzOKVAmR6sgPMvG9rd+M+aUsUfeJ95aQKjVaADdP8P3sFgBN5E+wo3gKTeTdxYmZffn+90Msb1RVQd08ysL49CkKugIq0MB1HHYGsgIiWAvdPiL8tNUADvBZDuQWyW7BpelISK+mly/CnBiTbESmSsqHxjnTJ93bAfx2QgZTmLcyTPUhAWGbeoMiQs/Pq3Mnng+NJLSqzmeCVwdUhCBRRnnlEcdYEhBmEsQoiPuHY++7XoC70lqgodFyQ+X+xZg4rAfOoDYBlc/VussULvTkZUWxw5XdJlaQ4merguz+IlgQ1YqAPqnshL4RsPyxb6TwWbWCdEvx6zSygu3BKoPbIL8GnUvXcQc4An+/KSdF2NFbbxfT1Uso8Jg027jaoKwQEMWQAAp9cBWJC7IO77t+adFzo8P6CBpRZQjCfoVDFPlVS4RHMz4ocsgjuuhaKDKE0LFNrX90xtdmmYvAJyq9uWIJLwf8LNxACE7gG7PCc1osKEx7D2BPA4A3f2myUqaEtIrSaKRPtEYOOwQ0dgVXjfiWA4XplMWwXs7Cp2c8lLwo1xuG3di+gv5jQrPuDoBNByB0/5rhzvFxtGyl926rCsySEto83vq+M5DGMgW1CNiPuT6l7IcZpr8Oq8Z63LY+MAM2zo9IJV7z6ft6iPY3bzpd4hSQdsh3lOe2FAqyZrQ3FSvq+wNzBn920RozPz8trZ5uMUPaTJ0RvHAuphxZv+rgJRGSODhys7YPyUwdlqd5Qj1McBCAtXMI2h2UE2NC3YmyAon7mRR0ufWh6IQtvibRawkA398gDizvdW2hsGN0oLomvGJG5mOwR/0bFOb74qIuTtApqks+0GJJqE1bSYZ4XKLde2z87Ro2JTgAST1M7NnnkjglW2t00slufXsBjRES3//mE4/y/vbNWIGJ0399k8oSOTu+gQw1oirxcckyKYzUqbZIA0aiNpFGJKyKXc/3xJao4o6HnYBHyRXhLMnM7mliF/T12mIwuqqr6nsLXaXlD5HeCDoP+DHRMJthwINEJAcWPSN/N5MRO1bs+7e3nAKm6ITe51NrtehafllMErLOoaGrzW0NfIwACQp6COMD0Os5/zAgzxrzLDCPME6E3NyWsgYWHlxT5Ik+9IxNSwlHmA00N08tD8MIPgOPrPqBYsS7Wu498yd04Tv1/Ih7KgRgxnHXnXQEJoOgLfPX1nQ9U47mF8Wy4eRoa/pXJ4F60VBaR8KHlXeCLxnVdHyEZapPiRQ3S82J/V8qtzxjc7CTqmGIXUYOi4uBj+rOTR2lN+0aF4z0wL0Hic7JC3M+zPLlrx7wtxLz9IOpHjEZFdQkc09chGulTy7KIw3t6CgIvr82/DzlrJzaQuuZA6F8me5JdYROznjK4SmANBsSYLayWkEBJ9gmFdNsTZCBAVOyLFwbif4Xlwurbj9cP34tUn97k0a/0+/jEsJjS/KPhTxRrXoA1gUf8adBz9TPOQCP6HhJmG33yA+04lVMW3aE5OVhLUuNPtct7qbDMVsXteqC0ySzGeaYYTQiAW5u4CM63URzBmBp0WmMBgxxK0Dy+P4aYT+8JHjQ3oTpPcJ0/yQZY+NLSapLwiwGYml4ZMwkWFnLvunBzhapMN6R9kF86EvpsuoQ7FPHVfLYqAunxrd4OVhsEuuy0dGMIeWUQ13H3hsTHNCkmbCHK4iRFq9ee3UNLEHWxoCmBxpUS+v2A7b2S6kQg5+81rAmM1/m/hCrNZVO9DmJjn45iU6kVbwqyP9q0nZ+14xRPbw39iQcXtHDrGmMSfrMa5k2SubaNJFzddxiki2tr740wIXvDrlpR8doPFe7tANTZcyA2M9hVc7bmlFmeuHWjeTtCDFTS+KgKC1c6rjIDLOOVG8fxtu5LFXgO3+uCDnBLHa8hwJpxWtnHnbx1AUBSofU4GH9nYENlvAEZld/5noui8+OvJQGsCpijBIFDVjqpfZE3DjqRW/5E67jJrFpkbBCFHIUOZQAl6xVzrqbe1aPL+NPp3pzUyTq8+pkzSIeCC6jFKPZfYM2lgHILX+qNUwixRtpZ/3kRJrkgM8ZfvTzEKpgsKSQ+U/lZbrTEiXIHn/5odWvrrmPVljlAuGEd7903yOpKh73Iit2g+siBo4R1314ntigOT/Ad6fudthEmDzIcIjdFQ3v5JUWhds0BnP8OKHwFnic6M2LtH6OMvUr+S7lFX+L1MRuGgEHfJVMIhWu/VXwVxC64471w0Bl5y4i+lx5yk+nyVYVOsrOkCldU2JHOxTTN8pkZ/rr1Du0VudQGfiNrLm/n6gk9cVoxeIsgj4VD02UMSr9UcnaQCdOB71QI5Bd/btbJ+JgAaqRUTIbbV/bDdoFqyyC3Cm4GdJwLEHWbMeRHXYwKIDgSkgJqSbJwEP+anonv5ohB/+xjIOYxJ9jOMopjs/SsHUGUlHVojiaQ+mAhrDhm5qxJf2JI82Q06O6Vr51pDJUDXR1SRwBidGa1NNSW6gb7Bsww5p/gnpB9xus8JsJYJtcnC0OpPsCzgjOCk8GgiL1dKkzjyrk069kGPZJJ+aMTihPPam4sxmiZX276ZhpuPU201ht5TWHyFPQqq8slnWFn6hrzLWj+lHezNuDr25dDFlCyUEwbAdQ0M3+LEbyMi8phYASfgncCY87dYus0viSFgdnJyfwTB1VTTg4TPsC6f+tkvOzqfFb7nd5QveF9XP7H/4bpOaSaNEYJVhuqTiSGFOAz6DG+uGNKA+nDZDxvFjJhFgjFoYGyEXjgsQE9TCd6D5NMuhJIAKJcbq2Lec4GhgDcNFVyzd9StzeWk7EQH39lgmkcecU6MnV+AOBgZ0VKEK4zrcCihKoUv02st7sEjfZUaL+r5+ETGd4IYcDAs4edl9ubaQ1W7p7Buf65ppxUqZQwR/yqtfcARzatVxl8zviaUevDwcs15vhw+H+zYu3HtSl4UeRUPY3NC3HQJFAiCKGtRCLvIXSbcQDNAELzTry5W9wMi6r8u5tzFpMR0kl+e/zl1Loyr52qufr19L+BbzoecsnP88SnIjj0liMl+1NVkbmw+f1pmZ6LpyUNUUyL22UWrjxztHb4VIm373WACLE/dxAPp5b/hZbfRZ2lksZypBZx/GvPVptr9DshO3moAeeyq/0YBkMNnUA3oL4lHwUbqft8GZ9kuMEZkIZ7RB0LbHkK3H+WVrwkuqOXwcZzPKlEMQWycbhvoBtOsXqiiufoK+zbL+JwQzaGxkiwUw9wWEqB119ZR3egKocriYKQ/7jpqPIxx+3bIfqP/TVjOu/QvdhCfLAgJ7K4aH94SBLoipqAfzejLS3qVL6CEpfVpG5n0h/YMRNks6WzPnzfvsMet4PF0MnGO/NOgH0TvWmN88AKQsN8wA2Wm48xNZg3kQxyFwCmNdspAF03K5qBF4XeH971imIR6uqvBLlDFZKRPuWlcUgywtZicNlSvaUNLgv0zuJF4CX5Zzg4vbt7ckLDuBtWedlLVdPFwGKlusZAuyEzCoV7ooh5C/5ntaRLpxOqNHrFa+eb3t5hgR0dxgi+4/Ja5THOTKYOXdZb6C4Oo8u3+gkQH4Eb6Cq8FAf+iO0ITvw1hVge4ICwf/D9S8hfSTd/rPRysTIUnCbKZf8PmE27RPrw53+x94OhNQWZejf/Ywr6e+q4q4c69000dCBwNm1DuyCSAXnpA3hEvf2MFnIctiWqVpKMHDm19P/yQaWbYhhkb3RdzZ6en+tMYnP5nn1Sblg8qt4K+S8QiiTm4VMLHdg7aI7jb170Nb6NUmowL1y6NVX0TIFu/tOzHi5qTEMO6qVZIIdxoX4foe13VEiAD9659n4XhFPoQTrk3J10OoZ/oV2Wv+Bo9/aD5/PTPbuT7iA9ddzpy1HrxursC/kU5+5ffCXGD2M4+nayoKNNOUT2XJy+80ZJ/7KpdpXsswmTYQcTW/UTgzraLoV7SUQ9OwY7pLnwX3iQBuv8rHLg4u4YQ1uRKhXpU2heYLiqmtO1ls41W0cM3/k9ptRKndmUsNFx326xqV1mvxCV5CXHQNJgibSyFRjysge+8CDZNmoePcvrwYvT+KN/Dl6Atstl8PevJdwfCQjOJZwB2SM2oZiC7JUgEpTOKHMzdbLt8qMlOAcFf0k4rQrIb4KK+Xo8+XH1tBhjETyPxQ5YpKPMTZUeqBmkzN8v+xIL93F73j2AX1vausp0RP9k8WyaI2nxPPRYM2Nz7oVrq/7FD6WjsRUFxS1sJW0g7mHhp5X6VuuhyVx7mgVHIOgfXWunKbd+F3Rep9/la2NLDaUS5Dz0f2P57Esz2oBLnyPZU91Ud/050tjxoJS5TAPACp5hkvqPyCfONQbZO/XS3fyVQRK+oRA+xCmbHmpmIAJ0fAaOx+KYJ5r/Q2nCoDAMx+jvw6t50cTMKvEqkrKKX9WdnYcP1WxDGqD3hOyZ/q2zEhmXZBQx4WfXNNAfkNPCeO2GIv66m09Mi8E6dn1EGAsxZ//41obiQOH99IRHrdU2nD18Z6eecjyTN924lPRYHm3zpgXP0WOr5Pr7JPdiTAOGcVFRj+fzI6uou8zg0hY1gD6raFh/LZDJi1lNC2RgVjfeR5iCfIW8Dde0eSW2BslqJhBlx4GYuLXPJzH9pfDG73mU489aq+WqdH6FEt/E8ZH8CDgcv+ndkbZo3IxB/Eirw2fownbqcjZZMqz1p4PnsnPSzT0X6sZgjunlLQonDlT8mf9aW7xaweLcTDTeY4deNhp3yycGO8wLt2xKHGssBHQjsnXIy0ZJv0WOlqhIyxKYGM+ZulKAy6H+8jOLawF1piUE/2GdJ3VRY28vGyaGXD06rxZqwG+BxtG4ZUTNePeKuViWk8mZQQQd7iVM2LsX4d1m7yTM9Na7D4yvRLj39iScsunw3aEiph0vfLM+Qwu2yaa/kC6AKBbwu2G3kkqHGhueMPrFq1dkIxS3+dBKw+wbS1Os/NFzlahXbN0ZueQFgiTFJpNnwoB7Byd8+WdbIpjUZ5VnLw2QElWX0AfPpe4VcEM2uir/F21RI3td8DOuwqogQwSeEYIhto79GCENOdo8jwDhxBqFzyQCCODJd/Kl6S8whvUI47klJn9puzkkTFnwxOaJUnEEDcXLCGRbJUpRC2r9644UCoi/0P6UUiPPLrDddzP/CHJjvhWBGoCVhHf9VV5yMz6wDAbApUVE4KX2kf+KliLcJYRdYQ8lEt1fFGcfAOmkJUSPApNZ+ZMXtlblNaiCV8BApDBOf7d6ettifeaUBMIYB/crnbdGMoeKh4SfNryPuUK7G/fEi8/kjFIkipNhevRusWMIGK+NqyC2GnrlsgQGQG0lbojIfLPaoyER9zgLyWbQYir98aPIFCv+2PmgR9et+RSsgz3+I5jBg0Tm+B8z7D4+rB67qSgw2Bnb0YAL0T5Q7bA/dXIGQNVnN8cF5bfqr42lHMGFEHGHZVd+fDOewk6BIPdTYty0yy5TzTKLr35WReFK/AUX6XmpRk9d0KXslyiAjxoiPI4R5zbEY5HMXIV7OTcMQEZ25/pv8GkC/Sv1NeTfMntMNfDOJSqgZeZjHK5v8mR1jPYa56ViYwGwFn/7RF3d+aOhB7uKA4lljp3Kiiv5lXsykVdIu92hVbA071hXQOn6XBH5B/aolF9HGOm5AgszzVh01V1R+jj8R9aFBk1P0KDISsp5c89a51Db48r/ksHxbccoK+JSykD72vyOrrCbIE/CpV+6E1TxOZcMQHzQnv+reZLm5KE/i+DW9dvmDqQODnYRoqcHjf7z+hKXALOQJHRDDgrYDljLCKZ+jEdN2RbjpDBU3y37ZjWVhPKoOBETneynKIlBR9D10hbYpiOhFH8szQGuEGvJzr4MNhc8OsLMrgyYDT9RCOrE04Hrj8ftfUsg0tvDcWyl6bRJ2xV34NOcRQ/c4fXY6ImCepYWJrqPW5kcf6j+GnueZ+DtbQgG9hObqqc45TqxW60R3qP/PjeJSDOOqEDsHwG3bNU7KcQW1Pos9Yq0ndSODyPED48RAkfbAB7qnmYcXDFpyOhGePF5RriWHEBRRn821/0NI3NH6KZlbEhrsYIUJPy9ValiDDdrkOvYghNVXdeIrvt2m4fMDjPqXefxIsKjJBY7CG+DlD21+rL+Q4fWMtpyYhbA6ISOQ9c87Q46cummn+Fs9x4dBSTdpGFbPtNypE0AB1XuumhQbBj35d9inOW3/yfnPMyop8vEv0PYWOcWNOotGsJG0uyTaUo69uq6rFJ6c3KiG9KN0xQIICFSD5NeXjlsfuwUYIAyn7383jEX47GoeQLQMjuN5sQ6kI3eMHVXWZ4vRf2gdm9GfZUhIwRpNvEoX3TAHILYAfUloiBvR9ihRJwJMwdRdQbwWqS6SZJmjIMELJYrktLh6sFB+Ft+TqeupsFNsEFAsOaQqBBrUpfz41Qfb162zmaH7IISiJcTtu1F9KjdgesUuDcKMSI7kbpRVn7xqqcFf3RRKwwzfY3ek/cQgLfKXUSBukA+Q84oYUcP5uQyMN4GR1PfXg1hzc5JqWY9cX7ACQclr77dTjhlU9Y87Tgp5dAe25Bj0yMOHqnOWZa2cbhcrw2CjTNChKRZ6eIZyV0QKGWZDQLC8hgHI33gdydPLaxMg2IGXYhmfEF2MiLmBxikBdkknZ/1DXWnUPHDHT7xAcgWAjLWn9xhAqjU8XUJRGRUntF096jPFY+47W60oNIMgTCUIOqN3ZAsjWCwt6ijGv/1CYi0i1ffDb7MEAdTL9MzcexMlG9lOsEAv6pV4e8qbtwxJzo8EbLZNBcJh0bE0vO9xYCvHzKuecUL7JKiDtCDjT9ay4d+eVuxwN+Upmv2I6zmN2353O+VkBDFscev2ZDXhjL8Tm8zoy6SnZMSFOoeBbveHIqAfArkCZXSlh9X5C6RSzNX7VNiRiJMKYJ9tPD7do4eLBMMANBFT2FJ3PkR50sFZ7W8j0x/vH7HX6mDnBYzwsX/V+LZCPq0e4yJzRlH6GtG0rBLFDmbxU68t+CPEkt1SJLMgJzLEhvI3WqrsSDKWBosGFTf2kufTGtt8LIvkbvmc2+ojuzeOPIvq6UOyMTS0BNyA3ty2//R/wvc7mIkdoa8j/u+GsyqbrQFo8ez0PsaxeULurdW0Xr9cPP1JwSl+CPR4UtBQUoTYKMdsXDkmaD6j1zYLkkFEdTsHzObhVo4EYLa+42PL/r1F2KlFdEsrLO0HFJ2re8l0F5qPPdnOrB9jUpuFybWhS1m0NRtd4dneKtNPwNi0/FopuBC0IK1Tl8al5WOaqnoOuYPO6Tt5rBH0ovDp9qAmTUoXU1nCtq8f252pxDlT5f51wTJOIYL7d2vj9mm66ySR4yu3ZajVAlIbQjOmU1itwzvgZYI6k6k9B8o7I83MpN3jWOKrmjyKjo0Z+rIkHzIL/lQj+RJz62R77MfddM4BncpV1ohEFxPpPViSfxjF4ObcMGpjXDpdPpkVLuB9Pp2ox9EIwmW5v+PgNEsObszmx91lMrDAuX36lFLwFgJDSJAWOds1U4iUfP7I3I6M1xiWHNr2E0YTdYTiaCnlZDKBe18apSMe2vWyRjRdWgk6gMG+l0yCMiMEq+7sj5ha7jMiwyAeajWYitvCo4OYWa6YufDZ9l0rHnoAaWPhDAMSzf1D00ml/v5W8Dj3fqjKwGZWdlR8EDysBeyh/4PJULufnl6HKhrvE/FQiEXfDCdAnpZBrR6t/j97XFcWrkLwFW6oVemuUpw/XhpOv8iY3QP4gsQScIWVzWmbtxSjBp++6erQQHiomc1m3/k+IhUQXh3HrqsGwoKRIXwOHJPH1oB8hxw7Q/uSMLGSK7jnWiMywjFCRYDh/VJSMXzOTXnH0QVFAMmYuOEccGd0+fWgR8cTuhzP5k1eL0S+kf6PiRM8jMq6W9yINhZkaSPDxC4yChw1D70onDIFvESSMHF9uwzmwoujgIZ/Lt9gTyfS4iz+5Mh3DSxtFojuQH4w4JW1+rstS77mmSKCWVSPCBfDYUHQz81QypG0K9hloTaTOkmWyh3WzVZr4SLI0XHRG3Wu+sZFR5EoENQDGV+OMJhxE2Au6oZC6YjR/dtdtOsTvJFhpKghK6rVwsI8zYDLeP+HMf30PjG7HjsI6pL7agZYqYdAbH2paUHlaf55yU8dwnN0Zl3txY8eaHkKvQSlHHM+xZru67kt7BiyHOywPiS+0O+sptiT3MRdnJvjrPq51QMv4K2BrxoC4D1bSWedP8y9vpi4KTgNLhwO7DZREajM+pt9J6xIF7U7fI8kNbEIjGFa/mlMKMcHSkkR0llPgQyHlWC/l5UHVaMfxAQso0QEa96VozDodAY+GDD1gAw7WeYrlbBWuepplabih7YVNPWqJpXHqoJE4L3WE6plymWRVBvHRS2pGwWAY3aYE1pRxmU4OZgDv4n6OtG6deBFV/BUxltvK/zl11ZZtqRA1f9ujzOmycBE1mDWpoTyp50ha3L2D3rwWW/PgeJX7LHeEiFI5VDmjbvvJ5WmxRKGxo9XTEc48zqsa0GA77ZAUE4TQD+g7PquSGfH6Qx2N2bYCPf16Nw+15oMKuPvdfhx4+O0PY8LAnnoTgvzxsChr/M8x9bVXN5OS4lSn5l9Q3RhKHNXLaWg/ZnT3Vpjte24th7T30XWUH2eE8V7DNREm+AwF2rM6JZ3JRU1nxhwc48JlCG1kijkO5NXXJJdxWsYYH8LYiFFax2SwhsQnkAtQEY5iysm0OIpDRmcyr/S+Re4DcAu9ulco0nJWAEgV5do1CHgjlBOgbfdA+6kVvHJazJpOA99pfiIjUFnyLZVMN/ZRG2ANB0YNY52cpcZNS2HXYlZ2lWdoHnUWkE9sI9JsXjG1j+8xbI7jDECfzsrj2+pF7Iz3nZUP94T1Y6tjYKnbzTkVqP8NKWKYeOmATayGYsNxI27l63eroiJUDv0M5+wgiyIbmNkQzumIgZzJX49C/8N8TG1p+SxVqLmfSPahw6D70kmTlh4l8h8VODn/EQPh/Y+RmF5WOGG8AIP3X31+cvi6s2fmKDjPuEWJODwhG05zfo8IyG0bt4EdhTYgYOm7s9W6VZ7AnTNcL8ps9hsl9wYVIM20SqjlN8jeLhvN19hJXWntt+pIMTr2EM9ctgsA7hIBRoW3DXzef4A6kJ1yCSZLNhmxVyz++rxAYKYQ/CuefcfZXwa1kmQehOspZUlJp5K6vDlGAt9QoB72JupD12hFl0um5PpgLima6pgrDbdGC6VxlUKKPvKWuaoinm0RD3HzFtkXCiRx07numU/+NmuH+n6/kY9DwQb8St0Xoh/2IzcImRvq5aHLUA7Us7vvjSxZLOgzAoKfmX4AC1aH3/IpN8ofvIpx6n7Rwxc5ArH5H5WpA/gHe3DdsGD8vROIpl5Dem85iSxVSBEZJ/lQHwgW40m5AJrci683CH5OCa/r3iKwCa67q1k1g4V2mNIRQWmg6BNjvziRtn8XkVzo2FtbaQnq8wCesTiKAQWd88QzVgDJeOvqAf/nfnF8maaPcM9hqZVoiTQ8a0JOsNJJI+7Mp0DjjbXn8ld9PLD/GjT4EEpwkE1cNKAzmeCPpsJXf7YDUra/Wux7YN01s5B8I0ZZ9pyDxcEDPvjfn1Pbq+D2aqKEvcPhAFu+D+gUTbzQOCCL3bhBony4YLBerFs+nKZEvEoTU9pNwpgUdHKO9Rt+IDocKYx3O3QMjc2Yf5GB3NbXZGncIRspUzg3QmdjbvZcfk+nF+7Gf/JpNhjZnImCV2WfQzd6FZ82C67QwBd7A7lpS/RQL9HgubZ8P7HMbYc8zw/MikgzkXvBTh5XT/lHeMogGsNwHzm5FobHqEEkd3+VJemqcSlZUaKXqML6ujtdVC9O0d91JOBc8F3p4pg7J8ga77H9p8/rwPpL/D6D2NxbplO0uhE3EkbpHkQ4DAYpH3W4SYdM3iatJNe4HY05Ep0CGIxVDfxbSdcRq44bPRAJiFRstYjdIX93CN/U5l86Gw95K0TTYvg+Ae3nUXs7QBz1zecVVJxHD3fDjSG4uZv4hlHb0IpUD3AnnvFXz9nsZOCwMMrC02JrhrQOLWtGeKgthTOEYmmnKEmpEh9Wu05efzrPSJXgW6CEHr6ejl+SYqeV0K++E2rsxrNf4F8vpP0L7aRK+gabbo+F3gw1lM0q/mu9e4fxZ8fYV//xTZUmzsT2qPc5GpxvpyRX+2Sl59YC1YvDkutKnH/ZAEeslY99dNl1fL5Wic0PyY/A2EeoujlSvVs6kA2VfGss4c/TZyKwBnB3B0grLV+64P3A+rhRB4cww1Tn5meoqN994ZP2sXAzeCxkVoxsVg+fIy3PyPoa6wrRtzuXjorIzSA1LeYacgJoMgab/HEheWK2t/RFzE24hvsOvufpLAizbC/cIspgPRUiTLcDRztSZdYBroCz6SqkSVhPwPQetGAIdaKhxynQqI6NQFBNvZqxWvfesQvmRB2lmXjboNawjCJzKc16T9S3avrRJhXFaW1tKMye/VTjoByvDL97NMwzFKGKPxtMiFa7TlWwhj+QpnRzswiUfc1juIwedGroXcLCOaY1a8uAjVfWxrTA7Y4mHBG1GRFjbY9gkN1FSaZQynZYaX0xOYyGQT7fQQJ4TiI2gehy2ah7AUPikdkQxr0NvGwp3RbQuwC0giGfCyL8+J9/tBSyUvIfWWUkOEpWEFpN/7R+eM4AqPrG7Kjk2dmWaJjPvOXdW1O2wGEfKH90df6YNq3n2vBfja2hTlbvv3hGf6r/9svx+2ChytgBEln3g+rGso4TO7tigahPss4YBkUu+S8RcG0CFHPmXHAgG9tf07Ayi4KNBWNeiKPKIZjjpS4T38XV6vhXOQCt/OWHTAydYgwgjowC7JmUu6sOMHtXsq7aEJlxCzKanZAvMHjMAwt4R+bYRRPdhukHlJ1v/xTiC+Lf2Pi7EFZ9elkJLp/yRxSY1CQa+07UNCpZNFNHidhVD+Wcne36lZGlM42tcOlK6lZa8yxs5Ttk4lc8/dCTzw/pWqSUlynrYlTgQ/W4QVkS+tA8vOS7E49K4PXzOEGqlL2WsktgUTcuTeFNteP46hdNfu8rtTi5cwPBNMEmaM0o1+qNm2yCRCx83r20UFSZ1ZbSRto7vXEfKrZ50i6zPDoyo8nb+ceahHr16dVQxJ32ocMuFchZ201e49bPUQ4ARzRWM7LGraaXdrnWEevEgLg742VTef+/ZqZNrCgzLMDw40dgXD6LVRjqy6rncpj8wfHmLM/pyv5jMtuJhz6UpfktUJai9enZ8wQX5tEwKsvIO1YuhpfYtl5RN6aZOXRqQL8rCkRekMCwYYf9H6YDA3ZcmnCxS9mycigWXIMKtSEqYPgpy3XzPtvAv0SSwvgFo3Y+g+2ND6wC2z/5KXcbJzaTHG77dRC2PSYZZL5umBUFn216Bog+vNS0lRm96Ut4L69rDWBaNfPpsWTzlgv9z06qxr9ArCdWWAFt62KOF3PRQo/lT1+jKfKY249iw0QaXnc/fGb0KwoD/+E5sfxvmpq0ISyS2u2JKTrzczh9V9ovaEAuSHnDEurALkXjD1emFcmNWeQ5p5S9AosabohZRNsZMB5Njq8gAWMuOSKDEqnp/DPS1IXnUZfX/3D4/bT4FVnEFxsvDG5i0qFA7QtW76f7+EPXNXN4d49+8BYsx/4Q7R8y0SgY9RJa+1ethj3KAhI7wpo8z2ufbUZCmd7NjbTsEnHzJz1AN5fw0BV67Lylj49P8V0Cz+mJpaY6nvKNDu6vcHuE15e1AyuupRE6ztecqkp/iYzfgafp7acNXCt4UyAwbAX0By6rsbpFdoz1MR1N1yB7suGgMk9cUlRXnNgOL2r836Q1qNlqO8KzDJSUUKuPEg/H/CKPJIbvHmbvXfukG/9ae28FptjceTJ7r7HDTKl3KoUrwjEIsGKqJQj2Ej3ERQv0MVaatOhtB7Be865x+FZ/6+4tU2RfDFUosSrvFEcQ2QGQd68XsTOZeaXBJUAThQxZwFBcV8Msvab+vUZ7/mPXP2lXwlEkyCUKqvR8PpATmc3+8EM03G2UodYCxtF7OcJ0FpRymnDI6ph4kzZAj+ZXuKINe2jE0UANfzhzZIwwQSQydHKj7eI83GQTDC78WMrcdMmsfL2KVJX5ufQQXlSQp1MorCK8Jc1iqm6PqHqveIJsRggmb9OdmqMMVn61eY1q2HIyWXv77/rCT3+cz8AE4isvKrlp0d+66RJN5+6mkNZssrTgAPIFoWPYx+83o8Zv4ZKFG4uclYGgV8VI5pOrrrGnm9rhdXL2lrYa9lgRgMqFtEB0iwm/xW2XgZ5Zk7XtTrZu3gt/BWSzpCvqBgcgSVpP/f8mhdyeSy7uknCrU3QRxV36DcsBEM5N+kv3VwNlvFfRv/NBAKogI/VyXATTXUHTpiutZJ/kxgRgOt2Scpo9bvCgS/vj5lx6aHt4/AFvbw9mTp2e/QmvS/TaX9jKUPDa+G7UvKWlhK3AFFimG862gX89zuK3Jmr71s9EwoCBGSSB4BVR/u+biOQUnOqR+wnQnmUS7zonwdc2kBgmH+I3Kz6H0jeOMA6oUEEAOuyUnVhO175dr4NbYEnwISdvcnBuT48XBh5Rj23kEUxif6qyyzEn6r1lwFOF9sYlM9QOm7T5MGamwrpT8VO870vLWq+pkMbU3MaWQtXM4lTtwotV30fSchCKYvrsCrwxco+THK+aG36VNN02EQfZqN7dEU5Bg+/M/WciCaRtowMuMBYpC6fIyJtz9TPWZFSiUKNdjQa5qjtt63/5MSBCIkt80vxN0zK+wll+VZdxxBNb2BBsnWAyYqr9zDt8hZ9qnxV6XAAyCtQ+KxL12D9knA0UqqeOOOYlj4FpBAPTkRSU5UDS2BZWrNz5t9t0Ej0aHF5h+nvO7zPPl8DcjGbHg8+FwxnreUhYKK0qnO6ZwfAiJN8Zey7EQOzdaW883vEP4QRE3PsvwoJP3S6UgiydoRhSkWjdmlUDafZfnrtip/zdUvQT/dXRgjM3qRjXBEt762jahcFAyWehXchx7w4/o3tM/GnyCKP6atElrlefhUhcEKXxzh8cLa+oaCH19Y2vZyAawGA6VPmpwFuDY79kaXT+OGNkey9oO8OHiEEqtaeIQ6AS6TGPuegxzB/A/uO1XttqoNXpX+mUT6gFgqVvD6xxqVgI+PxA5rD8nXYXV5j8nod6gw6jaedAMUmG0dQelzp5j/YJDh+TSabo/N4fE72GZZwGK9AB5qptkPz7HLsAD0I1Yn7eQ8W6NLOBDvHAIy43dcq3Ex4Sc/ki1SmzRV4BB63zBEoz0MVuDXlLVrdRL05kVH7BoF6eX/y3pZnKzJ4FEbLDt+JUfMZCss4ivjGyNxYNLS2gxeyQV47U9dWiaMPKxu1mQq9cfg0Z6WbkODWxdId1anUyVy5J1dJ+M/mHGtYdA43b+VESIBj0H+zojQBQDRiFt5srr34l0v3/DU7SMeEVbTv2ZKlwhQRhaf9Y73+2+xtuJomI6jpJa4NUwC9PhgmKI9oTZ7WNDpiBfafo0Ha8WKzFLYfZIoYKnt5E5GJU3pG+uHj63WjnqsS8GUzW90ceRcYrBFqdRA7RZNfX8x9PqxjLwrVbutTA1QRro3JVqSyOUrDuvLnqW/CwtmjR+ycOsFVGCfsfq67ZbQBOlnVdKa/7oYgsFR5ECrrww/eXdC/kz8/TbVFQXKvYtZSL/nxaIlO/hapua2yiUNratdCCUtsLXMalNQYEzMvgQl06VKg2rzAyYQ9rgx7mQVuvpBHXXaIYlwKHvyvWDSDL2ooIqt1bkB2XwbJJ/PYorTzuHzXzjO3tE7eiKVZCemvxJ8qtUjDqfl6zsoemAmrCibr8vtvU8FQUULiV8o2qmuB05hwvfab4zgn8Z89Wpil9X21hiD06vCQ08gYMbXNOEU+aVCalLXUDWE4JI9m/8p9grRb3hr8+VIBQBcMCEaCy9x4gPxyBpn7BtnelliP5EFP1R08wAgZ/yDo3bkU6dd2nrv5Vad1Dvz56romLJohzQhVWbRtskgCB5b1AeW4dDvunPUgyVeqRb2y9Y+pVVUFi9Le9iweDLqURQojunQmJtyNvI1+1RCUHvQZqAohaL8/TkVOS0gu/yQ1aJemRA9cw/c74yHxmT1BEI9sq1ES4/p8kR4u462Puo55qUeYPjxX/dvMsIFJwV+rreVv3ohJwq2A3zFJTeXboSGyEv5rG/M6uuMgO9BE/kgZOvqn8c+jcEbLcmS0PmiRWZWs5f3IgMtgKYW2a9MwpBXiV8Lu2x41SbOGNjmf8j0yytLb8sVPNFtcpcDOQwuIwik1xT3jrpqG65MHMASKhosWDi8JrF97GtRhj0Xrjvn5JlZFA+lqtzZ1UicI4oY9qNDJBs/DTeCw8GwQTdiEKOTnz4pFFt2SrvKRXan03ZXZxetKy3OOfI/uV9zZqddl9gfGLSC5xw8fimkDIc3dL1lIf2GSeL2DHB136R1qx1kiajpXb0SdVuhBLNd+17Tgo+CsJ7EoWtK9EDtmAxBUvlv36W6NaVzNXw7HWofyB/zEpgf/45A3ubgh28G89GMyuxD9UyepLJPYS3/Btmxyzte69jxJaJGETo6gJSlOuT4By9RXZyxpX+d4DNaJS5E0o4BigvhpY6KL8q6o0Lqynl23U9xEGALeUHEK3tr1gpk/ogR/pDoKN1S9N7GP2qCvz19QAQPaumdvQXpdcFjvLK9PGjCV1dpQ2VY78DRQbCcpIvL4giQw4SP5RYludUtVudaNunOSxDryoQALhMg4tq9TZWTXPdA4Pg5r5WI8ukEExX1v2znxuq9SNSYiXoIFyya/hDwr8fRUin2FpRRo9WTyKyyLvwGQpW2VFxZDRzSubh+vKvnaiMsmhGazigNnb3h3vEve6A6zY40Dsq7crt4Et2CtUd5+3EeDKTp2rbiIF7gs8L/VhxumrgyqNgaWULophMKRwM3c2KlME0zpEB6Rb214Dnd8PEa0WTD6AO30SFXwekfAzLlcXEulc44U3s69pD22Ps7WVeYGu3pMimFRq3iwPQF52KugyJ0spMp1pMxmYhgVW7D1vkxSyyX8Xxbjz650V/qnEtAuyjrh3cXNoRBNRO/hIf1xgEyQOrcXAkwUd0nhkA0qu9L2wmCNDp91fAVwbd+b3S0A5JT0sz+1Afrr6RKR3zKsP+X6b2Qbw7gfmKChQVpEA4fa2ov+vQPR/M0qHOEbg6FPwBizNIyMe6rWm2MiN7QHrS1KbEaN/aufw/g0BlSzsr5lYTrcO8L0ZZDTSx+89Mir3Z0PLqx2WRaufRfUp6YVNMRzlMuseMJuoQTetwEaRqDUkWUt6tYGvyxCcODK6JP606Iwr+1GL1mW5Dg48v0r3BnlzrtshxMxM0pohgKEXsGjvLQws/ODB1DY5ZeWREQooZF322XiGP4WtrOUUhG9Cbff+ChrYSfvdZqPNPlHpY9o8PWSiAo5pNi1FLnIq8A1Kd2OR32wtbSG4qNuQTFvQeYRxJ/zFdt/SXlfrYtE5U99lywbkcYLUY4vqeKWcHASedbRP2UatF1ZxBZkpXTwgAB83FQ2hNCHf2wxx3ZZ77ibQuKYZq62vNiYP1d1pG4mlwuHLMh+4rT0sXevBqcZYdm+G9ARNEDPCN8Egb5Gvtl7ypt3GhxfIFTOvyeORBkVT1rdSEmCIn80TFOB+u9obgq4G1JFuVEEEfrTGGfePG7YRiBoW33Rvr+X0Nm3H5k2qKtc2JkSWY546tOFXPkj5tcvhgBHKMk1n1F8Rpxdga9v2n2EuklNiZcKVZ06SJ71o9UXtCzb48o2SlsKqgZzQeZaN7eemT9Asuk/p0PhIVNOE8fz1MO4aG2Pp02QY3WjQ3S+wLaJIzjt5yu8GwSM1IwE3MVdkvHQiLcrCMxlQ5VMg+lHlN8G+OlqT1lvDF/7ntBV4OsNMN3lMeT41LI92YDEaj7e+LoHPElEEW/TicRPgV8emtXxWIqaVbyNHlI2ceDpjEwUKTNa7KCUO1/jnYm0te8zZBHgDUQU6p4XQH69Ga8bRxJYSQ5zC7SbRAhTAX8hSpW+Kwtq3xSzhL/1ocVkAC4wVMPAXX3nVfP07KRTispZGrMaPmdwOAQaTE5nkQqsXLV4bTe6VWuVX2zQhPlnkGpqUENHC357nIlVVd/8leVf1bCgQA9DPwv3hWMGFlpxosLf1B+Us8yi5DbXf1I2DvDSCj6e3a+pfCSNZCXLBzivuc9urYxCJ0NITKOQXJs3zdVRt3jWYizaMdrwGSG6jXhkerhjhKhBz/nyH8QxequeWUhuB91PWpcHkNuWYNK6WQOhCvDCMJmjLsflRLSoi8a2rw6Frg2xqLIdg+9VmorgMDD/B536hmdTJipb+WNZpApm2K1DaVbNH+b/0T3UFnLGX2veJnM83jBbPvxgE0a2/AvWl3Yxy7MJfrbVpNbuAgg8930BxAGnTBeLibmH/lJKSL7x6eMTkxxkzHjDY+uRc+7o2KkzTnVVEEf9Rawy2Pq1b9zOztsvMWdMCtiuec6q44oUmACER/zQPhR17Fd3NxWC7ym/aLHuHmgRngliWrUvrwNNjv6ewyyu14vNXzlMqDc/hjWl4MILguxu3KHaQA/bG/I8BmPx0brUPvbmjtTPccrWi3Rl4WgN3mh0IBlP1G+9UiVDSWn8kX3Ka2o62eSZ0FnhG6DNGlO2EAjPHpGf5yDTd48c2q2Le8sCOHOY6th9Q2JckiBH48cFwJdqYkVUwSVMAp21Bc1vAIr67CCQ8kRa9mFvEyqt6TCxlDkonYoBUBD3qbdv9/bhtpjhyh/avhvtxAZv9oSbd6sZqj2/sRhzqCQEd1nxcH59JQtI9xrerGrII3EDdXot/p3xi+N1z+u9VTDq3myqfYQfodIWpeoTloZz4JAlPrsWnqMfNnoUDrAVM8iLeGYYccRRYMOwJi7yyUbLD1cYCBZhwfzGaLc2df8ou1DyCSTR5osc5KiIa4v93QpR8LVLouNqMTKfIsew4AgPRK1wSJUzSdZWQaElC8NYGoPJfQLkFkN6TeY5Aaxtz47eUUsdvpnLunAs0h/0MK0WrVik3/GUpczHUW56golrYQZ/No4oTYo02bIilLLEhlbdngDBCRckWKA4NM433K4UFc497aJt9TrOsFFpa2INgUrb3TPuM5cITWlsm8PZXnNJXe/wpvs9jrj7UUeZ5WJdc775w4GjQko5dgv4pOYIFWqNILqfx9Xc/vxg6ouY4swfDu/AyDvu46Hs7sFw53vSk8a7t7pYuduPhVz8FV1Z3Gmh4+MDxtYirKvJ6o2pLScmUuuEamAcfAol5bFO23hCL8YRh9adHZOgVv0tTJ6KTVF9dVE2L0qlDS1zpl1xOPl5zNssuJoOhWaufPafUdoKB0wDxNz+PuQbHoxvheEu0E2GnHBLia8iXdw/2vTdbhelBvS93EPKhd1UkXj4JFWgjQbtYkhAXhdzaXUVi37A94as9z9jn+no6/IqqaG9Ztd3IMSf2hkkbaERzEq1FB6ritCn2x6RTsk8dDYMJHvWCdPB2Z76LRyBB06XBrlHSVieBcuseUcc/sP5HXU/Hg6E18dHmCqqnY67IBTbyEkcNRDeV5IVdV4bjR/p6m0CrnH20yMoosjtDeyhU4YYId8KKg5sULl4xWeNPu/ADC3dMpa2NcCpjApFxzeiTfQe5ClYiUxjsPiLiyOEHVIorqYpvI3x8Hfc3jc99BvleJLP1NlLWFaLryrPyjL4cb5aYFmVgu2rehviyA/BBZ/7KyrSh1KgdF3Htl+s8IOL4aD5VuQO7SSenk/c+RJTh2pn8G1RHRJUjnD82IAQz+9hSEoxE7K/oR52vzIMRJlrwnEUPwsbpBHlAVrl6Pwo+j6LQS64ulyvOfdNnrRmlaBnbgTJ7iIHzYy07OLrNE/t7s3INdse5hLZ9v3sKxcTdMqPD1vs/FdZer9r3/oHgKDmU9DbrR1R+E98sJjR5g2CxQqAlF1qHPr3A8QSasQiu6sPAZZy95DlISl+SrWOT0c+fp6eOlCzUKBHtkNXT+NyJTkg//3AVKAg2FJk1NueXSpgZt3PnOm9/014t1VYlosFT46c0B9l3YzjQjiCBYSz+dTYnYTpEcATmPCOFusHvYS1NfxIt8LjNFjm5Ngds1IpqR42EKTPA8vdO55cYZxz8mH5tAYmNF9xllBVTUVPORxWsIb8LXxOZ/+5W5krMCXhpJDdTh0CzrZ7f+XCaSrlnYeVtU7yJN2NYnn5UyBkkiXH3WslNEVoon4E3iWcaSlwYpzSgK9KNBmGuzWeRv5/7l2X65V3c+XgUnv88zJtlsA0K2GRuQPjspMgI3vEarYxCORFfzaZqhN+DsPg7CAbcLvMr2wYIchAFmwNjOkWvFtXkVpRtF+ZLuUzaI50Hbr36EPB/wVQ/XLG+mCdeb0y0Tun0K3lhPU4ZqMknspXA/nxftqjKyZTp1HEN98PQyDSc0B3GM3jVTcCCXOOHPwe6kooMCdZrcDrS3kLU8f6J6O9Fevh96BWVuKpYmtq/DsryB/M6cycgOALvz5S51OlkJ9OF7A4siAQ4FBekhGaYzre8SRRdG+dKMZDCyOHKDsRFTUDTY6Sly2dONrNw/IO+AzzDbABDYM8Fu6Q9pYMhxwN1XEBu3pt/NlJ98wFbqfRF3+TlybtIg1zlTIdw5rOUyqQGu5QAMhqL8IqpsD8mcDRsywaJbFx+UBnu4Krc2IqKxBcH7cW7fuX8qMNna3Vp2SglwjsOBltdWI6t88EJUN7oxdtxULBnuuAHcd5i/J77b7GrGjCwri1uVOWgt0oDUva9JtxYlU0VwpZCxdPy3V5XvDA//LZhlBmi7Wsf5Mq1h/fTgMw33ukte89jAtdqI5t/5jnHgMGzXZxYmt6fSOEhXFj+aGXxQiX2oPNdcLdtCiCsNnw9lQCAI45Ok9vyuzYooOl8mFizRpqnzI9C2F/MpCBALKdvC2g3cf6EqnmOG4IbiXBy2dYtL83nYqAwhYbRihcgpMKvAHqdlfb1GcezOccPZyx8aijSBnJ4Ei42vO7xD0bqh9Sm9m2mj/VjTWtnrxSU00MstLmuG1RtNXBgcOtexGv9elV3EH41jvYnuAMF9Azb1IfqsPkEidukIcp+QELS8XGhM+kEQoOmTpwGlBUEo3EZ7BYa3uMF/w+gZ4wfW5nF7N5IE2Pc+xvemgf4tJy8XYnwwoI2LqMUL5mxfl6mC+3BmQQOa3S28IxUPKecvo3L0hAPmLrABfeL1PkNDSVhKCBqv26r5rW2l1dBc+Mf3zkUxliHKHHxQW0jM0CueoHMSH7sXXwBlSBKUJRGUWtv1hfEMtHjM8oFFyOm1mOFOCSQbromPI66KCrNInyIa9BXqivVp7B35CPivV9jktfYGhDs0qrJFmVYSt2+5iVKF3eFkK0UBTH3asHvxf+hHjMkOHDrZ4LdHNYq6lN53yRY7FnYmTedQ5BaFx3du0P+cupWbHw25/MUcVkWeniWa1UfIP1+h6jX4srjOTMVF06El7D0GgPTKE2gd8KU+ylqpqz7/mfQROHRxucTyKZiDN3idPNDM6jhlcT6s3TGANvzlWiFic/SB0pLGeflWmCvJescwRUBrYoscUMOvmq7bvoJ+BAMERVKeuCU+xJCBsNRet7KYI8L+gZ07oSjKQZstL6gGVktnp64Gf2FXEhCXSjpl2gUZKXXggz58v1DCy863xInxrFGeMw7adt75OCl0TfyoIt40Do71SViLmMhUiXJXtFfRxn3VS+0xLJV4L5E+hATy4sVBy99CdpF9eOG1lVDUlxvJHpHB6MHug0ArOtVA3L5q0nmkT5hdcGjSgiSlgyR/BE1C1ktgzegnq+D4sa7npQItk4qIyeFTOJYSCR1oM5imqRJfk0NHoSUDiAi10NIDqLuZ/iD16YlCJ4LgbyjJzW0cD0sMw3PdAHcJqw6gRRSrNBcn1d/6+fMh89EDbSTTPUMQg0j0WmI+Wn22ydEtwxsQK90fjgrYvK6DmhMktr9EMi2qXQuoiW74eYNR9aCFRjpAJzzO+nB4aKfOeax0q6l0UWwYhKkgLrC1JhRyvwtGkdU1iQVb24aHSiqyVqKs5BLjUPbujvwUpOLsMbFjNKSfY0EQ0zyMU8yAwC4LUFb0nNfIrGAcin1kzmLMGvH8K/SX4LfzXneMfy+HU/reysndWa6NCXzr5kupmMazmo+MQQKkyae29RgKkVgXVu1T8BbGJEem/SF1GJ1o9gFaKb7P5UaIRcpLYJ4JIHKGK9avRGdh1q0xCg+zmaOj8nZQ4DyXPAtnz+BgITHY6w6UzWJCLC/VgXxX0Jl9PlXG+Mw3+v3KBpSQSvsplC7sbmfKqM4jfdABjxG0hhT+RGCftqiwZlLcWvHRLGxvBABadfIAQvj/0Q8N6VvA3RptcaTiJEO+O8mIuo9EG54HA9YWT3rQJDZSSVeSlW+9/+/N1cUUSVR+zx7OOo3bL/6Fh+gUoc0TtEDAQSxxR0e4tmA/RKv+giJY8ju/gUBrtGqe/B0IGuemhbsLOdGn+Sz1WvWGnbNtZ5Nlk4gLz/fdkichCfKHJ1mPKnA4Mxc65QL+pwGbXGSR1J6+LVtOEuM9n+H8YG+MiuAAJSGQTlNelTtVuqlFjcWuAj464M9TBNqsjLUDV7cpPD4UnOnkCACYVPfKzXBzwglJgqK4xOzRyNXUoAo//ObNX11V8U+/wpZTziYoiqnfcZWtml4qoNTNzkwK1haK4PkkYZ5e8T5Gv8rd3GJMGfT9lz916+C7CzvMCm3ZwVA8IqxL5isCjwxQHhkdd173kzU0jrcp1X/N/lxLZwcZgVBWq2lvvgNjh8WaaN82HFyhGxBe2ESpj4Uaj/Qk/zRE2XxmaC03qa75PhEcjG5cnkMtcL8+ID2yKgr51VLJaDSrnSQLK03zcoYjfGz0+OIEY8rJJjX/jmqAuL5Q6FFn+k3P3rVp+7dEn25ppW49OTXynVBVe8XjJsHVVh3iUdDiUPlV/mXlJLIx6+Wx1OS+2ph/l0zKI/rT8hGJFWB2bmFpgtoMfKs6ylwe/FXRrOqUhAFGM/ufUXWpr78V9dB8eUKYwgffCN4RZU9GNUeLTXsc6np/aBMX+8oIJsFH1TVmNXIX3iM1knX9+FnljnJNEcmXni6Ekf/HO3bUt9J+vMvPhGdC86QH1METa0S3SUtsqBFpLk4yNB9aYeETPSxV9RzC7kwuh4zs7qtC8FAUU+FToqSUScbFudpJvmsxTqSlZJzpOvmDadD0NrupFW+rQO9QX3r0QYb8fmxoWzZwY9R3T07Qum0o7MCcW2fa6YzC0APG5OqQmrQd5m/6kThhlJ5BCvtpPpwk3uNj/pjHDEFTTHu2KFWka1XXtadSkqYhjqWMHykC/aWyLVl8bXPtx+30CnYrlx9obut0eV5qhUJ8CAJMaK+QfIRCaGEm1T/1/Sv7D/0uwkhKlI91s7y6tuD4AHZcUiCTQTG/vSqSOuYPEHhz2SPMfXbtlH+C9i5D4y0NYuYAsPTk1998GHqzuLa+GiEYiLOvayHYV7PjSbM/9+7F5Lu0rUII297WzNY5YtGam4qNYvgW5m2DVpdZVw0kaZ2y6b0ItfJVg2gJ/U86T3QHusqNBWW5d/QRMASUzDOdCWleI2jcCBQpM2xvYjKMDxI22WXUQWTMYRc3YBhWJhAOj4fZpsGAsmPo0vGeGV/T0n98hc7cOKqzW1GnOfm94OoamGPHDEU4kD9K7f1AmTgRifM6Ar9tsBKdOaVCDSv5SmE17iimqhQfCemwp4sUmlpkzjocTK3Ul9NCMHTmTXof+vu/MQLSFLu1QKoNlxlK4quQvH+TsFrJtseVaaR+pRKqDsWPB4yjGvKzpsF6IwCY4PM4mmvMq8pAnbiO+sw4nVH5nLmgrBUOkjs5tKkZAB9tPHXZ6C+YGD0DX5wvh2Xiw1Gfaml44YtbEfLDxiChn12SR6LJAS7SyhJeAyiO5AfOUARt5/KsbAnWZpil8P2N2ArYTloWpUNbwqPYW/cBY8YqOoccceJwjbnOmZLjxVtPcdYgaNGzQzsXsxJ1BN9UK/RFDFuk1qLAsPc1skZckMkwKmGSXkOdTpsWSoM6pJ5tV9uhGA02d63UwmgRnq8LsG9TeEjanCOUo4uieJwaZrehbwR6NhaeYOua8jo2PfFnSk6vjawuOSdewQ3OQuxFOojC8wUYy2uUo/zqYfK0v2D1AXYj5dMiHtjgPjpCdc29C01D6qo1jyHhUdjBOKoaMHm7Y7nalHGxDRBPMCWXPX8F24c6fD/upPyOhHZCKBUZgOA/3vq1ZnQemiIqBdSYu6IdnqVspetuVNQKFjWGddTwzx+bauIpkAFAUMOKaQTdEEtkHU45qbtXx/AJlE2g09HmlVxoVslIFIZKd+XDQjt4SnL6YfnE4NonFjDA5hTOnYQWDwJ1vA/CXAZESlBz8CB/LdiYux3VBrxnkFRy4ZixaAFHvs+R1zkEE9OPAZW9uUlYxXQ6rLF55Hdylw22Mb/avQteY4sq5KsJ7tSeeccKIsrgTftm4MPPW4fhL+D8gqpx9v0cxcXfwuMdU5268IdPSBIWWHqYGre2ygAA+fDysKwFNnAs/9j4K0ndP9BtEHQuOYXLhnBPFUwozjukg6RizHIV6e3UP9I5x2lPnIUd4DNbmrG6tAA0iodO1vwwEFAHf2igkk+oKm5ylvqK1WrO6WStJOFwlQC5GUM9PPAqW6/G65QZOpJVw5X17MW465rtpnmDrRkIPjVmh90JOqJhTM0bnrrRqIJiqzH9dbOeJi+aMsFCBMDypJA40aKcDkGrMUrTPPnS+R8tFFY9fTZRXe0RUuMxm6nH24q51FVni2t6mOD7gl5crLSfYVk/1EctcYUlYcIdyXkY0+Vlu41btskWgB9zsz10lwCNEK8itEmlv0MGlm+A5tfd2Ec5w+nNajOl7F1l4+Vujq7qbrs0ef1jrwHo9OcNAShEwXBT5gq4RMrJwsyf292jp6kBCBwnxCyhJiJXywN6igyNS+YG7ewm7VAb1QoMbERFsmFFfODD+e9IyHlU4S2UPHoGh5oin8EcY11Es7QQGSGayVnZ7UVgW1qL/FIsuYdvxtf2QxPl7AqHu8GYOXjOog2NKA/yRMnh1/QMbOwrl37CRzF/Q8pGmXYtPjFRaHT34MFL+D8lebL8DAEW4U1pX9b8AOh2tnmw/TmGLCsp4JKOhz+ojziAHi4JOsjgUReVYSC/lPpnWshFPCDeZd03whYAyEp8YFk7bkWXXfRx1LD83vBddHJ3qN+dkYgRiGCrcyPfpi/mvQJzpnBrCsq+lkfeoE+NRWwDwv7huOns30HDNFK0kHt6oJY/MvUZ6LEziyaPfKutEH1+3AYeNeqRbZgICnzi/jyWUcarDijppUv2s6aTGofeg8wPfRsxi90X3pV/OcWAcsylZ5l8nUKcmJUzLrHNIGQcIM00U4nF1dNmXF8mPLlfpf4hK5j/2RC1Ej7PdMr7FZ8+DdkrjJm+KGmx10lEj0bgUxIuLfGdvu9Q9+HAY81wXRyONGIPcgCQ2/vIJqoXjVwGJoBy0NzewUUqWYwv2NBLoFUfpxG2keZBkQtXKzW3sYolNF3iV++buI1iyD6aQqvuSdqZDDG7o+cRwDh/oHCXTwHdDJaCG6+FBVKkrkHBLDptsK/R4m5EGAd8kuThswZ+HiX0xmJcd7Fn06aIxpJJn0W8xE8eZg6a4+VXlcSNLH3UnO2ALHfUAZexcd0pI8pdCaf3wnCckrUPrBhbi25HXo4wuzym5nY990Nm45oxlExbZhuKLaGEmhJXv5lEJGZzcRiOfJBrn2pArnAKKYqKtZo1XqmYGYfy73JcuM+e3SZvIfEw3zPJxRMCb9evoSUasxo0/lAoT491r2JFftLFWSJgX35ZYlv+ZhQsOMpuvjNWUOXuYlzmLGhtWrU8V6WowgBk2ADvx/GUp9V6rfd6NKobmBk8ZmnRhOpIu6mdIfMZBtcQJvLrOsN/8W0cRd4j2YWmk+lr0T9zlSee3SBEMXyiTSs6o4YnBA1EW68AgXol3XFP48BvgMJImc4JcE4doK5dAMpr6tYhL3NGUKhD6wM1ed0PnhkkXA+t6hWimFrQPo1vExHKamM5SA95x+z7KhI5YnsyDlbl7y8yeZAV7jw6UH7G9PAxRGbvkFJQ/cWWwyCtm5Vmvm6hUlgRJiEIpOTvDA/cgjzWySnR7jZ2gCk1QHo7gW3S2SAdcc64P9daumqZomI0rU/oCgHeRgAlh/84I8o/d/3ET6gWA6LT8Kf/QzOElM/qut5nWPRM+8niDdC1sXMCBMmKUY7JKAHmNaCdP/3DoqfY56Unmdy9HbRz2xQKMgH6jcA10/LPjogRvA9zX+Pf3SFSZMKE5Jx3O9s2VVjBkOBwEfF1MrUL7/PxrjSnvV1Nr5gvGCEvyn+P+QyE99wR35XIgwhcYlef4Njg9weeaMnjnRvlsiE7BwwmW6wS9ZFghqsTEnhwf4ay6I2v9k5kodbN/jlu31TCu/IvRJD3gflcrn0Hf67oQGxvVwxvyyxShcYmxcq/OpvGQP9XZuvQ+13iJCbfv+JzRYsL0v4UGPdadtDhXP/kbC09mE2Zd3eT8/+1eA1bJehX2EPhL0WbyzgEwN5cIOoFRmcMs0ZVg/nWcb+xQF7pYGBonlrIlIaDiSWfQhdcjs8dsZSF744g4BlwgnZX6NJO4smplHuMzhEz8FM6r+eo870WhnXtYahBQlTlVA8vnXYQyEIR+akR8iybRq5UfmV9f0LB2HDdJdK/GT5Vmn2f4zonF/QHCOr41KTYPMDbgqwk+2GiLxqPpLQynfl2o18TIpzTNmg4+lg6+4LzM99ms6DPUwxBgDD91opKiFI3Dd/oaSd3OEBS328thBeZHpRll/WuPxYf7ilcYwOD+foRZhMEZ65eKuGFT1+sYm45jwVEKcOfFXHqMAfm0SzOIVTV/OLo/emNYQLxXXyclWbz9EbB5slSM7wkp0ez9igFW1gYOQr1g4qgIDT3ARgfrZdrGhtkx6uLIvVsOSU/CGC+plhhE8b3UI7zVn2SzeS97hYaNrBSMrLH1tmjM6UXShp9u4AooNONJyR4+VvSGHbHxxTJpa49NVPkH1QnKOijBNX7BnIcIEQwDQviNXTadQqIUD1yIqehpKf0leI+tTZsTvK1IjUBZ1vMRauKCPi9r/+IV5/EICeU4hZLHfeJG8MYJctioZHmcnUKoWwL1zRU+ZomISV7YOfmGZdo0lfu7QGtusikLGqJrdQDSFNcmoNExyRNleH9WDtFWrY/kMREA37DWCsAIV4lKsywN2rJy/ex2qckBBqSU3m8Z78n0vV+9KCm8eNxuvhRZ+HocDrHb3M/0aVoQrD+USQVGCSn+6wroCKuabvCqaAdp8mqnlgQsTOyWWiSf+h4nzmIx0G9ie5yOhpC33Ff6UuDSrzLK9jciDcOHTZrkcgm/cKi3G8rEqJ5SrGKEaBooqap2a06vcRkqsduHFsT6QTHgSlrLEHqCl+x710Nnvd85oWisBamZbu8PsM5tHaulaZGk4wu7CO0MywjS7WAce6DfYre7xy52cwq/8rv3WCCvJmkL5MJdU5hEq+mF44sWelN2sO38+V14itf0bo0TBRMZUckzWUmF233jCkdJKAbmy/F6O+Ikm6vZfI2/Hb/+bAFxz79HwCV0WXIcEE1eTlwXflgND1j/uZcLAI0Hj/Gpey2l/iy2VDlXc21Utq7WYTiNOmKJJeiYFBa/J9U+/ZejQWJgsm85COLMZV65Blzov+DLIbzpmXL7GMFau7RgaSf7+aapv4NSbIB4Cq85h8Ba7WJwHHd85iIYPleMC+vQm0jewpMk/hMOE+9FZ9khk6WVu3k9CdPstOmfC2kbuK+UJ5CqcZ7+i+d6IKkJr4Rxs9HtWUjJ4nFMGMKXqJtYpKJ3oVu+bSOOIjJLlMrcYMVHKWoTrBolzNs5x5QhTffEfHcO82jsR9dZLmB8YF1/AOqUxCrHgB8FaOMnXyhMP0RjqOZieE8zb+kjIymBX4nAj5QvR4rgbCDFkqQQ0xYBrxKajKBx7R/HOK1J+XtTwynr72qgtubO23hyH8T7XNnp16O5gH8ewy++jzrDtJIXKvqoVIVDnUd3PDLIY927R1Nua5Ws0v6T4d2R+Tmu2F0F63qkl5as4TP9M7INsMl1NGNDtvtKQkS82+q3U2OUwLW8Phjz+8LzDwSAnK2bEbziSzKIyCtwi5n0pJuF4Fw+TfdSKsQTHkg5DZg4eL2GwDLE11eU7lShUuzVzEgG0Rj1vEfnyCdxPBVogYyqdni+xBhSOLrMUO16aCMQ6rdQjCBaxmABaXE+u8MDbtcRWvYm7VCM48IqD4l3rLZSgUfuwhTIMTy2DdrgGq2v7WpUg74AvniqscUGxJ7ZUpqthru9XFQfjyxKjCDgKZN5sidQmjUPoyQp6e/r41a1HL32d3/CL5W/DUHzhnGH85uolxhVV5Q66Zt/Itxkm5M0sDFLXKujYWjtU6S19xvFLokHu+RdGNj5ppxGfacR3YwbJoX1LglCTK4GCn2kvQNjmAdvc5qouXD+WwIz6YVm6lhZHCnkZm3SMJpUxxwfaGb/ce/wLv11XjCE1RP17YRaTKW19A+Zfl/wXXW/AWUPHvbvxYxzJMDr9SZO1U+YfMUH3Jb6NSH4yu1ZrfZIRz8MecLc43krUDzgrvram0KhJTHQWgkIMB4F8BVxRNwXTE//SYpoeGKxWWKgjPrsxu+ybXxCv1QG+1eENEbeEumGIsbuGgTwVmVzp6Aq5NACCjIiFaDPzbkBqwVs+J6p9e+sBIu+2RKOy29q/MQ9jsfeG3B5eP4BEvaO9JGhcrU5jrZDOMtJw/xmtMV09rh3N5JHkTaGBVMeQ1RdUyeL8bYR4dZ5Yy43ihIxV5LFibQ1rRaDfmqGkEpXkoETeWxhdpF7Cuzfq3CBWMsfhfjhYx42bp8zHCJR+UKPt2QWh6ND5ihQXAPzYoliSdWzZ+ehnf2iGyMDt7ikqekMplqU+6i3Mo8C3Dh38tHzS9/7MYaBK37fgZ3t92PpDomzCMTTzU164FZAMFgwNWDaIuOqMfK9k704P9poitdWRCEwNcdo1On2xPsuJbcLCDaDdX4dHqSu7niwUNP6U7ve1ocWyOvRgvM2XMZr27KbXlofZH0DA/82Crs04YH8zL5TpN0x6/vCj+7zU7ee+4Dz/JlbHZXsLkuFrFrB5+KyrPrgXTi3qW6flNUFd73Pv5O6oZ+XKjGevtG26ntnQJzGh0oWFk5YnmNG63hFjYRzq+/I600hRLtgEYlL8zI/qMxYYW8Sd3v7Jfz0/SuoIDKxiq4r5y6seCYQnZMl/tLvaYTMs308zzvNM0CzaxYRSy6b12/7BkHLZXvSvBeuIItiDQSEoTcCuHB7YVe3z42kjLyYih8ohZP7oCjUP7cewJBBXeeUntBLKBBQThrLu0Xbm0wDDoJOWyb51i5Ng8WfLuyMom5NdRc9D5Wat/XTHIsOurnmgQLwJQ6dAFPDx5q3OyI6Cshupc+ypUu5HIjlWyvnv6TtUEPIStZf+7JlfuoBQ44eE0QvtxiitrlJc78UlVvxaGRD6fKxtxo04tKjU3DD77q8apzgbGEmTs+hlMoZSyJ1DvDdl/TduBJem/rD5peH1pB5+pRzxvyhcKJ53HBiP9xcWzBJetYX2QB9VxcjRjjEub1h0lNrjD0Ten9ZV4UetuNQ+pg6PxmlLIE/XfWPv0gnbZan6fMtSlupTtPTEHfBKSCc47OObe9OKBYWiClVhr55H913ExTEHw0SBlP/aIpTy8klosXu8RfNUuYF/i/mVwZgY/PzmOFm7R3IgDemFhjyuzFwNmEGXDbQ5YBKgLAU4qOlk2YQYV4i4feg2C4Ngohik+iZMBp35+wDyASVHww944VFAKVKuRRTSTSfFgOe8VR22FyKwzZ2LRo7pdX41fIwGYkK81TJUEPKX4K1pmX0WavxsdJL6rbdyXjjx0+4flu7T3JjmB1p7kQsVZHHOEpBt6Od8kBaAExW0PTfJTLCIpFdDBM//c9KpxAZScFIfc3P/IOqB7mp46dfAiYInMb0xGJBKFBCptwH4ikf4fkUiGg8yXBP7q+1K5feEuiPgS64OS0BLxYoxaNZvF1ioSUUXV8JGNdPHU2drFd6H3SFFGgA34mt/X9e5PnSeK5+QQH5kgkCniql2+tK+9XkpoNLrSldo4FYfFWBoHwzVMyZxnW8lbPFyInRBuJQFIpUp4LVqfIV44AwH3bqn7biWZNKboIlT6bhh7sVZCxckmDlBFQB48xra4fE0rqkpwbNZrTUadZtyggrz+dwvM7bS+LivIiHsmaa13R1d3Wnej5yPzYy/EA0g/vNvO+P/jkKzuobeOS5Rt6C3DDaxM5k5JRe2Grk9lcMCXWJ0Cetxcj6Ld0ON2mUd1VTumniGEOFB/01Niwt6NWKejGaTF1UM/htShxGepIi+6mGqAfhcmGyA5xK5nkIXX4k0mJxMF6uoC8eyUWUfED65hGH6K4zTs8wCkefewKCtbDk2GsYLc763PCLSdo/CE8ctOBseCL3uDAdvvGWmHODwsRztUQZLoSCmDNmaEcL8YQ3npEE6JMogLK50xgDWDBufMWO0eAILC9donNqmSVKBIP7Wpp4JUxDH5JL+5FswI5QnbIaRViYhCSEP7oa1cUUxNfGAILyMTGpdJ6uuZKOUikwrRilHwKflZMplYZZBO1SI1rJlWJHVtehCiQ23l6DPH/gMWumJHvcYxOdXmyfi75UpE0pTIUyGbyckRW6FX4rJ9b7Hck7h10oBqEESg4sC149w3pwI73scHY3j0aotstX0jwEvHinfOcFF5g5Htaz/xghXrPjoGvE/LzkYJ9k6wl3W78lwLI1dAHSScepxHb4Y1csEPZYs4ZQsLdW7fafoosmSeMgs1AH2zii/v713lGah1wJ7S+j29JtA+U1A0XYZt0U9ou+9NosZMRM5WQNv5ywKezBwBXvrG2HBpxDZV8cj8+W1el8c2dtFSnCm5P1xXTRkJJ7iaSTYvc7U/f/quyNDcQEMYP2IwQwcojz6xYMcvgZQhPmOPGkV6xolkInWb6InQzMLJKIzLO4BYnPJwP0xcBVwBiYxGW/JvfdqAJiZ67nHO0cNK8IMeCIuWSCiW54NogvU0UB2e105kFLIaCAPDL9Ylv5iI6i0aBfCQh4cpH58gPCYMTfJVoChTlxyrAX/mHQW4AdzJKOtTjNW/3wJZHc2efUQYIrtlqzwL00RZV0iC/HRBw5WhFxcbjKtqYdlv3NGme4QSSGg9XNLK8GDaeXMQ9MJ8eyj5CV6PjFmLLuudse0cBZs/J0lln5FozOM0R6mt3fuvwnDsfb7wb1JSgBamacWU3+TZLq47AUd969HL5pmvpoxGNMIz9VHPs6PA+Sx1/b1IonhXJFgmF5IubLZwCcSgIonOUt68v4b3KtTF4TEwZ7l6uGo0w7vo28OdItb1dVNZa86JxjH+fSbv1zKvT/gXr9AEhoV9DaSDXyUWysMkwQmJi26DX0cF+0i6eUYDhyVaXfgzqXl2xJ8nQFDXDIBjl6uptgpaQ3lokjJw/DGBdYvX8s2L9MiRsuVErHts/3reyaVY/V/Dwmlv5xv2BlsflSlBm1ocqQUmZUkyFnOIjQBNn+uIS4WKThmXWdBAOybdRUXT17tbtKk99Crut1JjAc+2qI5B/eLeXB/hxoE43rF2vhw8lwP9roZTiMJYP7WGU0rYiMeXmiPvpWZP/Oaxztmv3jctuJ7INY8hks7P33klXYxlpprlJxrsK7phfRkU1wGJr96ZqZHtlznlS7e6xhgZLuywtGXGq+WsrNV7kOtHbA7fds+qn3qBRiM3YvHSV0/GALoi514ANiWH4eIbAcSP8vL15X/UiSPI7104RJ9fmHqNn19jUp0ZfZMvDM6rEu5hJiRkdFj5PncD3aASjslmP0By8sW6tuRYEApMIQf76hq8Cwni1cGQ+IdSfRC+jf1zsmjacbA/ah+feQ2fXVjpNhr5uTSiT8o931fuR5m/IINVGhruhRCMWXLTmbJs02YyL5Mm9tdxmV21PQZZdP270aO0pVJXKdptLYgIyQbhArjka/xgflL9HrGmZywvdGmisOsOMmO+5MJKyy7Fj0L7osKb+8s7dfQqbjpGxhFL0Dp3ipGKj6dUq7cF87VVNJUs0baNlEk3r33aw07PH8FaAxkp6fd0L4h1l8z0Fgz+fQfzeKLdQLhCceNQ4CqW38Ed4qvgLAE8KmjtiMsEVTvPiPnh112q2gOGT/w3DGqzLgM132pGAeqfcfWu9wTUL23fBLrgBthzEAphjqC6fc515CQj8yBTU38U4375cdQJmsauFT+AnLBD9UDzgJbXP0iywh7H85U+lsOZuUdulcphLFZFSQdkIzpucd3vI+zL031asnaVk9V4xnoO1vc7oL1+r/1A3uJI4fG6KKZYXvCREpmUPLEKh/OnLXVYeDAmuEri0GeWA4NeN6ZSkO8BqQ/FaQQMIL0E5vHKVYhOFWPc2g1IEGqcFJh/6HD2A92N1cK4grsAvpGS3YSufqxwnmF++WfcUQWn3XmKGawj15rMJAjCXMV+xQUbxGYqrNolBwgfVcoohJbe8V4DBwqoHbgQQ2PpuDxOEvEN7kXcjPXTsc6/JPkxYkK1M2Va62F9+oAJ1hSphLxri8KErjNObxQezOJHokbZlYU683tRtfh+9icsArIi75UH53AtN+XdWrXZ95/ImlPL79Zm+sdayJry0jz7V4292+/wCmU73yO59oodnRc2liRQ0UOoZ2re/4dimNsQ6XYj2O+V5Aek543v0M0wpCT8vsXaST/cQxcTib4zDEFvsXkmit6zV+RiRFkYJts9dZtCevoaik7kR1TOKfHxFi891i1b4V7s8DS6HNldJDrYJHIU25tDFQxMy1wyrEW+hfAYv8sN/SJlKIINCPGic7js5t0nSfZunYPFHyAkvYeIAElH21oMlpzsaFfx55Av2H56/Yt8IJ9VEMD2/MIwn1vI44M3DQe52a4/0Tg8wO9+/NGhRQpRqHEG0RbbVX4QNVeuN3Eruu8VqIF0z4jtZ9X+sfn3b9393mIVgvjX1uGj6hWw5Jter/NWqb4YPdHiQlHeh0D8x9YtGanpX8RnRMBrjci8s3tUQKhVjLbhm9VxgE8Pvd389bhRCNKgsJf6sOS7rR37R4qUuHRjUSqMSIhikWD2oXNnv6hyN2HZkQ3n+zZQXojMTEpw66vyC3qhyYhf2Rax8jZp+mw9LgGmElEa+Qudru4THDpXiHh1GNfgEbeObZ58Gi+pcOv3y0Jkaszlnn3hf5ZyT4pY0Ft9LYqx9SacZLPDf+eCCKGrTK/UV2e9b8REeyOp6f8qUf+00rXkAgINl6+zSid7ncyt8/XW4IynzxkW2snQyv7cq5ctMiD47eAPDYQ6XMGPS+nFR0Ys2Z0WB8t7nnAn8Tlrgy5H/rDRbpwNuvLQX1iaYmwHJvPZWB+snDMLXUZxkRfNBA1J2WMDW0/xzlKwVzXj65TCn9FusYq9jfoGMn/r4/io4zpbZxb/Bxqs5kkSEGJPSnWyxBNpL25RJAG40E/dd+tXIHNd1yHuJt257fpUdiVhHwT3Tmn5ZYY8v3QTQKUUBXIWn09bSmgoTF5mCRyTxyw+5FgvAck8uFNDWowCsw4lbjvPOpai3b2qDnjKqeuY997IuLHMwXZ78cMhcfybJRII++JXgq1gs1RiowNhqsHMeumkXa8uLWmuNaVCd2PQtwxfLi1mRY24+C0CW0IP0CXVM+foCAp+tALnD+c00I52DSGIjRhin7qM1vOjpO4ycsfYzlEK2yUOXpkn3zYJiP6imztTA5mtxWMT7AMVuEXU7x2k1Dhjy8E9LrislwUPETq7RJfqK96nCsYk5gcHuYHT+ME9obfXhJWKeHrBMyXcvSnuhl2pElFiIdKdH4Op0sZiHE8a+OApNAgxJKs8yVU/0G5g7smJXeWI6dQp59Wp+7AIJl0QDmmV92cC61Ts1sep5+I16X2aY62Vn0ot3qAxLyvSwNbMDSVhVD1OnknFcDppLcS9EsXlqKdaalf/KJMeRaph3aU0WgisCNZVPlVIUDmSOaxJmHDzzyCD8IEQGqRdZ6tG7EdCo5sCB0eo5mo3dfiiyU0g7B89aHq60LGDjrqU0mYgaaQAwlncwxzwBed5HJEtjjwiqO769NX6W/Kvi3EMRgxUEOT0bO9fpDhlWvO8LtZPnicKrFPnrLOVVPtM0zSNEqBE+A98cbo5IfnhNgr2MsHP4SGOnSwUqBBn3KQZ9rA3EqB1dSJMuVDw1ueqDp6XLCrIlh525MjnBUXxrvmq/CcYVB4xZRcqXqkUBXuSJY8bgLSBRSF/KNMSRvPiZ4aJeduE00fMPPLPVkRGnjWUjne7IYAU815JEW/oNz3LzHuChgQ6KvkwHEwh00bhoJ5f1Ed3mV8PTjl5NjNxrP9fUW3xGEUxlgOqcFMQmbDRtrDHqPl52NzKiveGdeOAd79huNjMIz/Fc9/pqBTI7yfVlaDQY0jz6wUTTMOtrra60P6iHH6gu211M37dgXa68bYcvemblO9atmdaULn8nt2uaQFbCqg+MGhU/H8A5vVii1JRy6R5wJ/sB/nR1AwhiRBZV7KRz+Ljhiti8fEoHs5svfogKXN/3nEsLnNKDDZGn0HCHkLzNY29vSwx4KgUt8RvM1mSqcW+aOHoFIYZkkm3ri56YHCeMpZ9zK5mG0R5+wrsHskdEXWvsgWKBx50NsIpsKsTnSjKLEm5Q/cW/BkqQHPVtiNULxlXB8YvPiGBsF9JsvMb3IfmUq6q3zUcd4lWjtqgNuR8cmpgrxiWFmRpXF8XbE5HSTRz4EeLZHJRkNqTpfZ6zo//X5/aff/iubnmfl9DvH+gslBauC/ORvWzGXCA88C/gnVfaLcgMX6hfTWu+6DqsDjsccQkSNH7xA8E8b0qqqc0LRMQUHR2YSnMyU8xo4QwfVRT5P28M5/ULy9i02fziU8M1UTJafjmBKv3bFQeAidI3aU/JYjRjyJoodTdKmLwGKtY2ZvnZ5l3/0x8sIAgHSMlTwuib40jF3F8sl7lxqtenDyG20WO7E+dloQBBDUU90d+t+iPDSeZmkhXeWUUdfL2IZOvLK35SRwe7rOCftA/KXHR9+48Kk202i8pfnpbPAdNX76cMoHBn4PsTTYwCixB/JIMFQjfUbBGgsdjbQUkrjjt+X+CXQVGLj8vqB52iesYsmsv3CrhgD8gbF9fvzsBcQ96DTaMKuAaZd1DYgEvzrBQ781Z6WaQhJWNbigy4qidfP5rYbpMe2H8QgoBezycXSm0TbvdrCoOk+DToz2RjjOWnWQ/FpinhpNvc9ZYXcWSrkapMEvbww4KM7lyEBUgOKfl+rLnKz0bjCxQU+b4mD91AxrvKNBJ5Tjx43+XHYimW+eMY9tme5+FhgP6HlxDRtkq1JPQep222yayDKP2kk03q2k2INyh+kVn1UZK6OfgVi37kewWPUPBGO5yc/iEgb4aSsvW3ixTITtEYDxxd5ITDOBJiwVCEBXO8hUgDG2bCBcNDh9TiD4WSze8M7IpFnUUFMOb1fyTjhHO9Ttv4PFmvAZQwUT1xoOpL117yyTgqifzd51dqGYnP8A7JtE1wy7nrFjuFsPKZndpns8zCeivwpYBVUPTDalSXfZ+FAck0T7qzqqUvMBt8dwjOti/0J4NG093WFEvjmwLsEJ3RUbSv5+g13kJuC2XF9E5hHMKqG5/ci2noQC4EFauehvpyBTn0LPtrCKvYE8YgolWLD+Px/C4nTwwCeqwqQlCdA93HjxkuF9F+LN2U/qTEt+XrjFk+sJQ+TPh6hgQ5v6h4clydJdfOw0P73TSWA7Tbs+2s3XavbfeewlC/KSo1YP2rO19XsztjOLzNmyESBxIQXfOc3jnXiP/ttreqBfg05DKkUwGfkuUyZFVQeszTfkj0mj8ButYPemm2jlZOpYGrmLW7FaoFnMtzDK+SMS+gW+p6Xb6D7PILj2RapAxOzgOfb3npbyJPa+4/xooalf8bGg+QlStXi4Q8zsUYtUKTN/moOv50wS4nv1ozxCkPO6bJsktvmB7IR29ghPTjbdpIe+DSvsF4Jk7OiNAobfe0d9VFEODTCU4GLh9Sa9OGQNvPBSlWVR9/0AZOFLeUwhjxZL4LMGigRvU1yaGwYqGEY0qzRl+vX55POBy0XmI3TaPoNwsy/PhQYlT/ZVnnG8kgq0gCSc5QsEzhhfWRvQUREd5Mw9VjYoLX7c0lJ+DNOaMdvRW8+Mj3WyqJxmkLeEtldgikcXoiCUZtFZOGEYSkymIop0Dn6r5HG7bqv0za3XQCeG89HVQ/rUwzbs8AXaydWRquaGyk4PuxgqPVtFDOI2zbJDmZU5zd3k6ZX4Yj2Tvo0FsXR0fr47vDT3Cz6NPwoWfvoGwHvdF8Ud83ErOgkfTdoOWD4yxj8iEtt4lrjwoihfmwP/7iHFT0mc+58tr/LXvaqP0b/wIoYWfxwL2OQA/9VQbXiMSNxaQOGnhyujM9jHeWUZB6lse6cupzWBTPumdjizIBLJXaxFzVQI4EwaK7Xa0QPUSrQhHz2QrkjBNAvMnrxgTpbm7dsSTUFF22rC8HcJC2jRSTs5O2TyRve+SJ+gwmOmyuVxM6dvtuFUHYL8FILKBetER8Ixsb5Yl6Co+vtdubXdALjxjyd7tlqlY0QgosdIAznMECRlplI08bIyD7/c0Co6NmBDMM8T/Wd3//oBig2ItrURgsIS5VTTu36oKcaaiEBs7DRZLHfTz67HbAbRqfweakKkgGl/fOdON17gyo/gwskfWUvUIS0pABiIwvrT9oLHuUtyaFcw5sDquLGYJkKgXNoiDOKZPQWvCxuDGkVxFSbtIuIRS0LVwXPWdp1nRgwkd9hik20OX2xRrKkg1+xEorq3gQG22GWaYjOQxu6aNg5gfbIt5FnZQ9Typd15106LA2cCjkmAAor3l0vs53ZhREyF2IQ6IBSx+MenUZyyKkUI/PulLc2cqPPZBarL49/FqeUsMv0oeZBuI6smlWlvtDv1Br8UbQI0Uk9y/OOcal8bkNYTvClT1kpiLzBkMZvm16G0dGd6VzL7ARJNeIVWUBXpHJHTwwT0rhxVXB0pkiQ0m7d1+exEQJhIMx3rU4ESer0G3Bx6qLsh2ZawcMaiSi+0ms8xNpkJ67UBOrvG/Iusn9KbZhfZflLMXnZdqh5Fje70xTbdP0vR06v36x+98u8l/QdvDURTfLWTgLDjJue3CjmQ9o7I22mUR7eTTZWKH6sK41yyelQ9JNX6ZyLSQQ0VqGbvMip+N3+vtRL0K3OS6PZ7hOUIzuAEMM+vRLTnacQg/56dmQrt1qe7JUcn+93TVeMXIw0UoTxOaJ770hD0KCfKouSJED9g6AfcAhX654cBpc0YPQVw9p9Y6HbxwBnGel6TteeGHGY0pz7gMLOf59c/bWMkoTOXyUDW1i9bCDokVJKYGrlU4fMCAjBXTSf3SsUKgOd6bSMWMMgQQOjXZWI4yg3OrgcfkIauVW6kRUe5B7bcBBeYeSIOPT5lipPi0MmHauAubBlW0R43tQ6ksLKOVMedCQg2rxiY360M+VWNcG6ycNvIaWoqbAuTl/+d4janBu6jT1aVhoYWtPc+s7TE1uMFy/CuA06luyO+3wq9Ogm/EmLyJi3OfdDyOl6pwZiP6NwpjMoyF9zwesIkDZdR+GLSnR59EuS7LM82LPXZchK5YYJaOR6r1YNAJN10sqzYqbYvGQnRQz/UQIdKat3x8oIwbPMB2LjIhKfXdNzLYFRQb1oo8JHrR7/+9a3Ks+JvCK2Mt3pASgjBTHxwvtlaeHRHYdWcKTsNObdjIv6wKwWDjaEO3tdfpvNYr3P6UYmUmAFWbekCsg6gV3JJIWiK9HFvV1T7AZqhVElB9l27qJNMt3aaonwq9HMpQCk2LAzTghrEjdTF19XgBiZWjGBxTS1HKhQQJGhqcS7b0v/g/lRMwg/xC0W+epmL/5srRnp3FXid+ILiuoWML3Bi015KxqUyMVxhRg9vAZDra3qNw29+TxreV3GQTJK6dIB0NsQLY0VIYzCnzMNUvF5tdeTPxgdh1VkpYKf9j/3PJ60mYXIPCpdE4KcTc64DDIj4CgkPvYFoxDmJXRGq7G0ht1rZ03FAvPJz2YpfNITN6Le9LC9OKpVtOUMcF47JFRoF9pDEyDIY1OK5BLY9A6HzYZvakkAyMPSFHpdw1hTCKfFj7yszKoPZYdHmXs+jJYj2aZRfPkKvEm2Sx1NTRvaxUbXbUr3GPJC3xw/RhV14G6SrmAYMI8yYB8DbsdJinSQw+SXz0Cmx/L9TUYjZVYUmoQ6W6L6HBl9U8MXPXq5WAycW0Kfg4STYJnyhLHtYHm2LnoAguzeLNTOm4aFg0+xTBTfCzU/Rr8qG4uIAWM6Ey1cCUyfz/SWRUfwaGvqG+2JwOBo38YYdDXM894n+KsqWOavYkpK8ZmFKLopHQCCh88aVQyIdlDnwpyRxnW5AYdf6yyx9M7t1fzuzJJ78P2krKaKBWx0YuA4PASCO5ZNXHjLcFtlOhL4eIfdnYY/HmsaIydTdSgk3iQXFemnbC91O82H8/i5AYUE+ur9E+5GpsLyvrlIJmW7EgyAI90n7kU8Xy291HuW6Zdiubhf4ESiBdwf4p1SQ+M/k0rxJx6g2/wyi0wxBPCzN51WSnlPw8eoe1xr3y584zhHlYKku0oAwq2fbtgAPGj7Uu9LFqBK/kYDTTNlqRw5maNoX8TCbbpE0jg/+aFd/TrN6M0c8jvcM9THyrVMlPjJ1VWYKh4hOfroh6jeo4KRun8EdGUpDSOKX80l3/EQBeJW5t9Gf5rEPIP/kMFEwcwUI37gnQsyCr3zKwSJgfggtnkAoGR1zS7gWJJX1qYiJ5gqieN2fHgi6s8DFAk4BII8ad6DCG1RcYe5A1/1A6fAK5Kf0hfPFp3k6EqZIpMa3Cw8eQkRswkU1hVruzqZ4aqHIc9VqQ6DNMmGEhotgO5UNX0eQnrNNwNQxPzKds9ztVlse71WJsjTjsFwWSojHzPHXPRk4PCuZuW0o7e/74QBSc9l+Jel/Tm4Y/lxXqkNpjBkyNMSQBfNyJ/WvK/TE5Z/yl0suiE4Q+yApsYKCIQsOfMT9cTTGPLFIIbDEgXgbcWHxpdYYLpxNa9Ay5yyz7+lccDoqUGJYjTLNgRDTCLN/QCmaG4SZr1elZuHhXUHVCKL8uPQRP31x8ExCwedVjKpKVNzgi3HK1MhBGGdVjMxZhwAadrPhOAIWMYozC6yazcpJo6+gzImxwq+yuOox5vKcVyPow2FxmAZP55DkAV7nagCYEdXttRUuJDuGh07RGKigTh6fiqi95eLA0MEbeZe90jKwHRFliOkaV0ehgykskNTRIJvw4iGP8kWEHTFmrGj/Yh3Qces2hFfqqpatUSQ5xAvoJajKTPEytHyo9fMUGC6H5X9S8A0tNdk3QgqJxNYcv32cl6pubURZer+A1lDDA9Bh4LsH15C7aN4OorkPl19mQpxMMp9eiwCn+0TidhZTVuvkh+mSb59f9sCBosC/jTW9hEdCSmQP90QRT7Qx1Z7YQ4s4F1l0kbjYpHsHS5R5op7tn5KSC1KalME8XFTnWAEX6/Mp6FQaeBCsV/Mra9299jJTusFLXwkcRAV8/ak09/LVli9bExMjhYjdpJgfn1/AvYHY5DTtlLKH8cVBCDCSpBVhKrkDXS3BHS2TAqvsgyagrLUkgBW//yf4f6fX5Jv3cjK8SsKoetEcESFMkpycvTUX76781i0UrSs6EKOhigzBi4X9T3Y89H+iWYBXB1D521vLLyC/jl/lIsoGXq1QoU7kB7yJx52hJFOIQ6cFWaWlNRRRFjHetmJmBogMtemYL/3mifDatC+l0E7BD1MJJfjJrldagbJGG5jMf1jTnavMM/p5SKJJMCLeQ+FWOpk8VGY7Lmzhp2dXp92zjzD9Rp0YgPxBHru0WSRdR+Q4D3R4CvUM7FOZU/KHhyt0n+PfNlSgLr/qOh21qlINOIJrtBdFFgfAJiKoAsPxIabAEfubJ6TaLsYKpqaXzeFoaO07+SKU89jU1ndLnB6+az1tUmiBPz2Shl48lXrGhDiVwqVPs1bYmi1TX/AnF17JJn3dQjiChMLZBWqVo2oJW/oFo3y1zW9aZVNLWcB/xm+DI5cE646SC4RX+GZOFii1OBTEhn4zrYz/TiSWxgFc9lZfIXz6G9yXlkKBVVMCVNUsXfdgfz+3QgGZGHZYsv5JfBbwrRe8fiPucgIP2jHRb/5Pov1NKlLr9wykjhT+/kWzJ0OnwLS9Jke04xyoOfGD5W4AVnwn66nDMc3Rg/LI9OrMUCcqBLN3b4UWx2z2qa6RItTuniTtDmwBBghZzurstQFNTeJ8AVvIw/1qFDzNhkFBQvktd5p1wLPO7vxsHDxF3wu4MPE/9TVWBKr4ik33M9sMlTIyYxvPNw7gzpwKUy6rY7CleTUfCEX//TEllC+EjeXzhg6Y2aqQc11ql1rhYHikU0H5d0oVb5zT5uzSOcat+h4F4NfWCSMiPa+bX9FSNDv9/5KmUBlzZXyLgYfsbzxMWc/nSYaIi4X/LO7Fc8ArvhbybxDFZV16KWtiteTMJ8GMf6+buyHh8pyu+1pql63fNnabpjpNWvuIE26Q7DNG8BpexckLbKFBxe16Xs25McGcV0KFgUmQts8ls2OzJQ6y+N0zrgxZdr8fKX+3DR6Jd7xcPIGcPKPboaGY9rAJLX6PqZAGryt3R5pGAQtGxmJyO49IQab4s3HmZb/4taA2vhjyi9SJGltO+pKLR8AZJY4mjO/eYcwG8JGCfCRdFEo8PnonISJG/FvdovNafs2AjI2hmDXZE/uaqizcNcd3B6GajgZ7KLyWpNFU1v0JJBtTnQmSDRNzhiWWM/Ubs4xy8Hiui1qsa9zvvfAJMw2YvFj5Dau2Guk0/CT509GQIAzw9gpmb2iAyfuaaTmSKvhPx1NX3bm45XTZYYYkWextbWPota+4L7Noxk5WOXTsvJf7hhYSFOKcp1qMTjs2CThQrWeORMk6M2auYUcnhjNAd4lQYnKatQSYB3NRL4bBt940HdcsspRndDSh1zRoXo32flgJ/URLLsCKs1ZfEkd9SG1vJC6NLNnYhT6EDTdD4xgdWoL58FPCVlxjsq3p37vc4fTP2ixHh2GjMVUO06JIxeUjZrpXRPPsSOhNixbYiL9TWqO2b2XxPZ2BQcxvsbx7H4Pa5/IwquLrxcWzdoeUoiM03VS/RXdcFPfX+wtOAf1t3Rw2S3j8rijZFUuZOZtUbBSDe8CeiVXGsb2kKkLi4iFSJosdYuyl7pOSpFhltvMV0c5bHnp/vDfyXd4VDJtXLhcB50KMzHzIcQC/DaHmjCt16SYMA3StefQTFJVB1QrBMM25Z0574z4b2mok2dycPr15dJrOd9Zjm+U+hGNvcrjNRjSismSZYlBCxS0idbGi1ewRgZF6nR4z2+pTzyvtS1SBEG8bKi8LhIXuP5XR2SbPPh0wWyTV3c9eLYzlILhiSrrutDgcfJy7dYtA4tmekYvQ2RWLSud7gWiO158qyZeOHaMbMkLDqfoos6spXoqkRL0L1t8ar3lxaWPBPaWh6HMFCYS3BXBtSNTSqj7AUTKo7UszAGki9UtcHdfufBG1a7WIfUxBD48bT12SJOd7/YAf7uth9EBTPlkmd6/8ps2Y3ifMpmSVZTfDus/WbB97avYdLisdLktVzyaUxEc7Mzid3FZog6JKVwQp2VbF0ntZvlJyFFTFBl7wTKK62qzZnD0cG+45aNKsSQxenEI0P+En/kgTSbiARvaoPF/jLsdVULI1gCaL4mAub6/X9duzN++Hf1LXxiYVM3ZEL7q9WC7M8EtuXSvVWf0lsZUQne82ozAG8d/gWtcFceFELI+xIaXYIuW7Olb5EJ79tmmRSmQCmXrT7NivDrw9O22+mAILUUaloyyEhczMhZ9qEnoBkqGMVkA0r8r5tcHLeHO5DVyCqyV37t76tcJ+eUTSNlS3gIbqzaH+8URx/plsh6SgW8Hen+kAXGV1TX4W1qb7DQNV1IrAMpcJq9mVbLOQricxFVqarDusMkPGfsejulIRk1qIJDCt4mPTf9cgnNoF/HjSOmNPcHvXw5pvQ4j6YSx2IcRz/AwM1FPod1Qkz8lxktaDuxxd/1VbLAyUtSQPFK7Hb6R5IwsqLwJXYmwNVEQaBZp2IUB8cGQQJDW+4MjcTo1TxV2C3LV/0aR+q7DViKr/6g5VwcdGhpBPQGG2mU1qtCGup5QylUo3iHNWfIpUIsxovfgi43DZG54CpVBNAhJcitoc13Nig+ExPuxidibFNyilV8G9ho33SFHegzwZKTk5dZSKV9UqV5QSQfg6WSgm59jhP5cRIe2HWt1UAfWX3PANlHDUXX/frQ8IpPej/lfgM/o0oSVoSE3puW4Iv7kKp6tZKHlIMRW1ThXSNP+QwuJg1fuFbHLxDjFY4YI79vFoo2Z0WXa+GUdd1eVMkRgtGlJRft9rVjiKouT0zY3FMm4EjVjN4LjiJRGTMqq9jiGU+8jOL9ha0T2ISmyeC2mV4ZfJA7XcAXMWnxCpaco+lE7NtFWriH5GX6cwXHjA1m4vdgkud/0GIHFugx7Ttc5hmdutwJbeAYUFQbtxXTDLuJIHPU1S0Vqm3WEoH+NQq5YM6WucQRb9RWh/AaPADy54BnREFnmreNdraG4ny+M4gdSnIuME9j4kw4b6wdmUtkwzS/oTvPnxck9CbeEvtOMfwh8+26q8x8jJkxQ0VP99nqq2XqWRWZNVyBcARFrpBiT+eSp08JuRUXYZM3qpSRHG5yCqlIo2lQP7yfqxyYp6a7hes+fqDfFM5ukEgN1111CVeXFiwEuhEpaYla6Fjy4Uym2czk2h0yr2jInwR7drjULFzp8AZaLsnN6iseZsNc1sSC1GeTybn6n81V4DP/UrvXIPYljZO9lfNnbDQlS47gCOZrDIzagfiWpsOq9ll9Y0Q4YRqzJqiaXx9TICLkSZLoPbjsa+5zhd/jeRHI2OI/qc/vm9ZiNsmxdH0VduZEEJMk4wCwuoAXizk8P6HSj1TaIm/vIKCJtrf8lvU7NhRaZQpgeG4RPiA0Kf416VS6Lsp/GyyC+tpEayxBHMOnEKI1JLKBEMfpJtkGHTWl34Fgau+d+/d2AzVLAUEcUUjDlhwhWd1/e4F4FcCFIN/srmoogZQeoRK0VZ6jAnJPFyCjc0y0G6o67gU5uZnDkatO1iuiL3FgoyzizlcTAXzHY23tc3tc7DrqU5NMbiqEeyEy8Cs54wqKiOZ1Ok0GEtlXIVi8MFAPz1SaXWcO8AyUpRXgu/viYehYBZ5YEtaoLLEizhS+6lj9snXGDxhlLqXGXTJ6sUohdtjMMg7SBb0k65GVCMJ6TJRbVkhna7XXFKD9DuKrg7vW+a9AQ3JFbh7W11SPP0f6lCVjBKWwfBhGznrOLqqbJOREQO65CoiiKxP1csiPbE+0HUk4Kzlz33d5b+ms9gJzqXcpVXleTgdjqzZJXsvCGmIwaVqGrPq4n319aiZh8zZW7P2axGjMIrLOFEhFA7quKqx6LPcR2H0JelyZjMpD098cUW21THenuioBgvkIiRmn49BbPXx97AHlciO6w/tBKBM/YhwqqngI7mMKKZl9stL4B8KlJSvEbOQvEyx9VAffNvOXb6+6nFx7PPzB3sW+wKk5G43bqSWmlmVIhctUw+UvDLq7MgBqnFNBnVkM9+9qyTIRT0NurHkiyDhtiKr8neR+3Na+69IAJ9apmtNum9u5TnYszWfZn8raArRVUXEkAyP57olqdeDl/6ZTldgaBtVaCvQJOFPMEP8jwBEum/mx3nTj+iy47VdvKXwo7Tmzdj7cyc8WWgn1hzBeFPIIPgJxX64tFhjnDC+HBgvJ22NKnngkUQFZdCaJA+anyI3F4gYpKMZOP7fe4wGRbq27PR4fzf3AojSFpLQoNoEM3Z/DCXDUVel08e1AACaqjOyZEN7NhrEirxf9zYs51fB8EMBWkNHJZgtTDy+gcMlUctpFmMMsRDSLavQ8fOFF3W+4nOf4pML88mgfqyEBx7HNaDWGv2hW7SJGSJkqP1U0mtxlohzkoiGUVRPRARJ39K7ygtwvFKa04dvm2zOvDFIryfJ522IgGF3P6AliZiJG95avKnN5cKumgoBKC5RkERucTC90KiSTo71g+PcT66rZVA3txtHt/C6qp5yYaFqxxMYoZBX7jQ3Kp79imsfPBM9KCJo5BbOATlYo30/c1fsRaFOODuHb1tE8Ok6hxF5RaPNg14pvO14wNPf8Cq1Q7iaKT0BdbNXE4DIYYT9QUUfB/6U6DFc2XKQ+ZDraN0yaXE8CRa8t2DSboyyYBT3wlngBj+COkgldJJX/dMgSj9Y2DGUg/hH6PnAHByhAqxSrFd3VHyHkyFWxE951c3trAOLaPmVEk0KZ7fg/RpIqUVeBwr3C8d9H+6mNf8eW/0CZOHg/5EyFs3sbzMpL7gVqaz54xeT0zBpQkrc2uRWBMcXlk5tGE5wjH3VC3dXjU6hldZOkBBP4AkHygeT4/szr5PMbqf0cGlcYNJGx5CSJgikOGz1bBDg7MCHX5DOSCLOz0DsGj2nF/fdjUE6uDxeDYiDr4tzkhNAXdIHftLnY8wC465iCxq4A0IF3pZgPgH+FVGnFG5BtbXG9orgxhI998Zewz5UqSJE8Cv5yQQ+KKKpw4/6bmI7tgozUkb07LU324RR8k+DFxpa0vDgtq4uySF7RU5ra6GnV9ZDqmR4+bMksJAAh0WtSx9I2kV2SW+cKzFpnIWTMPI/VPeFX36E8ca1gMwwhFfFdTz2R/WqbLwOG/gYj5wWssB/adK4pH+uWMvrBA4LK9GBi30MzOxJPcJ7mNuH7ZooxdRT8OvdMlroY0UCezE8fVT1mJASj0PnGkl1a27eIn+xFhJ5h1pMuYggkhZWB9FAdClCTQegKr8+8HcqrGUgU5J6ua4GPrqJTvvp7JC1B0XsjInY0jKAxHn3JafezL3kptAEp3c3nXhwuIcJkS7oqQp3TF8ynsCiGV9sJ/wJB3FdMyEFQ+uNptjG1sZQRVxuzvZ92HevkJy5d5N6zBwhSxFCFgJ8ECBFVP272l7sEZI3njGoenxlq/KpqWucRak94i3j/pIk49gQvS1kSwQg1ZDTBPUwSjWUbIp3ZycgAvmaZ2ZSkwryME2qHZLUBkg67s4AKd1KSG8BbaEI2Rb5YKdAJ6XbvWWNmtpMfghi3VonRI2o2bOAaSjfEWnbHH2X98k0MqOFMBZsArXNuRGLrKJhyL6EBiBeI5Rm2+hqCFYRWP9q/+puD2c4kBoCrgN0977hjrQnKtxxU3nFI3ogXbnJvMK4Ok9hMpjdl3I5pV/XVCPKOwMcgZZGerqAoq9uCnTZN8aHIUSc19M+pSJvnVjUdb4FGQ8Yw22Nxpo5nI9inhCEAXfdo8LXno60tbImwHnOian4dmHEW/W9TsYphQURHVToSgyoJ8qweDJ2NdyYYAI8+seTZ8oVBkeB1BrNFuHJL1N8Q18GYrPD/hMciMhSMVx4r17qP+YV1IuKJh6MFZDDrmnImt4C2oxNMrRwlAABfVS5WVJhLmgHdrXlF99TcryhHQsFwJobWqwSxp2+zbPZgIoBvg2oxEJPmYh0lVg9K9tFgS92uRLPMhB7KwMjgsvf9eUDXJtzDEvDpU2YpEjvRZJaDqheSN2OP7+NWA9zI9Ol0UsvQvpm2fyyJ7c/Vfeq4yNW0hknJxTUDD+/nXNsmDzy0K0GAaSC+gM7sfn2qaHe7GvX8vTiwcSWHVFyP8EUls6WmatcVeLfAl69UjNVlRxJEO9W09x1xcOTshQthegLFu3ZFMkFgB8sZTgs3WJiPYZb/0LcJ3TYpPlnYaDwDU3quodlAD+VCn6LGMqRKdYjOzIji5dcAu9jPowyeFbLQIWJTmpEx9B0Mv5YJxXqDWeXYWLCz/5w50+4QFKRp3jepECS3AsCk2R/kAZGN5t0E/E4O/iYz1shqEREmhjgeNJR10uLN/gduqUs+F3DN7P0eQWVozUI6kjL0kfWNH717icOd3aIB1CZ6lj1vizR+pn0bRN/8egfIzjAyEr8yWM7RzGHcQat7CqTVQzkZC0/IgzRDUoFlO/Rf7nD/f1msqNwUUfvsUCcMY3eqk4PvpzGg/zm/tYYnapMilvQIqBr8Fh9whhrk3rAdDZmwlQQFlmvU9YCTN3HfWd8SG1lRIXp3ex8ea4iJj1stR37uNQAJAedMFBdsdLbiEM7V9cHA+/L5FEr3TJSn7bPamtz/8xFLBS85JSMtRRW7+WD/WSnwPtPTTNWwZ+wLy2rxMpWVAdADMCwj8/ZSwvAFKeesUdgOym/XFTvDjtjqJ5K4ySFzei13bfydcCsRZP0P00T196lZMnsz+PeIHt+CdflXFKIOFy2IcJRtxhpukpA3eEKLTQLJTtOz5THfPVhcPybZIThD/b1kjUYsPNWbIqMlv0R7ERkyKkZGnA0ReWk6NQmXEOOKgxbavfNIIaKnAz2MSDLdDHaRZZD6OmSy/WKMc5KXGD4GZyMM9x++TbzafE7sVmNjni2o1XViMoPg4ut22HEx2bORU6TRqqfWt22LwhTh0D3jb8nNnvDD2l5MKGZW4Vp1xWHxNVOmgCAsFnHDYZaTSGSund2xRxsL31sWKpPaI5J55vpAvGcMrWYFnnM9GPsKDieV297SO6VG4VYzydODVQqnV9kKRAxeJzHQ81/ocum7HsGQGJCt/RHj5pbHIJksTFOoWlZnmFwJr+sSEl654YVTwnpAqZEH58SoyHQaVHElyHhfzPXwyNzR8U/jSlOgTjeDcEwJRSAqSmcCnt46g4JddXBtXAqJPeAj3Ke/UteCWXaUqplfMu/epbMdeRHxDUAfcqKP/EyQggZL6zvZwF/tXuzulwnFEsNwIeW2OEOE1C7jxgDvZTwUBUvY7scoSKm81Nrsq1KaP5iY5mJefEluAW77JF971HtigHF9XqTLCeQ+csU9dLhxBfFKl4IWhrHupGlQkvVheiBIXttTYKLS9dFBdmwJT5nOsC0fUcm8t3tCXnSejKltjSP6PjZR8xlkL77k8ZUH7fyA3cxQA1w5ixdhBlAk/o5YyJW6GYR/6uwaekMi7yJHpT0m6x9TZqUzQclGg+ZwpX9XOfxzOfmCi3U50no2sIbZ622cMCWmhHnW0ApTbrw2ndGefqHcZ1xFXACJSrAtZsJW5pZhF0+fLlLKQKqy6JiUiVaRifQPTV2abEueRok0I9VkMbjuDnxwr3fSOSL484JZMf8fufzpVxHl0uuxS/6a3TiSQgXfJPydq9g7v23nd3z4XTknNcx8ZuF+x5Xv/Zi4PgBlXKvcWfDI7n6MJ9EaF35cJXejaRkOaYQWRuSD3SReHn7WeGo+0Ju1TR6atAVQ9SkxDn2C1h4IDCmjuEjjDWRlj4jTo7DjSHBflEOnc9Ylu47Uv/fCIV/GE2zDUlhR4A5kTP3OgbF/YcACWGW7sD9JBEKpOBsAX0WeoPO5BV++8SMd36Yxf42lijynj7+OmB9pf5wiHldL4LI4mG9iW0sjL2JinPz2UBrtBvthkXMYQQAE3BJ63bJ6X0a84bEI8Axx4lA4tja+iEj2P00ER4HOp9nNbsO4C8qrx4FCwIePqnv8EiixMI7Jfmemu2BOH70iaVpvZiaF9vxRHHv3GnGQcKThlg0VNpj/Yzq8KXFF4IARF8Z4KhhhsBakpbqShLwE6iOeluLdx7DqX1S84FBMt+uFxALk5MBi3Q+b01ll/fT8w4cs2KTQ+2I7zCpnIn7yEKw0M/6vLBrYAMJf81sP8YY42QccojGfirFOvnsih42qHCtLRcF/Dit5T92bcI1UqWZpRq1upiWl3kJaueefIh4AN8HOyrMtPI7JYz4Ev/H+FDSbhS6wDRIhIdjc14uUxNE/NhklJErzvJCCzwhmCZN28OqIv3PPSyS8FgA0tUWF+ZjVBs2EgNlvzqTxYCVrTXCwG3ASfWdC7w2gPNmlEsyDpDLGlo33uYgtTMoF5hNn25PsDzCMPHvTW4GTMRiLGgaNJfA7FtUO8hOXXQL9m7v9aFmJ8tMjrFk5E7uiza17A8lSIfD50DpLnp4jEpbNvwa5oob9htCEYxEEn3eGE8lZDvlsHV8kC8fzChMp9OSl1jtM2pF9XTlg/Y2xxN3m2GKipr3WdT+MzuKxXeyMoC0icuDzSg9BufQKBa+qzpJHNr/kPjs3vyhdEXA2CR+W6b6SlRNp7HYD3uiznNUUnGZKBrd0fp+dKoyhF0V6EVEKztFG94LMa6b0ENbM/FS4GGjzya7gb33Cz3I6lyX08p0tbre2/71Uf1Ni+KUpNCXNpTQXIjGfQrnpJ1ifk0APQjbHG7geP3LTZPT05DmEITpFX8NbEqWkvFj/YVgUXgk0kavvNsI9H7b4jBVlKZyu7c5kg055H9ExAvmlawOXmG6jBLDxGQYYb8hhIGnTRFWbPmt0lMnpFSsAQXQOAVa19rgg2Zipy6bCzfUFCJJYfQa+SC2EsfugpAu2HqVjEX3N4pz3JhJus8+C3uQTZyLX1x3/U+ZF6VOAIXyvZ2nS0iLP2TVrTOwSA3bJeFI43uPtEs32NHWPNt55l54j/GybNb7Wh5T/Y0jTzAvg8L593ADD/d4ID1U75d8bdIE0uxe+mXmPoVrEUil3K7nvDGBjN9ppvf/xBIU/8mzW+22fDt2OoB2A+XdDHyecTX8ItJTFDwSK/xr3be7fvPSd4g/djUDEXIJXIxis+xqjTz9AT6LwRVnQdb7/SabPpQ/4scaojjQfuBOOuPkbkLi/F7oRCTfTBf8lzwsEMDLLp6K//C+xc+lKwV73rVgRGvhNiBdsW2qHYEOGvDS7cWBPDNf4umm1/TulFtG4Ni5BhrymmUvrcjFSLr42h0uUdZQY5HOqzujaSJ4XTFyaSfNl6A2GCXqwVFcIiguJwGtB+fil2NVmarbGej3Ax5yhGagpFVbEaQ4ZJH3g3IX7KUZXyTknUKf+eEAOW+qX6aZJg0L41Gl8Nef62W37xoVAr+hCnkkhErD/KLf/LWqj4uGNzeqcoELESp9FVKHktyVent8UYCE9XvBvopg1W57UBlG3AN6xGtLgB/OgG8rBjBJEgSVekEM4U0ChZtcDzFi0JyKOkUTSjzzO9XrfxKdvcMP9kAaMvnZZe0Ew5XbK55gYtz/M8bXy/K7AfIKOpJZScpRIyhHeyvoIO40euH98QWFNfsRC3lj8rrygOPF41wPNEX2GlxIr8XdU4w5JJ1oaf7EAf1Fuc4EbD0U1KSOth77/BI1FW0fvFPihEuKZjvvYsF5/sapoTvLAQvLxUx2eZ7vZPq2Mh87PRWuCIXzzgrNlgI1pnHcGWk9IHZnzMILfst3GCySPYX/NcS1SkVxXDoqm01eO9XEkokxUHPXQExzcspXmI+HdHxyienrkFQQrRJxKlXLX48XmonIJB6O/OG5EJVKjk6JkcX/09XSS1Cl+8+U5tO3BLdaA/m4T4oNfZDTUEX952UJsjJxhVQ9940mbGWiXm5dUB0QOEklQ9a+yLFl/62xoknjvhmDu3TV8PrclnD0Me6QwLkZpnA45zGHq+adxTHsohsMzKe+2PxVnxoIy34zS6wAOYdBlYlgSDsFHPhLyfdw/EF+hzbxvOlnQ6y5YY0+Gi3JERRavxdJmNGDfOE2c6NGINAoyObyZCotMECjwW3u2pSWhgaFxf74w/25VwJA/WVWjPcAgjMSVk5lzttxWrf2Pg5muJTZrzdCcBaIzX6ZPmq4F7NYXGMhfynFB5u0mW38JbrxIuk6u686k742qkYditoahPGa3yfsoZ1SlIdopLev0UqRHtbbpgag06IBuvzkmmaxjGQW9eigh0i7+0mzxDw/9tFQHlzOPfFGTr5oAW7HmUFfIEVc23jSOUj6lyGvX+eIBGL4vlqkLUyzTByHGemnoMYeiovTnbbDz/kXQMJYAZED7o6/gCQuZERtR+CBAtLHnOa4rTCCSIMODObpw2PSAHAb9K0cpgJB0e9BJsXVBQZTrUjT8kiEksS4HDuUaAdew1Jg4F2MHey7JTOZitOzaHbkygo6L5FY86JHpmFlmj0POMMrQf84AEgfCz5QcuH4x18yyqd64KRRXaESCIRJ9pYtIYvIVQKvuTpu+eRLhKVvQ3D1te/4BRfxr9uq88y465bJm+MOoLSSEuOB4yiZv5vmZH4cSFL6X9gSI7Dckwg0+MmRzAeD+fmdapX15EUg5ziuNsqN0HgflWb2ts9boawAEUIpsVjX3LVYjGKQ6YDceQXsK7hB5RWAZobI528Lp5p0xfPTM7lqRh6SoBx5TcXCy04UTtaOVFth6rCLCAkSwHvsaLQ2wMpAXm55hvzcOVKqvlZLQOZCfJ+iUxGkZs9LHRCJsaWDRO7OQEr2BmccDdWD3nIJQrVor7U9Cmd+VpnKWZn5CWS2KVkk5y4Jm2kzcXyxZSvolygBSe2UwJoJkE7B3Ml13XSY70V6EROPIZckb2LcoIBxuJiYWHhEWZ++imK+OekY6zfyKz31U9jUnxLD/U8drmY6DoqUKlhDvZmB1tNwvcAhjMeODke6xd2Q556baVrzJTbRvGCK0pDNLOWFPA3Y8ixTmp1QyxY95lSCBr+Q3JH4OcP8xfYFR3CPzI0QZVoOBCCtgNohPqEvC6aObLtFBP3c5vCnP09MmBFLG1nV8nx3TPxyY9+xGbxQo+qLZevMIwSSS6LsNF0HDREPVBjiVsV5C/klrMIJZTO7+f6rcTyT39ORYnQLavM4BvdDH0MQ3r3PTDyIUBI5vVaQmkYoxSA94TQ6e68M5ZbBN6A3KKJTY//Mgtfr1wz5nYGUiuUT61EVam6xQz6TjT4GW/Zbi9JCJfRPhlTMYiQrypXUNxuS7c8dvB7LdS+TMPGJgg7ZPttDh9r3QFjTcwwVDw7jNNVuaAGJQk+h4ns890FZwa82g/FUc9/MjEf4HaNj8h0auh/xrgs+jXPHqr2pSXE3WIxNZcqA8HnrzUpgXDEckVdJ0GaPYFr/bfkc9pjWCROftcTktC6vXuoaxqFlslO2LTvatIFfaQw9oNZpQ4rpOIXrsbIwZEBFiBOMbP4g6JjAvK5TD0Ls2vetf2mj7z2BFvWIo91J9+O6hfYvcu5SV72pvBSY0dx8BzH1gnif3Jtt9hNBYCBJzi9lbZofBGu/WVfRbc5uUBGxBLhpnzYVFUoz4+oeHbVHU/+fu0sfjMxQ8Xck3uY/Qkba4xhFUfmflRQrAc7tNL65nrKyHQzuBcMloPqDVp0HNR6Hi0Fqfj0E34uBEPesmgXuO3NmBSn1T2A5XEukkeAoC1dDOGIl4uhMka6s51G3YYA6pX7Ib8r5lcl/i3cvLybDLqMOrOHkGO/sue1iahOLc+DI5uofBxN1hrzwGFoPobs7chD+ARXG8lUW3OBNSryJCCgykTwkx+gxwnQWQ25qRkY5eeI7s89VvweGHGfCtqOCtCXUIAxSwuYFSAhNl5zyz5+nWOhNs21NkimQOTLAFQbnsjUrDkOPESix6OxG/N3tlxIAKKyING44DBRYxHZ4GrNeaOSJTs410bG7x/1Rr9OS9SPc0oSPB0DVqPZwytPzTm6Voi2UBT48pXfGSfk6zmSsmXE0z5++pjJ2B/JZ7UKe7/eC/z6Ufi9E+n9NzhaI8ctUDNHvSoNHAk58k5ybIjDGSUQfSFTcc238xDc1oyzqeVYOB5SOMFsq2zMUktUYWapIyfw9nQFEhrjBwQhog50nxIzLDy1j6tr6OkZR1jpKk4znHtizEPhRTXP3sLYRGXY6CbP9Mec3hR1hkrc0lcKNb5yUrgPzeufozUNPtIkI3zPaRiNKtTddnOl/WJo9vpx6S85ADq29OqMlNoPeh9v9Ubk8PsVKo4euUh/N1Eb3Gq3y0rt54t3EGvGM7iHVS/4JaMu5gj6mpXAgIXomjq7BUtzaWIrtUcv/3DfTTao/kimKfB9AiJ/HM/93BqPpc2cnxYMOsLOhTZjqznhNoiMLwxFinaJiDakvCeBJj4DiCfnuWvb5jUwA+lI5zZFWNyq/NN4t4dyWXDQ334a1CxgUqAb+jmhn/veXTPdfvBoPlrJL3sdsIOOeqmIbnh2ZSHOM1MS+GVXPsWtnjhAt67MOzyZVMLhppGYrmTSqOerCdIEI6x9vcFGTEI+p3oA6gxbuWUoPZJc6+AjKP262DP7qx3r77slaAIE3mnklvRTR1k1zgXh7dc2EJ/hWKHbIfpkuIVOCLwog44gid19igMcDAeZRZ6EHgWRoTLvq8U9OAFt971pmRvQ8ZGG74WvWwxcaEmfL38tgVNQv0TdFyXnzYiiqY/NPUQq2rP12HhvMi+IO+ePWnXEhCHO2nMvNlL2tIoR5hkbpo4XP9OLyvIC85X2SCNqHIVYPB4q15iqwBtWzhoMCyl+TXsPsqRXfzMZ/VX9iJgCh5PXhEGoyu460cuXKd+qR6ZFzKbmhui4TOk21bdu1m5F3yaiIGyYGRiWtk13EWSUWyDcpMkjiwf0TLpz0IZ88OwUDdROfAGUZ9LEY1TIVuwUCm0HOg2py7YpU1o4j7gpiveIZdUU/NuUZJn3AQ3nUBWlwz9Fjz9y9m5BzF4WKp0elZ+PcMlwZJ5hnS9En6/nIC8p8flpgPu3TSy7W43ttYLO6DpnzvlLhfeWBke9jkm/5xrX6O+P7q6F1GdnAKPg/CfU8PCzqGGgmig8re6nXQ/NDtmGawDX1D8uFtysEW96BLNPKAKUzr989d7QmH+/tRNrOBtzDUhivQnoBzaCcZy1DiwSA4M1IS3kHjaXA2fg9w6jorRCW8HXXblkke7lSzky18ed/FSL9FfTwczw9oYbu+Ao8WzKhP7mvqBm77qczbeg5X/Pk79ovCc9TuiVlOZPRVqKT0zRB2ZMShbepGJ6DWbT0XbU0+dFTOd0CI1WRtOCEhOIKeGbiFysZZir9JCpQ2fztA9LxgjPZYSGj7cSBhyBTZMZMSxqTnFaANUwuFSLce5ME2KejXa5kHecojdhmD4uiF9s84un1pX7BNtIAb7ZVpP+SGkTxxOhX3QJOTX9CQUoPXIOycTlR7zora15frJAiMcprfm0N+ysd/ht+NDAewrWVhdYxV7vRE4dwzpYNHRXxKGI6JHo9JGGPf6COC9Hyo2kv+6i/0fkAmHyd63SRbbyOk95f2CgSBxrh7vMPKLdk+qSS3G1QN5S5k61jwXYNg55mHYMOfjjI9zIIHjuyu//fNMQlqq3+RNMV76iClfn5zidpnTlLsDwitvZTrJN1DsKRgCFypSdoHsWeQMeWHb8sRYN1AUYYWusL3VIFFUP++W6JYlKqGV/jCNgHpCQUCbY4Ap3gVFsEzXwUwNU8SsOaC0R85v8FXczmpyePX8uU/IhN2nFOBrp677pVeAxLro7ko3hcxSHfdMMSN1uTidtX5IfNKFwtFuZAGi+ao/GULeBtzd0LHYF8inOmqFj7dlpRFyYu3r96b8epT8+G4ZeQt9cbjyMZFpTScxNrBp6Oey0DAIOyrDIoRit1MAK70HAK4hgh6a8oIOXmOh55P3TgU31gfH37D0BkLtjjuqQ/eojHoaGqW8r+Vdi6J/DzGS0TxgoIMerW/kG3fMKYDJGRPDr0DsYbf8qz3WwLV4c1OAbaiby1x729K/FXQIVOjulGMHZAGeuHMdlseQFp9kBeCo2nfF838vH/R9ykDtS+XBF5V7CRbpzULil6gM9npbna/9KGWnNjSfLB/RDaovuC0RjoqlOcES9TSzIu4jUwZCim8rSWNgCgDn6hSaKdBgc8t62J7pfj7w4IyzeoSluEVOF5qAo01fKZ3JGJh1dqCqJzdUg+MA5KCk8eBZyBRCqjTxajcv8c5Q/IQd56FZzBc/ql/JONNJoHzcsRrTmfW6ZGJqkkBwuuWhReXAOseJV7vosD6WW68koYUSZDhSGic9Ik0+tg8m6ll2D0poGuYep4NTLBXQ5qbwwUMhg0FQn/UstmwkIHIJN0FehMW+EhqIsvvENeuv2qXQAt4ygy4RbiesmYq3aq6LaOQilQpv/anVaaEcxN+A1++NJRDjv+HGgOZ9iBIgaAG+77FsS7di/N1aSiMPDcdWOMzpmxbi6gD3hVP5bEgLmXfULQ32iCCxzpa5Co68BBfaDPx7vQckdKPyeqJ1Tg6i+ReOhgUam7+wD4Z9CqIYfBMJ3J4Vuc5meDOjSJ6VALdUzECq9a35St4xqfIjnLQGr2tPA8v/d3H26JXM/KjLM5DOc8AwIYRa3CtzH5k14BN09TnhDj32+bh4HAgQBWZLKCBxHNu5XTahrFyfu16aEVnUSA8IY8MZLMvGiSoblvRMwcp5rWsttUDiBIkRqrt9rYLEU4090Xusfhla29WdjEbs0iIOh0Y017g4ofaNtKwVenPenEXSj2EHkHz9VhBNTjCew7rE//uRdZSXAsHuvC0G7FSkyfScEaX5KYTW5KIJAou7eDSH7rubi54YuevQsAP2xj//14KjW2K6foqLDrQxv/gIMkrSSMvXKoUoAIeTKxzchaeOX/2iDP1LfhgfLUf7+4TZRoC7RRgDmwoilFRI5pi9noVmSKvN73T4jJ6ax0ZUgwhawFRjiQGp/w/R8cxELSz9K9Xg/mx6PgDSJsphizd/oO0x2z7C5/eoZPGRsVGGsU8dFmLGx22IsLzn3AM8/ZlKK76szgLQHUUYvDbRmw4AQZBoegjUYODwHYNoJ0sqR5Ch20KMU4E4E45lWbFRynlQvnRwh5BPPj1WOe6in1csrr0IqIQUxAU1PYvgVt9lBASStqnYitGpR4CJbPu3bf9fqLxeZmkU5Jq2GcOpGa/Nbo82vBxlfus6B7urbxrpIEj+wp/Rf39IAdD/7VMOJAavUge+gikw955jOs2pGgdgHEDuS7/D7jcoaGEuh0FZOqI8H08ORdGpt+57RQmQMsdBVezSFV5FpCefcXHKUWAjWr7xP5+q5UzZs+cz/BDnbuVpzM2KWlePw+XkxJWWEoeCIRRPTJRfbqmc+qA+jbj/nXm3AICRvLMXd3xAMWF2K7QiCkN2UwfLQYFKDg36SG4e+DJ9UeLJjk/9tikNEW2fW+Ls65hydrGmi+DHGYMRmBL+T7BJHta3foprkG3aL9Yhdudru0Uc55GTAynqheAig/56l7TdIEiDqvhqVlQIqOaNQxdMiS2e+RdNnDtTw9Zpl57THr3gh6RshwgT/4ktIvZhD0tvJj0lqgZc0WV3+QwO3TEr0q48LvmCrMxyYa5Pm9clmIzJCSjEIbq1esO59iVXjeyah6Ppzs5TRTugq79K8waiRS1T4NN6vJYnySjE9lE0jVduLG5881R4UZ0NxCgJEikfQAZOeB9RSlHJGvDBoFEnJB5KgsOiGXbgkYTxWFz1nRu5774teYfKLu47dVyD9C8f3PQq4J9s5lat4XZHiv/kve4GZXO8QgfrqkTgR3Jt65mHB379Jn1pifZ6A+UZ4kaYeJFGSbizqIebPevTR0Cf6rKL4oaBmGqZpwR+jkoGa5avhqAECpfbRuXBUQpoJ9VZq5cOaTyaZW1JlYydSpU1v8Vy0GSuWuAdhTCpOuH8hWroyg7DVfqE0RPNDpVY3SClE07LtmaI4r/5kcXdd+5GrLoMsIc0ATk7sjFXLjde23IHObmyu2HkZekK7v00Y3hIhVMZ3XQWeJLrm0dqvb6EUpSt2cNRllscIoKlHXXD9rOvxyoYgHbNyo8iJyKmetrRlgPhBR+I9fMmzdQdP+WDmJFw63PK7wtpGVi4egcDi1S16JC1N7Dv+0Vt8imW2hpF6qn02SU+B+n99Zq9xZ8aEeAE1YzrL1/jyz96aVkno9wxttaSKt34IBJfCBI+9UCpM5B6WYeonYHa70frOO1cA7uAPsvsNGYhZZrdS0JZxPZPEx0PLE9GlRF6nKKSltDCmlfUzTBV2IyuNr6o6DlGONQ8/9AfjfUJm3ezU420mTFuO5sEAIvPXjU58YvSoJ0JzKI7EjGMEJdLkbX4tNQ8o7knREhQIZAPWRK0+V3TEM5d5iTmslgFqu1Gsr4+ju87VDIR7hLth5kqKgMjGvaUVO5ocQaVaTMZfV93Hj13cMxW+L63s0fO56FbFKujwBEcDc9mV0zQ5rZhQPjSk5AQssPl8RuVFP8BrCdixW6Xm51MJQC/PUJvIJuqUan9nCal+TTsI8c6opcbSXzcrbIqrh5JTQcGgndeLc+fBwflDrc9r4HqB/OVq4C9aPnmxBFNSAMol5vSfYaphKg/K2fhWD8s87aDpXuvrhNgDA6cPiV0SVJbpZuNlso8PXawy1nYFi5B2XkZXRpb7J42mU/o0HiEyDomNRbtbEe2bvts00HvmIBF4xkQlYVy2+Ae6wgHDYCRVdHMReGvo8XVyItEmAy8LXlAHUDTkjj2OQGShQ1QZx1s75uFQvkzpeNxdD2QU7QworBTKKtGksVxGYFQlKxOGcHua72Sgo4Fol3QT0Cl6MAPaIZIN9ob4uczgwRw2TmxtLX0bZiFaSCMBrIhnqHkd3w3bCID+tVJBmKx9X/6omk54+XFaispVLbVlr7iF0IQl/9BjU+cwjFpdjxMNCZLAEmybxOWHoS/8XrRS/Pu7OEGcDEIct5Jne4CD5Z0aGKvdPZZMsLHaAQ7gM/6x9MQuGDYnHVkwge+YlDvGCEnl4smUtjWvWh/fGyJOp2t0n53JW6UFOfrWX6Wdl5xpUHppe1LAFTkehZ1hhlGvNzQcvrNzQg9EIQUOVxLnYdCafGpefBDot5lxG3HBphU/CIJz92a3v/5ouI5lJ1FqyXruHkTzNRHH5okfJUisvpcAxfUYpBAFNipHaiIwBDTiN6IN8fCmzmZZy9/0PZ30XvWuAVXv9/tYoFVANzrbJGqOB1kn3fX0BF0PuciPUYxm5w5FU8aTFIl+KZYAADp4Wf02MUMvqtcX1UyLkuLbylfcIaCS+1aDORPmZDlbYY8u50AOOt/wEWst4WV9kq6z5+DGGdshtWB6nwq3qxt9hrvSrujbNfZ+6SsGbYNfZBvgEsPot+VSlGaLRvhH/ie3pREGnx3pm2vdMHI1XRLD2wI2VkN+VRypf+NgsvGloivyH4cVQBBR8bSOUGE+VGjuiHIJE7EyNQso0PcqS+lhBWL1LFiRO3g7eXIw2kK5ySGb2EcKnYoLAWn1LbMZtDVVlUSV4/W5KicJb13etZuTfwZ3GtZUw7fUJ+zn026pHWdh0WrRtxqvrzBazwNxu2Uor7/YuZc3evxEUT597MJARKeamqMdLwuH4Mpz3bWxekZVoYy49a4RfzfmA1VNBgBUQmmj3DSNqlK/BG2imDtKwQvTKN9nqAi83nW4GqLphHoWGhC1okREoS+taKj2q8JTyXfn2I/RE7odJNP/0bexzlOMsYNFEjNa2Ste1AYkVhM496UK2jdNTycW9eraKwqVvWXbzqGyHyuDi1c2D36JQdGwzozK6clQZQ9JAeqeLTy8QVYW1TtmkSlD6dpM+gChabY/+sKaYjAyK/xscODh2GKKYEQXXm28bZIbXq1YT5FbexBMq6PvsPG43AX3D5uSsNuAFvmDFkxC5JMDlYxl0S75oXFs/98/1rzSm24IOF8J9R9hM4W+Z6NxVuQ7X7pJkLO6rSbxGpmiHjAQbYWKYWKT6WAnUBTb01FNy+L99fJ/mkkm/IASmABrbJPo0wms9vCIpQba7BVjFCBXbUNXo3y3yn9MpG4+rgtWP28nLSnBxcYpygF510fjHBaa1z8ykgSl38JTc5KFaNndz78rEcaiYbqMAgX3QuGHwW4CciBcoz0G6/c3RjwEb/ilT3k0aqUTcX8O6vr0Ugh8vsA53kThN73xsStpCdmXTgEgBtRu8LcjgbAZC/2dI4TLdzCqwfUZTfNRRZcZTEcfQFa5vhfTPEaaF4wIpwMgJSnvXBG58ZH/bhC5aumgQADsAPvmVrV5GE+amPqG+sByt3iQTXqkkMJLUAOEENOyDq5n/aS1Uo9wz70HXeZpIOTHQTDBDjIbEd0tRYZ275KSzy51Ua6NFSwLM0rqzhEkBQAimIYijBhjZJbKEKjQfav1rBlHb6SCzLy8U/NvtHTwoY/pjzO1Gzk7LhcnCpKHl2owpwBPoCkhDlgS/lsEHA1HJoLblVhoJhkqAHcU/+NCQvFcRDs/2RnS+35F3twuRi0/iQ3ejmPljX3nKn+Ceumwebu33o5/Jai1eOpX6E8AEDQpUgfW6sgr2ehujY7g4CGwudS4n/W5nSjzfK3kOyA97n2fRNX23lu+3ktGCSZdKrMOZ/NJdN1v18FvMHLGUwPniw6hyRrBn3h6UD+g1W885OMf31pTvSetHv9hCfnpJ9VVjncT2nIkhZkpDzJLwFWfXkqySqftcIZ09OlAyL2mBYLZiNsUkbXdcu4S5hKIry8TLL4O4osMn7F4HgG0kL8B27I9Z0pXbvlk01J8UPLpaON7wDTnsAvZEF+DqKOChl2aYTW4iRfl5Bwf1bymYV7luyDUO00cYM+oBHTBQeNHYY99JzW9OMaQTB8v2KXbTTx7bS+YAAjYn8AvzGzK0hlPd2fJQpE92yxJK3s90PDSPmS76V3lG/1S/rpw+gjYYU2ueVdpiMn6zdfA7xNRtvuH/QPLjnbxVXTOPd4PxE/oFmE/4FYfNyll7nEAm+VsX/yXYVn9PE8+BFpuI3Mu6SD2+wmuSo9JOFKj5YbvEDKcitti+JeocbyYPGLdZkx018jnhNsI/vyPz6FmhY8U2oPLbUkHekLpBWQGEAiiXpJfiXbKNRpXtdVYAXiD21uybdGEoGLsGZjv5ESoKiEdNj0gtHPxScFzC1zUHVkWMDaJy/N+C/Cqeto9swMDHb76VN3rQXAi/DgeOTS0h+eZU4AhRCOa51coEr4tyQAuoeWDG7pojaWpd0Oyg5D4CnoAS/vhC7fPHl5WOnNbQAMds/NLYadAIG62x7AYIUhovTUMxQ+TseOmC2+cA6EJ4TsDCyltr6WDhg9cCK32UdYNAZXPwuCYbCsqyq64uOKYWaXoQ3NoRuvs66sF6hfSAyChr6JpqB3pMlNdNZ4FlJat1J4n+qx97P8mnDpIvDdF7G/HFV96p+FKBoYNX4WR6/JthV7LQqMcWadj8CaL6H+4HhZoRp280MlU/xNa7ofnZMaX4fGGEsrhBniZ0U3srJkB0WV/J63wd5ynRUKc0ArS3BHrOzunT3MO8R0npqvez8854YBh379bC5MpX15OLkzw/ZZTNUoWotgmrE8D1Q8mVAiAxOiG018ryoLFq4nf1MHk1x0vWmihktltxUy66rqAvJ7ArM7YbIdNUptbrELKQ3BDBpe020y7l1memDTNk6/4GHrqXx+7hLtgR5rDe9ipvbpVgrlrfryXKTzgRdomZ8sI/Y5NVbUO0o30VnPROMS+74uz7d4d/mhrtW0a4BT/uesEX/0kApYXi3vjd+kokOeFaEhRvHIOKhpfKvStdeuuXoVi1Rg6fyOyab8lSYCxPY3t0MHQYbHmcVdjSGQ6HvakQqOwFYLqFQQFmujIGyVBcy28yGrdENK9MMWSrz1Gvt+IyeCYzJ8RNmpcTAgGZWzynUEYL4Fmtxxdnu6CMPmfjuCZ30+bkf1n8F/dnydB62FcZia5rvKwCh+siOCSrt/8Ppt1VpKq5mWXjmeJysT35lsyfPByTNEcQecZZhhjcdk7iMO02ZZCpSw4G7F0Y/5vCzundTOx6HNnnelRAz/8O9KbbVxYWzJcypYJO1T7PfaB58MivHOAr1aYjQHbjLAEudXZnOa7Nq05jKyXsQWJ39Q7RNxtoe0WDZUm6ZE/6nj3judwFsbaqSJM6E4VxI4OijMHKLQyy1augGOVi4gk8CkcBMGZkvF5/VeaxnBRtCjAWXowfcX9/gjDilFGzVGRmYo1NiPNy7jklm89MRPoA69Ft6ZfAF8ihAf85304XdNkrQZe5KL5Ssac+ZWDujAhi8NKdts9F07toCWW24OMkLQzJu3W2MG0Vuy2tLHNhsp/92yik+w6c5PscaEnkOQGSrD2cz1H4DgPVs+PZoW9a6F7Omcpru04KBzcFgGi6WgJuUOhAxqbRI3sxGi01XvAzGK92kEV2ecXlEnKjIV//lg7pJoYfj04LZTLWmuL0U5tA+EL3XkTOU2/D/uxjriOpJZQF0CIv8LtiNbCb0mR/xDYIefVlwv1sdPMohxWKWc39w0wcwSiOSvyXixCYQpFcVfWBEd+riJG2ASJp4XiU5SxjuQzL/CzF07b43a8PbY0bUNE7bfMSnv2t3a7B2YtPqhhL1pnSz7JnoxBGcTh/PVRpgtfgm/pixgD1soy92h8zvoJ3F/6ikvccwaArE/ZvA174FSiD4QHd2GQRooUO6a0WmeXgqaoCyEZf45oLRTBo9yhdQIpho4yXy0nZ24tZ2qAVmvya+e6KLKMLN7ZIh53TdeFyekXnj74QG/gX7j+FgIQ54Usxt7meF5mvRsh9sp8pvVg8HfW/Q1j+GpGoFEb/Me3D7lCTnC6WKCGOWlqXwxliVazroFyRKp9XQ2sZSdlevzB5q4Kvdn6WG+QbZbgz4eaCJqrepNcpTm0IImQZnUHXpFXS2RKqlrPeWv3kqX7175RgE9TlTEkEVp8eTNLwq+ejgDi5/C143JkbYS6An6RMFtyGQBz9QcFeyChpc4gPnkqPltugm5mCNf1rslQGbiVPC9Sp/7T6eJD86ORDS2m5d4G6fj9iD/D5LMHDtN/knj29YrvrQ43udtxcx60OCY1XYzZn5hsTc67on+wNbUmb4pokk5Ti84pi60e3Pg8xfPoUqxDiD17OoeaIO2y3QrjDhJjIIx2IeNr5nIim5Pmyh6xwB8urzaS5jzXh69Z9dJmMYQY2K8mF3Y4Ddn6cofOmAe3vN2T4JeU5mP184AUaSj19cpbsFumEMOZD7lb1uM922BF02Fme/gmzYu5oFHt/7Iu94GbszR5fvHbwjCbVYXYFObhUdYBuZZoXwGV8J5+IgcckblJrGEoNwu/XEm2lnECgk8FsiLFHulDmSuM1CJs0iOr12QuYaeeYj3VMw0W0NrIFSXQQ2+BWPAAJhw8cyldaVKjC8mdaPp3lJms6Gq5R1DKtGuwdPUdxMTHo/NsKPMarDgbnD/TlWHIZN8sx4e+HL20U/0ERjOgLjnTaYB+laLWuVwJJjq654uwiXvS5AUhVDQpLQIt3HXpZl/mGePo3C+Xwze8v8j8bqPu445c1GoOwOEBxGzldqC4CKvcKgWzNR0XyJx4vhQ6vXnaXxia2SLuFuiAiCDtMhAyula4JCZQ106dMGiEj1LKLS8G/2zx8sjkEd+stv3o4TNneR/irdVyZfQ0Mnp79psJJ12qmLM7lgjfmcmpYG3LPWZf/02Fg2oSLK0ex6kbcN0Ij1e5FkOjSburgHeuMnVQvaShD9bdNWIqznQ98si6GkVuyczPDZlcOxtxZYtAtuHUTi6lEjHnREhFXwY1upqTd7nO8hX5l/HW2itrb/Uou/R8eRGGOjc7uCiric7FKMdT1ubm2rXZfmbWviYznjp555yHxGCMeoBrure3LmEMxyfIWejY9n8+PY8mMnk0Wf+UL2KFtT5jjV1fTxXjedDztgFYx6D2r3NuIni1FHp4Jdh3cIcKqOAKBbo8ZQlo4e/QWm29TaE3IR3fRCMAcPHh528NNRsO+rtmZDWqMh3i41j4njfclCed0iQdvVGN85iqB69segoHX8H1vNVlQMji4WtzUmr7o6H6yKe+oU2BpmHF3v3TG4xqQuaI75OqLTUQgxRQYjQ/09Q3Cte+dquRwvESiLsy+jF/igLsaaORtGXocj1210GRjt0AYUxAxmVx9wlFpoeQPdug4I3XYA/Y1xgmx0IAgO4d8oL27EVd3IadvqJUnV4/VcRJQ5vlPZGTBGmxSNYen2YJZ9dAWnAz7zVeecLfBfKEmg2PkNIJyb4awXZibaN/ysoB8FRh1wGJKd2h8e+pH+GTBf6k4zr2lXCvb0rCofkwGHF1j573r2LyUrq4XMgfVXnwKA251ZVV+CmUJKmoVIG7b/x3+WE6IwHE1J7YeNeUsiXcrYHRvhyuwajAnJl43pNOmMIQjp1N5QPrOZWbvCqZxvUoi5B1jSgGlA9ItaWl5DiRIFZF0Jb1nygnpmDaOBsbeU8W+yDRR3AjA9pUUobV5s/73oaJ8DZl9UjF++Zzo79Ta4heNR2R8b5D1lm2IejszuFj/KJ4u1ziP+2mQSeJvtrlOU/rlnILCyRyyXWapyTk9FuXK6/mPbuF9cCUxydDK6VEWWqs400iCh/DUSgPY81vFYoUJmOhr4cgFS7xd6db1enUzRxNZLfsD8GDHB+EdwWWEM3meI3JYmN1Dc8hbd92H1l8UZGtsJuQ6V31R1Adp+t6HHdb0qEgyTngYLmYfg6OBikdbwCtZl7WbPt4TM+R26s0wZBVwdATfVgd/KLqtdb9Ubi1HC6/t5TTXuHE/wKupKOg5YQ9vwR6fN/2qv5r+9q5aJbX0SWn8V8n6SzhtOH+wFB3W5H4501Qg63Lry/SkK4q8r7q/Qb7sqULoRq+xfIWqCL/0prgy6JW6qdTkPhwqjLgFIjyLv7it+aqCmkku2a0KSSxR1x/+HiJe8OcuWGpOMz2hGlgiwZsCvZt1X6KYjQk3Ty3MgmhJPPpIa6LgCS7Z+BI5vq3nuGvyQ6JS9Vbt78VpIiDzJf934ifANmjxQBY1l3/xYa/OQ6MavhRu5W7NzAqU7ktekd49A1PTdipmBrGj8HxrxGUTNrQ30kwZMPFUwcnwzd/oGOhuigVbQV6MyE1bzy+3ojYkrm/Zx9COEQmUl7aIvUM0BQFJVnnDtodrH4n3pGkJuTOMEdGS0c5IT1xMy82NKtf1bN+uY0lEOy9L8Ss5vUSql72pqHX7G52TbSsTWbSPXbmsrAY+MMd4GASI9GyuIufwxJoZ3X8kuJl7O5Pvat3vP2egB1AS2R4I3VGko9ki5yrg9RZN8GhLqQxloYEtifrSW8+yoNly+n37NbID/DZUgOZBXiqoH2h1x7dLFK2GGlBOjaif8R9TkG/r0RxhIC/DM2l+TZtkcmGjT7fCAqK0Wa5UtZqG3V6IUPZIUJ4WkTbaHbyZkfwtDrSQGTszyrYhq0IX//3uloT81/ByidroKlxCSdPmblxJFIxt53vc0VQk19iSsGxJKgOopbeG+ctO8YWnF0dGvtLHIGMGKspLwZpI0WJl2loQ3h2vOj1HhdM4SSGRWuENkHDgG0rZIirUmJbQQZH/jCGHz1zvn2kMBbJetzUoxnxmvEJ/+QqyB4q9qf7ssJ4R57X9QCBJb/c1mSkEHuPZ52CV/mb1QPM0qnCGR582GM24Hc01Bk6VSiAmv/4hvdRCNWYbA3HkQXYmvZy7seHrEXvnmsF2D6vhX+dMn3Zw+odRIfKaz0JnxY0IAqmsEfkUyE5bHPdBah843rzOdSsrlZJ5I0DRLnwjKZhxBbc5ybqQFGR3hZtw2+TNkEKDuOVN85y9VY5eGtzNrBl6j07jj6GgmU1aJwuQ3P6pvDB14Fsy3EZEdMcwfeOb96gYS6V3KdNEPntkI+H+nAUJIQfYGn0INhkYDk5t+txBmptDoIETrl8Bxy3P7XVP78k4+N+Wv0bT34H8EBD4ikOFrRCDZqNItXOQsH08PlpTJlnXtVjVIcuHdwLb7oodcpu92HiLOlwYwjAyLy94b1FkdAnOg1RfPIHJuRlZfrIMnsX6rhAUPg2yyRS6LvGxknHL6u8i3KPSMlsz/Vs36eaJF6uhnPn8hOwpvSNTbPYlApHrMMtTj1s9uZoPhTB9KY0qX2YlUbAxPP0ucYgLWwT4F6py5IwIq0rFcLlyvG3sL68M24VaXRYOJljr+SXSBuJiVVYJHyxxImNoOYxSMGA+e8aDzhFsxwB22/fkllBOhdUP5RKjehl718mFJv55Pvz8YzuJNtvMK/LzDBb6ROwKUEgDzeRA+FKZA1O1Dw7CzazIQ5OM8REbyayXjF8ZQivrPE13FwctZB74vVriGAqn2b7o9A5YJRd7RP3kDMg1ZQ1ZC1oCLhQcI4CyW7V+PWZVcbM+AJ4o98Mij/g50CnMedqGfKFtOyAmk9xWSOBMLbF6vVA31stZWkZdRKyFeX0YF2cTEb5RLcNp9NLqcGNbsJrcqxPdSXKaWrbNg8N+qFVVO7Lo/FH6rvxBy85xn929jx6NeapX23XokZDGAw/TeiVxhKyGGbCbQQ+RsfwBMhEh05aOMtoSn0LQvbeNIFTH57PYOlTro0IlJxv0RMcMW+znbJPi8OP6V0em9o4otIH0g84v2H+Myn58w9un1moyW6EYW7Wo5CiFtNDCc3BXnRLg2TfG1D3jI7h5Wff52ylnVDdycBLYRgW/BlYd3K+z6JZnsw+syy3uxaLcrUgmFegP1JmdRhPGXGTlV0r+p2B26Uwx8LDQJLxn/FWNmr3Ocw9ZV+wbBAjFi1n/isCbxfxmIER72gQJobFUBAwl1WIus5KAutL0hVyw9mFpcEm+lw9vvUb4nt2Gq1uLokpDB53EieoNU3lGULw4miaj31C0umvo/oV27WQBHbZpAzj0SSJh1vRLIu1NhC4TcTpeCYrnAo9UyonwmbNHry/H3k9VIAzic6NFwAoZO5PcZF/V5hgs940oefz6p6LPiOUMcjSx7jq9ihOkQDOY0/gbHC2LnfZHq+5uqW12aY2lBDoD3hnH4U3Msj1qjBoNm4nBu+vKJNQ0IsN4AAqpMyuiuqihcmUlVM9JLQZ9oQa9bhyamjCwwqHWYDBS3E7Mw3ouNEc4tP8rugjMBFs/0Lv1rt9WCtJVA3p2SGvq8LVnYuGYo6uJWIrFkuELEomPMjyK7OW59TKaGi7o3+JSFx9HNooZODxU4ijKhT33LQ0nLnLG/X61ylSseimQbJNzuHBF7CdM/x0jTA/XKnDv0WB4WGDzANED5CbvzIXQGukMvDYLSnsDvDDVI5DvfLn+VjwhSQe10NKK3G/6MSL+I1dpGb0CUCT4BcGPrlzpO7ktVV0LHO9qcreOZQUFuVc2jY7050AM0ftGVMzSCbc+LAlCM9qP4TO+hZKM1y3T09ZeRJ0pfe7aDMeod9YaQA73+DNRGQASVepRZVIMYeBayRSkjVeERb+GoEMOf3g6go8U9JZo9ZBJ+d/F+rJjUl5INj2TmVBq0X2IajXZfIU8UFOpBqiE0e0ICQ1oUc4MprZ3DISSvepIbIxhPfo0AWJ9TZZyQU6Au/3Vq4Zo90k+V0ny54qkHv5rz2T3/AHkq5kVLL5Gl42uI6AWSYqdodwgiACL/ddS/96Iwe3gGrg9GQpUK93UJF/CRS21S5L0Ee9K4oxFO/UVrGjmHl8uF0OhHpRlJtYotBwByVi5F6vWu+wR7G4x89tC+4WajRIpoybf+iwOASZeAjHrzdsRdrIA5sZoTNgOeveArApQzhiwAUuTH9tHm3ASpu7ca0RVkVxh8xu6Do3NMy2tjg3PRf83QPUEznOVJUJyQipXy9XT2m1YfaC/N3Utu2gG/OOnH6bIZEg+8QCrKYSfRB018iNgP1eLDVNCoApWAF6TqSZJx8bmW7ZAJNGQ9eIJUjJwBpqMZA5g6lScxbPmIjiQLJlIPVN9zm+IHuRQru/nF80eATMY5puZfkaCTVgW5JXzJtPQBjvBhNvomC2bmO942yzfsUCeVpUXeG6wyL4dtZNoOdcQHTUYqJneFHuavjR4LnT1DjlpChdbh5kc7FP3DwL74F7joetXDEXS0cz9807fvg2zHFNkJ+qjYNL37tT2LyoPrcOzBOf0zzpJZO0nKKjVxH65obWqH8BAikGGUOKsjNj8o9FqpTBshfrIwqS42MsrRnklfE1x/KzYZbtVDlcpn5Xkjv79x/Gmvntrb3Y2ox+wsD0Y4rC730+fuG8GZP3DdOn2ZmjOJMFapn1enjFWGXnsCKMbahV19EVnpZDuOSkxHZJa9YEzDQokmsUu73mROE9b1xCmgkjm+vGMEXiLDiKL5YapBW60sSmN3kvwT8cAotQTbJBqk7aW13SThFK5tIxZRd7aY1dSZQuEnBL35wf8VlheKzrieytlaAjyGagJJW/GHntA+NJ9Pss0jj8KRpcj3+MqjzBaK1Fd7BAtXbsLoNC7+Nmvh7kqFD/M6hMjISgg9jRsZuLLcAd2uPG4b4B7WOpJrt4oRWcjSV531BxdPqZG8CryET+S8amB7W568gu7Epq1JQrYx/D9TMBJX+h2VP56Jwrrh+VKhffHGgE2+jUaQfaHvXoDyWmNkvOteH5LIB2HnirThieJS+wSBnBNHCxZzxFQBp3m+hTRgEbVyeKr34/LCpKQOo/YZDpgjBvjSAzSg+/z29edN9C3RNiZCaeHkYuD03A61DoF/FqigKBHS2CdxwvVBhhh74HUTNTC3CRqAemj0KDVJ8Fl36huoU/UTNdYJCl1ayLskFnXHEXAOY6bB8k/NIQ0mwkAa2+iSAoV5SQrsmU+T73+LliI3tqZ0roKq69y1JZ76cn09V2tZGQakpJkdJX2+K7OcVS7gagFI0ZgROkzS3MwcCRPGI3QdR9auRw6ziYpsmEYZj8JQvbjzkoTlEz0cRTYzp43i9vrn83pL+himQWukx0nkF5sA3nJeC+6jQ/cJMg3aYK/bP/laXHTLS0ZEQ1hY3+G7Yxd3zlD7tFtJXx2JlbTynqvI4gMQNkTOQl/jBLox7k3J0ex6H3hBvRrBoyn82u+6TMGCSwGGg5uxWVqkgIzY1/69PGUkd2eZtil0ixvXZo1+W99rHkxctcLRo+Apcp3svKR8MGsOyCLpQWNPML1jmxRF7gZVv9Ie/QHsL8Ldd4g9kB2s3X9FXSCkbm073hAkJF3SOzHk4mYxQr3xnuz8r+Tg3NWhZWZFBg8hCWGDqDXfIVr0/7s+T/SgljbqbncOawEoPrghMUn782aheSNH/m7pislfv8hO3jwRvvOZELy1J3yKkAO/GX+iaALiX5jKRj9zNkWI+M+zrPKg0N2vJWb/M6z+gPoZUXzGqWMLTi/aRygNH8tRQArgtgXFq30uSXP5ri/U4RW9vgUDIyxOTiFVlH/J3QClDrdxCB/02am0xgr45PqR7YKD1+dDAc3njqYIb2f5ug9EDGi1lFuxZTAkw2XMOQQaxdyGN67+tRkiXzBJ+AAP5WQ67EeZ2j9TnVnNPXJGPwuFLcTJL6wZHa/ec79tWdPy5YPCngHtR8srjMRrEsWq0KuVsfdXvUvu6fWdSr4VQ69r6D37++2bdturcJdTvAGFSMOkbyNwrL3qFx4qwuCctJHlzy+ZT034wTJsqNSZa7XSj7uv6cYXNfbw/LS4vbM2c0SfoKu0taaXBliPnsfSh+7/6EjHK4UFthTIrc9pYntSS8em5tXYEH7uc+yzALHTaeWkXSpeFJpQQgCxmywOowuv0T/jvBJmbv2FS5sy6Fb2JQhuHTSY6RCuusH69i2KeISYu08nlL8V7wEDTyQ8iR12qa+GP6xEm6Ni9d07ieClMfpXBO8hx3If/4fA+KtAmcPx966HTij8kEwHxVZrF0t0ylDfzs58Gx2P2K0EwJDC5s/2xEMYCd8kM5TA9HM4FsUYAWtCRkZIvyuE9O/jzraGykK+P5l4t/CAcxZBClYUdkjWpnpIMGWGa92VSKR1bfYI90yqJC+T59/S1WbSXu+Jh93ugwHx+db4IObv1Vjt5/JBT/CUVWXEApeyY/flpjjd4h+0Ct7DBSIl55CPBIbFssux/Hc24g5myYvU7sea5Qifa+IznT1aFMk36v/ljYgAj3fQea3cXfdQLi2bm34Z2ILgMsbmo+KmdMuChUwdNRLqOAG1x7Mg1ZDyPkUSf8icheciTieuCwXWin6RbdpZmLXX2MAORKCk003psuVhkYAn3N7HvgdTiAttQKpf748dcyd2MnTR+ZuHMYuOtPFqdXtsicKBY8+18xGEmIpHYiC7MzeMiskWNnj+MNcF51z9JfNrsJP5IuzVkTHLCosZFx7Hb1+MoDjc5u31CtLyS/t0ulWk7KcdQ1AO+3WwldFRKrw5DkRBxyH18/EoKEqZalxXt8bIhJ/10NPNZ1vdpiZCdkbSL2V5ZaiGp3cO6uKt2PXexH2I4pdyCP7cazMiNDLqJCvhNBj2gFntGnHcV0mMSVo7nYsy9BXEYGMKBcgSRyjjdPYAHj+9BYFFVCnGQ/6p74IKcpmlKZ0uAf8o1Kgab5tRqvbEHkvJVy/ykKUpTmfIgr0GHnMrWbkqjOTaj+fmfgB2ghY4+Ip8RI+t3a1B9g81ahVNqlr3LdBAhC9p6cwWtFP06/Rw2rNr0N7DvmrNRhyr93uNwyN7RwqRQkKdOop3cA7uDto8Or6mD+GoZViztNbhqQSqo86dBO/SbBKy9Lv69JSjGgYBFqoaqr+AZ6eDX5AfDAJRsXK1u+gTz3MgQHf3JL3qAMWOFOBpGH5m6jHcWRtnLCuqVSUaY6y3OV8szkcXIsvr7r1eOTjQUlQXDr5RCLLnL/GJ/rtxCSvL97Vr9UxBN9MXgEqMpTcMRo5bDYloJzgKDGhXqQ7/PkS+uio+xniSzrvweoCEvXpD3Uuu6AmHkoH3qmngpMPUzjMzA7tZLY8hAiT5nSAFWOJRoX3DNTM5WW/CXHpczRAAxRhTL47g/bEKJu+2g3LSOng6akwg2fj44PA927KJz7BDZ5i3OBOXKh34tZk29ikdCER9n15YfNtKTTjjANZRjkpA7YD5KZyiuZAkB7KTalQwYYI8tUIJifw8SKOmU6nkKLaXaxwsMhoDpp6JAL1p1xvha/YWNx8E3wBxxrhsq66lI8oO0La27I6ooZBkC0owXixXTwg0GjxzWK1xK2hstYNwXKJovZwhKTEnruLQkDClBF+v8rPCDsMYXw51P0/TjZ87HMWsHqklD06WPtHUlBVeg16GPHMdnZApZ5CcAbF/jx2pelO51bKrAiAk9ZDlo+i0RHEjtaBpHhZezJcIjiM9caIcXdti9g7lt2ViQCHmh4BGIJ8Y/7PL371xQmEh2f27sJEp2FgmNh3UhekItUwX1rCOXqS4CkHMpAo5zxuzm7AA0HD1Hed0UrRMHMjBemMRu4+W+MQv02DcradfknfHFHeUGPg1GlTwIkS0VnpJKdzHLhr2rFXfAGDK6/TyijZloVFSWzzIgsdaBKViP8CRLVCZ97vrVmMSyY02YTLYbMcxmFoNlIWrgcvtuc9NtC0MbXo0E3VgICKAOQNlTvQSxKSzCWSM7BvSQTpMTSbraukjVEO3q3C90vta+xQiS/rr/9wRzknXsTv/75LxHl4qk8QA+KH51mtCr+MS8nu0KsR/PMTYbzaeICHvPJ+DoU98BFsmv3VFc/lkmUzeD6JV9zfUBT3xX+AyCYQlEYn0uPFiiJCM97f3FyIpW/7Yasbq6Ze55+ljnu40czaPmebRksr6BZaUgL+t6tDP+XAgYVLFdNiwJecPxg7sl4iEIV6VrnozF0LVGIs1sDZa2nkI9KEwnMGDyH1qyBbErPh2HKSccTGowOalTjz0IZM5R3OesTYVVSYuttknJB55JfqHCSZDjPgAuQKXV1Ut7PD3eg5DGgJFhP2T0Y1zDH58hhI962R4hwsXOaeC6HlAfJRs4nzFT/7/DeozhV8KKPvMnbVcakypkON2dZWAxrjcUUIqPxF6aPhYPLzYkeZNCYhDzHtucqO01TVKdGY4NDr7+OXDwiZWYYRJ1ifxFqK3PlO3ETZOueRqUkcaBro6lEsajFObngbwNtUlrmClEf4+QkIZjt6IgejIcIVGmVVbdhYTdTK0Me9I7dfQt6mWE1/7qVZHWJGTQ0xJImxdd7ZlQcCOtl6sd8LI581rdqIVMbs2hSBvWNXfEAGEu26IEkZHF76UIujHpcQJ+WnppdyS8f7/JEbP/0hwmc2elnKqxk0zpoztjbJXDd0fAfjoBm+Zj425xtYy14Zbo7guOqtASJ0UTU5gffp+sdbGunRuIJOHKQ8fpoAdOawlGXfdwKO5eIobOE6x7/SIIDtknX3DKbtaAQ/2CVNDTGkuIsyf75M2cv3dwcYMd/GlznDzw5DdVGLJzrGNeSvfaRHWhvfPbTWKihN025RmHIZQsIUSmJGq0oPmZEBDoVMYgmxo99tn74aemf9WtpvUay2MVAkRVOm5hk81WNA9PCjWzIzeRQ2r8h5MpgHgCj2ey1UyBCIjbgiRsQwXkgXxpAQ5GURNWAWALpCKI90J4+WJR8Jw3Jh20zjwSdBHlJ8Bb1fpzXifZyM1b4100Wk+8HaaIWZYnPXrw1pc6pBLYKFyLIBn6csWJszqCCtEfSaR8I8KtUshmFGz6obaugV0Z449u5VRZ1wk+2P6HoXNlW9aeVfmSXmX26PcyOkfGs155FCNEJ4aqrIwHwyFocMMZcKV8TfzRRolDz7qeA1w2LaYnaM3C+FGrdXT9kZ60xP+UGxJb12SSAz+63sKHCtuOz0lI4FCPje0G69VCVWElDPyupkYKZSjFf1Unw6Hs/5q8uvAtSKiQhEY0Aim4MQf29AqEB7nw/N08Ch3beyeBDjjz0ONSNf76AQ0FJiG2aVo/NQwG75jEv22xeQBVDqdis91JeqylYjGUKWhMlaYJNjC/aeqGXhDWhxDjWS4r37wCoagJrthpHpJNQyUWFBCAje2NekP5Asc9XsQYD6T5Gk9S5FtZDs9vAXQHxaLCUTL6v8GBwd7GYEjcL9TAkw7B9UV0Slt3PE+R28YhOF1ujBJjmZL2XA/6lxKhFA+oz9quqqrZ04rMHvhPHBnTdN5cZ+SFQaRxOCq0HQH/BMp9D+imUXAz3gpM2v5429Yny23Te4eS/nkmYwPboxiN+lsHSOoZSVflESePUpyjEeDXub1or9yDjL7QkrNvNc5BDz0XUIw0Nv/766gxAWftG4Gajudg3qj/5xO2HRr2vHFY3DNMXrbqw0gx5qfwULknAvDuzeS85jqun+8XnFPYUnFGxkRTSl3wT06RH6FPhOSdyvUDCtd0EHbkja+6DwqiveFoM/vaemtWPQKJK+rCgnCnRdDwuM26pTavfcgkxciIbH59CgDFNjNAp9O3pMZ6psAWIUXLXSnUddAu2IztlJcKtWFpq+cWJZX3TBtxSWf1+7MGTDiWIe22IXPZkQPNuJvSX0ZU4kpmN/tOcjrbsHi4H0bMDnEGsR25dmXJC+ETQmHsmQ5rxdpDrhFw7LggV3f/guahvsJ251HWiYEoWQ/Een40LiqBWkob9WD5Hjlytwu+AdeUd6Jv7LLTtVbTzfbAW1qR84X1pxe+tceGw7ou76GOpd8tJAri/CgNCliOnl9wOe7rjqA7sxL1sLImcqnkTkD5IYl9SfLGfbzSgZSf/Ss5At85dAN6qHvlo6U33Vaq5QHe6LiQoGxal2YCyq1GYaq5wa9ajUpW13HmQGuHGJSk44JpYqALjUOBKx9lcRjC5XPwJ478JZgwV9lIKcfKoPT0KXsprlpMOy9ulkz3+IixvVFLUyo6F6yANaqkv7y6sqFlgpbYxHARxvokuFX/1dL0h/EImqTu7RzSZrcySlXHy/IXvFOY6+zxokNQfP7ticQ2lvBIZA60lRoU4q9b2QuTI7W+kodcx2ArkrKWbBtjFwoS8d8g5v6SgvcQ142PHl8KpsTmYR7yDfOMyY2LeKzLuugPbD5nf7YxtSBd8LnT3w6nxGrgH8LKG1oJMx7FSR/mVVqbVqv6KkeKLxagHRY1ECUm+3VuRKfVSs2/zD/kK343e4bXEem1J6oWGD00cwDoZAjDnQXszWBcD4O4/TF4ofO4hRaVHOD0PlPDWbEd4RXWB4NIeMRtvXA5/dehCZXJBlCqrguKTtH7YA2lQuU9cThZXHf5cGmLOhbvT547QoQvFZgtAVlSQjtwDbupIi446CjePWZH9VBTKNwJUiQtaBgkYiV22bpCbfz+NeAAj4V7K66QsvxID0NwI5Gc0iT+rxS7xUQu4X4xY/hs+eBwwP76TBTDAwoN6kuY04uRYTtEa8g+GIk5DrjfFw2BiuJUVE3N13gleEnyCvfPhpSnZVtfmhDNIrFw9p9LlEoAkrTuSaLEOv/0IuuZKp2V1AXEYlS1ByhT3d39SMJClvJUtZMUzhvNgOPe8dVdKKVnCTQh9B8hV7ykW9ZX2tQdZ9eO/Of2MRVtTpk4CnBRdLXlZ02eImTqkYKRkchXlR9ZZqKV9VG6F7s9iG8ZIwrgxkfICoIswonvCYxqjLYhnoZ80BD0KYAPG4Kwz1VARMLukUs+UKeUNLFJarmT7lF0X6KEOGSLzZqUJKhHEnBkDLCoMn5mZt9dPNKwxpvc5OpFqnL1ApG7ESihMB3Ma6/MxNzrnUBHYxhKX9qAvNswYzeSFcRtr23voeb1YF+yUa/TJDqamY7CQ6Vtdv/r18A0UHqHOEbJ+ZNiX4znWwKjt8MhGtNsmXbfCltXM98HROkpIKxOzEEJStjredkNCtCjvFUSlLqi0e0vR7WSRzW/sp7WJdcqxU2QsJg1bBP737avlJx3xpEyAPuASJl1cPyOdfHgTSQ/RGyADANWlwk3GuggEcOwNtJAD9Bhf5P48NhTLzcoQkb32N0mngVA+/qSY4Hs5EvnzH+DqhPKuwvrVXc3TEv0J0cXG0VjkCQ89GoMr7RtSZnV6n0B+wWXvzT0HyN/GIrc18thpOEhhY+oV4tfS+Pyn5F91UoGVZpmwVm+vi+ukJ0OGES1k37O/sX4lz4Cy3zcy5Ygjn6ZN7ijiPiSjUseWD/FbOiOYBHrtLdnx1iNLgMavfzSGtV2kqweTf2lMRH8IcUtgrrTu5y/FB3Pavlyh9ePuXo0u+TrqHAFAmpzXqnIzcXwKDwBcJ5Ujfk/Dj7IlEYg0jUzUtDx5o+TRxsCa0T83wSGbLa5W17fqZKu8qHjfgJGuUk3e/wkp+v5dR6lBr2b815G8eZgC7HDWvBaAmXT+5iR+KuAe/AJyJuXTXs6dCw1u1zMWZzALX/Nu/fEd7/zzQgGIVwugNJaeFBmc/cDCS3Vfo+EUylm73l9paPOhNcfjsqJ3G1+DSlaEsfkU3AfvITTwIeBRJEDD8CO3rNecaVkZ4F8UJNuEAPb1c98pW3VAWxAL34mzBSczVDptvOV+s0BWPlA01Jw7IC4mJI5GzbMDhWJl5wn8gfekbHwV2CyOcya6qp3CimyUbx09DsHFvHkuZeDu0tmoX9+e0CrORu1tE35WWF0zDo96ZkDyqeFDQdTWh+gNme3DyBx59whx1sHdJkM/hrge8TAjvyfSjESQFpjxj6ctEHVHT+19MEj8hRZZ0Qx84LBUDMoZr4K5RwXE140KgigO0LMlLjC5j0ocQ8cRHHzRa+VReC7inD3OYETh//qpCrbp85qDhhA7MBWCg+zQ3jFY6QJxZapyMiyRRTnZoAcWx5ihPc6KIladb+DchZq4P+wq1KtYj5JK+KBIbeMeQM0haSSvIKSNwd1IDZXfjCyVJcCEj9cFC7gVMY5vAGYuoAsdH+5Rs8twO3QCajN4jroExRFS250a+mukOuR6Sn2ZdJLTkqloQKVLbie32vIEzDk2z7BRU7h/8d3t1WS96dv6pioLv8yFsN4m51zmIddwaKBilT0BByF4mmzeJGCrFWlrFehk6vpVHFk7rEj1UjjmgblMwtShNA1e/6axVXeMAgA+ZwUtceq8DKUzaYun4UN1gtP096Nw+TCJKQEkPDlJg0isgHq8JHN3ED9c0J0UjWBIXby0w16dsIFqpiStU6nhsE1W2R/BXRBoRuQcbZglMOUbzeHDnCoDSeUhyLdTounOXEdtqu9RsbZJl9YV7bxllt4ybe34XKHzf9ag/OzczRby/UkgocP2jmCXepl7AQAn9qD1R4WA2WE7bhG4NiybUI+HT6aFZF0HYdliUThuLMcfJRSf0ubQi/omjMF59lLpy1F836SWDaNUh+P2Lq1Nxac1A2naG8o5lnoTUKdP5vgwBZhadCTNUX/fPDE4ogEdTouF5Z1JaR2Lcawu7tvy/aDddrmU9BZSHDfQpugCQSsrGAK4rT8n40aFf71e3uwhmvdHF8L5iVsCDo63/oxMjiaEubh85m/Cn0VE40ts3mmfxkadTyvaEaBy0/jkN4PuXtCTbaeVooB6Li4X0Pb16KbRZRLQ4R2qQtb2jhlTq4NKSl4jvMzIR5c91Z6EpwRcBKvoUBh+zZqmutwwFVZTFwPR3k1aMJvZepALKC5eYlZDVIimOrjwcfbRkQOnpxT1OkB2aC3avTlVzwNdi50m+TLMHD2fLTRvuMeV+g3R9PVkyqnGrdtYmFlPcubjvgJJIJKEsr3VcMJjKeFaMlFlP2pw5wZn9pn9zImmemQPM0D/JOC4/+RTmoliPsmprinQm0z8xhaiOklKL6GabcmPIk0jsY29RNPTingoGz6E/AcDqf4nDalEMa7+vGbyOAJD05rZXiZkDdmqrSPAkDxO9GUfB+omRwDTCGj5ytkbYd9MqCbiMwOJj6EbJIcUFRgKKHPjmFbBS3asCWxBABtgRCDAsF7i+JCXAAPc7sDPmQXEoXtWLFbmihEsert+XzxAURkP7HTzMpK8eCbmtZdWfdzPHvhR9hQeheNjlTZIu7vJjydU442qgpyuGtDrkfq3NWY08sIFpJnEUp9100aWXOhj1LAl9yC29Wrs2uIkNNZxncn2p0RD0VoTIBugTwLlo2Rq5TZN5QpXcrIuD0ihkfltckXq5m7hBSlQ4vAJNrG97mgEdoO22s9pzZDAj9J5Cuc/4Ev+pfrYFCJ5smhDfyp+tkkszC/Zk3KJCsy6Tkuynfw7YCsyRJZZW4kBYNkmJdAhGQgOlAFqAM3McN7k0Z4b7lgP6GwSBD4ypaf5hCN/15dA2YbtUcVLCSXgO9mjiwKcAm85wUf4izO/jpq/HWucxFNBz8HGWTsBOW+MRX4cpjcYXdcYvMOLENQTWjJsRexe9B+UO6b/HHhclZJtvqIbEN1Tk0eFWJzITiSB165Vaidrx+oEeQmjiif94FdoWHh2Tdk3PlgH4tWS/3nEexoHjaZOMK0gngsUHn562x+XjanF7MzVJ26LuzJNqvmTwfM9yrIay80mbvJqrR/4VnforLByDZEFjd5BGfxgx0+23h56uomqSm9qH2HnCrfSF/tiEtLhB6DnuZBprO2kwNz75xrkV5QtFGyfJyovPO4tkVdwG6f1x9oLq+lUzwLNtJ6kQITQbRDrSJCozASuVBM79EluuKPYivCIJiQiPXJsaeiXAZgsfP37OZZ8JAjHkitl8/BdWV+lFD+X1Du0mNPNDXv7SZUp7sMxcZ9QYnpO2ovePmHIXl/nm7l9Fo3To0D+yFMddTS8pRctvai+RJdWCQWaJBjRO3TWkAwHjjmXno3ZTk16a9SFvfk/hucrg/MSKYm3aAcC9j8r2r8M7MrhO/A7fv3pUz100jt/tjpy4tyaG0CJ+rJw+fdz+C/frxnOkMMj93vOF181xD8qSaoTQijwfqbsfXXWclUwrtY16l1ABuLgvBoPPR/Ll6WvJeSz8Okyj/MUnD7SaJxSNrI5DvoMWd97QIbOel8zCglArUIibWDZS0ZI0Tk9xGy4ir1btaBblkyWkC3BYTGMKoozxb9iUMTAC+IoAmSUzpvs+FSCGhV1tz68T+QCmH5+tdG4o6PBshpncqD7g33+EzsvBeQ+LL9ubodByRgcrF5V1iGBM90TomR8npdjA2JshHoawlXy74YIxYL8Z+OIutnuO/LqMb6AohjXSSgz8Q2DbY5l68wCXKzmoW2XPo97p39IH1SLhBY5UHXHdZrwDtrRENwVgA5S1QJXpipfUQQ9XdowiKxdnhrak619ngb40LSJxeFpwfJfeSpGCk+tm8EcnWKNNPvarSGNgxI/F7dgbMxiaiTL7N/f8uoDETgYtrZ6DG+mRN/dLDPlg5i9aEjpDUE2lHpMs9xMxF4ryvWv+gBh8/Mb4pa0uTddhLJ4G70bxG5Vm1btQpPm55+iXStV/D8OJ6zCwcTIKu9CC2XlUX5QuG1iCTVfS69pfdGiinizCKJ+PFRS+v+tJPyz/B5zOchzSDmAyRCvjbhDXinRTHIZAbKCIn5LvYBy1puLJdkgKH8ItqdAiYKiEjiuQ5S+Rf4tct9zs/woYc5s0FFvQ9Khp/Is9jamrxTfYbmIdp/1hytNYyzh3VB8FqblYbMLlC6K4d+xgBIMJoKD0o8jPX2zgXDaCbSujI0DRWUvnPpzw+Ie5TAHTqrEoaay7OGbEb5OVUi+qjj+Qwemkvac+FoZM9XQYRaeYUnKPvhwUyxC3GZbbanbAERAtP0jUSY2cx2jaqMciXOEREKwU0v1ETssm8wqpkNr/uWnH2m3scm+ttCyUYmAep1OlH24DN9dy1Zkzn5fhw7x7JJ7IplPoFfRc9/raqSiOFR5bdcneSVCZ47vGaMWvo0fuai+HWd+mTTsDSWJhIdArwBlwTWrQdID6B79jk78DY2Nq+uQbksO/vUGsgOFREDT+jHIlF4q1d1MujvKfpx0enOgxO+MGfD3oZmz3eclMBwpnXjYo9vWwvurM8aDVl3cKsA41XID0Oh5cJB6dgCt7xKfdopsVgtXScjLtgae+4tCxlomoESi3VpNjzkIerU+RP5bAf4T6JLpcA4L8jrn0mwattpmIkWV2yAJWUa+J7ktPtZQTYIiX3sHZfTYtHCpMvQHTo09/w30LeklMek7oqbKCtTb944nPTZyBDyue69WikR9jT0dw+X4bH3Zo0ICmrtuN881j79OtuWlaSTD9/gyZDTX2Im+dDLQ6RSpHcf/DujIRrt9eA1Z8nM5/2qXSxP/PkCyYtwOW6hY3lcB/EUOSUrNVGfbEgFjSKgEjZeW3E8un6exHaiKsgXbL2oLEArxlcslD6EfIhjHClzk+/tJthUsNd2onBFTSPCqNhctPTqSykwXJzjrxfIBcPtZ2fTOwv62Oe9rBlQ+Oll4fZRnoKDoOxk+WiEcM/B9/4V949ju43xuSCKp3tAzUHSLXGkpJ/LSkAmWGLvtnPQoCQ2LnOF0Mj+4RcXUeCpmj0kMEEmOHIAk5kj+/2KBFoE9fYBqcM5ot0836boyidIPCrBR/NF29sUGOlPyLntf2ccOSrdHdGXxKTi1XqkR3tK/AW5WbSjIP3/DJ6vn/XB9JdfUo20/VwJlGje/tdfMnKKpRUQzbAPWol5peawVxePmJtgSeI2Drip+9dSRy3cTrcFTvENwg3kIPP4tptWGYUFCHHJzXcQzOMY2EctC6sXJGZ2aYj1bW2qp4q1PizzcqU4PZAGKHypnvI7UAH3Ws+bczzvf11oaTCxLD94vQpC+f4LXT5TkXSk6RJZC+MG0CEhj4npx1Uq0TYrbZhchKSC5bYoAKOtV4bVQF8ZTcBe5pvkpJOtKiO2XHa/2RqCPzMT9Tx1qQFbgrUlREmmblXJoal3Iav5vPYSxDjBE9VQ/LoAU2QX4rfFJuPAxSLixNaZsFAU/scy2kSgmvkiyJb19h3VmCdKrsTZSrUGCJFRIpqWSlsxt4TmagGz2U4hFGkwFHLXat/U/0Je54F1PT00iT8Kmk8otPHJFaysKWSdNBh2vNYsL45Bij/lsZswnVvKBDZVGHWi6njPNi60gvBbC4m2j4Yb+/CCdtQk4eiiZVeE9KcgPvFqPgCVRj8GQfNJa2k9VVTfIP5RrTHtckdwE9GNZeW6OJ4aRC/q3nmcn5zF0XrcnKh/h+xj33uhFD4CUgS/P2yOQicuXBrYaFpxnr0aYvyey+SKwympeYOFazNHUwvYsSY0Mkt/EV1aBWrvDCInQGZP84ZNlBWXV1MygN6azaFAdJWZVjKohySv7/2AgXOFxfIVQh8kxySkorBWAhnvMEvSVc2B22AMVEZIULGk7l1M9spDyp1NmVdST2mIXoKn7nWIK4NyQHgDQGgNEQ1uc+e0Co3wWRFrQIUpICOLhEFwRazOBQxSAMJt2HOU91+/+briv96s9OkxMy8PDA8ijfpHXMmybPkB9AmYJLsniJe6HWpVPymyudIgy74MmFCLqStMvt86l/KXjzlFnuhVgvMV8IWtkZDZ/+GL013vp8gDnemZKod0zl0z/ORyYmdImxDsF4G05bf0XnE1K68fnG6U07lj4LGa9IO1tFgDcZqp89IarIfbVeKj8kBIJfzLj4pjSGYrRCyMfuUJr9mWR1aLQAPO2erE2cw8OwYEmRDBXFoDXvmZfXGcVBsOWnffT+mpya9Cj23Z5LugOu+wwCnN6uzIGH3WmPv0qeRIFdEp1Qq9NBp+sDjSr8ozmSU8GCTZFgYnLS1Xy72o3Cq4ydJ5Esz1He8xQLKDagp6Pd1nF8yCD3+LZ5rVB9PAShOwNO9+G8DnEFwfBJsTrVbxItgWPx3kh6DKDgBAUt2bWRiKs8py4M/Kd8fSrmG/pDHgxdvn4+TSG/SlvTHZV5Phu+yTwKPS/HiB4cCKxRfy8DFGS9hrd1YsHvbZeGG2IPnJqChStVRIecX1RSNTMfBvIYn/h2P1xrH6E/cKY8hWIUmyAzcsIsZZQiNq1OXayL0JEwnDuXtBQMT5MrqsHnUBUftbgm08W7JCSYm7wubjZWuE/X/qh0BiPS6Mi4ZIE3rpfQXi0wNRQqPzkZRyK1COvEtjGTa48jQjFkhCrukICNLItW8kF6UD19gUfIbh3x14MrCrbsNdNT0ts3YwCbXDP4sKPN3+JwO3+ljyE8cN7hURvD1rfarz6LE0KLl/94ZQiJbtocp1M5WeG8bJx/y9DBNqrrd0GhS7KoFanlqSfaOlKD7E98yMIiglBLjhDy27fhOGicrQHJADkiVUj7IjsY8q2sDNnoQ9v6wJSL4r/W4vGszp3WY1aXf8obrHn2x7dCTtDYsZvJkBKBdcPGbCuhQlCuAWLRnT9b213kbCpEjRyG1cCAhXQnVW3utw00Sj4nkz2Mr1Ny+iVTJzmwBa9pHvhwPKZtyDteR+8XxMhklWFTDfwZhodXzOtJDgSoy2ye4MfOJF07y9eMjDZt0abIpNeR7I0rP29C9qtqQZrg92ikDaViehlNkzqqhfxc+fM+bAb26yt+ARD5eqBNz+B/WK2h1JcWA/qLxeEL3pZbX8w1ixEV1HN772urId61htbWHuitpK8Bh1Tv9JgLj2t5SR9G+1UllYLWXyX3UleYRnhG24XmFej7Y3Jdzye4X5s4jfI+Yd28da1H/eoeMRbKBwzhLBgPwLQwc2GFY7WWR5OR0M3BKMPHWU6zFnxOnPqbr7tq+l5awi7Jj8YFr5CznWwgxXSpm8X7htlUMi4UdU4jlnEgzELDafdXuv7BlNVecZwz5TXSH8L1J6r0DSHvWLPA4iUXpD0A/nC5HJKWhsqk79V0Bybh3v9ydNFHSIRk0um9SdQ1Gxjy7CcbMwz2GNosYgBJSvSC53Z2cjEPCYhrM0DY7ghPDh4jaPeHot1bGqtkj5ft01IBkcXxfTqMjjGnpdIL3ym3gIeTrMuYZnlIh36HojtN15H7oZMqiuxG7++b+7iWH1kO0koLr9BHilZcb+ZZ/6ACSkYoL9oSpIByQTyA1Gh7MSIWY00U+qIt+6P0d25dG+jNqP3wx7l/fKVWd55hlM8rJVOhzfMn9oVUfG3kiQg/n73XVp/pSEKM+PnvCerDMRtE55wbe+TfKH/kF9DIm+lb/ghVyfZvh0Yvssips+sIXHZ3rfV0QzddwYoxwG/MxhrLeCFYb9Tgqwr1+2D5XGWAcB5GRDRt26f/rTpeLlDX4M2BAjS7D5j/v6YgEvN5bJkFQff3Ven6X7TjHlmCP5ep1Uv4u/60ud/9MyRP9Rn9BY7TCUg0rLhpK1XUsE/7r/2IfoE7KYr1MAduR5UGLL6dnhNkE+0yLFCNDCTAsq0rPdRJIEnauR3ms7d2R/RP14P49r0KIvePpvjCtvcHcBMdlbLTpS//tUHEuKMKSh8ljqxDgb01IelptMe76oWxUYQfjNEzOksOPGbCTz8liZVNtmBuFUEx8jhzrLthauq8xXUGWg4z7JWYsdCR7tuUjoQeKQrkQtNejIJKnfG2nxvVf4FOdTCt3jkQahAz6edtmQ6ao7/eQzFVhAUSHC0f8mawAHEhq4cYu9HS2SgUxgBYCwTZI9KIraeP5Q4zJCdV4K+d604lXQjq4980oJOuFLzSba8911MdZZnTOOWMsmwHWVvKFQ0KcluEWT1C02cdZN/DyOQ3OEaYjH6mynm97UFQ90ZlXnHoQ7/ePV8CAFByU49tE1RREDqL0JP8OxPnnraZ2ZD9H4uKFy8/AwrSObmT6h8Cjr2c6XaedXBTZ57WCnn/H5Wyn0mK+3ypsJKTec1mBRiQczEfbQFVA/6bMg7PUO3oQVIfPYuf1hl4LlEX+s5FOxTKbrijvuyGjOhBaO9prRFJ3CS8Gm21DZcavtMfDAvQLJFi3MPYa9nK5SftCILk1lbr8F9cDTFBHaSlnYEtzDbar8lgmeOseU+E+Ho/xF06qxC1j2qh4+f3Ve3XNmgcoDMj9M6/RgDMjll7IxFSwt6xBwgrEnExHTxwzwHxTVnXqRkMScMst060hJ+vxPiWoNba1SNLJeDfVSDwFxX84ycvRhXu6ZRZHVgp5vct+6Ndg6sSM20ngpY4Y799af+xQR8ANQcfOliE0rFJX4yx3RRKGUmbqG9ZAU6m5bTnhGGiNshS3avCr6svFsr2lxhgK1iRJbuXpVduq0dv1tgMiBQU59R8kL4SuDenXD7RTsqbejziLh5AlLpswaa6Aq2kSkpQcqD1xlJAdF+5cU9EvG4K7/1AwYPeW+jbz65+7tmSzfrpdRss79eGS2Hm+BObyLpSI/zibKkNVU250i3OVAAZZELT7u6bC3xfSHYmC1p4qUuN7q43YDjwYTKihQyB/6D0vSxpN+POBFMHafEzKQGaGGiZxZIyLGH3pQNi+cft3+UrZ3oMfXO1wKdc5zeIL0fuzdAiMUhPY6PbWvdEajQ5bigwhnjJtsYn0rOgJ75D5tTGHxjOsYtwv0WBlUKyQLBvjuASVlCj98FvlBf3qqpasZPH1kBqPpyx/ZFhaPRHU69Ugj7Y56hQ89e14yhwDCNRrFrjljocO7blSQjJSpjhWWh32i3Ad8XwmnATtY8/bVeFZlsvHK+7oSryw6q383buXQmGIpZdW47k/DT80PiFNnG5gX/V8GSVukbw75C5P+NkLd06aOnNlEauejM5LBxwZXKTL8qVPDydihOdniG4OUfJ1e6MMJYguXzdBRLKVExMPB5ddTKiYwkPAvS6ddMaNtEIWvZV6xAwJ07wdXOX1jsVxWcp+rtJ1IFK/Zp2cT0bV4QomZu1yNzMkO8kzm+pUYrJPLXgqeoxYNzJdDhRKNCwYX6qHjxMv+gRpLUIri80U6YqOE/qfxCC07H0nm028w0m01NW/cU7zk5ZYTa76QJZrKKy34RCQRRYb8/eqJ192Xigfiij66GxcETXmusOI8tz1RgYITkmgljfBR96RquyYMx7U7j1eiK37VZU4KjtPzyIdNwvid83f+AftlMGN1dt2Uk+g4uMnrktYnVSLbht9IAIANOe3f2RWDF/5hry18Cjw/i9/a+IvH04fFQEn6Hsv84aZcdJ8sDnTv5BgE6JCvHihrvRXI7gUscUFymW5swmBsVeLcYmCcE/1BiCjb6U0nydLgdqGCCZMW4nAIxIMFkXqtha4bDTOvImFL2XtKsnYvHGNpa182J0nJ5vscJxpoqY3Xjfharb/gEsjk+lGbAB/443mRWqrNrWloMybukV3ifbY+f+Eq/ESzbtNf+zkBKaOjuapbM0rYKsQZ+QNw73d8Df18HT2C5jx1xD+ZOexHkyD6cA/7g9xvE0edtPHbkCNN9qfl0SplzLTrCqgvqJIqkyxe4KVPzbSwL6S9DDOFZTzCX55MmYyG8gxTUPEzoUj+ZYKiwqA6WQXrT/n480mSCXVg4omzYS9WLOtHuJqse3k7CgL68xJeuvKL4PiEG/YtoCzsbE6msCQrpG6NUMwX1delmSzJpdpwuVr2ak69XFsmmGGGRtYxzFME0NYIHkVk9HWa6rMC2R5cLj+gokE6MKgZrYD64JWfEel1hxtYAM3qNP2vkh03wmuKbffeaEuIoKL6D1ZfheGckjINT1u0Mk1L1eTaQMzOu24B/PchEeqxjcOUv7fDKgWuXbYXXVE8VnNK1ii9yWdL65fwUZLzN4IwJTjRHm01zIU8xXr8adGKNycU/7nhEt0y3BMXJkpt+mcmkEjxVwBjkPq43gwMdgxecUhKlwrhK4wfIvJL3GqchfQRjyD+azeZWOma+nKWiOMnhEf9+BFWA5mu2mctQQaSoGvkDfAnyal3uUpwPes63+0NWKtvWRAIh0DH7fm5ipD7Y8qCcJbkeohHP5TesI00Ie21ENLFIQdCreXSqedqxO8yrtMNmnYgq7yh8FpNxIEdQ2PXcZ8G5qoYqsAVQw8dQgQIn2jOWrtY+GgBl8tU631UryLKj+9ugHN3MvPzFnuA4HmLQCkzmESPJGswvnPAmiGENFLSTpcaE5dRtdHE8NKBpBfkl9J7O/fk5UVAI/vaGzJo2noIjOo/BYWlokDEfM3xkiQl/3h0HdeBBkLku9+B5JpTbATjGI7Ps8fJLg5h6JTlrB5Lq3L/Rh27qWh4j9Tseic8las/s5DfLq51z25G9fOHZPuqP+ij7L65Ji34Ph5hSF5OUdAtQZMxiX6BVAgIH/vJVizxz4troklE0n8fgkUSiPwZE8tpc2JTTQwOVNTWg6rZ+0qorQ1+j6xoi2qm3nZWwXZ07i4OUUwG5+ivsqejr07hughg7orroAmZMh8a0YAxp9o91gQ3XmRApxRVAcm1enpayBr181ZkNeYVRYQCTmMOpau1xxAIRQ2ShxBWIMJrEV+FACeGZTZVfkmzAM5+Wb0iRX8VYTIkjoZVW3dYhmYi0F6c3A9EovLhK88yvAqoTh/nhgSYw1FGaZIy2QsZlOZsQzwv3CVhCaKbd3PNDdoK/6WZBbXQCCX/KNZCBY397eC3sQxM+qzBQFt478LxvQjfg2+iPXN1NushczYy+2MPxPXcOvVqBctpwkmNHGqtkhgzR0xE33mtrl5ufsEx5WxjBQ+m9SyH1CYImsOVkfwiVWsSyL+vIKYP2bygyUQ56a+MaMa+Z0rodkNhZOmD1MXN+vN6ko4/5efalkfuCrch5zpbmcWo/djNAGO0hJQAMQfsQ1EM5Z/hanoL3LRUkOO8AgEhn3sBrtIHkLBEkJF/QzhKkpMo++kKrrhy4d9xnQZPdMVAazyeBaEGWNpWZQV69bsuqlZZnDe4fqSUdEKVgDaNaJUIYMsKC0glYVSSOS2Eq2n007wVNdYEm7T2TH5qmHb50hHaiM8fiFNcW0yIjCxuQRyTa44L7wjEDlE5M8oKnMJeQEq019l+5FGBRzg//810BVazgAwBg+FVYyMzG64lyGEFokOWASeHStAQU/phq4J+OKnHMKYvZPL5yPSjbau9HiTJMzCWRxf9Et472hgAwABc5nfK60Gt/NR+uLgpuCfT4B2oBWJBVhwnij0Wew8v8Ms3bSQJIeqYZm79+UhtfRfCGNgaKgyS2ijDezQGapTIWU+ZTrSEfrqBkrlUENxszr91MHATh4LbKB/zF4UQJUB2kCIecofemxllpvRxMJTRpBDvXKrj9FPzukGYIHB+oLrm+CPHTxEu2fU5Q78Zqlc9XhgD6cFzmY8eaXcVhgnxLX6eAhpXSXl95SeCy8wO/ieSeExjRUlc93gyDOH1BsRO0V6XKLImpQmqGsnB9KifCs2+EMNXzc+UilWhidSPGJZ/psxuzEutI3tcieMVoNaVDK0iUrYumXUoDvsgJ8XvValKn8+EEPZwwxX4mvMwhL1ke77Nmzdqc+9kmVZS+xp5VFUgqkiDIeS0GX9f5XlwmoZ2lLpJUdJodA55QFyfl+ePlQx6lPRckoJ4mkYgkX8GqHzd8e555duSIm/z0voAH+QwFwW5vVfhUNbImrzTs62QtkD0InaEqgmWN65xKmv4T/kKOYFs3Fn9FVoeZEqDgikP/QwRVXGtz6ZTu8EiGqASCi2OT3GzLrHCRpMPaPYN4ucXcWAZexy7rzo8COIHTReIfppz9GYQmBRpH2L2kBPmiqT805R/J67AeI2A4igaO8azH6suTJD8NuLMulb0xcLdOKoxYFrDF76FLEYsnI2yvD5bLWw9nJGqQbQVID89xKtoXkgFjWj1KigigDg4sLdAhYpP15K68C5klFqiEYYsBVPnz1IinXF9SXj3Mg3IOvEHrcv+/zXjmWezAq7njsHXH1ZMvFDp3fyJTsi3HY/v5wgGDyFR96Zyg1xSSsg3xn3f81dOi4JmgKaD0/qKa+ACqSlJF6mVmgAxEPCyFB6Ga/UwHoaIbCmcYvmj8BkFXLpNek3gAiZ8eJmPFXZM1qSKVQiBAvJHeNLMzH5kOMG1QtpWOX7mvg9OrLkxCGFbWtLdyZNdc7DNtNhm9IqnEq4dhOQ5Et4G+HLM6lNvhLmrE+1TXLzvrZF/loIpm+Kw+3Clfb3XLBnp8GR6yJR8JYxlS5HrwVXm+oNLTViK7df5Ku4qoEV3xnSa9f4JRx+tnCYkutdVnUNY0cdukZp4DtjTp8Ijw3KqPIhjGXyv3K0ce/zQF1Yc2GpXNdXutEsFeRM/vkz40hBKXdQ7g4Y9TfBQRnPmBsQ1AW7rYcJSkAGJjZV6NtPFSgVJsSaD/ygq+UBdGhr3PyyCS7LYb4IItzJr/6PxotrVWMfz+WtsyHaaFrpEYiMzxXtPAILLDlO1VoFjkd5KMus8oIrH/DcjSOmIgv7MCs/1Y0W8h+qZvAOZRAMC8RhNNdH0mprA+ZE/yo012bwx0I3DNj5WKMDX2GJr/yUJH8JKkX4aj+0zbFhYRAmCEyvB/hQcRlSwwDSKZDNBZDXqAV5TZqQ==\"}" +} diff --git a/app-shell/src/config.js b/app-shell/src/config.js new file mode 100644 index 0000000..216e61b --- /dev/null +++ b/app-shell/src/config.js @@ -0,0 +1,18 @@ + + +const config = { + admin_pass: "f606f4c2", + admin_email: "admin@flatlogic.com", + schema_encryption_key: process.env.SCHEMA_ENCRYPTION_KEY || '', + + project_uuid: 'f606f4c2-efd0-4474-98a0-146ab785f002', + flHost: process.env.NODE_ENV === 'production' ? 'https://flatlogic.com/projects' : 'http://localhost:3000/projects', + + gitea_domain: process.env.GITEA_DOMAIN || 'gitea.flatlogic.app', + gitea_username: process.env.GITEA_USERNAME || 'admin', + gitea_api_token: process.env.GITEA_API_TOKEN || null, + github_repo_url: process.env.GITHUB_REPO_URL || null, + github_token: process.env.GITHUB_TOKEN || null, +}; + +module.exports = config; diff --git a/app-shell/src/helpers.js b/app-shell/src/helpers.js new file mode 100644 index 0000000..1d918b5 --- /dev/null +++ b/app-shell/src/helpers.js @@ -0,0 +1,23 @@ +const jwt = require('jsonwebtoken'); +const config = require('./config'); + +module.exports = class Helpers { + static wrapAsync(fn) { + return function (req, res, next) { + fn(req, res, next).catch(next); + }; + } + + static commonErrorHandler(error, req, res, next) { + if ([400, 403, 404].includes(error.code)) { + return res.status(error.code).send(error.message); + } + + console.error(error); + return res.status(500).send(error.message); + } + + static jwtSign(data) { + return jwt.sign(data, config.secret_key, { expiresIn: '6h' }); + } +}; diff --git a/app-shell/src/index.js b/app-shell/src/index.js new file mode 100644 index 0000000..9428c0b --- /dev/null +++ b/app-shell/src/index.js @@ -0,0 +1,54 @@ +const express = require('express'); +const cors = require('cors'); +const app = express(); +const bodyParser = require('body-parser'); +const checkPermissions = require('./middlewares/check-permissions'); +const modifyPath = require('./middlewares/modify-path'); +const VCS = require('./services/vcs'); + +const executorRoutes = require('./routes/executor'); +const vcsRoutes = require('./routes/vcs'); + +// Function to initialize the Git repository +function initRepo() { + const projectId = '34407'; + return VCS.initRepo(projectId); +} + +// Start the Express app on APP_SHELL_PORT (4000) +function startServer() { + const PORT = 4000; + app.listen(PORT, () => { + console.log(`Listening on port ${PORT}`); + }); +} + +// Run Git check after the server is up +function runGitCheck() { + initRepo() + .then(result => { + console.log(result?.message ? result.message : result); + // Here you can add additional logic if needed + }) + .catch(err => { + console.error('Error during repo initialization:', err); + // Optionally exit the process if Git check is critical: + // process.exit(1); + }); +} + +app.use(cors({ origin: true })); +app.use(bodyParser.json()); +app.use(checkPermissions); +app.use(modifyPath); + +app.use('/executor', executorRoutes); +app.use('/vcs', vcsRoutes); + +// Start the app_shell server +startServer(); + +// Now perform Git check +runGitCheck(); + +module.exports = app; diff --git a/app-shell/src/middlewares/check-permissions.js b/app-shell/src/middlewares/check-permissions.js new file mode 100644 index 0000000..cc9d90a --- /dev/null +++ b/app-shell/src/middlewares/check-permissions.js @@ -0,0 +1,17 @@ +const config = require('../config'); + +function checkPermissions(req, res, next) { + const project_uuid = config.project_uuid; + const requiredHeader = 'X-Project-UUID'; + const headerValue = req.headers[requiredHeader.toLowerCase()]; + // Logging whatever request we're getting + console.log('Request:', req.url, req.method, req.body, req.headers); + + if (headerValue && headerValue === project_uuid) { + next(); + } else { + res.status(403).send({ error: 'Stop right there, criminal scum! Your project UUID is invalid or missing.' }); + } +} + +module.exports = checkPermissions; \ No newline at end of file diff --git a/app-shell/src/middlewares/modify-path.js b/app-shell/src/middlewares/modify-path.js new file mode 100644 index 0000000..0154280 --- /dev/null +++ b/app-shell/src/middlewares/modify-path.js @@ -0,0 +1,8 @@ +function modifyPath(req, res, next) { + if (req.body && req.body.path) { + req.body.path = '../../../' + req.body.path; + } + next(); + } + +module.exports = modifyPath; \ No newline at end of file diff --git a/app-shell/src/routes/executor.js b/app-shell/src/routes/executor.js new file mode 100644 index 0000000..7cb490b --- /dev/null +++ b/app-shell/src/routes/executor.js @@ -0,0 +1,346 @@ +const express = require('express'); +const multer = require('multer'); +const upload = multer({ dest: 'uploads/' }); +const fs = require('fs'); + +const ExecutorService = require('../services/executor'); +const { takeScreenshot } = require("../services/screenshot_service"); + +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +router.post( + '/read_project_tree', + wrapAsync(async (req, res) => { + const { path } = req.body; + const tree = await ExecutorService.readProjectTree(path); + res.status(200).send(tree); + }), +); + +router.post( + '/read_file', + wrapAsync(async (req, res) => { + const { path, showLines } = req.body; + const content = await ExecutorService.readFileContents(path, showLines); + res.status(200).send(content); + }), +); + +router.post( + '/count_file_lines', + wrapAsync(async (req, res) => { + const { path } = req.body; + const content = await ExecutorService.countFileLines(path); + res.status(200).send(content); + }), +); + +// router.post( +// '/read_file_header', +// wrapAsync(async (req, res) => { +// const { path, N } = req.body; +// try { +// const header = await ExecutorService.readFileHeader(path, N); +// res.status(200).send(header); +// } catch (error) { +// res.status(500).send({ +// error: true, +// message: error.message, +// details: error.details || error.stack, +// validation: error.validation +// }); +// } +// }), +// ); + +router.post( + '/read_file_line_context', + wrapAsync(async (req, res) => { + const { path, lineNumber, windowSize, showLines } = req.body; + try { + const context = await ExecutorService.readFileLineContext(path, lineNumber, windowSize, showLines); + res.status(200).send(context); + } catch (error) { + res.status(500).send({ + error: true, + message: error.message, + details: error.details || error.stack, + validation: error.validation + }); + } + }), +); + +router.post( + '/write_file', + wrapAsync(async (req, res) => { + const { path, fileContents, comment } = req.body; + try { + await ExecutorService.writeFile(path, fileContents, comment); + res.status(200).send({ message: 'File written successfully' }); + } catch (error) { + res.status(500).send({ + error: true, + message: error.message, + details: error.details || error.stack, + validation: error.validation + }); + } + }), +); + +router.post( + '/insert_file_content', + wrapAsync(async (req, res) => { + const { path, lineNumber, newContent, message } = req.body; + try { + await ExecutorService.insertFileContent(path, lineNumber, newContent, message); + res.status(200).send({ message: 'File written successfully' }); + } catch (error) { + res.status(500).send({ + error: true, + message: error.message, + details: error.details || error.stack, + validation: error.validation + }); + } + }), +); + +router.post( + '/replace_file_line', + wrapAsync(async (req, res) => { + const { path, lineNumber, newText } = req.body; + try { + const result = await ExecutorService.replaceFileLine(path, lineNumber, newText); + res.status(200).send(result); + } catch (error) { + res.status(500).send({ + error: true, + message: error.message, + details: error.details || error.stack, + validation: error.validation + }); + } + }), +); +router.post( + '/replace_file_chunk', + wrapAsync(async (req, res) => { + const { path, startLine, endLine, newCode } = req.body; + try { + const result = await ExecutorService.replaceFileChunk(path, startLine, endLine, newCode); + res.status(200).send(result); + } catch (error) { + res.status(500).send({ + error: true, + message: error.message, + details: error.details || error.stack, + validation: error.validation + }); + } + }), +); + +router.post( + '/delete_file_lines', + wrapAsync(async (req, res) => { + const { path, startLine, endLine, message } = req.body; + try { + const result = await ExecutorService.deleteFileLines(path, startLine, endLine, message); + res.status(200).send(result); + } catch (error) { + res.status(500).send({ + error: true, + message: error.message, + details: error.details || error.stack, + validation: error.validation + }); + } + }), +); + +router.post( + '/validate_file', + wrapAsync(async (req, res) => { + const { path } = req.body; + try { + const validationResult = await ExecutorService.validateFile(path); + res.status(200).send({ validationResult }); + } catch (error) { + res.status(500).send({ + error: true, + message: error.message, + details: error.details || error.stack, + validation: error.validation + }); + } + }), +); + + +router.post( + '/check_frontend_runtime_error', + wrapAsync(async (req, res) => { + try { + const result = await ExecutorService.checkFrontendRuntimeLogs(); + res.status(200).send(result); + } catch (error) { + res.status(500).send({ error: error }); + } + }), +); + + +router.post( + '/replace_code_block', + wrapAsync(async (req, res) => { + const {path, oldCode, newCode, message} = req.body; + try { + const response = await ExecutorService.replaceCodeBlock(path, oldCode, newCode, message); + res.status(200).send(response); + } catch (error) { + res.status(500).send({ + error: true, + message: error.message, + details: error.details || error.stack, + validation: error.validation + }) + } + }) +) + +router.post('/update_project_files_from_scheme', + upload.single('file'), // 'file' - name of the field in the form + async (req, res) => { + console.log('Request received'); + console.log('Headers:', req.headers); + if (!req.file) { + return res.status(400).json({ error: 'No file uploaded' }); + } + + console.log('File info:', { + originalname: req.file.originalname, + path: req.file.path, + size: req.file.size, + mimetype: req.file.mimetype + }); + + try { + console.log('Starting update process...'); + const result = await ExecutorService.updateProjectFilesFromScheme(req.file.path); + console.log('Update completed, result:', result); + + console.log('Removing temp file...'); + fs.unlinkSync(req.file.path); + console.log('Temp file removed'); + + console.log('Sending response...'); + return res.json(result); + } catch (error) { + console.error('Error in route handler:', error); + if (req.file) { + try { + fs.unlinkSync(req.file.path); + console.log('Temp file removed after error'); + } catch (unlinkError) { + console.error('Error removing temp file:', unlinkError); + } + } + console.error('Update project files error:', error); + return res.status(500).json({ + error: error.message, + stack: process.env.NODE_ENV === 'development' ? error.stack : undefined + }); + } + } +); + +router.post( + '/get_db_schema', + wrapAsync(async (req, res) => { + try { + + const jsonSchema = await ExecutorService.getDBSchema(); + res.status(200).send({ jsonSchema }); + } catch (error) { + res.status(500).send({ error: error }); + } + }), +); + +router.post( + '/execute_sql', + wrapAsync(async (req, res) => { + try { + const { query } = req.body; + const result = await ExecutorService.executeSQL(query); + res.status(200).send(result); + } catch (error) { + res.status(500).send({ error: error }); + } + }), +); + +router.post( + '/search_files', + wrapAsync(async (req, res) => { + try { + const { searchStrings } = req.body; + + if ( + typeof searchStrings !== 'string' && + !( + Array.isArray(searchStrings) && + searchStrings.every(item => typeof item === 'string') + ) + ) { + return res.status(400).send({ error: 'searchStrings must be a string or an array of strings' }); + } + + const result = await ExecutorService.searchFiles(searchStrings); + res.status(200).send(result); + } catch (error) { + res.status(500).send({ error: error.message }); + } + }), +); + +router.post( + '/screenshot', + wrapAsync(async (req, res) => { + const FRONT_PORT = process.env.FRONT_PORT || 3001; + const targetUrl = `http://localhost:${FRONT_PORT}${req.body.url || '/'}`; + + const filename = req.query.filename || `screenshot-${Date.now()}.png`; + const fullPage = req.query.fullPage !== 'false'; + + try { + console.log(`[App-Shell/Screenshot Route]: request to take screenshot of ${targetUrl} with filename ${filename} and fullPage=${fullPage}`); + const outputPath = await takeScreenshot(targetUrl, filename, fullPage); + + res.sendFile(outputPath, async (err) => { + if (err) { + console.error(`[App-Shell/Screenshot Route]: error sending screenshot ${outputPath}:`, err); + if (err.code === 'ENOENT') { + res.status(404).send('Screenshot not found.'); + } else { + res.status(500).send('Error sending screenshot: ' + err.message); + } + } else { + console.log(`[App-Shell/Screenshot Route]: Screenshot sent successfully: ${outputPath}`); + } + }); + + } catch (error) { + console.error(`[App-Shell/Screenshot Route]: Could not take screenshot of ${targetUrl}:`, error); + res.status(500).send(`Error taking screenshot: ${error.message}`); + } + }) +) + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/app-shell/src/routes/vcs.js b/app-shell/src/routes/vcs.js new file mode 100644 index 0000000..926498d --- /dev/null +++ b/app-shell/src/routes/vcs.js @@ -0,0 +1,40 @@ +const express = require('express'); +const wrapAsync = require('../helpers').wrapAsync; // Ваша обёртка для обработки асинхронных маршрутов +const VSC = require('../services/vcs'); +const router = express.Router(); + +router.post('/init', wrapAsync(async (req, res) => { + const result = await VSC.initRepo(); + res.status(200).send(result); +})); + +router.post('/commit', wrapAsync(async (req, res) => { + const { message, files, dev_schema } = req.body; + const result = await VSC.commitChanges(message, files, dev_schema); + res.status(200).send(result); +})); + +router.post('/log', wrapAsync(async (req, res) => { + const result = await VSC.getLog(); + res.status(200).send(result); +})); + +router.post('/rollback', wrapAsync(async (req, res) => { + const { ref } = req.body; + // const result = await VSC.checkout(ref); + const result = await VSC.revert(ref); + res.status(200).send(result); +})); + +router.post('/sync-to-stable', wrapAsync(async (req, res) => { + const result = await VSC.mergeDevIntoMaster(); + res.status(200).send(result); +})); + +router.post('/reset-dev', wrapAsync(async (req, res) => { + const result = await VSC.resetDevBranch(); + res.status(200).send(result); +})); + +router.use('/', require('../helpers').commonErrorHandler); +module.exports = router; \ No newline at end of file diff --git a/app-shell/src/services/database.js b/app-shell/src/services/database.js new file mode 100644 index 0000000..bf8f3a9 --- /dev/null +++ b/app-shell/src/services/database.js @@ -0,0 +1,88 @@ +// Database.js +const { Client } = require('pg'); +const config = require('../../../backend/src/db/db.config'); + +const env = process.env.NODE_ENV || 'development'; +const dbConfig = config[env]; + +class Database { + constructor() { + this.client = new Client({ + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database, + host: dbConfig.host, + port: dbConfig.port + }); + + // Connect once, reuse the client + this.client.connect().catch(err => { + console.error('Error connecting to the database:', err); + throw err; + }); + } + + async executeSQL(query) { + try { + const result = await this.client.query(query); + return { + success: true, + rows: result.rows + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + // Method to fetch simple table/column info from 'information_schema' + // (You can expand this to handle constraints, indexes, etc.) + async getDBSchema(schemaName = 'public') { + try { + const tableQuery = ` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = $1 + AND table_type = 'BASE TABLE' + ORDER BY table_name + `; + + const columnQuery = ` + SELECT table_name, column_name, data_type, is_nullable + FROM information_schema.columns + WHERE table_schema = $1 + ORDER BY table_name, ordinal_position + `; + + const [tablesResult, columnsResult] = await Promise.all([ + this.client.query(tableQuery, [schemaName]), + this.client.query(columnQuery, [schemaName]), + ]); + + // Build a simple schema object: + const tables = tablesResult.rows.map(row => row.table_name); + const columnsByTable = {}; + + columnsResult.rows.forEach(row => { + const { table_name, column_name, data_type, is_nullable } = row; + if (!columnsByTable[table_name]) columnsByTable[table_name] = []; + columnsByTable[table_name].push({ column_name, data_type, is_nullable }); + }); + + // Combine tables with their columns + return tables.map(table => ({ + table, + columns: columnsByTable[table] || [], + })); + } catch (error) { + console.error('Error fetching schema:', error); + throw error; + } + } + + async close() { + await this.client.end(); + } +} + +module.exports = new Database(); diff --git a/app-shell/src/services/executor.js b/app-shell/src/services/executor.js new file mode 100644 index 0000000..eecb869 --- /dev/null +++ b/app-shell/src/services/executor.js @@ -0,0 +1,1206 @@ +const fs = require('fs').promises; +const os = require('os'); +const path = require('path'); +const AdmZip = require('adm-zip'); +const { exec } = require('child_process'); +const util = require('util'); +const ProjectEventsService = require('./project-events'); +const config = require('../config.js'); +// Babel Parser for JS/TS/TSX +const babelParser = require('@babel/parser'); +const babelParse = babelParser.parse; + +// Local App DB Connection +const database = require('./database'); + +// PostCSS for CSS +const postcss = require('postcss'); + +const execAsync = util.promisify(exec); + +module.exports = class ExecutorService { + static async readProjectTree(directoryPath) { + const paths = { + frontend: '../../../frontend', + backend: '../../../backend', + default: '../../../' + }; + + try { + const publicDir = path.join(__dirname, paths[directoryPath] || directoryPath || paths.default); + + return await getDirectoryTree(publicDir); + } catch (error) { + console.error('Error reading directory:', error); + + throw error; + } + } + + static async readFileContents(filePath, showLines) { + try { + const fullPath = path.join(__dirname, filePath); + const content = await fs.readFile(fullPath, 'utf8'); + + if (showLines) { + const lines = content.split('\n'); + + const lineObject = {}; + lines.forEach((line, index) => { + lineObject[index + 1] = line; + }); + + return lineObject; + } else { + return content; + } + } catch (error) { + console.error('Error reading file:', error); + throw error; + } + } + + static async countFileLines(filePath) { + try { + const fullPath = path.join(__dirname, filePath); + + // Check file exists + await fs.access(fullPath); + + // Read file content + const content = await fs.readFile(fullPath, 'utf8'); + + // Split by newline and count + const lines = content.split('\n'); + + return { + success: true, + lineCount: lines.length + }; + } catch (error) { + console.error('Error counting file lines:', error); + return { + success: false, + message: error.message + }; + } + } + + // static async readFileHeader(filePath, N = 30) { + // try { + // const fullPath = path.join(__dirname, filePath); + // const content = await fs.readFile(fullPath, 'utf8'); + // const lines = content.split('\n'); + // + // if (lines.length < N) { + // return { error: `File has less than ${N} lines` }; + // } + // + // const headerLines = lines.slice(0, Math.min(50, lines.length)); + // + // const lineObject = {}; + // headerLines.forEach((line, index) => { + // lineObject[index + 1] = line; + // }); + // + // return lineObject; + // } catch (error) { + // console.error('Error reading file header:', error); + // throw error; + // } + // } + + static async readFileLineContext(filePath, lineNumber, windowSize, showLines) { + try { + const fullPath = path.join(__dirname, filePath); + const content = await fs.readFile(fullPath, 'utf8'); + const lines = content.split('\n'); + + const start = Math.max(0, lineNumber - windowSize); + const end = Math.min(lines.length, lineNumber + windowSize + 1); + + const contextLines = lines.slice(start, end); + + if (showLines) { + const lineObject = {}; + contextLines.forEach((line, index) => { + lineObject[start + index + 1] = line; + }); + + return lineObject; + } else { + return contextLines.join('\n'); + } + } catch (error) { + console.error('Error reading file line context:', error); + throw error; + } + } + + static async validateFile(filePath) { + console.log('Validating file:', filePath); + + // Read file content + let content; + try { + content = await fs.readFile(filePath, 'utf8'); + } catch (err) { + throw new Error(`Could not read file: ${filePath}\n${err.message}`); + } + + // Determine file extension + let ext = path.extname(filePath).toLowerCase(); + if (ext === '.temp') { + ext = path.extname(filePath.slice(0, -5)).toLowerCase(); + } + + try { + switch (ext) { + case '.js': + case '.ts': + case '.tsx': { + // Parse JS/TS/TSX with Babel + babelParse(content, { + sourceType: 'module', + // plugins array covers JS, TS, TSX, and optional JS flavors + plugins: ['jsx', 'typescript'] + }); + break; + } + + case '.css': { + // Parse CSS with PostCSS + postcss.parse(content); + break; + } + + default: { + // If the extension isn't recognized, assume it's "valid" + // or you could throw an error to force a known extension + console.warn(`No validation implemented for extension "${ext}". Skipping syntax check.`); + } + } + + // If parsing succeeded, return true + return true; + + } catch (parseError) { + // Rethrow parse errors with a friendlier message + throw parseError; + } + } + + static async checkFrontendRuntimeLogs() { + const frontendLogPath = '../frontend/json/runtimeError.json'; + + try { + // Check if file exists + try { + console.log('Accessing frontend logs:', frontendLogPath); + await fs.access(frontendLogPath); + } catch (error) { + console.log('Frontend logs not found:', error); + // File doesn't exist - return empty object + return {runtime_error: {}}; + } + + // File exists, try to read it + try { + // Read the entire file instead of using tail + const fileContent = await fs.readFile(frontendLogPath, 'utf8'); + console.log('Reading frontend logs:', fileContent); + + // Handle empty file + if (!fileContent || fileContent.trim() === '') { + return {runtime_error: {}}; + } + + // Parse JSON content + const runtime_error = JSON.parse(fileContent); + + console.log('Parsed frontend logs:', runtime_error); + return {runtime_error}; + } catch (error) { + // Error reading or parsing file + console.error('Error reading frontend runtime logs:', error); + return {runtime_error: {}}; + } + } catch (error) { + // Unexpected error + console.log('Error checking frontend logs:', error); + return {runtime_error: {}}; + } + } + + static async writeFile(filePath, fileContents, comment) { + try { + console.log(comment) + const fullPath = path.join(__dirname, filePath); + + // Write to a temp file first + const tempPath = `${fullPath}.temp`; + await fs.writeFile(tempPath, fileContents, 'utf8'); + + // Validate the temp file + await this.validateFile(tempPath); + + // Rename temp file to original path + await fs.rename(tempPath, fullPath); + + return true; + } catch (error) { + console.error('Error writing file:', error); + throw error; + } + } + + static async insertFileContent(filePath, lineNumber, newContent, message) { + try { + const fullPath = path.join(__dirname, filePath); + + // Check file exists + await fs.access(fullPath); + + // Read and split by line + const content = await fs.readFile(fullPath, 'utf8'); + const lines = content.split('\n'); + + // Ensure lineNumber is within [1 ... lines.length + 1] + // 1 means "insert at the very first line" + // lines.length + 1 means "append at the end" + if (lineNumber < 1) { + lineNumber = 1; + } + if (lineNumber > lines.length + 1) { + lineNumber = lines.length + 1; + } + + // Convert to 0-based index + const insertIndex = lineNumber - 1; + + // Prepare preview + const preview = { + insertionLine: lineNumber, + insertedLines: newContent.split('\n') + }; + + // Insert newContent lines at the specified index + lines.splice(insertIndex, 0, ...newContent.split('\n')); + + // Write changes to a temp file first + const updatedContent = lines.join('\n'); + const tempPath = `${fullPath}.temp`; + await fs.writeFile(tempPath, updatedContent, 'utf8'); + + await this.validateFile(tempPath); + + // Rename temp file to original path + await fs.rename(tempPath, fullPath); + + return { + success: true + }; + + } catch (error) { + console.error('Error inserting file content:', error); + throw error; + } + } + + static async replaceFileLine(filePath, lineNumber, newText, message = null) { + const fullPath = path.join(__dirname, filePath); + try { + + try { + await fs.access(fullPath); + } catch (error) { + throw new Error(`File not found: ${filePath}`); + } + + const content = await fs.readFile(fullPath, 'utf8'); + const lines = content.split('\n'); + + if (lineNumber < 1 || lineNumber > lines.length) { + throw new Error(`Invalid line number: ${lineNumber}. File has ${lines.length} lines`); + } + + if (typeof newText !== 'string') { + throw new Error('New text must be a string'); + } + + const preview = { + oldLine: lines[lineNumber - 1], + newLine: newText, + lineNumber: lineNumber + }; + + lines[lineNumber - 1] = newText; + const newContent = lines.join('\n'); + const tempPath = `${fullPath}.temp`; + await fs.writeFile(tempPath, newContent, 'utf8'); + + await this.validateFile(tempPath); + + await fs.rename(tempPath, fullPath); + + return { + success: true + }; + + } catch (error) { + console.error('Error updating file line:', error); + + try { + await fs.unlink(`${fullPath}.temp`); + } catch { + } + + throw { + error: error, + message: error.message, + details: error.stack + }; + } + } + + static async replaceFileChunk(filePath, startLine, endLine, newCode) { + try { + // Check if this is a single-line change + const newCodeLines = newCode.split('\n'); + if (newCodeLines.length === 1 && endLine === startLine) { + // Redirect to replace_file_line + return await this.replaceFileLine(filePath, startLine, newCode); + } + + const fullPath = path.join(__dirname, filePath); + + // Check if file exists + try { + await fs.access(fullPath); + } catch (error) { + throw new Error(`File not found: ${filePath}`); + } + + const content = await fs.readFile(fullPath, 'utf8'); + const lines = content.split('\n'); + + // Adjust line numbers to array indices (subtract 1) + const startIndex = startLine - 1; + const endIndex = endLine - 1; + + // Validate input parameters + if (startIndex < 0 || endIndex >= lines.length || startIndex > endIndex) { + throw new Error(`Invalid line range: ${startLine}-${endLine}. File has ${lines.length} lines`); + } + + // Check type of new code + if (typeof newCode !== 'string') { + throw new Error('New code must be a string'); + } + + // Create changes preview + const preview = { + oldLines: lines.slice(startIndex, endIndex + 1), + newLines: newCode.split('\n'), + startLine, + endLine + }; + + // Apply changes to temp file first + lines.splice(startIndex, endIndex - startIndex + 1, ...newCode.split('\n')); + const newContent = lines.join(os.EOL); + const tempPath = `${fullPath}.temp`; + await fs.writeFile(tempPath, newContent, 'utf8'); + await this.validateFile(tempPath); + // Apply changes if all validations passed + await fs.rename(tempPath, fullPath); + + return { + success: true + }; + + } catch (error) { + console.error('Error updating file slice:', error); + + // Clean up temp file if exists + try { + await fs.unlink(`${fullPath}.temp`); + } catch { + } + + throw { + error: error, + message: error.message, + details: error.details || error.stack + }; + } + } + + static async replaceCodeBlock(filePath, oldCode, newCode, message) { + try { + console.log(message); + const fullPath = path.join(__dirname, filePath); + + // Check file exists + await fs.access(fullPath); + + // Read file content + let content = await fs.readFile(fullPath, 'utf8'); + + // A small helper to unify line breaks to just `\n` + const unifyLineBreaks = (str) => str.replace(/\r\n/g, '\n'); + + // Normalize line breaks in file content, oldCode, and newCode + content = unifyLineBreaks(content); + oldCode = unifyLineBreaks(oldCode); + newCode = unifyLineBreaks(newCode); + + // Optional: Trim trailing spaces or handle other whitespace normalization if needed + // oldCode = oldCode.trim(); + // newCode = newCode.trim(); + + // Check if oldCode actually exists in the content + const index = content.indexOf(oldCode); + if (index === -1) { + return { + success: false, + message: 'Old code not found in file.' + }; + } + + // Create a preview before replacing + const preview = { + oldCodeSnippet: oldCode, + newCodeSnippet: newCode + }; + + // Perform replacement (single occurrence). For multiple, use replaceAll or a loop. + // If you want a global replacement, consider: + // content = content.split(oldCode).join(newCode); + content = content.replace(oldCode, newCode); + + // Write to a temp file first + const tempPath = `${fullPath}.temp`; + await fs.writeFile(tempPath, content, 'utf8'); + + await this.validateFile(tempPath); + // Rename temp file to original + await fs.rename(tempPath, fullPath); + + return { + success: true + }; + + } catch (error) { + console.error('Error replacing code:', error); + return { + error: error, + message: error.message, + details: error.details || error.stack + }; + } + } + + //todo add validation + static async deleteFileLines(filePath, startLine, endLine, veryShortDescription) { + try { + const fullPath = path.join(__dirname, filePath); + + // Check if file exists + await fs.access(fullPath); + + // Read file content + const content = await fs.readFile(fullPath, 'utf8'); + const lines = content.split('\n'); + + // Convert to zero-based indices + const startIndex = startLine - 1; + const endIndex = endLine - 1; + + // Validate range + if (startIndex < 0 || endIndex >= lines.length || startIndex > endIndex) { + throw new Error( + `Invalid line range: ${startLine}-${endLine}. File has ${lines.length} lines` + ); + } + + // Prepare a preview of the lines being deleted + const preview = { + deletedLines: lines.slice(startIndex, endIndex + 1), + startLine, + endLine + }; + + // Remove lines + lines.splice(startIndex, endIndex - startIndex + 1); + + // Join remaining lines and write to a temporary file + const newContent = lines.join('\n'); + const tempPath = `${fullPath}.temp`; + await fs.writeFile(tempPath, newContent, 'utf8'); + + await this.validateFile(tempPath); + // Rename temp file to original + await fs.rename(tempPath, fullPath); + + return { + success: true + }; + + } catch (error) { + console.error('Error deleting file lines:', error); + return { + error: error, + message: error.message, + details: error.details || error.stack + }; + } + } + + static async validateTypeScript(filePath, content = null) { + try { + // Basic validation of JSX syntax + const jsxErrors = []; + + if (content !== null) { + // Check for matching braces + if ((content.match(/{/g) || []).length !== (content.match(/}/g) || []).length) { + jsxErrors.push("Unmatched curly braces"); + } + + // Check for invalid syntax in JSX attributes + if (content.includes('label={')) { + if (!content.match(/label={[^}]+}/)) { + jsxErrors.push("Invalid label attribute syntax"); + } + } + + if (jsxErrors.length > 0) { + return { + valid: false, + errors: jsxErrors.map(error => ({ + code: 'JSX_SYNTAX_ERROR', + severity: 'error', + location: '', + message: error + })) + }; + } + } + + return { + valid: true, + errors: [], + errorCount: 0, + warningCount: 0 + }; + + } catch (error) { + console.error('TypeScript validation error:', error); + return { + valid: false, + errors: [{ + code: 'VALIDATION_FAILED', + severity: 'error', + location: '', + message: `TypeScript validation error: ${error.message}` + }], + errorCount: 1, + warningCount: 0 + }; + } + } + + static async validateBackendFiles(backendPath) { + try { + // Check for syntax errors + await execAsync(`node --check ${backendPath}/src/index.js`); + + // Try to run the code in a test environment + const testProcess = exec( + 'NODE_ENV=test node -e "try { require(\'./src/index.js\') } catch(e) { console.error(e); process.exit(1) }"', + {cwd: backendPath} + ); + + return new Promise((resolve) => { + let output = ''; + let error = ''; + + testProcess.stdout.on('data', (data) => { + output += data; + }); + + testProcess.stderr.on('data', (data) => { + error += data; + }); + + testProcess.on('close', (code) => { + if (code === 0) { + resolve({valid: true}); + } else { + resolve({ + valid: false, + error: error || output + }); + } + }); + + // Timeout on validation + setTimeout(() => { + testProcess.kill(); + resolve({ + valid: true, + warning: 'Validation timeout, but no immediate errors found' + }); + }, 5000); + }); + } catch (error) { + return { + valid: false, + error: error.message + }; + } + } + + static async createBackup(ROOT_PATH) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const backupDir = path.join(ROOT_PATH, 'backups', timestamp); + + try { + await fs.mkdir(path.join(ROOT_PATH, 'backups'), {recursive: true}); + + const dirsToBackup = ['frontend', 'backend']; + + for (const dir of dirsToBackup) { + const sourceDir = path.join(ROOT_PATH, dir); + const targetDir = path.join(backupDir, dir); + + await fs.mkdir(targetDir, {recursive: true}); + + await execAsync( + `cd "${sourceDir}" && ` + + `find . -type f -not -path "*/node_modules/*" -not -path "*/\\.*" | ` + + `while read file; do ` + + `mkdir -p "${targetDir}/$(dirname "$file")" && ` + + `cp "$file" "${targetDir}/$file"; ` + + `done` + ); + } + + console.log('Backup created at:', backupDir); + return backupDir; + } catch (error) { + console.error('Error creating backup:', error); + throw error; + } + } + + static async restoreFromBackup(backupDir, ROOT_PATH) { + try { + console.log('Restoring from backup:', backupDir); + await execAsync(`rm -rf ${ROOT_PATH}/backend/*`); + await execAsync(`cp -r ${backupDir}/* ${ROOT_PATH}/backend/`); + return true; + } catch (error) { + console.error('Error restoring from backup:', error); + throw error; + } + } + + static async updateProjectFilesFromScheme(zipFilePath) { + const MAX_FILE_SIZE = 10 * 1024 * 1024; + const ROOT_PATH = path.join(__dirname, '../../../'); + + try { + console.log('Checking file access...'); + await fs.access(zipFilePath); + + console.log('Getting file stats...'); + const stats = await fs.stat(zipFilePath); + console.log('File size:', stats.size); + + if (stats.size > MAX_FILE_SIZE) { + console.log('File size exceeds limit'); + return {success: false, error: 'File size exceeds limit'}; + } + + // Copying zip file to /tmp + const tempZipPath = path.join('/tmp', path.basename(zipFilePath)); + await fs.copyFile(zipFilePath, tempZipPath); + + // Launching background update process + const servicesUpdate = (async () => { + try { + console.log('Stopping services...'); + + // await ProjectEventsService.sendEvent('SERVICE_STOP_STARTED', { + // message: 'Stopping services', + // timestamp: new Date().toISOString() + // }); + + await stopServices(); + + // await ProjectEventsService.sendEvent('SERVICE_STOP_COMPLETED', { + // message: 'Services stopped successfully', + // timestamp: new Date().toISOString() + // }); + + console.log('Creating zip instance...'); + const zip = new AdmZip(tempZipPath); + + console.log('Extracting files to:', ROOT_PATH); + zip.extractAllTo(ROOT_PATH, true); + console.log('Files extracted'); + + const removedFilesPath = path.join(ROOT_PATH, 'removed_files.json'); + try { + await fs.access(removedFilesPath); + const removedFilesContent = await fs.readFile(removedFilesPath, 'utf8'); + const filesToRemove = JSON.parse(removedFilesContent); + await removeFiles(filesToRemove, ROOT_PATH); + + await fs.unlink(removedFilesPath); + } catch (error) { + console.log('No removed files to process or error accessing removed_files.json:', error); + } + + // Remove temp zip file + await fs.unlink(tempZipPath); + + // await ProjectEventsService.sendEvent('SERVICE_START_STARTED', { + // message: 'Starting services', + // timestamp: new Date().toISOString() + // }); + + // Start services after a delay + setTimeout(async () => { + try { + await startServices(); + console.log('Services started successfully'); + + await ProjectEventsService.sendEvent('SERVICE_START_COMPLETED', { + message: 'All files have been successfully retrieved and applied.', + timestamp: new Date().toISOString() + }); + } catch (e) { + console.error('Failed to start services:', e); + } + }, 3000); + + } catch (error) { + console.error('Error in service update process:', error); + } + })(); + + servicesUpdate.catch(error => { + console.error('Background update process failed:', error); + }); + + console.log('Returning immediate response'); + + return { + success: true, + message: 'Update process initiated' + }; + + } catch (error) { + console.error('Critical error in updateProjectFilesFromScheme:', error); + return { + success: false, + error: error.message + }; + } + } + + static async getDBSchema() { + try { + return await database.getDBSchema(); + } catch (error) { + console.error('Error reading schema:', error); + throw { + error: error, + message: error.message, + details: error.details || error.stack + }; + } + } + + static async executeSQL(query) { + try { + return await database.executeSQL(query); + } catch (error) { + console.error('Error executing query:', error); + throw { + error: error, + message: error.message, + details: error.details || error.stack + }; + } + } + + static async stopServices() { + return await stopServices(); + } + + static async startServices() { + return await startServices(); + } + + static async checkServicesStatus() { + return await checkStatus(); + } + + static async searchFiles(searchStrings) { + const results = {}; + const ROOT_PATH = path.join(__dirname, '../../../'); + const directories = [`${ROOT_PATH}backend/`, `${ROOT_PATH}frontend/`]; + const excludeDirs = ['node_modules', 'build', 'app_shell']; + + if (!Array.isArray(searchStrings)) { + searchStrings = [searchStrings]; + } + + for (const searchString of searchStrings) { + try { + for (const directoryPath of directories) { + const findCommand = `find '${directoryPath}' -type f ${excludeDirs.map(dir => `-not -path "*/${dir}/*"`).join(' ')} -print | xargs grep -nH -C 1 -e '${searchString}'`; + + try { + const { stdout } = await execAsync(findCommand); + + const lines = stdout.trim().split('\n').filter(line => line !== ''); + const searchResults = {}; + // searchResults['__raw_lines__'] = lines; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const parts = line.split(':'); + let filePath = ''; + let lineNumberStr = ''; + let content = ''; + let relativeFilePath = ''; + let lineNum = null; + + if (parts.length >= 3 && !parts[0].includes('-')) { + filePath = parts.shift(); + lineNumberStr = parts.shift(); + content = parts.join(':').trim(); + relativeFilePath = filePath.replace(`${ROOT_PATH}`, ''); + lineNum = parseInt(lineNumberStr, 10) + 1; + } else { + content = line.trim(); + } + + const context = []; + if (i > 0 && lines[i - 1].includes(':')) { + const prevLineParts = lines[i - 1].split(':'); + if (prevLineParts.length >= 3 && !prevLineParts[0].includes('-')) { + prevLineParts.shift(); + prevLineParts.shift(); + context.push(prevLineParts.join(':').trim()); + } else { + context.push(lines[i - 1].trim()); + } + } + context.push(content); + if (i < lines.length - 1 && lines[i + 1].includes(':')) { + const nextLineParts = lines[i + 1].split(':'); + if (nextLineParts.length >= 3 && !nextLineParts[0].includes('-')) { + nextLineParts.shift(); + nextLineParts.shift(); + context.push(nextLineParts.join(':').trim()); + } else { + context.push(lines[i + 1].trim()); + } + } + + if (relativeFilePath && !searchResults[relativeFilePath]) { + searchResults[relativeFilePath] = []; + } + if (relativeFilePath) { + searchResults[relativeFilePath].push({ + lineNumber: lineNum, + context: context.join('\n'), + // __filePathAndLine__: filePath + ':' + lineNumberStr + ':' + content, + }); + } + } + + if (!results[searchString]) { + results[searchString] = {}; + } + Object.assign(results[searchString], searchResults); + } catch (err) { + if (!err.message.includes('No such file or directory') && !err.stderr.includes('No such file or directory')) { + console.error(`Error using find/grep for "${searchString}" in ${directoryPath}:`, err); + } + } + } + } catch (error) { + console.error(`Error searching for "${searchString}":`, error); + results[searchString] = { error: error.message }; + } + } + + return results; + } + +} + +async function getDirectoryTree(dirPath) { + const entries = await fs.readdir(dirPath, { withFileTypes: true }); + const result = {}; + + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + + if (entry.isDirectory() && ( + entry.name === 'node_modules' || + entry.name === 'app-shell' || + entry.name === '.git' || + entry.name === '.idea' + )) { + continue; + } + + const relativePath = fullPath.replace('/app', ''); + + if (entry.isDirectory()) { + const subTree = await getDirectoryTree(fullPath); + Object.keys(subTree).forEach(key => { + result[key.replace('/app', '')] = subTree[key]; + }); + } else { + const fileContent = await fs.readFile(fullPath, 'utf8'); + const lineCount = fileContent.split('\n').length; + result[relativePath] = lineCount; + } + } + + return result; +} + +async function stopServices() { + try { + console.log('Finding service processes...'); + // await ProjectEventsService.sendEvent('SERVICE_STOP_INITIATED', { + // message: 'Initiating service stop', + // timestamp: new Date().toISOString() + // }); + // Frontend stopping + const { stdout: frontendProcess } = await execAsync("ps -o pid,cmd | grep '[n]ext-server' | awk '{print $1}'"); + if (frontendProcess.trim()) { + console.log('Stopping frontend, pid:', frontendProcess.trim()); + + // await ProjectEventsService.sendEvent('FRONTEND_STOP_STARTED', { + // message: `Stopping frontend, pid: ${frontendProcess.trim()}`, + // timestamp: new Date().toISOString() + // }); + + // await execAsync(`kill -15 ${frontendProcess.trim()}`); + + // await ProjectEventsService.sendEvent('FRONTEND_STOP_COMPLETED', { + // message: 'Frontend stopped successfully', + // timestamp: new Date().toISOString() + // }); + } + + // Backend stopping + const { stdout: backendProcess } = await execAsync("ps -o pid,cmd | grep '[n]ode ./src/index.js' | grep -v app-shell | awk '{print $1}'"); + if (backendProcess.trim()) { + console.log('Stopping backend, pid:', backendProcess.trim()); + + // await ProjectEventsService.sendEvent('BACKEND_STOP_STARTED', { + // message: `Stopping backend, pid: ${backendProcess.trim()}`, + // timestamp: new Date().toISOString() + // }); + + // await execAsync(`kill -15 ${backendProcess.trim()}`); + + // await ProjectEventsService.sendEvent('BACKEND_STOP_COMPLETED', { + // message: 'Backend stopped successfully', + // timestamp: new Date().toISOString() + // }); + } + + await new Promise(resolve => setTimeout(resolve, 4000)); + + + // await ProjectEventsService.sendEvent('SERVICE_STOP_COMPLETED', { + // message: 'All services stopped successfully', + // timestamp: new Date().toISOString() + // }); + + return { success: true }; + } catch (error) { + console.error('Error stopping services:', error); + + await ProjectEventsService.sendEvent('SERVICE_STOP_FAILED', { + message: 'Error stopping services', + error: error.message, + timestamp: new Date().toISOString() + }); + + return { success: false, error: error.message }; + } +} + +async function startServices() { + try { + console.log('Starting services...'); + // await ProjectEventsService.sendEvent('SERVICE_START_INITIATED', { + // message: 'Initiating service start', + // timestamp: new Date().toISOString() + // }); + + // await ProjectEventsService.sendEvent('FRONTEND_START_STARTED', { + // message: 'Starting frontend service', + // timestamp: new Date().toISOString() + // }); + // await execAsync('yarn --cwd /app/frontend dev &'); + // await ProjectEventsService.sendEvent('FRONTEND_START_COMPLETED', { + // message: 'Frontend service started successfully', + // timestamp: new Date().toISOString() + // }); + + // await ProjectEventsService.sendEvent('BACKEND_START_STARTED', { + // message: 'Starting backend service', + // timestamp: new Date().toISOString() + // }); + // await execAsync('yarn --cwd /app/backend start &'); + // await ProjectEventsService.sendEvent('BACKEND_START_COMPLETED', { + // message: 'Backend service started successfully', + // timestamp: new Date().toISOString() + // }); + + // await ProjectEventsService.sendEvent('SERVICE_START_COMPLETED', { + // message: 'All services started successfully', + // timestamp: new Date().toISOString() + // }); + + return { success: true }; + } catch (error) { + console.error('Error starting services:', error); + await ProjectEventsService.sendEvent('SERVICE_START_FAILED', { + message: 'Error starting services', + error: error.message, + timestamp: new Date().toISOString() + }); + return { success: false, error: error.message }; + } +} + +async function checkStatus() { + try { + const { stdout } = await execAsync('ps aux'); + return { + success: true, + frontendRunning: stdout.includes('next-server'), + backendRunning: stdout.includes('nodemon') && stdout.includes('/app/backend'), + nginxRunning: stdout.includes('nginx: master process') + }; + } catch (error) { + return { + success: false, + error: error.message + }; + } +} + +async function validateJSXSyntax(code) { + // Define validation rules for JSX + const rules = [ + { + // JSX attribute with expression + pattern: /^[a-zA-Z][a-zA-Z0-9]*={.*}$/, + message: 'Invalid JSX attribute syntax' + }, + { + // Invalid sequences + pattern: /,{2,}/, + message: 'Invalid character sequence detected', + shouldNotMatch: true + }, + { + // Ternary expressions + pattern: /^[a-zA-Z][a-zA-Z0-9]*={[\w\s]+\?[^}]+:[^}]+}$/, + message: 'Invalid ternary expression in JSX' + } + ]; + + // Validate each line + const lines = code.split('\n'); + for (const line of lines) { + const trimmedLine = line.trim(); + + // Skip empty lines + if (!trimmedLine) continue; + + // Check each rule + for (const rule of rules) { + if (rule.shouldNotMatch) { + // For patterns that should not be present + if (rule.pattern.test(trimmedLine)) { + return { + valid: false, + errors: [{ + code: 'JSX_SYNTAX_ERROR', + severity: 'error', + location: '', + message: rule.message + }] + }; + } + } else { + // For patterns that should match + if (trimmedLine.includes('=') && !rule.pattern.test(trimmedLine)) { + return { + valid: false, + errors: [{ + code: 'JSX_SYNTAX_ERROR', + severity: 'error', + location: '', + message: rule.message + }] + }; + } + } + } + + // Additional JSX-specific checks + if ((trimmedLine.match(/{/g) || []).length !== (trimmedLine.match(/}/g) || []).length) { + return { + valid: false, + errors: [{ + code: 'JSX_SYNTAX_ERROR', + severity: 'error', + location: '', + message: 'Unmatched curly braces in JSX' + }] + }; + } + } + + // If all checks pass + return { + valid: true, + errors: [] + }; +} + +async function removeFiles(files, rootPath) { + try { + for (const file of files) { + const fullPath = path.join(rootPath, file); + try { + await fs.unlink(fullPath); + console.log(`File removed: ${fullPath}`); + } catch (error) { + console.error(`Error when trying to delete a file ${fullPath}:`, error); + } + } + } catch (error) { + console.error('Error removing files:', error); + throw error; + } +} \ No newline at end of file diff --git a/app-shell/src/services/notifications/errors/forbidden.js b/app-shell/src/services/notifications/errors/forbidden.js new file mode 100644 index 0000000..192fa10 --- /dev/null +++ b/app-shell/src/services/notifications/errors/forbidden.js @@ -0,0 +1,16 @@ +const { getNotification, isNotification } = require('../helpers'); + +module.exports = class ForbiddenError extends Error { + constructor(messageCode) { + let message; + + if (messageCode && isNotification(messageCode)) { + message = getNotification(messageCode); + } + + message = message || getNotification('errors.forbidden.message'); + + super(message); + this.code = 403; + } +}; diff --git a/app-shell/src/services/notifications/errors/validation.js b/app-shell/src/services/notifications/errors/validation.js new file mode 100644 index 0000000..464550c --- /dev/null +++ b/app-shell/src/services/notifications/errors/validation.js @@ -0,0 +1,16 @@ +const { getNotification, isNotification } = require('../helpers'); + +module.exports = class ValidationError extends Error { + constructor(messageCode) { + let message; + + if (messageCode && isNotification(messageCode)) { + message = getNotification(messageCode); + } + + message = message || getNotification('errors.validation.message'); + + super(message); + this.code = 400; + } +}; diff --git a/app-shell/src/services/notifications/helpers.js b/app-shell/src/services/notifications/helpers.js new file mode 100644 index 0000000..1c3a60f --- /dev/null +++ b/app-shell/src/services/notifications/helpers.js @@ -0,0 +1,30 @@ +const _get = require('lodash/get'); +const errors = require('./list'); + +function format(message, args) { + if (!message) { + return null; + } + + return message.replace(/{(\d+)}/g, function (match, number) { + return typeof args[number] != 'undefined' ? args[number] : match; + }); +} + +const isNotification = (key) => { + const message = _get(errors, key); + return !!message; +}; + +const getNotification = (key, ...args) => { + const message = _get(errors, key); + + if (!message) { + return key; + } + + return format(message, args); +}; + +exports.getNotification = getNotification; +exports.isNotification = isNotification; diff --git a/app-shell/src/services/notifications/list.js b/app-shell/src/services/notifications/list.js new file mode 100644 index 0000000..a0a1613 --- /dev/null +++ b/app-shell/src/services/notifications/list.js @@ -0,0 +1,100 @@ +const errors = { + app: { + title: 'test', + }, + + auth: { + userDisabled: 'Your account is disabled', + forbidden: 'Forbidden', + unauthorized: 'Unauthorized', + userNotFound: `Sorry, we don't recognize your credentials`, + wrongPassword: `Sorry, we don't recognize your credentials`, + weakPassword: 'This password is too weak', + emailAlreadyInUse: 'Email is already in use', + invalidEmail: 'Please provide a valid email', + passwordReset: { + invalidToken: 'Password reset link is invalid or has expired', + error: `Email not recognized`, + }, + passwordUpdate: { + samePassword: `You can't use the same password. Please create new password`, + }, + userNotVerified: `Sorry, your email has not been verified yet`, + emailAddressVerificationEmail: { + invalidToken: 'Email verification link is invalid or has expired', + error: `Email not recognized`, + }, + }, + + iam: { + errors: { + userAlreadyExists: 'User with this email already exists', + userNotFound: 'User not found', + disablingHimself: `You can't disable yourself`, + revokingOwnPermission: `You can't revoke your own owner permission`, + deletingHimself: `You can't delete yourself`, + emailRequired: 'Email is required', + }, + }, + + importer: { + errors: { + invalidFileEmpty: 'The file is empty', + invalidFileExcel: 'Only excel (.xlsx) files are allowed', + invalidFileUpload: + 'Invalid file. Make sure you are using the last version of the template.', + importHashRequired: 'Import hash is required', + importHashExistent: 'Data has already been imported', + userEmailMissing: 'Some items in the CSV do not have an email', + }, + }, + + errors: { + forbidden: { + message: 'Forbidden', + }, + validation: { + message: 'An error occurred', + }, + searchQueryRequired: { + message: 'Search query is required', + }, + }, + + emails: { + invitation: { + subject: `You've been invited to {0}`, + body: ` +

Hello,

+

You've been invited to {0} set password for your {1} account.

+

{2}

+

Thanks,

+

Your {0} team

+ `, + }, + emailAddressVerification: { + subject: `Verify your email for {0}`, + body: ` +

Hello,

+

Follow this link to verify your email address.

+

{0}

+

If you didn't ask to verify this address, you can ignore this email.

+

Thanks,

+

Your {1} team

+ `, + }, + passwordReset: { + subject: `Reset your password for {0}`, + body: ` +

Hello,

+

Follow this link to reset your {0} password for your {1} account.

+

{2}

+

If you didn't ask to reset your password, you can ignore this email.

+

Thanks,

+

Your {0} team

+ `, + }, + }, +}; + +module.exports = errors; diff --git a/app-shell/src/services/project-events.js b/app-shell/src/services/project-events.js new file mode 100644 index 0000000..dabc32d --- /dev/null +++ b/app-shell/src/services/project-events.js @@ -0,0 +1,67 @@ +const axios = require('axios'); +const config = require('../config.js'); + +class ProjectEventsService { + /** + * Sends a project event to the Rails backend + * + * @param {string} eventType - Type of the event + * @param {object} payload - Event payload data + * @param {object} options - Additional options + * @param {string} [options.conversationId] - Optional conversation ID + * @param {boolean} [options.isError=false] - Whether this is an error event + * @returns {Promise} - Response from the webhook + */ + static async sendEvent(eventType, payload = {}, options = {}) { + try { + console.log(`[DEBUG] Sending project event: ${eventType}`); + + const webhookUrl = `https://flatlogic.com/projects/events_webhook`; + + // Prepare the event data + const eventData = { + project_uuid: config.project_uuid, + event_type: eventType, + payload: { + ...payload, + message: `[APP] ${payload.message}`, + is_error: options.isError || false, + system_message: true, + is_command_info: true + } + }; + + // Add conversation ID if provided + if (options.conversationId) { + eventData.conversation_id = options.conversationId; + } + + const headers = { + 'Content-Type': 'application/json', + 'x-project-uuid': config.project_uuid + }; + + console.log(`[DEBUG] Event data: ${JSON.stringify(eventData)}`); + + const response = await axios.post(webhookUrl, eventData, { headers }); + + console.log(`[DEBUG] Event sent successfully, status: ${response.status}`); + return response.data; + } catch (error) { + console.error(`[ERROR] Failed to send project event: ${error.message}`); + if (error.response) { + console.error(`[ERROR] Response status: ${error.response.status}`); + console.error(`[ERROR] Response data: ${JSON.stringify(error.response.data)}`); + } + + // Don't throw the error, just return a failed status + // This prevents errors in the event service from breaking app functionality + return { + success: false, + error: error.message + }; + } + } +} + +module.exports = ProjectEventsService; \ No newline at end of file diff --git a/app-shell/src/services/screenshot_service.js b/app-shell/src/services/screenshot_service.js new file mode 100644 index 0000000..9042b90 --- /dev/null +++ b/app-shell/src/services/screenshot_service.js @@ -0,0 +1,83 @@ +const puppeteer = require('puppeteer'); +const path = require('path'); +const fs = require('fs/promises'); +const config = require('../config'); + +const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3000'; +const SCREENSHOT_DIR = process.env.SCREENSHOT_DIR || '/tmp/screenshots'; +fs.mkdir(SCREENSHOT_DIR, { recursive: true }).catch(console.error); + +async function takeScreenshot(url, filename = `screenshot-${Date.now()}.png`, fullPage = true) { + let browser; + + const response = await axios.post( + `${BACKEND_URL}/api/auth/signin/local`, + { email: config.admin_email, password: config.admin_pass }, + { + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + const token = response.data; + + const outputPath = path.join(SCREENSHOT_DIR, filename); + + try { + browser = await puppeteer.launch({ + headless: true, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-accelerated-2d-canvas', + '--disable-gpu', + 'window-size=1920,1080' + ] + }); + const page = await browser.newPage(); + + await page.setViewport({ width: 1920, height: 1080 }); + + if (token) { + await page.setRequestInterception(true); + + page.on('request', interceptedRequest => { + if (interceptedRequest.isInterceptResolutionHandled()) { + return; + } + + const headers = interceptedRequest.headers(); + if (!headers['authorization'] && !headers['Authorization']) { + headers['Authorization'] = `Bearer ${token}`; + } + + interceptedRequest.continue({ headers }); + }); + + page.on('requestfailed', request => { + console.error(`[ScreenshotService]: Request failed: ${request.url()} - ${request.failure().errorText}`); + }); + } + + await page.goto(url, { waitUntil: 'load', timeout: 60000 }); + + await page.screenshot({ + path: outputPath, + fullPage: true, + }); + + console.log(`[ScreenshotService]: Screenshot saved to ${outputPath}`); + return outputPath; + } catch (error) { + console.error(`[ScreenshotService]: Error taking screenshot: ${error.message}`); + throw error; + } finally { + if (browser) { + await browser.close(); + } + } +} + +module.exports = { takeScreenshot }; \ No newline at end of file diff --git a/app-shell/src/services/vcs.js b/app-shell/src/services/vcs.js new file mode 100644 index 0000000..6235c57 --- /dev/null +++ b/app-shell/src/services/vcs.js @@ -0,0 +1,1205 @@ +const util = require('util'); +const exec = util.promisify(require('child_process').exec); +const path = require('path'); +const { promises: fs } = require("fs"); +const axios = require('axios'); +const config = require('../config.js'); + +const ROOT_PATH = '/app'; +const MAX_BUFFER = 1024 * 1024 * 50; +const GITEA_DOMAIN = config.gitea_domain; +const USERNAME = config.gitea_username; +const API_TOKEN = config.gitea_api_token; +const GITHUB_REPO_URL = config.github_repo_url; +const GITHUB_TOKEN = config.github_token; + +const devSchemaFilePath = path.join(ROOT_PATH, '/app-shell/src/_schema.json'); + +class VCS { + static isInitRepoRunning = false; + // Main method – controller of the repository initialization process + static async initRepo(projectId = 'test') { + if (VCS.isInitRepoRunning) { + console.warn('[WARNING] initRepo is already running. Skipping.'); + return; + } + VCS.isInitRepoRunning = true; + try { + console.log(`[DEBUG] Starting repository initialization for project "${projectId}"...`); + + await this._waitForGitLockRelease(path.join(ROOT_PATH, '.git')); + // await this._removeGitLockIfExists(path.join(ROOT_PATH, '.git')); + console.log('[DEBUG] Git lock released, proceeding with initialization...'); + + if (GITHUB_REPO_URL) { + console.log(`[DEBUG] GitHub repository URL provided: ${GITHUB_REPO_URL}`); + console.log(`[DEBUG] Setting up local GitHub repository...`); + await this.setupLocalGitHubRepo(); + console.log(`[DEBUG] GitHub repository setup completed.`); + } else { + console.log(`[DEBUG] No GitHub repository URL provided. Skipping GitHub setup.`); + } + + console.log(`[DEBUG] Setting up Gitea remote repository for project "${projectId}"...`); + const giteaRemoteUrl = await this.setupGiteaRemote(projectId); + console.log(`[DEBUG] Gitea remote URL: ${giteaRemoteUrl.replace(/\/\/.*?@/, '//***@')}`); + + if (!GITHUB_REPO_URL) { + console.log(`[DEBUG] Setting up local repository with Gitea remote...`); + await this.setupLocalRepo(giteaRemoteUrl); + console.log(`[DEBUG] Local repository setup with Gitea remote completed.`); + } else { + console.log(`[DEBUG] Adding Gitea as additional remote to existing GitHub repository...`); + await this._addGiteaRemote(giteaRemoteUrl); + console.log(`[DEBUG] Gitea remote added to GitHub repository.`); + } + + console.log(`[DEBUG] Repository initialization for project "${projectId}" completed successfully.`); + console.log(`[DEBUG] Repository configuration: GitHub: ${GITHUB_REPO_URL ? 'Yes' : 'No'}, Gitea: Yes`); + + return { message: `Repository ${projectId} is ready.` }; + } catch (error) { + console.error(`[ERROR] Repository initialization for project "${projectId}" failed: ${error?.message}`); + + throw new Error(`Error during repo initialization: ${error.message}`); + } finally { + VCS.isInitRepoRunning = false; + console.log(`[DEBUG] Repository initialization process for "${projectId}" finished.`); + } + } + + // Checks for the existence of the remote repo and creates it if it doesn't exist + static async setupGiteaRemote(projectId) { + console.log(`[DEBUG] Checking Gitea remote repository "${projectId}"...`); + let repoData = await this.checkRepoExists(projectId); + if (!repoData) { + console.log(`[DEBUG] Gitea remote repository "${projectId}" does not exist. Creating...`); + repoData = await this.createRemoteRepo(projectId); + console.log(`[DEBUG] Gitea remote repository created: ${JSON.stringify(repoData)}`); + } else { + console.log(`[DEBUG] Gitea remote repository "${projectId}" already exists.`); + } + // Return the URL with token authentication + return `https://${USERNAME}:${API_TOKEN}@${GITEA_DOMAIN}/${USERNAME}/${projectId}.git`; + } + + // Sets up the local repository: either fetches/reset if .git exists, + // initializes git in a non-empty directory, or clones the repository if empty. + static async setupLocalRepo(remoteUrl) { + const gitDir = path.join(ROOT_PATH, '.git'); + const localRepoExists = await this.exists(gitDir); + if (localRepoExists) { + await this.fetchAndResetRepo(); + } else { + const files = await fs.readdir(ROOT_PATH); + if (files.length > 0) { + await this.initializeGitRepo(remoteUrl); + } else { + console.log('[DEBUG] Local directory is empty. Cloning remote repository...'); + await exec(`git clone ${remoteUrl} .`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } + } + } + + static async setupLocalGitHubRepo() { + try { + if (!GITHUB_REPO_URL) { + console.log('[DEBUG] GITHUB_REPO_URL is not set. Skipping GitHub repo setup.'); + return; + } + + const gitDir = path.join(ROOT_PATH, '.git'); + const repoExists = await this.exists(gitDir); + + if (repoExists) { + console.log('[DEBUG] Git repository already initialized. Fetching and resetting...'); + + await this._addGithubRemote(); + + console.log('[DEBUG] Fetching GitHub remote...'); + await exec(`git fetch github`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + try { + console.log('[DEBUG] Checking for remote branch "github/ai-dev"...'); + await exec(`git rev-parse --verify github/ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Remote branch "github/ai-dev" exists. Resetting local repository to github/ai-dev...'); + await exec(`git reset --hard github/ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Switching to branch "ai-dev"...'); + await exec(`git checkout -B ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } catch (e) { + console.log('[DEBUG] Remote branch "github/ai-dev" does NOT exist. Creating initial commit...'); + await this.commitInitialChanges(); + } + return; + } + + console.log('[DEBUG] Initializing git in existing directory...'); + // const gitignorePath = path.join(ROOT_PATH, '.gitignore'); + // const ignoreContent = `node_modules/\n*/node_modules/\n*/build/\n`; + // await fs.writeFile(gitignorePath, ignoreContent, 'utf8'); + + await exec(`git init`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Configuring git user...'); + await exec(`git config user.email "support@flatlogic.com"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + await exec(`git config user.name "Flatlogic Bot"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + await this._addGithubRemote(); + + console.log('[DEBUG] Fetching GitHub remote...'); + await exec(`git fetch github`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + try { + console.log('[DEBUG] Checking for remote branch "github/ai-dev"...'); + await exec(`git rev-parse --verify github/ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Remote branch "github/ai-dev" exists. Resetting local repository to github/ai-dev...'); + await exec(`git reset --hard github/ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Switching to branch "ai-dev"...'); + await exec(`git checkout -B ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } catch (e) { + console.log('[DEBUG] Remote branch "github/ai-dev" does NOT exist. Creating initial commit...'); + await this.commitInitialChanges(); + } + } catch (error) { + console.error(`[ERROR] Failed to setup local GitHub repo: ${error?.message}`); + throw error; + } + } + + // Check if a file/directory exists + static async exists(pathToCheck) { + try { + await fs.access(pathToCheck); + return true; + } catch { + return false; + } + } + + // If the local repository exists, fetches remote data and resets the repository state + static async fetchAndResetRepo() { + console.log('[DEBUG] Local repository exists. Fetching remotes...'); + + if (GITHUB_REPO_URL) { + await exec(`git fetch github`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + const branchReset = await this.tryResetToBranch('ai-dev', 'github'); + + if (branchReset) { + return; + } + } + + await exec(`git fetch gitea`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + const branchReset = await this.tryResetToBranch('ai-dev', 'gitea'); + + if (!branchReset) { + const masterReset = await this.tryResetToBranch('master', 'gitea'); + if (masterReset) { + console.log('[DEBUG] Creating and switching to branch "ai-dev"...'); + await exec(`git branch ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + await exec(`git checkout ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Pushing ai-dev branch to remotes...'); + await exec(`git push -u gitea ai-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (GITHUB_REPO_URL) { + await exec(`git push -u github ai-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } + } else { + console.log('[DEBUG] Neither "gitea/ai-dev" nor "gitea/master" exist. Creating initial commit...'); + await this.commitInitialChanges(); + } + } + } + + // Tries to check out and reset to the specified branch + static async tryResetToBranch(branchName, remote) { + try { + console.log(`[DEBUG] Checking for remote branch "${remote}/${branchName}"...`); + await exec(`git rev-parse --verify ${remote}/${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Remote branch "${remote}/${branchName}" found. Resetting local repository to "${remote}/${branchName}"...`); + await exec(`git reset --hard ${remote}/${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + await exec(`git checkout ${branchName === 'ai-dev' ? 'ai-dev' : branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + return true; + } catch (e) { + console.log(`[DEBUG] Remote branch "${remote}/${branchName}" does NOT exist.`); + + return false; + } + } + + // If remote branch doesn't exist, make the initial commit and set up branches + static async commitInitialChanges() { + console.log('[DEBUG] Adding all files for initial commit...'); + await exec(`git add .`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + const { stdout: status } = await exec(`git status --porcelain`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (status.trim()) { + await exec(`git commit -m "Initial version"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + if (GITHUB_REPO_URL) { + await exec(`git push -u github master --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } + await exec(`git push -u gitea master --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + console.log('[DEBUG] Creating and switching to branch "ai-dev"...'); + await exec(`git branch ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + await exec(`git checkout ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Making ai-dev branch identical to master...'); + + if (GITHUB_REPO_URL) { + await exec(`git reset --hard github/master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } else { + await exec(`git reset --hard gitea/master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } + + console.log('[DEBUG] Pushing ai-dev branch to remotes...'); + if (GITHUB_REPO_URL) { + await exec(`git push -u github ai-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } + await exec(`git push -u gitea ai-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } else { + console.log('[DEBUG] No local changes to commit.'); + } + } + + // If the local directory is not empty but .git doesn't exist, initialize git, + // add .gitignore, configure the user, and add the remote origin. + static async initializeGitRepo(giteaRemoteUrl) { + console.log('[DEBUG] Local directory is not empty. Initializing git...'); + const gitignorePath = path.join(ROOT_PATH, '.gitignore'); + const ignoreContent = `node_modules/\n*/node_modules/\n*/build/\n`; + await fs.writeFile(gitignorePath, ignoreContent, 'utf8'); + + await exec(`git init`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Configuring git user...'); + await exec(`git config user.email "support@flatlogic.com"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + await exec(`git config user.name "Flatlogic Bot"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + console.log(`[DEBUG] Adding Gitea remote ${giteaRemoteUrl}...`); + await exec(`git remote add gitea ${giteaRemoteUrl}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + if (GITHUB_REPO_URL) { + await this._addGithubRemote(); + } + + console.log('[DEBUG] Fetching Gitea remote...'); + await exec(`git fetch gitea`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + try { + console.log('[DEBUG] Checking for remote branch "gitea/ai-dev"...'); + await exec(`git rev-parse --verify gitea/ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Remote branch "gitea/ai-dev" exists. Resetting local repository to gitea/ai-dev...'); + await exec(`git reset --hard gitea/ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Switching to branch "ai-dev"...'); + await exec(`git checkout -B ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } catch (e) { + console.log('[DEBUG] Remote branch "gitea/ai-dev" does NOT exist. Creating initial commit...'); + await this.commitInitialChanges(); + } + } + + // Method to check if the repository exists on remote server + static async checkRepoExists(repoName) { + const url = `https://${GITEA_DOMAIN}/api/v1/repos/${USERNAME}/${repoName}`; + try { + const response = await axios.get(url, { + headers: { Authorization: `token ${API_TOKEN}` } + }); + return response.data; + } catch (err) { + if (err.response && err.response.status === 404) { + return null; + } + throw new Error('Error checking repository existence: ' + err?.message); + } + } + + // Method to create a remote repository via API + static async createRemoteRepo(repoName) { + const createUrl = `https://${GITEA_DOMAIN}/api/v1/user/repos`; + console.log("[DEBUG] createUrl", createUrl); + + try { + const response = await axios.post(createUrl, { + name: repoName, + description: `Repository for project ${repoName}`, + private: false + }, { + headers: { Authorization: `token ${API_TOKEN}` } + }); + + return response.data; + } catch (err) { + console.log('Error creating repository via API: ' + err?.message) + // throw new Error('Error creating repository via API: ' + err.message); + } + } + + static async commitChanges(message = "", files = '.', dev_schema) { + try { + console.log(`[DEBUG] Starting commit process...`); + await this._ensureDevBranch(); + + console.log(`[DEBUG] Ensuring .gitignore is properly configured...`); + await this._ensureGitignore(); + + // Save dev_schema + await this._saveDevSchema(message, dev_schema); + + console.log(`[DEBUG] Adding files to git index: ${files}`); + if (files === '.') { + await exec(`git add .`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } else { + await exec(`git add ${files}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } + + const { stdout: status } = await exec('git status --porcelain', { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Git status before commit: ${status}`); + + if (!status.trim()) { + console.log(`[DEBUG] No changes to commit`); + return { message: "No changes to commit" }; + } + + const now = new Date(); + const commitMessage = message || `Auto commit: ${now.toISOString()}`; + console.log(`[DEBUG] Committing changes with message: "${commitMessage}"`); + + const { stdout: commitOutput, stderr: commitError } = await exec(`git commit -m "${commitMessage}"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Commit output: ${commitOutput}`); + if (commitError) { + console.log(`[DEBUG] Commit stderr: ${commitError}`); + } + + const { stdout: currentBranch } = await exec(`git rev-parse --abbrev-ref HEAD`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + const branchName = currentBranch.trim(); + console.log(`[DEBUG] Current branch: ${branchName}`); + + console.log(`[DEBUG] Pushing changes to Gitea...`); + try { + const { stdout: giteaPushOutput, stderr: giteaPushError } = await exec(`git push gitea ${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Gitea push output: ${giteaPushOutput}`); + if (giteaPushError) { + console.log(`[DEBUG] Gitea push stderr: ${giteaPushError}`); + } + } catch (giteaError) { + console.error(`[ERROR] Failed to push to Gitea: ${giteaError?.message}`); + + if (giteaError.stderr && giteaError.stderr.includes('rejected')) { + console.log(`[DEBUG] Push rejected, trying with --force...`); + try { + const { stdout, stderr } = await exec(`git push gitea ${branchName} --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Force push to Gitea output: ${stdout}`); + if (stderr) { + console.log(`[DEBUG] Force push to Gitea stderr: ${stderr}`); + } + } catch (forceError) { + console.error(`[ERROR] Force push to Gitea failed: ${forceError?.message}`); + } + } + } + + if (GITHUB_REPO_URL) { + console.log(`[DEBUG] Pushing changes to GitHub...`); + try { + const { stdout: githubPushOutput, stderr: githubPushError } = await exec(`git push github ${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] GitHub push output: ${githubPushOutput}`); + if (githubPushError) { + console.log(`[DEBUG] GitHub push stderr: ${githubPushError}`); + } + } catch (githubError) { + console.error(`[ERROR] Failed to push to GitHub: ${githubError?.message}`); + + if (githubError.stderr && githubError.stderr.includes('rejected')) { + console.log(`[DEBUG] Push rejected, trying with --force...`); + try { + const { stdout, stderr } = await exec(`git push github ${branchName} --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Force push to GitHub output: ${stdout}`); + if (stderr) { + console.log(`[DEBUG] Force push to GitHub stderr: ${stderr}`); + } + } catch (forceError) { + console.error(`[ERROR] Force push to GitHub failed: ${forceError?.message}`); + } + } + } + } + + console.log(`[DEBUG] Commit process completed`); + return { message: "Changes committed" }; + } catch (error) { + console.error(`[ERROR] Error during commit process: ${error?.message}`); + } + } + + static async getLog() { + try { + const remote = GITHUB_REPO_URL ? 'github' : 'gitea'; + + const { stdout: remotes } = await exec(`git remote -v`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Remotes: ${remotes}`); + + const { stdout: branches } = await exec(`git branch -a`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Branches: ${branches}`); + + const { stdout } = await exec(`git log ${remote}/ai-dev --oneline`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + const lines = stdout.split(/\r?\n/).filter(line => line.trim() !== ''); + const result = {}; + lines.forEach((line) => { + const firstSpaceIndex = line.indexOf(' '); + if (firstSpaceIndex > 0) { + const hash = line.substring(0, firstSpaceIndex); + const message = line.substring(firstSpaceIndex + 1).trim(); + result[hash] = message; + } + }); + return result; + } catch (error) { + console.error(`[ERROR] Error during get log: ${error?.message}`); + throw error; + } + } + + static async checkout(ref) { + try { + await exec(`git checkout ${ref}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + return { message: `Checked out to ${ref}` }; + } catch (error) { + throw new Error(`Error during checkout: ${error?.message}`); + } + } + + static async revert(commitHash) { + try { + const { stdout: currentBranch } = await exec(`git rev-parse --abbrev-ref HEAD`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + const branchName = currentBranch.trim(); + + await exec(`git reset --hard`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + await exec( + `git revert --no-edit ${commitHash}..HEAD --no-commit`, + { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER } + ); + + await exec( + `git commit -m "Revert to version ${commitHash}"`, + { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER } + ); + + await exec(`git push gitea ${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (GITHUB_REPO_URL) { + await exec(`git push github ${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } + + const commitMessage = await this._getCommitMessageByHash(commitHash); + const devSchema = await this._getDevSchemaByCommitMessage(commitMessage); + + return { message: `Reverted to commit ${commitHash}`, devSchema }; + } catch (error) { + console.error("Error during revert:", error?.message); + if (error.stdout) { + console.error("Revert stdout:", error.stdout); + } + if (error.stderr) { + console.error("Revert stderr:", error.stderr); + } + throw new Error(`Error during revert: ${error?.message}`); + } + } + + static async mergeDevIntoMaster() { + try { + // First, make sure we have the latest changes from both branches + console.log('[DEBUG] Fetching latest changes from remote repositories...'); + await exec(`git fetch gitea`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (GITHUB_REPO_URL) { + await exec(`git fetch github`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } + + // Switch to branch 'master' + console.log('[DEBUG] Switching to branch "master"...'); + await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + // Pull latest changes from master + console.log('[DEBUG] Pulling latest changes from master branch...'); + try { + await exec(`git pull gitea master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Successfully pulled from Gitea master'); + } catch (pullError) { + console.warn(`[WARN] Failed to pull from Gitea master: ${pullError?.message}`); + // Try to continue anyway + } + + // Switch to ai-dev and make sure it's up to date + console.log('[DEBUG] Switching to branch "ai-dev"...'); + await exec(`git checkout ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + // Pull latest changes from ai-dev + console.log('[DEBUG] Pulling latest changes from ai-dev branch...'); + try { + await exec(`git pull gitea ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log('[DEBUG] Successfully pulled from Gitea ai-dev'); + } catch (pullError) { + console.warn(`[WARN] Failed to pull from Gitea ai-dev: ${pullError?.message}`); + // Try to continue anyway + } + + // Switch back to master for the merge + console.log('[DEBUG] Switching back to branch "master"...'); + await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + // Merge branch 'ai-dev' into 'master' with a forced merge. + // Parameter -X theirs is used to resolve conflicts by keeping the changes from the branch being merged in case of conflicts. + console.log('[DEBUG] Merging branch "ai-dev" into "master" (force merge with -X theirs)...'); + try { + const { stdout: mergeOutput, stderr: mergeError } = await exec( + `git merge ai-dev --no-ff -X theirs -m "Forced merge: merge ai-dev into master"`, + { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER } + ); + console.log(`[DEBUG] Merge output: ${mergeOutput}`); + if (mergeError) { + console.log(`[DEBUG] Merge stderr: ${mergeError}`); + } + } catch (mergeError) { + console.error(`[ERROR] Merge failed: ${mergeError?.message}`); + if (mergeError.stdout) { + console.error(`[ERROR] Merge stdout: ${mergeError.stdout}`); + } + if (mergeError.stderr) { + console.error(`[ERROR] Merge stderr: ${mergeError.stderr}`); + } + + // Abort the merge if it failed + console.log('[DEBUG] Aborting failed merge...'); + await exec(`git merge --abort`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + throw new Error(`Failed to merge ai-dev into master: ${mergeError?.message}`); + } + + // Push the merged 'master' branch to both remotes + console.log('[DEBUG] Pushing merged master branch to Gitea remote...'); + try { + const { stdout: giteaPushOutput, stderr: giteaPushError } = await exec(`git push gitea master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Gitea push output: ${giteaPushOutput}`); + if (giteaPushError) { + console.log(`[DEBUG] Gitea push stderr: ${giteaPushError}`); + } + } catch (pushError) { + console.error(`[ERROR] Failed to push to Gitea: ${pushError?.message}`); + + // If push is rejected, try with --force + if (pushError.stderr && pushError.stderr.includes('rejected')) { + console.log('[DEBUG] Push rejected, trying with --force...'); + try { + const { stdout, stderr } = await exec(`git push gitea master --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Force push to Gitea output: ${stdout}`); + if (stderr) { + console.log(`[DEBUG] Force push to Gitea stderr: ${stderr}`); + } + } catch (forceError) { + console.error(`[ERROR] Force push to Gitea also failed: ${forceError?.message}`); + throw forceError; + } + } else { + throw pushError; + } + } + + if (GITHUB_REPO_URL) { + console.log('[DEBUG] Pushing merged master branch to GitHub remote...'); + try { + const { stdout: githubPushOutput, stderr: githubPushError } = await exec(`git push github master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] GitHub push output: ${githubPushOutput}`); + if (githubPushError) { + console.log(`[DEBUG] GitHub push stderr: ${githubPushError}`); + } + } catch (pushError) { + console.error(`[ERROR] Failed to push to GitHub: ${pushError?.message}`); + + // If push is rejected, try with --force + if (pushError.stderr && pushError.stderr.includes('rejected')) { + console.log('[DEBUG] Push rejected, trying with --force...'); + try { + const { stdout, stderr } = await exec(`git push github master --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Force push to GitHub output: ${stdout}`); + if (stderr) { + console.log(`[DEBUG] Force push to GitHub stderr: ${stderr}`); + } + } catch (forceError) { + console.error(`[ERROR] Force push to GitHub also failed: ${forceError?.message}`); + throw forceError; + } + } else { + throw pushError; + } + } + } + + return { message: "Branch ai-dev merged into master and pushed to all remotes" }; + } catch (error) { + console.error(`[ERROR] Error during mergeDevIntoMaster: ${error?.message}`); + throw new Error(`Error during merge of ai-dev into master: ${error?.message}`); + } + } + + static async _mergeDevIntoMasterGitHub() { + try { + // Switch to branch 'master' + console.log('Switching to branch "master" (GitHub)...'); + await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + // Merge branch 'ai-dev' into 'master' with a forced merge. + console.log('Merging branch "ai-dev" into "master" (GitHub, force merge with -X theirs)...'); + await exec( + `git merge ai-dev --no-ff -X theirs -m "Forced merge: merge ai-dev into master"`, + { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER } + ); + + // Push the merged 'master' branch to remote (GitHub) + console.log('Pushing merged master branch to remote (GitHub)...'); + const { stdout, stderr } = await exec(`git push -f github master`, { + cwd: ROOT_PATH, + maxBuffer: MAX_BUFFER + }); + if (stdout) { + console.log("Git push GitHub stdout:", stdout); + } + if (stderr) { + console.error("Git push GitHub stderr:", stderr); + } + return { message: "Branch ai-dev merged into master and pushed to GitHub remote" }; + } catch (error) { + console.error("Error during mergeDevIntoMasterGitHub:", error?.message); + if (error.stdout) { + console.error("Merge GitHub stdout:", error.stdout); + } + if (error.stderr) { + console.error("Merge GitHub stderr:", error.stderr); + } + throw error; + } + } + + static async resetDevBranch() { + try { + console.log(`[DEBUG] Starting reset of ai-dev branch to match master...`); + + // First, fetch all remote branches to ensure we have the latest information + console.log(`[DEBUG] Fetching latest changes from remotes...`); + await exec(`git fetch --all`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + // Check current branch state + const { stdout: initialBranches } = await exec(`git branch -a`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Initial branches: ${initialBranches}`); + + // Check if master branch exists + const { stdout: masterExists } = await exec(`git branch --list master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (!masterExists.trim()) { + console.log(`[DEBUG] Master branch does not exist. Creating it...`); + await exec(`git checkout -b master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } + + // Switch to master branch + console.log(`[DEBUG] Switching to branch "master"...`); + await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + // Pull latest changes from master + console.log(`[DEBUG] Pulling latest changes from master...`); + try { + await exec(`git pull gitea master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } catch (error) { + console.log(`[DEBUG] Error pulling from master: ${error?.message}`); + } + + // Verify we are on master branch + const { stdout: currentBranch } = await exec(`git rev-parse --abbrev-ref HEAD`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Current branch after checkout: ${currentBranch.trim()}`); + + // Get master branch commit hash + const { stdout: masterCommit } = await exec(`git rev-parse master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Master branch commit hash: ${masterCommit.trim()}`); + + // Delete local ai-dev branch if it exists + try { + await exec(`git branch -D ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Local branch ai-dev deleted successfully`); + } catch (error) { + console.log(`[DEBUG] Local branch ai-dev does not exist or could not be deleted: ${error?.message}`); + } + + // Create new ai-dev branch from master using the exact commit hash + await exec(`git branch ai-dev ${masterCommit.trim()}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Created new ai-dev branch from master commit ${masterCommit.trim()}`); + + // Switch to the new ai-dev branch + await exec(`git checkout ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Switched to new ai-dev branch`); + + // Verify we are on ai-dev branch + const { stdout: newCurrentBranch } = await exec(`git rev-parse --abbrev-ref HEAD`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Current branch after creating ai-dev: ${newCurrentBranch.trim()}`); + + // Verify that ai-dev points to the same commit as master + const { stdout: aiDevCommit } = await exec(`git rev-parse ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] ai-dev branch commit hash: ${aiDevCommit.trim()}`); + + if (aiDevCommit.trim() !== masterCommit.trim()) { + console.error(`[ERROR] ai-dev branch does not point to the same commit as master!`); + console.error(`[ERROR] master: ${masterCommit.trim()}, ai-dev: ${aiDevCommit.trim()}`); + throw new Error(`Failed to create ai-dev branch from master`); + } + + console.log(`[DEBUG] Verified: ai-dev branch points to the same commit as master`); + + // Delete remote ai-dev branches if they exist + console.log(`[DEBUG] Deleting remote ai-dev branches if they exist...`); + + // For Gitea + try { + // First check if the remote branch exists + const { stdout: giteaBranches } = await exec(`git ls-remote --heads gitea ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + if (giteaBranches.trim()) { + console.log(`[DEBUG] Remote branch ai-dev exists on Gitea, deleting it...`); + await exec(`git push gitea --delete ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Remote branch ai-dev on Gitea deleted successfully`); + + // Verify deletion + const { stdout: verifyGiteaDeletion } = await exec(`git ls-remote --heads gitea ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (verifyGiteaDeletion.trim()) { + console.log(`[WARN] Remote branch ai-dev on Gitea still exists after deletion attempt`); + } else { + console.log(`[DEBUG] Verified: Remote branch ai-dev on Gitea is deleted`); + } + } else { + console.log(`[DEBUG] Remote branch ai-dev does not exist on Gitea`); + } + } catch (error) { + console.log(`[DEBUG] Error checking/deleting remote branch on Gitea: ${error?.message}`); + } + + // For GitHub + if (GITHUB_REPO_URL) { + try { + // First check if the remote branch exists + const { stdout: githubBranches } = await exec(`git ls-remote --heads github ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + if (githubBranches.trim()) { + console.log(`[DEBUG] Remote branch ai-dev exists on GitHub, deleting it...`); + await exec(`git push github --delete ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Remote branch ai-dev on GitHub deleted successfully`); + + // Verify deletion + const { stdout: verifyGithubDeletion } = await exec(`git ls-remote --heads github ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (verifyGithubDeletion.trim()) { + console.log(`[WARN] Remote branch ai-dev on GitHub still exists after deletion attempt`); + } else { + console.log(`[DEBUG] Verified: Remote branch ai-dev on GitHub is deleted`); + } + } else { + console.log(`[DEBUG] Remote branch ai-dev does not exist on GitHub`); + } + } catch (error) { + console.log(`[DEBUG] Error checking/deleting remote branch on GitHub: ${error?.message}`); + } + } + + // Wait a moment to ensure deletion is processed + console.log(`[DEBUG] Waiting for remote branch deletion to be processed...`); + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Push new ai-dev branch to remote repositories with force + console.log(`[DEBUG] Pushing new ai-dev branch to Gitea (force push)...`); + try { + const { stdout, stderr } = await exec(`git push -u gitea ai-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Gitea force push output: ${stdout}`); + if (stderr) { + console.log(`[DEBUG] Gitea force push stderr: ${stderr}`); + } + + // Verify the push + const { stdout: verifyGiteaPush } = await exec(`git ls-remote --heads gitea ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Gitea ai-dev branch after push: ${verifyGiteaPush}`); + + // Extract the hash from the output + const giteaAiDevHash = verifyGiteaPush.split(/\s+/)[0]; + + if (giteaAiDevHash === masterCommit.trim()) { + console.log(`[DEBUG] Verified: Gitea ai-dev branch matches master branch`); + } else { + console.log(`[WARN] Gitea ai-dev branch does not match master branch!`); + console.log(`[WARN] master: ${masterCommit.trim()}, Gitea ai-dev: ${giteaAiDevHash}`); + } + } catch (error) { + console.error(`[ERROR] Force push to Gitea failed: ${error?.message}`); + throw error; + } + + if (GITHUB_REPO_URL) { + console.log(`[DEBUG] Pushing new ai-dev branch to GitHub (force push)...`); + try { + const { stdout, stderr } = await exec(`git push -u github ai-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] GitHub force push output: ${stdout}`); + if (stderr) { + console.log(`[DEBUG] GitHub force push stderr: ${stderr}`); + } + + // Verify the push + const { stdout: verifyGithubPush } = await exec(`git ls-remote --heads github ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] GitHub ai-dev branch after push: ${verifyGithubPush}`); + + // Extract the hash from the output + const githubAiDevHash = verifyGithubPush.split(/\s+/)[0]; + + if (githubAiDevHash === masterCommit.trim()) { + console.log(`[DEBUG] Verified: GitHub ai-dev branch matches master branch`); + } else { + console.log(`[WARN] GitHub ai-dev branch does not match master branch!`); + console.log(`[WARN] master: ${masterCommit.trim()}, GitHub ai-dev: ${githubAiDevHash}`); + } + } catch (error) { + console.error(`[ERROR] Force push to GitHub failed: ${error?.message}`); + throw error; + } + } + + // Final verification + console.log(`[DEBUG] Performing final verification...`); + + // Get master commit hash again to ensure it hasn't changed + const { stdout: finalMasterCommit } = await exec(`git rev-parse master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Final master branch commit hash: ${finalMasterCommit.trim()}`); + + // Get ai-dev commit hash + const { stdout: finalAiDevCommit } = await exec(`git rev-parse ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Final ai-dev branch commit hash: ${finalAiDevCommit.trim()}`); + + // Get remote branches + const { stdout: finalRemoteBranches } = await exec(`git ls-remote --heads`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Final remote branches: ${finalRemoteBranches}`); + + if (finalAiDevCommit.trim() !== finalMasterCommit.trim()) { + console.error(`[ERROR] Final verification failed: ai-dev and master branches point to different commits!`); + console.error(`[ERROR] master: ${finalMasterCommit.trim()}, ai-dev: ${finalAiDevCommit.trim()}`); + } else { + console.log(`[DEBUG] Final verification passed: ai-dev and master branches point to the same commit`); + } + + console.log(`[DEBUG] Reset of ai-dev branch completed successfully`); + return { message: "Branch ai-dev has been reset to be an exact copy of master" }; + } catch (error) { + console.error(`[ERROR] Error during reset of dev branch: ${error?.message}`); + throw new Error(`Error during reset of dev branch: ${error?.message}`); + } + } + + static async _resetDevBranchGitHub() { + try { + console.log('[DEBUG] Switching to branch "master" (GitHub)...'); + await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + console.log('[DEBUG] Resetting branch "ai-dev" to be identical to "master" (GitHub)...'); + await exec(`git checkout -B ai-dev master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + console.log('[DEBUG] Pushing updated branch "ai-dev" to remote (GitHub, force push)...'); + await exec(`git push -f github ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + + return { message: 'ai-dev branch successfully reset to master (GitHub).' }; + } catch (error) { + console.error("Error during resetting ai-dev branch (GitHub):", error?.message); + if (error.stdout) { + console.error("Reset GitHub stdout:", error.stdout); + } + if (error.stderr) { + console.error("Reset GitHub stderr:", error.stderr); + } + throw new Error(`Error during resetting ai-dev branch (GitHub): ${error?.message}`); + } + } + + static async _pushChangesToGitea() { + try { + const { stdout, stderr } = await exec(`git push gitea ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (stdout) { + console.log("Git push Gitea stdout:", stdout); + } + if (stderr) { + console.error("Git push Gitea stderr:", stderr); + } + return { message: "Changes pushed to Gitea remote repository (ai-dev branch)" }; + } catch (error) { + console.error("Git push Gitea error:", error?.message); + if (error.stdout) { + console.error("Git push Gitea stdout:", error.stdout); + } + if (error.stderr) { + console.error("Git push Gitea stderr:", error.stderr); + } + throw error; + } + } + + static async _pushChangesToGithub() { + try { + const { stdout, stderr } = await exec(`git push github ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (stdout) { + console.log("Git push GitHub stdout:", stdout); + } + if (stderr) { + console.error("Git push GitHub stderr:", stderr); + } + return { message: "Changes pushed to GitHub repository (ai-dev branch)" }; + } catch (error) { + console.error("Git push GitHub error:", error?.message); + if (error.stdout) { + console.error("Git push GitHub stdout:", error.stdout); + } + if (error.stderr) { + console.error("Git push GitHub stderr:", error.stderr); + } + throw error; + } + } + + static async _addGithubRemote() { + if (GITHUB_REPO_URL) { + try { + const { stdout: remotes } = await exec(`git remote -v`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (!remotes.includes('github')) { + console.log(`[DEBUG] Adding GitHub remote: git remote add github ${GITHUB_REPO_URL}`); + await exec(`git remote add github ${GITHUB_REPO_URL}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] GitHub remote added: ${GITHUB_REPO_URL}`); + } else { + console.log(`[DEBUG] GitHub remote already exists.`); + } + } catch (error) { + console.error(`[ERROR] Failed to add GitHub remote: ${error?.message}`); + if (error.stdout) { + console.error(`[ERROR] git remote add stdout: ${error.stdout}`); + } + if (error.stderr) { + console.error(`[ERROR] git remote add stderr: ${error.stderr}`); + } + throw error; + } + } + } + + static async _addGiteaRemote(giteaRemoteUrl) { + try { + const { stdout: remotes } = await exec(`git remote -v`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + if (!remotes.includes('gitea')) { + console.log(`[DEBUG] Adding Gitea remote: git remote add gitea ${giteaRemoteUrl}`); + await exec(`git remote add gitea ${giteaRemoteUrl}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + console.log(`[DEBUG] Gitea remote added: ${giteaRemoteUrl}`); + } else { + console.log(`[DEBUG] Gitea remote already exists.`); + } + } catch (error) { + console.error(`[ERROR] Failed to add Gitea remote: ${error?.message}`); + if (error.stdout) { + console.error(`[ERROR] git remote add stdout: ${error.stdout}`); + } + if (error.stderr) { + console.error(`[ERROR] git remote add stderr: ${error.stderr}`); + } + throw error; + } + } + + static async _revertGitHubChanges(branchName) { + try { + await exec(`git push -f github ${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } catch (error) { + console.error("Error during revertGitHubChanges:", error?.message); + if (error.stdout) { + console.error("revertGitHubChanges stdout:", error.stdout); + } + if (error.stderr) { + console.error("revertGitHubChanges stderr:", error.stderr); + } + throw new Error(`Error during revertGitHubChanges: ${error?.message}`); + } + } + + static async _ensureDevBranch() { + try { + console.log(`[DEBUG] Ensuring we are on 'ai-dev' branch...`); + + const { stdout: branchList } = await exec(`git branch --list ai-dev`, { + cwd: ROOT_PATH, + maxBuffer: MAX_BUFFER, + }); + + if (!branchList || branchList.trim() === '') { + console.log(`[DEBUG] Branch 'ai-dev' not found. Creating branch 'ai-dev'.`); + await exec(`git checkout -b ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } else { + const { stdout: currentBranchStdout } = await exec(`git rev-parse --abbrev-ref HEAD`, { + cwd: ROOT_PATH, + maxBuffer: MAX_BUFFER, + }); + const currentBranch = currentBranchStdout.trim(); + + if (currentBranch !== 'ai-dev') { + console.log(`[DEBUG] Switching from branch '${currentBranch}' to 'ai-dev'.`); + await exec(`git checkout ai-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }); + } else { + console.log(`[DEBUG] Already on branch 'ai-dev'.`); + } + } + + console.log(`[DEBUG] Successfully ensured we are on 'ai-dev' branch.`); + } catch (error) { + console.error(`[ERROR] Error ensuring branch 'ai-dev': ${error?.message}`); + if (error.stdout) { + console.error(`[ERROR] stdout: ${error.stdout}`); + } + if (error.stderr) { + console.error(`[ERROR] stderr: ${error.stderr}`); + } + throw new Error(`Error ensuring branch 'ai-dev': ${error?.message}`); + } + } + + static async _ensureGitignore() { + try { + console.log(`[DEBUG] Checking .gitignore file...`); + const gitignorePath = path.join(ROOT_PATH, '.gitignore'); + + let gitignoreContent = ''; + try { + gitignoreContent = await fs.readFile(gitignorePath, 'utf8'); + console.log(`[DEBUG] Existing .gitignore found.`); + } catch (error) { + console.log(`[DEBUG] .gitignore file not found, creating new one.`); + } + + + const requiredPatterns = [ + 'node_modules/', + '*/node_modules/', + '**/node_modules/', + '*/build/', + '**/build/', + '.DS_Store', + '.env' + ]; + + let needsUpdate = false; + for (const pattern of requiredPatterns) { + if (!gitignoreContent.includes(pattern)) { + gitignoreContent += `\n${pattern}`; + needsUpdate = true; + } + } + + if (needsUpdate) { + console.log(`[DEBUG] Updating .gitignore file with missing patterns.`); + await fs.writeFile(gitignorePath, gitignoreContent.trim(), 'utf8'); + console.log(`[DEBUG] .gitignore file updated successfully.`); + } else { + console.log(`[DEBUG] .gitignore file is up to date.`); + } + + return true; + } catch (error) { + console.error(`[ERROR] Error ensuring .gitignore: ${error?.message}`); + return false; + } + } + + static async _waitForGitLockRelease(gitDir, timeout = 10000, interval = 500) { + const lockFilePath = path.join(gitDir, 'index.lock'); + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + try { + await fs.access(lockFilePath); + console.log('[DEBUG] index.lock file exists. Waiting...'); + await new Promise(resolve => setTimeout(resolve, interval)); + } catch (err) { + console.log('[DEBUG] index.lock file no longer exists. Proceeding...'); + return; + } + } + + throw new Error('Timeout waiting for index.lock to be released'); + } + + static async _removeGitLockIfExists(gitDir) { + const lockFilePath = path.join(gitDir, 'index.lock'); + try { + await fs.access(lockFilePath); + console.log('[DEBUG] index.lock file exists. Removing...'); + await fs.unlink(lockFilePath); + console.log('[DEBUG] index.lock file removed.'); + } catch (err) { + console.log('[DEBUG] index.lock file does not exist. No action needed.'); + } + } + + static async _saveDevSchema(commitMessage, dev_schema) { + try { + let devSchemaData = {}; + try { + const fileContent = await fs.readFile(devSchemaFilePath, 'utf8'); + devSchemaData = JSON.parse(fileContent); + } catch (readError) { + console.log(`[DEBUG] _dev_schema.json not found or empty, creating new.`); + devSchemaData = {}; + } + + const schema = JSON.parse(dev_schema); + + devSchemaData[commitMessage] = JSON.stringify(schema); + + await fs.writeFile(devSchemaFilePath, JSON.stringify(devSchemaData, null, 2), 'utf8'); + console.log(`[DEBUG] _dev_schema.json updated with new schema for commit '${commitMessage}'`); + } catch (error) { + console.error(`[ERROR] Error saving dev schema: ${error?.message}`); + throw new Error(`Error saving dev schema: ${error?.message}`); + } + } + + static async _getDevSchemaByHash(hash) { + try { + const fileContent = await fs.readFile(devSchemaFilePath, 'utf8'); + const devSchemaData = JSON.parse(fileContent); + + if (devSchemaData[hash]) { + return devSchemaData[hash]; + } else { + throw new Error(`Schema not found for commit hash: ${hash}`); + } + } catch (error) { + console.error(`[ERROR] Error reading dev schema: ${error?.message}`); + console.error(`Error reading dev schema: ${error?.message}`); + } + } + + static async _getDevSchemaByCommitMessage(commitMessage) { + try { + const fileContent = await fs.readFile(devSchemaFilePath, 'utf8'); + const devSchemaData = JSON.parse(fileContent); + + if (devSchemaData[commitMessage]) { + return devSchemaData[commitMessage]; + } else { + throw new Error(`Schema not found for commit message: ${commitMessage}`); + } + } catch (error) { + console.error(`[ERROR] Error retrieving dev schema: ${error.message}`); + throw new Error(`Error retrieving dev schema: ${error.message}`); + } + } + + static async _getCommitMessageByHash(commitHash) { + return new Promise((resolve, reject) => { + exec(`git log -1 --format=%B ${commitHash}`, (error, stdout, stderr) => { + if (error) { + reject(`Error getting commit message: ${stderr}`); + } else { + resolve(stdout.trim()); + } + }); + }); + } +} + +module.exports = VCS; \ No newline at end of file diff --git a/app-shell/yarn.lock b/app-shell/yarn.lock new file mode 100644 index 0000000..63ccb71 --- /dev/null +++ b/app-shell/yarn.lock @@ -0,0 +1,3044 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@mapbox/node-pre-gyp@^1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa" + integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + +"@one-ini/wasm@0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@one-ini/wasm/-/wasm-0.1.1.tgz#6013659736c9dbfccc96e8a9c2b3de317df39323" + integrity sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw== + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +abbrev@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-2.0.0.tgz#cf59829b8b4f03f89dda2771cb7f3653828c89bf" + integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ== + +accepts@^1.3.7, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-regex@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1" + integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== + +ansi-regex@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + +ansi-styles@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +anymatch@~3.1.1, anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + dependencies: + call-bind "^1.0.5" + is-array-buffer "^3.0.4" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array.prototype.map@^1.0.1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/array.prototype.map/-/array.prototype.map-1.0.7.tgz#82fa4d6027272d1fca28a63bbda424d0185d78a7" + integrity sha512-XpcFfLoBEAhezrrNw1V+yLXkE7M6uR7xJEsxbG6c/V9v043qurwVJB9r9UTnoSioFDoz1i1VOydpWGmJpfVZbg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-array-method-boxes-properly "^1.0.0" + es-object-atoms "^1.0.0" + is-string "^1.0.7" + +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" + is-shared-array-buffer "^1.0.2" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + +axios@^1.6.7: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64url@3.x.x: + version "3.0.1" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" + integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== + +bcrypt@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.1.1.tgz#0f732c6dcb4e12e5b70a25e326a72965879ba6e2" + integrity sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.11" + node-addon-api "^5.0.0" + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +busboy@^0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" + integrity sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg== + dependencies: + dicer "0.2.5" + readable-stream "1.1.x" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" + integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.4.0" + optionalDependencies: + fsevents "~2.1.2" + +chokidar@^3.5.2: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +cli-color@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-2.0.4.tgz#d658080290968816b322248b7306fad2346fb2c8" + integrity sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA== + dependencies: + d "^1.0.1" + es5-ext "^0.10.64" + es6-iterator "^2.0.3" + memoizee "^0.4.15" + timers-ext "^0.1.7" + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-support@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +config-chain@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" + integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + +console-control-strings@^1.0.0, console-control-strings@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cross-env@7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^7.0.0, cross-spawn@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +csv-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/csv-parser/-/csv-parser-3.0.0.tgz#b88a6256d79e090a97a1b56451f9327b01d710e7" + integrity sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ== + dependencies: + minimist "^1.2.0" + +d@1, d@^1.0.1, d@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.2.tgz#2aefd554b81981e7dccf72d6842ae725cb17e5de" + integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== + dependencies: + es5-ext "^0.10.64" + type "^2.7.2" + +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + +debug@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.1.2, define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-libc@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + +dicer@0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" + integrity sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg== + dependencies: + readable-stream "1.1.x" + streamsearch "0.1.2" + +diff@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +editorconfig@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-1.0.4.tgz#040c9a8e9a6c5288388b87c2db07028aa89f53a3" + integrity sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q== + dependencies: + "@one-ini/wasm" "0.1.1" + commander "^10.0.0" + minimatch "9.0.1" + semver "^7.5.3" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +es-abstract@^1.17.0-next.1, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.2: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + dependencies: + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" + is-string "^1.0.7" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" + +es-array-method-boxes-properly@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" + integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-get-iterator@^1.0.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.7" + isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" + +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14, es5-ext@~0.10.2: + version "0.10.64" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714" + integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + esniff "^2.0.1" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c" + integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg== + dependencies: + d "^1.0.2" + ext "^1.7.0" + +es6-weak-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" + integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== + dependencies: + d "1" + es5-ext "^0.10.46" + es6-iterator "^2.0.3" + es6-symbol "^3.1.1" + +escalade@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== + dependencies: + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA== + dependencies: + d "1" + es5-ext "~0.10.14" + +express@4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +ext@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +flat@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.1.tgz#a392059cc382881ff98642f5da4dde0a959f309b" + integrity sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA== + dependencies: + is-buffer "~2.0.3" + +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +form-data@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48" + integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formidable@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" + integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2, fresh@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.1, function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + +get-caller-file@^2.0.1, get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + dependencies: + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + +glob-parent@~5.1.0, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^10.3.3: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globalthis@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1, has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.0, has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== + +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +helmet@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-4.1.1.tgz#751f0e273d809ace9c172073e0003bed27d27a4a" + integrity sha512-Avg4XxSBrehD94mkRwEljnO+6RZx7AGfk8Wa6K1nxaU+hbXlFOhlOIMgPfFqOYQB/dBCsTpootTGuiOG+CHiQA== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@^1.3.4: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +internal-slot@^1.0.4, internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arguments@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-buffer@~2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-core-module@^2.13.0: + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + dependencies: + is-typed-array "^1.1.13" + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-map@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== + +is-promise@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" + integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-set@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + dependencies: + call-bind "^1.0.7" + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +iterate-iterator@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.2.tgz#551b804c9eaa15b847ea6a7cdc2f5bf1ec150f91" + integrity sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw== + +iterate-value@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/iterate-value/-/iterate-value-1.0.2.tgz#935115bd37d006a52046535ebc8d07e9c9337f57" + integrity sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ== + dependencies: + es-get-iterator "^1.0.2" + iterate-iterator "^1.0.1" + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +js-beautify@^1.14.5: + version "1.15.1" + resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.15.1.tgz#4695afb508c324e1084ee0b952a102023fc65b64" + integrity sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA== + dependencies: + config-chain "^1.1.13" + editorconfig "^1.0.4" + glob "^10.3.3" + js-cookie "^3.0.5" + nopt "^7.2.0" + +js-cookie@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" + integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== + +js-yaml@3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +json2csv@^5.0.7: + version "5.0.7" + resolved "https://registry.yarnpkg.com/json2csv/-/json2csv-5.0.7.tgz#f3a583c25abd9804be873e495d1e65ad8d1b54ae" + integrity sha512-YRZbUnyaJZLZUJSRi2G/MqahCyRv9n/ds+4oIetjDF3jWQA7AG7iSeKTiZiCNqtMZM7HDyt0e/W6lEnoGEmMGA== + dependencies: + commander "^6.1.0" + jsonparse "^1.3.1" + lodash.get "^4.4.2" + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonparse@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== + +jsonwebtoken@8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + +jsonwebtoken@^9.0.0: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +lodash@4.17.21, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== + dependencies: + chalk "^4.0.0" + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lru-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" + integrity sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ== + dependencies: + es5-ext "~0.10.2" + +make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memoizee@^0.4.15: + version "0.4.17" + resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.17.tgz#942a5f8acee281fa6fb9c620bddc57e3b7382949" + integrity sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA== + dependencies: + d "^1.0.2" + es5-ext "^0.10.64" + es6-weak-map "^2.0.3" + event-emitter "^0.3.5" + is-promise "^2.2.2" + lru-queue "^0.1.0" + next-tick "^1.1.0" + timers-ext "^0.1.7" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-descriptors@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +methods@^1.1.2, methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0, mime@^1.3.4: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +minimatch@3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimatch@9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.1.tgz#8a555f541cf976c622daf078bb28f29fb927c253" + integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass@^3.0.0: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@^0.5.4: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mocha@8.1.3: + version "8.1.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.1.3.tgz#5e93f873e35dfdd69617ea75f9c68c2ca61c2ac5" + integrity sha512-ZbaYib4hT4PpF4bdSO2DohooKXIn4lDeiYqB+vTmCdr6l2woW0b6H3pf5x4sM5nwQMru9RvjjHYWVGltR50ZBw== + dependencies: + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.4.2" + debug "4.1.1" + diff "4.0.2" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.1.6" + growl "1.10.5" + he "1.2.0" + js-yaml "3.14.0" + log-symbols "4.0.0" + minimatch "3.0.4" + ms "2.1.2" + object.assign "4.1.0" + promise.allsettled "1.0.2" + serialize-javascript "4.0.0" + strip-json-comments "3.0.1" + supports-color "7.1.0" + which "2.0.2" + wide-align "1.1.3" + workerpool "6.0.0" + yargs "13.3.2" + yargs-parser "13.1.2" + yargs-unparser "1.6.1" + +moment@2.30.1: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multer@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4.tgz#e2bc6cac0df57a8832b858d7418ccaa8ebaf7d8c" + integrity sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw== + dependencies: + append-field "^1.0.0" + busboy "^0.2.11" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + on-finished "^2.3.0" + type-is "^1.6.4" + xtend "^4.0.0" + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + +node-addon-api@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" + integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== + +node-fetch@^2.6.7: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-mocks-http@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/node-mocks-http/-/node-mocks-http-1.9.0.tgz#6000c570fc4b809603782309be81c73a71d85b71" + integrity sha512-ILf7Ws8xyX9Rl2fLZ7xhZBovrRwgaP84M13esndP6V17M/8j25TpwNzb7Im8U9XCo6fRhdwqiQajWXpsas/E6w== + dependencies: + accepts "^1.3.7" + depd "^1.1.0" + fresh "^0.5.2" + merge-descriptors "^1.0.1" + methods "^1.1.2" + mime "^1.3.4" + parseurl "^1.3.3" + range-parser "^1.2.0" + type-is "^1.6.18" + +nodemon@^3.1.7: + version "3.1.7" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.7.tgz#07cb1f455f8bece6a499e0d72b5e029485521a54" + integrity sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ== + dependencies: + chokidar "^3.5.2" + debug "^4" + ignore-by-default "^1.0.1" + minimatch "^3.1.2" + pstree.remy "^1.1.8" + semver "^7.5.3" + simple-update-notifier "^2.0.0" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + +nopt@^7.2.0: + version "7.2.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-7.2.1.tgz#1cac0eab9b8e97c9093338446eddd40b2c8ca1e7" + integrity sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w== + dependencies: + abbrev "^2.0.0" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" + +oauth@0.10.x: + version "0.10.0" + resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.10.0.tgz#3551c4c9b95c53ea437e1e21e46b649482339c58" + integrity sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q== + +oauth@0.9.x: + version "0.9.15" + resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" + integrity sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA== + +object-assign@^4, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +object-keys@^1.0.11, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +on-finished@2.4.1, on-finished@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +p-limit@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + +parseurl@^1.3.3, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +passport-google-oauth2@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/passport-google-oauth2/-/passport-google-oauth2-0.2.0.tgz#fc9ea59e7091f02e24fd16d6be9257ea982ebbc3" + integrity sha512-62EdPtbfVdc55nIXi0p1WOa/fFMM8v/M8uQGnbcXA4OexZWCnfsEi3wo2buag+Is5oqpuHzOtI64JpHk0Xi5RQ== + dependencies: + passport-oauth2 "^1.1.2" + +passport-jwt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.1.tgz#c443795eff322c38d173faa0a3c481479646ec3d" + integrity sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ== + dependencies: + jsonwebtoken "^9.0.0" + passport-strategy "^1.0.0" + +passport-microsoft@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/passport-microsoft/-/passport-microsoft-0.1.0.tgz#dc72c1a38b294d74f4dc55fe93f52e25cb9aa5b4" + integrity sha512-0giBDgE1fnR5X84zJZkQ11hnKVrzEgViwRO6RGsormK9zTxFQmN/UHMTDbIpvhk989VqALewB6Pk1R5vNr3GHw== + dependencies: + passport-oauth2 "1.2.0" + pkginfo "0.2.x" + +passport-oauth2@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.2.0.tgz#49613a3eca85c7a1e65bf1019e2b6b80a10c8ac2" + integrity sha512-6128N+n/MOrJdXxdC2q/PVKXtqgihGFIeup+9bsPybAvMPOUKqdGhh9ZIzZF8rFKJOlxUP9fgP3H0JQe18n0rg== + dependencies: + oauth "0.9.x" + passport-strategy "1.x.x" + uid2 "0.0.x" + +passport-oauth2@^1.1.2: + version "1.8.0" + resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.8.0.tgz#55725771d160f09bbb191828d5e3d559eee079c8" + integrity sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA== + dependencies: + base64url "3.x.x" + oauth "0.10.x" + passport-strategy "1.x.x" + uid2 "0.0.x" + utils-merge "1.x.x" + +passport-strategy@1.x.x, passport-strategy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== + +passport@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.7.0.tgz#3688415a59a48cf8068417a8a8092d4492ca3a05" + integrity sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + utils-merge "^1.0.1" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkginfo@0.2.x: + version "0.2.3" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.2.3.tgz#7239c42a5ef6c30b8f328439d9b9ff71042490f8" + integrity sha512-7W7wTrE/NsY8xv/DTGjwNIyNah81EQH0MWcTzrHL6pOpMocOGZc0Mbdz9aXxSrp+U0mSmkU8jrNCDCfUs3sOBg== + +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +promise.allsettled@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.2.tgz#d66f78fbb600e83e863d893e98b3d4376a9c47c9" + integrity sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg== + dependencies: + array.prototype.map "^1.0.1" + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + iterate-value "^1.0.0" + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.0, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readable-stream@1.1.x: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.2.2: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== + dependencies: + picomatch "^2.2.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +regexp.prototype.flags@^1.5.2: + version "1.5.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz#b3ae40b1d2499b8350ab2c3fe6ef3845d3a96f42" + integrity sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +resolve@^1.22.1: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + has-symbols "^1.0.3" + isarray "^2.0.5" + +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^5.6.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.0.0: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.5, semver@^7.5.3, semver@^7.5.4: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +sequelize-cli@6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/sequelize-cli/-/sequelize-cli-6.6.2.tgz#8d838b25c988cf136914cdc3843e19d88c3dcb67" + integrity sha512-V8Oh+XMz2+uquLZltZES6MVAD+yEnmMfwfn+gpXcDiwE3jyQygLt4xoI0zG8gKt6cRcs84hsKnXAKDQjG/JAgg== + dependencies: + cli-color "^2.0.3" + fs-extra "^9.1.0" + js-beautify "^1.14.5" + lodash "^4.17.21" + resolve "^1.22.1" + umzug "^2.3.0" + yargs "^16.2.0" + +sequelize-json-schema@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/sequelize-json-schema/-/sequelize-json-schema-2.1.1.tgz#a82d3813925e81485d76ce291f4ff5c8cb2ae492" + integrity sha512-yCGaHnmQQeL6MQ/fOxhkR5C2aOGZyTD6OrgjP4yw1rbuujuIUVdzWN3AsC6r6AvlGZ3EUBBbCJHKl8OIFFES4Q== + +serialize-javascript@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.0: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +simple-update-notifier@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" + integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== + dependencies: + semver "^7.5.3" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +stop-iteration-iterator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" + integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== + dependencies: + internal-slot "^1.0.4" + +streamsearch@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + integrity sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA== + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" + +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow== + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-json-comments@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" + integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== + +supports-color@7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + +supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tar@^6.1.11: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +timers-ext@^0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.8.tgz#b4e442f10b7624a29dd2aa42c295e257150cf16c" + integrity sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww== + dependencies: + es5-ext "^0.10.64" + next-tick "^1.1.0" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +touch@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694" + integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +type-is@^1.6.18, type-is@^1.6.4, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +type@^2.7.2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.3.tgz#436981652129285cc3ba94f392886c2637ea0486" + integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ== + +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" + +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +uid2@0.0.x: + version "0.0.4" + resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.4.tgz#033f3b1d5d32505f5ce5f888b9f3b667123c0a44" + integrity sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA== + +umzug@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/umzug/-/umzug-2.3.0.tgz#0ef42b62df54e216b05dcaf627830a6a8b84a184" + integrity sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw== + dependencies: + bluebird "^3.7.2" + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1, utils-merge@1.x.x, utils-merge@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-module@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" + integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== + +which-typed-array@^1.1.14, which-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + +which@2.0.2, which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wide-align@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +wide-align@^1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + +workerpool@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.0.0.tgz#85aad67fa1a2c8ef9386a1b43539900f61d03d58" + integrity sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA== + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@13.1.2, yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^15.0.1: + version "15.0.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.3.tgz#316e263d5febe8b38eef61ac092b33dfcc9b1115" + integrity sha512-/MVEVjTXy/cGAjdtQf8dW3V9b97bPN7rNn8ETj6BmAQL7ibC7O1Q9SPJbGjgh3SlwoBNXMzj/ZGIj8mBgl12YA== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.1.tgz#bd4b0ee05b4c94d058929c32cb09e3fce71d3c5f" + integrity sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA== + dependencies: + camelcase "^5.3.1" + decamelize "^1.2.0" + flat "^4.1.0" + is-plain-obj "^1.1.0" + yargs "^14.2.3" + +yargs@13.3.2: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2" + +yargs@^14.2.3: + version "14.2.3" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414" + integrity sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg== + dependencies: + cliui "^5.0.0" + decamelize "^1.2.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^15.0.1" + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/backend/.prettierrc b/backend/.prettierrc new file mode 100644 index 0000000..bb087f2 --- /dev/null +++ b/backend/.prettierrc @@ -0,0 +1,11 @@ +{ + "singleQuote": true, + "tabWidth": 2, + "printWidth": 80, + "trailingComma": "all", + "quoteProps": "as-needed", + "jsxSingleQuote": true, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always" +} diff --git a/backend/.sequelizerc b/backend/.sequelizerc new file mode 100644 index 0000000..fe89188 --- /dev/null +++ b/backend/.sequelizerc @@ -0,0 +1,7 @@ +const path = require('path'); +module.exports = { + "config": path.resolve("src", "db", "db.config.js"), + "models-path": path.resolve("src", "db", "models"), + "seeders-path": path.resolve("src", "db", "seeders"), + "migrations-path": path.resolve("src", "db", "migrations") +}; \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..581cb98 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,23 @@ +FROM node:20.15.1-alpine + +RUN apk update && apk add bash +# Create app directory +WORKDIR /usr/src/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ + +RUN yarn install +# If you are building your code for production +# RUN npm ci --only=production + + +# Bundle app source +COPY . . + + +EXPOSE 8080 + +CMD [ "yarn", "start" ] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..6dbcd4e --- /dev/null +++ b/backend/README.md @@ -0,0 +1,67 @@ +#Title Enterprise Dashboards Finance HR Ops - template backend, + +#### Run App on local machine: + +##### Install local dependencies: + +- `yarn install` + +--- + +##### Adjust local db: + +###### 1. Install postgres: + +- MacOS: + + - `brew install postgres` + +- Ubuntu: + - `sudo apt update` + - `sudo apt install postgresql postgresql-contrib` + +###### 2. Create db and admin user: + +- Before run and test connection, make sure you have created a database as described in the above configuration. You can use the `psql` command to create a user and database. + + - `psql postgres --u postgres` + +- Next, type this command for creating a new user with password then give access for creating the database. + + - `postgres-# CREATE ROLE admin WITH LOGIN PASSWORD 'admin_pass';` + - `postgres-# ALTER ROLE admin CREATEDB;` + +- Quit `psql` then log in again using the new user that previously created. + + - `postgres-# \q` + - `psql postgres -U admin` + +- Type this command to creating a new database. + + - `postgres=> CREATE DATABASE db_title_enterprise_dashboards_finance_hr_ops;` + +- Then give that new user privileges to the new database then quit the `psql`. + - `postgres=> GRANT ALL PRIVILEGES ON DATABASE db_title_enterprise_dashboards_finance_hr_ops TO admin;` + - `postgres=> \q` + +--- + +#### Api Documentation (Swagger) + +http://localhost:8080/api-docs (local host) + +http://host_name/api-docs + +--- + +##### Setup database tables or update after schema change + +- `yarn db:migrate` + +##### Seed the initial data (admin accounts, relevant for the first setup): + +- `yarn db:seed` + +##### Start build: + +- `yarn start` diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..633db02 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,53 @@ +{ + "name": "titleenterprisedashboardsfinancehrops", + "description": "Title Enterprise Dashboards Finance HR Ops - template backend", + "scripts": { + "start": "npm run db:migrate && npm run db:seed && npm run watch", + "db:migrate": "sequelize-cli db:migrate", + "db:seed": "sequelize-cli db:seed:all", + "db:drop": "sequelize-cli db:drop", + "db:create": "sequelize-cli db:create", + "watch": "node watcher.js" + }, + "dependencies": { + "@google-cloud/storage": "^5.18.2", + "axios": "^1.6.7", + "bcrypt": "5.1.1", + "chokidar": "^4.0.3", + "cors": "2.8.5", + "csv-parser": "^3.0.0", + "express": "4.18.2", + "formidable": "1.2.2", + "helmet": "4.1.1", + "json2csv": "^5.0.7", + "jsonwebtoken": "8.5.1", + "lodash": "4.17.21", + "moment": "2.30.1", + "multer": "^1.4.4", + "mysql2": "2.2.5", + "nodemailer": "6.9.9", + "passport": "^0.7.0", + "passport-google-oauth2": "^0.2.0", + "passport-jwt": "^4.0.1", + "passport-microsoft": "^0.1.0", + "pg": "8.4.1", + "pg-hstore": "2.3.4", + "sequelize": "6.35.2", + "sequelize-json-schema": "^2.1.1", + "sqlite": "4.0.15", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.0", + "tedious": "^18.2.4" + }, + "engines": { + "node": ">=18" + }, + "private": true, + "devDependencies": { + "cross-env": "7.0.3", + "mocha": "8.1.3", + "node-mocks-http": "1.9.0", + "nodemon": "2.0.5", + "sequelize-cli": "6.6.2" + } +} diff --git a/backend/src/auth/auth.js b/backend/src/auth/auth.js new file mode 100644 index 0000000..0634d88 --- /dev/null +++ b/backend/src/auth/auth.js @@ -0,0 +1,79 @@ +const config = require('../config'); +const providers = config.providers; +const helpers = require('../helpers'); +const db = require('../db/models'); + +const passport = require('passport'); +const JWTstrategy = require('passport-jwt').Strategy; +const ExtractJWT = require('passport-jwt').ExtractJwt; +const GoogleStrategy = require('passport-google-oauth2').Strategy; +const MicrosoftStrategy = require('passport-microsoft').Strategy; +const UsersDBApi = require('../db/api/users'); + +passport.use( + new JWTstrategy( + { + passReqToCallback: true, + secretOrKey: config.secret_key, + jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(), + }, + async (req, token, done) => { + try { + const user = await UsersDBApi.findBy({ email: token.user.email }); + + if (user && user.disabled) { + return done(new Error(`User '${user.email}' is disabled`)); + } + + req.currentUser = user; + + return done(null, user); + } catch (error) { + done(error); + } + }, + ), +); + +passport.use( + new GoogleStrategy( + { + clientID: config.google.clientId, + clientSecret: config.google.clientSecret, + callbackURL: config.apiUrl + '/auth/signin/google/callback', + passReqToCallback: true, + }, + function (request, accessToken, refreshToken, profile, done) { + socialStrategy(profile.email, profile, providers.GOOGLE, done); + }, + ), +); + +passport.use( + new MicrosoftStrategy( + { + clientID: config.microsoft.clientId, + clientSecret: config.microsoft.clientSecret, + callbackURL: config.apiUrl + '/auth/signin/microsoft/callback', + passReqToCallback: true, + }, + function (request, accessToken, refreshToken, profile, done) { + const email = profile._json.mail || profile._json.userPrincipalName; + socialStrategy(email, profile, providers.MICROSOFT, done); + }, + ), +); + +function socialStrategy(email, profile, provider, done) { + db.users + .findOrCreate({ where: { email, provider } }) + .then(([user, created]) => { + const body = { + id: user.id, + email: user.email, + name: profile.displayName, + }; + const token = helpers.jwtSign({ user: body }); + return done(null, { token }); + }); +} diff --git a/backend/src/config.js b/backend/src/config.js new file mode 100644 index 0000000..ac60aea --- /dev/null +++ b/backend/src/config.js @@ -0,0 +1,77 @@ +const os = require('os'); + +const config = { + gcloud: { + bucket: 'fldemo-files', + hash: 'b15fde4d61c26a545359249c07ed2252', + }, + bcrypt: { + saltRounds: 12, + }, + admin_pass: 'f606f4c2', + user_pass: '146ab785f002', + admin_email: 'admin@flatlogic.com', + providers: { + LOCAL: 'local', + GOOGLE: 'google', + MICROSOFT: 'microsoft', + }, + secret_key: process.env.SECRET_KEY || '', + remote: '', + port: process.env.NODE_ENV === 'production' ? '' : '8080', + hostUI: process.env.NODE_ENV === 'production' ? '' : 'http://localhost', + portUI: process.env.NODE_ENV === 'production' ? '' : '3000', + + portUIProd: process.env.NODE_ENV === 'production' ? '' : ':3000', + + swaggerUI: process.env.NODE_ENV === 'production' ? '' : 'http://localhost', + swaggerPort: process.env.NODE_ENV === 'production' ? '' : ':8080', + google: { + clientId: process.env.GOOGLE_CLIENT_ID || '', + clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', + }, + microsoft: { + clientId: process.env.MS_CLIENT_ID || '', + clientSecret: process.env.MS_CLIENT_SECRET || '', + }, + uploadDir: os.tmpdir(), + email: { + from: 'Title Enterprise Dashboards Finance HR Ops ', + host: 'email-smtp.us-east-1.amazonaws.com', + port: 587, + auth: { + user: process.env.EMAIL_USER || '', + pass: process.env.EMAIL_PASS, + }, + tls: { + rejectUnauthorized: false, + }, + }, + roles: { + super_admin: 'Super Administrator', + + admin: 'Administrator', + + user: 'Data Viewer', + }, + + project_uuid: 'f606f4c2-efd0-4474-98a0-146ab785f002', + flHost: + process.env.NODE_ENV === 'production' || + process.env.NODE_ENV === 'dev_stage' + ? 'https://flatlogic.com/projects' + : 'http://localhost:3000/projects', + + gpt_key: process.env.GPT_KEY || '', +}; + +config.pexelsKey = process.env.PEXELS_KEY || ''; +config.pexelsQuery = 'Abstract data visualization concept'; +config.host = + process.env.NODE_ENV === 'production' ? config.remote : 'http://localhost'; +config.apiUrl = `${config.host}${config.port ? `:${config.port}` : ``}/api`; +config.swaggerUrl = `${config.swaggerUI}${config.swaggerPort}`; +config.uiUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}/#`; +config.backUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}`; + +module.exports = config; diff --git a/backend/src/db/api/audit_logs.js b/backend/src/db/api/audit_logs.js new file mode 100644 index 0000000..f8b2db8 --- /dev/null +++ b/backend/src/db/api/audit_logs.js @@ -0,0 +1,428 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Audit_logsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const audit_logs = await db.audit_logs.create( + { + id: data.id || undefined, + + action: data.action || null, + entity: data.entity || null, + entity_id: data.entity_id || null, + diff_json: data.diff_json || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await audit_logs.setOrg(data.org || null, { + transaction, + }); + + await audit_logs.setUser(data.user || null, { + transaction, + }); + + await audit_logs.setOrganizations(data.organizations || null, { + transaction, + }); + + return audit_logs; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const audit_logsData = data.map((item, index) => ({ + id: item.id || undefined, + + action: item.action || null, + entity: item.entity || null, + entity_id: item.entity_id || null, + diff_json: item.diff_json || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const audit_logs = await db.audit_logs.bulkCreate(audit_logsData, { + transaction, + }); + + // For each item created, replace relation files + + return audit_logs; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const audit_logs = await db.audit_logs.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.action !== undefined) updatePayload.action = data.action; + + if (data.entity !== undefined) updatePayload.entity = data.entity; + + if (data.entity_id !== undefined) updatePayload.entity_id = data.entity_id; + + if (data.diff_json !== undefined) updatePayload.diff_json = data.diff_json; + + updatePayload.updatedById = currentUser.id; + + await audit_logs.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await audit_logs.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.user !== undefined) { + await audit_logs.setUser( + data.user, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await audit_logs.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return audit_logs; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const audit_logs = await db.audit_logs.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of audit_logs) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of audit_logs) { + await record.destroy({ transaction }); + } + }); + + return audit_logs; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const audit_logs = await db.audit_logs.findByPk(id, options); + + await audit_logs.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await audit_logs.destroy({ + transaction, + }); + + return audit_logs; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const audit_logs = await db.audit_logs.findOne({ where }, { transaction }); + + if (!audit_logs) { + return audit_logs; + } + + const output = audit_logs.get({ plain: true }); + + output.org = await audit_logs.getOrg({ + transaction, + }); + + output.user = await audit_logs.getUser({ + transaction, + }); + + output.organizations = await audit_logs.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.users, + as: 'user', + + where: filter.user + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.user + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + firstName: { + [Op.or]: filter.user + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.action) { + where = { + ...where, + [Op.and]: Utils.ilike('audit_logs', 'action', filter.action), + }; + } + + if (filter.entity) { + where = { + ...where, + [Op.and]: Utils.ilike('audit_logs', 'entity', filter.entity), + }; + } + + if (filter.diff_json) { + where = { + ...where, + [Op.and]: Utils.ilike('audit_logs', 'diff_json', filter.diff_json), + }; + } + + if (filter.entity_idRange) { + const [start, end] = filter.entity_idRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + entity_id: { + ...where.entity_id, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + entity_id: { + ...where.entity_id, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.audit_logs.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('audit_logs', 'action', query), + ], + }; + } + + const records = await db.audit_logs.findAll({ + attributes: ['id', 'action'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['action', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.action, + })); + } +}; diff --git a/backend/src/db/api/data_connections.js b/backend/src/db/api/data_connections.js new file mode 100644 index 0000000..af4079b --- /dev/null +++ b/backend/src/db/api/data_connections.js @@ -0,0 +1,413 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Data_connectionsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const data_connections = await db.data_connections.create( + { + id: data.id || undefined, + + type: data.type || null, + name: data.name || null, + config_json: data.config_json || null, + status: data.status || null, + last_sync_at: data.last_sync_at || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await data_connections.setOrg(data.org || null, { + transaction, + }); + + await data_connections.setOrganizations(data.organizations || null, { + transaction, + }); + + return data_connections; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const data_connectionsData = data.map((item, index) => ({ + id: item.id || undefined, + + type: item.type || null, + name: item.name || null, + config_json: item.config_json || null, + status: item.status || null, + last_sync_at: item.last_sync_at || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const data_connections = await db.data_connections.bulkCreate( + data_connectionsData, + { transaction }, + ); + + // For each item created, replace relation files + + return data_connections; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const data_connections = await db.data_connections.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.type !== undefined) updatePayload.type = data.type; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.config_json !== undefined) + updatePayload.config_json = data.config_json; + + if (data.status !== undefined) updatePayload.status = data.status; + + if (data.last_sync_at !== undefined) + updatePayload.last_sync_at = data.last_sync_at; + + updatePayload.updatedById = currentUser.id; + + await data_connections.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await data_connections.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await data_connections.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return data_connections; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const data_connections = await db.data_connections.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of data_connections) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of data_connections) { + await record.destroy({ transaction }); + } + }); + + return data_connections; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const data_connections = await db.data_connections.findByPk(id, options); + + await data_connections.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await data_connections.destroy({ + transaction, + }); + + return data_connections; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const data_connections = await db.data_connections.findOne( + { where }, + { transaction }, + ); + + if (!data_connections) { + return data_connections; + } + + const output = data_connections.get({ plain: true }); + + output.org = await data_connections.getOrg({ + transaction, + }); + + output.organizations = await data_connections.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('data_connections', 'name', filter.name), + }; + } + + if (filter.config_json) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'data_connections', + 'config_json', + filter.config_json, + ), + }; + } + + if (filter.last_sync_atRange) { + const [start, end] = filter.last_sync_atRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + last_sync_at: { + ...where.last_sync_at, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + last_sync_at: { + ...where.last_sync_at, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.type) { + where = { + ...where, + type: filter.type, + }; + } + + if (filter.status) { + where = { + ...where, + status: filter.status, + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.data_connections.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('data_connections', 'name', query), + ], + }; + } + + const records = await db.data_connections.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/api/file.js b/backend/src/db/api/file.js new file mode 100644 index 0000000..22f9b6f --- /dev/null +++ b/backend/src/db/api/file.js @@ -0,0 +1,73 @@ +const db = require('../models'); +const assert = require('assert'); +const services = require('../../services/file'); + +module.exports = class FileDBApi { + static async replaceRelationFiles(relation, rawFiles, options) { + assert(relation.belongsTo, 'belongsTo is required'); + assert(relation.belongsToColumn, 'belongsToColumn is required'); + assert(relation.belongsToId, 'belongsToId is required'); + + let files = []; + + if (Array.isArray(rawFiles)) { + files = rawFiles; + } else { + files = rawFiles ? [rawFiles] : []; + } + + await this._removeLegacyFiles(relation, files, options); + await this._addFiles(relation, files, options); + } + + static async _addFiles(relation, files, options) { + const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || { id: null }; + + const inexistentFiles = files.filter((file) => !!file.new); + + for (const file of inexistentFiles) { + await db.file.create( + { + belongsTo: relation.belongsTo, + belongsToColumn: relation.belongsToColumn, + belongsToId: relation.belongsToId, + name: file.name, + sizeInBytes: file.sizeInBytes, + privateUrl: file.privateUrl, + publicUrl: file.publicUrl, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { + transaction, + }, + ); + } + } + + static async _removeLegacyFiles(relation, files, options) { + const transaction = (options && options.transaction) || undefined; + + const filesToDelete = await db.file.findAll({ + where: { + belongsTo: relation.belongsTo, + belongsToId: relation.belongsToId, + belongsToColumn: relation.belongsToColumn, + id: { + [db.Sequelize.Op.notIn]: files + .filter((file) => !file.new) + .map((file) => file.id), + }, + }, + transaction, + }); + + for (let file of filesToDelete) { + await services.deleteGCloud(file.privateUrl); + await file.destroy({ + transaction, + }); + } + } +}; diff --git a/backend/src/db/api/fin_accounts.js b/backend/src/db/api/fin_accounts.js new file mode 100644 index 0000000..543b2fd --- /dev/null +++ b/backend/src/db/api/fin_accounts.js @@ -0,0 +1,435 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Fin_accountsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_accounts = await db.fin_accounts.create( + { + id: data.id || undefined, + + code: data.code || null, + name: data.name || null, + type: data.type || null, + is_active: data.is_active || false, + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await fin_accounts.setOrg(data.org || null, { + transaction, + }); + + await fin_accounts.setParent(data.parent || null, { + transaction, + }); + + await fin_accounts.setOrganizations(data.organizations || null, { + transaction, + }); + + return fin_accounts; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const fin_accountsData = data.map((item, index) => ({ + id: item.id || undefined, + + code: item.code || null, + name: item.name || null, + type: item.type || null, + is_active: item.is_active || false, + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const fin_accounts = await db.fin_accounts.bulkCreate(fin_accountsData, { + transaction, + }); + + // For each item created, replace relation files + + return fin_accounts; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const fin_accounts = await db.fin_accounts.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.code !== undefined) updatePayload.code = data.code; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.type !== undefined) updatePayload.type = data.type; + + if (data.is_active !== undefined) updatePayload.is_active = data.is_active; + + updatePayload.updatedById = currentUser.id; + + await fin_accounts.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await fin_accounts.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.parent !== undefined) { + await fin_accounts.setParent( + data.parent, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await fin_accounts.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return fin_accounts; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_accounts = await db.fin_accounts.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of fin_accounts) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of fin_accounts) { + await record.destroy({ transaction }); + } + }); + + return fin_accounts; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_accounts = await db.fin_accounts.findByPk(id, options); + + await fin_accounts.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await fin_accounts.destroy({ + transaction, + }); + + return fin_accounts; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const fin_accounts = await db.fin_accounts.findOne( + { where }, + { transaction }, + ); + + if (!fin_accounts) { + return fin_accounts; + } + + const output = fin_accounts.get({ plain: true }); + + output.fin_budgets_account = await fin_accounts.getFin_budgets_account({ + transaction, + }); + + output.fin_forecasts_account = await fin_accounts.getFin_forecasts_account({ + transaction, + }); + + output.fin_transactions_account = + await fin_accounts.getFin_transactions_account({ + transaction, + }); + + output.org = await fin_accounts.getOrg({ + transaction, + }); + + output.parent = await fin_accounts.getParent({ + transaction, + }); + + output.organizations = await fin_accounts.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.fin_accounts, + as: 'parent', + + where: filter.parent + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.parent + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.parent + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.code) { + where = { + ...where, + [Op.and]: Utils.ilike('fin_accounts', 'code', filter.code), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('fin_accounts', 'name', filter.name), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.type) { + where = { + ...where, + type: filter.type, + }; + } + + if (filter.is_active) { + where = { + ...where, + is_active: filter.is_active, + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.fin_accounts.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('fin_accounts', 'name', query), + ], + }; + } + + const records = await db.fin_accounts.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/api/fin_ap.js b/backend/src/db/api/fin_ap.js new file mode 100644 index 0000000..5f64bf1 --- /dev/null +++ b/backend/src/db/api/fin_ap.js @@ -0,0 +1,443 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Fin_apDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_ap = await db.fin_ap.create( + { + id: data.id || undefined, + + bill_no: data.bill_no || null, + due_date: data.due_date || null, + amount: data.amount || null, + status: data.status || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await fin_ap.setOrg(data.org || null, { + transaction, + }); + + await fin_ap.setVendor(data.vendor || null, { + transaction, + }); + + await fin_ap.setOrganizations(data.organizations || null, { + transaction, + }); + + return fin_ap; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const fin_apData = data.map((item, index) => ({ + id: item.id || undefined, + + bill_no: item.bill_no || null, + due_date: item.due_date || null, + amount: item.amount || null, + status: item.status || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const fin_ap = await db.fin_ap.bulkCreate(fin_apData, { transaction }); + + // For each item created, replace relation files + + return fin_ap; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const fin_ap = await db.fin_ap.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.bill_no !== undefined) updatePayload.bill_no = data.bill_no; + + if (data.due_date !== undefined) updatePayload.due_date = data.due_date; + + if (data.amount !== undefined) updatePayload.amount = data.amount; + + if (data.status !== undefined) updatePayload.status = data.status; + + updatePayload.updatedById = currentUser.id; + + await fin_ap.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await fin_ap.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.vendor !== undefined) { + await fin_ap.setVendor( + data.vendor, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await fin_ap.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return fin_ap; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_ap = await db.fin_ap.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of fin_ap) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of fin_ap) { + await record.destroy({ transaction }); + } + }); + + return fin_ap; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_ap = await db.fin_ap.findByPk(id, options); + + await fin_ap.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await fin_ap.destroy({ + transaction, + }); + + return fin_ap; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const fin_ap = await db.fin_ap.findOne({ where }, { transaction }); + + if (!fin_ap) { + return fin_ap; + } + + const output = fin_ap.get({ plain: true }); + + output.org = await fin_ap.getOrg({ + transaction, + }); + + output.vendor = await fin_ap.getVendor({ + transaction, + }); + + output.organizations = await fin_ap.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.fin_vendors, + as: 'vendor', + + where: filter.vendor + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.vendor + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.vendor + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.bill_no) { + where = { + ...where, + [Op.and]: Utils.ilike('fin_ap', 'bill_no', filter.bill_no), + }; + } + + if (filter.due_dateRange) { + const [start, end] = filter.due_dateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + due_date: { + ...where.due_date, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + due_date: { + ...where.due_date, + [Op.lte]: end, + }, + }; + } + } + + if (filter.amountRange) { + const [start, end] = filter.amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + amount: { + ...where.amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + amount: { + ...where.amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.status) { + where = { + ...where, + status: filter.status, + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.fin_ap.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('fin_ap', 'bill_no', query), + ], + }; + } + + const records = await db.fin_ap.findAll({ + attributes: ['id', 'bill_no'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['bill_no', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.bill_no, + })); + } +}; diff --git a/backend/src/db/api/fin_ar.js b/backend/src/db/api/fin_ar.js new file mode 100644 index 0000000..f828600 --- /dev/null +++ b/backend/src/db/api/fin_ar.js @@ -0,0 +1,444 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Fin_arDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_ar = await db.fin_ar.create( + { + id: data.id || undefined, + + invoice_no: data.invoice_no || null, + due_date: data.due_date || null, + amount: data.amount || null, + status: data.status || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await fin_ar.setOrg(data.org || null, { + transaction, + }); + + await fin_ar.setCustomer(data.customer || null, { + transaction, + }); + + await fin_ar.setOrganizations(data.organizations || null, { + transaction, + }); + + return fin_ar; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const fin_arData = data.map((item, index) => ({ + id: item.id || undefined, + + invoice_no: item.invoice_no || null, + due_date: item.due_date || null, + amount: item.amount || null, + status: item.status || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const fin_ar = await db.fin_ar.bulkCreate(fin_arData, { transaction }); + + // For each item created, replace relation files + + return fin_ar; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const fin_ar = await db.fin_ar.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.invoice_no !== undefined) + updatePayload.invoice_no = data.invoice_no; + + if (data.due_date !== undefined) updatePayload.due_date = data.due_date; + + if (data.amount !== undefined) updatePayload.amount = data.amount; + + if (data.status !== undefined) updatePayload.status = data.status; + + updatePayload.updatedById = currentUser.id; + + await fin_ar.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await fin_ar.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.customer !== undefined) { + await fin_ar.setCustomer( + data.customer, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await fin_ar.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return fin_ar; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_ar = await db.fin_ar.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of fin_ar) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of fin_ar) { + await record.destroy({ transaction }); + } + }); + + return fin_ar; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_ar = await db.fin_ar.findByPk(id, options); + + await fin_ar.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await fin_ar.destroy({ + transaction, + }); + + return fin_ar; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const fin_ar = await db.fin_ar.findOne({ where }, { transaction }); + + if (!fin_ar) { + return fin_ar; + } + + const output = fin_ar.get({ plain: true }); + + output.org = await fin_ar.getOrg({ + transaction, + }); + + output.customer = await fin_ar.getCustomer({ + transaction, + }); + + output.organizations = await fin_ar.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.fin_customers, + as: 'customer', + + where: filter.customer + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.customer + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.customer + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.invoice_no) { + where = { + ...where, + [Op.and]: Utils.ilike('fin_ar', 'invoice_no', filter.invoice_no), + }; + } + + if (filter.due_dateRange) { + const [start, end] = filter.due_dateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + due_date: { + ...where.due_date, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + due_date: { + ...where.due_date, + [Op.lte]: end, + }, + }; + } + } + + if (filter.amountRange) { + const [start, end] = filter.amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + amount: { + ...where.amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + amount: { + ...where.amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.status) { + where = { + ...where, + status: filter.status, + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.fin_ar.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('fin_ar', 'invoice_no', query), + ], + }; + } + + const records = await db.fin_ar.findAll({ + attributes: ['id', 'invoice_no'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['invoice_no', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.invoice_no, + })); + } +}; diff --git a/backend/src/db/api/fin_budgets.js b/backend/src/db/api/fin_budgets.js new file mode 100644 index 0000000..380725d --- /dev/null +++ b/backend/src/db/api/fin_budgets.js @@ -0,0 +1,473 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Fin_budgetsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_budgets = await db.fin_budgets.create( + { + id: data.id || undefined, + + fiscal_year: data.fiscal_year || null, + cost_center: data.cost_center || null, + month: data.month || null, + amount: data.amount || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await fin_budgets.setOrg(data.org || null, { + transaction, + }); + + await fin_budgets.setAccount(data.account || null, { + transaction, + }); + + await fin_budgets.setOrganizations(data.organizations || null, { + transaction, + }); + + return fin_budgets; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const fin_budgetsData = data.map((item, index) => ({ + id: item.id || undefined, + + fiscal_year: item.fiscal_year || null, + cost_center: item.cost_center || null, + month: item.month || null, + amount: item.amount || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const fin_budgets = await db.fin_budgets.bulkCreate(fin_budgetsData, { + transaction, + }); + + // For each item created, replace relation files + + return fin_budgets; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const fin_budgets = await db.fin_budgets.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.fiscal_year !== undefined) + updatePayload.fiscal_year = data.fiscal_year; + + if (data.cost_center !== undefined) + updatePayload.cost_center = data.cost_center; + + if (data.month !== undefined) updatePayload.month = data.month; + + if (data.amount !== undefined) updatePayload.amount = data.amount; + + updatePayload.updatedById = currentUser.id; + + await fin_budgets.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await fin_budgets.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.account !== undefined) { + await fin_budgets.setAccount( + data.account, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await fin_budgets.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return fin_budgets; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_budgets = await db.fin_budgets.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of fin_budgets) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of fin_budgets) { + await record.destroy({ transaction }); + } + }); + + return fin_budgets; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_budgets = await db.fin_budgets.findByPk(id, options); + + await fin_budgets.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await fin_budgets.destroy({ + transaction, + }); + + return fin_budgets; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const fin_budgets = await db.fin_budgets.findOne( + { where }, + { transaction }, + ); + + if (!fin_budgets) { + return fin_budgets; + } + + const output = fin_budgets.get({ plain: true }); + + output.org = await fin_budgets.getOrg({ + transaction, + }); + + output.account = await fin_budgets.getAccount({ + transaction, + }); + + output.organizations = await fin_budgets.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.fin_accounts, + as: 'account', + + where: filter.account + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.account + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.account + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.cost_center) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'fin_budgets', + 'cost_center', + filter.cost_center, + ), + }; + } + + if (filter.fiscal_yearRange) { + const [start, end] = filter.fiscal_yearRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + fiscal_year: { + ...where.fiscal_year, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + fiscal_year: { + ...where.fiscal_year, + [Op.lte]: end, + }, + }; + } + } + + if (filter.monthRange) { + const [start, end] = filter.monthRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + month: { + ...where.month, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + month: { + ...where.month, + [Op.lte]: end, + }, + }; + } + } + + if (filter.amountRange) { + const [start, end] = filter.amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + amount: { + ...where.amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + amount: { + ...where.amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.fin_budgets.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('fin_budgets', 'fiscal_year', query), + ], + }; + } + + const records = await db.fin_budgets.findAll({ + attributes: ['id', 'fiscal_year'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['fiscal_year', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.fiscal_year, + })); + } +}; diff --git a/backend/src/db/api/fin_customers.js b/backend/src/db/api/fin_customers.js new file mode 100644 index 0000000..5784192 --- /dev/null +++ b/backend/src/db/api/fin_customers.js @@ -0,0 +1,380 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Fin_customersDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_customers = await db.fin_customers.create( + { + id: data.id || undefined, + + name: data.name || null, + segment: data.segment || null, + country: data.country || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await fin_customers.setOrg(data.org || null, { + transaction, + }); + + await fin_customers.setOrganizations(data.organizations || null, { + transaction, + }); + + return fin_customers; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const fin_customersData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + segment: item.segment || null, + country: item.country || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const fin_customers = await db.fin_customers.bulkCreate(fin_customersData, { + transaction, + }); + + // For each item created, replace relation files + + return fin_customers; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const fin_customers = await db.fin_customers.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.segment !== undefined) updatePayload.segment = data.segment; + + if (data.country !== undefined) updatePayload.country = data.country; + + updatePayload.updatedById = currentUser.id; + + await fin_customers.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await fin_customers.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await fin_customers.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return fin_customers; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_customers = await db.fin_customers.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of fin_customers) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of fin_customers) { + await record.destroy({ transaction }); + } + }); + + return fin_customers; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_customers = await db.fin_customers.findByPk(id, options); + + await fin_customers.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await fin_customers.destroy({ + transaction, + }); + + return fin_customers; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const fin_customers = await db.fin_customers.findOne( + { where }, + { transaction }, + ); + + if (!fin_customers) { + return fin_customers; + } + + const output = fin_customers.get({ plain: true }); + + output.fin_ar_customer = await fin_customers.getFin_ar_customer({ + transaction, + }); + + output.fin_transactions_customer = + await fin_customers.getFin_transactions_customer({ + transaction, + }); + + output.ops_orders_customer = await fin_customers.getOps_orders_customer({ + transaction, + }); + + output.org = await fin_customers.getOrg({ + transaction, + }); + + output.organizations = await fin_customers.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('fin_customers', 'name', filter.name), + }; + } + + if (filter.segment) { + where = { + ...where, + [Op.and]: Utils.ilike('fin_customers', 'segment', filter.segment), + }; + } + + if (filter.country) { + where = { + ...where, + [Op.and]: Utils.ilike('fin_customers', 'country', filter.country), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.fin_customers.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('fin_customers', 'name', query), + ], + }; + } + + const records = await db.fin_customers.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/api/fin_forecasts.js b/backend/src/db/api/fin_forecasts.js new file mode 100644 index 0000000..703f5cd --- /dev/null +++ b/backend/src/db/api/fin_forecasts.js @@ -0,0 +1,477 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Fin_forecastsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_forecasts = await db.fin_forecasts.create( + { + id: data.id || undefined, + + fiscal_year: data.fiscal_year || null, + cost_center: data.cost_center || null, + month: data.month || null, + amount: data.amount || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await fin_forecasts.setOrg(data.org || null, { + transaction, + }); + + await fin_forecasts.setAccount(data.account || null, { + transaction, + }); + + await fin_forecasts.setOrganizations(data.organizations || null, { + transaction, + }); + + return fin_forecasts; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const fin_forecastsData = data.map((item, index) => ({ + id: item.id || undefined, + + fiscal_year: item.fiscal_year || null, + cost_center: item.cost_center || null, + month: item.month || null, + amount: item.amount || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const fin_forecasts = await db.fin_forecasts.bulkCreate(fin_forecastsData, { + transaction, + }); + + // For each item created, replace relation files + + return fin_forecasts; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const fin_forecasts = await db.fin_forecasts.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.fiscal_year !== undefined) + updatePayload.fiscal_year = data.fiscal_year; + + if (data.cost_center !== undefined) + updatePayload.cost_center = data.cost_center; + + if (data.month !== undefined) updatePayload.month = data.month; + + if (data.amount !== undefined) updatePayload.amount = data.amount; + + updatePayload.updatedById = currentUser.id; + + await fin_forecasts.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await fin_forecasts.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.account !== undefined) { + await fin_forecasts.setAccount( + data.account, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await fin_forecasts.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return fin_forecasts; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_forecasts = await db.fin_forecasts.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of fin_forecasts) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of fin_forecasts) { + await record.destroy({ transaction }); + } + }); + + return fin_forecasts; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_forecasts = await db.fin_forecasts.findByPk(id, options); + + await fin_forecasts.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await fin_forecasts.destroy({ + transaction, + }); + + return fin_forecasts; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const fin_forecasts = await db.fin_forecasts.findOne( + { where }, + { transaction }, + ); + + if (!fin_forecasts) { + return fin_forecasts; + } + + const output = fin_forecasts.get({ plain: true }); + + output.org = await fin_forecasts.getOrg({ + transaction, + }); + + output.account = await fin_forecasts.getAccount({ + transaction, + }); + + output.organizations = await fin_forecasts.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.fin_accounts, + as: 'account', + + where: filter.account + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.account + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.account + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.cost_center) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'fin_forecasts', + 'cost_center', + filter.cost_center, + ), + }; + } + + if (filter.fiscal_yearRange) { + const [start, end] = filter.fiscal_yearRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + fiscal_year: { + ...where.fiscal_year, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + fiscal_year: { + ...where.fiscal_year, + [Op.lte]: end, + }, + }; + } + } + + if (filter.monthRange) { + const [start, end] = filter.monthRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + month: { + ...where.month, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + month: { + ...where.month, + [Op.lte]: end, + }, + }; + } + } + + if (filter.amountRange) { + const [start, end] = filter.amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + amount: { + ...where.amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + amount: { + ...where.amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.fin_forecasts.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('fin_forecasts', 'fiscal_year', query), + ], + }; + } + + const records = await db.fin_forecasts.findAll({ + attributes: ['id', 'fiscal_year'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['fiscal_year', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.fiscal_year, + })); + } +}; diff --git a/backend/src/db/api/fin_transactions.js b/backend/src/db/api/fin_transactions.js new file mode 100644 index 0000000..c8857ac --- /dev/null +++ b/backend/src/db/api/fin_transactions.js @@ -0,0 +1,614 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Fin_transactionsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_transactions = await db.fin_transactions.create( + { + id: data.id || undefined, + + tx_ts: data.tx_ts || null, + amount: data.amount || null, + currency: data.currency || null, + fx_rate: data.fx_rate || null, + cost_center: data.cost_center || null, + project_code: data.project_code || null, + memo: data.memo || null, + source: data.source || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await fin_transactions.setOrg(data.org || null, { + transaction, + }); + + await fin_transactions.setAccount(data.account || null, { + transaction, + }); + + await fin_transactions.setVendor(data.vendor || null, { + transaction, + }); + + await fin_transactions.setCustomer(data.customer || null, { + transaction, + }); + + await fin_transactions.setOrganizations(data.organizations || null, { + transaction, + }); + + return fin_transactions; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const fin_transactionsData = data.map((item, index) => ({ + id: item.id || undefined, + + tx_ts: item.tx_ts || null, + amount: item.amount || null, + currency: item.currency || null, + fx_rate: item.fx_rate || null, + cost_center: item.cost_center || null, + project_code: item.project_code || null, + memo: item.memo || null, + source: item.source || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const fin_transactions = await db.fin_transactions.bulkCreate( + fin_transactionsData, + { transaction }, + ); + + // For each item created, replace relation files + + return fin_transactions; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const fin_transactions = await db.fin_transactions.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.tx_ts !== undefined) updatePayload.tx_ts = data.tx_ts; + + if (data.amount !== undefined) updatePayload.amount = data.amount; + + if (data.currency !== undefined) updatePayload.currency = data.currency; + + if (data.fx_rate !== undefined) updatePayload.fx_rate = data.fx_rate; + + if (data.cost_center !== undefined) + updatePayload.cost_center = data.cost_center; + + if (data.project_code !== undefined) + updatePayload.project_code = data.project_code; + + if (data.memo !== undefined) updatePayload.memo = data.memo; + + if (data.source !== undefined) updatePayload.source = data.source; + + updatePayload.updatedById = currentUser.id; + + await fin_transactions.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await fin_transactions.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.account !== undefined) { + await fin_transactions.setAccount( + data.account, + + { transaction }, + ); + } + + if (data.vendor !== undefined) { + await fin_transactions.setVendor( + data.vendor, + + { transaction }, + ); + } + + if (data.customer !== undefined) { + await fin_transactions.setCustomer( + data.customer, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await fin_transactions.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return fin_transactions; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_transactions = await db.fin_transactions.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of fin_transactions) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of fin_transactions) { + await record.destroy({ transaction }); + } + }); + + return fin_transactions; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_transactions = await db.fin_transactions.findByPk(id, options); + + await fin_transactions.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await fin_transactions.destroy({ + transaction, + }); + + return fin_transactions; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const fin_transactions = await db.fin_transactions.findOne( + { where }, + { transaction }, + ); + + if (!fin_transactions) { + return fin_transactions; + } + + const output = fin_transactions.get({ plain: true }); + + output.org = await fin_transactions.getOrg({ + transaction, + }); + + output.account = await fin_transactions.getAccount({ + transaction, + }); + + output.vendor = await fin_transactions.getVendor({ + transaction, + }); + + output.customer = await fin_transactions.getCustomer({ + transaction, + }); + + output.organizations = await fin_transactions.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.fin_accounts, + as: 'account', + + where: filter.account + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.account + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.account + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.fin_vendors, + as: 'vendor', + + where: filter.vendor + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.vendor + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.vendor + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.fin_customers, + as: 'customer', + + where: filter.customer + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.customer + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.customer + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.currency) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'fin_transactions', + 'currency', + filter.currency, + ), + }; + } + + if (filter.cost_center) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'fin_transactions', + 'cost_center', + filter.cost_center, + ), + }; + } + + if (filter.project_code) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'fin_transactions', + 'project_code', + filter.project_code, + ), + }; + } + + if (filter.memo) { + where = { + ...where, + [Op.and]: Utils.ilike('fin_transactions', 'memo', filter.memo), + }; + } + + if (filter.source) { + where = { + ...where, + [Op.and]: Utils.ilike('fin_transactions', 'source', filter.source), + }; + } + + if (filter.tx_tsRange) { + const [start, end] = filter.tx_tsRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + tx_ts: { + ...where.tx_ts, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + tx_ts: { + ...where.tx_ts, + [Op.lte]: end, + }, + }; + } + } + + if (filter.amountRange) { + const [start, end] = filter.amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + amount: { + ...where.amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + amount: { + ...where.amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.fx_rateRange) { + const [start, end] = filter.fx_rateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + fx_rate: { + ...where.fx_rate, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + fx_rate: { + ...where.fx_rate, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.fin_transactions.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('fin_transactions', 'memo', query), + ], + }; + } + + const records = await db.fin_transactions.findAll({ + attributes: ['id', 'memo'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['memo', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.memo, + })); + } +}; diff --git a/backend/src/db/api/fin_vendors.js b/backend/src/db/api/fin_vendors.js new file mode 100644 index 0000000..4763d6b --- /dev/null +++ b/backend/src/db/api/fin_vendors.js @@ -0,0 +1,372 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Fin_vendorsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_vendors = await db.fin_vendors.create( + { + id: data.id || undefined, + + name: data.name || null, + tax_id: data.tax_id || null, + country: data.country || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await fin_vendors.setOrg(data.org || null, { + transaction, + }); + + await fin_vendors.setOrganizations(data.organizations || null, { + transaction, + }); + + return fin_vendors; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const fin_vendorsData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + tax_id: item.tax_id || null, + country: item.country || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const fin_vendors = await db.fin_vendors.bulkCreate(fin_vendorsData, { + transaction, + }); + + // For each item created, replace relation files + + return fin_vendors; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const fin_vendors = await db.fin_vendors.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.tax_id !== undefined) updatePayload.tax_id = data.tax_id; + + if (data.country !== undefined) updatePayload.country = data.country; + + updatePayload.updatedById = currentUser.id; + + await fin_vendors.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await fin_vendors.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await fin_vendors.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return fin_vendors; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_vendors = await db.fin_vendors.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of fin_vendors) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of fin_vendors) { + await record.destroy({ transaction }); + } + }); + + return fin_vendors; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const fin_vendors = await db.fin_vendors.findByPk(id, options); + + await fin_vendors.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await fin_vendors.destroy({ + transaction, + }); + + return fin_vendors; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const fin_vendors = await db.fin_vendors.findOne( + { where }, + { transaction }, + ); + + if (!fin_vendors) { + return fin_vendors; + } + + const output = fin_vendors.get({ plain: true }); + + output.fin_ap_vendor = await fin_vendors.getFin_ap_vendor({ + transaction, + }); + + output.fin_transactions_vendor = + await fin_vendors.getFin_transactions_vendor({ + transaction, + }); + + output.org = await fin_vendors.getOrg({ + transaction, + }); + + output.organizations = await fin_vendors.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('fin_vendors', 'name', filter.name), + }; + } + + if (filter.tax_id) { + where = { + ...where, + [Op.and]: Utils.ilike('fin_vendors', 'tax_id', filter.tax_id), + }; + } + + if (filter.country) { + where = { + ...where, + [Op.and]: Utils.ilike('fin_vendors', 'country', filter.country), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.fin_vendors.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('fin_vendors', 'name', query), + ], + }; + } + + const records = await db.fin_vendors.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/api/hr_applicants.js b/backend/src/db/api/hr_applicants.js new file mode 100644 index 0000000..80f6d4e --- /dev/null +++ b/backend/src/db/api/hr_applicants.js @@ -0,0 +1,467 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Hr_applicantsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const hr_applicants = await db.hr_applicants.create( + { + id: data.id || undefined, + + name: data.name || null, + stage: data.stage || null, + source: data.source || null, + offer_extended_at: data.offer_extended_at || null, + offer_accepted_at: data.offer_accepted_at || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await hr_applicants.setOrg(data.org || null, { + transaction, + }); + + await hr_applicants.setReq(data.req || null, { + transaction, + }); + + await hr_applicants.setOrganizations(data.organizations || null, { + transaction, + }); + + return hr_applicants; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const hr_applicantsData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + stage: item.stage || null, + source: item.source || null, + offer_extended_at: item.offer_extended_at || null, + offer_accepted_at: item.offer_accepted_at || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const hr_applicants = await db.hr_applicants.bulkCreate(hr_applicantsData, { + transaction, + }); + + // For each item created, replace relation files + + return hr_applicants; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const hr_applicants = await db.hr_applicants.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.stage !== undefined) updatePayload.stage = data.stage; + + if (data.source !== undefined) updatePayload.source = data.source; + + if (data.offer_extended_at !== undefined) + updatePayload.offer_extended_at = data.offer_extended_at; + + if (data.offer_accepted_at !== undefined) + updatePayload.offer_accepted_at = data.offer_accepted_at; + + updatePayload.updatedById = currentUser.id; + + await hr_applicants.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await hr_applicants.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.req !== undefined) { + await hr_applicants.setReq( + data.req, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await hr_applicants.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return hr_applicants; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const hr_applicants = await db.hr_applicants.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of hr_applicants) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of hr_applicants) { + await record.destroy({ transaction }); + } + }); + + return hr_applicants; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const hr_applicants = await db.hr_applicants.findByPk(id, options); + + await hr_applicants.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await hr_applicants.destroy({ + transaction, + }); + + return hr_applicants; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const hr_applicants = await db.hr_applicants.findOne( + { where }, + { transaction }, + ); + + if (!hr_applicants) { + return hr_applicants; + } + + const output = hr_applicants.get({ plain: true }); + + output.org = await hr_applicants.getOrg({ + transaction, + }); + + output.req = await hr_applicants.getReq({ + transaction, + }); + + output.organizations = await hr_applicants.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.hr_reqs, + as: 'req', + + where: filter.req + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.req + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + recruiter: { + [Op.or]: filter.req + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('hr_applicants', 'name', filter.name), + }; + } + + if (filter.source) { + where = { + ...where, + [Op.and]: Utils.ilike('hr_applicants', 'source', filter.source), + }; + } + + if (filter.offer_extended_atRange) { + const [start, end] = filter.offer_extended_atRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + offer_extended_at: { + ...where.offer_extended_at, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + offer_extended_at: { + ...where.offer_extended_at, + [Op.lte]: end, + }, + }; + } + } + + if (filter.offer_accepted_atRange) { + const [start, end] = filter.offer_accepted_atRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + offer_accepted_at: { + ...where.offer_accepted_at, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + offer_accepted_at: { + ...where.offer_accepted_at, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.stage) { + where = { + ...where, + stage: filter.stage, + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.hr_applicants.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('hr_applicants', 'name', query), + ], + }; + } + + const records = await db.hr_applicants.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/api/hr_attendance.js b/backend/src/db/api/hr_attendance.js new file mode 100644 index 0000000..54c4eca --- /dev/null +++ b/backend/src/db/api/hr_attendance.js @@ -0,0 +1,463 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Hr_attendanceDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const hr_attendance = await db.hr_attendance.create( + { + id: data.id || undefined, + + date: data.date || null, + hours_worked: data.hours_worked || null, + leave_type: data.leave_type || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await hr_attendance.setOrg(data.org || null, { + transaction, + }); + + await hr_attendance.setEmployee(data.employee || null, { + transaction, + }); + + await hr_attendance.setOrganizations(data.organizations || null, { + transaction, + }); + + return hr_attendance; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const hr_attendanceData = data.map((item, index) => ({ + id: item.id || undefined, + + date: item.date || null, + hours_worked: item.hours_worked || null, + leave_type: item.leave_type || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const hr_attendance = await db.hr_attendance.bulkCreate(hr_attendanceData, { + transaction, + }); + + // For each item created, replace relation files + + return hr_attendance; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const hr_attendance = await db.hr_attendance.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.date !== undefined) updatePayload.date = data.date; + + if (data.hours_worked !== undefined) + updatePayload.hours_worked = data.hours_worked; + + if (data.leave_type !== undefined) + updatePayload.leave_type = data.leave_type; + + updatePayload.updatedById = currentUser.id; + + await hr_attendance.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await hr_attendance.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.employee !== undefined) { + await hr_attendance.setEmployee( + data.employee, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await hr_attendance.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return hr_attendance; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const hr_attendance = await db.hr_attendance.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of hr_attendance) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of hr_attendance) { + await record.destroy({ transaction }); + } + }); + + return hr_attendance; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const hr_attendance = await db.hr_attendance.findByPk(id, options); + + await hr_attendance.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await hr_attendance.destroy({ + transaction, + }); + + return hr_attendance; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const hr_attendance = await db.hr_attendance.findOne( + { where }, + { transaction }, + ); + + if (!hr_attendance) { + return hr_attendance; + } + + const output = hr_attendance.get({ plain: true }); + + output.org = await hr_attendance.getOrg({ + transaction, + }); + + output.employee = await hr_attendance.getEmployee({ + transaction, + }); + + output.organizations = await hr_attendance.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.hr_employees, + as: 'employee', + + where: filter.employee + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.employee + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.employee + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.calendarStart && filter.calendarEnd) { + where = { + ...where, + [Op.or]: [ + { + date: { + [Op.between]: [filter.calendarStart, filter.calendarEnd], + }, + }, + { + date: { + [Op.between]: [filter.calendarStart, filter.calendarEnd], + }, + }, + ], + }; + } + + if (filter.dateRange) { + const [start, end] = filter.dateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + date: { + ...where.date, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + date: { + ...where.date, + [Op.lte]: end, + }, + }; + } + } + + if (filter.hours_workedRange) { + const [start, end] = filter.hours_workedRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + hours_worked: { + ...where.hours_worked, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + hours_worked: { + ...where.hours_worked, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.leave_type) { + where = { + ...where, + leave_type: filter.leave_type, + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.hr_attendance.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('hr_attendance', 'date', query), + ], + }; + } + + const records = await db.hr_attendance.findAll({ + attributes: ['id', 'date'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['date', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.date, + })); + } +}; diff --git a/backend/src/db/api/hr_employees.js b/backend/src/db/api/hr_employees.js new file mode 100644 index 0000000..79c9613 --- /dev/null +++ b/backend/src/db/api/hr_employees.js @@ -0,0 +1,529 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Hr_employeesDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const hr_employees = await db.hr_employees.create( + { + id: data.id || undefined, + + employee_no: data.employee_no || null, + name: data.name || null, + email: data.email || null, + department: data.department || null, + location: data.location || null, + grade: data.grade || null, + hire_date: data.hire_date || null, + termination_date: data.termination_date || null, + status: data.status || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await hr_employees.setOrg(data.org || null, { + transaction, + }); + + await hr_employees.setManager(data.manager || null, { + transaction, + }); + + await hr_employees.setOrganizations(data.organizations || null, { + transaction, + }); + + return hr_employees; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const hr_employeesData = data.map((item, index) => ({ + id: item.id || undefined, + + employee_no: item.employee_no || null, + name: item.name || null, + email: item.email || null, + department: item.department || null, + location: item.location || null, + grade: item.grade || null, + hire_date: item.hire_date || null, + termination_date: item.termination_date || null, + status: item.status || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const hr_employees = await db.hr_employees.bulkCreate(hr_employeesData, { + transaction, + }); + + // For each item created, replace relation files + + return hr_employees; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const hr_employees = await db.hr_employees.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.employee_no !== undefined) + updatePayload.employee_no = data.employee_no; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.email !== undefined) updatePayload.email = data.email; + + if (data.department !== undefined) + updatePayload.department = data.department; + + if (data.location !== undefined) updatePayload.location = data.location; + + if (data.grade !== undefined) updatePayload.grade = data.grade; + + if (data.hire_date !== undefined) updatePayload.hire_date = data.hire_date; + + if (data.termination_date !== undefined) + updatePayload.termination_date = data.termination_date; + + if (data.status !== undefined) updatePayload.status = data.status; + + updatePayload.updatedById = currentUser.id; + + await hr_employees.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await hr_employees.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.manager !== undefined) { + await hr_employees.setManager( + data.manager, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await hr_employees.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return hr_employees; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const hr_employees = await db.hr_employees.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of hr_employees) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of hr_employees) { + await record.destroy({ transaction }); + } + }); + + return hr_employees; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const hr_employees = await db.hr_employees.findByPk(id, options); + + await hr_employees.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await hr_employees.destroy({ + transaction, + }); + + return hr_employees; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const hr_employees = await db.hr_employees.findOne( + { where }, + { transaction }, + ); + + if (!hr_employees) { + return hr_employees; + } + + const output = hr_employees.get({ plain: true }); + + output.hr_attendance_employee = + await hr_employees.getHr_attendance_employee({ + transaction, + }); + + output.hr_payroll_employee = await hr_employees.getHr_payroll_employee({ + transaction, + }); + + output.org = await hr_employees.getOrg({ + transaction, + }); + + output.manager = await hr_employees.getManager({ + transaction, + }); + + output.organizations = await hr_employees.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.hr_employees, + as: 'manager', + + where: filter.manager + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.manager + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.manager + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.employee_no) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'hr_employees', + 'employee_no', + filter.employee_no, + ), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('hr_employees', 'name', filter.name), + }; + } + + if (filter.email) { + where = { + ...where, + [Op.and]: Utils.ilike('hr_employees', 'email', filter.email), + }; + } + + if (filter.department) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'hr_employees', + 'department', + filter.department, + ), + }; + } + + if (filter.location) { + where = { + ...where, + [Op.and]: Utils.ilike('hr_employees', 'location', filter.location), + }; + } + + if (filter.grade) { + where = { + ...where, + [Op.and]: Utils.ilike('hr_employees', 'grade', filter.grade), + }; + } + + if (filter.hire_dateRange) { + const [start, end] = filter.hire_dateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + hire_date: { + ...where.hire_date, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + hire_date: { + ...where.hire_date, + [Op.lte]: end, + }, + }; + } + } + + if (filter.termination_dateRange) { + const [start, end] = filter.termination_dateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + termination_date: { + ...where.termination_date, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + termination_date: { + ...where.termination_date, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.status) { + where = { + ...where, + status: filter.status, + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.hr_employees.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('hr_employees', 'name', query), + ], + }; + } + + const records = await db.hr_employees.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/api/hr_payroll.js b/backend/src/db/api/hr_payroll.js new file mode 100644 index 0000000..ad053c3 --- /dev/null +++ b/backend/src/db/api/hr_payroll.js @@ -0,0 +1,491 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Hr_payrollDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const hr_payroll = await db.hr_payroll.create( + { + id: data.id || undefined, + + period: data.period || null, + base_pay: data.base_pay || null, + bonus: data.bonus || null, + overtime_hours: data.overtime_hours || null, + currency: data.currency || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await hr_payroll.setOrg(data.org || null, { + transaction, + }); + + await hr_payroll.setEmployee(data.employee || null, { + transaction, + }); + + await hr_payroll.setOrganizations(data.organizations || null, { + transaction, + }); + + return hr_payroll; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const hr_payrollData = data.map((item, index) => ({ + id: item.id || undefined, + + period: item.period || null, + base_pay: item.base_pay || null, + bonus: item.bonus || null, + overtime_hours: item.overtime_hours || null, + currency: item.currency || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const hr_payroll = await db.hr_payroll.bulkCreate(hr_payrollData, { + transaction, + }); + + // For each item created, replace relation files + + return hr_payroll; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const hr_payroll = await db.hr_payroll.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.period !== undefined) updatePayload.period = data.period; + + if (data.base_pay !== undefined) updatePayload.base_pay = data.base_pay; + + if (data.bonus !== undefined) updatePayload.bonus = data.bonus; + + if (data.overtime_hours !== undefined) + updatePayload.overtime_hours = data.overtime_hours; + + if (data.currency !== undefined) updatePayload.currency = data.currency; + + updatePayload.updatedById = currentUser.id; + + await hr_payroll.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await hr_payroll.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.employee !== undefined) { + await hr_payroll.setEmployee( + data.employee, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await hr_payroll.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return hr_payroll; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const hr_payroll = await db.hr_payroll.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of hr_payroll) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of hr_payroll) { + await record.destroy({ transaction }); + } + }); + + return hr_payroll; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const hr_payroll = await db.hr_payroll.findByPk(id, options); + + await hr_payroll.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await hr_payroll.destroy({ + transaction, + }); + + return hr_payroll; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const hr_payroll = await db.hr_payroll.findOne({ where }, { transaction }); + + if (!hr_payroll) { + return hr_payroll; + } + + const output = hr_payroll.get({ plain: true }); + + output.org = await hr_payroll.getOrg({ + transaction, + }); + + output.employee = await hr_payroll.getEmployee({ + transaction, + }); + + output.organizations = await hr_payroll.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.hr_employees, + as: 'employee', + + where: filter.employee + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.employee + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.employee + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.currency) { + where = { + ...where, + [Op.and]: Utils.ilike('hr_payroll', 'currency', filter.currency), + }; + } + + if (filter.periodRange) { + const [start, end] = filter.periodRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + period: { + ...where.period, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + period: { + ...where.period, + [Op.lte]: end, + }, + }; + } + } + + if (filter.base_payRange) { + const [start, end] = filter.base_payRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + base_pay: { + ...where.base_pay, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + base_pay: { + ...where.base_pay, + [Op.lte]: end, + }, + }; + } + } + + if (filter.bonusRange) { + const [start, end] = filter.bonusRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + bonus: { + ...where.bonus, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + bonus: { + ...where.bonus, + [Op.lte]: end, + }, + }; + } + } + + if (filter.overtime_hoursRange) { + const [start, end] = filter.overtime_hoursRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + overtime_hours: { + ...where.overtime_hours, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + overtime_hours: { + ...where.overtime_hours, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.hr_payroll.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('hr_payroll', 'period', query), + ], + }; + } + + const records = await db.hr_payroll.findAll({ + attributes: ['id', 'period'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['period', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.period, + })); + } +}; diff --git a/backend/src/db/api/hr_positions.js b/backend/src/db/api/hr_positions.js new file mode 100644 index 0000000..a7c09cb --- /dev/null +++ b/backend/src/db/api/hr_positions.js @@ -0,0 +1,398 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Hr_positionsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const hr_positions = await db.hr_positions.create( + { + id: data.id || undefined, + + title: data.title || null, + department: data.department || null, + level: data.level || null, + location: data.location || null, + status: data.status || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await hr_positions.setOrg(data.org || null, { + transaction, + }); + + await hr_positions.setOrganizations(data.organizations || null, { + transaction, + }); + + return hr_positions; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const hr_positionsData = data.map((item, index) => ({ + id: item.id || undefined, + + title: item.title || null, + department: item.department || null, + level: item.level || null, + location: item.location || null, + status: item.status || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const hr_positions = await db.hr_positions.bulkCreate(hr_positionsData, { + transaction, + }); + + // For each item created, replace relation files + + return hr_positions; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const hr_positions = await db.hr_positions.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.title !== undefined) updatePayload.title = data.title; + + if (data.department !== undefined) + updatePayload.department = data.department; + + if (data.level !== undefined) updatePayload.level = data.level; + + if (data.location !== undefined) updatePayload.location = data.location; + + if (data.status !== undefined) updatePayload.status = data.status; + + updatePayload.updatedById = currentUser.id; + + await hr_positions.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await hr_positions.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await hr_positions.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return hr_positions; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const hr_positions = await db.hr_positions.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of hr_positions) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of hr_positions) { + await record.destroy({ transaction }); + } + }); + + return hr_positions; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const hr_positions = await db.hr_positions.findByPk(id, options); + + await hr_positions.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await hr_positions.destroy({ + transaction, + }); + + return hr_positions; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const hr_positions = await db.hr_positions.findOne( + { where }, + { transaction }, + ); + + if (!hr_positions) { + return hr_positions; + } + + const output = hr_positions.get({ plain: true }); + + output.hr_reqs_position = await hr_positions.getHr_reqs_position({ + transaction, + }); + + output.org = await hr_positions.getOrg({ + transaction, + }); + + output.organizations = await hr_positions.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.title) { + where = { + ...where, + [Op.and]: Utils.ilike('hr_positions', 'title', filter.title), + }; + } + + if (filter.department) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'hr_positions', + 'department', + filter.department, + ), + }; + } + + if (filter.level) { + where = { + ...where, + [Op.and]: Utils.ilike('hr_positions', 'level', filter.level), + }; + } + + if (filter.location) { + where = { + ...where, + [Op.and]: Utils.ilike('hr_positions', 'location', filter.location), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.status) { + where = { + ...where, + status: filter.status, + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.hr_positions.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('hr_positions', 'title', query), + ], + }; + } + + const records = await db.hr_positions.findAll({ + attributes: ['id', 'title'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['title', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.title, + })); + } +}; diff --git a/backend/src/db/api/hr_reqs.js b/backend/src/db/api/hr_reqs.js new file mode 100644 index 0000000..5b3c3e2 --- /dev/null +++ b/backend/src/db/api/hr_reqs.js @@ -0,0 +1,458 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Hr_reqsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const hr_reqs = await db.hr_reqs.create( + { + id: data.id || undefined, + + opened_at: data.opened_at || null, + filled_at: data.filled_at || null, + status: data.status || null, + recruiter: data.recruiter || null, + priority: data.priority || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await hr_reqs.setOrg(data.org || null, { + transaction, + }); + + await hr_reqs.setPosition(data.position || null, { + transaction, + }); + + await hr_reqs.setOrganizations(data.organizations || null, { + transaction, + }); + + return hr_reqs; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const hr_reqsData = data.map((item, index) => ({ + id: item.id || undefined, + + opened_at: item.opened_at || null, + filled_at: item.filled_at || null, + status: item.status || null, + recruiter: item.recruiter || null, + priority: item.priority || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const hr_reqs = await db.hr_reqs.bulkCreate(hr_reqsData, { transaction }); + + // For each item created, replace relation files + + return hr_reqs; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const hr_reqs = await db.hr_reqs.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.opened_at !== undefined) updatePayload.opened_at = data.opened_at; + + if (data.filled_at !== undefined) updatePayload.filled_at = data.filled_at; + + if (data.status !== undefined) updatePayload.status = data.status; + + if (data.recruiter !== undefined) updatePayload.recruiter = data.recruiter; + + if (data.priority !== undefined) updatePayload.priority = data.priority; + + updatePayload.updatedById = currentUser.id; + + await hr_reqs.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await hr_reqs.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.position !== undefined) { + await hr_reqs.setPosition( + data.position, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await hr_reqs.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return hr_reqs; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const hr_reqs = await db.hr_reqs.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of hr_reqs) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of hr_reqs) { + await record.destroy({ transaction }); + } + }); + + return hr_reqs; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const hr_reqs = await db.hr_reqs.findByPk(id, options); + + await hr_reqs.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await hr_reqs.destroy({ + transaction, + }); + + return hr_reqs; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const hr_reqs = await db.hr_reqs.findOne({ where }, { transaction }); + + if (!hr_reqs) { + return hr_reqs; + } + + const output = hr_reqs.get({ plain: true }); + + output.hr_applicants_req = await hr_reqs.getHr_applicants_req({ + transaction, + }); + + output.org = await hr_reqs.getOrg({ + transaction, + }); + + output.position = await hr_reqs.getPosition({ + transaction, + }); + + output.organizations = await hr_reqs.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.hr_positions, + as: 'position', + + where: filter.position + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.position + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + title: { + [Op.or]: filter.position + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.recruiter) { + where = { + ...where, + [Op.and]: Utils.ilike('hr_reqs', 'recruiter', filter.recruiter), + }; + } + + if (filter.opened_atRange) { + const [start, end] = filter.opened_atRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + opened_at: { + ...where.opened_at, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + opened_at: { + ...where.opened_at, + [Op.lte]: end, + }, + }; + } + } + + if (filter.filled_atRange) { + const [start, end] = filter.filled_atRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + filled_at: { + ...where.filled_at, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + filled_at: { + ...where.filled_at, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.status) { + where = { + ...where, + status: filter.status, + }; + } + + if (filter.priority) { + where = { + ...where, + priority: filter.priority, + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.hr_reqs.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('hr_reqs', 'recruiter', query), + ], + }; + } + + const records = await db.hr_reqs.findAll({ + attributes: ['id', 'recruiter'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['recruiter', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.recruiter, + })); + } +}; diff --git a/backend/src/db/api/ops_incidents.js b/backend/src/db/api/ops_incidents.js new file mode 100644 index 0000000..6028999 --- /dev/null +++ b/backend/src/db/api/ops_incidents.js @@ -0,0 +1,514 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Ops_incidentsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_incidents = await db.ops_incidents.create( + { + id: data.id || undefined, + + occurred_at: data.occurred_at || null, + category: data.category || null, + severity: data.severity || null, + duration_min: data.duration_min || null, + root_cause: data.root_cause || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await ops_incidents.setOrg(data.org || null, { + transaction, + }); + + await ops_incidents.setPlant(data.plant || null, { + transaction, + }); + + await ops_incidents.setLine(data.line || null, { + transaction, + }); + + await ops_incidents.setOrganizations(data.organizations || null, { + transaction, + }); + + return ops_incidents; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const ops_incidentsData = data.map((item, index) => ({ + id: item.id || undefined, + + occurred_at: item.occurred_at || null, + category: item.category || null, + severity: item.severity || null, + duration_min: item.duration_min || null, + root_cause: item.root_cause || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const ops_incidents = await db.ops_incidents.bulkCreate(ops_incidentsData, { + transaction, + }); + + // For each item created, replace relation files + + return ops_incidents; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const ops_incidents = await db.ops_incidents.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.occurred_at !== undefined) + updatePayload.occurred_at = data.occurred_at; + + if (data.category !== undefined) updatePayload.category = data.category; + + if (data.severity !== undefined) updatePayload.severity = data.severity; + + if (data.duration_min !== undefined) + updatePayload.duration_min = data.duration_min; + + if (data.root_cause !== undefined) + updatePayload.root_cause = data.root_cause; + + updatePayload.updatedById = currentUser.id; + + await ops_incidents.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await ops_incidents.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.plant !== undefined) { + await ops_incidents.setPlant( + data.plant, + + { transaction }, + ); + } + + if (data.line !== undefined) { + await ops_incidents.setLine( + data.line, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await ops_incidents.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return ops_incidents; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_incidents = await db.ops_incidents.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of ops_incidents) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of ops_incidents) { + await record.destroy({ transaction }); + } + }); + + return ops_incidents; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_incidents = await db.ops_incidents.findByPk(id, options); + + await ops_incidents.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await ops_incidents.destroy({ + transaction, + }); + + return ops_incidents; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const ops_incidents = await db.ops_incidents.findOne( + { where }, + { transaction }, + ); + + if (!ops_incidents) { + return ops_incidents; + } + + const output = ops_incidents.get({ plain: true }); + + output.org = await ops_incidents.getOrg({ + transaction, + }); + + output.plant = await ops_incidents.getPlant({ + transaction, + }); + + output.line = await ops_incidents.getLine({ + transaction, + }); + + output.organizations = await ops_incidents.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.ops_plants, + as: 'plant', + + where: filter.plant + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.plant + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.plant + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.ops_lines, + as: 'line', + + where: filter.line + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.line + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.line + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.category) { + where = { + ...where, + [Op.and]: Utils.ilike('ops_incidents', 'category', filter.category), + }; + } + + if (filter.root_cause) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'ops_incidents', + 'root_cause', + filter.root_cause, + ), + }; + } + + if (filter.occurred_atRange) { + const [start, end] = filter.occurred_atRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + occurred_at: { + ...where.occurred_at, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + occurred_at: { + ...where.occurred_at, + [Op.lte]: end, + }, + }; + } + } + + if (filter.duration_minRange) { + const [start, end] = filter.duration_minRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + duration_min: { + ...where.duration_min, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + duration_min: { + ...where.duration_min, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.severity) { + where = { + ...where, + severity: filter.severity, + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.ops_incidents.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('ops_incidents', 'category', query), + ], + }; + } + + const records = await db.ops_incidents.findAll({ + attributes: ['id', 'category'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['category', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.category, + })); + } +}; diff --git a/backend/src/db/api/ops_inventory.js b/backend/src/db/api/ops_inventory.js new file mode 100644 index 0000000..79c6773 --- /dev/null +++ b/backend/src/db/api/ops_inventory.js @@ -0,0 +1,469 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Ops_inventoryDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_inventory = await db.ops_inventory.create( + { + id: data.id || undefined, + + sku: data.sku || null, + location: data.location || null, + on_hand: data.on_hand || null, + on_order: data.on_order || null, + safety_stock: data.safety_stock || null, + unit_cost: data.unit_cost || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await ops_inventory.setOrg(data.org || null, { + transaction, + }); + + await ops_inventory.setOrganizations(data.organizations || null, { + transaction, + }); + + return ops_inventory; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const ops_inventoryData = data.map((item, index) => ({ + id: item.id || undefined, + + sku: item.sku || null, + location: item.location || null, + on_hand: item.on_hand || null, + on_order: item.on_order || null, + safety_stock: item.safety_stock || null, + unit_cost: item.unit_cost || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const ops_inventory = await db.ops_inventory.bulkCreate(ops_inventoryData, { + transaction, + }); + + // For each item created, replace relation files + + return ops_inventory; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const ops_inventory = await db.ops_inventory.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.sku !== undefined) updatePayload.sku = data.sku; + + if (data.location !== undefined) updatePayload.location = data.location; + + if (data.on_hand !== undefined) updatePayload.on_hand = data.on_hand; + + if (data.on_order !== undefined) updatePayload.on_order = data.on_order; + + if (data.safety_stock !== undefined) + updatePayload.safety_stock = data.safety_stock; + + if (data.unit_cost !== undefined) updatePayload.unit_cost = data.unit_cost; + + updatePayload.updatedById = currentUser.id; + + await ops_inventory.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await ops_inventory.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await ops_inventory.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return ops_inventory; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_inventory = await db.ops_inventory.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of ops_inventory) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of ops_inventory) { + await record.destroy({ transaction }); + } + }); + + return ops_inventory; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_inventory = await db.ops_inventory.findByPk(id, options); + + await ops_inventory.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await ops_inventory.destroy({ + transaction, + }); + + return ops_inventory; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const ops_inventory = await db.ops_inventory.findOne( + { where }, + { transaction }, + ); + + if (!ops_inventory) { + return ops_inventory; + } + + const output = ops_inventory.get({ plain: true }); + + output.org = await ops_inventory.getOrg({ + transaction, + }); + + output.organizations = await ops_inventory.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.sku) { + where = { + ...where, + [Op.and]: Utils.ilike('ops_inventory', 'sku', filter.sku), + }; + } + + if (filter.location) { + where = { + ...where, + [Op.and]: Utils.ilike('ops_inventory', 'location', filter.location), + }; + } + + if (filter.on_handRange) { + const [start, end] = filter.on_handRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + on_hand: { + ...where.on_hand, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + on_hand: { + ...where.on_hand, + [Op.lte]: end, + }, + }; + } + } + + if (filter.on_orderRange) { + const [start, end] = filter.on_orderRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + on_order: { + ...where.on_order, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + on_order: { + ...where.on_order, + [Op.lte]: end, + }, + }; + } + } + + if (filter.safety_stockRange) { + const [start, end] = filter.safety_stockRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + safety_stock: { + ...where.safety_stock, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + safety_stock: { + ...where.safety_stock, + [Op.lte]: end, + }, + }; + } + } + + if (filter.unit_costRange) { + const [start, end] = filter.unit_costRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + unit_cost: { + ...where.unit_cost, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + unit_cost: { + ...where.unit_cost, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.ops_inventory.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('ops_inventory', 'sku', query), + ], + }; + } + + const records = await db.ops_inventory.findAll({ + attributes: ['id', 'sku'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['sku', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.sku, + })); + } +}; diff --git a/backend/src/db/api/ops_lines.js b/backend/src/db/api/ops_lines.js new file mode 100644 index 0000000..5cec965 --- /dev/null +++ b/backend/src/db/api/ops_lines.js @@ -0,0 +1,388 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Ops_linesDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_lines = await db.ops_lines.create( + { + id: data.id || undefined, + + name: data.name || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await ops_lines.setOrg(data.org || null, { + transaction, + }); + + await ops_lines.setPlant(data.plant || null, { + transaction, + }); + + await ops_lines.setOrganizations(data.organizations || null, { + transaction, + }); + + return ops_lines; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const ops_linesData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const ops_lines = await db.ops_lines.bulkCreate(ops_linesData, { + transaction, + }); + + // For each item created, replace relation files + + return ops_lines; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const ops_lines = await db.ops_lines.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + updatePayload.updatedById = currentUser.id; + + await ops_lines.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await ops_lines.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.plant !== undefined) { + await ops_lines.setPlant( + data.plant, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await ops_lines.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return ops_lines; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_lines = await db.ops_lines.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of ops_lines) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of ops_lines) { + await record.destroy({ transaction }); + } + }); + + return ops_lines; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_lines = await db.ops_lines.findByPk(id, options); + + await ops_lines.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await ops_lines.destroy({ + transaction, + }); + + return ops_lines; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const ops_lines = await db.ops_lines.findOne({ where }, { transaction }); + + if (!ops_lines) { + return ops_lines; + } + + const output = ops_lines.get({ plain: true }); + + output.ops_incidents_line = await ops_lines.getOps_incidents_line({ + transaction, + }); + + output.ops_production_log_line = await ops_lines.getOps_production_log_line( + { + transaction, + }, + ); + + output.org = await ops_lines.getOrg({ + transaction, + }); + + output.plant = await ops_lines.getPlant({ + transaction, + }); + + output.organizations = await ops_lines.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.ops_plants, + as: 'plant', + + where: filter.plant + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.plant + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.plant + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('ops_lines', 'name', filter.name), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.ops_lines.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('ops_lines', 'name', query), + ], + }; + } + + const records = await db.ops_lines.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/api/ops_orders.js b/backend/src/db/api/ops_orders.js new file mode 100644 index 0000000..7a517d8 --- /dev/null +++ b/backend/src/db/api/ops_orders.js @@ -0,0 +1,445 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Ops_ordersDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_orders = await db.ops_orders.create( + { + id: data.id || undefined, + + order_no: data.order_no || null, + due_date: data.due_date || null, + qty: data.qty || null, + status: data.status || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await ops_orders.setOrg(data.org || null, { + transaction, + }); + + await ops_orders.setCustomer(data.customer || null, { + transaction, + }); + + await ops_orders.setOrganizations(data.organizations || null, { + transaction, + }); + + return ops_orders; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const ops_ordersData = data.map((item, index) => ({ + id: item.id || undefined, + + order_no: item.order_no || null, + due_date: item.due_date || null, + qty: item.qty || null, + status: item.status || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const ops_orders = await db.ops_orders.bulkCreate(ops_ordersData, { + transaction, + }); + + // For each item created, replace relation files + + return ops_orders; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const ops_orders = await db.ops_orders.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.order_no !== undefined) updatePayload.order_no = data.order_no; + + if (data.due_date !== undefined) updatePayload.due_date = data.due_date; + + if (data.qty !== undefined) updatePayload.qty = data.qty; + + if (data.status !== undefined) updatePayload.status = data.status; + + updatePayload.updatedById = currentUser.id; + + await ops_orders.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await ops_orders.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.customer !== undefined) { + await ops_orders.setCustomer( + data.customer, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await ops_orders.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return ops_orders; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_orders = await db.ops_orders.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of ops_orders) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of ops_orders) { + await record.destroy({ transaction }); + } + }); + + return ops_orders; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_orders = await db.ops_orders.findByPk(id, options); + + await ops_orders.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await ops_orders.destroy({ + transaction, + }); + + return ops_orders; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const ops_orders = await db.ops_orders.findOne({ where }, { transaction }); + + if (!ops_orders) { + return ops_orders; + } + + const output = ops_orders.get({ plain: true }); + + output.org = await ops_orders.getOrg({ + transaction, + }); + + output.customer = await ops_orders.getCustomer({ + transaction, + }); + + output.organizations = await ops_orders.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.fin_customers, + as: 'customer', + + where: filter.customer + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.customer + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.customer + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.order_no) { + where = { + ...where, + [Op.and]: Utils.ilike('ops_orders', 'order_no', filter.order_no), + }; + } + + if (filter.due_dateRange) { + const [start, end] = filter.due_dateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + due_date: { + ...where.due_date, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + due_date: { + ...where.due_date, + [Op.lte]: end, + }, + }; + } + } + + if (filter.qtyRange) { + const [start, end] = filter.qtyRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + qty: { + ...where.qty, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + qty: { + ...where.qty, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.status) { + where = { + ...where, + status: filter.status, + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.ops_orders.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('ops_orders', 'order_no', query), + ], + }; + } + + const records = await db.ops_orders.findAll({ + attributes: ['id', 'order_no'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['order_no', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.order_no, + })); + } +}; diff --git a/backend/src/db/api/ops_plants.js b/backend/src/db/api/ops_plants.js new file mode 100644 index 0000000..48d91dc --- /dev/null +++ b/backend/src/db/api/ops_plants.js @@ -0,0 +1,366 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Ops_plantsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_plants = await db.ops_plants.create( + { + id: data.id || undefined, + + name: data.name || null, + location: data.location || null, + timezone: data.timezone || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await ops_plants.setOrg(data.org || null, { + transaction, + }); + + await ops_plants.setOrganizations(data.organizations || null, { + transaction, + }); + + return ops_plants; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const ops_plantsData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + location: item.location || null, + timezone: item.timezone || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const ops_plants = await db.ops_plants.bulkCreate(ops_plantsData, { + transaction, + }); + + // For each item created, replace relation files + + return ops_plants; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const ops_plants = await db.ops_plants.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.location !== undefined) updatePayload.location = data.location; + + if (data.timezone !== undefined) updatePayload.timezone = data.timezone; + + updatePayload.updatedById = currentUser.id; + + await ops_plants.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await ops_plants.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await ops_plants.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return ops_plants; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_plants = await db.ops_plants.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of ops_plants) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of ops_plants) { + await record.destroy({ transaction }); + } + }); + + return ops_plants; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_plants = await db.ops_plants.findByPk(id, options); + + await ops_plants.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await ops_plants.destroy({ + transaction, + }); + + return ops_plants; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const ops_plants = await db.ops_plants.findOne({ where }, { transaction }); + + if (!ops_plants) { + return ops_plants; + } + + const output = ops_plants.get({ plain: true }); + + output.ops_incidents_plant = await ops_plants.getOps_incidents_plant({ + transaction, + }); + + output.ops_lines_plant = await ops_plants.getOps_lines_plant({ + transaction, + }); + + output.org = await ops_plants.getOrg({ + transaction, + }); + + output.organizations = await ops_plants.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('ops_plants', 'name', filter.name), + }; + } + + if (filter.location) { + where = { + ...where, + [Op.and]: Utils.ilike('ops_plants', 'location', filter.location), + }; + } + + if (filter.timezone) { + where = { + ...where, + [Op.and]: Utils.ilike('ops_plants', 'timezone', filter.timezone), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.ops_plants.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('ops_plants', 'name', query), + ], + }; + } + + const records = await db.ops_plants.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/api/ops_production_log.js b/backend/src/db/api/ops_production_log.js new file mode 100644 index 0000000..4c19737 --- /dev/null +++ b/backend/src/db/api/ops_production_log.js @@ -0,0 +1,523 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Ops_production_logDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_production_log = await db.ops_production_log.create( + { + id: data.id || undefined, + + ts: data.ts || null, + units_produced: data.units_produced || null, + planned_units: data.planned_units || null, + downtime_min: data.downtime_min || null, + defects: data.defects || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await ops_production_log.setOrg(data.org || null, { + transaction, + }); + + await ops_production_log.setLine(data.line || null, { + transaction, + }); + + await ops_production_log.setOrganizations(data.organizations || null, { + transaction, + }); + + return ops_production_log; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const ops_production_logData = data.map((item, index) => ({ + id: item.id || undefined, + + ts: item.ts || null, + units_produced: item.units_produced || null, + planned_units: item.planned_units || null, + downtime_min: item.downtime_min || null, + defects: item.defects || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const ops_production_log = await db.ops_production_log.bulkCreate( + ops_production_logData, + { transaction }, + ); + + // For each item created, replace relation files + + return ops_production_log; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const ops_production_log = await db.ops_production_log.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.ts !== undefined) updatePayload.ts = data.ts; + + if (data.units_produced !== undefined) + updatePayload.units_produced = data.units_produced; + + if (data.planned_units !== undefined) + updatePayload.planned_units = data.planned_units; + + if (data.downtime_min !== undefined) + updatePayload.downtime_min = data.downtime_min; + + if (data.defects !== undefined) updatePayload.defects = data.defects; + + updatePayload.updatedById = currentUser.id; + + await ops_production_log.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await ops_production_log.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.line !== undefined) { + await ops_production_log.setLine( + data.line, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await ops_production_log.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return ops_production_log; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_production_log = await db.ops_production_log.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of ops_production_log) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of ops_production_log) { + await record.destroy({ transaction }); + } + }); + + return ops_production_log; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_production_log = await db.ops_production_log.findByPk( + id, + options, + ); + + await ops_production_log.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await ops_production_log.destroy({ + transaction, + }); + + return ops_production_log; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const ops_production_log = await db.ops_production_log.findOne( + { where }, + { transaction }, + ); + + if (!ops_production_log) { + return ops_production_log; + } + + const output = ops_production_log.get({ plain: true }); + + output.org = await ops_production_log.getOrg({ + transaction, + }); + + output.line = await ops_production_log.getLine({ + transaction, + }); + + output.organizations = await ops_production_log.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.ops_lines, + as: 'line', + + where: filter.line + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.line + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.line + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.tsRange) { + const [start, end] = filter.tsRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ts: { + ...where.ts, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ts: { + ...where.ts, + [Op.lte]: end, + }, + }; + } + } + + if (filter.units_producedRange) { + const [start, end] = filter.units_producedRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + units_produced: { + ...where.units_produced, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + units_produced: { + ...where.units_produced, + [Op.lte]: end, + }, + }; + } + } + + if (filter.planned_unitsRange) { + const [start, end] = filter.planned_unitsRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + planned_units: { + ...where.planned_units, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + planned_units: { + ...where.planned_units, + [Op.lte]: end, + }, + }; + } + } + + if (filter.downtime_minRange) { + const [start, end] = filter.downtime_minRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + downtime_min: { + ...where.downtime_min, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + downtime_min: { + ...where.downtime_min, + [Op.lte]: end, + }, + }; + } + } + + if (filter.defectsRange) { + const [start, end] = filter.defectsRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + defects: { + ...where.defects, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + defects: { + ...where.defects, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.ops_production_log.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('ops_production_log', 'ts', query), + ], + }; + } + + const records = await db.ops_production_log.findAll({ + attributes: ['id', 'ts'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['ts', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.ts, + })); + } +}; diff --git a/backend/src/db/api/ops_shipments.js b/backend/src/db/api/ops_shipments.js new file mode 100644 index 0000000..f03f039 --- /dev/null +++ b/backend/src/db/api/ops_shipments.js @@ -0,0 +1,425 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Ops_shipmentsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_shipments = await db.ops_shipments.create( + { + id: data.id || undefined, + + order_no: data.order_no || null, + shipped_at: data.shipped_at || null, + carrier: data.carrier || null, + delivered_at: data.delivered_at || null, + status: data.status || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await ops_shipments.setOrg(data.org || null, { + transaction, + }); + + await ops_shipments.setOrganizations(data.organizations || null, { + transaction, + }); + + return ops_shipments; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const ops_shipmentsData = data.map((item, index) => ({ + id: item.id || undefined, + + order_no: item.order_no || null, + shipped_at: item.shipped_at || null, + carrier: item.carrier || null, + delivered_at: item.delivered_at || null, + status: item.status || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const ops_shipments = await db.ops_shipments.bulkCreate(ops_shipmentsData, { + transaction, + }); + + // For each item created, replace relation files + + return ops_shipments; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const ops_shipments = await db.ops_shipments.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.order_no !== undefined) updatePayload.order_no = data.order_no; + + if (data.shipped_at !== undefined) + updatePayload.shipped_at = data.shipped_at; + + if (data.carrier !== undefined) updatePayload.carrier = data.carrier; + + if (data.delivered_at !== undefined) + updatePayload.delivered_at = data.delivered_at; + + if (data.status !== undefined) updatePayload.status = data.status; + + updatePayload.updatedById = currentUser.id; + + await ops_shipments.update(updatePayload, { transaction }); + + if (data.org !== undefined) { + await ops_shipments.setOrg( + data.org, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await ops_shipments.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return ops_shipments; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_shipments = await db.ops_shipments.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of ops_shipments) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of ops_shipments) { + await record.destroy({ transaction }); + } + }); + + return ops_shipments; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const ops_shipments = await db.ops_shipments.findByPk(id, options); + + await ops_shipments.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await ops_shipments.destroy({ + transaction, + }); + + return ops_shipments; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const ops_shipments = await db.ops_shipments.findOne( + { where }, + { transaction }, + ); + + if (!ops_shipments) { + return ops_shipments; + } + + const output = ops_shipments.get({ plain: true }); + + output.org = await ops_shipments.getOrg({ + transaction, + }); + + output.organizations = await ops_shipments.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'org', + }, + + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.order_no) { + where = { + ...where, + [Op.and]: Utils.ilike('ops_shipments', 'order_no', filter.order_no), + }; + } + + if (filter.carrier) { + where = { + ...where, + [Op.and]: Utils.ilike('ops_shipments', 'carrier', filter.carrier), + }; + } + + if (filter.shipped_atRange) { + const [start, end] = filter.shipped_atRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + shipped_at: { + ...where.shipped_at, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + shipped_at: { + ...where.shipped_at, + [Op.lte]: end, + }, + }; + } + } + + if (filter.delivered_atRange) { + const [start, end] = filter.delivered_atRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + delivered_at: { + ...where.delivered_at, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + delivered_at: { + ...where.delivered_at, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.status) { + where = { + ...where, + status: filter.status, + }; + } + + if (filter.org) { + const listItems = filter.org.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + orgId: { [Op.or]: listItems }, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.ops_shipments.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('ops_shipments', 'order_no', query), + ], + }; + } + + const records = await db.ops_shipments.findAll({ + attributes: ['id', 'order_no'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['order_no', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.order_no, + })); + } +}; diff --git a/backend/src/db/api/organizations.js b/backend/src/db/api/organizations.js new file mode 100644 index 0000000..4d174b0 --- /dev/null +++ b/backend/src/db/api/organizations.js @@ -0,0 +1,492 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class OrganizationsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const organizations = await db.organizations.create( + { + id: data.id || undefined, + + name: data.name || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return organizations; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const organizationsData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const organizations = await db.organizations.bulkCreate(organizationsData, { + transaction, + }); + + // For each item created, replace relation files + + return organizations; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const organizations = await db.organizations.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + updatePayload.updatedById = currentUser.id; + + await organizations.update(updatePayload, { transaction }); + + return organizations; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const organizations = await db.organizations.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of organizations) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of organizations) { + await record.destroy({ transaction }); + } + }); + + return organizations; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const organizations = await db.organizations.findByPk(id, options); + + await organizations.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await organizations.destroy({ + transaction, + }); + + return organizations; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const organizations = await db.organizations.findOne( + { where }, + { transaction }, + ); + + if (!organizations) { + return organizations; + } + + const output = organizations.get({ plain: true }); + + output.users_organizations = await organizations.getUsers_organizations({ + transaction, + }); + + output.audit_logs_org = await organizations.getAudit_logs_org({ + transaction, + }); + + output.audit_logs_organizations = + await organizations.getAudit_logs_organizations({ + transaction, + }); + + output.data_connections_org = await organizations.getData_connections_org({ + transaction, + }); + + output.data_connections_organizations = + await organizations.getData_connections_organizations({ + transaction, + }); + + output.fin_accounts_org = await organizations.getFin_accounts_org({ + transaction, + }); + + output.fin_accounts_organizations = + await organizations.getFin_accounts_organizations({ + transaction, + }); + + output.fin_ap_org = await organizations.getFin_ap_org({ + transaction, + }); + + output.fin_ap_organizations = await organizations.getFin_ap_organizations({ + transaction, + }); + + output.fin_ar_org = await organizations.getFin_ar_org({ + transaction, + }); + + output.fin_ar_organizations = await organizations.getFin_ar_organizations({ + transaction, + }); + + output.fin_budgets_org = await organizations.getFin_budgets_org({ + transaction, + }); + + output.fin_budgets_organizations = + await organizations.getFin_budgets_organizations({ + transaction, + }); + + output.fin_customers_org = await organizations.getFin_customers_org({ + transaction, + }); + + output.fin_customers_organizations = + await organizations.getFin_customers_organizations({ + transaction, + }); + + output.fin_forecasts_org = await organizations.getFin_forecasts_org({ + transaction, + }); + + output.fin_forecasts_organizations = + await organizations.getFin_forecasts_organizations({ + transaction, + }); + + output.fin_transactions_org = await organizations.getFin_transactions_org({ + transaction, + }); + + output.fin_transactions_organizations = + await organizations.getFin_transactions_organizations({ + transaction, + }); + + output.fin_vendors_org = await organizations.getFin_vendors_org({ + transaction, + }); + + output.fin_vendors_organizations = + await organizations.getFin_vendors_organizations({ + transaction, + }); + + output.hr_applicants_org = await organizations.getHr_applicants_org({ + transaction, + }); + + output.hr_applicants_organizations = + await organizations.getHr_applicants_organizations({ + transaction, + }); + + output.hr_attendance_org = await organizations.getHr_attendance_org({ + transaction, + }); + + output.hr_attendance_organizations = + await organizations.getHr_attendance_organizations({ + transaction, + }); + + output.hr_employees_org = await organizations.getHr_employees_org({ + transaction, + }); + + output.hr_employees_organizations = + await organizations.getHr_employees_organizations({ + transaction, + }); + + output.hr_payroll_org = await organizations.getHr_payroll_org({ + transaction, + }); + + output.hr_payroll_organizations = + await organizations.getHr_payroll_organizations({ + transaction, + }); + + output.hr_positions_org = await organizations.getHr_positions_org({ + transaction, + }); + + output.hr_positions_organizations = + await organizations.getHr_positions_organizations({ + transaction, + }); + + output.hr_reqs_org = await organizations.getHr_reqs_org({ + transaction, + }); + + output.hr_reqs_organizations = await organizations.getHr_reqs_organizations( + { + transaction, + }, + ); + + output.ops_incidents_org = await organizations.getOps_incidents_org({ + transaction, + }); + + output.ops_incidents_organizations = + await organizations.getOps_incidents_organizations({ + transaction, + }); + + output.ops_inventory_org = await organizations.getOps_inventory_org({ + transaction, + }); + + output.ops_inventory_organizations = + await organizations.getOps_inventory_organizations({ + transaction, + }); + + output.ops_lines_org = await organizations.getOps_lines_org({ + transaction, + }); + + output.ops_lines_organizations = + await organizations.getOps_lines_organizations({ + transaction, + }); + + output.ops_orders_org = await organizations.getOps_orders_org({ + transaction, + }); + + output.ops_orders_organizations = + await organizations.getOps_orders_organizations({ + transaction, + }); + + output.ops_plants_org = await organizations.getOps_plants_org({ + transaction, + }); + + output.ops_plants_organizations = + await organizations.getOps_plants_organizations({ + transaction, + }); + + output.ops_production_log_org = + await organizations.getOps_production_log_org({ + transaction, + }); + + output.ops_production_log_organizations = + await organizations.getOps_production_log_organizations({ + transaction, + }); + + output.ops_shipments_org = await organizations.getOps_shipments_org({ + transaction, + }); + + output.ops_shipments_organizations = + await organizations.getOps_shipments_organizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('organizations', 'name', filter.name), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.organizations.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('organizations', 'name', query), + ], + }; + } + + const records = await db.organizations.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/api/permissions.js b/backend/src/db/api/permissions.js new file mode 100644 index 0000000..2873582 --- /dev/null +++ b/backend/src/db/api/permissions.js @@ -0,0 +1,257 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class PermissionsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const permissions = await db.permissions.create( + { + id: data.id || undefined, + + name: data.name || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return permissions; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const permissionsData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const permissions = await db.permissions.bulkCreate(permissionsData, { + transaction, + }); + + // For each item created, replace relation files + + return permissions; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const permissions = await db.permissions.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + updatePayload.updatedById = currentUser.id; + + await permissions.update(updatePayload, { transaction }); + + return permissions; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const permissions = await db.permissions.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of permissions) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of permissions) { + await record.destroy({ transaction }); + } + }); + + return permissions; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const permissions = await db.permissions.findByPk(id, options); + + await permissions.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await permissions.destroy({ + transaction, + }); + + return permissions; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const permissions = await db.permissions.findOne( + { where }, + { transaction }, + ); + + if (!permissions) { + return permissions; + } + + const output = permissions.get({ plain: true }); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('permissions', 'name', filter.name), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.permissions.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('permissions', 'name', query), + ], + }; + } + + const records = await db.permissions.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/api/roles.js b/backend/src/db/api/roles.js new file mode 100644 index 0000000..5202540 --- /dev/null +++ b/backend/src/db/api/roles.js @@ -0,0 +1,344 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const config = require('../../config'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class RolesDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const roles = await db.roles.create( + { + id: data.id || undefined, + + name: data.name || null, + role_customization: data.role_customization || null, + globalAccess: data.globalAccess || false, + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await roles.setPermissions(data.permissions || [], { + transaction, + }); + + return roles; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const rolesData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + role_customization: item.role_customization || null, + globalAccess: item.globalAccess || false, + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const roles = await db.roles.bulkCreate(rolesData, { transaction }); + + // For each item created, replace relation files + + return roles; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const roles = await db.roles.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.role_customization !== undefined) + updatePayload.role_customization = data.role_customization; + + if (data.globalAccess !== undefined) + updatePayload.globalAccess = data.globalAccess; + + updatePayload.updatedById = currentUser.id; + + await roles.update(updatePayload, { transaction }); + + if (data.permissions !== undefined) { + await roles.setPermissions(data.permissions, { transaction }); + } + + return roles; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const roles = await db.roles.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of roles) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of roles) { + await record.destroy({ transaction }); + } + }); + + return roles; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const roles = await db.roles.findByPk(id, options); + + await roles.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await roles.destroy({ + transaction, + }); + + return roles; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const roles = await db.roles.findOne({ where }, { transaction }); + + if (!roles) { + return roles; + } + + const output = roles.get({ plain: true }); + + output.users_app_role = await roles.getUsers_app_role({ + transaction, + }); + + output.permissions = await roles.getPermissions({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.permissions, + as: 'permissions', + required: false, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('roles', 'name', filter.name), + }; + } + + if (filter.role_customization) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'roles', + 'role_customization', + filter.role_customization, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.globalAccess) { + where = { + ...where, + globalAccess: filter.globalAccess, + }; + } + + if (filter.permissions) { + const searchTerms = filter.permissions.split('|'); + + include = [ + { + model: db.permissions, + as: 'permissions_filter', + required: searchTerms.length > 0, + where: + searchTerms.length > 0 + ? { + [Op.or]: [ + { + id: { + [Op.in]: searchTerms.map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: searchTerms.map((term) => ({ + [Op.iLike]: `%${term}%`, + })), + }, + }, + ], + } + : undefined, + }, + ...include, + ]; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (!globalAccess) { + where = { name: { [Op.ne]: config.roles.super_admin } }; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.roles.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset, globalAccess) { + let where = {}; + + if (!globalAccess) { + where = { name: { [Op.ne]: config.roles.super_admin } }; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('roles', 'name', query), + ], + }; + } + + const records = await db.roles.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js new file mode 100644 index 0000000..6603290 --- /dev/null +++ b/backend/src/db/api/users.js @@ -0,0 +1,804 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const bcrypt = require('bcrypt'); +const config = require('../../config'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class UsersDBApi { + static async create(data, globalAccess, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.create( + { + id: data.data.id || undefined, + + firstName: data.data.firstName || null, + lastName: data.data.lastName || null, + phoneNumber: data.data.phoneNumber || null, + email: data.data.email || null, + disabled: data.data.disabled || false, + + password: data.data.password || null, + emailVerified: data.data.emailVerified || true, + + emailVerificationToken: data.data.emailVerificationToken || null, + emailVerificationTokenExpiresAt: + data.data.emailVerificationTokenExpiresAt || null, + passwordResetToken: data.data.passwordResetToken || null, + passwordResetTokenExpiresAt: + data.data.passwordResetTokenExpiresAt || null, + provider: data.data.provider || null, + importHash: data.data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + if (!data.data.app_role) { + const role = await db.roles.findOne({ + where: { name: 'User' }, + }); + if (role) { + await users.setApp_role(role, { + transaction, + }); + } + } else { + await users.setApp_role(data.data.app_role || null, { + transaction, + }); + } + + await users.setOrganizations(data.data.organizations || null, { + transaction, + }); + + await users.setCustom_permissions(data.data.custom_permissions || [], { + transaction, + }); + + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.users.getTableName(), + belongsToColumn: 'avatar', + belongsToId: users.id, + }, + data.data.avatar, + options, + ); + + return users; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const usersData = data.map((item, index) => ({ + id: item.id || undefined, + + firstName: item.firstName || null, + lastName: item.lastName || null, + phoneNumber: item.phoneNumber || null, + email: item.email || null, + disabled: item.disabled || false, + + password: item.password || null, + emailVerified: item.emailVerified || false, + + emailVerificationToken: item.emailVerificationToken || null, + emailVerificationTokenExpiresAt: + item.emailVerificationTokenExpiresAt || null, + passwordResetToken: item.passwordResetToken || null, + passwordResetTokenExpiresAt: item.passwordResetTokenExpiresAt || null, + provider: item.provider || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const users = await db.users.bulkCreate(usersData, { transaction }); + + // For each item created, replace relation files + + for (let i = 0; i < users.length; i++) { + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.users.getTableName(), + belongsToColumn: 'avatar', + belongsToId: users[i].id, + }, + data[i].avatar, + options, + ); + } + + return users; + } + + static async update(id, data, globalAccess, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findByPk(id, {}, { transaction }); + + if (!data?.app_role) { + data.app_role = users?.app_role?.id; + } + if (!data?.custom_permissions) { + data.custom_permissions = users?.custom_permissions?.map( + (item) => item.id, + ); + } + + if (data.password) { + data.password = bcrypt.hashSync(data.password, config.bcrypt.saltRounds); + } else { + data.password = users.password; + } + + const updatePayload = {}; + + if (data.firstName !== undefined) updatePayload.firstName = data.firstName; + + if (data.lastName !== undefined) updatePayload.lastName = data.lastName; + + if (data.phoneNumber !== undefined) + updatePayload.phoneNumber = data.phoneNumber; + + if (data.email !== undefined) updatePayload.email = data.email; + + if (data.disabled !== undefined) updatePayload.disabled = data.disabled; + + if (data.password !== undefined) updatePayload.password = data.password; + + if (data.emailVerified !== undefined) + updatePayload.emailVerified = data.emailVerified; + else updatePayload.emailVerified = true; + + if (data.emailVerificationToken !== undefined) + updatePayload.emailVerificationToken = data.emailVerificationToken; + + if (data.emailVerificationTokenExpiresAt !== undefined) + updatePayload.emailVerificationTokenExpiresAt = + data.emailVerificationTokenExpiresAt; + + if (data.passwordResetToken !== undefined) + updatePayload.passwordResetToken = data.passwordResetToken; + + if (data.passwordResetTokenExpiresAt !== undefined) + updatePayload.passwordResetTokenExpiresAt = + data.passwordResetTokenExpiresAt; + + if (data.provider !== undefined) updatePayload.provider = data.provider; + + updatePayload.updatedById = currentUser.id; + + await users.update(updatePayload, { transaction }); + + if (data.app_role !== undefined) { + await users.setApp_role( + data.app_role, + + { transaction }, + ); + } + + if (data.organizations !== undefined) { + await users.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + if (data.custom_permissions !== undefined) { + await users.setCustom_permissions(data.custom_permissions, { + transaction, + }); + } + + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.users.getTableName(), + belongsToColumn: 'avatar', + belongsToId: users.id, + }, + data.avatar, + options, + ); + + return users; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of users) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of users) { + await record.destroy({ transaction }); + } + }); + + return users; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findByPk(id, options); + + await users.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await users.destroy({ + transaction, + }); + + return users; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findOne({ where }, { transaction }); + + if (!users) { + return users; + } + + const output = users.get({ plain: true }); + + output.audit_logs_user = await users.getAudit_logs_user({ + transaction, + }); + + output.avatar = await users.getAvatar({ + transaction, + }); + + output.app_role = await users.getApp_role({ + transaction, + }); + + if (output.app_role) { + output.app_role_permissions = await output.app_role.getPermissions({ + transaction, + }); + } + + output.custom_permissions = await users.getCustom_permissions({ + transaction, + }); + + output.organizations = await users.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.roles, + as: 'app_role', + + where: filter.app_role + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.app_role + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.app_role + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.organizations, + as: 'organizations', + }, + + { + model: db.permissions, + as: 'custom_permissions', + required: false, + }, + + { + model: db.file, + as: 'avatar', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.firstName) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'firstName', filter.firstName), + }; + } + + if (filter.lastName) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'lastName', filter.lastName), + }; + } + + if (filter.phoneNumber) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'phoneNumber', filter.phoneNumber), + }; + } + + if (filter.email) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'email', filter.email), + }; + } + + if (filter.password) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'password', filter.password), + }; + } + + if (filter.emailVerificationToken) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'users', + 'emailVerificationToken', + filter.emailVerificationToken, + ), + }; + } + + if (filter.passwordResetToken) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'users', + 'passwordResetToken', + filter.passwordResetToken, + ), + }; + } + + if (filter.provider) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'provider', filter.provider), + }; + } + + if (filter.emailVerificationTokenExpiresAtRange) { + const [start, end] = filter.emailVerificationTokenExpiresAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + emailVerificationTokenExpiresAt: { + ...where.emailVerificationTokenExpiresAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + emailVerificationTokenExpiresAt: { + ...where.emailVerificationTokenExpiresAt, + [Op.lte]: end, + }, + }; + } + } + + if (filter.passwordResetTokenExpiresAtRange) { + const [start, end] = filter.passwordResetTokenExpiresAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + passwordResetTokenExpiresAt: { + ...where.passwordResetTokenExpiresAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + passwordResetTokenExpiresAt: { + ...where.passwordResetTokenExpiresAt, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.disabled) { + where = { + ...where, + disabled: filter.disabled, + }; + } + + if (filter.emailVerified) { + where = { + ...where, + emailVerified: filter.emailVerified, + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.custom_permissions) { + const searchTerms = filter.custom_permissions.split('|'); + + include = [ + { + model: db.permissions, + as: 'custom_permissions_filter', + required: searchTerms.length > 0, + where: + searchTerms.length > 0 + ? { + [Op.or]: [ + { + id: { + [Op.in]: searchTerms.map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: searchTerms.map((term) => ({ + [Op.iLike]: `%${term}%`, + })), + }, + }, + ], + } + : undefined, + }, + ...include, + ]; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.users.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('users', 'firstName', query), + ], + }; + } + + const records = await db.users.findAll({ + attributes: ['id', 'firstName'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['firstName', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.firstName, + })); + } + + static async createFromAuth(data, options) { + const transaction = (options && options.transaction) || undefined; + const users = await db.users.create( + { + email: data.email, + firstName: data.firstName, + authenticationUid: data.authenticationUid, + password: data.password, + + organizationId: data.organizationId, + }, + { transaction }, + ); + + const app_role = await db.roles.findOne({ + where: { name: config.roles?.user || 'User' }, + }); + if (app_role?.id) { + await users.setApp_role(app_role?.id || null, { + transaction, + }); + } + + await users.update( + { + authenticationUid: users.id, + }, + { transaction }, + ); + + delete users.password; + return users; + } + + static async updatePassword(id, password, options) { + const currentUser = (options && options.currentUser) || { id: null }; + + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findByPk(id, { + transaction, + }); + + await users.update( + { + password, + authenticationUid: id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return users; + } + + static async generateEmailVerificationToken(email, options) { + return this._generateToken( + ['emailVerificationToken', 'emailVerificationTokenExpiresAt'], + email, + options, + ); + } + + static async generatePasswordResetToken(email, options) { + return this._generateToken( + ['passwordResetToken', 'passwordResetTokenExpiresAt'], + email, + options, + ); + } + + static async findByPasswordResetToken(token, options) { + const transaction = (options && options.transaction) || undefined; + + return db.users.findOne( + { + where: { + passwordResetToken: token, + passwordResetTokenExpiresAt: { + [db.Sequelize.Op.gt]: Date.now(), + }, + }, + }, + { transaction }, + ); + } + + static async findByEmailVerificationToken(token, options) { + const transaction = (options && options.transaction) || undefined; + return db.users.findOne( + { + where: { + emailVerificationToken: token, + emailVerificationTokenExpiresAt: { + [db.Sequelize.Op.gt]: Date.now(), + }, + }, + }, + { transaction }, + ); + } + + static async markEmailVerified(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findByPk(id, { + transaction, + }); + + await users.update( + { + emailVerified: true, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return true; + } + + static async _generateToken(keyNames, email, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const users = await db.users.findOne( + { + where: { email: email.toLowerCase() }, + }, + { + transaction, + }, + ); + + const token = crypto.randomBytes(20).toString('hex'); + const tokenExpiresAt = Date.now() + 360000; + + if (users) { + await users.update( + { + [keyNames[0]]: token, + [keyNames[1]]: tokenExpiresAt, + updatedById: currentUser.id, + }, + { transaction }, + ); + } + + return token; + } +}; diff --git a/backend/src/db/db.config.js b/backend/src/db/db.config.js new file mode 100644 index 0000000..787185e --- /dev/null +++ b/backend/src/db/db.config.js @@ -0,0 +1,31 @@ +module.exports = { + production: { + dialect: 'postgres', + username: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + host: process.env.DB_HOST, + port: process.env.DB_PORT, + logging: console.log, + seederStorage: 'sequelize', + }, + development: { + username: 'postgres', + dialect: 'postgres', + password: '', + database: 'db_title_enterprise_dashboards_finance_hr_ops', + host: process.env.DB_HOST || 'localhost', + logging: console.log, + seederStorage: 'sequelize', + }, + dev_stage: { + dialect: 'postgres', + username: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + host: process.env.DB_HOST, + port: process.env.DB_PORT, + logging: console.log, + seederStorage: 'sequelize', + }, +}; diff --git a/backend/src/db/migrations/1758865238752.js b/backend/src/db/migrations/1758865238752.js new file mode 100644 index 0000000..5f2cfcd --- /dev/null +++ b/backend/src/db/migrations/1758865238752.js @@ -0,0 +1,3771 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.createTable( + 'users', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'audit_logs', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'data_connections', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'fin_accounts', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'fin_ap', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'fin_ar', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'fin_budgets', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'fin_customers', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'fin_forecasts', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'fin_transactions', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'fin_vendors', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'hr_applicants', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'hr_attendance', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'hr_employees', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'hr_payroll', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'hr_positions', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'hr_reqs', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'ops_incidents', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'ops_inventory', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'ops_lines', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'ops_orders', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'ops_plants', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'ops_production_log', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'ops_shipments', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'roles', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'permissions', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'organizations', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'firstName', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'lastName', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'phoneNumber', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'email', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'disabled', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'password', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'emailVerified', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'emailVerificationToken', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'emailVerificationTokenExpiresAt', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'passwordResetToken', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'passwordResetTokenExpiresAt', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'provider', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'audit_logs', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'audit_logs', + 'userId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'users', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'audit_logs', + 'action', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'audit_logs', + 'entity', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'audit_logs', + 'entity_id', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'audit_logs', + 'diff_json', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'data_connections', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'data_connections', + 'type', + { + type: Sequelize.DataTypes.ENUM, + + values: ['Snowflake', 'BigQuery', 'Redshift', 'Postgres'], + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'data_connections', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'data_connections', + 'config_json', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'data_connections', + 'status', + { + type: Sequelize.DataTypes.ENUM, + + values: ['Active', 'Inactive'], + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'data_connections', + 'last_sync_at', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_accounts', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_accounts', + 'code', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_accounts', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_accounts', + 'type', + { + type: Sequelize.DataTypes.ENUM, + + values: ['asset', 'liability', 'equity', 'revenue', 'expense'], + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_accounts', + 'parentId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'fin_accounts', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_accounts', + 'is_active', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_ap', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_ap', + 'bill_no', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_ap', + 'vendorId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'fin_vendors', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_ap', + 'due_date', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_ap', + 'amount', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_ap', + 'status', + { + type: Sequelize.DataTypes.ENUM, + + values: ['Pending', 'Paid', 'Overdue'], + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_ar', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_ar', + 'invoice_no', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_ar', + 'customerId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'fin_customers', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_ar', + 'due_date', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_ar', + 'amount', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_ar', + 'status', + { + type: Sequelize.DataTypes.ENUM, + + values: ['Pending', 'Paid', 'Overdue'], + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_budgets', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_budgets', + 'fiscal_year', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_budgets', + 'cost_center', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_budgets', + 'accountId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'fin_accounts', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_budgets', + 'month', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_budgets', + 'amount', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_customers', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_customers', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_customers', + 'segment', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_customers', + 'country', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_forecasts', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_forecasts', + 'fiscal_year', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_forecasts', + 'cost_center', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_forecasts', + 'accountId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'fin_accounts', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_forecasts', + 'month', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_forecasts', + 'amount', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_transactions', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_transactions', + 'accountId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'fin_accounts', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_transactions', + 'tx_ts', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_transactions', + 'amount', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_transactions', + 'currency', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_transactions', + 'fx_rate', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_transactions', + 'cost_center', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_transactions', + 'project_code', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_transactions', + 'vendorId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'fin_vendors', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_transactions', + 'customerId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'fin_customers', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_transactions', + 'memo', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_transactions', + 'source', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_vendors', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_vendors', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_vendors', + 'tax_id', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_vendors', + 'country', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_applicants', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_applicants', + 'reqId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'hr_reqs', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_applicants', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_applicants', + 'stage', + { + type: Sequelize.DataTypes.ENUM, + + values: ['Applied', 'Interview', 'Offer', 'Hired'], + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_applicants', + 'source', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_applicants', + 'offer_extended_at', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_applicants', + 'offer_accepted_at', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_attendance', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_attendance', + 'employeeId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'hr_employees', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_attendance', + 'date', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_attendance', + 'hours_worked', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_attendance', + 'leave_type', + { + type: Sequelize.DataTypes.ENUM, + + values: ['Sick', 'Vacation', 'Unpaid'], + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_employees', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_employees', + 'employee_no', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_employees', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_employees', + 'email', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_employees', + 'managerId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'hr_employees', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_employees', + 'department', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_employees', + 'location', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_employees', + 'grade', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_employees', + 'hire_date', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_employees', + 'termination_date', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_employees', + 'status', + { + type: Sequelize.DataTypes.ENUM, + + values: ['Active', 'Inactive'], + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_payroll', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_payroll', + 'employeeId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'hr_employees', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_payroll', + 'period', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_payroll', + 'base_pay', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_payroll', + 'bonus', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_payroll', + 'overtime_hours', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_payroll', + 'currency', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_positions', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_positions', + 'title', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_positions', + 'department', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_positions', + 'level', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_positions', + 'location', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_positions', + 'status', + { + type: Sequelize.DataTypes.ENUM, + + values: ['Open', 'Closed'], + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_reqs', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_reqs', + 'positionId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'hr_positions', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_reqs', + 'opened_at', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_reqs', + 'filled_at', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_reqs', + 'status', + { + type: Sequelize.DataTypes.ENUM, + + values: ['Open', 'Closed', 'OnHold'], + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_reqs', + 'recruiter', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_reqs', + 'priority', + { + type: Sequelize.DataTypes.ENUM, + + values: ['High', 'Medium', 'Low'], + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_incidents', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_incidents', + 'plantId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'ops_plants', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_incidents', + 'lineId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'ops_lines', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_incidents', + 'occurred_at', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_incidents', + 'category', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_incidents', + 'severity', + { + type: Sequelize.DataTypes.ENUM, + + values: ['Low', 'Medium', 'High'], + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_incidents', + 'duration_min', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_incidents', + 'root_cause', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_inventory', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_inventory', + 'sku', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_inventory', + 'location', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_inventory', + 'on_hand', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_inventory', + 'on_order', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_inventory', + 'safety_stock', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_inventory', + 'unit_cost', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_lines', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_lines', + 'plantId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'ops_plants', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_lines', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_orders', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_orders', + 'order_no', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_orders', + 'customerId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'fin_customers', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_orders', + 'due_date', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_orders', + 'qty', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_orders', + 'status', + { + type: Sequelize.DataTypes.ENUM, + + values: ['Pending', 'Completed', 'Cancelled'], + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_plants', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_plants', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_plants', + 'location', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_plants', + 'timezone', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_production_log', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_production_log', + 'lineId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'ops_lines', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_production_log', + 'ts', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_production_log', + 'units_produced', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_production_log', + 'planned_units', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_production_log', + 'downtime_min', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_production_log', + 'defects', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_shipments', + 'orgId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_shipments', + 'order_no', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_shipments', + 'shipped_at', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_shipments', + 'carrier', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_shipments', + 'delivered_at', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_shipments', + 'status', + { + type: Sequelize.DataTypes.ENUM, + + values: ['InTransit', 'Delivered', 'Delayed'], + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'permissions', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'roles', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'roles', + 'role_customization', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'app_roleId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'roles', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'organizations', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'roles', + 'globalAccess', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'audit_logs', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'data_connections', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_accounts', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_ap', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_ar', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_budgets', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_customers', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_forecasts', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_transactions', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'fin_vendors', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_applicants', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_attendance', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_employees', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_payroll', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_positions', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'hr_reqs', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_incidents', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_inventory', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_lines', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_orders', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_plants', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_production_log', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'ops_shipments', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('ops_shipments', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn( + 'ops_production_log', + 'organizationsId', + { transaction }, + ); + + await queryInterface.removeColumn('ops_plants', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('ops_orders', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('ops_lines', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('ops_inventory', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('ops_incidents', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('hr_reqs', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('hr_positions', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('hr_payroll', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('hr_employees', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('hr_attendance', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('hr_applicants', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('fin_vendors', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('fin_transactions', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('fin_forecasts', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('fin_customers', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('fin_budgets', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('fin_ar', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('fin_ap', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('fin_accounts', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('data_connections', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('audit_logs', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('users', 'organizationsId', { + transaction, + }); + + await queryInterface.removeColumn('roles', 'globalAccess', { + transaction, + }); + + await queryInterface.removeColumn('organizations', 'name', { + transaction, + }); + + await queryInterface.removeColumn('users', 'app_roleId', { transaction }); + + await queryInterface.removeColumn('roles', 'role_customization', { + transaction, + }); + + await queryInterface.removeColumn('roles', 'name', { transaction }); + + await queryInterface.removeColumn('permissions', 'name', { transaction }); + + await queryInterface.removeColumn('ops_shipments', 'status', { + transaction, + }); + + await queryInterface.removeColumn('ops_shipments', 'delivered_at', { + transaction, + }); + + await queryInterface.removeColumn('ops_shipments', 'carrier', { + transaction, + }); + + await queryInterface.removeColumn('ops_shipments', 'shipped_at', { + transaction, + }); + + await queryInterface.removeColumn('ops_shipments', 'order_no', { + transaction, + }); + + await queryInterface.removeColumn('ops_shipments', 'orgId', { + transaction, + }); + + await queryInterface.removeColumn('ops_production_log', 'defects', { + transaction, + }); + + await queryInterface.removeColumn('ops_production_log', 'downtime_min', { + transaction, + }); + + await queryInterface.removeColumn('ops_production_log', 'planned_units', { + transaction, + }); + + await queryInterface.removeColumn( + 'ops_production_log', + 'units_produced', + { transaction }, + ); + + await queryInterface.removeColumn('ops_production_log', 'ts', { + transaction, + }); + + await queryInterface.removeColumn('ops_production_log', 'lineId', { + transaction, + }); + + await queryInterface.removeColumn('ops_production_log', 'orgId', { + transaction, + }); + + await queryInterface.removeColumn('ops_plants', 'timezone', { + transaction, + }); + + await queryInterface.removeColumn('ops_plants', 'location', { + transaction, + }); + + await queryInterface.removeColumn('ops_plants', 'name', { transaction }); + + await queryInterface.removeColumn('ops_plants', 'orgId', { transaction }); + + await queryInterface.removeColumn('ops_orders', 'status', { + transaction, + }); + + await queryInterface.removeColumn('ops_orders', 'qty', { transaction }); + + await queryInterface.removeColumn('ops_orders', 'due_date', { + transaction, + }); + + await queryInterface.removeColumn('ops_orders', 'customerId', { + transaction, + }); + + await queryInterface.removeColumn('ops_orders', 'order_no', { + transaction, + }); + + await queryInterface.removeColumn('ops_orders', 'orgId', { transaction }); + + await queryInterface.removeColumn('ops_lines', 'name', { transaction }); + + await queryInterface.removeColumn('ops_lines', 'plantId', { + transaction, + }); + + await queryInterface.removeColumn('ops_lines', 'orgId', { transaction }); + + await queryInterface.removeColumn('ops_inventory', 'unit_cost', { + transaction, + }); + + await queryInterface.removeColumn('ops_inventory', 'safety_stock', { + transaction, + }); + + await queryInterface.removeColumn('ops_inventory', 'on_order', { + transaction, + }); + + await queryInterface.removeColumn('ops_inventory', 'on_hand', { + transaction, + }); + + await queryInterface.removeColumn('ops_inventory', 'location', { + transaction, + }); + + await queryInterface.removeColumn('ops_inventory', 'sku', { + transaction, + }); + + await queryInterface.removeColumn('ops_inventory', 'orgId', { + transaction, + }); + + await queryInterface.removeColumn('ops_incidents', 'root_cause', { + transaction, + }); + + await queryInterface.removeColumn('ops_incidents', 'duration_min', { + transaction, + }); + + await queryInterface.removeColumn('ops_incidents', 'severity', { + transaction, + }); + + await queryInterface.removeColumn('ops_incidents', 'category', { + transaction, + }); + + await queryInterface.removeColumn('ops_incidents', 'occurred_at', { + transaction, + }); + + await queryInterface.removeColumn('ops_incidents', 'lineId', { + transaction, + }); + + await queryInterface.removeColumn('ops_incidents', 'plantId', { + transaction, + }); + + await queryInterface.removeColumn('ops_incidents', 'orgId', { + transaction, + }); + + await queryInterface.removeColumn('hr_reqs', 'priority', { transaction }); + + await queryInterface.removeColumn('hr_reqs', 'recruiter', { + transaction, + }); + + await queryInterface.removeColumn('hr_reqs', 'status', { transaction }); + + await queryInterface.removeColumn('hr_reqs', 'filled_at', { + transaction, + }); + + await queryInterface.removeColumn('hr_reqs', 'opened_at', { + transaction, + }); + + await queryInterface.removeColumn('hr_reqs', 'positionId', { + transaction, + }); + + await queryInterface.removeColumn('hr_reqs', 'orgId', { transaction }); + + await queryInterface.removeColumn('hr_positions', 'status', { + transaction, + }); + + await queryInterface.removeColumn('hr_positions', 'location', { + transaction, + }); + + await queryInterface.removeColumn('hr_positions', 'level', { + transaction, + }); + + await queryInterface.removeColumn('hr_positions', 'department', { + transaction, + }); + + await queryInterface.removeColumn('hr_positions', 'title', { + transaction, + }); + + await queryInterface.removeColumn('hr_positions', 'orgId', { + transaction, + }); + + await queryInterface.removeColumn('hr_payroll', 'currency', { + transaction, + }); + + await queryInterface.removeColumn('hr_payroll', 'overtime_hours', { + transaction, + }); + + await queryInterface.removeColumn('hr_payroll', 'bonus', { transaction }); + + await queryInterface.removeColumn('hr_payroll', 'base_pay', { + transaction, + }); + + await queryInterface.removeColumn('hr_payroll', 'period', { + transaction, + }); + + await queryInterface.removeColumn('hr_payroll', 'employeeId', { + transaction, + }); + + await queryInterface.removeColumn('hr_payroll', 'orgId', { transaction }); + + await queryInterface.removeColumn('hr_employees', 'status', { + transaction, + }); + + await queryInterface.removeColumn('hr_employees', 'termination_date', { + transaction, + }); + + await queryInterface.removeColumn('hr_employees', 'hire_date', { + transaction, + }); + + await queryInterface.removeColumn('hr_employees', 'grade', { + transaction, + }); + + await queryInterface.removeColumn('hr_employees', 'location', { + transaction, + }); + + await queryInterface.removeColumn('hr_employees', 'department', { + transaction, + }); + + await queryInterface.removeColumn('hr_employees', 'managerId', { + transaction, + }); + + await queryInterface.removeColumn('hr_employees', 'email', { + transaction, + }); + + await queryInterface.removeColumn('hr_employees', 'name', { + transaction, + }); + + await queryInterface.removeColumn('hr_employees', 'employee_no', { + transaction, + }); + + await queryInterface.removeColumn('hr_employees', 'orgId', { + transaction, + }); + + await queryInterface.removeColumn('hr_attendance', 'leave_type', { + transaction, + }); + + await queryInterface.removeColumn('hr_attendance', 'hours_worked', { + transaction, + }); + + await queryInterface.removeColumn('hr_attendance', 'date', { + transaction, + }); + + await queryInterface.removeColumn('hr_attendance', 'employeeId', { + transaction, + }); + + await queryInterface.removeColumn('hr_attendance', 'orgId', { + transaction, + }); + + await queryInterface.removeColumn('hr_applicants', 'offer_accepted_at', { + transaction, + }); + + await queryInterface.removeColumn('hr_applicants', 'offer_extended_at', { + transaction, + }); + + await queryInterface.removeColumn('hr_applicants', 'source', { + transaction, + }); + + await queryInterface.removeColumn('hr_applicants', 'stage', { + transaction, + }); + + await queryInterface.removeColumn('hr_applicants', 'name', { + transaction, + }); + + await queryInterface.removeColumn('hr_applicants', 'reqId', { + transaction, + }); + + await queryInterface.removeColumn('hr_applicants', 'orgId', { + transaction, + }); + + await queryInterface.removeColumn('fin_vendors', 'country', { + transaction, + }); + + await queryInterface.removeColumn('fin_vendors', 'tax_id', { + transaction, + }); + + await queryInterface.removeColumn('fin_vendors', 'name', { transaction }); + + await queryInterface.removeColumn('fin_vendors', 'orgId', { + transaction, + }); + + await queryInterface.removeColumn('fin_transactions', 'source', { + transaction, + }); + + await queryInterface.removeColumn('fin_transactions', 'memo', { + transaction, + }); + + await queryInterface.removeColumn('fin_transactions', 'customerId', { + transaction, + }); + + await queryInterface.removeColumn('fin_transactions', 'vendorId', { + transaction, + }); + + await queryInterface.removeColumn('fin_transactions', 'project_code', { + transaction, + }); + + await queryInterface.removeColumn('fin_transactions', 'cost_center', { + transaction, + }); + + await queryInterface.removeColumn('fin_transactions', 'fx_rate', { + transaction, + }); + + await queryInterface.removeColumn('fin_transactions', 'currency', { + transaction, + }); + + await queryInterface.removeColumn('fin_transactions', 'amount', { + transaction, + }); + + await queryInterface.removeColumn('fin_transactions', 'tx_ts', { + transaction, + }); + + await queryInterface.removeColumn('fin_transactions', 'accountId', { + transaction, + }); + + await queryInterface.removeColumn('fin_transactions', 'orgId', { + transaction, + }); + + await queryInterface.removeColumn('fin_forecasts', 'amount', { + transaction, + }); + + await queryInterface.removeColumn('fin_forecasts', 'month', { + transaction, + }); + + await queryInterface.removeColumn('fin_forecasts', 'accountId', { + transaction, + }); + + await queryInterface.removeColumn('fin_forecasts', 'cost_center', { + transaction, + }); + + await queryInterface.removeColumn('fin_forecasts', 'fiscal_year', { + transaction, + }); + + await queryInterface.removeColumn('fin_forecasts', 'orgId', { + transaction, + }); + + await queryInterface.removeColumn('fin_customers', 'country', { + transaction, + }); + + await queryInterface.removeColumn('fin_customers', 'segment', { + transaction, + }); + + await queryInterface.removeColumn('fin_customers', 'name', { + transaction, + }); + + await queryInterface.removeColumn('fin_customers', 'orgId', { + transaction, + }); + + await queryInterface.removeColumn('fin_budgets', 'amount', { + transaction, + }); + + await queryInterface.removeColumn('fin_budgets', 'month', { + transaction, + }); + + await queryInterface.removeColumn('fin_budgets', 'accountId', { + transaction, + }); + + await queryInterface.removeColumn('fin_budgets', 'cost_center', { + transaction, + }); + + await queryInterface.removeColumn('fin_budgets', 'fiscal_year', { + transaction, + }); + + await queryInterface.removeColumn('fin_budgets', 'orgId', { + transaction, + }); + + await queryInterface.removeColumn('fin_ar', 'status', { transaction }); + + await queryInterface.removeColumn('fin_ar', 'amount', { transaction }); + + await queryInterface.removeColumn('fin_ar', 'due_date', { transaction }); + + await queryInterface.removeColumn('fin_ar', 'customerId', { + transaction, + }); + + await queryInterface.removeColumn('fin_ar', 'invoice_no', { + transaction, + }); + + await queryInterface.removeColumn('fin_ar', 'orgId', { transaction }); + + await queryInterface.removeColumn('fin_ap', 'status', { transaction }); + + await queryInterface.removeColumn('fin_ap', 'amount', { transaction }); + + await queryInterface.removeColumn('fin_ap', 'due_date', { transaction }); + + await queryInterface.removeColumn('fin_ap', 'vendorId', { transaction }); + + await queryInterface.removeColumn('fin_ap', 'bill_no', { transaction }); + + await queryInterface.removeColumn('fin_ap', 'orgId', { transaction }); + + await queryInterface.removeColumn('fin_accounts', 'is_active', { + transaction, + }); + + await queryInterface.removeColumn('fin_accounts', 'parentId', { + transaction, + }); + + await queryInterface.removeColumn('fin_accounts', 'type', { + transaction, + }); + + await queryInterface.removeColumn('fin_accounts', 'name', { + transaction, + }); + + await queryInterface.removeColumn('fin_accounts', 'code', { + transaction, + }); + + await queryInterface.removeColumn('fin_accounts', 'orgId', { + transaction, + }); + + await queryInterface.removeColumn('data_connections', 'last_sync_at', { + transaction, + }); + + await queryInterface.removeColumn('data_connections', 'status', { + transaction, + }); + + await queryInterface.removeColumn('data_connections', 'config_json', { + transaction, + }); + + await queryInterface.removeColumn('data_connections', 'name', { + transaction, + }); + + await queryInterface.removeColumn('data_connections', 'type', { + transaction, + }); + + await queryInterface.removeColumn('data_connections', 'orgId', { + transaction, + }); + + await queryInterface.removeColumn('audit_logs', 'diff_json', { + transaction, + }); + + await queryInterface.removeColumn('audit_logs', 'entity_id', { + transaction, + }); + + await queryInterface.removeColumn('audit_logs', 'entity', { + transaction, + }); + + await queryInterface.removeColumn('audit_logs', 'action', { + transaction, + }); + + await queryInterface.removeColumn('audit_logs', 'userId', { + transaction, + }); + + await queryInterface.removeColumn('audit_logs', 'orgId', { transaction }); + + await queryInterface.removeColumn('users', 'provider', { transaction }); + + await queryInterface.removeColumn( + 'users', + 'passwordResetTokenExpiresAt', + { transaction }, + ); + + await queryInterface.removeColumn('users', 'passwordResetToken', { + transaction, + }); + + await queryInterface.removeColumn( + 'users', + 'emailVerificationTokenExpiresAt', + { transaction }, + ); + + await queryInterface.removeColumn('users', 'emailVerificationToken', { + transaction, + }); + + await queryInterface.removeColumn('users', 'emailVerified', { + transaction, + }); + + await queryInterface.removeColumn('users', 'password', { transaction }); + + await queryInterface.removeColumn('users', 'disabled', { transaction }); + + await queryInterface.removeColumn('users', 'email', { transaction }); + + await queryInterface.removeColumn('users', 'phoneNumber', { + transaction, + }); + + await queryInterface.removeColumn('users', 'lastName', { transaction }); + + await queryInterface.removeColumn('users', 'firstName', { transaction }); + + await queryInterface.dropTable('organizations', { transaction }); + + await queryInterface.dropTable('permissions', { transaction }); + + await queryInterface.dropTable('roles', { transaction }); + + await queryInterface.dropTable('ops_shipments', { transaction }); + + await queryInterface.dropTable('ops_production_log', { transaction }); + + await queryInterface.dropTable('ops_plants', { transaction }); + + await queryInterface.dropTable('ops_orders', { transaction }); + + await queryInterface.dropTable('ops_lines', { transaction }); + + await queryInterface.dropTable('ops_inventory', { transaction }); + + await queryInterface.dropTable('ops_incidents', { transaction }); + + await queryInterface.dropTable('hr_reqs', { transaction }); + + await queryInterface.dropTable('hr_positions', { transaction }); + + await queryInterface.dropTable('hr_payroll', { transaction }); + + await queryInterface.dropTable('hr_employees', { transaction }); + + await queryInterface.dropTable('hr_attendance', { transaction }); + + await queryInterface.dropTable('hr_applicants', { transaction }); + + await queryInterface.dropTable('fin_vendors', { transaction }); + + await queryInterface.dropTable('fin_transactions', { transaction }); + + await queryInterface.dropTable('fin_forecasts', { transaction }); + + await queryInterface.dropTable('fin_customers', { transaction }); + + await queryInterface.dropTable('fin_budgets', { transaction }); + + await queryInterface.dropTable('fin_ar', { transaction }); + + await queryInterface.dropTable('fin_ap', { transaction }); + + await queryInterface.dropTable('fin_accounts', { transaction }); + + await queryInterface.dropTable('data_connections', { transaction }); + + await queryInterface.dropTable('audit_logs', { transaction }); + + await queryInterface.dropTable('users', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/audit_logs.js b/backend/src/db/models/audit_logs.js new file mode 100644 index 0000000..4aa68ab --- /dev/null +++ b/backend/src/db/models/audit_logs.js @@ -0,0 +1,85 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const audit_logs = sequelize.define( + 'audit_logs', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + action: { + type: DataTypes.TEXT, + }, + + entity: { + type: DataTypes.TEXT, + }, + + entity_id: { + type: DataTypes.INTEGER, + }, + + diff_json: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + audit_logs.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.audit_logs.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.audit_logs.belongsTo(db.users, { + as: 'user', + foreignKey: { + name: 'userId', + }, + constraints: false, + }); + + db.audit_logs.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.audit_logs.belongsTo(db.users, { + as: 'createdBy', + }); + + db.audit_logs.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return audit_logs; +}; diff --git a/backend/src/db/models/data_connections.js b/backend/src/db/models/data_connections.js new file mode 100644 index 0000000..9d018c6 --- /dev/null +++ b/backend/src/db/models/data_connections.js @@ -0,0 +1,85 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const data_connections = sequelize.define( + 'data_connections', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + type: { + type: DataTypes.ENUM, + + values: ['Snowflake', 'BigQuery', 'Redshift', 'Postgres'], + }, + + name: { + type: DataTypes.TEXT, + }, + + config_json: { + type: DataTypes.TEXT, + }, + + status: { + type: DataTypes.ENUM, + + values: ['Active', 'Inactive'], + }, + + last_sync_at: { + type: DataTypes.DATE, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + data_connections.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.data_connections.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.data_connections.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.data_connections.belongsTo(db.users, { + as: 'createdBy', + }); + + db.data_connections.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return data_connections; +}; diff --git a/backend/src/db/models/file.js b/backend/src/db/models/file.js new file mode 100644 index 0000000..84ee670 --- /dev/null +++ b/backend/src/db/models/file.js @@ -0,0 +1,53 @@ +module.exports = function (sequelize, DataTypes) { + const file = sequelize.define( + 'file', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + belongsTo: DataTypes.STRING(255), + belongsToId: DataTypes.UUID, + belongsToColumn: DataTypes.STRING(255), + name: { + type: DataTypes.STRING(2083), + allowNull: false, + validate: { + notEmpty: true, + }, + }, + sizeInBytes: { + type: DataTypes.INTEGER, + allowNull: true, + }, + privateUrl: { + type: DataTypes.STRING(2083), + allowNull: true, + }, + publicUrl: { + type: DataTypes.STRING(2083), + allowNull: false, + validate: { + notEmpty: true, + }, + }, + }, + { + timestamps: true, + paranoid: true, + }, + ); + + file.associate = (db) => { + db.file.belongsTo(db.users, { + as: 'createdBy', + }); + + db.file.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return file; +}; diff --git a/backend/src/db/models/fin_accounts.js b/backend/src/db/models/fin_accounts.js new file mode 100644 index 0000000..1718273 --- /dev/null +++ b/backend/src/db/models/fin_accounts.js @@ -0,0 +1,114 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const fin_accounts = sequelize.define( + 'fin_accounts', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + code: { + type: DataTypes.TEXT, + }, + + name: { + type: DataTypes.TEXT, + }, + + type: { + type: DataTypes.ENUM, + + values: ['asset', 'liability', 'equity', 'revenue', 'expense'], + }, + + is_active: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + fin_accounts.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.fin_accounts.hasMany(db.fin_budgets, { + as: 'fin_budgets_account', + foreignKey: { + name: 'accountId', + }, + constraints: false, + }); + + db.fin_accounts.hasMany(db.fin_forecasts, { + as: 'fin_forecasts_account', + foreignKey: { + name: 'accountId', + }, + constraints: false, + }); + + db.fin_accounts.hasMany(db.fin_transactions, { + as: 'fin_transactions_account', + foreignKey: { + name: 'accountId', + }, + constraints: false, + }); + + //end loop + + db.fin_accounts.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.fin_accounts.belongsTo(db.fin_accounts, { + as: 'parent', + foreignKey: { + name: 'parentId', + }, + constraints: false, + }); + + db.fin_accounts.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.fin_accounts.belongsTo(db.users, { + as: 'createdBy', + }); + + db.fin_accounts.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return fin_accounts; +}; diff --git a/backend/src/db/models/fin_ap.js b/backend/src/db/models/fin_ap.js new file mode 100644 index 0000000..dfe865e --- /dev/null +++ b/backend/src/db/models/fin_ap.js @@ -0,0 +1,87 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const fin_ap = sequelize.define( + 'fin_ap', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + bill_no: { + type: DataTypes.TEXT, + }, + + due_date: { + type: DataTypes.DATE, + }, + + amount: { + type: DataTypes.DECIMAL, + }, + + status: { + type: DataTypes.ENUM, + + values: ['Pending', 'Paid', 'Overdue'], + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + fin_ap.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.fin_ap.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.fin_ap.belongsTo(db.fin_vendors, { + as: 'vendor', + foreignKey: { + name: 'vendorId', + }, + constraints: false, + }); + + db.fin_ap.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.fin_ap.belongsTo(db.users, { + as: 'createdBy', + }); + + db.fin_ap.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return fin_ap; +}; diff --git a/backend/src/db/models/fin_ar.js b/backend/src/db/models/fin_ar.js new file mode 100644 index 0000000..8f3a719 --- /dev/null +++ b/backend/src/db/models/fin_ar.js @@ -0,0 +1,87 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const fin_ar = sequelize.define( + 'fin_ar', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + invoice_no: { + type: DataTypes.TEXT, + }, + + due_date: { + type: DataTypes.DATE, + }, + + amount: { + type: DataTypes.DECIMAL, + }, + + status: { + type: DataTypes.ENUM, + + values: ['Pending', 'Paid', 'Overdue'], + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + fin_ar.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.fin_ar.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.fin_ar.belongsTo(db.fin_customers, { + as: 'customer', + foreignKey: { + name: 'customerId', + }, + constraints: false, + }); + + db.fin_ar.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.fin_ar.belongsTo(db.users, { + as: 'createdBy', + }); + + db.fin_ar.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return fin_ar; +}; diff --git a/backend/src/db/models/fin_budgets.js b/backend/src/db/models/fin_budgets.js new file mode 100644 index 0000000..18b3c64 --- /dev/null +++ b/backend/src/db/models/fin_budgets.js @@ -0,0 +1,85 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const fin_budgets = sequelize.define( + 'fin_budgets', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + fiscal_year: { + type: DataTypes.INTEGER, + }, + + cost_center: { + type: DataTypes.TEXT, + }, + + month: { + type: DataTypes.INTEGER, + }, + + amount: { + type: DataTypes.DECIMAL, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + fin_budgets.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.fin_budgets.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.fin_budgets.belongsTo(db.fin_accounts, { + as: 'account', + foreignKey: { + name: 'accountId', + }, + constraints: false, + }); + + db.fin_budgets.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.fin_budgets.belongsTo(db.users, { + as: 'createdBy', + }); + + db.fin_budgets.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return fin_budgets; +}; diff --git a/backend/src/db/models/fin_customers.js b/backend/src/db/models/fin_customers.js new file mode 100644 index 0000000..e17aaf7 --- /dev/null +++ b/backend/src/db/models/fin_customers.js @@ -0,0 +1,97 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const fin_customers = sequelize.define( + 'fin_customers', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + segment: { + type: DataTypes.TEXT, + }, + + country: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + fin_customers.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.fin_customers.hasMany(db.fin_ar, { + as: 'fin_ar_customer', + foreignKey: { + name: 'customerId', + }, + constraints: false, + }); + + db.fin_customers.hasMany(db.fin_transactions, { + as: 'fin_transactions_customer', + foreignKey: { + name: 'customerId', + }, + constraints: false, + }); + + db.fin_customers.hasMany(db.ops_orders, { + as: 'ops_orders_customer', + foreignKey: { + name: 'customerId', + }, + constraints: false, + }); + + //end loop + + db.fin_customers.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.fin_customers.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.fin_customers.belongsTo(db.users, { + as: 'createdBy', + }); + + db.fin_customers.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return fin_customers; +}; diff --git a/backend/src/db/models/fin_forecasts.js b/backend/src/db/models/fin_forecasts.js new file mode 100644 index 0000000..3a52d06 --- /dev/null +++ b/backend/src/db/models/fin_forecasts.js @@ -0,0 +1,85 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const fin_forecasts = sequelize.define( + 'fin_forecasts', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + fiscal_year: { + type: DataTypes.INTEGER, + }, + + cost_center: { + type: DataTypes.TEXT, + }, + + month: { + type: DataTypes.INTEGER, + }, + + amount: { + type: DataTypes.DECIMAL, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + fin_forecasts.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.fin_forecasts.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.fin_forecasts.belongsTo(db.fin_accounts, { + as: 'account', + foreignKey: { + name: 'accountId', + }, + constraints: false, + }); + + db.fin_forecasts.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.fin_forecasts.belongsTo(db.users, { + as: 'createdBy', + }); + + db.fin_forecasts.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return fin_forecasts; +}; diff --git a/backend/src/db/models/fin_transactions.js b/backend/src/db/models/fin_transactions.js new file mode 100644 index 0000000..1c1f0fe --- /dev/null +++ b/backend/src/db/models/fin_transactions.js @@ -0,0 +1,117 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const fin_transactions = sequelize.define( + 'fin_transactions', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + tx_ts: { + type: DataTypes.DATE, + }, + + amount: { + type: DataTypes.DECIMAL, + }, + + currency: { + type: DataTypes.TEXT, + }, + + fx_rate: { + type: DataTypes.DECIMAL, + }, + + cost_center: { + type: DataTypes.TEXT, + }, + + project_code: { + type: DataTypes.TEXT, + }, + + memo: { + type: DataTypes.TEXT, + }, + + source: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + fin_transactions.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.fin_transactions.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.fin_transactions.belongsTo(db.fin_accounts, { + as: 'account', + foreignKey: { + name: 'accountId', + }, + constraints: false, + }); + + db.fin_transactions.belongsTo(db.fin_vendors, { + as: 'vendor', + foreignKey: { + name: 'vendorId', + }, + constraints: false, + }); + + db.fin_transactions.belongsTo(db.fin_customers, { + as: 'customer', + foreignKey: { + name: 'customerId', + }, + constraints: false, + }); + + db.fin_transactions.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.fin_transactions.belongsTo(db.users, { + as: 'createdBy', + }); + + db.fin_transactions.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return fin_transactions; +}; diff --git a/backend/src/db/models/fin_vendors.js b/backend/src/db/models/fin_vendors.js new file mode 100644 index 0000000..1a8749f --- /dev/null +++ b/backend/src/db/models/fin_vendors.js @@ -0,0 +1,89 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const fin_vendors = sequelize.define( + 'fin_vendors', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + tax_id: { + type: DataTypes.TEXT, + }, + + country: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + fin_vendors.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.fin_vendors.hasMany(db.fin_ap, { + as: 'fin_ap_vendor', + foreignKey: { + name: 'vendorId', + }, + constraints: false, + }); + + db.fin_vendors.hasMany(db.fin_transactions, { + as: 'fin_transactions_vendor', + foreignKey: { + name: 'vendorId', + }, + constraints: false, + }); + + //end loop + + db.fin_vendors.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.fin_vendors.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.fin_vendors.belongsTo(db.users, { + as: 'createdBy', + }); + + db.fin_vendors.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return fin_vendors; +}; diff --git a/backend/src/db/models/hr_applicants.js b/backend/src/db/models/hr_applicants.js new file mode 100644 index 0000000..21fdb52 --- /dev/null +++ b/backend/src/db/models/hr_applicants.js @@ -0,0 +1,91 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const hr_applicants = sequelize.define( + 'hr_applicants', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + stage: { + type: DataTypes.ENUM, + + values: ['Applied', 'Interview', 'Offer', 'Hired'], + }, + + source: { + type: DataTypes.TEXT, + }, + + offer_extended_at: { + type: DataTypes.DATE, + }, + + offer_accepted_at: { + type: DataTypes.DATE, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + hr_applicants.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.hr_applicants.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.hr_applicants.belongsTo(db.hr_reqs, { + as: 'req', + foreignKey: { + name: 'reqId', + }, + constraints: false, + }); + + db.hr_applicants.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.hr_applicants.belongsTo(db.users, { + as: 'createdBy', + }); + + db.hr_applicants.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return hr_applicants; +}; diff --git a/backend/src/db/models/hr_attendance.js b/backend/src/db/models/hr_attendance.js new file mode 100644 index 0000000..6e06cd7 --- /dev/null +++ b/backend/src/db/models/hr_attendance.js @@ -0,0 +1,83 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const hr_attendance = sequelize.define( + 'hr_attendance', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + date: { + type: DataTypes.DATE, + }, + + hours_worked: { + type: DataTypes.DECIMAL, + }, + + leave_type: { + type: DataTypes.ENUM, + + values: ['Sick', 'Vacation', 'Unpaid'], + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + hr_attendance.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.hr_attendance.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.hr_attendance.belongsTo(db.hr_employees, { + as: 'employee', + foreignKey: { + name: 'employeeId', + }, + constraints: false, + }); + + db.hr_attendance.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.hr_attendance.belongsTo(db.users, { + as: 'createdBy', + }); + + db.hr_attendance.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return hr_attendance; +}; diff --git a/backend/src/db/models/hr_employees.js b/backend/src/db/models/hr_employees.js new file mode 100644 index 0000000..56d0bce --- /dev/null +++ b/backend/src/db/models/hr_employees.js @@ -0,0 +1,123 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const hr_employees = sequelize.define( + 'hr_employees', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + employee_no: { + type: DataTypes.TEXT, + }, + + name: { + type: DataTypes.TEXT, + }, + + email: { + type: DataTypes.TEXT, + }, + + department: { + type: DataTypes.TEXT, + }, + + location: { + type: DataTypes.TEXT, + }, + + grade: { + type: DataTypes.TEXT, + }, + + hire_date: { + type: DataTypes.DATE, + }, + + termination_date: { + type: DataTypes.DATE, + }, + + status: { + type: DataTypes.ENUM, + + values: ['Active', 'Inactive'], + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + hr_employees.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.hr_employees.hasMany(db.hr_attendance, { + as: 'hr_attendance_employee', + foreignKey: { + name: 'employeeId', + }, + constraints: false, + }); + + db.hr_employees.hasMany(db.hr_payroll, { + as: 'hr_payroll_employee', + foreignKey: { + name: 'employeeId', + }, + constraints: false, + }); + + //end loop + + db.hr_employees.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.hr_employees.belongsTo(db.hr_employees, { + as: 'manager', + foreignKey: { + name: 'managerId', + }, + constraints: false, + }); + + db.hr_employees.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.hr_employees.belongsTo(db.users, { + as: 'createdBy', + }); + + db.hr_employees.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return hr_employees; +}; diff --git a/backend/src/db/models/hr_payroll.js b/backend/src/db/models/hr_payroll.js new file mode 100644 index 0000000..f0a7a7e --- /dev/null +++ b/backend/src/db/models/hr_payroll.js @@ -0,0 +1,89 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const hr_payroll = sequelize.define( + 'hr_payroll', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + period: { + type: DataTypes.DATE, + }, + + base_pay: { + type: DataTypes.DECIMAL, + }, + + bonus: { + type: DataTypes.DECIMAL, + }, + + overtime_hours: { + type: DataTypes.DECIMAL, + }, + + currency: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + hr_payroll.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.hr_payroll.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.hr_payroll.belongsTo(db.hr_employees, { + as: 'employee', + foreignKey: { + name: 'employeeId', + }, + constraints: false, + }); + + db.hr_payroll.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.hr_payroll.belongsTo(db.users, { + as: 'createdBy', + }); + + db.hr_payroll.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return hr_payroll; +}; diff --git a/backend/src/db/models/hr_positions.js b/backend/src/db/models/hr_positions.js new file mode 100644 index 0000000..ae7d214 --- /dev/null +++ b/backend/src/db/models/hr_positions.js @@ -0,0 +1,91 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const hr_positions = sequelize.define( + 'hr_positions', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + title: { + type: DataTypes.TEXT, + }, + + department: { + type: DataTypes.TEXT, + }, + + level: { + type: DataTypes.TEXT, + }, + + location: { + type: DataTypes.TEXT, + }, + + status: { + type: DataTypes.ENUM, + + values: ['Open', 'Closed'], + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + hr_positions.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.hr_positions.hasMany(db.hr_reqs, { + as: 'hr_reqs_position', + foreignKey: { + name: 'positionId', + }, + constraints: false, + }); + + //end loop + + db.hr_positions.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.hr_positions.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.hr_positions.belongsTo(db.users, { + as: 'createdBy', + }); + + db.hr_positions.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return hr_positions; +}; diff --git a/backend/src/db/models/hr_reqs.js b/backend/src/db/models/hr_reqs.js new file mode 100644 index 0000000..6790eb0 --- /dev/null +++ b/backend/src/db/models/hr_reqs.js @@ -0,0 +1,101 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const hr_reqs = sequelize.define( + 'hr_reqs', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + opened_at: { + type: DataTypes.DATE, + }, + + filled_at: { + type: DataTypes.DATE, + }, + + status: { + type: DataTypes.ENUM, + + values: ['Open', 'Closed', 'OnHold'], + }, + + recruiter: { + type: DataTypes.TEXT, + }, + + priority: { + type: DataTypes.ENUM, + + values: ['High', 'Medium', 'Low'], + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + hr_reqs.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.hr_reqs.hasMany(db.hr_applicants, { + as: 'hr_applicants_req', + foreignKey: { + name: 'reqId', + }, + constraints: false, + }); + + //end loop + + db.hr_reqs.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.hr_reqs.belongsTo(db.hr_positions, { + as: 'position', + foreignKey: { + name: 'positionId', + }, + constraints: false, + }); + + db.hr_reqs.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.hr_reqs.belongsTo(db.users, { + as: 'createdBy', + }); + + db.hr_reqs.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return hr_reqs; +}; diff --git a/backend/src/db/models/index.js b/backend/src/db/models/index.js new file mode 100644 index 0000000..e326416 --- /dev/null +++ b/backend/src/db/models/index.js @@ -0,0 +1,47 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const Sequelize = require('sequelize'); +const basename = path.basename(__filename); +const env = process.env.NODE_ENV || 'development'; +const config = require('../db.config')[env]; +const db = {}; + +let sequelize; +console.log(env); +if (config.use_env_variable) { + sequelize = new Sequelize(process.env[config.use_env_variable], config); +} else { + sequelize = new Sequelize( + config.database, + config.username, + config.password, + config, + ); +} + +fs.readdirSync(__dirname) + .filter((file) => { + return ( + file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js' + ); + }) + .forEach((file) => { + const model = require(path.join(__dirname, file))( + sequelize, + Sequelize.DataTypes, + ); + db[model.name] = model; + }); + +Object.keys(db).forEach((modelName) => { + if (db[modelName].associate) { + db[modelName].associate(db); + } +}); + +db.sequelize = sequelize; +db.Sequelize = Sequelize; + +module.exports = db; diff --git a/backend/src/db/models/ops_incidents.js b/backend/src/db/models/ops_incidents.js new file mode 100644 index 0000000..bdd6090 --- /dev/null +++ b/backend/src/db/models/ops_incidents.js @@ -0,0 +1,99 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const ops_incidents = sequelize.define( + 'ops_incidents', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + occurred_at: { + type: DataTypes.DATE, + }, + + category: { + type: DataTypes.TEXT, + }, + + severity: { + type: DataTypes.ENUM, + + values: ['Low', 'Medium', 'High'], + }, + + duration_min: { + type: DataTypes.INTEGER, + }, + + root_cause: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + ops_incidents.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.ops_incidents.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.ops_incidents.belongsTo(db.ops_plants, { + as: 'plant', + foreignKey: { + name: 'plantId', + }, + constraints: false, + }); + + db.ops_incidents.belongsTo(db.ops_lines, { + as: 'line', + foreignKey: { + name: 'lineId', + }, + constraints: false, + }); + + db.ops_incidents.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.ops_incidents.belongsTo(db.users, { + as: 'createdBy', + }); + + db.ops_incidents.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return ops_incidents; +}; diff --git a/backend/src/db/models/ops_inventory.js b/backend/src/db/models/ops_inventory.js new file mode 100644 index 0000000..fad90a0 --- /dev/null +++ b/backend/src/db/models/ops_inventory.js @@ -0,0 +1,85 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const ops_inventory = sequelize.define( + 'ops_inventory', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + sku: { + type: DataTypes.TEXT, + }, + + location: { + type: DataTypes.TEXT, + }, + + on_hand: { + type: DataTypes.INTEGER, + }, + + on_order: { + type: DataTypes.INTEGER, + }, + + safety_stock: { + type: DataTypes.INTEGER, + }, + + unit_cost: { + type: DataTypes.DECIMAL, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + ops_inventory.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.ops_inventory.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.ops_inventory.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.ops_inventory.belongsTo(db.users, { + as: 'createdBy', + }); + + db.ops_inventory.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return ops_inventory; +}; diff --git a/backend/src/db/models/ops_lines.js b/backend/src/db/models/ops_lines.js new file mode 100644 index 0000000..2cf7bfe --- /dev/null +++ b/backend/src/db/models/ops_lines.js @@ -0,0 +1,89 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const ops_lines = sequelize.define( + 'ops_lines', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + ops_lines.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.ops_lines.hasMany(db.ops_incidents, { + as: 'ops_incidents_line', + foreignKey: { + name: 'lineId', + }, + constraints: false, + }); + + db.ops_lines.hasMany(db.ops_production_log, { + as: 'ops_production_log_line', + foreignKey: { + name: 'lineId', + }, + constraints: false, + }); + + //end loop + + db.ops_lines.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.ops_lines.belongsTo(db.ops_plants, { + as: 'plant', + foreignKey: { + name: 'plantId', + }, + constraints: false, + }); + + db.ops_lines.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.ops_lines.belongsTo(db.users, { + as: 'createdBy', + }); + + db.ops_lines.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return ops_lines; +}; diff --git a/backend/src/db/models/ops_orders.js b/backend/src/db/models/ops_orders.js new file mode 100644 index 0000000..336c11f --- /dev/null +++ b/backend/src/db/models/ops_orders.js @@ -0,0 +1,87 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const ops_orders = sequelize.define( + 'ops_orders', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + order_no: { + type: DataTypes.TEXT, + }, + + due_date: { + type: DataTypes.DATE, + }, + + qty: { + type: DataTypes.INTEGER, + }, + + status: { + type: DataTypes.ENUM, + + values: ['Pending', 'Completed', 'Cancelled'], + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + ops_orders.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.ops_orders.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.ops_orders.belongsTo(db.fin_customers, { + as: 'customer', + foreignKey: { + name: 'customerId', + }, + constraints: false, + }); + + db.ops_orders.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.ops_orders.belongsTo(db.users, { + as: 'createdBy', + }); + + db.ops_orders.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return ops_orders; +}; diff --git a/backend/src/db/models/ops_plants.js b/backend/src/db/models/ops_plants.js new file mode 100644 index 0000000..d08b4e3 --- /dev/null +++ b/backend/src/db/models/ops_plants.js @@ -0,0 +1,89 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const ops_plants = sequelize.define( + 'ops_plants', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + location: { + type: DataTypes.TEXT, + }, + + timezone: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + ops_plants.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.ops_plants.hasMany(db.ops_incidents, { + as: 'ops_incidents_plant', + foreignKey: { + name: 'plantId', + }, + constraints: false, + }); + + db.ops_plants.hasMany(db.ops_lines, { + as: 'ops_lines_plant', + foreignKey: { + name: 'plantId', + }, + constraints: false, + }); + + //end loop + + db.ops_plants.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.ops_plants.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.ops_plants.belongsTo(db.users, { + as: 'createdBy', + }); + + db.ops_plants.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return ops_plants; +}; diff --git a/backend/src/db/models/ops_production_log.js b/backend/src/db/models/ops_production_log.js new file mode 100644 index 0000000..ebb9466 --- /dev/null +++ b/backend/src/db/models/ops_production_log.js @@ -0,0 +1,89 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const ops_production_log = sequelize.define( + 'ops_production_log', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + ts: { + type: DataTypes.DATE, + }, + + units_produced: { + type: DataTypes.INTEGER, + }, + + planned_units: { + type: DataTypes.INTEGER, + }, + + downtime_min: { + type: DataTypes.INTEGER, + }, + + defects: { + type: DataTypes.INTEGER, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + ops_production_log.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.ops_production_log.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.ops_production_log.belongsTo(db.ops_lines, { + as: 'line', + foreignKey: { + name: 'lineId', + }, + constraints: false, + }); + + db.ops_production_log.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.ops_production_log.belongsTo(db.users, { + as: 'createdBy', + }); + + db.ops_production_log.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return ops_production_log; +}; diff --git a/backend/src/db/models/ops_shipments.js b/backend/src/db/models/ops_shipments.js new file mode 100644 index 0000000..4ee8e6f --- /dev/null +++ b/backend/src/db/models/ops_shipments.js @@ -0,0 +1,83 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const ops_shipments = sequelize.define( + 'ops_shipments', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + order_no: { + type: DataTypes.TEXT, + }, + + shipped_at: { + type: DataTypes.DATE, + }, + + carrier: { + type: DataTypes.TEXT, + }, + + delivered_at: { + type: DataTypes.DATE, + }, + + status: { + type: DataTypes.ENUM, + + values: ['InTransit', 'Delivered', 'Delayed'], + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + ops_shipments.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.ops_shipments.belongsTo(db.organizations, { + as: 'org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.ops_shipments.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.ops_shipments.belongsTo(db.users, { + as: 'createdBy', + }); + + db.ops_shipments.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return ops_shipments; +}; diff --git a/backend/src/db/models/organizations.js b/backend/src/db/models/organizations.js new file mode 100644 index 0000000..5ad9d7b --- /dev/null +++ b/backend/src/db/models/organizations.js @@ -0,0 +1,425 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const organizations = sequelize.define( + 'organizations', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + organizations.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.organizations.hasMany(db.users, { + as: 'users_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.audit_logs, { + as: 'audit_logs_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.audit_logs, { + as: 'audit_logs_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.data_connections, { + as: 'data_connections_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.data_connections, { + as: 'data_connections_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.fin_accounts, { + as: 'fin_accounts_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.fin_accounts, { + as: 'fin_accounts_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.fin_ap, { + as: 'fin_ap_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.fin_ap, { + as: 'fin_ap_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.fin_ar, { + as: 'fin_ar_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.fin_ar, { + as: 'fin_ar_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.fin_budgets, { + as: 'fin_budgets_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.fin_budgets, { + as: 'fin_budgets_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.fin_customers, { + as: 'fin_customers_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.fin_customers, { + as: 'fin_customers_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.fin_forecasts, { + as: 'fin_forecasts_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.fin_forecasts, { + as: 'fin_forecasts_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.fin_transactions, { + as: 'fin_transactions_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.fin_transactions, { + as: 'fin_transactions_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.fin_vendors, { + as: 'fin_vendors_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.fin_vendors, { + as: 'fin_vendors_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.hr_applicants, { + as: 'hr_applicants_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.hr_applicants, { + as: 'hr_applicants_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.hr_attendance, { + as: 'hr_attendance_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.hr_attendance, { + as: 'hr_attendance_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.hr_employees, { + as: 'hr_employees_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.hr_employees, { + as: 'hr_employees_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.hr_payroll, { + as: 'hr_payroll_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.hr_payroll, { + as: 'hr_payroll_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.hr_positions, { + as: 'hr_positions_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.hr_positions, { + as: 'hr_positions_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.hr_reqs, { + as: 'hr_reqs_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.hr_reqs, { + as: 'hr_reqs_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.ops_incidents, { + as: 'ops_incidents_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.ops_incidents, { + as: 'ops_incidents_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.ops_inventory, { + as: 'ops_inventory_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.ops_inventory, { + as: 'ops_inventory_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.ops_lines, { + as: 'ops_lines_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.ops_lines, { + as: 'ops_lines_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.ops_orders, { + as: 'ops_orders_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.ops_orders, { + as: 'ops_orders_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.ops_plants, { + as: 'ops_plants_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.ops_plants, { + as: 'ops_plants_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.ops_production_log, { + as: 'ops_production_log_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.ops_production_log, { + as: 'ops_production_log_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.ops_shipments, { + as: 'ops_shipments_org', + foreignKey: { + name: 'orgId', + }, + constraints: false, + }); + + db.organizations.hasMany(db.ops_shipments, { + as: 'ops_shipments_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + //end loop + + db.organizations.belongsTo(db.users, { + as: 'createdBy', + }); + + db.organizations.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return organizations; +}; diff --git a/backend/src/db/models/permissions.js b/backend/src/db/models/permissions.js new file mode 100644 index 0000000..d647c73 --- /dev/null +++ b/backend/src/db/models/permissions.js @@ -0,0 +1,49 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const permissions = sequelize.define( + 'permissions', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + permissions.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.permissions.belongsTo(db.users, { + as: 'createdBy', + }); + + db.permissions.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return permissions; +}; diff --git a/backend/src/db/models/roles.js b/backend/src/db/models/roles.js new file mode 100644 index 0000000..0f144d5 --- /dev/null +++ b/backend/src/db/models/roles.js @@ -0,0 +1,86 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const roles = sequelize.define( + 'roles', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + role_customization: { + type: DataTypes.TEXT, + }, + + globalAccess: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + roles.associate = (db) => { + db.roles.belongsToMany(db.permissions, { + as: 'permissions', + foreignKey: { + name: 'roles_permissionsId', + }, + constraints: false, + through: 'rolesPermissionsPermissions', + }); + + db.roles.belongsToMany(db.permissions, { + as: 'permissions_filter', + foreignKey: { + name: 'roles_permissionsId', + }, + constraints: false, + through: 'rolesPermissionsPermissions', + }); + + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.roles.hasMany(db.users, { + as: 'users_app_role', + foreignKey: { + name: 'app_roleId', + }, + constraints: false, + }); + + //end loop + + db.roles.belongsTo(db.users, { + as: 'createdBy', + }); + + db.roles.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return roles; +}; diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js new file mode 100644 index 0000000..a658508 --- /dev/null +++ b/backend/src/db/models/users.js @@ -0,0 +1,187 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const users = sequelize.define( + 'users', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + firstName: { + type: DataTypes.TEXT, + }, + + lastName: { + type: DataTypes.TEXT, + }, + + phoneNumber: { + type: DataTypes.TEXT, + }, + + email: { + type: DataTypes.TEXT, + }, + + disabled: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + }, + + password: { + type: DataTypes.TEXT, + }, + + emailVerified: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + }, + + emailVerificationToken: { + type: DataTypes.TEXT, + }, + + emailVerificationTokenExpiresAt: { + type: DataTypes.DATE, + }, + + passwordResetToken: { + type: DataTypes.TEXT, + }, + + passwordResetTokenExpiresAt: { + type: DataTypes.DATE, + }, + + provider: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + users.associate = (db) => { + db.users.belongsToMany(db.permissions, { + as: 'custom_permissions', + foreignKey: { + name: 'users_custom_permissionsId', + }, + constraints: false, + through: 'usersCustom_permissionsPermissions', + }); + + db.users.belongsToMany(db.permissions, { + as: 'custom_permissions_filter', + foreignKey: { + name: 'users_custom_permissionsId', + }, + constraints: false, + through: 'usersCustom_permissionsPermissions', + }); + + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.users.hasMany(db.audit_logs, { + as: 'audit_logs_user', + foreignKey: { + name: 'userId', + }, + constraints: false, + }); + + //end loop + + db.users.belongsTo(db.roles, { + as: 'app_role', + foreignKey: { + name: 'app_roleId', + }, + constraints: false, + }); + + db.users.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.users.hasMany(db.file, { + as: 'avatar', + foreignKey: 'belongsToId', + constraints: false, + scope: { + belongsTo: db.users.getTableName(), + belongsToColumn: 'avatar', + }, + }); + + db.users.belongsTo(db.users, { + as: 'createdBy', + }); + + db.users.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + users.beforeCreate((users, options) => { + users = trimStringFields(users); + + if ( + users.provider !== providers.LOCAL && + Object.values(providers).indexOf(users.provider) > -1 + ) { + users.emailVerified = true; + + if (!users.password) { + const password = crypto.randomBytes(20).toString('hex'); + + const hashedPassword = bcrypt.hashSync( + password, + config.bcrypt.saltRounds, + ); + + users.password = hashedPassword; + } + } + }); + + users.beforeUpdate((users, options) => { + users = trimStringFields(users); + }); + + return users; +}; + +function trimStringFields(users) { + users.email = users.email.trim(); + + users.firstName = users.firstName ? users.firstName.trim() : null; + + users.lastName = users.lastName ? users.lastName.trim() : null; + + return users; +} diff --git a/backend/src/db/reset.js b/backend/src/db/reset.js new file mode 100644 index 0000000..bc0b5f9 --- /dev/null +++ b/backend/src/db/reset.js @@ -0,0 +1,16 @@ +const db = require('./models'); +const { execSync } = require('child_process'); + +console.log('Resetting Database'); + +db.sequelize + .sync({ force: true }) + .then(() => { + execSync('sequelize db:seed:all'); + console.log('OK'); + process.exit(); + }) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/backend/src/db/seeders/20200430130759-admin-user.js b/backend/src/db/seeders/20200430130759-admin-user.js new file mode 100644 index 0000000..b60808f --- /dev/null +++ b/backend/src/db/seeders/20200430130759-admin-user.js @@ -0,0 +1,84 @@ +'use strict'; +const bcrypt = require('bcrypt'); +const config = require('../../config'); + +const ids = [ + '193bf4b5-9f07-4bd5-9a43-e7e41f3e96af', + 'af5a87be-8f9c-4630-902a-37a60b7005ba', + '5bc531ab-611f-41f3-9373-b7cc5d09c93d', + 'ab4cf9bf-4eef-4107-b73d-9d0274cf69bc', +]; + +module.exports = { + up: async (queryInterface, Sequelize) => { + let admin_hash = bcrypt.hashSync( + config.admin_pass, + config.bcrypt.saltRounds, + ); + let user_hash = bcrypt.hashSync(config.user_pass, config.bcrypt.saltRounds); + + try { + await queryInterface.bulkInsert('users', [ + { + id: ids[0], + firstName: 'Admin', + email: config.admin_email, + emailVerified: true, + provider: config.providers.LOCAL, + password: admin_hash, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: ids[1], + firstName: 'John', + email: 'john@doe.com', + emailVerified: true, + provider: config.providers.LOCAL, + password: user_hash, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: ids[2], + firstName: 'Client', + email: 'client@hello.com', + emailVerified: true, + provider: config.providers.LOCAL, + password: user_hash, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: ids[3], + firstName: 'Super Admin', + email: 'super_admin@flatlogic.com', + emailVerified: true, + provider: config.providers.LOCAL, + password: admin_hash, + createdAt: new Date(), + updatedAt: new Date(), + }, + ]); + } catch (error) { + console.error('Error during bulkInsert:', error); + throw error; + } + }, + down: async (queryInterface, Sequelize) => { + try { + await queryInterface.bulkDelete( + 'users', + { + id: { + [Sequelize.Op.in]: ids, + }, + }, + {}, + ); + } catch (error) { + console.error('Error during bulkDelete:', error); + throw error; + } + }, +}; diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js new file mode 100644 index 0000000..a87cecc --- /dev/null +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -0,0 +1,3131 @@ +const { v4: uuid } = require('uuid'); + +module.exports = { + /** + * @param{import("sequelize").QueryInterface} queryInterface + * @return {Promise} + */ + async up(queryInterface) { + const createdAt = new Date(); + const updatedAt = new Date(); + + /** @type {Map} */ + const idMap = new Map(); + + /** + * @param {string} key + * @return {string} + */ + function getId(key) { + if (idMap.has(key)) { + return idMap.get(key); + } + const id = uuid(); + idMap.set(key, id); + return id; + } + + await queryInterface.bulkInsert('roles', [ + { + id: getId('SuperAdmin'), + name: 'Super Administrator', + createdAt, + updatedAt, + }, + + { + id: getId('Administrator'), + name: 'Administrator', + createdAt, + updatedAt, + }, + + { + id: getId('ChiefExecutiveOfficer'), + name: 'Chief Executive Officer', + createdAt, + updatedAt, + }, + + { + id: getId('FinanceManager'), + name: 'Finance Manager', + createdAt, + updatedAt, + }, + + { + id: getId('HumanResourcesManager'), + name: 'Human Resources Manager', + createdAt, + updatedAt, + }, + + { + id: getId('OperationsManager'), + name: 'Operations Manager', + createdAt, + updatedAt, + }, + + { id: getId('DataViewer'), name: 'Data Viewer', createdAt, updatedAt }, + + { id: getId('Public'), name: 'Public', createdAt, updatedAt }, + ]); + + /** + * @param {string} name + */ + function createPermissions(name) { + return [ + { + id: getId(`CREATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `CREATE_${name.toUpperCase()}`, + }, + { + id: getId(`READ_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `READ_${name.toUpperCase()}`, + }, + { + id: getId(`UPDATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `UPDATE_${name.toUpperCase()}`, + }, + { + id: getId(`DELETE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `DELETE_${name.toUpperCase()}`, + }, + ]; + } + + const entities = [ + 'users', + 'audit_logs', + 'data_connections', + 'fin_accounts', + 'fin_ap', + 'fin_ar', + 'fin_budgets', + 'fin_customers', + 'fin_forecasts', + 'fin_transactions', + 'fin_vendors', + 'hr_applicants', + 'hr_attendance', + 'hr_employees', + 'hr_payroll', + 'hr_positions', + 'hr_reqs', + 'ops_incidents', + 'ops_inventory', + 'ops_lines', + 'ops_orders', + 'ops_plants', + 'ops_production_log', + 'ops_shipments', + 'roles', + 'permissions', + 'organizations', + , + ]; + await queryInterface.bulkInsert( + 'permissions', + entities.flatMap(createPermissions), + ); + await queryInterface.bulkInsert('permissions', [ + { + id: getId(`READ_API_DOCS`), + createdAt, + updatedAt, + name: `READ_API_DOCS`, + }, + ]); + await queryInterface.bulkInsert('permissions', [ + { + id: getId(`CREATE_SEARCH`), + createdAt, + updatedAt, + name: `CREATE_SEARCH`, + }, + ]); + + await queryInterface.bulkUpdate( + 'roles', + { globalAccess: true }, + { id: getId('SuperAdmin') }, + ); + + await queryInterface.sequelize + .query(`create table "rolesPermissionsPermissions" +( +"createdAt" timestamp with time zone not null, +"updatedAt" timestamp with time zone not null, +"roles_permissionsId" uuid not null, +"permissionId" uuid not null, +primary key ("roles_permissionsId", "permissionId") +);`); + + await queryInterface.bulkInsert('rolesPermissionsPermissions', [ + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_AUDIT_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_AUDIT_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_AUDIT_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_AUDIT_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_AUDIT_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_AUDIT_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_DATA_CONNECTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_DATA_CONNECTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_DATA_CONNECTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_DATA_CONNECTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_DATA_CONNECTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_DATA_CONNECTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_FIN_ACCOUNTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_FIN_ACCOUNTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_FIN_ACCOUNTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_FIN_ACCOUNTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('CREATE_FIN_ACCOUNTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('READ_FIN_ACCOUNTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('UPDATE_FIN_ACCOUNTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('DELETE_FIN_ACCOUNTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_FIN_ACCOUNTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_FIN_ACCOUNTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_FIN_AP'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_FIN_AP'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_FIN_AP'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_FIN_AP'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('CREATE_FIN_AP'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('READ_FIN_AP'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('UPDATE_FIN_AP'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('DELETE_FIN_AP'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_FIN_AP'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_FIN_AP'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_FIN_AR'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_FIN_AR'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_FIN_AR'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_FIN_AR'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('CREATE_FIN_AR'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('READ_FIN_AR'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('UPDATE_FIN_AR'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('DELETE_FIN_AR'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_FIN_AR'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_FIN_AR'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_FIN_BUDGETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_FIN_BUDGETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_FIN_BUDGETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_FIN_BUDGETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('CREATE_FIN_BUDGETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('READ_FIN_BUDGETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('UPDATE_FIN_BUDGETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('DELETE_FIN_BUDGETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_FIN_BUDGETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_FIN_BUDGETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_FIN_CUSTOMERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_FIN_CUSTOMERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_FIN_CUSTOMERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_FIN_CUSTOMERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('CREATE_FIN_CUSTOMERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('READ_FIN_CUSTOMERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('UPDATE_FIN_CUSTOMERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('DELETE_FIN_CUSTOMERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_FIN_CUSTOMERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_FIN_CUSTOMERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_FIN_FORECASTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_FIN_FORECASTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_FIN_FORECASTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_FIN_FORECASTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('CREATE_FIN_FORECASTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('READ_FIN_FORECASTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('UPDATE_FIN_FORECASTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('DELETE_FIN_FORECASTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_FIN_FORECASTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_FIN_FORECASTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_FIN_TRANSACTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_FIN_TRANSACTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_FIN_TRANSACTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_FIN_TRANSACTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('CREATE_FIN_TRANSACTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('READ_FIN_TRANSACTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('UPDATE_FIN_TRANSACTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('DELETE_FIN_TRANSACTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_FIN_TRANSACTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_FIN_TRANSACTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_FIN_VENDORS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_FIN_VENDORS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_FIN_VENDORS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_FIN_VENDORS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('CREATE_FIN_VENDORS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('READ_FIN_VENDORS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('UPDATE_FIN_VENDORS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('DELETE_FIN_VENDORS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_FIN_VENDORS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_FIN_VENDORS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_HR_APPLICANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_HR_APPLICANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_HR_APPLICANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_HR_APPLICANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('CREATE_HR_APPLICANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('READ_HR_APPLICANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('UPDATE_HR_APPLICANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('DELETE_HR_APPLICANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_HR_APPLICANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_HR_APPLICANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_HR_ATTENDANCE'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_HR_ATTENDANCE'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_HR_ATTENDANCE'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_HR_ATTENDANCE'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('CREATE_HR_ATTENDANCE'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('READ_HR_ATTENDANCE'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('UPDATE_HR_ATTENDANCE'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('DELETE_HR_ATTENDANCE'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_HR_ATTENDANCE'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_HR_ATTENDANCE'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_HR_EMPLOYEES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_HR_EMPLOYEES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_HR_EMPLOYEES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_HR_EMPLOYEES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('CREATE_HR_EMPLOYEES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('READ_HR_EMPLOYEES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('UPDATE_HR_EMPLOYEES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('DELETE_HR_EMPLOYEES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_HR_EMPLOYEES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_HR_EMPLOYEES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_HR_PAYROLL'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_HR_PAYROLL'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_HR_PAYROLL'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_HR_PAYROLL'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('CREATE_HR_PAYROLL'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('READ_HR_PAYROLL'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('UPDATE_HR_PAYROLL'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('DELETE_HR_PAYROLL'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_HR_PAYROLL'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_HR_PAYROLL'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_HR_POSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_HR_POSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_HR_POSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_HR_POSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('CREATE_HR_POSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('READ_HR_POSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('UPDATE_HR_POSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('DELETE_HR_POSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_HR_POSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_HR_POSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_HR_REQS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_HR_REQS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_HR_REQS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_HR_REQS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('CREATE_HR_REQS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('READ_HR_REQS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('UPDATE_HR_REQS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('DELETE_HR_REQS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_HR_REQS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_HR_REQS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_OPS_INCIDENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_OPS_INCIDENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_OPS_INCIDENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_OPS_INCIDENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('CREATE_OPS_INCIDENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('READ_OPS_INCIDENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('UPDATE_OPS_INCIDENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('DELETE_OPS_INCIDENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_OPS_INCIDENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_OPS_INCIDENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_OPS_INVENTORY'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_OPS_INVENTORY'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_OPS_INVENTORY'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_OPS_INVENTORY'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('CREATE_OPS_INVENTORY'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('READ_OPS_INVENTORY'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('UPDATE_OPS_INVENTORY'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('DELETE_OPS_INVENTORY'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_OPS_INVENTORY'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_OPS_INVENTORY'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_OPS_LINES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_OPS_LINES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_OPS_LINES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_OPS_LINES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('CREATE_OPS_LINES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('READ_OPS_LINES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('UPDATE_OPS_LINES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('DELETE_OPS_LINES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_OPS_LINES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_OPS_LINES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_OPS_ORDERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_OPS_ORDERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_OPS_ORDERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_OPS_ORDERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('CREATE_OPS_ORDERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('READ_OPS_ORDERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('UPDATE_OPS_ORDERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('DELETE_OPS_ORDERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_OPS_ORDERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_OPS_ORDERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_OPS_PLANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_OPS_PLANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_OPS_PLANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_OPS_PLANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('CREATE_OPS_PLANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('READ_OPS_PLANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('UPDATE_OPS_PLANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('DELETE_OPS_PLANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_OPS_PLANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_OPS_PLANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_OPS_PRODUCTION_LOG'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_OPS_PRODUCTION_LOG'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_OPS_PRODUCTION_LOG'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_OPS_PRODUCTION_LOG'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('CREATE_OPS_PRODUCTION_LOG'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('READ_OPS_PRODUCTION_LOG'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('UPDATE_OPS_PRODUCTION_LOG'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('DELETE_OPS_PRODUCTION_LOG'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_OPS_PRODUCTION_LOG'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_OPS_PRODUCTION_LOG'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_OPS_SHIPMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('READ_OPS_SHIPMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('UPDATE_OPS_SHIPMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('DELETE_OPS_SHIPMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('CREATE_OPS_SHIPMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('READ_OPS_SHIPMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('UPDATE_OPS_SHIPMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('DELETE_OPS_SHIPMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('READ_OPS_SHIPMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('UPDATE_OPS_SHIPMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ChiefExecutiveOfficer'), + permissionId: getId('CREATE_SEARCH'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('FinanceManager'), + permissionId: getId('CREATE_SEARCH'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('HumanResourcesManager'), + permissionId: getId('CREATE_SEARCH'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('OperationsManager'), + permissionId: getId('CREATE_SEARCH'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('DataViewer'), + permissionId: getId('CREATE_SEARCH'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_USERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_USERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_USERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_AUDIT_LOGS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_AUDIT_LOGS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_AUDIT_LOGS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_AUDIT_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_DATA_CONNECTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_DATA_CONNECTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_DATA_CONNECTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_DATA_CONNECTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_FIN_ACCOUNTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_FIN_ACCOUNTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_FIN_ACCOUNTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_FIN_ACCOUNTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_FIN_AP'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_FIN_AP'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_FIN_AP'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_FIN_AP'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_FIN_AR'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_FIN_AR'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_FIN_AR'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_FIN_AR'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_FIN_BUDGETS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_FIN_BUDGETS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_FIN_BUDGETS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_FIN_BUDGETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_FIN_CUSTOMERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_FIN_CUSTOMERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_FIN_CUSTOMERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_FIN_CUSTOMERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_FIN_FORECASTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_FIN_FORECASTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_FIN_FORECASTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_FIN_FORECASTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_FIN_TRANSACTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_FIN_TRANSACTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_FIN_TRANSACTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_FIN_TRANSACTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_FIN_VENDORS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_FIN_VENDORS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_FIN_VENDORS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_FIN_VENDORS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_HR_APPLICANTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_HR_APPLICANTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_HR_APPLICANTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_HR_APPLICANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_HR_ATTENDANCE'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_HR_ATTENDANCE'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_HR_ATTENDANCE'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_HR_ATTENDANCE'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_HR_EMPLOYEES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_HR_EMPLOYEES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_HR_EMPLOYEES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_HR_EMPLOYEES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_HR_PAYROLL'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_HR_PAYROLL'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_HR_PAYROLL'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_HR_PAYROLL'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_HR_POSITIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_HR_POSITIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_HR_POSITIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_HR_POSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_HR_REQS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_HR_REQS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_HR_REQS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_HR_REQS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_OPS_INCIDENTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_OPS_INCIDENTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_OPS_INCIDENTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_OPS_INCIDENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_OPS_INVENTORY'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_OPS_INVENTORY'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_OPS_INVENTORY'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_OPS_INVENTORY'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_OPS_LINES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_OPS_LINES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_OPS_LINES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_OPS_LINES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_OPS_ORDERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_OPS_ORDERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_OPS_ORDERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_OPS_ORDERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_OPS_PLANTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_OPS_PLANTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_OPS_PLANTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_OPS_PLANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_OPS_PRODUCTION_LOG'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_OPS_PRODUCTION_LOG'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_OPS_PRODUCTION_LOG'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_OPS_PRODUCTION_LOG'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_OPS_SHIPMENTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_OPS_SHIPMENTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_OPS_SHIPMENTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_OPS_SHIPMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_USERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_USERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_USERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_AUDIT_LOGS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_AUDIT_LOGS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_AUDIT_LOGS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_AUDIT_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_DATA_CONNECTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_DATA_CONNECTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_DATA_CONNECTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_DATA_CONNECTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_FIN_ACCOUNTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_FIN_ACCOUNTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_FIN_ACCOUNTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_FIN_ACCOUNTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_FIN_AP'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_FIN_AP'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_FIN_AP'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_FIN_AP'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_FIN_AR'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_FIN_AR'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_FIN_AR'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_FIN_AR'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_FIN_BUDGETS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_FIN_BUDGETS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_FIN_BUDGETS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_FIN_BUDGETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_FIN_CUSTOMERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_FIN_CUSTOMERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_FIN_CUSTOMERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_FIN_CUSTOMERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_FIN_FORECASTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_FIN_FORECASTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_FIN_FORECASTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_FIN_FORECASTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_FIN_TRANSACTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_FIN_TRANSACTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_FIN_TRANSACTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_FIN_TRANSACTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_FIN_VENDORS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_FIN_VENDORS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_FIN_VENDORS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_FIN_VENDORS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_HR_APPLICANTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_HR_APPLICANTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_HR_APPLICANTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_HR_APPLICANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_HR_ATTENDANCE'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_HR_ATTENDANCE'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_HR_ATTENDANCE'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_HR_ATTENDANCE'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_HR_EMPLOYEES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_HR_EMPLOYEES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_HR_EMPLOYEES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_HR_EMPLOYEES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_HR_PAYROLL'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_HR_PAYROLL'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_HR_PAYROLL'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_HR_PAYROLL'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_HR_POSITIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_HR_POSITIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_HR_POSITIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_HR_POSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_HR_REQS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_HR_REQS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_HR_REQS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_HR_REQS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_OPS_INCIDENTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_OPS_INCIDENTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_OPS_INCIDENTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_OPS_INCIDENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_OPS_INVENTORY'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_OPS_INVENTORY'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_OPS_INVENTORY'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_OPS_INVENTORY'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_OPS_LINES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_OPS_LINES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_OPS_LINES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_OPS_LINES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_OPS_ORDERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_OPS_ORDERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_OPS_ORDERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_OPS_ORDERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_OPS_PLANTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_OPS_PLANTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_OPS_PLANTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_OPS_PLANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_OPS_PRODUCTION_LOG'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_OPS_PRODUCTION_LOG'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_OPS_PRODUCTION_LOG'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_OPS_PRODUCTION_LOG'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_OPS_SHIPMENTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_OPS_SHIPMENTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_OPS_SHIPMENTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_OPS_SHIPMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_ROLES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_ROLES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_ROLES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_ROLES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_PERMISSIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_PERMISSIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_PERMISSIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_PERMISSIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_ORGANIZATIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_ORGANIZATIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_ORGANIZATIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_ORGANIZATIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_API_DOCS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_SEARCH'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_API_DOCS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_SEARCH'), + }, + ]); + + await queryInterface.sequelize.query( + `UPDATE "users" SET "app_roleId"='${getId( + 'SuperAdmin', + )}' WHERE "email"='super_admin@flatlogic.com'`, + ); + await queryInterface.sequelize.query( + `UPDATE "users" SET "app_roleId"='${getId( + 'Administrator', + )}' WHERE "email"='admin@flatlogic.com'`, + ); + + await queryInterface.sequelize.query( + `UPDATE "users" SET "app_roleId"='${getId( + 'ChiefExecutiveOfficer', + )}' WHERE "email"='client@hello.com'`, + ); + await queryInterface.sequelize.query( + `UPDATE "users" SET "app_roleId"='${getId( + 'FinanceManager', + )}' WHERE "email"='john@doe.com'`, + ); + }, +}; diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js new file mode 100644 index 0000000..6383b9e --- /dev/null +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -0,0 +1,6042 @@ +const db = require('../models'); +const Users = db.users; + +const AuditLogs = db.audit_logs; + +const DataConnections = db.data_connections; + +const FinAccounts = db.fin_accounts; + +const FinAp = db.fin_ap; + +const FinAr = db.fin_ar; + +const FinBudgets = db.fin_budgets; + +const FinCustomers = db.fin_customers; + +const FinForecasts = db.fin_forecasts; + +const FinTransactions = db.fin_transactions; + +const FinVendors = db.fin_vendors; + +const HrApplicants = db.hr_applicants; + +const HrAttendance = db.hr_attendance; + +const HrEmployees = db.hr_employees; + +const HrPayroll = db.hr_payroll; + +const HrPositions = db.hr_positions; + +const HrReqs = db.hr_reqs; + +const OpsIncidents = db.ops_incidents; + +const OpsInventory = db.ops_inventory; + +const OpsLines = db.ops_lines; + +const OpsOrders = db.ops_orders; + +const OpsPlants = db.ops_plants; + +const OpsProductionLog = db.ops_production_log; + +const OpsShipments = db.ops_shipments; + +const Organizations = db.organizations; + +const AuditLogsData = [ + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + action: 'Update', + + entity: 'fin_accounts', + + entity_id: 1, + + diff_json: '{name:Updated Account Name}', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + action: 'Create', + + entity: 'fin_transactions', + + entity_id: 2, + + diff_json: '{amount:1000}', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + action: 'Delete', + + entity: 'hr_employees', + + entity_id: 3, + + diff_json: '{status:Inactive}', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + action: 'Read', + + entity: 'ops_orders', + + entity_id: 4, + + diff_json: '{}', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + action: 'Create', + + entity: 'fin_budgets', + + entity_id: 5, + + diff_json: '{amount:5000}', + + // type code here for "relation_one" field + }, +]; + +const DataConnectionsData = [ + { + // type code here for "relation_one" field + + type: 'Snowflake', + + name: 'Tech Innovators Snowflake', + + config_json: + '{account:techinnovators,warehouse:COMPUTE_WH,database:FINANCE_DB,schema:PUBLIC}', + + status: 'Inactive', + + last_sync_at: new Date('2023-10-01T12:00:00Z'), + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + type: 'Redshift', + + name: 'Global Solutions BigQuery', + + config_json: '{projectId:global-solutions,dataset:finance_data}', + + status: 'Active', + + last_sync_at: new Date('2023-10-02T15:00:00Z'), + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + type: 'Postgres', + + name: 'Green Energy Redshift', + + config_json: '{cluster:green-energy,database:ops_db,schema:public}', + + status: 'Active', + + last_sync_at: new Date('2023-09-30T09:00:00Z'), + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + type: 'Snowflake', + + name: 'HealthCare Partners Postgres', + + config_json: '{host:db.healthcarepartners.com,database:hr_db,user:admin}', + + status: 'Active', + + last_sync_at: new Date('2023-10-03T08:00:00Z'), + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + type: 'Snowflake', + + name: 'EduTech Snowflake', + + config_json: + '{account:edutech,warehouse:EDU_WH,database:EDU_DB,schema:PUBLIC}', + + status: 'Active', + + last_sync_at: new Date('2023-10-01T10:00:00Z'), + + // type code here for "relation_one" field + }, +]; + +const FinAccountsData = [ + { + // type code here for "relation_one" field + + code: 'Thomas Hunt Morgan', + + name: 'Isaac Newton', + + type: 'revenue', + + // type code here for "relation_one" field + + is_active: false, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + code: 'Anton van Leeuwenhoek', + + name: 'Arthur Eddington', + + type: 'liability', + + // type code here for "relation_one" field + + is_active: false, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + code: 'J. Robert Oppenheimer', + + name: 'Neils Bohr', + + type: 'revenue', + + // type code here for "relation_one" field + + is_active: false, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + code: 'Max Planck', + + name: 'Emil Kraepelin', + + type: 'asset', + + // type code here for "relation_one" field + + is_active: false, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + code: 'Rudolf Virchow', + + name: 'Sheldon Glashow', + + type: 'asset', + + // type code here for "relation_one" field + + is_active: false, + + // type code here for "relation_one" field + }, +]; + +const FinApData = [ + { + // type code here for "relation_one" field + + bill_no: 'Albrecht von Haller', + + // type code here for "relation_one" field + + due_date: new Date(Date.now()), + + amount: 72.65, + + status: 'Paid', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + bill_no: 'Konrad Lorenz', + + // type code here for "relation_one" field + + due_date: new Date(Date.now()), + + amount: 24.51, + + status: 'Pending', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + bill_no: 'Edward Teller', + + // type code here for "relation_one" field + + due_date: new Date(Date.now()), + + amount: 13.89, + + status: 'Pending', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + bill_no: 'James Clerk Maxwell', + + // type code here for "relation_one" field + + due_date: new Date(Date.now()), + + amount: 72.89, + + status: 'Overdue', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + bill_no: 'James Watson', + + // type code here for "relation_one" field + + due_date: new Date(Date.now()), + + amount: 36.33, + + status: 'Pending', + + // type code here for "relation_one" field + }, +]; + +const FinArData = [ + { + // type code here for "relation_one" field + + invoice_no: 'Johannes Kepler', + + // type code here for "relation_one" field + + due_date: new Date(Date.now()), + + amount: 23.51, + + status: 'Overdue', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + invoice_no: 'Max Born', + + // type code here for "relation_one" field + + due_date: new Date(Date.now()), + + amount: 15.13, + + status: 'Paid', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + invoice_no: 'Theodosius Dobzhansky', + + // type code here for "relation_one" field + + due_date: new Date(Date.now()), + + amount: 27.43, + + status: 'Overdue', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + invoice_no: 'Ernst Mayr', + + // type code here for "relation_one" field + + due_date: new Date(Date.now()), + + amount: 24.91, + + status: 'Pending', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + invoice_no: 'Andreas Vesalius', + + // type code here for "relation_one" field + + due_date: new Date(Date.now()), + + amount: 97.02, + + status: 'Pending', + + // type code here for "relation_one" field + }, +]; + +const FinBudgetsData = [ + { + // type code here for "relation_one" field + + fiscal_year: 6, + + cost_center: 'J. Robert Oppenheimer', + + // type code here for "relation_one" field + + month: 8, + + amount: 15.58, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + fiscal_year: 1, + + cost_center: 'J. Robert Oppenheimer', + + // type code here for "relation_one" field + + month: 9, + + amount: 85.76, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + fiscal_year: 5, + + cost_center: 'Albert Einstein', + + // type code here for "relation_one" field + + month: 8, + + amount: 36.96, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + fiscal_year: 5, + + cost_center: 'George Gaylord Simpson', + + // type code here for "relation_one" field + + month: 3, + + amount: 66.23, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + fiscal_year: 5, + + cost_center: 'Robert Koch', + + // type code here for "relation_one" field + + month: 9, + + amount: 89.24, + + // type code here for "relation_one" field + }, +]; + +const FinCustomersData = [ + { + // type code here for "relation_one" field + + name: 'Charles Lyell', + + segment: 'Willard Libby', + + country: 'Erwin Schrodinger', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'Justus Liebig', + + segment: 'Marie Curie', + + country: 'Thomas Hunt Morgan', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'George Gaylord Simpson', + + segment: 'Justus Liebig', + + country: 'Nicolaus Copernicus', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'Hermann von Helmholtz', + + segment: 'Thomas Hunt Morgan', + + country: 'Claude Bernard', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'Trofim Lysenko', + + segment: 'Hans Selye', + + country: 'Francis Crick', + + // type code here for "relation_one" field + }, +]; + +const FinForecastsData = [ + { + // type code here for "relation_one" field + + fiscal_year: 3, + + cost_center: 'John Bardeen', + + // type code here for "relation_one" field + + month: 7, + + amount: 20.71, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + fiscal_year: 2, + + cost_center: 'Max von Laue', + + // type code here for "relation_one" field + + month: 8, + + amount: 41.21, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + fiscal_year: 1, + + cost_center: 'Alfred Kinsey', + + // type code here for "relation_one" field + + month: 4, + + amount: 62.52, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + fiscal_year: 2, + + cost_center: 'Comte de Buffon', + + // type code here for "relation_one" field + + month: 3, + + amount: 27.25, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + fiscal_year: 9, + + cost_center: 'Archimedes', + + // type code here for "relation_one" field + + month: 9, + + amount: 61.07, + + // type code here for "relation_one" field + }, +]; + +const FinTransactionsData = [ + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + tx_ts: new Date(Date.now()), + + amount: 14.59, + + currency: 'Rudolf Virchow', + + fx_rate: 62.16, + + cost_center: 'Linus Pauling', + + project_code: 'Emil Fischer', + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + memo: 'Willard Libby', + + source: 'Anton van Leeuwenhoek', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + tx_ts: new Date(Date.now()), + + amount: 85.49, + + currency: 'Charles Sherrington', + + fx_rate: 69.12, + + cost_center: 'Edward Teller', + + project_code: 'Robert Koch', + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + memo: 'Theodosius Dobzhansky', + + source: 'Paul Ehrlich', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + tx_ts: new Date(Date.now()), + + amount: 71.36, + + currency: 'Ludwig Boltzmann', + + fx_rate: 41.98, + + cost_center: 'Trofim Lysenko', + + project_code: 'Edward O. Wilson', + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + memo: 'Frederick Sanger', + + source: 'Neils Bohr', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + tx_ts: new Date(Date.now()), + + amount: 37.24, + + currency: 'Max Planck', + + fx_rate: 29.02, + + cost_center: 'Charles Darwin', + + project_code: 'Dmitri Mendeleev', + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + memo: 'Theodosius Dobzhansky', + + source: 'Erwin Schrodinger', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + tx_ts: new Date(Date.now()), + + amount: 88.52, + + currency: 'Alfred Kinsey', + + fx_rate: 49.25, + + cost_center: 'Lucretius', + + project_code: 'Claude Levi-Strauss', + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + memo: 'Hans Selye', + + source: 'Noam Chomsky', + + // type code here for "relation_one" field + }, +]; + +const FinVendorsData = [ + { + // type code here for "relation_one" field + + name: 'Max Born', + + tax_id: 'Charles Darwin', + + country: 'Johannes Kepler', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'Michael Faraday', + + tax_id: 'Tycho Brahe', + + country: 'Alfred Kinsey', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'Erwin Schrodinger', + + tax_id: 'Emil Fischer', + + country: 'Hermann von Helmholtz', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'Paul Dirac', + + tax_id: 'Emil Fischer', + + country: 'Albrecht von Haller', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'Dmitri Mendeleev', + + tax_id: 'Archimedes', + + country: 'Euclid', + + // type code here for "relation_one" field + }, +]; + +const HrApplicantsData = [ + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + name: 'Jean Piaget', + + stage: 'Hired', + + source: 'Charles Darwin', + + offer_extended_at: new Date(Date.now()), + + offer_accepted_at: new Date(Date.now()), + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + name: 'Jean Piaget', + + stage: 'Applied', + + source: 'Paul Dirac', + + offer_extended_at: new Date(Date.now()), + + offer_accepted_at: new Date(Date.now()), + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + name: 'Franz Boas', + + stage: 'Interview', + + source: 'Joseph J. Thomson', + + offer_extended_at: new Date(Date.now()), + + offer_accepted_at: new Date(Date.now()), + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + name: 'August Kekule', + + stage: 'Applied', + + source: 'William Harvey', + + offer_extended_at: new Date(Date.now()), + + offer_accepted_at: new Date(Date.now()), + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + name: 'Karl Landsteiner', + + stage: 'Offer', + + source: 'Richard Feynman', + + offer_extended_at: new Date(Date.now()), + + offer_accepted_at: new Date(Date.now()), + + // type code here for "relation_one" field + }, +]; + +const HrAttendanceData = [ + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + date: new Date(Date.now()), + + hours_worked: 44.15, + + leave_type: 'Sick', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + date: new Date(Date.now()), + + hours_worked: 30.52, + + leave_type: 'Unpaid', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + date: new Date(Date.now()), + + hours_worked: 61.36, + + leave_type: 'Sick', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + date: new Date(Date.now()), + + hours_worked: 99.25, + + leave_type: 'Unpaid', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + date: new Date(Date.now()), + + hours_worked: 23.26, + + leave_type: 'Unpaid', + + // type code here for "relation_one" field + }, +]; + +const HrEmployeesData = [ + { + // type code here for "relation_one" field + + employee_no: 'Johannes Kepler', + + name: 'Dmitri Mendeleev', + + email: 'Erwin Schrodinger', + + // type code here for "relation_one" field + + department: 'Galileo Galilei', + + location: 'Thomas Hunt Morgan', + + grade: 'Erwin Schrodinger', + + hire_date: new Date(Date.now()), + + termination_date: new Date(Date.now()), + + status: 'Active', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + employee_no: 'William Herschel', + + name: 'Robert Koch', + + email: 'Erwin Schrodinger', + + // type code here for "relation_one" field + + department: 'Carl Gauss (Karl Friedrich Gauss)', + + location: 'Carl Linnaeus', + + grade: 'Max Planck', + + hire_date: new Date(Date.now()), + + termination_date: new Date(Date.now()), + + status: 'Active', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + employee_no: 'Ernest Rutherford', + + name: 'Leonard Euler', + + email: 'Tycho Brahe', + + // type code here for "relation_one" field + + department: 'Alfred Kinsey', + + location: 'Edward O. Wilson', + + grade: 'Gregor Mendel', + + hire_date: new Date(Date.now()), + + termination_date: new Date(Date.now()), + + status: 'Inactive', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + employee_no: 'Marcello Malpighi', + + name: 'Edwin Hubble', + + email: 'William Herschel', + + // type code here for "relation_one" field + + department: 'Max von Laue', + + location: 'Francis Crick', + + grade: 'Comte de Buffon', + + hire_date: new Date(Date.now()), + + termination_date: new Date(Date.now()), + + status: 'Active', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + employee_no: 'Thomas Hunt Morgan', + + name: 'Karl Landsteiner', + + email: 'Emil Kraepelin', + + // type code here for "relation_one" field + + department: 'Konrad Lorenz', + + location: 'Alfred Kinsey', + + grade: 'Max Born', + + hire_date: new Date(Date.now()), + + termination_date: new Date(Date.now()), + + status: 'Inactive', + + // type code here for "relation_one" field + }, +]; + +const HrPayrollData = [ + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + period: new Date(Date.now()), + + base_pay: 56.86, + + bonus: 51.37, + + overtime_hours: 60.64, + + currency: 'Murray Gell-Mann', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + period: new Date(Date.now()), + + base_pay: 91.09, + + bonus: 43.77, + + overtime_hours: 25.53, + + currency: 'Lucretius', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + period: new Date(Date.now()), + + base_pay: 14.75, + + bonus: 54.31, + + overtime_hours: 47.35, + + currency: 'Ludwig Boltzmann', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + period: new Date(Date.now()), + + base_pay: 17.29, + + bonus: 17.85, + + overtime_hours: 75.73, + + currency: 'Max Planck', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + period: new Date(Date.now()), + + base_pay: 26.53, + + bonus: 13.93, + + overtime_hours: 76.22, + + currency: 'Euclid', + + // type code here for "relation_one" field + }, +]; + +const HrPositionsData = [ + { + // type code here for "relation_one" field + + title: 'B. F. Skinner', + + department: 'Paul Dirac', + + level: 'James Clerk Maxwell', + + location: 'Alfred Binet', + + status: 'Open', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + title: 'Ernst Haeckel', + + department: 'John von Neumann', + + level: 'Alfred Binet', + + location: 'Gregor Mendel', + + status: 'Closed', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + title: 'Arthur Eddington', + + department: 'Willard Libby', + + level: 'Hans Bethe', + + location: 'Ernst Haeckel', + + status: 'Open', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + title: 'Charles Darwin', + + department: 'Werner Heisenberg', + + level: 'Werner Heisenberg', + + location: 'Charles Sherrington', + + status: 'Closed', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + title: 'James Watson', + + department: 'Willard Libby', + + level: 'Hans Bethe', + + location: 'Heike Kamerlingh Onnes', + + status: 'Closed', + + // type code here for "relation_one" field + }, +]; + +const HrReqsData = [ + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + opened_at: new Date(Date.now()), + + filled_at: new Date(Date.now()), + + status: 'Closed', + + recruiter: 'Alfred Wegener', + + priority: 'Medium', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + opened_at: new Date(Date.now()), + + filled_at: new Date(Date.now()), + + status: 'OnHold', + + recruiter: 'Sheldon Glashow', + + priority: 'Low', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + opened_at: new Date(Date.now()), + + filled_at: new Date(Date.now()), + + status: 'OnHold', + + recruiter: 'Carl Gauss (Karl Friedrich Gauss)', + + priority: 'High', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + opened_at: new Date(Date.now()), + + filled_at: new Date(Date.now()), + + status: 'Open', + + recruiter: 'Louis Pasteur', + + priority: 'High', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + opened_at: new Date(Date.now()), + + filled_at: new Date(Date.now()), + + status: 'OnHold', + + recruiter: 'Rudolf Virchow', + + priority: 'Medium', + + // type code here for "relation_one" field + }, +]; + +const OpsIncidentsData = [ + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + occurred_at: new Date(Date.now()), + + category: 'Ernst Mayr', + + severity: 'Low', + + duration_min: 8, + + root_cause: 'Paul Ehrlich', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + occurred_at: new Date(Date.now()), + + category: 'Max Born', + + severity: 'Low', + + duration_min: 1, + + root_cause: 'Max von Laue', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + occurred_at: new Date(Date.now()), + + category: 'James Watson', + + severity: 'Medium', + + duration_min: 7, + + root_cause: 'Hermann von Helmholtz', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + occurred_at: new Date(Date.now()), + + category: 'Gregor Mendel', + + severity: 'High', + + duration_min: 7, + + root_cause: 'Max von Laue', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + occurred_at: new Date(Date.now()), + + category: 'Ludwig Boltzmann', + + severity: 'High', + + duration_min: 4, + + root_cause: 'Justus Liebig', + + // type code here for "relation_one" field + }, +]; + +const OpsInventoryData = [ + { + // type code here for "relation_one" field + + sku: 'Marie Curie', + + location: 'Michael Faraday', + + on_hand: 4, + + on_order: 9, + + safety_stock: 2, + + unit_cost: 27.47, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + sku: 'Francis Crick', + + location: 'Ernest Rutherford', + + on_hand: 5, + + on_order: 5, + + safety_stock: 6, + + unit_cost: 79.67, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + sku: 'Konrad Lorenz', + + location: 'Rudolf Virchow', + + on_hand: 4, + + on_order: 8, + + safety_stock: 4, + + unit_cost: 20.61, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + sku: 'Ernest Rutherford', + + location: 'Frederick Sanger', + + on_hand: 6, + + on_order: 2, + + safety_stock: 9, + + unit_cost: 47.87, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + sku: 'Francis Galton', + + location: 'James Watson', + + on_hand: 1, + + on_order: 2, + + safety_stock: 1, + + unit_cost: 67.78, + + // type code here for "relation_one" field + }, +]; + +const OpsLinesData = [ + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + name: 'Christiaan Huygens', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + name: 'William Herschel', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + name: 'Rudolf Virchow', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + name: 'Isaac Newton', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + name: 'Joseph J. Thomson', + + // type code here for "relation_one" field + }, +]; + +const OpsOrdersData = [ + { + // type code here for "relation_one" field + + order_no: 'Murray Gell-Mann', + + // type code here for "relation_one" field + + due_date: new Date(Date.now()), + + qty: 4, + + status: 'Pending', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + order_no: 'Rudolf Virchow', + + // type code here for "relation_one" field + + due_date: new Date(Date.now()), + + qty: 4, + + status: 'Pending', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + order_no: 'John von Neumann', + + // type code here for "relation_one" field + + due_date: new Date(Date.now()), + + qty: 4, + + status: 'Pending', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + order_no: 'Galileo Galilei', + + // type code here for "relation_one" field + + due_date: new Date(Date.now()), + + qty: 6, + + status: 'Completed', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + order_no: 'Francis Crick', + + // type code here for "relation_one" field + + due_date: new Date(Date.now()), + + qty: 9, + + status: 'Cancelled', + + // type code here for "relation_one" field + }, +]; + +const OpsPlantsData = [ + { + // type code here for "relation_one" field + + name: 'Linus Pauling', + + location: 'Rudolf Virchow', + + timezone: 'Francis Crick', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'Konrad Lorenz', + + location: 'Linus Pauling', + + timezone: 'Rudolf Virchow', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'Nicolaus Copernicus', + + location: 'Marcello Malpighi', + + timezone: 'Sigmund Freud', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'Karl Landsteiner', + + location: 'Rudolf Virchow', + + timezone: 'William Herschel', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'Murray Gell-Mann', + + location: 'Carl Linnaeus', + + timezone: 'Isaac Newton', + + // type code here for "relation_one" field + }, +]; + +const OpsProductionLogData = [ + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + ts: new Date(Date.now()), + + units_produced: 2, + + planned_units: 6, + + downtime_min: 8, + + defects: 9, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + ts: new Date(Date.now()), + + units_produced: 2, + + planned_units: 1, + + downtime_min: 1, + + defects: 4, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + ts: new Date(Date.now()), + + units_produced: 1, + + planned_units: 4, + + downtime_min: 8, + + defects: 6, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + ts: new Date(Date.now()), + + units_produced: 4, + + planned_units: 5, + + downtime_min: 1, + + defects: 7, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + ts: new Date(Date.now()), + + units_produced: 1, + + planned_units: 6, + + downtime_min: 4, + + defects: 6, + + // type code here for "relation_one" field + }, +]; + +const OpsShipmentsData = [ + { + // type code here for "relation_one" field + + order_no: 'Gertrude Belle Elion', + + shipped_at: new Date(Date.now()), + + carrier: 'Arthur Eddington', + + delivered_at: new Date(Date.now()), + + status: 'InTransit', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + order_no: 'Neils Bohr', + + shipped_at: new Date(Date.now()), + + carrier: 'Edward O. Wilson', + + delivered_at: new Date(Date.now()), + + status: 'Delayed', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + order_no: 'Rudolf Virchow', + + shipped_at: new Date(Date.now()), + + carrier: 'Jean Piaget', + + delivered_at: new Date(Date.now()), + + status: 'InTransit', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + order_no: 'Dmitri Mendeleev', + + shipped_at: new Date(Date.now()), + + carrier: 'Arthur Eddington', + + delivered_at: new Date(Date.now()), + + status: 'InTransit', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + order_no: 'Comte de Buffon', + + shipped_at: new Date(Date.now()), + + carrier: 'Erwin Schrodinger', + + delivered_at: new Date(Date.now()), + + status: 'Delivered', + + // type code here for "relation_one" field + }, +]; + +const OrganizationsData = [ + { + name: 'Tech Innovators', + }, + + { + name: 'Global Solutions', + }, + + { + name: 'Green Energy Corp', + }, + + { + name: 'HealthCare Partners', + }, + + { + name: 'EduTech Group', + }, +]; + +// Similar logic for "relation_many" + +async function associateUserWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const User0 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (User0?.setOrganization) { + await User0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const User1 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (User1?.setOrganization) { + await User1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const User2 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (User2?.setOrganization) { + await User2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const User3 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (User3?.setOrganization) { + await User3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const User4 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (User4?.setOrganization) { + await User4.setOrganization(relatedOrganization4); + } +} + +async function associateAuditLogWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const AuditLog0 = await AuditLogs.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (AuditLog0?.setOrg) { + await AuditLog0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const AuditLog1 = await AuditLogs.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (AuditLog1?.setOrg) { + await AuditLog1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const AuditLog2 = await AuditLogs.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (AuditLog2?.setOrg) { + await AuditLog2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const AuditLog3 = await AuditLogs.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (AuditLog3?.setOrg) { + await AuditLog3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const AuditLog4 = await AuditLogs.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (AuditLog4?.setOrg) { + await AuditLog4.setOrg(relatedOrg4); + } +} + +async function associateAuditLogWithUser() { + const relatedUser0 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const AuditLog0 = await AuditLogs.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (AuditLog0?.setUser) { + await AuditLog0.setUser(relatedUser0); + } + + const relatedUser1 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const AuditLog1 = await AuditLogs.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (AuditLog1?.setUser) { + await AuditLog1.setUser(relatedUser1); + } + + const relatedUser2 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const AuditLog2 = await AuditLogs.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (AuditLog2?.setUser) { + await AuditLog2.setUser(relatedUser2); + } + + const relatedUser3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const AuditLog3 = await AuditLogs.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (AuditLog3?.setUser) { + await AuditLog3.setUser(relatedUser3); + } + + const relatedUser4 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const AuditLog4 = await AuditLogs.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (AuditLog4?.setUser) { + await AuditLog4.setUser(relatedUser4); + } +} + +async function associateAuditLogWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const AuditLog0 = await AuditLogs.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (AuditLog0?.setOrganization) { + await AuditLog0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const AuditLog1 = await AuditLogs.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (AuditLog1?.setOrganization) { + await AuditLog1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const AuditLog2 = await AuditLogs.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (AuditLog2?.setOrganization) { + await AuditLog2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const AuditLog3 = await AuditLogs.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (AuditLog3?.setOrganization) { + await AuditLog3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const AuditLog4 = await AuditLogs.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (AuditLog4?.setOrganization) { + await AuditLog4.setOrganization(relatedOrganization4); + } +} + +async function associateDataConnectionWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const DataConnection0 = await DataConnections.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (DataConnection0?.setOrg) { + await DataConnection0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const DataConnection1 = await DataConnections.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (DataConnection1?.setOrg) { + await DataConnection1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const DataConnection2 = await DataConnections.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (DataConnection2?.setOrg) { + await DataConnection2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const DataConnection3 = await DataConnections.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (DataConnection3?.setOrg) { + await DataConnection3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const DataConnection4 = await DataConnections.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (DataConnection4?.setOrg) { + await DataConnection4.setOrg(relatedOrg4); + } +} + +async function associateDataConnectionWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const DataConnection0 = await DataConnections.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (DataConnection0?.setOrganization) { + await DataConnection0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const DataConnection1 = await DataConnections.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (DataConnection1?.setOrganization) { + await DataConnection1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const DataConnection2 = await DataConnections.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (DataConnection2?.setOrganization) { + await DataConnection2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const DataConnection3 = await DataConnections.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (DataConnection3?.setOrganization) { + await DataConnection3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const DataConnection4 = await DataConnections.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (DataConnection4?.setOrganization) { + await DataConnection4.setOrganization(relatedOrganization4); + } +} + +async function associateFinAccountWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAccount0 = await FinAccounts.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinAccount0?.setOrg) { + await FinAccount0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAccount1 = await FinAccounts.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinAccount1?.setOrg) { + await FinAccount1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAccount2 = await FinAccounts.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinAccount2?.setOrg) { + await FinAccount2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAccount3 = await FinAccounts.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinAccount3?.setOrg) { + await FinAccount3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAccount4 = await FinAccounts.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinAccount4?.setOrg) { + await FinAccount4.setOrg(relatedOrg4); + } +} + +async function associateFinAccountWithParent() { + const relatedParent0 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinAccount0 = await FinAccounts.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinAccount0?.setParent) { + await FinAccount0.setParent(relatedParent0); + } + + const relatedParent1 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinAccount1 = await FinAccounts.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinAccount1?.setParent) { + await FinAccount1.setParent(relatedParent1); + } + + const relatedParent2 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinAccount2 = await FinAccounts.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinAccount2?.setParent) { + await FinAccount2.setParent(relatedParent2); + } + + const relatedParent3 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinAccount3 = await FinAccounts.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinAccount3?.setParent) { + await FinAccount3.setParent(relatedParent3); + } + + const relatedParent4 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinAccount4 = await FinAccounts.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinAccount4?.setParent) { + await FinAccount4.setParent(relatedParent4); + } +} + +async function associateFinAccountWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAccount0 = await FinAccounts.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinAccount0?.setOrganization) { + await FinAccount0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAccount1 = await FinAccounts.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinAccount1?.setOrganization) { + await FinAccount1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAccount2 = await FinAccounts.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinAccount2?.setOrganization) { + await FinAccount2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAccount3 = await FinAccounts.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinAccount3?.setOrganization) { + await FinAccount3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAccount4 = await FinAccounts.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinAccount4?.setOrganization) { + await FinAccount4.setOrganization(relatedOrganization4); + } +} + +async function associateFinApWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAp0 = await FinAp.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinAp0?.setOrg) { + await FinAp0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAp1 = await FinAp.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinAp1?.setOrg) { + await FinAp1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAp2 = await FinAp.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinAp2?.setOrg) { + await FinAp2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAp3 = await FinAp.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinAp3?.setOrg) { + await FinAp3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAp4 = await FinAp.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinAp4?.setOrg) { + await FinAp4.setOrg(relatedOrg4); + } +} + +async function associateFinApWithVendor() { + const relatedVendor0 = await FinVendors.findOne({ + offset: Math.floor(Math.random() * (await FinVendors.count())), + }); + const FinAp0 = await FinAp.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinAp0?.setVendor) { + await FinAp0.setVendor(relatedVendor0); + } + + const relatedVendor1 = await FinVendors.findOne({ + offset: Math.floor(Math.random() * (await FinVendors.count())), + }); + const FinAp1 = await FinAp.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinAp1?.setVendor) { + await FinAp1.setVendor(relatedVendor1); + } + + const relatedVendor2 = await FinVendors.findOne({ + offset: Math.floor(Math.random() * (await FinVendors.count())), + }); + const FinAp2 = await FinAp.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinAp2?.setVendor) { + await FinAp2.setVendor(relatedVendor2); + } + + const relatedVendor3 = await FinVendors.findOne({ + offset: Math.floor(Math.random() * (await FinVendors.count())), + }); + const FinAp3 = await FinAp.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinAp3?.setVendor) { + await FinAp3.setVendor(relatedVendor3); + } + + const relatedVendor4 = await FinVendors.findOne({ + offset: Math.floor(Math.random() * (await FinVendors.count())), + }); + const FinAp4 = await FinAp.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinAp4?.setVendor) { + await FinAp4.setVendor(relatedVendor4); + } +} + +async function associateFinApWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAp0 = await FinAp.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinAp0?.setOrganization) { + await FinAp0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAp1 = await FinAp.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinAp1?.setOrganization) { + await FinAp1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAp2 = await FinAp.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinAp2?.setOrganization) { + await FinAp2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAp3 = await FinAp.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinAp3?.setOrganization) { + await FinAp3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAp4 = await FinAp.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinAp4?.setOrganization) { + await FinAp4.setOrganization(relatedOrganization4); + } +} + +async function associateFinArWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAr0 = await FinAr.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinAr0?.setOrg) { + await FinAr0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAr1 = await FinAr.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinAr1?.setOrg) { + await FinAr1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAr2 = await FinAr.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinAr2?.setOrg) { + await FinAr2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAr3 = await FinAr.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinAr3?.setOrg) { + await FinAr3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAr4 = await FinAr.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinAr4?.setOrg) { + await FinAr4.setOrg(relatedOrg4); + } +} + +async function associateFinArWithCustomer() { + const relatedCustomer0 = await FinCustomers.findOne({ + offset: Math.floor(Math.random() * (await FinCustomers.count())), + }); + const FinAr0 = await FinAr.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinAr0?.setCustomer) { + await FinAr0.setCustomer(relatedCustomer0); + } + + const relatedCustomer1 = await FinCustomers.findOne({ + offset: Math.floor(Math.random() * (await FinCustomers.count())), + }); + const FinAr1 = await FinAr.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinAr1?.setCustomer) { + await FinAr1.setCustomer(relatedCustomer1); + } + + const relatedCustomer2 = await FinCustomers.findOne({ + offset: Math.floor(Math.random() * (await FinCustomers.count())), + }); + const FinAr2 = await FinAr.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinAr2?.setCustomer) { + await FinAr2.setCustomer(relatedCustomer2); + } + + const relatedCustomer3 = await FinCustomers.findOne({ + offset: Math.floor(Math.random() * (await FinCustomers.count())), + }); + const FinAr3 = await FinAr.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinAr3?.setCustomer) { + await FinAr3.setCustomer(relatedCustomer3); + } + + const relatedCustomer4 = await FinCustomers.findOne({ + offset: Math.floor(Math.random() * (await FinCustomers.count())), + }); + const FinAr4 = await FinAr.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinAr4?.setCustomer) { + await FinAr4.setCustomer(relatedCustomer4); + } +} + +async function associateFinArWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAr0 = await FinAr.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinAr0?.setOrganization) { + await FinAr0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAr1 = await FinAr.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinAr1?.setOrganization) { + await FinAr1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAr2 = await FinAr.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinAr2?.setOrganization) { + await FinAr2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAr3 = await FinAr.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinAr3?.setOrganization) { + await FinAr3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinAr4 = await FinAr.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinAr4?.setOrganization) { + await FinAr4.setOrganization(relatedOrganization4); + } +} + +async function associateFinBudgetWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinBudget0 = await FinBudgets.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinBudget0?.setOrg) { + await FinBudget0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinBudget1 = await FinBudgets.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinBudget1?.setOrg) { + await FinBudget1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinBudget2 = await FinBudgets.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinBudget2?.setOrg) { + await FinBudget2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinBudget3 = await FinBudgets.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinBudget3?.setOrg) { + await FinBudget3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinBudget4 = await FinBudgets.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinBudget4?.setOrg) { + await FinBudget4.setOrg(relatedOrg4); + } +} + +async function associateFinBudgetWithAccount() { + const relatedAccount0 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinBudget0 = await FinBudgets.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinBudget0?.setAccount) { + await FinBudget0.setAccount(relatedAccount0); + } + + const relatedAccount1 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinBudget1 = await FinBudgets.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinBudget1?.setAccount) { + await FinBudget1.setAccount(relatedAccount1); + } + + const relatedAccount2 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinBudget2 = await FinBudgets.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinBudget2?.setAccount) { + await FinBudget2.setAccount(relatedAccount2); + } + + const relatedAccount3 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinBudget3 = await FinBudgets.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinBudget3?.setAccount) { + await FinBudget3.setAccount(relatedAccount3); + } + + const relatedAccount4 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinBudget4 = await FinBudgets.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinBudget4?.setAccount) { + await FinBudget4.setAccount(relatedAccount4); + } +} + +async function associateFinBudgetWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinBudget0 = await FinBudgets.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinBudget0?.setOrganization) { + await FinBudget0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinBudget1 = await FinBudgets.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinBudget1?.setOrganization) { + await FinBudget1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinBudget2 = await FinBudgets.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinBudget2?.setOrganization) { + await FinBudget2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinBudget3 = await FinBudgets.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinBudget3?.setOrganization) { + await FinBudget3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinBudget4 = await FinBudgets.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinBudget4?.setOrganization) { + await FinBudget4.setOrganization(relatedOrganization4); + } +} + +async function associateFinCustomerWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinCustomer0 = await FinCustomers.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinCustomer0?.setOrg) { + await FinCustomer0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinCustomer1 = await FinCustomers.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinCustomer1?.setOrg) { + await FinCustomer1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinCustomer2 = await FinCustomers.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinCustomer2?.setOrg) { + await FinCustomer2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinCustomer3 = await FinCustomers.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinCustomer3?.setOrg) { + await FinCustomer3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinCustomer4 = await FinCustomers.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinCustomer4?.setOrg) { + await FinCustomer4.setOrg(relatedOrg4); + } +} + +async function associateFinCustomerWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinCustomer0 = await FinCustomers.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinCustomer0?.setOrganization) { + await FinCustomer0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinCustomer1 = await FinCustomers.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinCustomer1?.setOrganization) { + await FinCustomer1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinCustomer2 = await FinCustomers.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinCustomer2?.setOrganization) { + await FinCustomer2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinCustomer3 = await FinCustomers.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinCustomer3?.setOrganization) { + await FinCustomer3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinCustomer4 = await FinCustomers.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinCustomer4?.setOrganization) { + await FinCustomer4.setOrganization(relatedOrganization4); + } +} + +async function associateFinForecastWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinForecast0 = await FinForecasts.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinForecast0?.setOrg) { + await FinForecast0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinForecast1 = await FinForecasts.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinForecast1?.setOrg) { + await FinForecast1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinForecast2 = await FinForecasts.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinForecast2?.setOrg) { + await FinForecast2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinForecast3 = await FinForecasts.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinForecast3?.setOrg) { + await FinForecast3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinForecast4 = await FinForecasts.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinForecast4?.setOrg) { + await FinForecast4.setOrg(relatedOrg4); + } +} + +async function associateFinForecastWithAccount() { + const relatedAccount0 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinForecast0 = await FinForecasts.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinForecast0?.setAccount) { + await FinForecast0.setAccount(relatedAccount0); + } + + const relatedAccount1 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinForecast1 = await FinForecasts.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinForecast1?.setAccount) { + await FinForecast1.setAccount(relatedAccount1); + } + + const relatedAccount2 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinForecast2 = await FinForecasts.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinForecast2?.setAccount) { + await FinForecast2.setAccount(relatedAccount2); + } + + const relatedAccount3 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinForecast3 = await FinForecasts.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinForecast3?.setAccount) { + await FinForecast3.setAccount(relatedAccount3); + } + + const relatedAccount4 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinForecast4 = await FinForecasts.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinForecast4?.setAccount) { + await FinForecast4.setAccount(relatedAccount4); + } +} + +async function associateFinForecastWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinForecast0 = await FinForecasts.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinForecast0?.setOrganization) { + await FinForecast0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinForecast1 = await FinForecasts.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinForecast1?.setOrganization) { + await FinForecast1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinForecast2 = await FinForecasts.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinForecast2?.setOrganization) { + await FinForecast2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinForecast3 = await FinForecasts.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinForecast3?.setOrganization) { + await FinForecast3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinForecast4 = await FinForecasts.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinForecast4?.setOrganization) { + await FinForecast4.setOrganization(relatedOrganization4); + } +} + +async function associateFinTransactionWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinTransaction0 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinTransaction0?.setOrg) { + await FinTransaction0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinTransaction1 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinTransaction1?.setOrg) { + await FinTransaction1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinTransaction2 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinTransaction2?.setOrg) { + await FinTransaction2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinTransaction3 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinTransaction3?.setOrg) { + await FinTransaction3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinTransaction4 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinTransaction4?.setOrg) { + await FinTransaction4.setOrg(relatedOrg4); + } +} + +async function associateFinTransactionWithAccount() { + const relatedAccount0 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinTransaction0 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinTransaction0?.setAccount) { + await FinTransaction0.setAccount(relatedAccount0); + } + + const relatedAccount1 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinTransaction1 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinTransaction1?.setAccount) { + await FinTransaction1.setAccount(relatedAccount1); + } + + const relatedAccount2 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinTransaction2 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinTransaction2?.setAccount) { + await FinTransaction2.setAccount(relatedAccount2); + } + + const relatedAccount3 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinTransaction3 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinTransaction3?.setAccount) { + await FinTransaction3.setAccount(relatedAccount3); + } + + const relatedAccount4 = await FinAccounts.findOne({ + offset: Math.floor(Math.random() * (await FinAccounts.count())), + }); + const FinTransaction4 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinTransaction4?.setAccount) { + await FinTransaction4.setAccount(relatedAccount4); + } +} + +async function associateFinTransactionWithVendor() { + const relatedVendor0 = await FinVendors.findOne({ + offset: Math.floor(Math.random() * (await FinVendors.count())), + }); + const FinTransaction0 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinTransaction0?.setVendor) { + await FinTransaction0.setVendor(relatedVendor0); + } + + const relatedVendor1 = await FinVendors.findOne({ + offset: Math.floor(Math.random() * (await FinVendors.count())), + }); + const FinTransaction1 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinTransaction1?.setVendor) { + await FinTransaction1.setVendor(relatedVendor1); + } + + const relatedVendor2 = await FinVendors.findOne({ + offset: Math.floor(Math.random() * (await FinVendors.count())), + }); + const FinTransaction2 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinTransaction2?.setVendor) { + await FinTransaction2.setVendor(relatedVendor2); + } + + const relatedVendor3 = await FinVendors.findOne({ + offset: Math.floor(Math.random() * (await FinVendors.count())), + }); + const FinTransaction3 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinTransaction3?.setVendor) { + await FinTransaction3.setVendor(relatedVendor3); + } + + const relatedVendor4 = await FinVendors.findOne({ + offset: Math.floor(Math.random() * (await FinVendors.count())), + }); + const FinTransaction4 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinTransaction4?.setVendor) { + await FinTransaction4.setVendor(relatedVendor4); + } +} + +async function associateFinTransactionWithCustomer() { + const relatedCustomer0 = await FinCustomers.findOne({ + offset: Math.floor(Math.random() * (await FinCustomers.count())), + }); + const FinTransaction0 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinTransaction0?.setCustomer) { + await FinTransaction0.setCustomer(relatedCustomer0); + } + + const relatedCustomer1 = await FinCustomers.findOne({ + offset: Math.floor(Math.random() * (await FinCustomers.count())), + }); + const FinTransaction1 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinTransaction1?.setCustomer) { + await FinTransaction1.setCustomer(relatedCustomer1); + } + + const relatedCustomer2 = await FinCustomers.findOne({ + offset: Math.floor(Math.random() * (await FinCustomers.count())), + }); + const FinTransaction2 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinTransaction2?.setCustomer) { + await FinTransaction2.setCustomer(relatedCustomer2); + } + + const relatedCustomer3 = await FinCustomers.findOne({ + offset: Math.floor(Math.random() * (await FinCustomers.count())), + }); + const FinTransaction3 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinTransaction3?.setCustomer) { + await FinTransaction3.setCustomer(relatedCustomer3); + } + + const relatedCustomer4 = await FinCustomers.findOne({ + offset: Math.floor(Math.random() * (await FinCustomers.count())), + }); + const FinTransaction4 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinTransaction4?.setCustomer) { + await FinTransaction4.setCustomer(relatedCustomer4); + } +} + +async function associateFinTransactionWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinTransaction0 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinTransaction0?.setOrganization) { + await FinTransaction0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinTransaction1 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinTransaction1?.setOrganization) { + await FinTransaction1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinTransaction2 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinTransaction2?.setOrganization) { + await FinTransaction2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinTransaction3 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinTransaction3?.setOrganization) { + await FinTransaction3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinTransaction4 = await FinTransactions.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinTransaction4?.setOrganization) { + await FinTransaction4.setOrganization(relatedOrganization4); + } +} + +async function associateFinVendorWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinVendor0 = await FinVendors.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinVendor0?.setOrg) { + await FinVendor0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinVendor1 = await FinVendors.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinVendor1?.setOrg) { + await FinVendor1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinVendor2 = await FinVendors.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinVendor2?.setOrg) { + await FinVendor2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinVendor3 = await FinVendors.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinVendor3?.setOrg) { + await FinVendor3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinVendor4 = await FinVendors.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinVendor4?.setOrg) { + await FinVendor4.setOrg(relatedOrg4); + } +} + +async function associateFinVendorWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinVendor0 = await FinVendors.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (FinVendor0?.setOrganization) { + await FinVendor0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinVendor1 = await FinVendors.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (FinVendor1?.setOrganization) { + await FinVendor1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinVendor2 = await FinVendors.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (FinVendor2?.setOrganization) { + await FinVendor2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinVendor3 = await FinVendors.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (FinVendor3?.setOrganization) { + await FinVendor3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const FinVendor4 = await FinVendors.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (FinVendor4?.setOrganization) { + await FinVendor4.setOrganization(relatedOrganization4); + } +} + +async function associateHrApplicantWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrApplicant0 = await HrApplicants.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (HrApplicant0?.setOrg) { + await HrApplicant0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrApplicant1 = await HrApplicants.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (HrApplicant1?.setOrg) { + await HrApplicant1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrApplicant2 = await HrApplicants.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (HrApplicant2?.setOrg) { + await HrApplicant2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrApplicant3 = await HrApplicants.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (HrApplicant3?.setOrg) { + await HrApplicant3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrApplicant4 = await HrApplicants.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (HrApplicant4?.setOrg) { + await HrApplicant4.setOrg(relatedOrg4); + } +} + +async function associateHrApplicantWithReq() { + const relatedReq0 = await HrReqs.findOne({ + offset: Math.floor(Math.random() * (await HrReqs.count())), + }); + const HrApplicant0 = await HrApplicants.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (HrApplicant0?.setReq) { + await HrApplicant0.setReq(relatedReq0); + } + + const relatedReq1 = await HrReqs.findOne({ + offset: Math.floor(Math.random() * (await HrReqs.count())), + }); + const HrApplicant1 = await HrApplicants.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (HrApplicant1?.setReq) { + await HrApplicant1.setReq(relatedReq1); + } + + const relatedReq2 = await HrReqs.findOne({ + offset: Math.floor(Math.random() * (await HrReqs.count())), + }); + const HrApplicant2 = await HrApplicants.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (HrApplicant2?.setReq) { + await HrApplicant2.setReq(relatedReq2); + } + + const relatedReq3 = await HrReqs.findOne({ + offset: Math.floor(Math.random() * (await HrReqs.count())), + }); + const HrApplicant3 = await HrApplicants.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (HrApplicant3?.setReq) { + await HrApplicant3.setReq(relatedReq3); + } + + const relatedReq4 = await HrReqs.findOne({ + offset: Math.floor(Math.random() * (await HrReqs.count())), + }); + const HrApplicant4 = await HrApplicants.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (HrApplicant4?.setReq) { + await HrApplicant4.setReq(relatedReq4); + } +} + +async function associateHrApplicantWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrApplicant0 = await HrApplicants.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (HrApplicant0?.setOrganization) { + await HrApplicant0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrApplicant1 = await HrApplicants.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (HrApplicant1?.setOrganization) { + await HrApplicant1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrApplicant2 = await HrApplicants.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (HrApplicant2?.setOrganization) { + await HrApplicant2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrApplicant3 = await HrApplicants.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (HrApplicant3?.setOrganization) { + await HrApplicant3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrApplicant4 = await HrApplicants.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (HrApplicant4?.setOrganization) { + await HrApplicant4.setOrganization(relatedOrganization4); + } +} + +async function associateHrAttendanceWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrAttendance0 = await HrAttendance.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (HrAttendance0?.setOrg) { + await HrAttendance0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrAttendance1 = await HrAttendance.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (HrAttendance1?.setOrg) { + await HrAttendance1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrAttendance2 = await HrAttendance.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (HrAttendance2?.setOrg) { + await HrAttendance2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrAttendance3 = await HrAttendance.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (HrAttendance3?.setOrg) { + await HrAttendance3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrAttendance4 = await HrAttendance.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (HrAttendance4?.setOrg) { + await HrAttendance4.setOrg(relatedOrg4); + } +} + +async function associateHrAttendanceWithEmployee() { + const relatedEmployee0 = await HrEmployees.findOne({ + offset: Math.floor(Math.random() * (await HrEmployees.count())), + }); + const HrAttendance0 = await HrAttendance.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (HrAttendance0?.setEmployee) { + await HrAttendance0.setEmployee(relatedEmployee0); + } + + const relatedEmployee1 = await HrEmployees.findOne({ + offset: Math.floor(Math.random() * (await HrEmployees.count())), + }); + const HrAttendance1 = await HrAttendance.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (HrAttendance1?.setEmployee) { + await HrAttendance1.setEmployee(relatedEmployee1); + } + + const relatedEmployee2 = await HrEmployees.findOne({ + offset: Math.floor(Math.random() * (await HrEmployees.count())), + }); + const HrAttendance2 = await HrAttendance.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (HrAttendance2?.setEmployee) { + await HrAttendance2.setEmployee(relatedEmployee2); + } + + const relatedEmployee3 = await HrEmployees.findOne({ + offset: Math.floor(Math.random() * (await HrEmployees.count())), + }); + const HrAttendance3 = await HrAttendance.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (HrAttendance3?.setEmployee) { + await HrAttendance3.setEmployee(relatedEmployee3); + } + + const relatedEmployee4 = await HrEmployees.findOne({ + offset: Math.floor(Math.random() * (await HrEmployees.count())), + }); + const HrAttendance4 = await HrAttendance.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (HrAttendance4?.setEmployee) { + await HrAttendance4.setEmployee(relatedEmployee4); + } +} + +async function associateHrAttendanceWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrAttendance0 = await HrAttendance.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (HrAttendance0?.setOrganization) { + await HrAttendance0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrAttendance1 = await HrAttendance.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (HrAttendance1?.setOrganization) { + await HrAttendance1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrAttendance2 = await HrAttendance.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (HrAttendance2?.setOrganization) { + await HrAttendance2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrAttendance3 = await HrAttendance.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (HrAttendance3?.setOrganization) { + await HrAttendance3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrAttendance4 = await HrAttendance.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (HrAttendance4?.setOrganization) { + await HrAttendance4.setOrganization(relatedOrganization4); + } +} + +async function associateHrEmployeeWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrEmployee0 = await HrEmployees.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (HrEmployee0?.setOrg) { + await HrEmployee0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrEmployee1 = await HrEmployees.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (HrEmployee1?.setOrg) { + await HrEmployee1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrEmployee2 = await HrEmployees.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (HrEmployee2?.setOrg) { + await HrEmployee2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrEmployee3 = await HrEmployees.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (HrEmployee3?.setOrg) { + await HrEmployee3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrEmployee4 = await HrEmployees.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (HrEmployee4?.setOrg) { + await HrEmployee4.setOrg(relatedOrg4); + } +} + +async function associateHrEmployeeWithManager() { + const relatedManager0 = await HrEmployees.findOne({ + offset: Math.floor(Math.random() * (await HrEmployees.count())), + }); + const HrEmployee0 = await HrEmployees.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (HrEmployee0?.setManager) { + await HrEmployee0.setManager(relatedManager0); + } + + const relatedManager1 = await HrEmployees.findOne({ + offset: Math.floor(Math.random() * (await HrEmployees.count())), + }); + const HrEmployee1 = await HrEmployees.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (HrEmployee1?.setManager) { + await HrEmployee1.setManager(relatedManager1); + } + + const relatedManager2 = await HrEmployees.findOne({ + offset: Math.floor(Math.random() * (await HrEmployees.count())), + }); + const HrEmployee2 = await HrEmployees.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (HrEmployee2?.setManager) { + await HrEmployee2.setManager(relatedManager2); + } + + const relatedManager3 = await HrEmployees.findOne({ + offset: Math.floor(Math.random() * (await HrEmployees.count())), + }); + const HrEmployee3 = await HrEmployees.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (HrEmployee3?.setManager) { + await HrEmployee3.setManager(relatedManager3); + } + + const relatedManager4 = await HrEmployees.findOne({ + offset: Math.floor(Math.random() * (await HrEmployees.count())), + }); + const HrEmployee4 = await HrEmployees.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (HrEmployee4?.setManager) { + await HrEmployee4.setManager(relatedManager4); + } +} + +async function associateHrEmployeeWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrEmployee0 = await HrEmployees.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (HrEmployee0?.setOrganization) { + await HrEmployee0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrEmployee1 = await HrEmployees.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (HrEmployee1?.setOrganization) { + await HrEmployee1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrEmployee2 = await HrEmployees.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (HrEmployee2?.setOrganization) { + await HrEmployee2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrEmployee3 = await HrEmployees.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (HrEmployee3?.setOrganization) { + await HrEmployee3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrEmployee4 = await HrEmployees.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (HrEmployee4?.setOrganization) { + await HrEmployee4.setOrganization(relatedOrganization4); + } +} + +async function associateHrPayrollWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPayroll0 = await HrPayroll.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (HrPayroll0?.setOrg) { + await HrPayroll0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPayroll1 = await HrPayroll.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (HrPayroll1?.setOrg) { + await HrPayroll1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPayroll2 = await HrPayroll.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (HrPayroll2?.setOrg) { + await HrPayroll2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPayroll3 = await HrPayroll.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (HrPayroll3?.setOrg) { + await HrPayroll3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPayroll4 = await HrPayroll.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (HrPayroll4?.setOrg) { + await HrPayroll4.setOrg(relatedOrg4); + } +} + +async function associateHrPayrollWithEmployee() { + const relatedEmployee0 = await HrEmployees.findOne({ + offset: Math.floor(Math.random() * (await HrEmployees.count())), + }); + const HrPayroll0 = await HrPayroll.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (HrPayroll0?.setEmployee) { + await HrPayroll0.setEmployee(relatedEmployee0); + } + + const relatedEmployee1 = await HrEmployees.findOne({ + offset: Math.floor(Math.random() * (await HrEmployees.count())), + }); + const HrPayroll1 = await HrPayroll.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (HrPayroll1?.setEmployee) { + await HrPayroll1.setEmployee(relatedEmployee1); + } + + const relatedEmployee2 = await HrEmployees.findOne({ + offset: Math.floor(Math.random() * (await HrEmployees.count())), + }); + const HrPayroll2 = await HrPayroll.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (HrPayroll2?.setEmployee) { + await HrPayroll2.setEmployee(relatedEmployee2); + } + + const relatedEmployee3 = await HrEmployees.findOne({ + offset: Math.floor(Math.random() * (await HrEmployees.count())), + }); + const HrPayroll3 = await HrPayroll.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (HrPayroll3?.setEmployee) { + await HrPayroll3.setEmployee(relatedEmployee3); + } + + const relatedEmployee4 = await HrEmployees.findOne({ + offset: Math.floor(Math.random() * (await HrEmployees.count())), + }); + const HrPayroll4 = await HrPayroll.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (HrPayroll4?.setEmployee) { + await HrPayroll4.setEmployee(relatedEmployee4); + } +} + +async function associateHrPayrollWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPayroll0 = await HrPayroll.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (HrPayroll0?.setOrganization) { + await HrPayroll0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPayroll1 = await HrPayroll.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (HrPayroll1?.setOrganization) { + await HrPayroll1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPayroll2 = await HrPayroll.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (HrPayroll2?.setOrganization) { + await HrPayroll2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPayroll3 = await HrPayroll.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (HrPayroll3?.setOrganization) { + await HrPayroll3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPayroll4 = await HrPayroll.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (HrPayroll4?.setOrganization) { + await HrPayroll4.setOrganization(relatedOrganization4); + } +} + +async function associateHrPositionWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPosition0 = await HrPositions.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (HrPosition0?.setOrg) { + await HrPosition0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPosition1 = await HrPositions.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (HrPosition1?.setOrg) { + await HrPosition1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPosition2 = await HrPositions.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (HrPosition2?.setOrg) { + await HrPosition2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPosition3 = await HrPositions.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (HrPosition3?.setOrg) { + await HrPosition3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPosition4 = await HrPositions.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (HrPosition4?.setOrg) { + await HrPosition4.setOrg(relatedOrg4); + } +} + +async function associateHrPositionWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPosition0 = await HrPositions.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (HrPosition0?.setOrganization) { + await HrPosition0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPosition1 = await HrPositions.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (HrPosition1?.setOrganization) { + await HrPosition1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPosition2 = await HrPositions.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (HrPosition2?.setOrganization) { + await HrPosition2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPosition3 = await HrPositions.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (HrPosition3?.setOrganization) { + await HrPosition3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrPosition4 = await HrPositions.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (HrPosition4?.setOrganization) { + await HrPosition4.setOrganization(relatedOrganization4); + } +} + +async function associateHrReqWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrReq0 = await HrReqs.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (HrReq0?.setOrg) { + await HrReq0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrReq1 = await HrReqs.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (HrReq1?.setOrg) { + await HrReq1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrReq2 = await HrReqs.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (HrReq2?.setOrg) { + await HrReq2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrReq3 = await HrReqs.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (HrReq3?.setOrg) { + await HrReq3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrReq4 = await HrReqs.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (HrReq4?.setOrg) { + await HrReq4.setOrg(relatedOrg4); + } +} + +async function associateHrReqWithPosition() { + const relatedPosition0 = await HrPositions.findOne({ + offset: Math.floor(Math.random() * (await HrPositions.count())), + }); + const HrReq0 = await HrReqs.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (HrReq0?.setPosition) { + await HrReq0.setPosition(relatedPosition0); + } + + const relatedPosition1 = await HrPositions.findOne({ + offset: Math.floor(Math.random() * (await HrPositions.count())), + }); + const HrReq1 = await HrReqs.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (HrReq1?.setPosition) { + await HrReq1.setPosition(relatedPosition1); + } + + const relatedPosition2 = await HrPositions.findOne({ + offset: Math.floor(Math.random() * (await HrPositions.count())), + }); + const HrReq2 = await HrReqs.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (HrReq2?.setPosition) { + await HrReq2.setPosition(relatedPosition2); + } + + const relatedPosition3 = await HrPositions.findOne({ + offset: Math.floor(Math.random() * (await HrPositions.count())), + }); + const HrReq3 = await HrReqs.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (HrReq3?.setPosition) { + await HrReq3.setPosition(relatedPosition3); + } + + const relatedPosition4 = await HrPositions.findOne({ + offset: Math.floor(Math.random() * (await HrPositions.count())), + }); + const HrReq4 = await HrReqs.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (HrReq4?.setPosition) { + await HrReq4.setPosition(relatedPosition4); + } +} + +async function associateHrReqWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrReq0 = await HrReqs.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (HrReq0?.setOrganization) { + await HrReq0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrReq1 = await HrReqs.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (HrReq1?.setOrganization) { + await HrReq1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrReq2 = await HrReqs.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (HrReq2?.setOrganization) { + await HrReq2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrReq3 = await HrReqs.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (HrReq3?.setOrganization) { + await HrReq3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const HrReq4 = await HrReqs.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (HrReq4?.setOrganization) { + await HrReq4.setOrganization(relatedOrganization4); + } +} + +async function associateOpsIncidentWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsIncident0 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsIncident0?.setOrg) { + await OpsIncident0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsIncident1 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsIncident1?.setOrg) { + await OpsIncident1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsIncident2 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsIncident2?.setOrg) { + await OpsIncident2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsIncident3 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsIncident3?.setOrg) { + await OpsIncident3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsIncident4 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsIncident4?.setOrg) { + await OpsIncident4.setOrg(relatedOrg4); + } +} + +async function associateOpsIncidentWithPlant() { + const relatedPlant0 = await OpsPlants.findOne({ + offset: Math.floor(Math.random() * (await OpsPlants.count())), + }); + const OpsIncident0 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsIncident0?.setPlant) { + await OpsIncident0.setPlant(relatedPlant0); + } + + const relatedPlant1 = await OpsPlants.findOne({ + offset: Math.floor(Math.random() * (await OpsPlants.count())), + }); + const OpsIncident1 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsIncident1?.setPlant) { + await OpsIncident1.setPlant(relatedPlant1); + } + + const relatedPlant2 = await OpsPlants.findOne({ + offset: Math.floor(Math.random() * (await OpsPlants.count())), + }); + const OpsIncident2 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsIncident2?.setPlant) { + await OpsIncident2.setPlant(relatedPlant2); + } + + const relatedPlant3 = await OpsPlants.findOne({ + offset: Math.floor(Math.random() * (await OpsPlants.count())), + }); + const OpsIncident3 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsIncident3?.setPlant) { + await OpsIncident3.setPlant(relatedPlant3); + } + + const relatedPlant4 = await OpsPlants.findOne({ + offset: Math.floor(Math.random() * (await OpsPlants.count())), + }); + const OpsIncident4 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsIncident4?.setPlant) { + await OpsIncident4.setPlant(relatedPlant4); + } +} + +async function associateOpsIncidentWithLine() { + const relatedLine0 = await OpsLines.findOne({ + offset: Math.floor(Math.random() * (await OpsLines.count())), + }); + const OpsIncident0 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsIncident0?.setLine) { + await OpsIncident0.setLine(relatedLine0); + } + + const relatedLine1 = await OpsLines.findOne({ + offset: Math.floor(Math.random() * (await OpsLines.count())), + }); + const OpsIncident1 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsIncident1?.setLine) { + await OpsIncident1.setLine(relatedLine1); + } + + const relatedLine2 = await OpsLines.findOne({ + offset: Math.floor(Math.random() * (await OpsLines.count())), + }); + const OpsIncident2 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsIncident2?.setLine) { + await OpsIncident2.setLine(relatedLine2); + } + + const relatedLine3 = await OpsLines.findOne({ + offset: Math.floor(Math.random() * (await OpsLines.count())), + }); + const OpsIncident3 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsIncident3?.setLine) { + await OpsIncident3.setLine(relatedLine3); + } + + const relatedLine4 = await OpsLines.findOne({ + offset: Math.floor(Math.random() * (await OpsLines.count())), + }); + const OpsIncident4 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsIncident4?.setLine) { + await OpsIncident4.setLine(relatedLine4); + } +} + +async function associateOpsIncidentWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsIncident0 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsIncident0?.setOrganization) { + await OpsIncident0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsIncident1 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsIncident1?.setOrganization) { + await OpsIncident1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsIncident2 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsIncident2?.setOrganization) { + await OpsIncident2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsIncident3 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsIncident3?.setOrganization) { + await OpsIncident3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsIncident4 = await OpsIncidents.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsIncident4?.setOrganization) { + await OpsIncident4.setOrganization(relatedOrganization4); + } +} + +async function associateOpsInventoryWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsInventory0 = await OpsInventory.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsInventory0?.setOrg) { + await OpsInventory0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsInventory1 = await OpsInventory.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsInventory1?.setOrg) { + await OpsInventory1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsInventory2 = await OpsInventory.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsInventory2?.setOrg) { + await OpsInventory2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsInventory3 = await OpsInventory.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsInventory3?.setOrg) { + await OpsInventory3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsInventory4 = await OpsInventory.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsInventory4?.setOrg) { + await OpsInventory4.setOrg(relatedOrg4); + } +} + +async function associateOpsInventoryWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsInventory0 = await OpsInventory.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsInventory0?.setOrganization) { + await OpsInventory0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsInventory1 = await OpsInventory.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsInventory1?.setOrganization) { + await OpsInventory1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsInventory2 = await OpsInventory.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsInventory2?.setOrganization) { + await OpsInventory2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsInventory3 = await OpsInventory.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsInventory3?.setOrganization) { + await OpsInventory3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsInventory4 = await OpsInventory.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsInventory4?.setOrganization) { + await OpsInventory4.setOrganization(relatedOrganization4); + } +} + +async function associateOpsLineWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsLine0 = await OpsLines.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsLine0?.setOrg) { + await OpsLine0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsLine1 = await OpsLines.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsLine1?.setOrg) { + await OpsLine1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsLine2 = await OpsLines.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsLine2?.setOrg) { + await OpsLine2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsLine3 = await OpsLines.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsLine3?.setOrg) { + await OpsLine3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsLine4 = await OpsLines.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsLine4?.setOrg) { + await OpsLine4.setOrg(relatedOrg4); + } +} + +async function associateOpsLineWithPlant() { + const relatedPlant0 = await OpsPlants.findOne({ + offset: Math.floor(Math.random() * (await OpsPlants.count())), + }); + const OpsLine0 = await OpsLines.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsLine0?.setPlant) { + await OpsLine0.setPlant(relatedPlant0); + } + + const relatedPlant1 = await OpsPlants.findOne({ + offset: Math.floor(Math.random() * (await OpsPlants.count())), + }); + const OpsLine1 = await OpsLines.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsLine1?.setPlant) { + await OpsLine1.setPlant(relatedPlant1); + } + + const relatedPlant2 = await OpsPlants.findOne({ + offset: Math.floor(Math.random() * (await OpsPlants.count())), + }); + const OpsLine2 = await OpsLines.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsLine2?.setPlant) { + await OpsLine2.setPlant(relatedPlant2); + } + + const relatedPlant3 = await OpsPlants.findOne({ + offset: Math.floor(Math.random() * (await OpsPlants.count())), + }); + const OpsLine3 = await OpsLines.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsLine3?.setPlant) { + await OpsLine3.setPlant(relatedPlant3); + } + + const relatedPlant4 = await OpsPlants.findOne({ + offset: Math.floor(Math.random() * (await OpsPlants.count())), + }); + const OpsLine4 = await OpsLines.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsLine4?.setPlant) { + await OpsLine4.setPlant(relatedPlant4); + } +} + +async function associateOpsLineWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsLine0 = await OpsLines.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsLine0?.setOrganization) { + await OpsLine0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsLine1 = await OpsLines.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsLine1?.setOrganization) { + await OpsLine1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsLine2 = await OpsLines.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsLine2?.setOrganization) { + await OpsLine2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsLine3 = await OpsLines.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsLine3?.setOrganization) { + await OpsLine3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsLine4 = await OpsLines.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsLine4?.setOrganization) { + await OpsLine4.setOrganization(relatedOrganization4); + } +} + +async function associateOpsOrderWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsOrder0 = await OpsOrders.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsOrder0?.setOrg) { + await OpsOrder0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsOrder1 = await OpsOrders.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsOrder1?.setOrg) { + await OpsOrder1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsOrder2 = await OpsOrders.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsOrder2?.setOrg) { + await OpsOrder2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsOrder3 = await OpsOrders.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsOrder3?.setOrg) { + await OpsOrder3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsOrder4 = await OpsOrders.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsOrder4?.setOrg) { + await OpsOrder4.setOrg(relatedOrg4); + } +} + +async function associateOpsOrderWithCustomer() { + const relatedCustomer0 = await FinCustomers.findOne({ + offset: Math.floor(Math.random() * (await FinCustomers.count())), + }); + const OpsOrder0 = await OpsOrders.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsOrder0?.setCustomer) { + await OpsOrder0.setCustomer(relatedCustomer0); + } + + const relatedCustomer1 = await FinCustomers.findOne({ + offset: Math.floor(Math.random() * (await FinCustomers.count())), + }); + const OpsOrder1 = await OpsOrders.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsOrder1?.setCustomer) { + await OpsOrder1.setCustomer(relatedCustomer1); + } + + const relatedCustomer2 = await FinCustomers.findOne({ + offset: Math.floor(Math.random() * (await FinCustomers.count())), + }); + const OpsOrder2 = await OpsOrders.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsOrder2?.setCustomer) { + await OpsOrder2.setCustomer(relatedCustomer2); + } + + const relatedCustomer3 = await FinCustomers.findOne({ + offset: Math.floor(Math.random() * (await FinCustomers.count())), + }); + const OpsOrder3 = await OpsOrders.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsOrder3?.setCustomer) { + await OpsOrder3.setCustomer(relatedCustomer3); + } + + const relatedCustomer4 = await FinCustomers.findOne({ + offset: Math.floor(Math.random() * (await FinCustomers.count())), + }); + const OpsOrder4 = await OpsOrders.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsOrder4?.setCustomer) { + await OpsOrder4.setCustomer(relatedCustomer4); + } +} + +async function associateOpsOrderWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsOrder0 = await OpsOrders.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsOrder0?.setOrganization) { + await OpsOrder0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsOrder1 = await OpsOrders.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsOrder1?.setOrganization) { + await OpsOrder1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsOrder2 = await OpsOrders.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsOrder2?.setOrganization) { + await OpsOrder2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsOrder3 = await OpsOrders.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsOrder3?.setOrganization) { + await OpsOrder3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsOrder4 = await OpsOrders.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsOrder4?.setOrganization) { + await OpsOrder4.setOrganization(relatedOrganization4); + } +} + +async function associateOpsPlantWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsPlant0 = await OpsPlants.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsPlant0?.setOrg) { + await OpsPlant0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsPlant1 = await OpsPlants.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsPlant1?.setOrg) { + await OpsPlant1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsPlant2 = await OpsPlants.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsPlant2?.setOrg) { + await OpsPlant2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsPlant3 = await OpsPlants.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsPlant3?.setOrg) { + await OpsPlant3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsPlant4 = await OpsPlants.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsPlant4?.setOrg) { + await OpsPlant4.setOrg(relatedOrg4); + } +} + +async function associateOpsPlantWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsPlant0 = await OpsPlants.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsPlant0?.setOrganization) { + await OpsPlant0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsPlant1 = await OpsPlants.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsPlant1?.setOrganization) { + await OpsPlant1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsPlant2 = await OpsPlants.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsPlant2?.setOrganization) { + await OpsPlant2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsPlant3 = await OpsPlants.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsPlant3?.setOrganization) { + await OpsPlant3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsPlant4 = await OpsPlants.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsPlant4?.setOrganization) { + await OpsPlant4.setOrganization(relatedOrganization4); + } +} + +async function associateOpsProductionLogWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsProductionLog0 = await OpsProductionLog.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsProductionLog0?.setOrg) { + await OpsProductionLog0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsProductionLog1 = await OpsProductionLog.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsProductionLog1?.setOrg) { + await OpsProductionLog1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsProductionLog2 = await OpsProductionLog.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsProductionLog2?.setOrg) { + await OpsProductionLog2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsProductionLog3 = await OpsProductionLog.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsProductionLog3?.setOrg) { + await OpsProductionLog3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsProductionLog4 = await OpsProductionLog.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsProductionLog4?.setOrg) { + await OpsProductionLog4.setOrg(relatedOrg4); + } +} + +async function associateOpsProductionLogWithLine() { + const relatedLine0 = await OpsLines.findOne({ + offset: Math.floor(Math.random() * (await OpsLines.count())), + }); + const OpsProductionLog0 = await OpsProductionLog.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsProductionLog0?.setLine) { + await OpsProductionLog0.setLine(relatedLine0); + } + + const relatedLine1 = await OpsLines.findOne({ + offset: Math.floor(Math.random() * (await OpsLines.count())), + }); + const OpsProductionLog1 = await OpsProductionLog.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsProductionLog1?.setLine) { + await OpsProductionLog1.setLine(relatedLine1); + } + + const relatedLine2 = await OpsLines.findOne({ + offset: Math.floor(Math.random() * (await OpsLines.count())), + }); + const OpsProductionLog2 = await OpsProductionLog.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsProductionLog2?.setLine) { + await OpsProductionLog2.setLine(relatedLine2); + } + + const relatedLine3 = await OpsLines.findOne({ + offset: Math.floor(Math.random() * (await OpsLines.count())), + }); + const OpsProductionLog3 = await OpsProductionLog.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsProductionLog3?.setLine) { + await OpsProductionLog3.setLine(relatedLine3); + } + + const relatedLine4 = await OpsLines.findOne({ + offset: Math.floor(Math.random() * (await OpsLines.count())), + }); + const OpsProductionLog4 = await OpsProductionLog.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsProductionLog4?.setLine) { + await OpsProductionLog4.setLine(relatedLine4); + } +} + +async function associateOpsProductionLogWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsProductionLog0 = await OpsProductionLog.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsProductionLog0?.setOrganization) { + await OpsProductionLog0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsProductionLog1 = await OpsProductionLog.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsProductionLog1?.setOrganization) { + await OpsProductionLog1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsProductionLog2 = await OpsProductionLog.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsProductionLog2?.setOrganization) { + await OpsProductionLog2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsProductionLog3 = await OpsProductionLog.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsProductionLog3?.setOrganization) { + await OpsProductionLog3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsProductionLog4 = await OpsProductionLog.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsProductionLog4?.setOrganization) { + await OpsProductionLog4.setOrganization(relatedOrganization4); + } +} + +async function associateOpsShipmentWithOrg() { + const relatedOrg0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsShipment0 = await OpsShipments.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsShipment0?.setOrg) { + await OpsShipment0.setOrg(relatedOrg0); + } + + const relatedOrg1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsShipment1 = await OpsShipments.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsShipment1?.setOrg) { + await OpsShipment1.setOrg(relatedOrg1); + } + + const relatedOrg2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsShipment2 = await OpsShipments.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsShipment2?.setOrg) { + await OpsShipment2.setOrg(relatedOrg2); + } + + const relatedOrg3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsShipment3 = await OpsShipments.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsShipment3?.setOrg) { + await OpsShipment3.setOrg(relatedOrg3); + } + + const relatedOrg4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsShipment4 = await OpsShipments.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsShipment4?.setOrg) { + await OpsShipment4.setOrg(relatedOrg4); + } +} + +async function associateOpsShipmentWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsShipment0 = await OpsShipments.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (OpsShipment0?.setOrganization) { + await OpsShipment0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsShipment1 = await OpsShipments.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (OpsShipment1?.setOrganization) { + await OpsShipment1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsShipment2 = await OpsShipments.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (OpsShipment2?.setOrganization) { + await OpsShipment2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsShipment3 = await OpsShipments.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (OpsShipment3?.setOrganization) { + await OpsShipment3.setOrganization(relatedOrganization3); + } + + const relatedOrganization4 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const OpsShipment4 = await OpsShipments.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (OpsShipment4?.setOrganization) { + await OpsShipment4.setOrganization(relatedOrganization4); + } +} + +module.exports = { + up: async (queryInterface, Sequelize) => { + await AuditLogs.bulkCreate(AuditLogsData); + + await DataConnections.bulkCreate(DataConnectionsData); + + await FinAccounts.bulkCreate(FinAccountsData); + + await FinAp.bulkCreate(FinApData); + + await FinAr.bulkCreate(FinArData); + + await FinBudgets.bulkCreate(FinBudgetsData); + + await FinCustomers.bulkCreate(FinCustomersData); + + await FinForecasts.bulkCreate(FinForecastsData); + + await FinTransactions.bulkCreate(FinTransactionsData); + + await FinVendors.bulkCreate(FinVendorsData); + + await HrApplicants.bulkCreate(HrApplicantsData); + + await HrAttendance.bulkCreate(HrAttendanceData); + + await HrEmployees.bulkCreate(HrEmployeesData); + + await HrPayroll.bulkCreate(HrPayrollData); + + await HrPositions.bulkCreate(HrPositionsData); + + await HrReqs.bulkCreate(HrReqsData); + + await OpsIncidents.bulkCreate(OpsIncidentsData); + + await OpsInventory.bulkCreate(OpsInventoryData); + + await OpsLines.bulkCreate(OpsLinesData); + + await OpsOrders.bulkCreate(OpsOrdersData); + + await OpsPlants.bulkCreate(OpsPlantsData); + + await OpsProductionLog.bulkCreate(OpsProductionLogData); + + await OpsShipments.bulkCreate(OpsShipmentsData); + + await Organizations.bulkCreate(OrganizationsData); + + await Promise.all([ + // Similar logic for "relation_many" + + await associateUserWithOrganization(), + + await associateAuditLogWithOrg(), + + await associateAuditLogWithUser(), + + await associateAuditLogWithOrganization(), + + await associateDataConnectionWithOrg(), + + await associateDataConnectionWithOrganization(), + + await associateFinAccountWithOrg(), + + await associateFinAccountWithParent(), + + await associateFinAccountWithOrganization(), + + await associateFinApWithOrg(), + + await associateFinApWithVendor(), + + await associateFinApWithOrganization(), + + await associateFinArWithOrg(), + + await associateFinArWithCustomer(), + + await associateFinArWithOrganization(), + + await associateFinBudgetWithOrg(), + + await associateFinBudgetWithAccount(), + + await associateFinBudgetWithOrganization(), + + await associateFinCustomerWithOrg(), + + await associateFinCustomerWithOrganization(), + + await associateFinForecastWithOrg(), + + await associateFinForecastWithAccount(), + + await associateFinForecastWithOrganization(), + + await associateFinTransactionWithOrg(), + + await associateFinTransactionWithAccount(), + + await associateFinTransactionWithVendor(), + + await associateFinTransactionWithCustomer(), + + await associateFinTransactionWithOrganization(), + + await associateFinVendorWithOrg(), + + await associateFinVendorWithOrganization(), + + await associateHrApplicantWithOrg(), + + await associateHrApplicantWithReq(), + + await associateHrApplicantWithOrganization(), + + await associateHrAttendanceWithOrg(), + + await associateHrAttendanceWithEmployee(), + + await associateHrAttendanceWithOrganization(), + + await associateHrEmployeeWithOrg(), + + await associateHrEmployeeWithManager(), + + await associateHrEmployeeWithOrganization(), + + await associateHrPayrollWithOrg(), + + await associateHrPayrollWithEmployee(), + + await associateHrPayrollWithOrganization(), + + await associateHrPositionWithOrg(), + + await associateHrPositionWithOrganization(), + + await associateHrReqWithOrg(), + + await associateHrReqWithPosition(), + + await associateHrReqWithOrganization(), + + await associateOpsIncidentWithOrg(), + + await associateOpsIncidentWithPlant(), + + await associateOpsIncidentWithLine(), + + await associateOpsIncidentWithOrganization(), + + await associateOpsInventoryWithOrg(), + + await associateOpsInventoryWithOrganization(), + + await associateOpsLineWithOrg(), + + await associateOpsLineWithPlant(), + + await associateOpsLineWithOrganization(), + + await associateOpsOrderWithOrg(), + + await associateOpsOrderWithCustomer(), + + await associateOpsOrderWithOrganization(), + + await associateOpsPlantWithOrg(), + + await associateOpsPlantWithOrganization(), + + await associateOpsProductionLogWithOrg(), + + await associateOpsProductionLogWithLine(), + + await associateOpsProductionLogWithOrganization(), + + await associateOpsShipmentWithOrg(), + + await associateOpsShipmentWithOrganization(), + ]); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('audit_logs', null, {}); + + await queryInterface.bulkDelete('data_connections', null, {}); + + await queryInterface.bulkDelete('fin_accounts', null, {}); + + await queryInterface.bulkDelete('fin_ap', null, {}); + + await queryInterface.bulkDelete('fin_ar', null, {}); + + await queryInterface.bulkDelete('fin_budgets', null, {}); + + await queryInterface.bulkDelete('fin_customers', null, {}); + + await queryInterface.bulkDelete('fin_forecasts', null, {}); + + await queryInterface.bulkDelete('fin_transactions', null, {}); + + await queryInterface.bulkDelete('fin_vendors', null, {}); + + await queryInterface.bulkDelete('hr_applicants', null, {}); + + await queryInterface.bulkDelete('hr_attendance', null, {}); + + await queryInterface.bulkDelete('hr_employees', null, {}); + + await queryInterface.bulkDelete('hr_payroll', null, {}); + + await queryInterface.bulkDelete('hr_positions', null, {}); + + await queryInterface.bulkDelete('hr_reqs', null, {}); + + await queryInterface.bulkDelete('ops_incidents', null, {}); + + await queryInterface.bulkDelete('ops_inventory', null, {}); + + await queryInterface.bulkDelete('ops_lines', null, {}); + + await queryInterface.bulkDelete('ops_orders', null, {}); + + await queryInterface.bulkDelete('ops_plants', null, {}); + + await queryInterface.bulkDelete('ops_production_log', null, {}); + + await queryInterface.bulkDelete('ops_shipments', null, {}); + + await queryInterface.bulkDelete('organizations', null, {}); + }, +}; diff --git a/backend/src/db/utils.js b/backend/src/db/utils.js new file mode 100644 index 0000000..c257f8d --- /dev/null +++ b/backend/src/db/utils.js @@ -0,0 +1,24 @@ +const validator = require('validator'); +const { v4: uuid } = require('uuid'); +const Sequelize = require('./models').Sequelize; + +module.exports = class Utils { + static uuid(value) { + let id = value; + + if (!validator.isUUID(id)) { + id = uuid(); + } + + return id; + } + + static ilike(model, column, value) { + return Sequelize.where( + Sequelize.fn('lower', Sequelize.col(`${model}.${column}`)), + { + [Sequelize.Op.like]: `%${value}%`.toLowerCase(), + }, + ); + } +}; diff --git a/backend/src/helpers.js b/backend/src/helpers.js new file mode 100644 index 0000000..1d918b5 --- /dev/null +++ b/backend/src/helpers.js @@ -0,0 +1,23 @@ +const jwt = require('jsonwebtoken'); +const config = require('./config'); + +module.exports = class Helpers { + static wrapAsync(fn) { + return function (req, res, next) { + fn(req, res, next).catch(next); + }; + } + + static commonErrorHandler(error, req, res, next) { + if ([400, 403, 404].includes(error.code)) { + return res.status(error.code).send(error.message); + } + + console.error(error); + return res.status(500).send(error.message); + } + + static jwtSign(data) { + return jwt.sign(data, config.secret_key, { expiresIn: '6h' }); + } +}; diff --git a/backend/src/index.js b/backend/src/index.js new file mode 100644 index 0000000..c648090 --- /dev/null +++ b/backend/src/index.js @@ -0,0 +1,339 @@ +const express = require('express'); +const cors = require('cors'); +const app = express(); +const passport = require('passport'); +const path = require('path'); +const fs = require('fs'); +const bodyParser = require('body-parser'); +const db = require('./db/models'); +const config = require('./config'); +const swaggerUI = require('swagger-ui-express'); +const swaggerJsDoc = require('swagger-jsdoc'); + +const authRoutes = require('./routes/auth'); +const fileRoutes = require('./routes/file'); +const searchRoutes = require('./routes/search'); +const pexelsRoutes = require('./routes/pexels'); + +const organizationForAuthRoutes = require('./routes/organizationLogin'); + +const openaiRoutes = require('./routes/openai'); + +const contactFormRoutes = require('./routes/contactForm'); + +const usersRoutes = require('./routes/users'); + +const audit_logsRoutes = require('./routes/audit_logs'); + +const data_connectionsRoutes = require('./routes/data_connections'); + +const fin_accountsRoutes = require('./routes/fin_accounts'); + +const fin_apRoutes = require('./routes/fin_ap'); + +const fin_arRoutes = require('./routes/fin_ar'); + +const fin_budgetsRoutes = require('./routes/fin_budgets'); + +const fin_customersRoutes = require('./routes/fin_customers'); + +const fin_forecastsRoutes = require('./routes/fin_forecasts'); + +const fin_transactionsRoutes = require('./routes/fin_transactions'); + +const fin_vendorsRoutes = require('./routes/fin_vendors'); + +const hr_applicantsRoutes = require('./routes/hr_applicants'); + +const hr_attendanceRoutes = require('./routes/hr_attendance'); + +const hr_employeesRoutes = require('./routes/hr_employees'); + +const hr_payrollRoutes = require('./routes/hr_payroll'); + +const hr_positionsRoutes = require('./routes/hr_positions'); + +const hr_reqsRoutes = require('./routes/hr_reqs'); + +const ops_incidentsRoutes = require('./routes/ops_incidents'); + +const ops_inventoryRoutes = require('./routes/ops_inventory'); + +const ops_linesRoutes = require('./routes/ops_lines'); + +const ops_ordersRoutes = require('./routes/ops_orders'); + +const ops_plantsRoutes = require('./routes/ops_plants'); + +const ops_production_logRoutes = require('./routes/ops_production_log'); + +const ops_shipmentsRoutes = require('./routes/ops_shipments'); + +const rolesRoutes = require('./routes/roles'); + +const permissionsRoutes = require('./routes/permissions'); + +const organizationsRoutes = require('./routes/organizations'); + +const getBaseUrl = (url) => { + if (!url) return ''; + return url.endsWith('/api') ? url.slice(0, -4) : url; +}; + +const options = { + definition: { + openapi: '3.0.0', + info: { + version: '1.0.0', + title: 'Title Enterprise Dashboards Finance HR Ops', + description: + 'Title Enterprise Dashboards Finance HR Ops Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.', + }, + servers: [ + { + url: getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || config.swaggerUrl, + description: 'Development server', + }, + ], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + responses: { + UnauthorizedError: { + description: 'Access token is missing or invalid', + }, + }, + }, + security: [ + { + bearerAuth: [], + }, + ], + }, + apis: ['./src/routes/*.js'], +}; + +const specs = swaggerJsDoc(options); +app.use( + '/api-docs', + function (req, res, next) { + swaggerUI.host = + getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || req.get('host'); + next(); + }, + swaggerUI.serve, + swaggerUI.setup(specs), +); + +app.use(cors({ origin: true })); +require('./auth/auth'); + +app.use(bodyParser.json()); + +app.use('/api/auth', authRoutes); +app.use('/api/file', fileRoutes); +app.use('/api/pexels', pexelsRoutes); +app.enable('trust proxy'); + +app.use( + '/api/users', + passport.authenticate('jwt', { session: false }), + usersRoutes, +); + +app.use( + '/api/audit_logs', + passport.authenticate('jwt', { session: false }), + audit_logsRoutes, +); + +app.use( + '/api/data_connections', + passport.authenticate('jwt', { session: false }), + data_connectionsRoutes, +); + +app.use( + '/api/fin_accounts', + passport.authenticate('jwt', { session: false }), + fin_accountsRoutes, +); + +app.use( + '/api/fin_ap', + passport.authenticate('jwt', { session: false }), + fin_apRoutes, +); + +app.use( + '/api/fin_ar', + passport.authenticate('jwt', { session: false }), + fin_arRoutes, +); + +app.use( + '/api/fin_budgets', + passport.authenticate('jwt', { session: false }), + fin_budgetsRoutes, +); + +app.use( + '/api/fin_customers', + passport.authenticate('jwt', { session: false }), + fin_customersRoutes, +); + +app.use( + '/api/fin_forecasts', + passport.authenticate('jwt', { session: false }), + fin_forecastsRoutes, +); + +app.use( + '/api/fin_transactions', + passport.authenticate('jwt', { session: false }), + fin_transactionsRoutes, +); + +app.use( + '/api/fin_vendors', + passport.authenticate('jwt', { session: false }), + fin_vendorsRoutes, +); + +app.use( + '/api/hr_applicants', + passport.authenticate('jwt', { session: false }), + hr_applicantsRoutes, +); + +app.use( + '/api/hr_attendance', + passport.authenticate('jwt', { session: false }), + hr_attendanceRoutes, +); + +app.use( + '/api/hr_employees', + passport.authenticate('jwt', { session: false }), + hr_employeesRoutes, +); + +app.use( + '/api/hr_payroll', + passport.authenticate('jwt', { session: false }), + hr_payrollRoutes, +); + +app.use( + '/api/hr_positions', + passport.authenticate('jwt', { session: false }), + hr_positionsRoutes, +); + +app.use( + '/api/hr_reqs', + passport.authenticate('jwt', { session: false }), + hr_reqsRoutes, +); + +app.use( + '/api/ops_incidents', + passport.authenticate('jwt', { session: false }), + ops_incidentsRoutes, +); + +app.use( + '/api/ops_inventory', + passport.authenticate('jwt', { session: false }), + ops_inventoryRoutes, +); + +app.use( + '/api/ops_lines', + passport.authenticate('jwt', { session: false }), + ops_linesRoutes, +); + +app.use( + '/api/ops_orders', + passport.authenticate('jwt', { session: false }), + ops_ordersRoutes, +); + +app.use( + '/api/ops_plants', + passport.authenticate('jwt', { session: false }), + ops_plantsRoutes, +); + +app.use( + '/api/ops_production_log', + passport.authenticate('jwt', { session: false }), + ops_production_logRoutes, +); + +app.use( + '/api/ops_shipments', + passport.authenticate('jwt', { session: false }), + ops_shipmentsRoutes, +); + +app.use( + '/api/roles', + passport.authenticate('jwt', { session: false }), + rolesRoutes, +); + +app.use( + '/api/permissions', + passport.authenticate('jwt', { session: false }), + permissionsRoutes, +); + +app.use( + '/api/organizations', + passport.authenticate('jwt', { session: false }), + organizationsRoutes, +); + +app.use( + '/api/openai', + passport.authenticate('jwt', { session: false }), + openaiRoutes, +); + +app.use('/api/contact-form', contactFormRoutes); + +app.use( + '/api/search', + passport.authenticate('jwt', { session: false }), + searchRoutes, +); + +app.use('/api/org-for-auth', organizationForAuthRoutes); + +const publicDir = path.join(__dirname, '../public'); + +if (fs.existsSync(publicDir)) { + app.use('/', express.static(publicDir)); + + app.get('*', function (request, response) { + response.sendFile(path.resolve(publicDir, 'index.html')); + }); +} + +const PORT = process.env.NODE_ENV === 'dev_stage' ? 3000 : 8080; + +db.sequelize.sync().then(function () { + app.listen(PORT, () => { + console.log(`Listening on port ${PORT}`); + }); +}); + +module.exports = app; diff --git a/backend/src/middlewares/check-permissions.js b/backend/src/middlewares/check-permissions.js new file mode 100644 index 0000000..59772c7 --- /dev/null +++ b/backend/src/middlewares/check-permissions.js @@ -0,0 +1,176 @@ +const ValidationError = require('../services/notifications/errors/validation'); +const RolesDBApi = require('../db/api/roles'); + +// Cache for the 'Public' role object +let publicRoleCache = null; + +// Function to asynchronously fetch and cache the 'Public' role +async function fetchAndCachePublicRole() { + try { + // Use RolesDBApi to find the role by name 'Public' + publicRoleCache = await RolesDBApi.findBy({ name: 'Public' }); + + if (!publicRoleCache) { + console.error( + "WARNING: Role 'Public' not found in database during middleware startup. Check your migrations.", + ); + // The system might not function correctly without this role. May need to throw an error or use a fallback stub. + } else { + console.log("'Public' role successfully loaded and cached."); + } + } catch (error) { + console.error( + "Error fetching 'Public' role during middleware startup:", + error, + ); + // Handle the error during startup fetch + throw error; // Important to know if the app can proceed without the Public role + } +} + +// Trigger the role fetching when the check-permissions.js module is imported/loaded +// This should happen during application startup when routes are being configured. +fetchAndCachePublicRole().catch((error) => { + // Handle the case where the fetchAndCachePublicRole promise is rejected + console.error( + 'Critical error during permissions middleware initialization:', + error, + ); + // Decide here if the process should exit if the Public role is essential. + // process.exit(1); +}); + +/** + * Middleware creator to check if the current user (or Public role) has a specific permission. + * @param {string} permission - The name of the required permission. + * @return {import("express").RequestHandler} Express middleware function. + */ +function checkPermissions(permission) { + return async (req, res, next) => { + const { currentUser } = req; + + // 1. Check self-access bypass (only if the user is authenticated) + if ( + currentUser && + (currentUser.id === req.params.id || currentUser.id === req.body.id) + ) { + return next(); // User has access to their own resource + } + + // 2. Check Custom Permissions (only if the user is authenticated) + if (currentUser) { + // Ensure custom_permissions is an array before using find + const customPermissions = Array.isArray(currentUser.custom_permissions) + ? currentUser.custom_permissions + : []; + const userPermission = customPermissions.find( + (cp) => cp.name === permission, + ); + if (userPermission) { + return next(); // User has a custom permission + } + } + + // 3. Determine the "effective" role for permission check + let effectiveRole = null; + try { + if (currentUser && currentUser.app_role) { + // User is authenticated and has an assigned role + effectiveRole = currentUser.app_role; + } else { + // User is NOT authenticated OR is authenticated but has no role + // Use the cached 'Public' role + if (!publicRoleCache) { + // If the cache is unexpectedly empty (e.g., startup error caught), + // we can try fetching the role again synchronously (less ideal) or just deny access. + console.error( + 'Public role cache is empty. Attempting synchronous fetch...', + ); + // Less efficient fallback option: + effectiveRole = await RolesDBApi.findBy({ name: 'Public' }); // Could be slow + if (!effectiveRole) { + // If even the synchronous attempt failed + return next( + new Error( + 'Internal Server Error: Public role missing and cannot be fetched.', + ), + ); + } + } else { + effectiveRole = publicRoleCache; // Use the cached object + } + } + + // Check if we got a valid role object + if (!effectiveRole) { + return next( + new Error( + 'Internal Server Error: Could not determine effective role.', + ), + ); + } + + // 4. Check Permissions on the "effective" role + // Assume the effectiveRole object (from app_role or RolesDBApi) has a getPermissions() method + // or a 'permissions' property (if permissions are eagerly loaded). + let rolePermissions = []; + if (typeof effectiveRole.getPermissions === 'function') { + rolePermissions = await effectiveRole.getPermissions(); // Get permissions asynchronously if the method exists + } else if (Array.isArray(effectiveRole.permissions)) { + rolePermissions = effectiveRole.permissions; // Or take from property if permissions are pre-loaded + } else { + console.error( + 'Role object lacks getPermissions() method or permissions property:', + effectiveRole, + ); + return next( + new Error('Internal Server Error: Invalid role object format.'), + ); + } + + if (rolePermissions.find((p) => p.name === permission)) { + next(); // The "effective" role has the required permission + } else { + // The "effective" role does not have the required permission + const roleName = effectiveRole.name || 'unknown role'; + next( + new ValidationError( + 'auth.forbidden', + `Role '${roleName}' denied access to '${permission}'.`, + ), + ); + } + } catch (e) { + // Handle errors during role or permission fetching + console.error('Error during permission check:', e); + next(e); // Pass the error to the next middleware + } + }; +} + +const METHOD_MAP = { + POST: 'CREATE', + GET: 'READ', + PUT: 'UPDATE', + PATCH: 'UPDATE', + DELETE: 'DELETE', +}; + +/** + * Middleware creator to check standard CRUD permissions based on HTTP method and entity name. + * @param {string} name - The name of the entity. + * @return {import("express").RequestHandler} Express middleware function. + */ +function checkCrudPermissions(name) { + return (req, res, next) => { + // Dynamically determine the permission name (e.g., 'READ_USERS') + const permissionName = `${METHOD_MAP[req.method]}_${name.toUpperCase()}`; + // Call the checkPermissions middleware with the determined permission + checkPermissions(permissionName)(req, res, next); + }; +} + +module.exports = { + checkPermissions, + checkCrudPermissions, +}; diff --git a/backend/src/middlewares/upload.js b/backend/src/middlewares/upload.js new file mode 100644 index 0000000..6d88c73 --- /dev/null +++ b/backend/src/middlewares/upload.js @@ -0,0 +1,11 @@ +const util = require('util'); +const Multer = require('multer'); +const maxSize = 10 * 1024 * 1024; + +let processFile = Multer({ + storage: Multer.memoryStorage(), + limits: { fileSize: maxSize }, +}).single('file'); + +let processFileMiddleware = util.promisify(processFile); +module.exports = processFileMiddleware; diff --git a/backend/src/routes/audit_logs.js b/backend/src/routes/audit_logs.js new file mode 100644 index 0000000..95414ce --- /dev/null +++ b/backend/src/routes/audit_logs.js @@ -0,0 +1,462 @@ +const express = require('express'); + +const Audit_logsService = require('../services/audit_logs'); +const Audit_logsDBApi = require('../db/api/audit_logs'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('audit_logs')); + +/** + * @swagger + * components: + * schemas: + * Audit_logs: + * type: object + * properties: + + * action: + * type: string + * default: action + * entity: + * type: string + * default: entity + * diff_json: + * type: string + * default: diff_json + + * entity_id: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Audit_logs + * description: The Audit_logs managing API + */ + +/** + * @swagger + * /api/audit_logs: + * post: + * security: + * - bearerAuth: [] + * tags: [Audit_logs] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Audit_logs" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Audit_logs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Audit_logsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Audit_logs] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Audit_logs" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Audit_logs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Audit_logsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/audit_logs/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Audit_logs] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Audit_logs" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Audit_logs" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Audit_logsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/audit_logs/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Audit_logs] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Audit_logs" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Audit_logsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/audit_logs/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Audit_logs] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Audit_logs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Audit_logsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/audit_logs: + * get: + * security: + * - bearerAuth: [] + * tags: [Audit_logs] + * summary: Get all audit_logs + * description: Get all audit_logs + * responses: + * 200: + * description: Audit_logs list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Audit_logs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Audit_logsDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'action', 'entity', 'diff_json', 'entity_id']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/audit_logs/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Audit_logs] + * summary: Count all audit_logs + * description: Count all audit_logs + * responses: + * 200: + * description: Audit_logs count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Audit_logs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Audit_logsDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/audit_logs/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Audit_logs] + * summary: Find all audit_logs that match search criteria + * description: Find all audit_logs that match search criteria + * responses: + * 200: + * description: Audit_logs list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Audit_logs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Audit_logsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/audit_logs/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Audit_logs] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Audit_logs" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Audit_logsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js new file mode 100644 index 0000000..6a82b89 --- /dev/null +++ b/backend/src/routes/auth.js @@ -0,0 +1,270 @@ +const express = require('express'); +const passport = require('passport'); + +const config = require('../config'); +const AuthService = require('../services/auth'); +const ForbiddenError = require('../services/notifications/errors/forbidden'); +const EmailSender = require('../services/email'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +/** + * @swagger + * components: + * schemas: + * Auth: + * type: object + * required: + * - email + * - password + * properties: + * email: + * type: string + * default: admin@flatlogic.com + * description: User email + * password: + * type: string + * default: password + * description: User password + */ + +/** + * @swagger + * tags: + * name: Auth + * description: Authorization operations + */ + +/** + * @swagger + * /api/auth/signin/local: + * post: + * tags: [Auth] + * summary: Logs user into the system + * description: Logs user into the system + * requestBody: + * description: Set valid user email and password + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Auth" + * responses: + * 200: + * description: Successful login + * 400: + * description: Invalid username/password supplied + * x-codegen-request-body-name: body + */ + +router.post( + '/signin/local', + wrapAsync(async (req, res) => { + const payload = await AuthService.signin( + req.body.email, + req.body.password, + req, + ); + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/auth/me: + * get: + * security: + * - bearerAuth: [] + * tags: [Auth] + * summary: Get current authorized user info + * description: Get current authorized user info + * responses: + * 200: + * description: Successful retrieval of current authorized user data + * 400: + * description: Invalid username/password supplied + * x-codegen-request-body-name: body + */ + +router.get( + '/me', + passport.authenticate('jwt', { session: false }), + (req, res) => { + if (!req.currentUser || !req.currentUser.id) { + throw new ForbiddenError(); + } + + const payload = req.currentUser; + delete payload.password; + res.status(200).send(payload); + }, +); + +router.put( + '/password-reset', + wrapAsync(async (req, res) => { + const payload = await AuthService.passwordReset( + req.body.token, + req.body.password, + req, + ); + res.status(200).send(payload); + }), +); + +router.put( + '/password-update', + passport.authenticate('jwt', { session: false }), + wrapAsync(async (req, res) => { + const payload = await AuthService.passwordUpdate( + req.body.currentPassword, + req.body.newPassword, + req, + ); + res.status(200).send(payload); + }), +); + +router.post( + '/send-email-address-verification-email', + passport.authenticate('jwt', { session: false }), + wrapAsync(async (req, res) => { + if (!req.currentUser) { + throw new ForbiddenError(); + } + + await AuthService.sendEmailAddressVerificationEmail(req.currentUser.email); + const payload = true; + res.status(200).send(payload); + }), +); + +router.post( + '/send-password-reset-email', + wrapAsync(async (req, res) => { + const link = new URL(req.headers.referer); + await AuthService.sendPasswordResetEmail( + req.body.email, + 'register', + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/auth/signup: + * post: + * tags: [Auth] + * summary: Register new user into the system + * description: Register new user into the system + * requestBody: + * description: Set valid user email and password + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Auth" + * responses: + * 200: + * description: New user successfully signed up + * 400: + * description: Invalid username/password supplied + * 500: + * description: Some server error + * x-codegen-request-body-name: body + */ + +router.post( + '/signup', + wrapAsync(async (req, res) => { + const link = new URL(req.headers.referer); + const payload = await AuthService.signup( + req.body.email, + req.body.password, + + req.body.organizationId, + + req, + link.host, + ); + res.status(200).send(payload); + }), +); + +router.put( + '/profile', + passport.authenticate('jwt', { session: false }), + wrapAsync(async (req, res) => { + if (!req.currentUser || !req.currentUser.id) { + throw new ForbiddenError(); + } + + await AuthService.updateProfile(req.body.profile, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +router.put( + '/verify-email', + wrapAsync(async (req, res) => { + const payload = await AuthService.verifyEmail( + req.body.token, + req, + req.headers.referer, + ); + res.status(200).send(payload); + }), +); + +router.get('/email-configured', (req, res) => { + const payload = EmailSender.isConfigured; + res.status(200).send(payload); +}); + +router.get('/signin/google', (req, res, next) => { + passport.authenticate('google', { + scope: ['profile', 'email'], + state: req.query.app, + })(req, res, next); +}); + +router.get( + '/signin/google/callback', + passport.authenticate('google', { + failureRedirect: '/login', + session: false, + }), + + function (req, res) { + socialRedirect(res, req.query.state, req.user.token, config); + }, +); + +router.get('/signin/microsoft', (req, res, next) => { + passport.authenticate('microsoft', { + scope: ['https://graph.microsoft.com/user.read openid'], + state: req.query.app, + })(req, res, next); +}); + +router.get( + '/signin/microsoft/callback', + passport.authenticate('microsoft', { + failureRedirect: '/login', + session: false, + }), + function (req, res) { + socialRedirect(res, req.query.state, req.user.token, config); + }, +); + +router.use('/', require('../helpers').commonErrorHandler); + +function socialRedirect(res, state, token, config) { + res.redirect(config.uiUrl + '/login?token=' + token); +} + +module.exports = router; diff --git a/backend/src/routes/contactForm.js b/backend/src/routes/contactForm.js new file mode 100644 index 0000000..7934cb8 --- /dev/null +++ b/backend/src/routes/contactForm.js @@ -0,0 +1,33 @@ +const express = require('express'); +const router = express.Router(); +const EmailSender = require('../services/email'); + +router.post('/send', async (req, res) => { + try { + const { email, subject, message } = req.body; + + if (!email || !subject || !message) { + return res.status(400).json({ error: 'All fields are required' }); + } + + const emailSender = new EmailSender({ + to: 'einkap1@gmail.com', + subject: subject, + html: () => ` +

From: ${email}

+

Subject: ${subject}

+

Message:

+

${message}

+ `, + text: () => `From: ${email}\nSubject: ${subject}\nMessage:\n${message}`, + }); + + await emailSender.send(); + res.status(200).json({ message: 'Email sent successfully' }); + } catch (error) { + console.error('Error sending email:', error); + res.status(500).json({ error: 'Error sending email' }); + } +}); + +module.exports = router; diff --git a/backend/src/routes/data_connections.js b/backend/src/routes/data_connections.js new file mode 100644 index 0000000..5645ff6 --- /dev/null +++ b/backend/src/routes/data_connections.js @@ -0,0 +1,464 @@ +const express = require('express'); + +const Data_connectionsService = require('../services/data_connections'); +const Data_connectionsDBApi = require('../db/api/data_connections'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('data_connections')); + +/** + * @swagger + * components: + * schemas: + * Data_connections: + * type: object + * properties: + + * name: + * type: string + * default: name + * config_json: + * type: string + * default: config_json + + * + * + */ + +/** + * @swagger + * tags: + * name: Data_connections + * description: The Data_connections managing API + */ + +/** + * @swagger + * /api/data_connections: + * post: + * security: + * - bearerAuth: [] + * tags: [Data_connections] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Data_connections" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Data_connections" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Data_connectionsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Data_connections] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Data_connections" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Data_connections" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Data_connectionsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/data_connections/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Data_connections] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Data_connections" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Data_connections" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Data_connectionsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/data_connections/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Data_connections] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Data_connections" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Data_connectionsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/data_connections/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Data_connections] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Data_connections" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Data_connectionsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/data_connections: + * get: + * security: + * - bearerAuth: [] + * tags: [Data_connections] + * summary: Get all data_connections + * description: Get all data_connections + * responses: + * 200: + * description: Data_connections list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Data_connections" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Data_connectionsDBApi.findAll( + req.query, + globalAccess, + { currentUser }, + ); + if (filetype && filetype === 'csv') { + const fields = ['id', 'name', 'config_json', 'last_sync_at']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/data_connections/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Data_connections] + * summary: Count all data_connections + * description: Count all data_connections + * responses: + * 200: + * description: Data_connections count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Data_connections" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Data_connectionsDBApi.findAll( + req.query, + globalAccess, + { countOnly: true, currentUser }, + ); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/data_connections/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Data_connections] + * summary: Find all data_connections that match search criteria + * description: Find all data_connections that match search criteria + * responses: + * 200: + * description: Data_connections list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Data_connections" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Data_connectionsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/data_connections/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Data_connections] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Data_connections" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Data_connectionsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/file.js b/backend/src/routes/file.js new file mode 100644 index 0000000..e98d04c --- /dev/null +++ b/backend/src/routes/file.js @@ -0,0 +1,40 @@ +const express = require('express'); +const config = require('../config'); +const path = require('path'); +const passport = require('passport'); +const services = require('../services/file'); +const router = express.Router(); + +router.get('/download', (req, res) => { + if ( + process.env.NODE_ENV == 'production' || + process.env.NEXT_PUBLIC_BACK_API + ) { + services.downloadGCloud(req, res); + } else { + services.downloadLocal(req, res); + } +}); + +router.post( + '/upload/:table/:field', + passport.authenticate('jwt', { session: false }), + (req, res) => { + const fileName = `${req.params.table}/${req.params.field}`; + + if ( + process.env.NODE_ENV == 'production' || + process.env.NEXT_PUBLIC_BACK_API + ) { + services.uploadGCloud(fileName, req, res); + } else { + services.uploadLocal(fileName, { + entity: null, + maxFileSize: 10 * 1024 * 1024, + folderIncludesAuthenticationUid: false, + })(req, res); + } + }, +); + +module.exports = router; diff --git a/backend/src/routes/fin_accounts.js b/backend/src/routes/fin_accounts.js new file mode 100644 index 0000000..4ab0672 --- /dev/null +++ b/backend/src/routes/fin_accounts.js @@ -0,0 +1,460 @@ +const express = require('express'); + +const Fin_accountsService = require('../services/fin_accounts'); +const Fin_accountsDBApi = require('../db/api/fin_accounts'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('fin_accounts')); + +/** + * @swagger + * components: + * schemas: + * Fin_accounts: + * type: object + * properties: + + * code: + * type: string + * default: code + * name: + * type: string + * default: name + + * + */ + +/** + * @swagger + * tags: + * name: Fin_accounts + * description: The Fin_accounts managing API + */ + +/** + * @swagger + * /api/fin_accounts: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_accounts] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Fin_accounts" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_accounts" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Fin_accountsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_accounts] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Fin_accounts" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_accounts" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Fin_accountsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_accounts/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Fin_accounts] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Fin_accounts" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_accounts" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Fin_accountsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_accounts/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Fin_accounts] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_accounts" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Fin_accountsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_accounts/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_accounts] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_accounts" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Fin_accountsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_accounts: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_accounts] + * summary: Get all fin_accounts + * description: Get all fin_accounts + * responses: + * 200: + * description: Fin_accounts list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_accounts" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Fin_accountsDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'code', 'name']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/fin_accounts/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_accounts] + * summary: Count all fin_accounts + * description: Count all fin_accounts + * responses: + * 200: + * description: Fin_accounts count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_accounts" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Fin_accountsDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_accounts/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_accounts] + * summary: Find all fin_accounts that match search criteria + * description: Find all fin_accounts that match search criteria + * responses: + * 200: + * description: Fin_accounts list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_accounts" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Fin_accountsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/fin_accounts/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_accounts] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_accounts" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Fin_accountsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/fin_ap.js b/backend/src/routes/fin_ap.js new file mode 100644 index 0000000..789167e --- /dev/null +++ b/backend/src/routes/fin_ap.js @@ -0,0 +1,452 @@ +const express = require('express'); + +const Fin_apService = require('../services/fin_ap'); +const Fin_apDBApi = require('../db/api/fin_ap'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('fin_ap')); + +/** + * @swagger + * components: + * schemas: + * Fin_ap: + * type: object + * properties: + + * bill_no: + * type: string + * default: bill_no + + * amount: + * type: integer + * format: int64 + + * + */ + +/** + * @swagger + * tags: + * name: Fin_ap + * description: The Fin_ap managing API + */ + +/** + * @swagger + * /api/fin_ap: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_ap] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Fin_ap" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_ap" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Fin_apService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_ap] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Fin_ap" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_ap" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Fin_apService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_ap/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Fin_ap] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Fin_ap" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_ap" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Fin_apService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_ap/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Fin_ap] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_ap" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Fin_apService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_ap/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_ap] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_ap" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Fin_apService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_ap: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_ap] + * summary: Get all fin_ap + * description: Get all fin_ap + * responses: + * 200: + * description: Fin_ap list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_ap" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Fin_apDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'bill_no', 'amount', 'due_date']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/fin_ap/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_ap] + * summary: Count all fin_ap + * description: Count all fin_ap + * responses: + * 200: + * description: Fin_ap count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_ap" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Fin_apDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_ap/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_ap] + * summary: Find all fin_ap that match search criteria + * description: Find all fin_ap that match search criteria + * responses: + * 200: + * description: Fin_ap list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_ap" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Fin_apDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/fin_ap/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_ap] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_ap" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Fin_apDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/fin_ar.js b/backend/src/routes/fin_ar.js new file mode 100644 index 0000000..9fa7673 --- /dev/null +++ b/backend/src/routes/fin_ar.js @@ -0,0 +1,452 @@ +const express = require('express'); + +const Fin_arService = require('../services/fin_ar'); +const Fin_arDBApi = require('../db/api/fin_ar'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('fin_ar')); + +/** + * @swagger + * components: + * schemas: + * Fin_ar: + * type: object + * properties: + + * invoice_no: + * type: string + * default: invoice_no + + * amount: + * type: integer + * format: int64 + + * + */ + +/** + * @swagger + * tags: + * name: Fin_ar + * description: The Fin_ar managing API + */ + +/** + * @swagger + * /api/fin_ar: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_ar] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Fin_ar" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_ar" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Fin_arService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_ar] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Fin_ar" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_ar" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Fin_arService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_ar/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Fin_ar] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Fin_ar" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_ar" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Fin_arService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_ar/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Fin_ar] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_ar" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Fin_arService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_ar/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_ar] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_ar" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Fin_arService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_ar: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_ar] + * summary: Get all fin_ar + * description: Get all fin_ar + * responses: + * 200: + * description: Fin_ar list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_ar" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Fin_arDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'invoice_no', 'amount', 'due_date']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/fin_ar/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_ar] + * summary: Count all fin_ar + * description: Count all fin_ar + * responses: + * 200: + * description: Fin_ar count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_ar" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Fin_arDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_ar/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_ar] + * summary: Find all fin_ar that match search criteria + * description: Find all fin_ar that match search criteria + * responses: + * 200: + * description: Fin_ar list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_ar" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Fin_arDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/fin_ar/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_ar] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_ar" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Fin_arDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/fin_budgets.js b/backend/src/routes/fin_budgets.js new file mode 100644 index 0000000..7438148 --- /dev/null +++ b/backend/src/routes/fin_budgets.js @@ -0,0 +1,467 @@ +const express = require('express'); + +const Fin_budgetsService = require('../services/fin_budgets'); +const Fin_budgetsDBApi = require('../db/api/fin_budgets'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('fin_budgets')); + +/** + * @swagger + * components: + * schemas: + * Fin_budgets: + * type: object + * properties: + + * cost_center: + * type: string + * default: cost_center + + * fiscal_year: + * type: integer + * format: int64 + * month: + * type: integer + * format: int64 + + * amount: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Fin_budgets + * description: The Fin_budgets managing API + */ + +/** + * @swagger + * /api/fin_budgets: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_budgets] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Fin_budgets" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_budgets" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Fin_budgetsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_budgets] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Fin_budgets" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_budgets" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Fin_budgetsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_budgets/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Fin_budgets] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Fin_budgets" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_budgets" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Fin_budgetsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_budgets/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Fin_budgets] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_budgets" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Fin_budgetsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_budgets/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_budgets] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_budgets" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Fin_budgetsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_budgets: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_budgets] + * summary: Get all fin_budgets + * description: Get all fin_budgets + * responses: + * 200: + * description: Fin_budgets list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_budgets" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Fin_budgetsDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'cost_center', 'fiscal_year', 'month', 'amount']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/fin_budgets/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_budgets] + * summary: Count all fin_budgets + * description: Count all fin_budgets + * responses: + * 200: + * description: Fin_budgets count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_budgets" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Fin_budgetsDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_budgets/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_budgets] + * summary: Find all fin_budgets that match search criteria + * description: Find all fin_budgets that match search criteria + * responses: + * 200: + * description: Fin_budgets list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_budgets" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Fin_budgetsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/fin_budgets/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_budgets] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_budgets" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Fin_budgetsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/fin_customers.js b/backend/src/routes/fin_customers.js new file mode 100644 index 0000000..c752b8e --- /dev/null +++ b/backend/src/routes/fin_customers.js @@ -0,0 +1,462 @@ +const express = require('express'); + +const Fin_customersService = require('../services/fin_customers'); +const Fin_customersDBApi = require('../db/api/fin_customers'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('fin_customers')); + +/** + * @swagger + * components: + * schemas: + * Fin_customers: + * type: object + * properties: + + * name: + * type: string + * default: name + * segment: + * type: string + * default: segment + * country: + * type: string + * default: country + + */ + +/** + * @swagger + * tags: + * name: Fin_customers + * description: The Fin_customers managing API + */ + +/** + * @swagger + * /api/fin_customers: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_customers] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Fin_customers" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_customers" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Fin_customersService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_customers] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Fin_customers" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_customers" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Fin_customersService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_customers/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Fin_customers] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Fin_customers" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_customers" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Fin_customersService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_customers/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Fin_customers] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_customers" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Fin_customersService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_customers/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_customers] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_customers" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Fin_customersService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_customers: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_customers] + * summary: Get all fin_customers + * description: Get all fin_customers + * responses: + * 200: + * description: Fin_customers list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_customers" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Fin_customersDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'name', 'segment', 'country']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/fin_customers/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_customers] + * summary: Count all fin_customers + * description: Count all fin_customers + * responses: + * 200: + * description: Fin_customers count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_customers" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Fin_customersDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_customers/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_customers] + * summary: Find all fin_customers that match search criteria + * description: Find all fin_customers that match search criteria + * responses: + * 200: + * description: Fin_customers list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_customers" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Fin_customersDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/fin_customers/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_customers] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_customers" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Fin_customersDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/fin_forecasts.js b/backend/src/routes/fin_forecasts.js new file mode 100644 index 0000000..618b54b --- /dev/null +++ b/backend/src/routes/fin_forecasts.js @@ -0,0 +1,467 @@ +const express = require('express'); + +const Fin_forecastsService = require('../services/fin_forecasts'); +const Fin_forecastsDBApi = require('../db/api/fin_forecasts'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('fin_forecasts')); + +/** + * @swagger + * components: + * schemas: + * Fin_forecasts: + * type: object + * properties: + + * cost_center: + * type: string + * default: cost_center + + * fiscal_year: + * type: integer + * format: int64 + * month: + * type: integer + * format: int64 + + * amount: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Fin_forecasts + * description: The Fin_forecasts managing API + */ + +/** + * @swagger + * /api/fin_forecasts: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_forecasts] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Fin_forecasts" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_forecasts" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Fin_forecastsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_forecasts] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Fin_forecasts" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_forecasts" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Fin_forecastsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_forecasts/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Fin_forecasts] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Fin_forecasts" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_forecasts" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Fin_forecastsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_forecasts/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Fin_forecasts] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_forecasts" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Fin_forecastsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_forecasts/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_forecasts] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_forecasts" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Fin_forecastsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_forecasts: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_forecasts] + * summary: Get all fin_forecasts + * description: Get all fin_forecasts + * responses: + * 200: + * description: Fin_forecasts list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_forecasts" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Fin_forecastsDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'cost_center', 'fiscal_year', 'month', 'amount']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/fin_forecasts/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_forecasts] + * summary: Count all fin_forecasts + * description: Count all fin_forecasts + * responses: + * 200: + * description: Fin_forecasts count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_forecasts" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Fin_forecastsDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_forecasts/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_forecasts] + * summary: Find all fin_forecasts that match search criteria + * description: Find all fin_forecasts that match search criteria + * responses: + * 200: + * description: Fin_forecasts list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_forecasts" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Fin_forecastsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/fin_forecasts/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_forecasts] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_forecasts" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Fin_forecastsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/fin_transactions.js b/backend/src/routes/fin_transactions.js new file mode 100644 index 0000000..81f55fa --- /dev/null +++ b/backend/src/routes/fin_transactions.js @@ -0,0 +1,489 @@ +const express = require('express'); + +const Fin_transactionsService = require('../services/fin_transactions'); +const Fin_transactionsDBApi = require('../db/api/fin_transactions'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('fin_transactions')); + +/** + * @swagger + * components: + * schemas: + * Fin_transactions: + * type: object + * properties: + + * currency: + * type: string + * default: currency + * cost_center: + * type: string + * default: cost_center + * project_code: + * type: string + * default: project_code + * memo: + * type: string + * default: memo + * source: + * type: string + * default: source + + * amount: + * type: integer + * format: int64 + * fx_rate: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Fin_transactions + * description: The Fin_transactions managing API + */ + +/** + * @swagger + * /api/fin_transactions: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_transactions] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Fin_transactions" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_transactions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Fin_transactionsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_transactions] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Fin_transactions" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_transactions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Fin_transactionsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_transactions/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Fin_transactions] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Fin_transactions" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_transactions" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Fin_transactionsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_transactions/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Fin_transactions] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_transactions" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Fin_transactionsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_transactions/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_transactions] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_transactions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Fin_transactionsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_transactions: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_transactions] + * summary: Get all fin_transactions + * description: Get all fin_transactions + * responses: + * 200: + * description: Fin_transactions list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_transactions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Fin_transactionsDBApi.findAll( + req.query, + globalAccess, + { currentUser }, + ); + if (filetype && filetype === 'csv') { + const fields = [ + 'id', + 'currency', + 'cost_center', + 'project_code', + 'memo', + 'source', + + 'amount', + 'fx_rate', + 'tx_ts', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/fin_transactions/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_transactions] + * summary: Count all fin_transactions + * description: Count all fin_transactions + * responses: + * 200: + * description: Fin_transactions count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_transactions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Fin_transactionsDBApi.findAll( + req.query, + globalAccess, + { countOnly: true, currentUser }, + ); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_transactions/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_transactions] + * summary: Find all fin_transactions that match search criteria + * description: Find all fin_transactions that match search criteria + * responses: + * 200: + * description: Fin_transactions list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_transactions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Fin_transactionsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/fin_transactions/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_transactions] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_transactions" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Fin_transactionsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/fin_vendors.js b/backend/src/routes/fin_vendors.js new file mode 100644 index 0000000..258e3b3 --- /dev/null +++ b/backend/src/routes/fin_vendors.js @@ -0,0 +1,462 @@ +const express = require('express'); + +const Fin_vendorsService = require('../services/fin_vendors'); +const Fin_vendorsDBApi = require('../db/api/fin_vendors'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('fin_vendors')); + +/** + * @swagger + * components: + * schemas: + * Fin_vendors: + * type: object + * properties: + + * name: + * type: string + * default: name + * tax_id: + * type: string + * default: tax_id + * country: + * type: string + * default: country + + */ + +/** + * @swagger + * tags: + * name: Fin_vendors + * description: The Fin_vendors managing API + */ + +/** + * @swagger + * /api/fin_vendors: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_vendors] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Fin_vendors" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_vendors" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Fin_vendorsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_vendors] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Fin_vendors" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_vendors" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Fin_vendorsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_vendors/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Fin_vendors] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Fin_vendors" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_vendors" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Fin_vendorsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_vendors/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Fin_vendors] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_vendors" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Fin_vendorsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_vendors/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Fin_vendors] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_vendors" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Fin_vendorsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_vendors: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_vendors] + * summary: Get all fin_vendors + * description: Get all fin_vendors + * responses: + * 200: + * description: Fin_vendors list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_vendors" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Fin_vendorsDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'name', 'tax_id', 'country']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/fin_vendors/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_vendors] + * summary: Count all fin_vendors + * description: Count all fin_vendors + * responses: + * 200: + * description: Fin_vendors count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_vendors" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Fin_vendorsDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/fin_vendors/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_vendors] + * summary: Find all fin_vendors that match search criteria + * description: Find all fin_vendors that match search criteria + * responses: + * 200: + * description: Fin_vendors list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Fin_vendors" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Fin_vendorsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/fin_vendors/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Fin_vendors] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Fin_vendors" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Fin_vendorsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/hr_applicants.js b/backend/src/routes/hr_applicants.js new file mode 100644 index 0000000..0651969 --- /dev/null +++ b/backend/src/routes/hr_applicants.js @@ -0,0 +1,467 @@ +const express = require('express'); + +const Hr_applicantsService = require('../services/hr_applicants'); +const Hr_applicantsDBApi = require('../db/api/hr_applicants'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('hr_applicants')); + +/** + * @swagger + * components: + * schemas: + * Hr_applicants: + * type: object + * properties: + + * name: + * type: string + * default: name + * source: + * type: string + * default: source + + * + */ + +/** + * @swagger + * tags: + * name: Hr_applicants + * description: The Hr_applicants managing API + */ + +/** + * @swagger + * /api/hr_applicants: + * post: + * security: + * - bearerAuth: [] + * tags: [Hr_applicants] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Hr_applicants" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_applicants" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Hr_applicantsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Hr_applicants] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Hr_applicants" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_applicants" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Hr_applicantsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_applicants/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Hr_applicants] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Hr_applicants" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_applicants" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Hr_applicantsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_applicants/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Hr_applicants] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_applicants" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Hr_applicantsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_applicants/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Hr_applicants] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_applicants" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Hr_applicantsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_applicants: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_applicants] + * summary: Get all hr_applicants + * description: Get all hr_applicants + * responses: + * 200: + * description: Hr_applicants list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Hr_applicants" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Hr_applicantsDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = [ + 'id', + 'name', + 'source', + + 'offer_extended_at', + 'offer_accepted_at', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/hr_applicants/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_applicants] + * summary: Count all hr_applicants + * description: Count all hr_applicants + * responses: + * 200: + * description: Hr_applicants count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Hr_applicants" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Hr_applicantsDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_applicants/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_applicants] + * summary: Find all hr_applicants that match search criteria + * description: Find all hr_applicants that match search criteria + * responses: + * 200: + * description: Hr_applicants list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Hr_applicants" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Hr_applicantsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/hr_applicants/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_applicants] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_applicants" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Hr_applicantsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/hr_attendance.js b/backend/src/routes/hr_attendance.js new file mode 100644 index 0000000..acb7878 --- /dev/null +++ b/backend/src/routes/hr_attendance.js @@ -0,0 +1,457 @@ +const express = require('express'); + +const Hr_attendanceService = require('../services/hr_attendance'); +const Hr_attendanceDBApi = require('../db/api/hr_attendance'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('hr_attendance')); + +/** + * @swagger + * components: + * schemas: + * Hr_attendance: + * type: object + * properties: + + * hours_worked: + * type: integer + * format: int64 + + * + */ + +/** + * @swagger + * tags: + * name: Hr_attendance + * description: The Hr_attendance managing API + */ + +/** + * @swagger + * /api/hr_attendance: + * post: + * security: + * - bearerAuth: [] + * tags: [Hr_attendance] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Hr_attendance" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_attendance" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Hr_attendanceService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Hr_attendance] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Hr_attendance" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_attendance" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Hr_attendanceService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_attendance/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Hr_attendance] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Hr_attendance" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_attendance" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Hr_attendanceService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_attendance/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Hr_attendance] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_attendance" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Hr_attendanceService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_attendance/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Hr_attendance] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_attendance" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Hr_attendanceService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_attendance: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_attendance] + * summary: Get all hr_attendance + * description: Get all hr_attendance + * responses: + * 200: + * description: Hr_attendance list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Hr_attendance" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Hr_attendanceDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'hours_worked', 'date']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/hr_attendance/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_attendance] + * summary: Count all hr_attendance + * description: Count all hr_attendance + * responses: + * 200: + * description: Hr_attendance count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Hr_attendance" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Hr_attendanceDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_attendance/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_attendance] + * summary: Find all hr_attendance that match search criteria + * description: Find all hr_attendance that match search criteria + * responses: + * 200: + * description: Hr_attendance list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Hr_attendance" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Hr_attendanceDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/hr_attendance/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_attendance] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_attendance" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Hr_attendanceDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/hr_employees.js b/backend/src/routes/hr_employees.js new file mode 100644 index 0000000..9239c66 --- /dev/null +++ b/backend/src/routes/hr_employees.js @@ -0,0 +1,483 @@ +const express = require('express'); + +const Hr_employeesService = require('../services/hr_employees'); +const Hr_employeesDBApi = require('../db/api/hr_employees'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('hr_employees')); + +/** + * @swagger + * components: + * schemas: + * Hr_employees: + * type: object + * properties: + + * employee_no: + * type: string + * default: employee_no + * name: + * type: string + * default: name + * email: + * type: string + * default: email + * department: + * type: string + * default: department + * location: + * type: string + * default: location + * grade: + * type: string + * default: grade + + * + */ + +/** + * @swagger + * tags: + * name: Hr_employees + * description: The Hr_employees managing API + */ + +/** + * @swagger + * /api/hr_employees: + * post: + * security: + * - bearerAuth: [] + * tags: [Hr_employees] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Hr_employees" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_employees" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Hr_employeesService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Hr_employees] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Hr_employees" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_employees" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Hr_employeesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_employees/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Hr_employees] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Hr_employees" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_employees" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Hr_employeesService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_employees/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Hr_employees] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_employees" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Hr_employeesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_employees/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Hr_employees] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_employees" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Hr_employeesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_employees: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_employees] + * summary: Get all hr_employees + * description: Get all hr_employees + * responses: + * 200: + * description: Hr_employees list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Hr_employees" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Hr_employeesDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = [ + 'id', + 'employee_no', + 'name', + 'email', + 'department', + 'location', + 'grade', + + 'hire_date', + 'termination_date', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/hr_employees/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_employees] + * summary: Count all hr_employees + * description: Count all hr_employees + * responses: + * 200: + * description: Hr_employees count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Hr_employees" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Hr_employeesDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_employees/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_employees] + * summary: Find all hr_employees that match search criteria + * description: Find all hr_employees that match search criteria + * responses: + * 200: + * description: Hr_employees list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Hr_employees" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Hr_employeesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/hr_employees/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_employees] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_employees" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Hr_employeesDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/hr_payroll.js b/backend/src/routes/hr_payroll.js new file mode 100644 index 0000000..0824a40 --- /dev/null +++ b/backend/src/routes/hr_payroll.js @@ -0,0 +1,470 @@ +const express = require('express'); + +const Hr_payrollService = require('../services/hr_payroll'); +const Hr_payrollDBApi = require('../db/api/hr_payroll'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('hr_payroll')); + +/** + * @swagger + * components: + * schemas: + * Hr_payroll: + * type: object + * properties: + + * currency: + * type: string + * default: currency + + * base_pay: + * type: integer + * format: int64 + * bonus: + * type: integer + * format: int64 + * overtime_hours: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Hr_payroll + * description: The Hr_payroll managing API + */ + +/** + * @swagger + * /api/hr_payroll: + * post: + * security: + * - bearerAuth: [] + * tags: [Hr_payroll] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Hr_payroll" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_payroll" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Hr_payrollService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Hr_payroll] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Hr_payroll" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_payroll" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Hr_payrollService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_payroll/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Hr_payroll] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Hr_payroll" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_payroll" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Hr_payrollService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_payroll/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Hr_payroll] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_payroll" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Hr_payrollService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_payroll/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Hr_payroll] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_payroll" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Hr_payrollService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_payroll: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_payroll] + * summary: Get all hr_payroll + * description: Get all hr_payroll + * responses: + * 200: + * description: Hr_payroll list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Hr_payroll" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Hr_payrollDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = [ + 'id', + 'currency', + + 'base_pay', + 'bonus', + 'overtime_hours', + 'period', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/hr_payroll/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_payroll] + * summary: Count all hr_payroll + * description: Count all hr_payroll + * responses: + * 200: + * description: Hr_payroll count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Hr_payroll" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Hr_payrollDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_payroll/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_payroll] + * summary: Find all hr_payroll that match search criteria + * description: Find all hr_payroll that match search criteria + * responses: + * 200: + * description: Hr_payroll list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Hr_payroll" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Hr_payrollDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/hr_payroll/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_payroll] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_payroll" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Hr_payrollDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/hr_positions.js b/backend/src/routes/hr_positions.js new file mode 100644 index 0000000..1be9edc --- /dev/null +++ b/backend/src/routes/hr_positions.js @@ -0,0 +1,466 @@ +const express = require('express'); + +const Hr_positionsService = require('../services/hr_positions'); +const Hr_positionsDBApi = require('../db/api/hr_positions'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('hr_positions')); + +/** + * @swagger + * components: + * schemas: + * Hr_positions: + * type: object + * properties: + + * title: + * type: string + * default: title + * department: + * type: string + * default: department + * level: + * type: string + * default: level + * location: + * type: string + * default: location + + * + */ + +/** + * @swagger + * tags: + * name: Hr_positions + * description: The Hr_positions managing API + */ + +/** + * @swagger + * /api/hr_positions: + * post: + * security: + * - bearerAuth: [] + * tags: [Hr_positions] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Hr_positions" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_positions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Hr_positionsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Hr_positions] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Hr_positions" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_positions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Hr_positionsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_positions/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Hr_positions] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Hr_positions" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_positions" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Hr_positionsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_positions/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Hr_positions] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_positions" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Hr_positionsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_positions/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Hr_positions] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_positions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Hr_positionsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_positions: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_positions] + * summary: Get all hr_positions + * description: Get all hr_positions + * responses: + * 200: + * description: Hr_positions list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Hr_positions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Hr_positionsDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'title', 'department', 'level', 'location']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/hr_positions/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_positions] + * summary: Count all hr_positions + * description: Count all hr_positions + * responses: + * 200: + * description: Hr_positions count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Hr_positions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Hr_positionsDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_positions/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_positions] + * summary: Find all hr_positions that match search criteria + * description: Find all hr_positions that match search criteria + * responses: + * 200: + * description: Hr_positions list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Hr_positions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Hr_positionsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/hr_positions/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_positions] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_positions" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Hr_positionsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/hr_reqs.js b/backend/src/routes/hr_reqs.js new file mode 100644 index 0000000..f7c58d7 --- /dev/null +++ b/backend/src/routes/hr_reqs.js @@ -0,0 +1,454 @@ +const express = require('express'); + +const Hr_reqsService = require('../services/hr_reqs'); +const Hr_reqsDBApi = require('../db/api/hr_reqs'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('hr_reqs')); + +/** + * @swagger + * components: + * schemas: + * Hr_reqs: + * type: object + * properties: + + * recruiter: + * type: string + * default: recruiter + + * + * + */ + +/** + * @swagger + * tags: + * name: Hr_reqs + * description: The Hr_reqs managing API + */ + +/** + * @swagger + * /api/hr_reqs: + * post: + * security: + * - bearerAuth: [] + * tags: [Hr_reqs] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Hr_reqs" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_reqs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Hr_reqsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Hr_reqs] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Hr_reqs" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_reqs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Hr_reqsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_reqs/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Hr_reqs] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Hr_reqs" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_reqs" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Hr_reqsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_reqs/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Hr_reqs] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_reqs" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Hr_reqsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_reqs/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Hr_reqs] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_reqs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Hr_reqsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_reqs: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_reqs] + * summary: Get all hr_reqs + * description: Get all hr_reqs + * responses: + * 200: + * description: Hr_reqs list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Hr_reqs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Hr_reqsDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'recruiter', 'opened_at', 'filled_at']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/hr_reqs/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_reqs] + * summary: Count all hr_reqs + * description: Count all hr_reqs + * responses: + * 200: + * description: Hr_reqs count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Hr_reqs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Hr_reqsDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/hr_reqs/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_reqs] + * summary: Find all hr_reqs that match search criteria + * description: Find all hr_reqs that match search criteria + * responses: + * 200: + * description: Hr_reqs list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Hr_reqs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Hr_reqsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/hr_reqs/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Hr_reqs] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Hr_reqs" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Hr_reqsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/openai.js b/backend/src/routes/openai.js new file mode 100644 index 0000000..9260394 --- /dev/null +++ b/backend/src/routes/openai.js @@ -0,0 +1,244 @@ +const express = require('express'); +const db = require('../db/models'); +const wrapAsync = require('../helpers').wrapAsync; +const router = express.Router(); +const sjs = require('sequelize-json-schema'); +const { getWidget, askGpt } = require('../services/openai'); +const RolesService = require('../services/roles'); +const RolesDBApi = require('../db/api/roles'); + +/** + * @swagger + * /api/roles/roles-info/{infoId}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Remove role information by ID + * description: Remove specific role information by ID + * parameters: + * - in: path + * name: infoId + * description: ID of role information to remove + * required: true + * schema: + * type: string + * - in: query + * name: userId + * description: ID of the user + * required: true + * schema: + * type: string + * - in: query + * name: key + * description: Key of the role information to remove + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Role information successfully removed + * content: + * application/json: + * schema: + * type: object + * properties: + * user: + * type: string + * description: The user information + * 400: + * description: Invalid ID or key supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Role not found + * 500: + * description: Some server error + */ + +router.delete( + '/roles-info/:infoId', + wrapAsync(async (req, res) => { + const role = await RolesService.removeRoleInfoById( + req.query.infoId, + req.query.roleId, + req.query.key, + req.currentUser, + ); + + res.status(200).send(role); + }), +); + +/** + * @swagger + * /api/roles/role-info/{roleId}: + * get: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Get role information by key + * description: Get specific role information by key + * parameters: + * - in: path + * name: roleId + * description: ID of role to get information for + * required: true + * schema: + * type: string + * - in: query + * name: key + * description: Key of the role information to retrieve + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Role information successfully received + * content: + * application/json: + * schema: + * type: object + * properties: + * info: + * type: string + * description: The role information + * 400: + * description: Invalid ID or key supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Role not found + * 500: + * description: Some server error + */ + +router.get( + '/info-by-key', + wrapAsync(async (req, res) => { + const roleId = req.query.roleId; + const key = req.query.key; + const currentUser = req.currentUser; + let info = await RolesService.getRoleInfoByKey(key, roleId, currentUser); + const role = await RolesDBApi.findBy({ id: roleId }); + if (!role?.role_customization) { + await Promise.all( + ['pie', 'bar'].map(async (e) => { + const schema = await sjs.getSequelizeSchema(db.sequelize, {}); + const payload = { + description: `Create some cool ${e} chart`, + modelDefinition: schema.definitions, + }; + const widgetId = await getWidget(payload, currentUser?.id, roleId); + if (widgetId) { + await RolesService.addRoleInfo( + roleId, + currentUser?.id, + 'widgets', + widgetId, + req.currentUser, + ); + } + }), + ); + info = await RolesService.getRoleInfoByKey(key, roleId, currentUser); + } + res.status(200).send(info); + }), +); + +router.post( + '/create_widget', + wrapAsync(async (req, res) => { + const { description, userId, roleId } = req.body; + + const currentUser = req.currentUser; + const schema = await sjs.getSequelizeSchema(db.sequelize, {}); + const payload = { + description, + modelDefinition: schema.definitions, + }; + + const widgetId = await getWidget(payload, userId, roleId); + + if (widgetId) { + await RolesService.addRoleInfo( + roleId, + userId, + 'widgets', + widgetId, + currentUser, + ); + + return res.status(200).send(widgetId); + } else { + return res.status(400).send(widgetId); + } + }), +); + +/** + * @swagger + * /api/openai/ask: + * post: + * security: + * - bearerAuth: [] + * tags: [OpenAI] + * summary: Ask a question to ChatGPT + * description: Send a question to OpenAI's ChatGPT and get a response + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * question: + * type: string + * description: The question to ask ChatGPT + * apiKey: + * type: string + * description: OpenAI API key + * responses: + * 200: + * description: Question successfully answered + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Whether the request was successful + * data: + * type: string + * description: The answer from ChatGPT + * 400: + * description: Invalid request + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 500: + * description: Some server error + */ +router.post( + '/ask-gpt', + wrapAsync(async (req, res) => { + const { prompt } = req.body; + if (!prompt) { + return res.status(400).send({ + success: false, + error: 'Question and API key are required', + }); + } + + const response = await askGpt(prompt); + + if (response.success) { + return res.status(200).send(response); + } else { + return res.status(500).send(response); + } + }), +); + +module.exports = router; diff --git a/backend/src/routes/ops_incidents.js b/backend/src/routes/ops_incidents.js new file mode 100644 index 0000000..ed61815 --- /dev/null +++ b/backend/src/routes/ops_incidents.js @@ -0,0 +1,471 @@ +const express = require('express'); + +const Ops_incidentsService = require('../services/ops_incidents'); +const Ops_incidentsDBApi = require('../db/api/ops_incidents'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('ops_incidents')); + +/** + * @swagger + * components: + * schemas: + * Ops_incidents: + * type: object + * properties: + + * category: + * type: string + * default: category + * root_cause: + * type: string + * default: root_cause + + * duration_min: + * type: integer + * format: int64 + + * + */ + +/** + * @swagger + * tags: + * name: Ops_incidents + * description: The Ops_incidents managing API + */ + +/** + * @swagger + * /api/ops_incidents: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_incidents] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Ops_incidents" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_incidents" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Ops_incidentsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_incidents] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Ops_incidents" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_incidents" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Ops_incidentsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_incidents/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Ops_incidents] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Ops_incidents" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_incidents" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Ops_incidentsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_incidents/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Ops_incidents] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_incidents" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Ops_incidentsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_incidents/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_incidents] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_incidents" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Ops_incidentsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_incidents: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_incidents] + * summary: Get all ops_incidents + * description: Get all ops_incidents + * responses: + * 200: + * description: Ops_incidents list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_incidents" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Ops_incidentsDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = [ + 'id', + 'category', + 'root_cause', + 'duration_min', + + 'occurred_at', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/ops_incidents/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_incidents] + * summary: Count all ops_incidents + * description: Count all ops_incidents + * responses: + * 200: + * description: Ops_incidents count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_incidents" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Ops_incidentsDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_incidents/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_incidents] + * summary: Find all ops_incidents that match search criteria + * description: Find all ops_incidents that match search criteria + * responses: + * 200: + * description: Ops_incidents list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_incidents" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Ops_incidentsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/ops_incidents/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_incidents] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_incidents" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Ops_incidentsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/ops_inventory.js b/backend/src/routes/ops_inventory.js new file mode 100644 index 0000000..bfa086f --- /dev/null +++ b/backend/src/routes/ops_inventory.js @@ -0,0 +1,481 @@ +const express = require('express'); + +const Ops_inventoryService = require('../services/ops_inventory'); +const Ops_inventoryDBApi = require('../db/api/ops_inventory'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('ops_inventory')); + +/** + * @swagger + * components: + * schemas: + * Ops_inventory: + * type: object + * properties: + + * sku: + * type: string + * default: sku + * location: + * type: string + * default: location + + * on_hand: + * type: integer + * format: int64 + * on_order: + * type: integer + * format: int64 + * safety_stock: + * type: integer + * format: int64 + + * unit_cost: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Ops_inventory + * description: The Ops_inventory managing API + */ + +/** + * @swagger + * /api/ops_inventory: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_inventory] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Ops_inventory" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_inventory" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Ops_inventoryService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_inventory] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Ops_inventory" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_inventory" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Ops_inventoryService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_inventory/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Ops_inventory] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Ops_inventory" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_inventory" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Ops_inventoryService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_inventory/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Ops_inventory] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_inventory" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Ops_inventoryService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_inventory/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_inventory] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_inventory" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Ops_inventoryService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_inventory: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_inventory] + * summary: Get all ops_inventory + * description: Get all ops_inventory + * responses: + * 200: + * description: Ops_inventory list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_inventory" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Ops_inventoryDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = [ + 'id', + 'sku', + 'location', + 'on_hand', + 'on_order', + 'safety_stock', + 'unit_cost', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/ops_inventory/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_inventory] + * summary: Count all ops_inventory + * description: Count all ops_inventory + * responses: + * 200: + * description: Ops_inventory count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_inventory" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Ops_inventoryDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_inventory/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_inventory] + * summary: Find all ops_inventory that match search criteria + * description: Find all ops_inventory that match search criteria + * responses: + * 200: + * description: Ops_inventory list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_inventory" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Ops_inventoryDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/ops_inventory/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_inventory] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_inventory" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Ops_inventoryDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/ops_lines.js b/backend/src/routes/ops_lines.js new file mode 100644 index 0000000..32d94e3 --- /dev/null +++ b/backend/src/routes/ops_lines.js @@ -0,0 +1,452 @@ +const express = require('express'); + +const Ops_linesService = require('../services/ops_lines'); +const Ops_linesDBApi = require('../db/api/ops_lines'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('ops_lines')); + +/** + * @swagger + * components: + * schemas: + * Ops_lines: + * type: object + * properties: + + * name: + * type: string + * default: name + + */ + +/** + * @swagger + * tags: + * name: Ops_lines + * description: The Ops_lines managing API + */ + +/** + * @swagger + * /api/ops_lines: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_lines] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Ops_lines" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_lines" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Ops_linesService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_lines] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Ops_lines" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_lines" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Ops_linesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_lines/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Ops_lines] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Ops_lines" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_lines" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Ops_linesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_lines/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Ops_lines] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_lines" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Ops_linesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_lines/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_lines] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_lines" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Ops_linesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_lines: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_lines] + * summary: Get all ops_lines + * description: Get all ops_lines + * responses: + * 200: + * description: Ops_lines list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_lines" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Ops_linesDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'name']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/ops_lines/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_lines] + * summary: Count all ops_lines + * description: Count all ops_lines + * responses: + * 200: + * description: Ops_lines count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_lines" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Ops_linesDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_lines/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_lines] + * summary: Find all ops_lines that match search criteria + * description: Find all ops_lines that match search criteria + * responses: + * 200: + * description: Ops_lines list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_lines" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Ops_linesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/ops_lines/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_lines] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_lines" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Ops_linesDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/ops_orders.js b/backend/src/routes/ops_orders.js new file mode 100644 index 0000000..f76fcb2 --- /dev/null +++ b/backend/src/routes/ops_orders.js @@ -0,0 +1,457 @@ +const express = require('express'); + +const Ops_ordersService = require('../services/ops_orders'); +const Ops_ordersDBApi = require('../db/api/ops_orders'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('ops_orders')); + +/** + * @swagger + * components: + * schemas: + * Ops_orders: + * type: object + * properties: + + * order_no: + * type: string + * default: order_no + + * qty: + * type: integer + * format: int64 + + * + */ + +/** + * @swagger + * tags: + * name: Ops_orders + * description: The Ops_orders managing API + */ + +/** + * @swagger + * /api/ops_orders: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_orders] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Ops_orders" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_orders" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Ops_ordersService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_orders] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Ops_orders" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_orders" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Ops_ordersService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_orders/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Ops_orders] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Ops_orders" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_orders" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Ops_ordersService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_orders/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Ops_orders] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_orders" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Ops_ordersService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_orders/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_orders] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_orders" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Ops_ordersService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_orders: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_orders] + * summary: Get all ops_orders + * description: Get all ops_orders + * responses: + * 200: + * description: Ops_orders list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_orders" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Ops_ordersDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'order_no', 'qty', 'due_date']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/ops_orders/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_orders] + * summary: Count all ops_orders + * description: Count all ops_orders + * responses: + * 200: + * description: Ops_orders count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_orders" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Ops_ordersDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_orders/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_orders] + * summary: Find all ops_orders that match search criteria + * description: Find all ops_orders that match search criteria + * responses: + * 200: + * description: Ops_orders list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_orders" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Ops_ordersDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/ops_orders/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_orders] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_orders" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Ops_ordersDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/ops_plants.js b/backend/src/routes/ops_plants.js new file mode 100644 index 0000000..bf9e70d --- /dev/null +++ b/backend/src/routes/ops_plants.js @@ -0,0 +1,458 @@ +const express = require('express'); + +const Ops_plantsService = require('../services/ops_plants'); +const Ops_plantsDBApi = require('../db/api/ops_plants'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('ops_plants')); + +/** + * @swagger + * components: + * schemas: + * Ops_plants: + * type: object + * properties: + + * name: + * type: string + * default: name + * location: + * type: string + * default: location + * timezone: + * type: string + * default: timezone + + */ + +/** + * @swagger + * tags: + * name: Ops_plants + * description: The Ops_plants managing API + */ + +/** + * @swagger + * /api/ops_plants: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_plants] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Ops_plants" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_plants" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Ops_plantsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_plants] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Ops_plants" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_plants" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Ops_plantsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_plants/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Ops_plants] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Ops_plants" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_plants" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Ops_plantsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_plants/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Ops_plants] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_plants" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Ops_plantsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_plants/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_plants] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_plants" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Ops_plantsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_plants: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_plants] + * summary: Get all ops_plants + * description: Get all ops_plants + * responses: + * 200: + * description: Ops_plants list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_plants" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Ops_plantsDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'name', 'location', 'timezone']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/ops_plants/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_plants] + * summary: Count all ops_plants + * description: Count all ops_plants + * responses: + * 200: + * description: Ops_plants count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_plants" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Ops_plantsDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_plants/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_plants] + * summary: Find all ops_plants that match search criteria + * description: Find all ops_plants that match search criteria + * responses: + * 200: + * description: Ops_plants list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_plants" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Ops_plantsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/ops_plants/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_plants] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_plants" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Ops_plantsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/ops_production_log.js b/backend/src/routes/ops_production_log.js new file mode 100644 index 0000000..517f653 --- /dev/null +++ b/backend/src/routes/ops_production_log.js @@ -0,0 +1,476 @@ +const express = require('express'); + +const Ops_production_logService = require('../services/ops_production_log'); +const Ops_production_logDBApi = require('../db/api/ops_production_log'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('ops_production_log')); + +/** + * @swagger + * components: + * schemas: + * Ops_production_log: + * type: object + * properties: + + * units_produced: + * type: integer + * format: int64 + * planned_units: + * type: integer + * format: int64 + * downtime_min: + * type: integer + * format: int64 + * defects: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Ops_production_log + * description: The Ops_production_log managing API + */ + +/** + * @swagger + * /api/ops_production_log: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_production_log] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Ops_production_log" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_production_log" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Ops_production_logService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_production_log] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Ops_production_log" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_production_log" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Ops_production_logService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_production_log/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Ops_production_log] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Ops_production_log" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_production_log" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Ops_production_logService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_production_log/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Ops_production_log] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_production_log" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Ops_production_logService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_production_log/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_production_log] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_production_log" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Ops_production_logService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_production_log: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_production_log] + * summary: Get all ops_production_log + * description: Get all ops_production_log + * responses: + * 200: + * description: Ops_production_log list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_production_log" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Ops_production_logDBApi.findAll( + req.query, + globalAccess, + { currentUser }, + ); + if (filetype && filetype === 'csv') { + const fields = [ + 'id', + 'units_produced', + 'planned_units', + 'downtime_min', + 'defects', + + 'ts', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/ops_production_log/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_production_log] + * summary: Count all ops_production_log + * description: Count all ops_production_log + * responses: + * 200: + * description: Ops_production_log count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_production_log" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Ops_production_logDBApi.findAll( + req.query, + globalAccess, + { countOnly: true, currentUser }, + ); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_production_log/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_production_log] + * summary: Find all ops_production_log that match search criteria + * description: Find all ops_production_log that match search criteria + * responses: + * 200: + * description: Ops_production_log list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_production_log" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Ops_production_logDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/ops_production_log/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_production_log] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_production_log" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Ops_production_logDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/ops_shipments.js b/backend/src/routes/ops_shipments.js new file mode 100644 index 0000000..df8becf --- /dev/null +++ b/backend/src/routes/ops_shipments.js @@ -0,0 +1,467 @@ +const express = require('express'); + +const Ops_shipmentsService = require('../services/ops_shipments'); +const Ops_shipmentsDBApi = require('../db/api/ops_shipments'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('ops_shipments')); + +/** + * @swagger + * components: + * schemas: + * Ops_shipments: + * type: object + * properties: + + * order_no: + * type: string + * default: order_no + * carrier: + * type: string + * default: carrier + + * + */ + +/** + * @swagger + * tags: + * name: Ops_shipments + * description: The Ops_shipments managing API + */ + +/** + * @swagger + * /api/ops_shipments: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_shipments] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Ops_shipments" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_shipments" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Ops_shipmentsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_shipments] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Ops_shipments" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_shipments" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Ops_shipmentsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_shipments/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Ops_shipments] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Ops_shipments" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_shipments" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Ops_shipmentsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_shipments/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Ops_shipments] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_shipments" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Ops_shipmentsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_shipments/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Ops_shipments] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_shipments" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Ops_shipmentsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_shipments: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_shipments] + * summary: Get all ops_shipments + * description: Get all ops_shipments + * responses: + * 200: + * description: Ops_shipments list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_shipments" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Ops_shipmentsDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = [ + 'id', + 'order_no', + 'carrier', + + 'shipped_at', + 'delivered_at', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/ops_shipments/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_shipments] + * summary: Count all ops_shipments + * description: Count all ops_shipments + * responses: + * 200: + * description: Ops_shipments count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_shipments" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Ops_shipmentsDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/ops_shipments/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_shipments] + * summary: Find all ops_shipments that match search criteria + * description: Find all ops_shipments that match search criteria + * responses: + * 200: + * description: Ops_shipments list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Ops_shipments" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Ops_shipmentsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/ops_shipments/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Ops_shipments] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Ops_shipments" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Ops_shipmentsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/organizationLogin.js b/backend/src/routes/organizationLogin.js new file mode 100644 index 0000000..be54c54 --- /dev/null +++ b/backend/src/routes/organizationLogin.js @@ -0,0 +1,46 @@ +const express = require('express'); + +const OrganizationsDBApi = require('../db/api/organizations'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +/** + * @swagger + * /api/organizations: + * get: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Get all organizations + * description: Get all organizations + * responses: + * 200: + * description: Organizations list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Organizations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ + +router.get( + '/', + wrapAsync(async (req, res) => { + const payload = await OrganizationsDBApi.findAll(req.query); + const simplifiedPayload = payload.rows.map((org) => ({ + id: org.id, + name: org.name, + })); + res.status(200).send(simplifiedPayload); + }), +); + +module.exports = router; diff --git a/backend/src/routes/organizations.js b/backend/src/routes/organizations.js new file mode 100644 index 0000000..61d72a9 --- /dev/null +++ b/backend/src/routes/organizations.js @@ -0,0 +1,456 @@ +const express = require('express'); + +const OrganizationsService = require('../services/organizations'); +const OrganizationsDBApi = require('../db/api/organizations'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('organizations')); + +/** + * @swagger + * components: + * schemas: + * Organizations: + * type: object + * properties: + + * name: + * type: string + * default: name + + */ + +/** + * @swagger + * tags: + * name: Organizations + * description: The Organizations managing API + */ + +/** + * @swagger + * /api/organizations: + * post: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Organizations" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Organizations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await OrganizationsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Organizations" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Organizations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await OrganizationsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/organizations/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Organizations" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Organizations" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await OrganizationsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/organizations/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Organizations" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await OrganizationsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/organizations/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Organizations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await OrganizationsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/organizations: + * get: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Get all organizations + * description: Get all organizations + * responses: + * 200: + * description: Organizations list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Organizations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await OrganizationsDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'name']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/organizations/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Count all organizations + * description: Count all organizations + * responses: + * 200: + * description: Organizations count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Organizations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await OrganizationsDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/organizations/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Find all organizations that match search criteria + * description: Find all organizations that match search criteria + * responses: + * 200: + * description: Organizations list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Organizations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await OrganizationsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/organizations/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Organizations" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await OrganizationsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/permissions.js b/backend/src/routes/permissions.js new file mode 100644 index 0000000..c3f1685 --- /dev/null +++ b/backend/src/routes/permissions.js @@ -0,0 +1,442 @@ +const express = require('express'); + +const PermissionsService = require('../services/permissions'); +const PermissionsDBApi = require('../db/api/permissions'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('permissions')); + +/** + * @swagger + * components: + * schemas: + * Permissions: + * type: object + * properties: + + * name: + * type: string + * default: name + + */ + +/** + * @swagger + * tags: + * name: Permissions + * description: The Permissions managing API + */ + +/** + * @swagger + * /api/permissions: + * post: + * security: + * - bearerAuth: [] + * tags: [Permissions] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Permissions" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Permissions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await PermissionsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Permissions] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Permissions" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Permissions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await PermissionsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/permissions/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Permissions] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Permissions" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Permissions" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await PermissionsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/permissions/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Permissions] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Permissions" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await PermissionsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/permissions/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Permissions] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Permissions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await PermissionsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/permissions: + * get: + * security: + * - bearerAuth: [] + * tags: [Permissions] + * summary: Get all permissions + * description: Get all permissions + * responses: + * 200: + * description: Permissions list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Permissions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const currentUser = req.currentUser; + const payload = await PermissionsDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'name']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/permissions/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Permissions] + * summary: Count all permissions + * description: Count all permissions + * responses: + * 200: + * description: Permissions count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Permissions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await PermissionsDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/permissions/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Permissions] + * summary: Find all permissions that match search criteria + * description: Find all permissions that match search criteria + * responses: + * 200: + * description: Permissions list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Permissions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await PermissionsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/permissions/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Permissions] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Permissions" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await PermissionsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/pexels.js b/backend/src/routes/pexels.js new file mode 100644 index 0000000..b5319cf --- /dev/null +++ b/backend/src/routes/pexels.js @@ -0,0 +1,106 @@ +const express = require('express'); +const router = express.Router(); +const { pexelsKey, pexelsQuery } = require('../config'); +const fetch = require('node-fetch'); + +const KEY = pexelsKey; + +router.get('/image', async (req, res) => { + const headers = { + Authorization: `${KEY}`, + }; + const query = pexelsQuery || 'nature'; + const orientation = 'portrait'; + const perPage = 1; + const url = `https://api.pexels.com/v1/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`; + + try { + const response = await fetch(url, { headers }); + const data = await response.json(); + res.status(200).json(data.photos[0]); + } catch (error) { + res.status(200).json({ error: 'Failed to fetch image' }); + } +}); + +router.get('/video', async (req, res) => { + const headers = { + Authorization: `${KEY}`, + }; + const query = pexelsQuery || 'nature'; + const orientation = 'portrait'; + const perPage = 1; + const url = `https://api.pexels.com/videos/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`; + + try { + const response = await fetch(url, { headers }); + const data = await response.json(); + res.status(200).json(data.videos[0]); + } catch (error) { + res.status(200).json({ error: 'Failed to fetch video' }); + } +}); + +router.get('/multiple-images', async (req, res) => { + const headers = { + Authorization: `${KEY}`, + }; + + const queries = req.query.queries + ? req.query.queries.split(',') + : ['home', 'apple', 'pizza', 'mountains', 'cat']; + const orientation = 'square'; + const perPage = 1; + + const fallbackImage = { + src: 'https://images.pexels.com/photos/8199252/pexels-photo-8199252.jpeg', + photographer: 'Yan Krukau', + photographer_url: 'https://www.pexels.com/@yankrukov', + }; + const fetchFallbackImage = async () => { + try { + const response = await fetch('https://picsum.photos/600'); + return { + src: response.url, + photographer: 'Random Picsum', + photographer_url: 'https://picsum.photos/', + }; + } catch (error) { + return fallbackImage; + } + }; + const fetchImage = async (query) => { + const url = `https://api.pexels.com/v1/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`; + const response = await fetch(url, { headers }); + const data = await response.json(); + return data.photos[0] || null; + }; + + const imagePromises = queries.map((query) => fetchImage(query)); + const imagesResults = await Promise.allSettled(imagePromises); + + const formattedImages = await Promise.all( + imagesResults.map(async (result) => { + if (result.status === 'fulfilled' && result.value) { + const image = result.value; + return { + src: image.src?.original || fallbackImage.src, + photographer: image.photographer || fallbackImage.photographer, + photographer_url: + image.photographer_url || fallbackImage.photographer_url, + }; + } else { + const fallback = await fetchFallbackImage(); + return { + src: fallback.src || '', + photographer: fallback.photographer || 'Unknown', + photographer_url: fallback.photographer_url || '', + }; + } + }), + ); + + res.json(formattedImages); +}); + +module.exports = router; diff --git a/backend/src/routes/roles.js b/backend/src/routes/roles.js new file mode 100644 index 0000000..b3999e9 --- /dev/null +++ b/backend/src/routes/roles.js @@ -0,0 +1,444 @@ +const express = require('express'); + +const RolesService = require('../services/roles'); +const RolesDBApi = require('../db/api/roles'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('roles')); + +/** + * @swagger + * components: + * schemas: + * Roles: + * type: object + * properties: + + * name: + * type: string + * default: name + + */ + +/** + * @swagger + * tags: + * name: Roles + * description: The Roles managing API + */ + +/** + * @swagger + * /api/roles: + * post: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Roles" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Roles" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await RolesService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Roles" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Roles" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await RolesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/roles/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Roles" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Roles" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await RolesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/roles/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Roles" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await RolesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/roles/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Roles" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await RolesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/roles: + * get: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Get all roles + * description: Get all roles + * responses: + * 200: + * description: Roles list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Roles" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await RolesDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'name']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/roles/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Count all roles + * description: Count all roles + * responses: + * 200: + * description: Roles count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Roles" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await RolesDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/roles/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Find all roles that match search criteria + * description: Find all roles that match search criteria + * responses: + * 200: + * description: Roles list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Roles" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const payload = await RolesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/roles/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Roles" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await RolesDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/search.js b/backend/src/routes/search.js new file mode 100644 index 0000000..756c981 --- /dev/null +++ b/backend/src/routes/search.js @@ -0,0 +1,60 @@ +const express = require('express'); +const SearchService = require('../services/search'); + +const config = require('../config'); + +const router = express.Router(); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); +router.use(checkCrudPermissions('search')); + +/** + * @swagger + * path: + * /api/search: + * post: + * summary: Search + * description: Search results across multiple tables + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * searchQuery: + * type: string + * required: + * - searchQuery + * responses: + * 200: + * description: Successful request + * 400: + * description: Invalid request + * 500: + * description: Internal server error + */ + +router.post('/', async (req, res) => { + const { searchQuery, organizationId } = req.body; + + const globalAccess = req.currentUser.app_role.globalAccess; + + if (!searchQuery) { + return res.status(400).json({ error: 'Please enter a search query' }); + } + + try { + const foundMatches = await SearchService.search( + searchQuery, + req.currentUser, + organizationId, + globalAccess, + ); + res.json(foundMatches); + } catch (error) { + console.error('Internal Server Error', error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}); + +module.exports = router; diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js new file mode 100644 index 0000000..fa9083e --- /dev/null +++ b/backend/src/routes/users.js @@ -0,0 +1,458 @@ +const express = require('express'); + +const UsersService = require('../services/users'); +const UsersDBApi = require('../db/api/users'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('users')); + +/** + * @swagger + * components: + * schemas: + * Users: + * type: object + * properties: + + * firstName: + * type: string + * default: firstName + * lastName: + * type: string + * default: lastName + * phoneNumber: + * type: string + * default: phoneNumber + * email: + * type: string + * default: email + + */ + +/** + * @swagger + * tags: + * name: Users + * description: The Users managing API + */ + +/** + * @swagger + * /api/users: + * post: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Users" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await UsersService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Users" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await UsersService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/users/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Users" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await UsersService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/users/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await UsersService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/users/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await UsersService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/users: + * get: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Get all users + * description: Get all users + * responses: + * 200: + * description: Users list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await UsersDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'firstName', 'lastName', 'phoneNumber', 'email']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/users/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Count all users + * description: Count all users + * responses: + * 200: + * description: Users count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await UsersDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/users/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Find all users that match search criteria + * description: Find all users that match search criteria + * responses: + * 200: + * description: Users list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await UsersDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/users/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await UsersDBApi.findBy({ id: req.params.id }); + + delete payload.password; + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/audit_logs.js b/backend/src/services/audit_logs.js new file mode 100644 index 0000000..f9b4103 --- /dev/null +++ b/backend/src/services/audit_logs.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const Audit_logsDBApi = require('../db/api/audit_logs'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Audit_logsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Audit_logsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Audit_logsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let audit_logs = await Audit_logsDBApi.findBy({ id }, { transaction }); + + if (!audit_logs) { + throw new ValidationError('audit_logsNotFound'); + } + + const updatedAudit_logs = await Audit_logsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedAudit_logs; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Audit_logsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Audit_logsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/auth.js b/backend/src/services/auth.js new file mode 100644 index 0000000..881244e --- /dev/null +++ b/backend/src/services/auth.js @@ -0,0 +1,228 @@ +const UsersDBApi = require('../db/api/users'); +const ValidationError = require('./notifications/errors/validation'); +const ForbiddenError = require('./notifications/errors/forbidden'); +const bcrypt = require('bcrypt'); +const EmailAddressVerificationEmail = require('./email/list/addressVerification'); +const InvitationEmail = require('./email/list/invitation'); +const PasswordResetEmail = require('./email/list/passwordReset'); +const EmailSender = require('./email'); +const config = require('../config'); +const helpers = require('../helpers'); + +class Auth { + static async signup(email, password, organizationId, options = {}, host) { + const user = await UsersDBApi.findBy({ email }); + + const hashedPassword = await bcrypt.hash( + password, + config.bcrypt.saltRounds, + ); + + if (user) { + if (user.authenticationUid) { + throw new ValidationError('auth.emailAlreadyInUse'); + } + + if (user.disabled) { + throw new ValidationError('auth.userDisabled'); + } + + await UsersDBApi.updatePassword(user.id, hashedPassword, options); + + if (EmailSender.isConfigured) { + await this.sendEmailAddressVerificationEmail(user.email, host); + } + + const data = { + user: { + id: user.id, + email: user.email, + }, + }; + + return helpers.jwtSign(data); + } + + const newUser = await UsersDBApi.createFromAuth( + { + firstName: email.split('@')[0], + password: hashedPassword, + email: email, + + organizationId: organizationId, + }, + options, + ); + + if (EmailSender.isConfigured) { + await this.sendEmailAddressVerificationEmail(newUser.email, host); + } + + const data = { + user: { + id: newUser.id, + email: newUser.email, + }, + }; + + return helpers.jwtSign(data); + } + + static async signin(email, password, options = {}) { + const user = await UsersDBApi.findBy({ email }); + + if (!user) { + throw new ValidationError('auth.userNotFound'); + } + + if (user.disabled) { + throw new ValidationError('auth.userDisabled'); + } + + if (!user.password) { + throw new ValidationError('auth.wrongPassword'); + } + + if (!EmailSender.isConfigured) { + user.emailVerified = true; + } + + if (!user.emailVerified) { + throw new ValidationError('auth.userNotVerified'); + } + + const passwordsMatch = await bcrypt.compare(password, user.password); + + if (!passwordsMatch) { + throw new ValidationError('auth.wrongPassword'); + } + + const data = { + user: { + id: user.id, + email: user.email, + }, + }; + + return helpers.jwtSign(data); + } + + static async sendEmailAddressVerificationEmail(email, host) { + let link; + try { + const token = await UsersDBApi.generateEmailVerificationToken(email); + link = `${host}/verify-email?token=${token}`; + } catch (error) { + console.error(error); + throw new ValidationError('auth.emailAddressVerificationEmail.error'); + } + + const emailAddressVerificationEmail = new EmailAddressVerificationEmail( + email, + link, + ); + + return new EmailSender(emailAddressVerificationEmail).send(); + } + + static async sendPasswordResetEmail(email, type = 'register', host) { + let link; + + try { + const token = await UsersDBApi.generatePasswordResetToken(email); + link = `${host}/password-reset?token=${token}`; + } catch (error) { + console.error(error); + throw new ValidationError('auth.passwordReset.error'); + } + + let passwordResetEmail; + if (type === 'register') { + passwordResetEmail = new PasswordResetEmail(email, link); + } + if (type === 'invitation') { + passwordResetEmail = new InvitationEmail(email, link); + } + + return new EmailSender(passwordResetEmail).send(); + } + + static async verifyEmail(token, options = {}) { + const user = await UsersDBApi.findByEmailVerificationToken(token, options); + + if (!user) { + throw new ValidationError( + 'auth.emailAddressVerificationEmail.invalidToken', + ); + } + + return UsersDBApi.markEmailVerified(user.id, options); + } + + static async passwordUpdate(currentPassword, newPassword, options) { + const currentUser = options.currentUser || null; + if (!currentUser) { + throw new ForbiddenError(); + } + + const currentPasswordMatch = await bcrypt.compare( + currentPassword, + currentUser.password, + ); + + if (!currentPasswordMatch) { + throw new ValidationError('auth.wrongPassword'); + } + + const newPasswordMatch = await bcrypt.compare( + newPassword, + currentUser.password, + ); + + if (newPasswordMatch) { + throw new ValidationError('auth.passwordUpdate.samePassword'); + } + + const hashedPassword = await bcrypt.hash( + newPassword, + config.bcrypt.saltRounds, + ); + + return UsersDBApi.updatePassword(currentUser.id, hashedPassword, options); + } + + static async passwordReset(token, password, options = {}) { + const user = await UsersDBApi.findByPasswordResetToken(token, options); + + if (!user) { + throw new ValidationError('auth.passwordReset.invalidToken'); + } + + const hashedPassword = await bcrypt.hash( + password, + config.bcrypt.saltRounds, + ); + + return UsersDBApi.updatePassword(user.id, hashedPassword, options); + } + + static async updateProfile(data, currentUser) { + let transaction = await db.sequelize.transaction(); + + try { + await UsersDBApi.findBy({ id: currentUser.id }, { transaction }); + + await UsersDBApi.update(currentUser.id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +} + +module.exports = Auth; diff --git a/backend/src/services/data_connections.js b/backend/src/services/data_connections.js new file mode 100644 index 0000000..58b7f3f --- /dev/null +++ b/backend/src/services/data_connections.js @@ -0,0 +1,121 @@ +const db = require('../db/models'); +const Data_connectionsDBApi = require('../db/api/data_connections'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Data_connectionsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Data_connectionsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Data_connectionsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let data_connections = await Data_connectionsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!data_connections) { + throw new ValidationError('data_connectionsNotFound'); + } + + const updatedData_connections = await Data_connectionsDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedData_connections; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Data_connectionsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Data_connectionsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/email/htmlTemplates/addressVerification/emailAddressVerification.html b/backend/src/services/email/htmlTemplates/addressVerification/emailAddressVerification.html new file mode 100644 index 0000000..95d8b3f --- /dev/null +++ b/backend/src/services/email/htmlTemplates/addressVerification/emailAddressVerification.html @@ -0,0 +1,52 @@ + + + + + + + + + diff --git a/backend/src/services/email/htmlTemplates/invitation/invitationTemplate.html b/backend/src/services/email/htmlTemplates/invitation/invitationTemplate.html new file mode 100644 index 0000000..e685483 --- /dev/null +++ b/backend/src/services/email/htmlTemplates/invitation/invitationTemplate.html @@ -0,0 +1,56 @@ + + + + + + + + + diff --git a/backend/src/services/email/htmlTemplates/passwordReset/passwordResetEmail.html b/backend/src/services/email/htmlTemplates/passwordReset/passwordResetEmail.html new file mode 100644 index 0000000..c77f215 --- /dev/null +++ b/backend/src/services/email/htmlTemplates/passwordReset/passwordResetEmail.html @@ -0,0 +1,55 @@ + + + + + + + + + diff --git a/backend/src/services/email/index.js b/backend/src/services/email/index.js new file mode 100644 index 0000000..fa7f3c7 --- /dev/null +++ b/backend/src/services/email/index.js @@ -0,0 +1,41 @@ +const config = require('../../config'); +const assert = require('assert'); +const nodemailer = require('nodemailer'); + +module.exports = class EmailSender { + constructor(email) { + this.email = email; + } + + async send() { + assert(this.email, 'email is required'); + assert(this.email.to, 'email.to is required'); + assert(this.email.subject, 'email.subject is required'); + assert(this.email.html, 'email.html is required'); + + const htmlContent = await this.email.html(); + + const transporter = nodemailer.createTransport(this.transportConfig); + + const mailOptions = { + from: this.from, + to: this.email.to, + subject: this.email.subject, + html: htmlContent, + }; + + return transporter.sendMail(mailOptions); + } + + static get isConfigured() { + return !!config.email?.auth?.pass && !!config.email?.auth?.user; + } + + get transportConfig() { + return config.email; + } + + get from() { + return config.email.from; + } +}; diff --git a/backend/src/services/email/list/addressVerification.js b/backend/src/services/email/list/addressVerification.js new file mode 100644 index 0000000..89be6d3 --- /dev/null +++ b/backend/src/services/email/list/addressVerification.js @@ -0,0 +1,41 @@ +const { getNotification } = require('../../notifications/helpers'); +const fs = require('fs').promises; +const path = require('path'); + +module.exports = class EmailAddressVerificationEmail { + constructor(to, link) { + this.to = to; + this.link = link; + } + + get subject() { + return getNotification( + 'emails.emailAddressVerification.subject', + getNotification('app.title'), + ); + } + + async html() { + try { + const templatePath = path.join( + __dirname, + '../../email/htmlTemplates/addressVerification/emailAddressVerification.html', + ); + + const template = await fs.readFile(templatePath, 'utf8'); + + const appTitle = getNotification('app.title'); + const signupUrl = this.link; + + let html = template + .replace(/{appTitle}/g, appTitle) + .replace(/{signupUrl}/g, signupUrl) + .replace(/{to}/g, this.to); + + return html; + } catch (error) { + console.error('Error generating invitation email HTML:', error); + throw error; + } + } +}; diff --git a/backend/src/services/email/list/invitation.js b/backend/src/services/email/list/invitation.js new file mode 100644 index 0000000..d2afc1e --- /dev/null +++ b/backend/src/services/email/list/invitation.js @@ -0,0 +1,41 @@ +const fs = require('fs').promises; +const path = require('path'); +const { getNotification } = require('../../notifications/helpers'); + +module.exports = class InvitationEmail { + constructor(to, host) { + this.to = to; + this.host = host; + } + + get subject() { + return getNotification( + 'emails.invitation.subject', + getNotification('app.title'), + ); + } + + async html() { + try { + const templatePath = path.join( + __dirname, + '../../email/htmlTemplates/invitation/invitationTemplate.html', + ); + + const template = await fs.readFile(templatePath, 'utf8'); + + const appTitle = getNotification('app.title'); + const signupUrl = `${this.host}&invitation=true`; + + let html = template + .replace(/{appTitle}/g, appTitle) + .replace(/{signupUrl}/g, signupUrl) + .replace(/{to}/g, this.to); + + return html; + } catch (error) { + console.error('Error generating invitation email HTML:', error); + throw error; + } + } +}; diff --git a/backend/src/services/email/list/passwordReset.js b/backend/src/services/email/list/passwordReset.js new file mode 100644 index 0000000..68ba353 --- /dev/null +++ b/backend/src/services/email/list/passwordReset.js @@ -0,0 +1,42 @@ +const { getNotification } = require('../../notifications/helpers'); +const path = require('path'); +const { promises: fs } = require('fs'); + +module.exports = class PasswordResetEmail { + constructor(to, link) { + this.to = to; + this.link = link; + } + + get subject() { + return getNotification( + 'emails.passwordReset.subject', + getNotification('app.title'), + ); + } + + async html() { + try { + const templatePath = path.join( + __dirname, + '../../email/htmlTemplates/passwordReset/passwordResetEmail.html', + ); + + const template = await fs.readFile(templatePath, 'utf8'); + + const appTitle = getNotification('app.title'); + const resetUrl = this.link; + const accountName = this.to; + + let html = template + .replace(/{appTitle}/g, appTitle) + .replace(/{resetUrl}/g, resetUrl) + .replace(/{accountName}/g, accountName); + + return html; + } catch (error) { + console.error('Error generating invitation email HTML:', error); + throw error; + } + } +}; diff --git a/backend/src/services/file.js b/backend/src/services/file.js new file mode 100644 index 0000000..cb08164 --- /dev/null +++ b/backend/src/services/file.js @@ -0,0 +1,202 @@ +const formidable = require('formidable'); +const fs = require('fs'); +const config = require('../config'); +const path = require('path'); +const { format } = require('util'); + +const ensureDirectoryExistence = (filePath) => { + const dirname = path.dirname(filePath); + + if (fs.existsSync(dirname)) { + return true; + } + + ensureDirectoryExistence(dirname); + fs.mkdirSync(dirname); +}; + +const uploadLocal = ( + folder, + validations = { + entity: null, + maxFileSize: null, + folderIncludesAuthenticationUid: false, + }, +) => { + return (req, res) => { + if (!req.currentUser) { + res.sendStatus(403); + return; + } + + if (validations.entity) { + res.sendStatus(403); + return; + } + + if (validations.folderIncludesAuthenticationUid) { + folder = folder.replace(':userId', req.currentUser.authenticationUid); + if ( + !req.currentUser.authenticationUid || + !folder.includes(req.currentUser.authenticationUid) + ) { + res.sendStatus(403); + return; + } + } + + const form = new formidable.IncomingForm(); + form.uploadDir = config.uploadDir; + + if (validations && validations.maxFileSize) { + form.maxFileSize = validations.maxFileSize; + } + + form.parse(req, function (err, fields, files) { + const filename = String(fields.filename); + const fileTempUrl = files.file.path; + + if (!filename) { + fs.unlinkSync(fileTempUrl); + res.sendStatus(500); + return; + } + + const privateUrl = path.join(form.uploadDir, folder, filename); + ensureDirectoryExistence(privateUrl); + fs.renameSync(fileTempUrl, privateUrl); + res.sendStatus(200); + }); + + form.on('error', function (err) { + res.status(500).send(err); + }); + }; +}; + +const downloadLocal = async (req, res) => { + const privateUrl = req.query.privateUrl; + if (!privateUrl) { + return res.sendStatus(404); + } + res.download(path.join(config.uploadDir, privateUrl)); +}; + +const initGCloud = () => { + const processFile = require('../middlewares/upload'); + const { Storage } = require('@google-cloud/storage'); + + const crypto = require('crypto'); + const hash = config.gcloud.hash; + + const privateKey = process.env.GC_PRIVATE_KEY.replace(/\\\n/g, '\n'); + + const storage = new Storage({ + projectId: process.env.GC_PROJECT_ID, + credentials: { + client_email: process.env.GC_CLIENT_EMAIL, + private_key: privateKey, + }, + }); + + const bucket = storage.bucket(config.gcloud.bucket); + return { hash, bucket, processFile }; +}; + +const uploadGCloud = async (folder, req, res) => { + try { + const { hash, bucket, processFile } = initGCloud(); + await processFile(req, res); + let buffer = await req.file.buffer; + let filename = await req.body.filename; + + if (!req.file) { + return res.status(400).send({ message: 'Please upload a file!' }); + } + + let path = `${hash}/${folder}/${filename}`; + let blob = bucket.file(path); + + console.log(path); + + const blobStream = blob.createWriteStream({ + resumable: false, + }); + + blobStream.on('error', (err) => { + console.log('Upload error'); + console.log(err.message); + res.status(500).send({ message: err.message }); + }); + + console.log(`https://storage.googleapis.com/${bucket.name}/${blob.name}`); + + blobStream.on('finish', async (data) => { + const publicUrl = format( + `https://storage.googleapis.com/${bucket.name}/${blob.name}`, + ); + + res.status(200).send({ + message: 'Uploaded the file successfully: ' + path, + url: publicUrl, + }); + }); + + blobStream.end(buffer); + } catch (err) { + console.log(err); + + res.status(500).send({ + message: `Could not upload the file. ${err}`, + }); + } +}; + +const downloadGCloud = async (req, res) => { + try { + const { hash, bucket, processFile } = initGCloud(); + + const privateUrl = await req.query.privateUrl; + const filePath = `${hash}/${privateUrl}`; + const file = bucket.file(filePath); + const fileExists = await file.exists(); + + if (fileExists[0]) { + const stream = file.createReadStream(); + stream.pipe(res); + } else { + res.status(404).send({ + message: 'Could not download the file. ' + err, + }); + } + } catch (err) { + res.status(404).send({ + message: 'Could not download the file. ' + err, + }); + } +}; + +const deleteGCloud = async (privateUrl) => { + try { + const { hash, bucket, processFile } = initGCloud(); + const filePath = `${hash}/${privateUrl}`; + + const file = bucket.file(filePath); + const fileExists = await file.exists(); + + if (fileExists[0]) { + file.delete(); + } + } catch (err) { + console.log(`Cannot find the file ${privateUrl}`); + } +}; + +module.exports = { + initGCloud, + uploadLocal, + downloadLocal, + deleteGCloud, + uploadGCloud, + downloadGCloud, +}; diff --git a/backend/src/services/fin_accounts.js b/backend/src/services/fin_accounts.js new file mode 100644 index 0000000..5e899da --- /dev/null +++ b/backend/src/services/fin_accounts.js @@ -0,0 +1,117 @@ +const db = require('../db/models'); +const Fin_accountsDBApi = require('../db/api/fin_accounts'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Fin_accountsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Fin_accountsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Fin_accountsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let fin_accounts = await Fin_accountsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!fin_accounts) { + throw new ValidationError('fin_accountsNotFound'); + } + + const updatedFin_accounts = await Fin_accountsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedFin_accounts; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Fin_accountsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Fin_accountsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/fin_ap.js b/backend/src/services/fin_ap.js new file mode 100644 index 0000000..20a809f --- /dev/null +++ b/backend/src/services/fin_ap.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const Fin_apDBApi = require('../db/api/fin_ap'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Fin_apService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Fin_apDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Fin_apDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let fin_ap = await Fin_apDBApi.findBy({ id }, { transaction }); + + if (!fin_ap) { + throw new ValidationError('fin_apNotFound'); + } + + const updatedFin_ap = await Fin_apDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedFin_ap; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Fin_apDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Fin_apDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/fin_ar.js b/backend/src/services/fin_ar.js new file mode 100644 index 0000000..ed83ab6 --- /dev/null +++ b/backend/src/services/fin_ar.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const Fin_arDBApi = require('../db/api/fin_ar'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Fin_arService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Fin_arDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Fin_arDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let fin_ar = await Fin_arDBApi.findBy({ id }, { transaction }); + + if (!fin_ar) { + throw new ValidationError('fin_arNotFound'); + } + + const updatedFin_ar = await Fin_arDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedFin_ar; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Fin_arDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Fin_arDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/fin_budgets.js b/backend/src/services/fin_budgets.js new file mode 100644 index 0000000..15fe688 --- /dev/null +++ b/backend/src/services/fin_budgets.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const Fin_budgetsDBApi = require('../db/api/fin_budgets'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Fin_budgetsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Fin_budgetsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Fin_budgetsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let fin_budgets = await Fin_budgetsDBApi.findBy({ id }, { transaction }); + + if (!fin_budgets) { + throw new ValidationError('fin_budgetsNotFound'); + } + + const updatedFin_budgets = await Fin_budgetsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedFin_budgets; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Fin_budgetsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Fin_budgetsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/fin_customers.js b/backend/src/services/fin_customers.js new file mode 100644 index 0000000..d02f745 --- /dev/null +++ b/backend/src/services/fin_customers.js @@ -0,0 +1,117 @@ +const db = require('../db/models'); +const Fin_customersDBApi = require('../db/api/fin_customers'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Fin_customersService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Fin_customersDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Fin_customersDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let fin_customers = await Fin_customersDBApi.findBy( + { id }, + { transaction }, + ); + + if (!fin_customers) { + throw new ValidationError('fin_customersNotFound'); + } + + const updatedFin_customers = await Fin_customersDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedFin_customers; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Fin_customersDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Fin_customersDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/fin_forecasts.js b/backend/src/services/fin_forecasts.js new file mode 100644 index 0000000..2401b5f --- /dev/null +++ b/backend/src/services/fin_forecasts.js @@ -0,0 +1,117 @@ +const db = require('../db/models'); +const Fin_forecastsDBApi = require('../db/api/fin_forecasts'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Fin_forecastsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Fin_forecastsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Fin_forecastsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let fin_forecasts = await Fin_forecastsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!fin_forecasts) { + throw new ValidationError('fin_forecastsNotFound'); + } + + const updatedFin_forecasts = await Fin_forecastsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedFin_forecasts; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Fin_forecastsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Fin_forecastsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/fin_transactions.js b/backend/src/services/fin_transactions.js new file mode 100644 index 0000000..6ac2f31 --- /dev/null +++ b/backend/src/services/fin_transactions.js @@ -0,0 +1,121 @@ +const db = require('../db/models'); +const Fin_transactionsDBApi = require('../db/api/fin_transactions'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Fin_transactionsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Fin_transactionsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Fin_transactionsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let fin_transactions = await Fin_transactionsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!fin_transactions) { + throw new ValidationError('fin_transactionsNotFound'); + } + + const updatedFin_transactions = await Fin_transactionsDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedFin_transactions; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Fin_transactionsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Fin_transactionsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/fin_vendors.js b/backend/src/services/fin_vendors.js new file mode 100644 index 0000000..87c10c1 --- /dev/null +++ b/backend/src/services/fin_vendors.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const Fin_vendorsDBApi = require('../db/api/fin_vendors'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Fin_vendorsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Fin_vendorsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Fin_vendorsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let fin_vendors = await Fin_vendorsDBApi.findBy({ id }, { transaction }); + + if (!fin_vendors) { + throw new ValidationError('fin_vendorsNotFound'); + } + + const updatedFin_vendors = await Fin_vendorsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedFin_vendors; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Fin_vendorsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Fin_vendorsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/hr_applicants.js b/backend/src/services/hr_applicants.js new file mode 100644 index 0000000..e423a0e --- /dev/null +++ b/backend/src/services/hr_applicants.js @@ -0,0 +1,117 @@ +const db = require('../db/models'); +const Hr_applicantsDBApi = require('../db/api/hr_applicants'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Hr_applicantsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Hr_applicantsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Hr_applicantsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let hr_applicants = await Hr_applicantsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!hr_applicants) { + throw new ValidationError('hr_applicantsNotFound'); + } + + const updatedHr_applicants = await Hr_applicantsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedHr_applicants; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Hr_applicantsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Hr_applicantsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/hr_attendance.js b/backend/src/services/hr_attendance.js new file mode 100644 index 0000000..7f4fd22 --- /dev/null +++ b/backend/src/services/hr_attendance.js @@ -0,0 +1,117 @@ +const db = require('../db/models'); +const Hr_attendanceDBApi = require('../db/api/hr_attendance'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Hr_attendanceService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Hr_attendanceDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Hr_attendanceDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let hr_attendance = await Hr_attendanceDBApi.findBy( + { id }, + { transaction }, + ); + + if (!hr_attendance) { + throw new ValidationError('hr_attendanceNotFound'); + } + + const updatedHr_attendance = await Hr_attendanceDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedHr_attendance; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Hr_attendanceDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Hr_attendanceDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/hr_employees.js b/backend/src/services/hr_employees.js new file mode 100644 index 0000000..9ed5f8e --- /dev/null +++ b/backend/src/services/hr_employees.js @@ -0,0 +1,117 @@ +const db = require('../db/models'); +const Hr_employeesDBApi = require('../db/api/hr_employees'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Hr_employeesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Hr_employeesDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Hr_employeesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let hr_employees = await Hr_employeesDBApi.findBy( + { id }, + { transaction }, + ); + + if (!hr_employees) { + throw new ValidationError('hr_employeesNotFound'); + } + + const updatedHr_employees = await Hr_employeesDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedHr_employees; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Hr_employeesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Hr_employeesDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/hr_payroll.js b/backend/src/services/hr_payroll.js new file mode 100644 index 0000000..17e4531 --- /dev/null +++ b/backend/src/services/hr_payroll.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const Hr_payrollDBApi = require('../db/api/hr_payroll'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Hr_payrollService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Hr_payrollDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Hr_payrollDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let hr_payroll = await Hr_payrollDBApi.findBy({ id }, { transaction }); + + if (!hr_payroll) { + throw new ValidationError('hr_payrollNotFound'); + } + + const updatedHr_payroll = await Hr_payrollDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedHr_payroll; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Hr_payrollDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Hr_payrollDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/hr_positions.js b/backend/src/services/hr_positions.js new file mode 100644 index 0000000..6ab4a8e --- /dev/null +++ b/backend/src/services/hr_positions.js @@ -0,0 +1,117 @@ +const db = require('../db/models'); +const Hr_positionsDBApi = require('../db/api/hr_positions'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Hr_positionsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Hr_positionsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Hr_positionsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let hr_positions = await Hr_positionsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!hr_positions) { + throw new ValidationError('hr_positionsNotFound'); + } + + const updatedHr_positions = await Hr_positionsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedHr_positions; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Hr_positionsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Hr_positionsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/hr_reqs.js b/backend/src/services/hr_reqs.js new file mode 100644 index 0000000..a58a5fe --- /dev/null +++ b/backend/src/services/hr_reqs.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const Hr_reqsDBApi = require('../db/api/hr_reqs'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Hr_reqsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Hr_reqsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Hr_reqsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let hr_reqs = await Hr_reqsDBApi.findBy({ id }, { transaction }); + + if (!hr_reqs) { + throw new ValidationError('hr_reqsNotFound'); + } + + const updatedHr_reqs = await Hr_reqsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedHr_reqs; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Hr_reqsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Hr_reqsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/notifications/errors/forbidden.js b/backend/src/services/notifications/errors/forbidden.js new file mode 100644 index 0000000..192fa10 --- /dev/null +++ b/backend/src/services/notifications/errors/forbidden.js @@ -0,0 +1,16 @@ +const { getNotification, isNotification } = require('../helpers'); + +module.exports = class ForbiddenError extends Error { + constructor(messageCode) { + let message; + + if (messageCode && isNotification(messageCode)) { + message = getNotification(messageCode); + } + + message = message || getNotification('errors.forbidden.message'); + + super(message); + this.code = 403; + } +}; diff --git a/backend/src/services/notifications/errors/validation.js b/backend/src/services/notifications/errors/validation.js new file mode 100644 index 0000000..464550c --- /dev/null +++ b/backend/src/services/notifications/errors/validation.js @@ -0,0 +1,16 @@ +const { getNotification, isNotification } = require('../helpers'); + +module.exports = class ValidationError extends Error { + constructor(messageCode) { + let message; + + if (messageCode && isNotification(messageCode)) { + message = getNotification(messageCode); + } + + message = message || getNotification('errors.validation.message'); + + super(message); + this.code = 400; + } +}; diff --git a/backend/src/services/notifications/helpers.js b/backend/src/services/notifications/helpers.js new file mode 100644 index 0000000..1c3a60f --- /dev/null +++ b/backend/src/services/notifications/helpers.js @@ -0,0 +1,30 @@ +const _get = require('lodash/get'); +const errors = require('./list'); + +function format(message, args) { + if (!message) { + return null; + } + + return message.replace(/{(\d+)}/g, function (match, number) { + return typeof args[number] != 'undefined' ? args[number] : match; + }); +} + +const isNotification = (key) => { + const message = _get(errors, key); + return !!message; +}; + +const getNotification = (key, ...args) => { + const message = _get(errors, key); + + if (!message) { + return key; + } + + return format(message, args); +}; + +exports.getNotification = getNotification; +exports.isNotification = isNotification; diff --git a/backend/src/services/notifications/list.js b/backend/src/services/notifications/list.js new file mode 100644 index 0000000..0c23132 --- /dev/null +++ b/backend/src/services/notifications/list.js @@ -0,0 +1,100 @@ +const errors = { + app: { + title: 'Title Enterprise Dashboards Finance HR Ops', + }, + + auth: { + userDisabled: 'Your account is disabled', + forbidden: 'Forbidden', + unauthorized: 'Unauthorized', + userNotFound: `Sorry, we don't recognize your credentials`, + wrongPassword: `Sorry, we don't recognize your credentials`, + weakPassword: 'This password is too weak', + emailAlreadyInUse: 'Email is already in use', + invalidEmail: 'Please provide a valid email', + passwordReset: { + invalidToken: 'Password reset link is invalid or has expired', + error: `Email not recognized`, + }, + passwordUpdate: { + samePassword: `You can't use the same password. Please create new password`, + }, + userNotVerified: `Sorry, your email has not been verified yet`, + emailAddressVerificationEmail: { + invalidToken: 'Email verification link is invalid or has expired', + error: `Email not recognized`, + }, + }, + + iam: { + errors: { + userAlreadyExists: 'User with this email already exists', + userNotFound: 'User not found', + disablingHimself: `You can't disable yourself`, + revokingOwnPermission: `You can't revoke your own owner permission`, + deletingHimself: `You can't delete yourself`, + emailRequired: 'Email is required', + }, + }, + + importer: { + errors: { + invalidFileEmpty: 'The file is empty', + invalidFileExcel: 'Only excel (.xlsx) files are allowed', + invalidFileUpload: + 'Invalid file. Make sure you are using the last version of the template.', + importHashRequired: 'Import hash is required', + importHashExistent: 'Data has already been imported', + userEmailMissing: 'Some items in the CSV do not have an email', + }, + }, + + errors: { + forbidden: { + message: 'Forbidden', + }, + validation: { + message: 'An error occurred', + }, + searchQueryRequired: { + message: 'Search query is required', + }, + }, + + emails: { + invitation: { + subject: `You've been invited to {0}`, + body: ` +

Hello,

+

You've been invited to {0} set password for your {1} account.

+

{2}

+

Thanks,

+

Your {0} team

+ `, + }, + emailAddressVerification: { + subject: `Verify your email for {0}`, + body: ` +

Hello,

+

Follow this link to verify your email address.

+

{0}

+

If you didn't ask to verify this address, you can ignore this email.

+

Thanks,

+

Your {1} team

+ `, + }, + passwordReset: { + subject: `Reset your password for {0}`, + body: ` +

Hello,

+

Follow this link to reset your {0} password for your {1} account.

+

{2}

+

If you didn't ask to reset your password, you can ignore this email.

+

Thanks,

+

Your {0} team

+ `, + }, + }, +}; + +module.exports = errors; diff --git a/backend/src/services/openai.js b/backend/src/services/openai.js new file mode 100644 index 0000000..152299d --- /dev/null +++ b/backend/src/services/openai.js @@ -0,0 +1,68 @@ +const axios = require('axios'); +const { v4: uuid } = require('uuid'); +const RoleService = require('./roles'); +const config = require('../config'); + +module.exports = class OpenAiService { + static async getWidget(payload, userId, roleId) { + const response = await axios.post( + `${config.flHost}/${config.project_uuid}/project_customization_widgets.json`, + payload, + ); + + if (response.status >= 200 && response.status < 300) { + const { widget_id } = await response.data; + await RoleService.addRoleInfo(roleId, userId, 'widgets', widget_id); + return widget_id; + } else { + console.error('=======error=======', response.data); + return { value: null, error: response.data }; + } + } + + static async askGpt(prompt) { + if (!config.gpt_key) { + return { + success: false, + error: 'API key is required', + }; + } + try { + const response = await axios.post( + 'https://api.openai.com/v1/chat/completions', + { + model: 'gpt-4o', + messages: [{ role: 'user', content: prompt }], + }, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${config.gpt_key}`, + }, + }, + ); + + if (response.status >= 200 && response.status < 300) { + return { + success: true, + data: response.data.choices[0].message.content, + }; + } else { + console.error('Error asking question to ChatGPT:', response.data); + return { + success: false, + error: response.data, + }; + } + } catch (error) { + console.error( + 'Error asking question to ChatGPT:', + error.response?.data || error.message, + ); + return { + success: false, + error: error.response?.data || error.message, + }; + } + } +}; diff --git a/backend/src/services/ops_incidents.js b/backend/src/services/ops_incidents.js new file mode 100644 index 0000000..1a2520e --- /dev/null +++ b/backend/src/services/ops_incidents.js @@ -0,0 +1,117 @@ +const db = require('../db/models'); +const Ops_incidentsDBApi = require('../db/api/ops_incidents'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Ops_incidentsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Ops_incidentsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Ops_incidentsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let ops_incidents = await Ops_incidentsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!ops_incidents) { + throw new ValidationError('ops_incidentsNotFound'); + } + + const updatedOps_incidents = await Ops_incidentsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedOps_incidents; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Ops_incidentsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Ops_incidentsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/ops_inventory.js b/backend/src/services/ops_inventory.js new file mode 100644 index 0000000..d901e04 --- /dev/null +++ b/backend/src/services/ops_inventory.js @@ -0,0 +1,117 @@ +const db = require('../db/models'); +const Ops_inventoryDBApi = require('../db/api/ops_inventory'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Ops_inventoryService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Ops_inventoryDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Ops_inventoryDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let ops_inventory = await Ops_inventoryDBApi.findBy( + { id }, + { transaction }, + ); + + if (!ops_inventory) { + throw new ValidationError('ops_inventoryNotFound'); + } + + const updatedOps_inventory = await Ops_inventoryDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedOps_inventory; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Ops_inventoryDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Ops_inventoryDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/ops_lines.js b/backend/src/services/ops_lines.js new file mode 100644 index 0000000..6fe5284 --- /dev/null +++ b/backend/src/services/ops_lines.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const Ops_linesDBApi = require('../db/api/ops_lines'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Ops_linesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Ops_linesDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Ops_linesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let ops_lines = await Ops_linesDBApi.findBy({ id }, { transaction }); + + if (!ops_lines) { + throw new ValidationError('ops_linesNotFound'); + } + + const updatedOps_lines = await Ops_linesDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedOps_lines; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Ops_linesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Ops_linesDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/ops_orders.js b/backend/src/services/ops_orders.js new file mode 100644 index 0000000..d69bb93 --- /dev/null +++ b/backend/src/services/ops_orders.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const Ops_ordersDBApi = require('../db/api/ops_orders'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Ops_ordersService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Ops_ordersDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Ops_ordersDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let ops_orders = await Ops_ordersDBApi.findBy({ id }, { transaction }); + + if (!ops_orders) { + throw new ValidationError('ops_ordersNotFound'); + } + + const updatedOps_orders = await Ops_ordersDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedOps_orders; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Ops_ordersDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Ops_ordersDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/ops_plants.js b/backend/src/services/ops_plants.js new file mode 100644 index 0000000..ec72508 --- /dev/null +++ b/backend/src/services/ops_plants.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const Ops_plantsDBApi = require('../db/api/ops_plants'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Ops_plantsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Ops_plantsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Ops_plantsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let ops_plants = await Ops_plantsDBApi.findBy({ id }, { transaction }); + + if (!ops_plants) { + throw new ValidationError('ops_plantsNotFound'); + } + + const updatedOps_plants = await Ops_plantsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedOps_plants; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Ops_plantsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Ops_plantsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/ops_production_log.js b/backend/src/services/ops_production_log.js new file mode 100644 index 0000000..7c05b0d --- /dev/null +++ b/backend/src/services/ops_production_log.js @@ -0,0 +1,121 @@ +const db = require('../db/models'); +const Ops_production_logDBApi = require('../db/api/ops_production_log'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Ops_production_logService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Ops_production_logDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Ops_production_logDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let ops_production_log = await Ops_production_logDBApi.findBy( + { id }, + { transaction }, + ); + + if (!ops_production_log) { + throw new ValidationError('ops_production_logNotFound'); + } + + const updatedOps_production_log = await Ops_production_logDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedOps_production_log; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Ops_production_logDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Ops_production_logDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/ops_shipments.js b/backend/src/services/ops_shipments.js new file mode 100644 index 0000000..59e1039 --- /dev/null +++ b/backend/src/services/ops_shipments.js @@ -0,0 +1,117 @@ +const db = require('../db/models'); +const Ops_shipmentsDBApi = require('../db/api/ops_shipments'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Ops_shipmentsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Ops_shipmentsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Ops_shipmentsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let ops_shipments = await Ops_shipmentsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!ops_shipments) { + throw new ValidationError('ops_shipmentsNotFound'); + } + + const updatedOps_shipments = await Ops_shipmentsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedOps_shipments; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Ops_shipmentsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Ops_shipmentsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/organizations.js b/backend/src/services/organizations.js new file mode 100644 index 0000000..453aaf3 --- /dev/null +++ b/backend/src/services/organizations.js @@ -0,0 +1,117 @@ +const db = require('../db/models'); +const OrganizationsDBApi = require('../db/api/organizations'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class OrganizationsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await OrganizationsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await OrganizationsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let organizations = await OrganizationsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!organizations) { + throw new ValidationError('organizationsNotFound'); + } + + const updatedOrganizations = await OrganizationsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedOrganizations; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await OrganizationsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await OrganizationsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/permissions.js b/backend/src/services/permissions.js new file mode 100644 index 0000000..ad78c26 --- /dev/null +++ b/backend/src/services/permissions.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const PermissionsDBApi = require('../db/api/permissions'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class PermissionsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await PermissionsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await PermissionsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let permissions = await PermissionsDBApi.findBy({ id }, { transaction }); + + if (!permissions) { + throw new ValidationError('permissionsNotFound'); + } + + const updatedPermissions = await PermissionsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedPermissions; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await PermissionsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await PermissionsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/roles.js b/backend/src/services/roles.js new file mode 100644 index 0000000..13afb2c --- /dev/null +++ b/backend/src/services/roles.js @@ -0,0 +1,389 @@ +const db = require('../db/models'); +const RolesDBApi = require('../db/api/roles'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +function buildWidgetResult(widget, queryResult, queryString) { + if (queryResult[0] && queryResult[0].length) { + const key = Object.keys(queryResult[0][0])[0]; + const value = + widget.widget_type === 'scalar' ? queryResult[0][0][key] : queryResult[0]; + const widgetData = JSON.parse(widget.data); + return { ...widget, ...widgetData, value, query: queryString }; + } else { + return { ...widget, value: [], query: queryString }; + } +} + +async function executeQuery(queryString, currentUser) { + try { + return await db.sequelize.query(queryString, { + replacements: { organizationId: currentUser.organizationId }, + }); + } catch (e) { + console.log(e); + return []; + } +} + +function insertWhereConditions(queryString, whereConditions) { + if (!whereConditions) return queryString; + + const whereIndex = queryString.toLowerCase().indexOf('where'); + const groupByIndex = queryString.toLowerCase().indexOf('group by'); + const insertIndex = + whereIndex === -1 + ? groupByIndex !== -1 + ? groupByIndex + : queryString.length + : whereIndex + 5; + + const prefix = queryString.substring(0, insertIndex); + const suffix = queryString.substring(insertIndex); + const conditionString = + whereIndex === -1 + ? ` WHERE ${whereConditions} ` + : ` ${whereConditions} AND `; + + return `${prefix}${conditionString}${suffix}`; +} + +function constructWhereConditions(mainTable, currentUser, replacements) { + const { + organizationId, + app_role: { globalAccess }, + } = currentUser; + const tablesWithoutOrgId = ['permissions', 'roles']; + let whereConditions = ''; + + if (!globalAccess && !tablesWithoutOrgId.includes(mainTable)) { + whereConditions += `"${mainTable}"."organizationId" = :organizationId`; + replacements.organizationId = organizationId; + } + + whereConditions += whereConditions ? ' AND ' : ''; + whereConditions += `"${mainTable}"."deletedAt" IS NULL`; + + return whereConditions; +} + +function extractTableName(queryString) { + const tableNameRegex = /FROM\s+("?)([^"\s]+)\1\s*/i; + const match = tableNameRegex.exec(queryString); + return match ? match[2] : null; +} + +function buildQueryString(widget, currentUser) { + let queryString = widget?.query || ''; + const tableName = extractTableName(queryString); + const mainTable = JSON.parse(widget?.data)?.main_table || tableName; + const replacements = {}; + const whereConditions = constructWhereConditions( + mainTable, + currentUser, + replacements, + ); + queryString = insertWhereConditions(queryString, whereConditions); + console.log(queryString, 'queryString'); + return queryString; +} + +async function constructWidgetsResults(widgets, currentUser) { + const widgetsResults = []; + for (const widget of widgets) { + if (!widget) continue; + const queryString = buildQueryString(widget, currentUser); + const queryResult = await executeQuery(queryString, currentUser); + widgetsResults.push(buildWidgetResult(widget, queryResult, queryString)); + } + return widgetsResults; +} + +async function fetchWidgetsData(widgets) { + const widgetPromises = (widgets || []).map((widgetId) => + axios.get( + `${config.flHost}/${config.project_uuid}/project_customization_widgets/${widgetId}.json`, + ), + ); + const widgetResults = widgetPromises + ? await Promise.allSettled(widgetPromises) + : []; + return widgetResults + .filter((result) => result.status === 'fulfilled') + .map((result) => result.value.data); +} + +async function processWidgets(widgets, currentUser) { + const widgetData = await fetchWidgetsData(widgets); + return constructWidgetsResults(widgetData, currentUser); +} + +function parseCustomization(role) { + try { + return JSON.parse(role.role_customization || '{}'); + } catch (e) { + console.log(e); + return {}; + } +} + +async function findRole(roleId, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + const role = roleId + ? await RolesDBApi.findBy({ id: roleId }, { transaction }) + : await RolesDBApi.findBy({ name: 'User' }, { transaction }); + await transaction.commit(); + return role; + } catch (error) { + await transaction.rollback(); + throw error; + } +} + +module.exports = class RolesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await RolesDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await RolesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let roles = await RolesDBApi.findBy({ id }, { transaction }); + + if (!roles) { + throw new ValidationError('rolesNotFound'); + } + + const updatedRoles = await RolesDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedRoles; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await RolesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await RolesDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async addRoleInfo(roleId, userId, key, widgetId, currentUser) { + const regexExpForUuid = + /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi; + const widgetIdIsUUID = regexExpForUuid.test(widgetId); + + const transaction = await db.sequelize.transaction(); + let role; + if (roleId) { + role = await RolesDBApi.findBy({ id: roleId }, { transaction }); + } else { + role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); + } + + if (!role) { + throw new ValidationError('rolesNotFound'); + } + + try { + let customization = {}; + try { + customization = JSON.parse(role.role_customization || '{}'); + } catch (e) { + console.log(e); + } + + if (widgetIdIsUUID && Array.isArray(customization[key])) { + const el = customization[key].find((e) => e === widgetId); + !el ? customization[key].unshift(widgetId) : null; + } + + if (widgetIdIsUUID && !customization[key]) { + customization[key] = [widgetId]; + } + + const newRole = await RolesDBApi.update( + role.id, + { + role_customization: JSON.stringify(customization), + name: role.name, + permissions: role.permissions, + globalAccess: role.globalAccess, + }, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + + return newRole; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async removeRoleInfoById(infoId, roleId, key, currentUser) { + const transaction = await db.sequelize.transaction(); + + let role; + if (roleId) { + role = await RolesDBApi.findBy({ id: roleId }, { transaction }); + } else { + role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); + } + if (!role) { + await transaction.rollback(); + throw new ValidationError('rolesNotFound'); + } + + let customization = {}; + try { + customization = JSON.parse(role.role_customization || '{}'); + } catch (e) { + console.log(e); + } + + customization[key] = customization[key].filter((item) => item !== infoId); + + const response = await axios.delete( + `${config.flHost}/${config.project_uuid}/project_customization_widgets/${infoId}.json`, + ); + const { status } = await response; + try { + const result = await RolesDBApi.update( + role.id, + { + role_customization: JSON.stringify(customization), + name: role.name, + permissions: role.permissions, + globalAccess: role.globalAccess, + }, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return result; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async getRoleInfoByKey(key, roleId, currentUser) { + const transaction = await db.sequelize.transaction(); + + const organizationId = currentUser.organizationId; + let globalAccess = currentUser.app_role?.globalAccess; + let queryString = ''; + + try { + const role = await findRole(roleId, currentUser); + const customization = parseCustomization(role); + + let result; + if (key === 'widgets') { + result = await processWidgets(customization[key], currentUser); + } else { + result = customization[key]; + } + + await transaction.commit(); + return result; + } catch (error) { + console.error(error); + await transaction.rollback(); + } finally { + if (transaction.finished !== 'commit') { + await transaction.rollback(); + } + } + } +}; diff --git a/backend/src/services/search.js b/backend/src/services/search.js new file mode 100644 index 0000000..141ec2e --- /dev/null +++ b/backend/src/services/search.js @@ -0,0 +1,235 @@ +const db = require('../db/models'); +const ValidationError = require('./notifications/errors/validation'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +/** + * @param {string} permission + * @param {object} currentUser + */ +async function checkPermissions(permission, currentUser) { + if (!currentUser) { + throw new ValidationError('auth.unauthorized'); + } + + const userPermission = currentUser.custom_permissions.find( + (cp) => cp.name === permission, + ); + + if (userPermission) { + return true; + } + + try { + if (!currentUser.app_role) { + throw new ValidationError('auth.forbidden'); + } + + const permissions = await currentUser.app_role.getPermissions(); + + return !!permissions.find((p) => p.name === permission); + } catch (e) { + throw e; + } +} + +module.exports = class SearchService { + static async search(searchQuery, currentUser, organizationId, globalAccess) { + try { + if (!searchQuery) { + throw new ValidationError('iam.errors.searchQueryRequired'); + } + const tableColumns = { + users: ['firstName', 'lastName', 'phoneNumber', 'email'], + + audit_logs: ['action', 'entity', 'diff_json'], + + data_connections: ['name', 'config_json'], + + fin_accounts: ['code', 'name'], + + fin_ap: ['bill_no'], + + fin_ar: ['invoice_no'], + + fin_budgets: ['cost_center'], + + fin_customers: ['name', 'segment', 'country'], + + fin_forecasts: ['cost_center'], + + fin_transactions: [ + 'currency', + + 'cost_center', + + 'project_code', + + 'memo', + + 'source', + ], + + fin_vendors: ['name', 'tax_id', 'country'], + + hr_applicants: ['name', 'source'], + + hr_employees: [ + 'employee_no', + + 'name', + + 'email', + + 'department', + + 'location', + + 'grade', + ], + + hr_payroll: ['currency'], + + hr_positions: ['title', 'department', 'level', 'location'], + + hr_reqs: ['recruiter'], + + ops_incidents: ['category', 'root_cause'], + + ops_inventory: ['sku', 'location'], + + ops_lines: ['name'], + + ops_orders: ['order_no'], + + ops_plants: ['name', 'location', 'timezone'], + + ops_shipments: ['order_no', 'carrier'], + + organizations: ['name'], + }; + const columnsInt = { + audit_logs: ['entity_id'], + + fin_ap: ['amount'], + + fin_ar: ['amount'], + + fin_budgets: ['fiscal_year', 'month', 'amount'], + + fin_forecasts: ['fiscal_year', 'month', 'amount'], + + fin_transactions: ['amount', 'fx_rate'], + + hr_attendance: ['hours_worked'], + + hr_payroll: ['base_pay', 'bonus', 'overtime_hours'], + + ops_incidents: ['duration_min'], + + ops_inventory: ['on_hand', 'on_order', 'safety_stock', 'unit_cost'], + + ops_orders: ['qty'], + + ops_production_log: [ + 'units_produced', + + 'planned_units', + + 'downtime_min', + + 'defects', + ], + }; + + let allFoundRecords = []; + + for (const tableName in tableColumns) { + if (tableColumns.hasOwnProperty(tableName)) { + const attributesToSearch = tableColumns[tableName]; + const attributesIntToSearch = columnsInt[tableName] || []; + const whereCondition = { + [Op.or]: [ + ...attributesToSearch.map((attribute) => ({ + [attribute]: { + [Op.iLike]: `%${searchQuery}%`, + }, + })), + ...attributesIntToSearch.map((attribute) => + Sequelize.where( + Sequelize.cast( + Sequelize.col(`${tableName}.${attribute}`), + 'varchar', + ), + { [Op.iLike]: `%${searchQuery}%` }, + ), + ), + ], + }; + + if ( + !globalAccess && + tableName !== 'organizations' && + organizationId + ) { + whereCondition.organizationId = organizationId; + } + + const hasPermission = await checkPermissions( + `READ_${tableName.toUpperCase()}`, + currentUser, + ); + if (!hasPermission) { + continue; + } + + const foundRecords = await db[tableName].findAll({ + where: whereCondition, + attributes: [ + ...tableColumns[tableName], + 'id', + ...attributesIntToSearch, + ], + }); + + const modifiedRecords = foundRecords.map((record) => { + const matchAttribute = []; + + for (const attribute of attributesToSearch) { + if ( + record[attribute] + ?.toLowerCase() + ?.includes(searchQuery.toLowerCase()) + ) { + matchAttribute.push(attribute); + } + } + + for (const attribute of attributesIntToSearch) { + const castedValue = String(record[attribute]); + if ( + castedValue && + castedValue.toLowerCase().includes(searchQuery.toLowerCase()) + ) { + matchAttribute.push(attribute); + } + } + + return { + ...record.get(), + matchAttribute, + tableName, + }; + }); + + allFoundRecords = allFoundRecords.concat(modifiedRecords); + } + } + + return allFoundRecords; + } catch (error) { + throw error; + } + } +}; diff --git a/backend/src/services/users.js b/backend/src/services/users.js new file mode 100644 index 0000000..e75abfb --- /dev/null +++ b/backend/src/services/users.js @@ -0,0 +1,163 @@ +const db = require('../db/models'); +const UsersDBApi = require('../db/api/users'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +const InvitationEmail = require('./email/list/invitation'); +const EmailSender = require('./email'); +const AuthService = require('./auth'); + +module.exports = class UsersService { + static async create(data, currentUser, sendInvitationEmails = true, host) { + let transaction = await db.sequelize.transaction(); + + const globalAccess = currentUser.app_role.globalAccess; + + let email = data.email; + let emailsToInvite = []; + try { + if (email) { + let user = await UsersDBApi.findBy({ email }, { transaction }); + if (user) { + throw new ValidationError('iam.errors.userAlreadyExists'); + } else { + await UsersDBApi.create( + { data }, + + globalAccess, + + { + currentUser, + transaction, + }, + ); + emailsToInvite.push(email); + } + } else { + throw new ValidationError('iam.errors.emailRequired'); + } + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + if (emailsToInvite && emailsToInvite.length) { + if (!sendInvitationEmails) return; + + AuthService.sendPasswordResetEmail(email, 'invitation', host); + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + let emailsToInvite = []; + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', () => { + console.log('results csv', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + const hasAllEmails = results.every((result) => result.email); + + if (!hasAllEmails) { + throw new ValidationError('importer.errors.userEmailMissing'); + } + + await UsersDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + emailsToInvite = results.map((result) => result.email); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + + if (emailsToInvite && emailsToInvite.length && !sendInvitationEmails) { + emailsToInvite.forEach((email) => { + AuthService.sendPasswordResetEmail(email, 'invitation', host); + }); + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + + const globalAccess = currentUser.app_role.globalAccess; + + try { + let users = await UsersDBApi.findBy({ id }, { transaction }); + + if (!users) { + throw new ValidationError('iam.errors.userNotFound'); + } + + const updatedUser = await UsersDBApi.update( + id, + data, + + globalAccess, + + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedUser; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + if (currentUser.id === id) { + throw new ValidationError('iam.errors.deletingHimself'); + } + + if ( + currentUser.app_role?.name !== config.roles.admin && + currentUser.app_role?.name !== config.roles.super_admin + ) { + throw new ValidationError('errors.forbidden.message'); + } + + await UsersDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/watcher.js b/backend/watcher.js new file mode 100644 index 0000000..6c021cc --- /dev/null +++ b/backend/watcher.js @@ -0,0 +1,45 @@ +const chokidar = require('chokidar'); +const { exec } = require('child_process'); +const nodemon = require('nodemon'); + +const migrationsWatcher = chokidar.watch('./src/db/migrations', { + persistent: true, + ignoreInitial: true, +}); +migrationsWatcher.on('add', (filePath) => { + console.log(`[DEBUG] New migration file: ${filePath}`); + exec('npm run db:migrate', (error, stdout, stderr) => { + console.log(stdout); + if (error) { + console.error(stderr); + } + }); +}); + +const seedersWatcher = chokidar.watch('./src/db/seeders', { + persistent: true, + ignoreInitial: true, +}); +seedersWatcher.on('add', (filePath) => { + console.log(`[DEBUG] New seed file: ${filePath}`); + exec('npm run db:seed', (error, stdout, stderr) => { + console.log(stdout); + if (error) { + console.error(stderr); + } + }); +}); + +nodemon({ + script: './src/index.js', + ignore: ['./src/db/migrations', './src/db/seeders'], + delay: '500', +}); + +nodemon.on('start', () => { + console.log('Nodemon started'); +}); + +nodemon.on('restart', (files) => { + console.log('Nodemon restarted due changes in:', files); +}); diff --git a/backend/yarn.lock b/backend/yarn.lock new file mode 100644 index 0000000..222a4f9 --- /dev/null +++ b/backend/yarn.lock @@ -0,0 +1,4470 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@apidevtools/json-schema-ref-parser@^9.0.6": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz#8ff5386b365d4c9faa7c8b566ff16a46a577d9b8" + integrity sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg== + dependencies: + "@jsdevtools/ono" "^7.1.3" + "@types/json-schema" "^7.0.6" + call-me-maybe "^1.0.1" + js-yaml "^4.1.0" + +"@apidevtools/openapi-schemas@^2.0.4": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz#9fa08017fb59d80538812f03fc7cac5992caaa17" + integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== + +"@apidevtools/swagger-methods@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267" + integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== + +"@apidevtools/swagger-parser@10.0.3": + version "10.0.3" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz#32057ae99487872c4dd96b314a1ab4b95d89eaf5" + integrity sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g== + dependencies: + "@apidevtools/json-schema-ref-parser" "^9.0.6" + "@apidevtools/openapi-schemas" "^2.0.4" + "@apidevtools/swagger-methods" "^3.0.2" + "@jsdevtools/ono" "^7.1.3" + call-me-maybe "^1.0.1" + z-schema "^5.0.1" + +"@azure/abort-controller@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-1.1.0.tgz#788ee78457a55af8a1ad342acb182383d2119249" + integrity sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw== + dependencies: + tslib "^2.2.0" + +"@azure/abort-controller@^2.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-2.1.2.tgz#42fe0ccab23841d9905812c58f1082d27784566d" + integrity sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA== + dependencies: + tslib "^2.6.2" + +"@azure/core-auth@^1.3.0", "@azure/core-auth@^1.4.0", "@azure/core-auth@^1.5.0": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.7.2.tgz#558b7cb7dd12b00beec07ae5df5907d74df1ebd9" + integrity sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-util" "^1.1.0" + tslib "^2.6.2" + +"@azure/core-client@^1.3.0", "@azure/core-client@^1.5.0", "@azure/core-client@^1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@azure/core-client/-/core-client-1.9.2.tgz#6fc69cee2816883ab6c5cdd653ee4f2ff9774f74" + integrity sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-auth" "^1.4.0" + "@azure/core-rest-pipeline" "^1.9.1" + "@azure/core-tracing" "^1.0.0" + "@azure/core-util" "^1.6.1" + "@azure/logger" "^1.0.0" + tslib "^2.6.2" + +"@azure/core-http-compat@^2.0.1": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@azure/core-http-compat/-/core-http-compat-2.1.2.tgz#d1585ada24ba750dc161d816169b33b35f762f0d" + integrity sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-client" "^1.3.0" + "@azure/core-rest-pipeline" "^1.3.0" + +"@azure/core-lro@^2.2.0": + version "2.7.2" + resolved "https://registry.yarnpkg.com/@azure/core-lro/-/core-lro-2.7.2.tgz#787105027a20e45c77651a98b01a4d3b01b75a08" + integrity sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-util" "^1.2.0" + "@azure/logger" "^1.0.0" + tslib "^2.6.2" + +"@azure/core-paging@^1.1.1": + version "1.6.2" + resolved "https://registry.yarnpkg.com/@azure/core-paging/-/core-paging-1.6.2.tgz#40d3860dc2df7f291d66350b2cfd9171526433e7" + integrity sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA== + dependencies: + tslib "^2.6.2" + +"@azure/core-rest-pipeline@^1.1.0", "@azure/core-rest-pipeline@^1.3.0", "@azure/core-rest-pipeline@^1.8.1", "@azure/core-rest-pipeline@^1.9.1": + version "1.16.2" + resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.2.tgz#3f71b09e45a65926cc598478b4f1bcd0fe67bf4b" + integrity sha512-Hnhm/PG9/SQ07JJyLDv3l9Qr8V3xgAe1hFoBYzt6LaalMxfL/ZqFaZf/bz5VN3pMcleCPwl8ivlS2Fjxq/iC8Q== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-auth" "^1.4.0" + "@azure/core-tracing" "^1.0.1" + "@azure/core-util" "^1.9.0" + "@azure/logger" "^1.0.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.0" + tslib "^2.6.2" + +"@azure/core-tracing@^1.0.0", "@azure/core-tracing@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@azure/core-tracing/-/core-tracing-1.1.2.tgz#065dab4e093fb61899988a1cdbc827d9ad90b4ee" + integrity sha512-dawW9ifvWAWmUm9/h+/UQ2jrdvjCJ7VJEuCJ6XVNudzcOwm53BFZH4Q845vjfgoUAM8ZxokvVNxNxAITc502YA== + dependencies: + tslib "^2.6.2" + +"@azure/core-util@^1.0.0", "@azure/core-util@^1.1.0", "@azure/core-util@^1.2.0", "@azure/core-util@^1.3.0", "@azure/core-util@^1.6.1", "@azure/core-util@^1.9.0": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.9.1.tgz#05ea9505c5cdf29c55ccf99a648c66ddd678590b" + integrity sha512-OLsq0etbHO1MA7j6FouXFghuHrAFGk+5C1imcpQ2e+0oZhYF07WLA+NW2Vqs70R7d+zOAWiWM3tbE1sXcDN66g== + dependencies: + "@azure/abort-controller" "^2.0.0" + tslib "^2.6.2" + +"@azure/identity@^4.2.1": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@azure/identity/-/identity-4.4.0.tgz#f2743e63d346000a70b0eed5a3b397dedd3984a7" + integrity sha512-oG6oFNMxUuoivYg/ElyZWVSZfw42JQyHbrp+lR7VJ1BYWsGzt34NwyDw3miPp1QI7Qm5+4KAd76wGsbHQmkpkg== + dependencies: + "@azure/abort-controller" "^1.0.0" + "@azure/core-auth" "^1.5.0" + "@azure/core-client" "^1.9.2" + "@azure/core-rest-pipeline" "^1.1.0" + "@azure/core-tracing" "^1.0.0" + "@azure/core-util" "^1.3.0" + "@azure/logger" "^1.0.0" + "@azure/msal-browser" "^3.14.0" + "@azure/msal-node" "^2.9.2" + events "^3.0.0" + jws "^4.0.0" + open "^8.0.0" + stoppable "^1.1.0" + tslib "^2.2.0" + +"@azure/keyvault-keys@^4.4.0": + version "4.8.0" + resolved "https://registry.yarnpkg.com/@azure/keyvault-keys/-/keyvault-keys-4.8.0.tgz#1513b3a187bb3a9a372b5980c593962fb793b2ad" + integrity sha512-jkuYxgkw0aaRfk40OQhFqDIupqblIOIlYESWB6DKCVDxQet1pyv86Tfk9M+5uFM0+mCs6+MUHU+Hxh3joiUn4Q== + dependencies: + "@azure/abort-controller" "^1.0.0" + "@azure/core-auth" "^1.3.0" + "@azure/core-client" "^1.5.0" + "@azure/core-http-compat" "^2.0.1" + "@azure/core-lro" "^2.2.0" + "@azure/core-paging" "^1.1.1" + "@azure/core-rest-pipeline" "^1.8.1" + "@azure/core-tracing" "^1.0.0" + "@azure/core-util" "^1.0.0" + "@azure/logger" "^1.0.0" + tslib "^2.2.0" + +"@azure/logger@^1.0.0": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@azure/logger/-/logger-1.1.3.tgz#09a8fd4850b9112865756e92d5e8b728ee457345" + integrity sha512-J8/cIKNQB1Fc9fuYqBVnrppiUtW+5WWJPCj/tAokC5LdSTwkWWttN+jsRgw9BLYD7JDBx7PceiqOBxJJ1tQz3Q== + dependencies: + tslib "^2.6.2" + +"@azure/msal-browser@^3.14.0": + version "3.19.1" + resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.19.1.tgz#c5e5a7996f95cadc11920bffa2bf6321e3a24555" + integrity sha512-pqYP2gK0GCEa4OxtOqlS+EdFQqhXV6ZuESgSTYWq2ABXyxBVVdd5KNuqgR5SU0OwI2V1YWdFVvLDe1487dyQ0g== + dependencies: + "@azure/msal-common" "14.13.1" + +"@azure/msal-common@14.13.1": + version "14.13.1" + resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.13.1.tgz#e296cf8cc556082af9c35d803496424e8a95d8b7" + integrity sha512-iUp3BYrsRZ4X3EiaZ2fDjNFjmtYMv9rEQd6c1op6ULn0HWk4ACvDmosL6NaBgWOhl1BAblIbd9vmB5/ilF8d4A== + +"@azure/msal-node@^2.9.2": + version "2.11.1" + resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.11.1.tgz#7fea67a1c6904301eb8853fae7df86c34306a9cc" + integrity sha512-8ECtug4RL+zsgh20VL8KYHjrRO3MJOeAKEPRXT2lwtiu5U3BdyIdBb50+QZthEkIi60K6pc/pdOx/k5Jp4sLng== + dependencies: + "@azure/msal-common" "14.13.1" + jsonwebtoken "^9.0.0" + uuid "^8.3.0" + +"@google-cloud/paginator@^3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-3.0.7.tgz#fb6f8e24ec841f99defaebf62c75c2e744dd419b" + integrity sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ== + dependencies: + arrify "^2.0.0" + extend "^3.0.2" + +"@google-cloud/projectify@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-2.1.1.tgz#ae6af4fee02d78d044ae434699a630f8df0084ef" + integrity sha512-+rssMZHnlh0twl122gXY4/aCrk0G1acBqkHFfYddtsqpYXGxA29nj9V5V9SfC+GyOG00l650f6lG9KL+EpFEWQ== + +"@google-cloud/promisify@^2.0.0": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-2.0.4.tgz#9d8705ecb2baa41b6b2673f3a8e9b7b7e1abc52a" + integrity sha512-j8yRSSqswWi1QqUGKVEKOG03Q7qOoZP6/h2zN2YO+F5h2+DHU0bSrHCK9Y7lo2DI9fBd8qGAw795sf+3Jva4yA== + +"@google-cloud/storage@^5.18.2": + version "5.20.5" + resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-5.20.5.tgz#1de71fc88d37934a886bc815722c134b162d335d" + integrity sha512-lOs/dCyveVF8TkVFnFSF7IGd0CJrTm91qiK6JLu+Z8qiT+7Ag0RyVhxZIWkhiACqwABo7kSHDm8FdH8p2wxSSw== + dependencies: + "@google-cloud/paginator" "^3.0.7" + "@google-cloud/projectify" "^2.0.0" + "@google-cloud/promisify" "^2.0.0" + abort-controller "^3.0.0" + arrify "^2.0.0" + async-retry "^1.3.3" + compressible "^2.0.12" + configstore "^5.0.0" + duplexify "^4.0.0" + ent "^2.2.0" + extend "^3.0.2" + gaxios "^4.0.0" + google-auth-library "^7.14.1" + hash-stream-validation "^0.2.2" + mime "^3.0.0" + mime-types "^2.0.8" + p-limit "^3.0.1" + pumpify "^2.0.0" + retry-request "^4.2.2" + stream-events "^1.0.4" + teeny-request "^7.1.3" + uuid "^8.0.0" + xdg-basedir "^4.0.0" + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@js-joda/core@^5.6.1": + version "5.6.3" + resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-5.6.3.tgz#41ae1c07de1ebe0f6dde1abcbc9700a09b9c6056" + integrity sha512-T1rRxzdqkEXcou0ZprN1q9yDRlvzCPLqmlNt5IIsGBzoEVgLCCYrKEwc84+TvsXuAc95VAZwtWD2zVsKPY4bcA== + +"@jsdevtools/ono@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + +"@mapbox/node-pre-gyp@^1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa" + integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + +"@one-ini/wasm@0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@one-ini/wasm/-/wasm-0.1.1.tgz#6013659736c9dbfccc96e8a9c2b3de317df39323" + integrity sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw== + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@sindresorhus/is@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" + integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + +"@szmarczak/http-timer@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" + integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== + dependencies: + defer-to-connect "^1.0.1" + +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + +"@types/debug@^4.1.8": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + +"@types/json-schema@^7.0.6": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/ms@*": + version "0.7.34" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" + integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== + +"@types/node@*", "@types/node@>=18": + version "20.14.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.11.tgz#09b300423343460455043ddd4d0ded6ac579b74b" + integrity sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA== + dependencies: + undici-types "~5.26.4" + +"@types/readable-stream@^4.0.0": + version "4.0.15" + resolved "https://registry.yarnpkg.com/@types/readable-stream/-/readable-stream-4.0.15.tgz#e6ec26fe5b02f578c60baf1fa9452e90957d2bfb" + integrity sha512-oAZ3kw+kJFkEqyh7xORZOku1YAKvsFTogRY8kVl4vHpEKiDkfnSA/My8haRE7fvmix5Zyy+1pwzOi7yycGLBJw== + dependencies: + "@types/node" "*" + safe-buffer "~5.1.1" + +"@types/validator@^13.7.17": + version "13.12.0" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.12.0.tgz#1fe4c3ae9de5cf5193ce64717c99ef2fa7d8756f" + integrity sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +abbrev@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-2.0.0.tgz#cf59829b8b4f03f89dda2771cb7f3653828c89bf" + integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +accepts@^1.3.7, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +agent-base@^7.0.2, agent-base@^7.1.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" + +ansi-align@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-regex@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1" + integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== + +ansi-regex@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + +ansi-styles@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +anymatch@~3.1.1, anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + dependencies: + call-bind "^1.0.5" + is-array-buffer "^3.0.4" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array.prototype.map@^1.0.1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/array.prototype.map/-/array.prototype.map-1.0.7.tgz#82fa4d6027272d1fca28a63bbda424d0185d78a7" + integrity sha512-XpcFfLoBEAhezrrNw1V+yLXkE7M6uR7xJEsxbG6c/V9v043qurwVJB9r9UTnoSioFDoz1i1VOydpWGmJpfVZbg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-array-method-boxes-properly "^1.0.0" + es-object-atoms "^1.0.0" + is-string "^1.0.7" + +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" + is-shared-array-buffer "^1.0.2" + +arrify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + +async-retry@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" + integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== + dependencies: + retry "0.13.1" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + +axios@^1.6.7: + version "1.7.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" + integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.0, base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +base64url@3.x.x: + version "3.0.1" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" + integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== + +bcrypt@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.1.1.tgz#0f732c6dcb4e12e5b70a25e326a72965879ba6e2" + integrity sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.11" + node-addon-api "^5.0.0" + +bignumber.js@^9.0.0: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bl@^6.0.11: + version "6.0.14" + resolved "https://registry.yarnpkg.com/bl/-/bl-6.0.14.tgz#b9ae9862118a3d2ebec999c5318466012314f96c" + integrity sha512-TJfbvGdL7KFGxTsEbsED7avqpFdY56q9IW0/aiytyheJzxST/+Io6cx/4Qx0K2/u0BPRDs65mjaQzYvMZeNocQ== + dependencies: + "@types/readable-stream" "^4.0.0" + buffer "^6.0.3" + inherits "^2.0.4" + readable-stream "^4.2.0" + +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +boxen@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" + integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^5.3.1" + chalk "^3.0.0" + cli-boxes "^2.2.0" + string-width "^4.1.0" + term-size "^2.1.0" + type-fest "^0.8.1" + widest-line "^3.1.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-writer@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" + integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +busboy@^0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" + integrity sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg== + dependencies: + dicer "0.2.5" + readable-stream "1.1.x" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cacheable-request@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" + integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^3.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^1.0.2" + +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +call-me-maybe@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" + integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== + +camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" + integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.4.0" + optionalDependencies: + fsevents "~2.1.2" + +chokidar@^3.2.2: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +cli-boxes@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== + +cli-color@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-2.0.4.tgz#d658080290968816b322248b7306fad2346fb2c8" + integrity sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA== + dependencies: + d "^1.0.1" + es5-ext "^0.10.64" + es6-iterator "^2.0.3" + memoizee "^0.4.15" + timers-ext "^0.1.7" + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone-response@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" + integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== + dependencies: + mimic-response "^1.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-support@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75" + integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== + +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + +compressible@^2.0.12: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +config-chain@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" + integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + +configstore@^5.0.0, configstore@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" + integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== + dependencies: + dot-prop "^5.2.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + unique-string "^2.0.0" + write-file-atomic "^3.0.0" + xdg-basedir "^4.0.0" + +console-control-strings@^1.0.0, console-control-strings@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cross-env@7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^7.0.0, cross-spawn@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + +csv-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/csv-parser/-/csv-parser-3.0.0.tgz#b88a6256d79e090a97a1b56451f9327b01d710e7" + integrity sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ== + dependencies: + minimist "^1.2.0" + +d@1, d@^1.0.1, d@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.2.tgz#2aefd554b81981e7dccf72d6842ae725cb17e5de" + integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== + dependencies: + es5-ext "^0.10.64" + type "^2.7.2" + +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4.1.1, debug@^4.3.4: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + +debug@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +debug@^3.2.6: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + +decompress-response@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + integrity sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA== + dependencies: + mimic-response "^1.0.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +defer-to-connect@^1.0.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" + integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== + +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +define-properties@^1.1.2, define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + +denque@^1.4.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" + integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-libc@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + +dicer@0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" + integrity sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg== + dependencies: + readable-stream "1.1.x" + streamsearch "0.1.2" + +diff@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +doctrine@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dot-prop@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== + dependencies: + is-obj "^2.0.0" + +dottie@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/dottie/-/dottie-2.0.6.tgz#34564ebfc6ec5e5772272d466424ad5b696484d4" + integrity sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA== + +duplexer3@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e" + integrity sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA== + +duplexify@^4.0.0, duplexify@^4.1.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.3.tgz#a07e1c0d0a2c001158563d32592ba58bddb0236f" + integrity sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA== + dependencies: + end-of-stream "^1.4.1" + inherits "^2.0.3" + readable-stream "^3.1.1" + stream-shift "^1.0.2" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +editorconfig@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-1.0.4.tgz#040c9a8e9a6c5288388b87c2db07028aa89f53a3" + integrity sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q== + dependencies: + "@one-ini/wasm" "0.1.1" + commander "^10.0.0" + minimatch "9.0.1" + semver "^7.5.3" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +ent@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.1.tgz#68dc99a002f115792c26239baedaaea9e70c0ca2" + integrity sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A== + dependencies: + punycode "^1.4.1" + +es-abstract@^1.17.0-next.1, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.2: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + dependencies: + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" + is-string "^1.0.7" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" + +es-array-method-boxes-properly@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" + integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-get-iterator@^1.0.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.7" + isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" + +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14, es5-ext@~0.10.2: + version "0.10.64" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714" + integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + esniff "^2.0.1" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c" + integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg== + dependencies: + d "^1.0.2" + ext "^1.7.0" + +es6-weak-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" + integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== + dependencies: + d "1" + es5-ext "^0.10.46" + es6-iterator "^2.0.3" + es6-symbol "^3.1.1" + +escalade@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +escape-goat@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" + integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== + dependencies: + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA== + dependencies: + d "1" + es5-ext "~0.10.14" + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +events@^3.0.0, events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +express@4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +ext@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + +extend@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fast-text-encoding@^1.0.0: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" + integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +flat@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.1.tgz#a392059cc382881ff98642f5da4dde0a959f309b" + integrity sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA== + dependencies: + is-buffer "~2.0.3" + +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +foreground-child@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.2.1.tgz#767004ccf3a5b30df39bed90718bab43fe0a59f7" + integrity sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formidable@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" + integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2, fresh@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.1, function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + +gaxios@^4.0.0: + version "4.3.3" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-4.3.3.tgz#d44bdefe52d34b6435cc41214fdb160b64abfc22" + integrity sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA== + dependencies: + abort-controller "^3.0.0" + extend "^3.0.2" + https-proxy-agent "^5.0.0" + is-stream "^2.0.0" + node-fetch "^2.6.7" + +gcp-metadata@^4.2.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-4.3.1.tgz#fb205fe6a90fef2fd9c85e6ba06e5559ee1eefa9" + integrity sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A== + dependencies: + gaxios "^4.0.0" + json-bigint "^1.0.0" + +generate-function@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== + dependencies: + is-property "^1.0.2" + +get-caller-file@^2.0.1, get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-stream@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + dependencies: + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + +glob-parent@~5.1.0, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^10.3.3: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-dirs@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.1.0.tgz#e9046a49c806ff04d6c1825e196c8f0091e8df4d" + integrity sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ== + dependencies: + ini "1.3.7" + +globalthis@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + +google-auth-library@^7.14.1: + version "7.14.1" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-7.14.1.tgz#e3483034162f24cc71b95c8a55a210008826213c" + integrity sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA== + dependencies: + arrify "^2.0.0" + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + fast-text-encoding "^1.0.0" + gaxios "^4.0.0" + gcp-metadata "^4.2.0" + gtoken "^5.0.4" + jws "^4.0.0" + lru-cache "^6.0.0" + +google-p12-pem@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.1.4.tgz#123f7b40da204de4ed1fbf2fd5be12c047fc8b3b" + integrity sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg== + dependencies: + node-forge "^1.3.1" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +got@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" + integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== + dependencies: + "@sindresorhus/is" "^0.14.0" + "@szmarczak/http-timer" "^1.1.2" + cacheable-request "^6.0.0" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^4.1.0" + lowercase-keys "^1.0.1" + mimic-response "^1.0.1" + p-cancelable "^1.0.0" + to-readable-stream "^1.0.0" + url-parse-lax "^3.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +gtoken@^5.0.4: + version "5.3.2" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.3.2.tgz#deb7dc876abe002178e0515e383382ea9446d58f" + integrity sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ== + dependencies: + gaxios "^4.0.0" + google-p12-pem "^3.1.3" + jws "^4.0.0" + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1, has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.0, has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== + +has-yarn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" + integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== + +hash-stream-validation@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512" + integrity sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ== + +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +helmet@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-4.1.1.tgz#751f0e273d809ace9c172073e0003bed27d27a4a" + integrity sha512-Avg4XxSBrehD94mkRwEljnO+6RZx7AGfk8Wa6K1nxaU+hbXlFOhlOIMgPfFqOYQB/dBCsTpootTGuiOG+CHiQA== + +http-cache-semantics@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + +http-proxy-agent@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +https-proxy-agent@^7.0.0: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== + dependencies: + agent-base "^7.0.2" + debug "4" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.6.2, iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + +import-lazy@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + integrity sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A== + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflection@^1.13.4: + version "1.13.4" + resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.13.4.tgz#65aa696c4e2da6225b148d7a154c449366633a32" + integrity sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" + integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== + +ini@^1.3.4, ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +internal-slot@^1.0.4, internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arguments@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-buffer@~2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-core-module@^2.13.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea" + integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA== + dependencies: + hasown "^2.0.2" + +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + dependencies: + is-typed-array "^1.1.13" + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-installed-globally@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" + integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== + dependencies: + global-dirs "^2.0.1" + is-path-inside "^3.0.1" + +is-map@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + +is-npm@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" + integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-inside@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== + +is-promise@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" + integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== + +is-property@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-set@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + dependencies: + call-bind "^1.0.7" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + +is-typedarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +is-yarn-global@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" + integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +iterate-iterator@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.2.tgz#551b804c9eaa15b847ea6a7cdc2f5bf1ec150f91" + integrity sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw== + +iterate-value@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/iterate-value/-/iterate-value-1.0.2.tgz#935115bd37d006a52046535ebc8d07e9c9337f57" + integrity sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ== + dependencies: + es-get-iterator "^1.0.2" + iterate-iterator "^1.0.1" + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +js-beautify@^1.14.5: + version "1.15.1" + resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.15.1.tgz#4695afb508c324e1084ee0b952a102023fc65b64" + integrity sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA== + dependencies: + config-chain "^1.1.13" + editorconfig "^1.0.4" + glob "^10.3.3" + js-cookie "^3.0.5" + nopt "^7.2.0" + +js-cookie@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" + integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== + +js-md4@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/js-md4/-/js-md4-0.3.2.tgz#cd3b3dc045b0c404556c81ddb5756c23e59d7cf5" + integrity sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA== + +js-yaml@3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + +json-buffer@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" + integrity sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ== + +json2csv@^5.0.7: + version "5.0.7" + resolved "https://registry.yarnpkg.com/json2csv/-/json2csv-5.0.7.tgz#f3a583c25abd9804be873e495d1e65ad8d1b54ae" + integrity sha512-YRZbUnyaJZLZUJSRi2G/MqahCyRv9n/ds+4oIetjDF3jWQA7AG7iSeKTiZiCNqtMZM7HDyt0e/W6lEnoGEmMGA== + dependencies: + commander "^6.1.0" + jsonparse "^1.3.1" + lodash.get "^4.4.2" + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonparse@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== + +jsonwebtoken@8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + +jsonwebtoken@^9.0.0: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jwa@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== + dependencies: + json-buffer "3.0.0" + +latest-version@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" + integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== + dependencies: + package-json "^6.3.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.mergewith@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +lodash@4.17.21, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== + dependencies: + chalk "^4.0.0" + +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +lru-cache@^7.14.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + +lru-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" + integrity sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ== + dependencies: + es5-ext "~0.10.2" + +make-dir@^3.0.0, make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memoizee@^0.4.15: + version "0.4.17" + resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.17.tgz#942a5f8acee281fa6fb9c620bddc57e3b7382949" + integrity sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA== + dependencies: + d "^1.0.2" + es5-ext "^0.10.64" + es6-weak-map "^2.0.3" + event-emitter "^0.3.5" + is-promise "^2.2.2" + lru-queue "^0.1.0" + next-tick "^1.1.0" + timers-ext "^0.1.7" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-descriptors@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +methods@^1.1.2, methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +"mime-db@>= 1.43.0 < 2": + version "1.53.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" + integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== + +mime-types@^2.0.8, mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0, mime@^1.3.4: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + +mimic-response@^1.0.0, mimic-response@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +minimatch@3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimatch@9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.1.tgz#8a555f541cf976c622daf078bb28f29fb927c253" + integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass@^3.0.0: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@^0.5.4: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mocha@8.1.3: + version "8.1.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.1.3.tgz#5e93f873e35dfdd69617ea75f9c68c2ca61c2ac5" + integrity sha512-ZbaYib4hT4PpF4bdSO2DohooKXIn4lDeiYqB+vTmCdr6l2woW0b6H3pf5x4sM5nwQMru9RvjjHYWVGltR50ZBw== + dependencies: + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.4.2" + debug "4.1.1" + diff "4.0.2" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.1.6" + growl "1.10.5" + he "1.2.0" + js-yaml "3.14.0" + log-symbols "4.0.0" + minimatch "3.0.4" + ms "2.1.2" + object.assign "4.1.0" + promise.allsettled "1.0.2" + serialize-javascript "4.0.0" + strip-json-comments "3.0.1" + supports-color "7.1.0" + which "2.0.2" + wide-align "1.1.3" + workerpool "6.0.0" + yargs "13.3.2" + yargs-parser "13.1.2" + yargs-unparser "1.6.1" + +moment-timezone@^0.5.43: + version "0.5.45" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.45.tgz#cb685acd56bac10e69d93c536366eb65aa6bcf5c" + integrity sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ== + dependencies: + moment "^2.29.4" + +moment@2.30.1, moment@^2.29.4: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multer@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4.tgz#e2bc6cac0df57a8832b858d7418ccaa8ebaf7d8c" + integrity sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw== + dependencies: + append-field "^1.0.0" + busboy "^0.2.11" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + on-finished "^2.3.0" + type-is "^1.6.4" + xtend "^4.0.0" + +mysql2@2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-2.2.5.tgz#72624ffb4816f80f96b9c97fedd8c00935f9f340" + integrity sha512-XRqPNxcZTpmFdXbJqb+/CtYVLCx14x1RTeNMD4954L331APu75IC74GDqnZMEt1kwaXy6TySo55rF2F3YJS78g== + dependencies: + denque "^1.4.1" + generate-function "^2.3.1" + iconv-lite "^0.6.2" + long "^4.0.0" + lru-cache "^6.0.0" + named-placeholders "^1.1.2" + seq-queue "^0.0.5" + sqlstring "^2.3.2" + +named-placeholders@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.3.tgz#df595799a36654da55dda6152ba7a137ad1d9351" + integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w== + dependencies: + lru-cache "^7.14.1" + +native-duplexpair@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/native-duplexpair/-/native-duplexpair-1.0.0.tgz#7899078e64bf3c8a3d732601b3d40ff05db58fa0" + integrity sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + +node-addon-api@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" + integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== + +node-fetch@^2.6.1, node-fetch@^2.6.7: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-forge@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-mocks-http@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/node-mocks-http/-/node-mocks-http-1.9.0.tgz#6000c570fc4b809603782309be81c73a71d85b71" + integrity sha512-ILf7Ws8xyX9Rl2fLZ7xhZBovrRwgaP84M13esndP6V17M/8j25TpwNzb7Im8U9XCo6fRhdwqiQajWXpsas/E6w== + dependencies: + accepts "^1.3.7" + depd "^1.1.0" + fresh "^0.5.2" + merge-descriptors "^1.0.1" + methods "^1.1.2" + mime "^1.3.4" + parseurl "^1.3.3" + range-parser "^1.2.0" + type-is "^1.6.18" + +nodemailer@6.9.9: + version "6.9.9" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.9.tgz#4549bfbf710cc6addec5064dd0f19874d24248d9" + integrity sha512-dexTll8zqQoVJEZPwQAKzxxtFn0qTnjdQTchoU6Re9BUUGBJiOy3YMn/0ShTW6J5M0dfQ1NeDeRTTl4oIWgQMA== + +nodemon@2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.5.tgz#df67fe1fd1312ddb0c1e393ae2cf55aacdcec2f3" + integrity sha512-6/jqtZvJdk092pVnD2AIH19KQ9GQZAKOZVy/yT1ueL6aoV+Ix7a1lVZStXzvEh0fP4zE41DDWlkVoHjR6WlozA== + dependencies: + chokidar "^3.2.2" + debug "^3.2.6" + ignore-by-default "^1.0.1" + minimatch "^3.0.4" + pstree.remy "^1.1.7" + semver "^5.7.1" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.3" + update-notifier "^4.1.0" + +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + +nopt@^7.2.0: + version "7.2.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-7.2.1.tgz#1cac0eab9b8e97c9093338446eddd40b2c8ca1e7" + integrity sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w== + dependencies: + abbrev "^2.0.0" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-url@^4.1.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" + integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== + +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" + +oauth@0.10.x: + version "0.10.0" + resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.10.0.tgz#3551c4c9b95c53ea437e1e21e46b649482339c58" + integrity sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q== + +oauth@0.9.x: + version "0.9.15" + resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" + integrity sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA== + +object-assign@^4, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +object-keys@^1.0.11, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +on-finished@2.4.1, on-finished@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +open@^8.0.0: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +p-cancelable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" + integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== + +p-limit@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.1, p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +package-json-from-dist@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" + integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== + +package-json@^6.3.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" + integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== + dependencies: + got "^9.6.0" + registry-auth-token "^4.0.0" + registry-url "^5.0.0" + semver "^6.2.0" + +packet-reader@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" + integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== + +parseurl@^1.3.3, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +passport-google-oauth2@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/passport-google-oauth2/-/passport-google-oauth2-0.2.0.tgz#fc9ea59e7091f02e24fd16d6be9257ea982ebbc3" + integrity sha512-62EdPtbfVdc55nIXi0p1WOa/fFMM8v/M8uQGnbcXA4OexZWCnfsEi3wo2buag+Is5oqpuHzOtI64JpHk0Xi5RQ== + dependencies: + passport-oauth2 "^1.1.2" + +passport-jwt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.1.tgz#c443795eff322c38d173faa0a3c481479646ec3d" + integrity sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ== + dependencies: + jsonwebtoken "^9.0.0" + passport-strategy "^1.0.0" + +passport-microsoft@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/passport-microsoft/-/passport-microsoft-0.1.0.tgz#dc72c1a38b294d74f4dc55fe93f52e25cb9aa5b4" + integrity sha512-0giBDgE1fnR5X84zJZkQ11hnKVrzEgViwRO6RGsormK9zTxFQmN/UHMTDbIpvhk989VqALewB6Pk1R5vNr3GHw== + dependencies: + passport-oauth2 "1.2.0" + pkginfo "0.2.x" + +passport-oauth2@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.2.0.tgz#49613a3eca85c7a1e65bf1019e2b6b80a10c8ac2" + integrity sha512-6128N+n/MOrJdXxdC2q/PVKXtqgihGFIeup+9bsPybAvMPOUKqdGhh9ZIzZF8rFKJOlxUP9fgP3H0JQe18n0rg== + dependencies: + oauth "0.9.x" + passport-strategy "1.x.x" + uid2 "0.0.x" + +passport-oauth2@^1.1.2: + version "1.8.0" + resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.8.0.tgz#55725771d160f09bbb191828d5e3d559eee079c8" + integrity sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA== + dependencies: + base64url "3.x.x" + oauth "0.10.x" + passport-strategy "1.x.x" + uid2 "0.0.x" + utils-merge "1.x.x" + +passport-strategy@1.x.x, passport-strategy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== + +passport@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.7.0.tgz#3688415a59a48cf8068417a8a8092d4492ca3a05" + integrity sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + utils-merge "^1.0.1" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== + +pg-connection-string@^2.4.0, pg-connection-string@^2.6.1: + version "2.6.4" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.4.tgz#f543862adfa49fa4e14bc8a8892d2a84d754246d" + integrity sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA== + +pg-hstore@2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/pg-hstore/-/pg-hstore-2.3.4.tgz#4425e3e2a3e15d2a334c35581186c27cf2e9b8dd" + integrity sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA== + dependencies: + underscore "^1.13.1" + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-pool@^3.2.1: + version "3.6.2" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.2.tgz#3a592370b8ae3f02a7c8130d245bc02fa2c5f3f2" + integrity sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg== + +pg-protocol@^1.3.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.1.tgz#21333e6d83b01faaebfe7a33a7ad6bfd9ed38cb3" + integrity sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg== + +pg-types@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg@8.4.1: + version "8.4.1" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.4.1.tgz#06cfb6208ae787a869b2f0022da11b90d13d933e" + integrity sha512-NRsH0aGMXmX1z8Dd0iaPCxWUw4ffu+lIAmGm+sTCwuDDWkpEgRCAHZYDwqaNhC5hG5DRMOjSUFasMWhvcmLN1A== + dependencies: + buffer-writer "2.0.0" + packet-reader "1.0.0" + pg-connection-string "^2.4.0" + pg-pool "^3.2.1" + pg-protocol "^1.3.0" + pg-types "^2.1.0" + pgpass "1.x" + +pgpass@1.x: + version "1.0.5" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" + integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== + dependencies: + split2 "^4.1.0" + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkginfo@0.2.x: + version "0.2.3" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.2.3.tgz#7239c42a5ef6c30b8f328439d9b9ff71042490f8" + integrity sha512-7W7wTrE/NsY8xv/DTGjwNIyNah81EQH0MWcTzrHL6pOpMocOGZc0Mbdz9aXxSrp+U0mSmkU8jrNCDCfUs3sOBg== + +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + integrity sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + +promise.allsettled@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.2.tgz#d66f78fbb600e83e863d893e98b3d4376a9c47c9" + integrity sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg== + dependencies: + array.prototype.map "^1.0.1" + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + iterate-value "^1.0.0" + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +pstree.remy@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-2.0.1.tgz#abfc7b5a621307c728b551decbbefb51f0e4aa1e" + integrity sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw== + dependencies: + duplexify "^4.1.1" + inherits "^2.0.3" + pump "^3.0.0" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + +pupa@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62" + integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A== + dependencies: + escape-goat "^2.0.0" + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.0, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc@1.2.8, rc@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@1.1.x: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.2.2: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1, readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@^4.2.0: + version "4.5.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" + integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + +readdirp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== + dependencies: + picomatch "^2.2.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +regexp.prototype.flags@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== + dependencies: + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" + +registry-auth-token@^4.0.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.2.tgz#f02d49c3668884612ca031419491a13539e21fac" + integrity sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg== + dependencies: + rc "1.2.8" + +registry-url@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" + integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== + dependencies: + rc "^1.2.8" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +resolve@^1.22.1: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +responselike@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" + integrity sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ== + dependencies: + lowercase-keys "^1.0.0" + +retry-as-promised@^7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-7.0.4.tgz#9df73adaeea08cb2948b9d34990549dc13d800a2" + integrity sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA== + +retry-request@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-4.2.2.tgz#b7d82210b6d2651ed249ba3497f07ea602f1a903" + integrity sha512-xA93uxUD/rogV7BV59agW/JHPGXeREMWiZc9jhcwY4YdZ7QOtC7qbomYg0n4wyk2lJhggjvKvhNX8wln/Aldhg== + dependencies: + debug "^4.1.1" + extend "^3.0.2" + +retry@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + has-symbols "^1.0.3" + isarray "^2.0.5" + +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver-diff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" + integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== + dependencies: + semver "^6.3.0" + +semver@^5.6.0, semver@^5.7.1: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.5, semver@^7.5.3, semver@^7.5.4: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +seq-queue@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e" + integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q== + +sequelize-cli@6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/sequelize-cli/-/sequelize-cli-6.6.2.tgz#8d838b25c988cf136914cdc3843e19d88c3dcb67" + integrity sha512-V8Oh+XMz2+uquLZltZES6MVAD+yEnmMfwfn+gpXcDiwE3jyQygLt4xoI0zG8gKt6cRcs84hsKnXAKDQjG/JAgg== + dependencies: + cli-color "^2.0.3" + fs-extra "^9.1.0" + js-beautify "^1.14.5" + lodash "^4.17.21" + resolve "^1.22.1" + umzug "^2.3.0" + yargs "^16.2.0" + +sequelize-json-schema@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/sequelize-json-schema/-/sequelize-json-schema-2.1.1.tgz#a82d3813925e81485d76ce291f4ff5c8cb2ae492" + integrity sha512-yCGaHnmQQeL6MQ/fOxhkR5C2aOGZyTD6OrgjP4yw1rbuujuIUVdzWN3AsC6r6AvlGZ3EUBBbCJHKl8OIFFES4Q== + +sequelize-pool@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-7.1.0.tgz#210b391af4002762f823188fd6ecfc7413020768" + integrity sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg== + +sequelize@6.35.2: + version "6.35.2" + resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.35.2.tgz#9276d24055a9a07bd6812c89ab402659f5853e70" + integrity sha512-EdzLaw2kK4/aOnWQ7ed/qh3B6/g+1DvmeXr66RwbcqSm/+QRS9X0LDI5INBibsy4eNJHWIRPo3+QK0zL+IPBHg== + dependencies: + "@types/debug" "^4.1.8" + "@types/validator" "^13.7.17" + debug "^4.3.4" + dottie "^2.0.6" + inflection "^1.13.4" + lodash "^4.17.21" + moment "^2.29.4" + moment-timezone "^0.5.43" + pg-connection-string "^2.6.1" + retry-as-promised "^7.0.4" + semver "^7.5.4" + sequelize-pool "^7.1.0" + toposort-class "^1.0.1" + uuid "^8.3.2" + validator "^13.9.0" + wkx "^0.5.0" + +serialize-javascript@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +split2@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +sqlite@4.0.15: + version "4.0.15" + resolved "https://registry.yarnpkg.com/sqlite/-/sqlite-4.0.15.tgz#071e0577afb327fbd74a75354ea15964378392e3" + integrity sha512-irPPTrbVoDvwzRGpe0v8vxpNwMl+q0tXQzffQTcCUnaJzQFO0hfLLvFwGDKxd6vYBuvEr3uvPkObVoGOvVsmzA== + +sqlstring@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.3.tgz#2ddc21f03bce2c387ed60680e739922c65751d0c" + integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +stop-iteration-iterator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" + integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== + dependencies: + internal-slot "^1.0.4" + +stoppable@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b" + integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw== + +stream-events@^1.0.4, stream-events@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" + integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg== + dependencies: + stubs "^3.0.0" + +stream-shift@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" + integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== + +streamsearch@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + integrity sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA== + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" + +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string_decoder@^1.1.1, string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow== + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-json-comments@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" + integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + +stubs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" + integrity sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw== + +supports-color@7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + +supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +swagger-jsdoc@^6.2.8: + version "6.2.8" + resolved "https://registry.yarnpkg.com/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz#6d33d9fb07ff4a7c1564379c52c08989ec7d0256" + integrity sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ== + dependencies: + commander "6.2.0" + doctrine "3.0.0" + glob "7.1.6" + lodash.mergewith "^4.6.2" + swagger-parser "^10.0.3" + yaml "2.0.0-1" + +swagger-parser@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/swagger-parser/-/swagger-parser-10.0.3.tgz#04cb01c18c3ac192b41161c77f81e79309135d03" + integrity sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg== + dependencies: + "@apidevtools/swagger-parser" "10.0.3" + +swagger-ui-dist@>=5.0.0: + version "5.17.14" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz#e2c222e5bf9e15ccf80ec4bc08b4aaac09792fd6" + integrity sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw== + +swagger-ui-express@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz#fb8c1b781d2793a6bd2f8a205a3f4bd6fa020dd8" + integrity sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA== + dependencies: + swagger-ui-dist ">=5.0.0" + +tar@^6.1.11: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +tedious@^18.2.4: + version "18.2.4" + resolved "https://registry.yarnpkg.com/tedious/-/tedious-18.2.4.tgz#c33986f2561b4fde92bb9df70f44ae1a14f71b46" + integrity sha512-+6Nzn/aURTQ+8OxLAJ8fKK5Fbb84HRTI3bHiAC3ZzBKrBg9BHtcHxjmlIni5Zn46hzKiZ5WrDMSwDH8oIYjV8w== + dependencies: + "@azure/identity" "^4.2.1" + "@azure/keyvault-keys" "^4.4.0" + "@js-joda/core" "^5.6.1" + "@types/node" ">=18" + bl "^6.0.11" + iconv-lite "^0.6.3" + js-md4 "^0.3.2" + native-duplexpair "^1.0.0" + sprintf-js "^1.1.3" + +teeny-request@^7.1.3: + version "7.2.0" + resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-7.2.0.tgz#41347ece068f08d741e7b86df38a4498208b2633" + integrity sha512-SyY0pek1zWsi0LRVAALem+avzMLc33MKW/JLLakdP4s9+D7+jHcy5x6P+h94g2QNZsAqQNfX5lsbd3WSeJXrrw== + dependencies: + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + node-fetch "^2.6.1" + stream-events "^1.0.5" + uuid "^8.0.0" + +term-size@^2.1.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" + integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg== + +timers-ext@^0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.8.tgz#b4e442f10b7624a29dd2aa42c295e257150cf16c" + integrity sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww== + dependencies: + es5-ext "^0.10.64" + next-tick "^1.1.0" + +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +toposort-class@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988" + integrity sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg== + +touch@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694" + integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +tslib@^2.2.0, tslib@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +type-is@^1.6.18, type-is@^1.6.4, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +type@^2.7.2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.3.tgz#436981652129285cc3ba94f392886c2637ea0486" + integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ== + +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" + +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +uid2@0.0.x: + version "0.0.4" + resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.4.tgz#033f3b1d5d32505f5ce5f888b9f3b667123c0a44" + integrity sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA== + +umzug@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/umzug/-/umzug-2.3.0.tgz#0ef42b62df54e216b05dcaf627830a6a8b84a184" + integrity sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw== + dependencies: + bluebird "^3.7.2" + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +undefsafe@^2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + +underscore@^1.13.1: + version "1.13.6" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" + integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-notifier@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.3.tgz#be86ee13e8ce48fb50043ff72057b5bd598e1ea3" + integrity sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A== + dependencies: + boxen "^4.2.0" + chalk "^3.0.0" + configstore "^5.0.1" + has-yarn "^2.1.0" + import-lazy "^2.1.0" + is-ci "^2.0.0" + is-installed-globally "^0.3.1" + is-npm "^4.0.0" + is-yarn-global "^0.3.0" + latest-version "^5.0.0" + pupa "^2.0.1" + semver-diff "^3.1.1" + xdg-basedir "^4.0.0" + +url-parse-lax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" + integrity sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ== + dependencies: + prepend-http "^2.0.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1, utils-merge@1.x.x, utils-merge@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.0.0, uuid@^8.3.0, uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +validator@^13.7.0, validator@^13.9.0: + version "13.12.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.12.0.tgz#7d78e76ba85504da3fee4fd1922b385914d4b35f" + integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-module@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" + integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== + +which-typed-array@^1.1.14, which-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + +which@2.0.2, which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wide-align@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +wide-align@^1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + +wkx@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.5.0.tgz#c6c37019acf40e517cc6b94657a25a3d4aa33e8c" + integrity sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg== + dependencies: + "@types/node" "*" + +workerpool@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.0.0.tgz#85aad67fa1a2c8ef9386a1b43539900f61d03d58" + integrity sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA== + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@2.0.0-1: + version "2.0.0-1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.0.0-1.tgz#8c3029b3ee2028306d5bcf396980623115ff8d18" + integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ== + +yargs-parser@13.1.2, yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^15.0.1: + version "15.0.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.3.tgz#316e263d5febe8b38eef61ac092b33dfcc9b1115" + integrity sha512-/MVEVjTXy/cGAjdtQf8dW3V9b97bPN7rNn8ETj6BmAQL7ibC7O1Q9SPJbGjgh3SlwoBNXMzj/ZGIj8mBgl12YA== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.1.tgz#bd4b0ee05b4c94d058929c32cb09e3fce71d3c5f" + integrity sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA== + dependencies: + camelcase "^5.3.1" + decamelize "^1.2.0" + flat "^4.1.0" + is-plain-obj "^1.1.0" + yargs "^14.2.3" + +yargs@13.3.2: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2" + +yargs@^14.2.3: + version "14.2.3" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414" + integrity sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg== + dependencies: + cliui "^5.0.0" + decamelize "^1.2.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^15.0.1" + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +z-schema@^5.0.1: + version "5.0.6" + resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-5.0.6.tgz#46d6a687b15e4a4369e18d6cb1c7b8618fc256c5" + integrity sha512-+XR1GhnWklYdfr8YaZv/iu+vY+ux7V5DS5zH1DQf6bO5ufrt/5cgNhVO5qyhsjFXvsqQb/f08DWE9b6uPscyAg== + dependencies: + lodash.get "^4.4.2" + lodash.isequal "^4.5.0" + validator "^13.7.0" + optionalDependencies: + commander "^10.0.0" diff --git a/cloudbuild.yaml b/cloudbuild.yaml new file mode 100644 index 0000000..6e1a402 --- /dev/null +++ b/cloudbuild.yaml @@ -0,0 +1,14 @@ +steps: + - name: 'gcr.io/cloud-builders/docker' + entrypoint: 'bash' + args: ['-c', 'docker pull gcr.io/fldemo-315215/title-enterprise-dashboards-finance-hr-ops-34407-dev:latest || exit 0'] + - name: 'gcr.io/cloud-builders/docker' + args: [ + 'build', + '-t', 'gcr.io/fldemo-315215/title-enterprise-dashboards-finance-hr-ops-34407-dev:latest', + '--file', 'Dockerfile.dev', + '--cache-from', 'gcr.io/fldemo-315215/title-enterprise-dashboards-finance-hr-ops-34407-dev:latest', + '.' + ] +images: ['gcr.io/fldemo-315215/title-enterprise-dashboards-finance-hr-ops-34407-dev:latest'] +logsBucket: 'gs://fldemo-315215-cloudbuild-logs' \ No newline at end of file diff --git a/docker/.gitignore b/docker/.gitignore new file mode 100644 index 0000000..adbb97d --- /dev/null +++ b/docker/.gitignore @@ -0,0 +1 @@ +data/ \ No newline at end of file diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..69d1021 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,46 @@ +## Description: + + The project contains the **docker folder** and the `Dockerfile`. + + The `Dockerfile` is used to Deploy the project to Google Cloud. + + The **docker folder** contains a couple of helper scripts: + +- `docker-compose.yml` (all our services: web, backend, db are described here) +- `start-backend.sh` (starts backend, but only after the database) +- `wait-for-it.sh` (imported from https://github.com/vishnubob/wait-for-it) + + > To avoid breaking the application, we recommend you don't edit the following files: everything that includes the **docker folder** and `Dokerfile`. + + + ## Run services: + + 1. Install docker compose (https://docs.docker.com/compose/install/) + + 2. Move to `docker` folder. All next steps should be done from this folder. + + ``` cd docker ``` + + 3. Make executables from `wait-for-it.sh` and `start-backend.sh`: + + ``` chmod +x start-backend.sh && chmod +x wait-for-it.sh ``` + + 4. Download dependend projects for services. + + 5. Review the docker-compose.yml file. Make sure that all services have Dockerfiles. Only db service doesn't require a Dockerfile. + + 6. Make sure you have needed ports (see them in `ports`) available on your local machine. + + 7. Start services: + + 7.1. With an empty database `rm -rf data && docker-compose up` + + 7.2. With a stored (from previus runs) database data `docker-compose up` + + 8. Check http://localhost:3000 + + 9. Stop services: + + 9.1. Just press `Ctr+C` + + diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..54b6db5 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,56 @@ + + +version: "3.9" +services: + web: + image: frontend + build: ../frontend + stdin_open: true # docker run -i + tty: true # docker run -t + ports: + - "3000:3000" + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + db: + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + image: postgres + volumes: + - ./data/db:/var/lib/postgresql/data + environment: + - POSTGRES_HOST_AUTH_METHOD=trust + - POSTGRES_DB=db_title_enterprise_dashboards_finance_hr_ops + ports: + - "5432:5432" + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + + backend: + image: backend + volumes: + - ./wait-for-it.sh:/usr/src/app/wait-for-it.sh + - ./start-backend.sh:/usr/src/app/start-backend.sh + build: ../backend + environment: + - DB_HOST=db + ports: + - "8080:8080" + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + depends_on: + - "db" + + command: ["bash", "./wait-for-it.sh", "db:5432", "--timeout=0", "--strict", "--", "bash", "./start-backend.sh"] + diff --git a/docker/start-backend.sh b/docker/start-backend.sh new file mode 100644 index 0000000..fb353bf --- /dev/null +++ b/docker/start-backend.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +yarn start diff --git a/docker/wait-for-it.sh b/docker/wait-for-it.sh new file mode 100644 index 0000000..d990e0d --- /dev/null +++ b/docker/wait-for-it.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 0000000..444b61f --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,10 @@ +module.exports = { + extends: [ + 'next/core-web-vitals', + 'plugin:@typescript-eslint/recommended', + 'eslint-config-prettier', + ], + rules: { + 'react/no-children-prop': 'off', + }, +}; diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..fdc0491 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,33 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local.js env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel +/.idea/ diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..cedf9c7 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,10 @@ +{ + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "quoteProps": "as-needed", + "jsxSingleQuote": true, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always" +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..56e10d0 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,19 @@ +FROM node:20.15.1-alpine + +# Create app directory +WORKDIR /usr/src/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ + +RUN yarn install +# If you are building your code for production +# RUN npm ci --only=production + +# Bundle app source +COPY . . + +EXPOSE 3000 +CMD [ "yarn", "dev" ] \ No newline at end of file diff --git a/frontend/LICENSE-justboil b/frontend/LICENSE-justboil new file mode 100644 index 0000000..798238d --- /dev/null +++ b/frontend/LICENSE-justboil @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019-current JustBoil.me (https://justboil.me) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..11b26f1 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,91 @@ +# Title Enterprise Dashboards Finance HR Ops + +## This project was generated by Flatlogic Platform. + +## Install + +`cd` to project's dir and run `npm install` + +### Builds + +Build are handled by Next.js CLI — [Info](https://nextjs.org/docs/api-reference/cli) + +### Hot-reloads for development + +``` +npm run dev +``` + +### Builds and minifies for production + +``` +npm run build +``` + +### Exports build for static hosts + +``` +npm run export +``` + +### Lint + +``` +npm run lint +``` + +### Format with prettier + +``` +npm run format +``` + +## Support + +For any additional information please refer to [Flatlogic homepage](https://flatlogic.com). + +## To start the project with Docker: + +### Description: + +The project contains the **docker folder** and the `Dockerfile`. + +The `Dockerfile` is used to Deploy the project to Google Cloud. + +The **docker folder** contains a couple of helper scripts: + +- `docker-compose.yml` (all our services: web, backend, db are described here) +- `start-backend.sh` (starts backend, but only after the database) +- `wait-for-it.sh` (imported from https://github.com/vishnubob/wait-for-it) + + > To avoid breaking the application, we recommend you don't edit the following files: everything that includes the **docker folder** and `Dokerfile`. + +### Run services: + +1. Install docker compose (https://docs.docker.com/compose/install/) + +2. Move to `docker` folder. All next steps should be done from this folder. + + `cd docker` + +3. Make executables from `wait-for-it.sh` and `start-backend.sh`: + + `chmod +x start-backend.sh && chmod +x wait-for-it.sh` + +4. Download dependend projects for services. + +5. Review the docker-compose.yml file. Make sure that all services have Dockerfiles. Only db service doesn't require a Dockerfile. + +6. Make sure you have needed ports (see them in `ports`) available on your local machine. + +7. Start services: + + 7.1. With an empty database `rm -rf data && docker-compose up` + + 7.2. With a stored (from previus runs) database data `docker-compose up` + +8. Check http://localhost:3000 + +9. Stop services: + + 9.1. Just press `Ctr+C` diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts new file mode 100644 index 0000000..00a8785 --- /dev/null +++ b/frontend/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs new file mode 100644 index 0000000..f5abee5 --- /dev/null +++ b/frontend/next.config.mjs @@ -0,0 +1,50 @@ +/** + * @type {import('next').NextConfig} + */ + +const output = process.env.NODE_ENV === 'production' ? 'export' : 'standalone'; +const nextConfig = { + trailingSlash: true, + distDir: 'build', + output, + basePath: '', + devIndicators: { + position: 'bottom-left', + }, + typescript: { + ignoreBuildErrors: true, + }, + eslint: { + ignoreDuringBuilds: true, + }, + images: { + unoptimized: true, + remotePatterns: [ + { + protocol: 'https', + hostname: '**', + }, + ], + }, + + async rewrites() { + return [ + { + source: '/services', + destination: '/web_pages/services', + }, + + { + source: '/contact', + destination: '/web_pages/contact', + }, + + { + source: '/faq', + destination: '/web_pages/faq', + }, + ]; + }, +}; + +export default nextConfig; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..1adcc23 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,77 @@ +{ + "private": true, + "scripts": { + "dev": "cross-env PORT=${FRONT_PORT:-3000} next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint", + "format": "prettier '{components,pages,src,interfaces,hooks}/**/*.{tsx,ts,js}' --write" + }, + "dependencies": { + "@emotion/react": "^11.11.3", + "@emotion/styled": "^11.11.0", + "@mdi/js": "^7.4.47", + "@mui/material": "^6.3.0", + "@mui/x-data-grid": "^6.19.2", + "@reduxjs/toolkit": "^2.1.0", + "@tailwindcss/typography": "^0.5.13", + "@tinymce/tinymce-react": "^4.3.2", + "apexcharts": "^3.45.2", + "axios": "^1.8.4", + "chart.js": "^4.4.1", + "chroma-js": "^2.4.2", + "dayjs": "^1.11.10", + "file-saver": "^2.0.5", + "formik": "^2.4.5", + "html2canvas": "^1.4.1", + "i18next": "^25.1.2", + "i18next-browser-languagedetector": "^8.1.0", + "i18next-http-backend": "^3.0.2", + "intro.js": "^7.2.0", + "intro.js-react": "^1.0.0", + "jsonwebtoken": "^9.0.2", + "jwt-decode": "^3.1.2", + "lodash": "^4.17.21", + "moment": "^2.30.1", + "next": "^15.3.1", + "next-i18next": "^15.4.2", + "numeral": "^2.0.6", + "query-string": "^8.1.0", + "react": "^19.0.0", + "react-apexcharts": "^1.4.1", + "react-big-calendar": "^1.10.3", + "react-chartjs-2": "^4.3.1", + "react-datepicker": "^4.10.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", + "react-dom": "^19.0.0", + "react-i18next": "^15.5.1", + "react-redux": "^8.0.2", + "react-select": "^5.7.0", + "react-select-async-paginate": "^0.7.9", + "react-switch": "^7.0.0", + "react-toastify": "^11.0.2", + "swr": "^1.3.0", + "uuid": "^9.0.0" + }, + "devDependencies": { + "@tailwindcss/forms": "^0.5.7", + "@tailwindcss/line-clamp": "^0.4.4", + "@types/node": "18.7.16", + "@types/numeral": "^2.0.2", + "@types/react-big-calendar": "^1.8.8", + "@types/react-redux": "^7.1.24", + "@typescript-eslint/eslint-plugin": "^5.37.0", + "@typescript-eslint/parser": "^5.37.0", + "autoprefixer": "^10.4.0", + "cross-env": "^7.0.3", + "eslint": "^8.23.1", + "eslint-config-next": "^13.0.4", + "eslint-config-prettier": "^8.5.0", + "postcss": "^8.4.4", + "postcss-import": "^14.1.0", + "prettier": "^3.2.4", + "tailwindcss": "^3.4.1", + "typescript": "^5.4.5" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..5bee7ce --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,9 @@ +/* eslint-env node */ +module.exports = { + plugins: { + 'postcss-import': {}, + 'tailwindcss/nesting': {}, + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/prettier.config.js b/frontend/prettier.config.js new file mode 100644 index 0000000..d0ee549 --- /dev/null +++ b/frontend/prettier.config.js @@ -0,0 +1,13 @@ +module.exports = { + semi: false, + singleQuote: true, + printWidth: 100, + trailingComma: 'es5', + arrowParens: 'always', + tabWidth: 2, + useTabs: false, + quoteProps: 'as-needed', + jsxSingleQuote: false, + bracketSpacing: true, + bracketSameLine: false, +}; diff --git a/frontend/public/data-sources/clients.json b/frontend/public/data-sources/clients.json new file mode 100644 index 0000000..c97621a --- /dev/null +++ b/frontend/public/data-sources/clients.json @@ -0,0 +1,224 @@ +{ + "data": [ + { + "id": 19, + "avatar": "https://avatars.dicebear.com/v2/gridy/Howell-Hand.svg", + "login": "percy64", + "name": "Howell Hand", + "company": "Kiehn-Green", + "city": "Emelyside", + "progress": 70, + "created": "Mar 3, 2022", + "created_mm_dd_yyyy": "03-03-2022" + }, + { + "id": 11, + "avatar": "https://avatars.dicebear.com/v2/gridy/Hope-Howe.svg", + "login": "dare.concepcion", + "name": "Hope Howe", + "company": "Nolan Inc", + "city": "Paristown", + "progress": 68, + "created": "Dec 1, 2022", + "created_mm_dd_yyyy": "12-01-2022" + }, + { + "id": 32, + "avatar": "https://avatars.dicebear.com/v2/gridy/Nelson-Jerde.svg", + "login": "geovanni.kessler", + "name": "Nelson Jerde", + "company": "Nitzsche LLC", + "city": "Jailynbury", + "progress": 49, + "created": "May 18, 2022", + "created_mm_dd_yyyy": "05-18-2022" + }, + { + "id": 22, + "avatar": "https://avatars.dicebear.com/v2/gridy/Kim-Weimann.svg", + "login": "macejkovic.dashawn", + "name": "Kim Weimann", + "company": "Brown-Lueilwitz", + "city": "New Emie", + "progress": 38, + "created": "May 4, 2022", + "created_mm_dd_yyyy": "05-04-2022" + }, + { + "id": 34, + "avatar": "https://avatars.dicebear.com/v2/gridy/Justice-OReilly.svg", + "login": "hilpert.leora", + "name": "Justice O'Reilly", + "company": "Lakin-Muller", + "city": "New Kacie", + "progress": 38, + "created": "Mar 27, 2022", + "created_mm_dd_yyyy": "03-27-2022" + }, + { + "id": 48, + "avatar": "https://avatars.dicebear.com/v2/gridy/Adrienne-Mayer-III.svg", + "login": "ferry.sophia", + "name": "Adrienne Mayer III", + "company": "Kozey, McLaughlin and Kuhn", + "city": "Howardbury", + "progress": 39, + "created": "Mar 29, 2022", + "created_mm_dd_yyyy": "03-29-2022" + }, + { + "id": 20, + "avatar": "https://avatars.dicebear.com/v2/gridy/Mr.-Julien-Ebert.svg", + "login": "gokuneva", + "name": "Mr. Julien Ebert", + "company": "Cormier LLC", + "city": "South Serenaburgh", + "progress": 29, + "created": "Jun 25, 2022", + "created_mm_dd_yyyy": "06-25-2022" + }, + { + "id": 47, + "avatar": "https://avatars.dicebear.com/v2/gridy/Lenna-Smitham.svg", + "login": "paolo.walter", + "name": "Lenna Smitham", + "company": "King Inc", + "city": "McCulloughfort", + "progress": 59, + "created": "Oct 8, 2022", + "created_mm_dd_yyyy": "10-08-2022" + }, + { + "id": 24, + "avatar": "https://avatars.dicebear.com/v2/gridy/Travis-Davis.svg", + "login": "lkessler", + "name": "Travis Davis", + "company": "Leannon and Sons", + "city": "West Frankton", + "progress": 52, + "created": "Oct 20, 2022", + "created_mm_dd_yyyy": "10-20-2022" + }, + { + "id": 49, + "avatar": "https://avatars.dicebear.com/v2/gridy/Prof.-Esteban-Steuber.svg", + "login": "shana.lang", + "name": "Prof. Esteban Steuber", + "company": "Langosh-Ernser", + "city": "East Sedrick", + "progress": 34, + "created": "May 16, 2022", + "created_mm_dd_yyyy": "05-16-2022" + }, + { + "id": 36, + "avatar": "https://avatars.dicebear.com/v2/gridy/Russell-Goodwin-V.svg", + "login": "jewel07", + "name": "Russell Goodwin V", + "company": "Nolan-Stracke", + "city": "Williamsonmouth", + "progress": 55, + "created": "Apr 22, 2022", + "created_mm_dd_yyyy": "04-22-2022" + }, + { + "id": 33, + "avatar": "https://avatars.dicebear.com/v2/gridy/Ms.-Cassidy-Wiegand-DVM.svg", + "login": "burnice.okuneva", + "name": "Ms. Cassidy Wiegand DVM", + "company": "Kuhlman-Hahn", + "city": "New Ruthiehaven", + "progress": 76, + "created": "Sep 16, 2022", + "created_mm_dd_yyyy": "09-16-2022" + }, + { + "id": 44, + "avatar": "https://avatars.dicebear.com/v2/gridy/Mr.-Watson-Brakus-PhD.svg", + "login": "oconnell.juanita", + "name": "Mr. Watson Brakus PhD", + "company": "Osinski, Bins and Kuhn", + "city": "Lake Gloria", + "progress": 58, + "created": "Jun 22, 2022", + "created_mm_dd_yyyy": "06-22-2022" + }, + { + "id": 46, + "avatar": "https://avatars.dicebear.com/v2/gridy/Mr.-Garrison-Friesen-V.svg", + "login": "vgutmann", + "name": "Mr. Garrison Friesen V", + "company": "VonRueden, Rippin and Pfeffer", + "city": "Port Cieloport", + "progress": 39, + "created": "Oct 19, 2022", + "created_mm_dd_yyyy": "10-19-2022" + }, + { + "id": 14, + "avatar": "https://avatars.dicebear.com/v2/gridy/Ms.-Sister-Morar.svg", + "login": "veum.lucio", + "name": "Ms. Sister Morar", + "company": "Gusikowski, Altenwerth and Abbott", + "city": "Lake Macville", + "progress": 34, + "created": "Jun 11, 2022", + "created_mm_dd_yyyy": "06-11-2022" + }, + { + "id": 40, + "avatar": "https://avatars.dicebear.com/v2/gridy/Ms.-Laisha-Reinger.svg", + "login": "edietrich", + "name": "Ms. Laisha Reinger", + "company": "Boehm PLC", + "city": "West Alexiemouth", + "progress": 73, + "created": "Nov 2, 2022", + "created_mm_dd_yyyy": "11-02-2022" + }, + { + "id": 5, + "avatar": "https://avatars.dicebear.com/v2/gridy/Cameron-Lind.svg", + "login": "mose44", + "name": "Cameron Lind", + "company": "Tremblay, Padberg and Pouros", + "city": "Naderview", + "progress": 59, + "created": "Sep 14, 2022", + "created_mm_dd_yyyy": "09-14-2022" + }, + { + "id": 43, + "avatar": "https://avatars.dicebear.com/v2/gridy/Sarai-Little.svg", + "login": "rau.abelardo", + "name": "Sarai Little", + "company": "Deckow LLC", + "city": "Jeanieborough", + "progress": 49, + "created": "Jun 13, 2022", + "created_mm_dd_yyyy": "06-13-2022" + }, + { + "id": 2, + "avatar": "https://avatars.dicebear.com/v2/gridy/Shyann-Kautzer.svg", + "login": "imurazik", + "name": "Shyann Kautzer", + "company": "Osinski, Boehm and Kihn", + "city": "New Alvera", + "progress": 41, + "created": "Feb 15, 2022", + "created_mm_dd_yyyy": "02-15-2022" + }, + { + "id": 15, + "avatar": "https://avatars.dicebear.com/v2/gridy/Lorna-Christiansen.svg", + "login": "annalise97", + "name": "Lorna Christiansen", + "company": "Altenwerth-Friesen", + "city": "Port Elbertland", + "progress": 36, + "created": "Mar 9, 2022", + "created_mm_dd_yyyy": "03-09-2022" + } + ] +} diff --git a/frontend/public/data-sources/history.json b/frontend/public/data-sources/history.json new file mode 100644 index 0000000..12e81b6 --- /dev/null +++ b/frontend/public/data-sources/history.json @@ -0,0 +1,40 @@ +{ + "data": [ + { + "id": 1, + "amount": 375.53, + "account": "45721474", + "name": "Home Loan Account", + "date": "3 days ago", + "type": "deposit", + "business": "Turcotte" + }, + { + "id": 2, + "amount": 470.26, + "account": "94486537", + "name": "Savings Account", + "date": "3 days ago", + "type": "payment", + "business": "Murazik - Graham" + }, + { + "id": 3, + "amount": 971.34, + "account": "63189893", + "name": "Checking Account", + "date": "5 days ago", + "type": "invoice", + "business": "Fahey - Keebler" + }, + { + "id": 4, + "amount": 374.63, + "account": "74828780", + "name": "Auto Loan Account", + "date": "7 days ago", + "type": "withdraw", + "business": "Collier - Hintz" + } + ] +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..c8c4e3e --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/locales/de/common.json b/frontend/public/locales/de/common.json new file mode 100644 index 0000000..0d13fe5 --- /dev/null +++ b/frontend/public/locales/de/common.json @@ -0,0 +1,55 @@ +{ + "pages": { + "dashboard": { + "pageTitle": "Dashboard", + "overview": "Übersicht", + "loadingWidgets": "Widgets werden geladen...", + "loading": "Laden..." + }, + "login": { + "pageTitle": "Anmeldung", + + "sampleCredentialsAdmin": "Verwenden Sie {{email}} / {{password}}, um sich als Administrator anzumelden", + "sampleCredentialsUser": "Verwenden Sie {{email}} / {{password}}, um sich als Benutzer anzumelden", + + "form": { + "loginLabel": "Login", + "loginHelp": "Bitte geben Sie Ihren Login ein", + "passwordLabel": "Passwort", + "passwordHelp": "Bitte geben Sie Ihr Passwort ein", + "remember": "Angemeldet bleiben", + "forgotPassword": "Passwort vergessen?", + "loginButton": "Anmelden", + "loading": "Wird geladen...", + "noAccountYet": "Noch kein Konto?", + "newAccount": "Neues Konto" + }, + + "pexels": { + "photoCredit": "Foto von {{photographer}} auf Pexels", + "videoCredit": "Video von {{name}} auf Pexels", + "videoUnsupported": "Ihr Browser unterstützt das Video-Tag nicht." + }, + + "footer": { + "copyright": "© {{year}} {{title}}. Alle Rechte vorbehalten", + "privacy": "Datenschutzrichtlinie" + } + } + }, + "components": { + "widgetCreator": { + "title": "Diagramm oder Widget erstellen", + "helpText": "Beschreiben Sie Ihr neues Widget oder Diagramm in natürlicher Sprache. Zum Beispiel: \"Anzahl der Admin-Benutzer\" ODER \"rotes Diagramm mit der Anzahl geschlossener Verträge gruppiert nach Monat\"", + "settingsTitle": "Einstellungen für Widget Creator", + "settingsDescription": "Für welche Rolle zeigen wir Widgets an und erstellen sie?", + "doneButton": "Fertig", + "loading": "Laden..." + }, + "search": { + "placeholder": "Suche", + "required": "Pflichtfeld", + "minLength": "Mindestlänge: {{count}} Zeichen" + } + } +} diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json new file mode 100644 index 0000000..2c1a38d --- /dev/null +++ b/frontend/public/locales/en/common.json @@ -0,0 +1,52 @@ +{ + "pages": { + "dashboard": { + "pageTitle": "Dashboard", + "overview": "Overview", + "loadingWidgets": "Loading widgets...", + "loading": "Loading..." + }, + "login": { + "pageTitle": "Login", + + "form": { + "loginLabel": "Login", + "loginHelp": "Please enter your login", + "passwordLabel": "Password", + "passwordHelp": "Please enter your password", + "remember": "Remember", + "forgotPassword": "Forgot password?", + "loginButton": "Login", + "loading": "Loading...", + "noAccountYet": "Don’t have an account yet?", + "newAccount": "New Account" + }, + + "pexels": { + "photoCredit": "Photo by {{photographer}} on Pexels", + "videoCredit": "Video by {{name}} on Pexels", + "videoUnsupported": "Your browser does not support the video tag." + }, + + "footer": { + "copyright": "© {{year}} {{title}}. All rights reserved", + "privacy": "Privacy Policy" + } + } + }, + "components": { + "widgetCreator": { + "title": "Create Chart or Widget", + "helpText": "Describe your new widget or chart in natural language. For example: \"Number of admin users\" OR \"red chart with number of closed contracts grouped by month\"", + "settingsTitle": "Widget Creator Settings", + "settingsDescription": "What role are we showing and creating widgets for?", + "doneButton": "Done", + "loading": "Loading..." + }, + "search": { + "placeholder": "Search", + "required": "Required", + "minLength": "Minimum length: {{count}} characters" + } + } +} diff --git a/frontend/public/locales/es/common.json b/frontend/public/locales/es/common.json new file mode 100644 index 0000000..a7ba27a --- /dev/null +++ b/frontend/public/locales/es/common.json @@ -0,0 +1,55 @@ +{ + "pages": { + "dashboard": { + "pageTitle": "Tablero", + "overview": "Resumen", + "loadingWidgets": "Cargando widgets...", + "loading": "Cargando..." + }, + "login": { + "pageTitle": "Inicio de sesión", + + "sampleCredentialsAdmin": "Use {{email}} / {{password}} para iniciar sesión como Administrador", + "sampleCredentialsUser": "Use {{email}} / {{password}} para iniciar sesión como Usuario", + + "form": { + "loginLabel": "Usuario", + "loginHelp": "Introduzca su usuario", + "passwordLabel": "Contraseña", + "passwordHelp": "Introduzca su contraseña", + "remember": "Recuérdame", + "forgotPassword": "¿Olvidó su contraseña?", + "loginButton": "Acceder", + "loading": "Cargando...", + "noAccountYet": "¿Aún no tiene una cuenta?", + "newAccount": "Crear cuenta" + }, + + "pexels": { + "photoCredit": "Foto de {{photographer}} en Pexels", + "videoCredit": "Vídeo de {{name}} en Pexels", + "videoUnsupported": "Su navegador no admite la etiqueta de vídeo." + }, + + "footer": { + "copyright": "© {{year}} {{title}}. Todos los derechos reservados", + "privacy": "Política de privacidad" + } + } + }, + "components": { + "widgetCreator": { + "title": "Crear gráfico o widget", + "helpText": "Describe tu nuevo widget o gráfico en lenguaje natural. Por ejemplo: \"Número de usuarios administradores\" O \"gráfico rojo con el número de contratos cerrados agrupados por mes\"", + "settingsTitle": "Configuración del creador de widgets", + "settingsDescription": "¿Para qué rol estamos mostrando y creando widgets?", + "doneButton": "Listo", + "loading": "Cargando..." + }, + "search": { + "placeholder": "Buscar", + "required": "Obligatorio", + "minLength": "Longitud mínima: {{count}} caracteres" + } + } +} diff --git a/frontend/public/locales/fr/common.json b/frontend/public/locales/fr/common.json new file mode 100644 index 0000000..8b8494a --- /dev/null +++ b/frontend/public/locales/fr/common.json @@ -0,0 +1,55 @@ +{ + "pages": { + "dashboard": { + "pageTitle": "Tableau de bord", + "overview": "Vue d'ensemble", + "loadingWidgets": "Chargement des widgets...", + "loading": "Chargement..." + }, + "login": { + "pageTitle": "Connexion", + + "sampleCredentialsAdmin": "Utilisez {{email}} / {{password}} pour vous connecter en tant qu’administrateur", + "sampleCredentialsUser": "Utilisez {{email}} / {{password}} pour vous connecter en tant qu’utilisateur", + + "form": { + "loginLabel": "Identifiant", + "loginHelp": "Veuillez saisir votre identifiant", + "passwordLabel": "Mot de passe", + "passwordHelp": "Veuillez saisir votre mot de passe", + "remember": "Se souvenir de moi", + "forgotPassword": "Mot de passe oublié ?", + "loginButton": "Se connecter", + "loading": "Chargement…", + "noAccountYet": "Vous n’avez pas encore de compte ?", + "newAccount": "Créer un compte" + }, + + "pexels": { + "photoCredit": "Photo de {{photographer}} sur Pexels", + "videoCredit": "Vidéo de {{name}} sur Pexels", + "videoUnsupported": "Votre navigateur ne prend pas en charge la balise vidéo." + }, + + "footer": { + "copyright": "© {{year}} {{title}}. Tous droits réservés", + "privacy": "Politique de confidentialité" + } + } + }, + "components": { + "widgetCreator": { + "title": "Créer un graphique ou un widget", + "helpText": "Décrivez votre nouveau widget ou graphique en langage naturel. Par exemple : \"Nombre d'utilisateurs administrateurs\" OU \"graphique rouge avec le nombre de contrats clôturés regroupés par mois\"", + "settingsTitle": "Paramètres du créateur de widget", + "settingsDescription": "Pour quel rôle affichons-nous et créons-nous des widgets ?", + "doneButton": "Terminé", + "loading": "Chargement..." + }, + "search": { + "placeholder": "Rechercher", + "required": "Champ requis", + "minLength": "Longueur minimale : {{count}} caractères" + } + } +} diff --git a/frontend/src/colors.ts b/frontend/src/colors.ts new file mode 100644 index 0000000..b59db11 --- /dev/null +++ b/frontend/src/colors.ts @@ -0,0 +1,150 @@ +import type { ColorButtonKey } from './interfaces'; + +export const gradientBgBase = 'bg-gradient-to-tr'; +export const colorBgBase = 'bg-midnightBlueTheme-mainBG'; +export const gradientBgPurplePink = `${gradientBgBase} from-purple-400 via-pink-500 to-red-500`; +export const gradientBgViolet = `${gradientBgBase} ${colorBgBase}`; +export const gradientBgDark = `${gradientBgBase} from-dark-700 via-dark-900 to-dark-800`; +export const gradientBgPinkRed = `${gradientBgBase} from-pink-400 via-red-500 to-yellow-500`; + +export const colorsBgLight = { + white: 'bg-white text-black', + light: + ' bg-midnightBlueTheme-outsideCardColor text-primaryText text-primaryText dark:bg-dark-900 dark:text-white', + contrast: 'bg-gray-800 text-white dark:bg-white dark:text-black', + success: + 'bg-emerald-500 border-emerald-500 dark:bg-pavitra-blue dark:border-pavitra-blue text-white', + danger: 'bg-red-500 border-red-500 text-white', + warning: 'bg-yellow-500 border-yellow-500 text-white', + info: 'bg-blue-500 border-blue-500 dark:bg-pavitra-blue dark:border-pavitra-blue text-white', +}; + +export const colorsText = { + white: 'text-black dark:text-slate-100', + light: 'text-primaryText dark:text-slate-400', + contrast: 'dark:text-white', + success: 'text-emerald-500', + danger: 'text-red-500', + warning: 'text-yellow-500', + info: 'text-blue-500', +}; + +export const colorsOutline = { + white: [colorsText.white, 'border-gray-100'].join(' '), + light: [colorsText.light, 'border-gray-100'].join(' '), + contrast: [colorsText.contrast, 'border-gray-900 dark:border-slate-100'].join( + ' ', + ), + success: [colorsText.success, 'border-emerald-500'].join(' '), + danger: [colorsText.danger, 'border-red-500'].join(' '), + warning: [colorsText.warning, 'border-yellow-500'].join(' '), + info: [colorsText.info, 'border-blue-500'].join(' '), +}; + +export const getButtonColor = ( + color: ColorButtonKey, + isOutlined: boolean, + hasHover: boolean, + isActive = false, +) => { + if (color === 'void') { + return ''; + } + + const colors = { + ring: { + white: 'ring-gray-200 dark:ring-gray-500', + whiteDark: 'ring-midnightBlueTheme-outsideCardColor dark:ring-dark-500', + lightDark: 'ring-gray-200 dark:ring-gray-500', + contrast: 'ring-gray-300 dark:ring-gray-400', + success: 'ring-emerald-300 dark:ring-pavitra-blue', + danger: 'ring-red-300 dark:ring-red-700', + warning: 'ring-yellow-300 dark:ring-yellow-700', + info: 'ring-midnightBlueTheme-buttonColor dark:ring-pavitra-blue', + }, + active: { + white: 'bg-gray-100', + whiteDark: 'bg-gray-100 dark:bg-dark-800', + lightDark: 'bg-gray-200 dark:bg-slate-700', + contrast: 'bg-gray-700 dark:bg-slate-100', + success: 'bg-emerald-700 dark:bg-pavitra-blue', + danger: 'bg-red-700 dark:bg-red-600', + warning: 'bg-yellow-700 dark:bg-yellow-600', + info: 'bg-midnightBlueTheme-buttonColor dark:bg-pavitra-blue', + }, + bg: { + white: 'bg-white text-black', + whiteDark: + 'bg-midnightBlueTheme-outsideCardColor text-primaryText dark:bg-dark-900 dark:text-white', + lightDark: 'bg-gray-100 text-black dark:bg-slate-800 dark:text-white', + contrast: 'bg-gray-800 text-white dark:bg-white dark:text-black', + success: 'bg-emerald-600 dark:bg-pavitra-blue text-white', + danger: + 'bg-midnightBlueTheme-outsideCardColor text-red-500 dark:text-white dark:bg-red-500 ', + warning: 'bg-yellow-600 dark:bg-yellow-500 text-white', + info: ' bg-midnightBlueTheme-buttonColor dark:bg-pavitra-blue text-white ', + }, + bgHover: { + white: 'hover:bg-gray-100', + whiteDark: + 'hover:bg-midnightBlueTheme-outsideCardColor hover:dark:bg-dark-800', + lightDark: 'hover:bg-gray-200 hover:dark:bg-slate-700', + contrast: 'hover:bg-gray-700 hover:dark:bg-slate-100', + success: + 'hover:bg-emerald-700 hover:border-emerald-700 hover:dark:bg-pavitra-blue hover:dark:border-pavitra-blue', + danger: + 'hover:bg-red-700 hover:border-red-700 hover:dark:bg-red-600 hover:dark:border-red-600', + warning: + 'hover:bg-yellow-700 hover:border-yellow-700 hover:dark:bg-yellow-600 hover:dark:border-yellow-600', + info: 'hover:bg-midnightBlueTheme-800 hover:border-midnightBlueTheme-buttonColor hover:text-primaryText hover:dark:bg-pavitra-blue/80 hover:dark:border-pavitra-blue/80', + }, + borders: { + white: 'border-white', + whiteDark: + 'border-midnightBlueTheme-outsideCardColor dark:border-dark-900', + lightDark: 'border-gray-100 dark:border-slate-800', + contrast: 'border-gray-800 dark:border-white', + success: 'border-emerald-600 dark:border-pavitra-blue', + danger: 'border-red-600 dark:border-red-500', + warning: 'border-yellow-600 dark:border-yellow-500', + info: 'border-midnightBlueTheme-buttonColor border-blue-600 dark:border-pavitra-blue', + }, + text: { + contrast: 'dark:text-slate-100', + success: 'text-emerald-600 dark:text-pavitra-blue', + danger: 'text-red-600 dark:text-red-500', + warning: 'text-yellow-600 dark:text-yellow-500', + info: ' dark:text-pavitra-blue', + }, + outlineHover: { + contrast: + 'hover:bg-gray-800 hover:text-gray-100 hover:dark:bg-slate-100 hover:dark:text-black', + success: + 'hover:bg-emerald-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-pavitra-blue', + danger: + 'hover:bg-red-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-red-600', + warning: + 'hover:bg-yellow-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-yellow-600', + info: 'hover:bg-midnightBlueTheme-buttonColor text-midnightBlueTheme-buttonColor hover:bg-blue-600 hover:text-white hover:dark:text-white hover:dark:border-pavitra-blue', + }, + }; + + const isOutlinedProcessed = + isOutlined && ['white', 'whiteDark', 'lightDark'].indexOf(color) < 0; + + const base = [colors.borders[color], colors.ring[color]]; + + if (isActive) { + base.push(colors.active[color]); + } else { + base.push(isOutlinedProcessed ? colors.text[color] : colors.bg[color]); + } + + if (hasHover) { + base.push( + isOutlinedProcessed ? colors.outlineHover[color] : colors.bgHover[color], + ); + } + + return base.join(' '); +}; diff --git a/frontend/src/components/AsideMenu.tsx b/frontend/src/components/AsideMenu.tsx new file mode 100644 index 0000000..0a1c120 --- /dev/null +++ b/frontend/src/components/AsideMenu.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { MenuAsideItem } from '../interfaces'; +import AsideMenuLayer from './AsideMenuLayer'; +import OverlayLayer from './OverlayLayer'; + +type Props = { + menu: MenuAsideItem[]; + isAsideMobileExpanded: boolean; + isAsideLgActive: boolean; + onAsideLgClose: () => void; +}; + +export default function AsideMenu({ + isAsideMobileExpanded = false, + isAsideLgActive = false, + ...props +}: Props) { + return ( + <> + + {isAsideLgActive && ( + + )} + + ); +} diff --git a/frontend/src/components/AsideMenuItem.tsx b/frontend/src/components/AsideMenuItem.tsx new file mode 100644 index 0000000..c5a8b8a --- /dev/null +++ b/frontend/src/components/AsideMenuItem.tsx @@ -0,0 +1,116 @@ +import React, { useEffect, useState } from 'react'; +import { mdiMinus, mdiPlus } from '@mdi/js'; +import BaseIcon from './BaseIcon'; +import Link from 'next/link'; +import { getButtonColor } from '../colors'; +import AsideMenuList from './AsideMenuList'; +import { MenuAsideItem } from '../interfaces'; +import { useAppSelector } from '../stores/hooks'; +import { useRouter } from 'next/router'; + +type Props = { + item: MenuAsideItem; + isDropdownList?: boolean; +}; + +const AsideMenuItem = ({ item, isDropdownList = false }: Props) => { + const [isLinkActive, setIsLinkActive] = useState(false); + const [isDropdownActive, setIsDropdownActive] = useState(false); + + const asideMenuItemStyle = useAppSelector( + (state) => state.style.asideMenuItemStyle, + ); + const asideMenuDropdownStyle = useAppSelector( + (state) => state.style.asideMenuDropdownStyle, + ); + const asideMenuItemActiveStyle = useAppSelector( + (state) => state.style.asideMenuItemActiveStyle, + ); + const borders = useAppSelector((state) => state.style.borders); + const activeLinkColor = useAppSelector( + (state) => state.style.activeLinkColor, + ); + const activeClassAddon = + !item.color && isLinkActive ? asideMenuItemActiveStyle : ''; + + const { asPath, isReady } = useRouter(); + + useEffect(() => { + if (item.href && isReady) { + const linkPathName = new URL(item.href, location.href).pathname + '/'; + const activePathname = new URL(asPath, location.href).pathname; + + const activeView = activePathname.split('/')[1]; + const linkPathNameView = linkPathName.split('/')[1]; + + setIsLinkActive(linkPathNameView === activeView); + } + }, [item.href, isReady, asPath]); + + const asideMenuItemInnerContents = ( + <> + {item.icon && ( + + )} + + {item.label} + + {item.menu && ( + + )} + + ); + + const componentClass = [ + 'flex cursor-pointer py-1.5 ', + isDropdownList ? 'px-6 text-sm' : '', + item.color + ? getButtonColor(item.color, false, true) + : `${asideMenuItemStyle}`, + isLinkActive + ? `text-black ${activeLinkColor} dark:text-white dark:bg-dark-800` + : '', + ].join(' '); + + return ( +
  • + {item.withDevider &&
    } + {item.href && ( + + {asideMenuItemInnerContents} + + )} + {!item.href && ( +
    setIsDropdownActive(!isDropdownActive)} + > + {asideMenuItemInnerContents} +
    + )} + {item.menu && ( + + )} +
  • + ); +}; + +export default AsideMenuItem; diff --git a/frontend/src/components/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx new file mode 100644 index 0000000..346296c --- /dev/null +++ b/frontend/src/components/AsideMenuLayer.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import { mdiLogout, mdiClose } from '@mdi/js'; +import BaseIcon from './BaseIcon'; +import AsideMenuList from './AsideMenuList'; +import { MenuAsideItem } from '../interfaces'; +import { useAppSelector } from '../stores/hooks'; +import Link from 'next/link'; + +import { useAppDispatch } from '../stores/hooks'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import axios from 'axios'; + +type Props = { + menu: MenuAsideItem[]; + className?: string; + onAsideLgCloseClick: () => void; +}; + +export default function AsideMenuLayer({ + menu, + className = '', + ...props +}: Props) { + const corners = useAppSelector((state) => state.style.corners); + const asideStyle = useAppSelector((state) => state.style.asideStyle); + const asideBrandStyle = useAppSelector( + (state) => state.style.asideBrandStyle, + ); + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const darkMode = useAppSelector((state) => state.style.darkMode); + + const handleAsideLgCloseClick = (e: React.MouseEvent) => { + e.preventDefault(); + props.onAsideLgCloseClick(); + }; + + const dispatch = useAppDispatch(); + const { currentUser } = useAppSelector((state) => state.auth); + const organizationsId = currentUser?.organizations?.id; + const [organizations, setOrganizations] = React.useState(null); + + const fetchOrganizations = createAsyncThunk('/org-for-auth', async () => { + try { + const response = await axios.get('/org-for-auth'); + setOrganizations(response.data); + return response.data; + } catch (error) { + console.error(error.response); + throw error; + } + }); + + React.useEffect(() => { + dispatch(fetchOrganizations()); + }, [dispatch]); + + let organizationName = organizations?.find( + (item) => item.id === organizationsId, + )?.name; + if (organizationName?.length > 25) { + organizationName = organizationName?.substring(0, 25) + '...'; + } + + return ( + + ); +} diff --git a/frontend/src/components/AsideMenuList.tsx b/frontend/src/components/AsideMenuList.tsx new file mode 100644 index 0000000..9f0434e --- /dev/null +++ b/frontend/src/components/AsideMenuList.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { MenuAsideItem } from '../interfaces'; +import AsideMenuItem from './AsideMenuItem'; +import { useAppSelector } from '../stores/hooks'; +import { hasPermission } from '../helpers/userPermissions'; + +type Props = { + menu: MenuAsideItem[]; + isDropdownList?: boolean; + className?: string; +}; + +export default function AsideMenuList({ + menu, + isDropdownList = false, + className = '', +}: Props) { + const { currentUser } = useAppSelector((state) => state.auth); + + if (!currentUser) return null; + + return ( +
      + {menu.map((item, index) => { + if (!hasPermission(currentUser, item.permissions)) return null; + + return ( +
      + +
      + ); + })} +
    + ); +} diff --git a/frontend/src/components/Audit_logs/CardAudit_logs.tsx b/frontend/src/components/Audit_logs/CardAudit_logs.tsx new file mode 100644 index 0000000..c61abb7 --- /dev/null +++ b/frontend/src/components/Audit_logs/CardAudit_logs.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + audit_logs: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardAudit_logs = ({ + audit_logs, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_AUDIT_LOGS'); + + return ( +
    + {loading && } +
      + {!loading && + audit_logs.map((item, index) => ( +
    • +
      + + {item.action} + + +
      + +
      +
      +
      +
      +
      + Organization +
      +
      +
      + {dataFormatter.organizationsOneListFormatter(item.org)} +
      +
      +
      + +
      +
      User
      +
      +
      + {dataFormatter.usersOneListFormatter(item.user)} +
      +
      +
      + +
      +
      + Action +
      +
      +
      + {item.action} +
      +
      +
      + +
      +
      + Entity +
      +
      +
      + {item.entity} +
      +
      +
      + +
      +
      + EntityID +
      +
      +
      + {item.entity_id} +
      +
      +
      + +
      +
      + DiffJSON +
      +
      +
      + {item.diff_json} +
      +
      +
      +
      +
    • + ))} + {!loading && audit_logs.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardAudit_logs; diff --git a/frontend/src/components/Audit_logs/ListAudit_logs.tsx b/frontend/src/components/Audit_logs/ListAudit_logs.tsx new file mode 100644 index 0000000..1c0534e --- /dev/null +++ b/frontend/src/components/Audit_logs/ListAudit_logs.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + audit_logs: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListAudit_logs = ({ + audit_logs, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_AUDIT_LOGS'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + audit_logs.map((item) => ( +
    + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Organization

    +

    + {dataFormatter.organizationsOneListFormatter(item.org)} +

    +
    + +
    +

    User

    +

    + {dataFormatter.usersOneListFormatter(item.user)} +

    +
    + +
    +

    Action

    +

    {item.action}

    +
    + +
    +

    Entity

    +

    {item.entity}

    +
    + +
    +

    EntityID

    +

    {item.entity_id}

    +
    + +
    +

    DiffJSON

    +

    {item.diff_json}

    +
    + + +
    +
    +
    + ))} + {!loading && audit_logs.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListAudit_logs; diff --git a/frontend/src/components/Audit_logs/TableAudit_logs.tsx b/frontend/src/components/Audit_logs/TableAudit_logs.tsx new file mode 100644 index 0000000..2cc0539 --- /dev/null +++ b/frontend/src/components/Audit_logs/TableAudit_logs.tsx @@ -0,0 +1,500 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/audit_logs/audit_logsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureAudit_logsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +import ListAudit_logs from './ListAudit_logs'; + +const perPage = 10; + +const TableSampleAudit_logs = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + audit_logs, + loading, + count, + notify: audit_logsNotify, + refetch, + } = useAppSelector((state) => state.audit_logs); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (audit_logsNotify.showNotification) { + notify( + audit_logsNotify.typeNotification, + audit_logsNotify.textNotification, + ); + } + }, [audit_logsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `audit_logs`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={audit_logs ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {audit_logs && Array.isArray(audit_logs) && !showGrid && ( + + )} + + {showGrid && dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleAudit_logs; diff --git a/frontend/src/components/Audit_logs/configureAudit_logsCols.tsx b/frontend/src/components/Audit_logs/configureAudit_logsCols.tsx new file mode 100644 index 0000000..33ba48a --- /dev/null +++ b/frontend/src/components/Audit_logs/configureAudit_logsCols.tsx @@ -0,0 +1,152 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_AUDIT_LOGS'); + + return [ + { + field: 'org', + headerName: 'Organization', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'user', + headerName: 'User', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('users'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'action', + headerName: 'Action', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'entity', + headerName: 'Entity', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'entity_id', + headerName: 'EntityID', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'diff_json', + headerName: 'DiffJSON', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/BaseButton.tsx b/frontend/src/components/BaseButton.tsx new file mode 100644 index 0000000..cb87f90 --- /dev/null +++ b/frontend/src/components/BaseButton.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import Link from 'next/link'; +import { getButtonColor } from '../colors'; +import BaseIcon from './BaseIcon'; +import type { ColorButtonKey } from '../interfaces'; +import { useAppSelector } from '../stores/hooks'; + +type Props = { + label?: string; + icon?: string; + iconSize?: string | number; + href?: string; + target?: string; + type?: string; + color?: ColorButtonKey; + className?: string; + iconClassName?: string; + asAnchor?: boolean; + small?: boolean; + outline?: boolean; + active?: boolean; + disabled?: boolean; + roundedFull?: boolean; + onClick?: (e: React.MouseEvent) => void; +}; + +export default function BaseButton({ + label, + icon, + iconSize, + href, + target, + type, + color = 'white', + className = '', + iconClassName = '', + asAnchor = false, + small = false, + outline = false, + active = false, + disabled = false, + roundedFull = false, + onClick, +}: Props) { + const corners = useAppSelector((state) => state.style.corners); + const componentClass = [ + 'inline-flex', + 'justify-center', + 'items-center', + 'whitespace-nowrap', + 'focus:outline-none', + 'transition-colors', + 'focus:ring', + 'duration-150', + 'border', + disabled ? 'cursor-not-allowed' : 'cursor-pointer', + roundedFull ? 'rounded-full' : `${corners}`, + getButtonColor(color, outline, !disabled, active), + className, + ]; + + if (!label && icon) { + componentClass.push('p-1'); + } else if (small) { + componentClass.push('text-sm', roundedFull ? 'px-3 py-1' : 'p-1'); + } else { + componentClass.push('py-2', roundedFull ? 'px-6' : 'px-3'); + } + + if (disabled) { + componentClass.push(outline ? 'opacity-50' : 'opacity-70'); + } + + const componentClassString = componentClass.join(' '); + + const componentChildren = ( + <> + {icon && ( + + )} + {label && ( + {label} + )} + + ); + + if (href && !disabled) { + return ( + + {componentChildren} + + ); + } + + return React.createElement( + asAnchor ? 'a' : 'button', + { + className: componentClassString, + type: type ?? 'button', + target, + disabled, + onClick, + }, + componentChildren, + ); +} diff --git a/frontend/src/components/BaseButtons.tsx b/frontend/src/components/BaseButtons.tsx new file mode 100644 index 0000000..c017c92 --- /dev/null +++ b/frontend/src/components/BaseButtons.tsx @@ -0,0 +1,40 @@ +import { Children, cloneElement, ReactElement } from 'react'; +import type { ReactNode } from 'react'; + +type Props = { + type?: string; + mb?: string; + noWrap?: boolean; + classAddon?: string; + children?: ReactNode; + className?: string; +}; + +const BaseButtons = ({ + type = 'justify-end', + mb = '-mb-3', + classAddon = 'mr-3 last:mr-0 mb-3', + noWrap = false, + children, + className, +}: Props) => { + return ( +
    + {Children.map(children, (child: ReactElement) => + child + ? cloneElement(child as ReactElement<{ className?: string }>, { + className: `${classAddon} ${ + (child.props as { className?: string }).className || '' + }`, + }) + : null, + )} +
    + ); +}; + +export default BaseButtons; diff --git a/frontend/src/components/BaseDivider.tsx b/frontend/src/components/BaseDivider.tsx new file mode 100644 index 0000000..52e7f29 --- /dev/null +++ b/frontend/src/components/BaseDivider.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { useAppSelector } from '../stores/hooks'; +type Props = { + navBar?: boolean; +}; + +export default function BaseDivider({ navBar = false }: Props) { + const borders = useAppSelector((state) => state.style.borders); + const classAddon = navBar + ? 'hidden lg:block lg:my-0.5 dark:border-dark-700' + : 'my-6 -mx-6 dark:border-dark-800'; + + return
    ; +} diff --git a/frontend/src/components/BaseIcon.tsx b/frontend/src/components/BaseIcon.tsx new file mode 100644 index 0000000..d26fe1c --- /dev/null +++ b/frontend/src/components/BaseIcon.tsx @@ -0,0 +1,39 @@ +import React, { ReactNode } from 'react'; + +type Props = { + path: string; + w?: string; + h?: string; + fill?: string; + size?: string | number | null; + className?: string; + children?: ReactNode; +}; + +export default function BaseIcon({ + path, + fill, + w = 'w-6', + h = 'h-6', + size = null, + className = '', + children, +}: Props) { + const iconSize = size ?? 16; + + return ( + + + + + {children} + + ); +} diff --git a/frontend/src/components/BigCalendar.tsx b/frontend/src/components/BigCalendar.tsx new file mode 100644 index 0000000..0b26ffe --- /dev/null +++ b/frontend/src/components/BigCalendar.tsx @@ -0,0 +1,171 @@ +import React, { useEffect, useMemo, useState, useRef } from 'react'; +import { + Calendar, + Views, + momentLocalizer, + SlotInfo, + EventProps, +} from 'react-big-calendar'; +import moment from 'moment'; +import 'react-big-calendar/lib/css/react-big-calendar.css'; +import ListActionsPopover from './ListActionsPopover'; +import Link from 'next/link'; + +import { useAppSelector } from '../stores/hooks'; +import { hasPermission } from '../helpers/userPermissions'; + +const localizer = momentLocalizer(moment); + +type TEvent = { + id: string; + title: string; + start: Date; + end: Date; +}; + +type Props = { + events: any[]; + handleDeleteAction: (id: string) => void; + handleCreateEventAction: (slotInfo: SlotInfo) => void; + onDateRangeChange: (range: { start: string; end: string }) => void; + entityName: string; + showField: string; + pathEdit?: string; + pathView?: string; + 'start-data-key': string; + 'end-data-key': string; +}; + +const BigCalendar = ({ + events, + handleDeleteAction, + handleCreateEventAction, + onDateRangeChange, + entityName, + showField, + pathEdit, + pathView, + 'start-data-key': startDataKey, + 'end-data-key': endDataKey, +}: Props) => { + const [myEvents, setMyEvents] = useState([]); + const prevRange = useRef<{ start: string; end: string } | null>(null); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = + currentUser && + hasPermission(currentUser, `UPDATE_${entityName.toUpperCase()}`); + const hasCreatePermission = + currentUser && + hasPermission(currentUser, `CREATE_${entityName.toUpperCase()}`); + + const { defaultDate, scrollToTime } = useMemo( + () => ({ + defaultDate: new Date(), + scrollToTime: new Date(1970, 1, 1, 6), + }), + [], + ); + + useEffect(() => { + if (!events || !Array.isArray(events) || !events?.length) return; + + const formattedEvents = events.map((event) => ({ + ...event, + start: new Date(event[startDataKey]), + end: new Date(event[endDataKey]), + title: event[showField], + })); + + setMyEvents(formattedEvents); + }, [endDataKey, events, startDataKey, showField]); + + const onRangeChange = (range: Date[] | { start: Date; end: Date }) => { + const newRange = { start: '', end: '' }; + const format = 'YYYY-MM-DDTHH:mm'; + + if (Array.isArray(range)) { + newRange.start = moment(range[0]).format(format); + newRange.end = moment(range[range.length - 1]).format(format); + } else { + newRange.start = moment(range.start).format(format); + newRange.end = moment(range.end).format(format); + } + + if (newRange.start === newRange.end) { + newRange.end = moment(newRange.end).add(1, 'days').format(format); + } + + // check if the range fits in the previous range + if ( + prevRange.current && + prevRange.current.start <= newRange.start && + prevRange.current.end >= newRange.end + ) { + return; + } + + prevRange.current = { start: newRange.start, end: newRange.end }; + onDateRangeChange(newRange); + }; + + return ( +
    + ( + + ), + }} + /> +
    + ); +}; + +const MyCustomEvent = ( + props: { + onDelete: (id: string) => void; + hasUpdatePermission: boolean; + pathEdit?: string; + pathView?: string; + } & EventProps, +) => { + const { onDelete, hasUpdatePermission, title, event, pathEdit, pathView } = + props; + + return ( +
    + + {title} + + +
    + ); +}; + +export default BigCalendar; diff --git a/frontend/src/components/CardBox.tsx b/frontend/src/components/CardBox.tsx new file mode 100644 index 0000000..dea7269 --- /dev/null +++ b/frontend/src/components/CardBox.tsx @@ -0,0 +1,70 @@ +import React, { ReactNode } from 'react'; +import CardBoxComponentBody from './CardBoxComponentBody'; +import CardBoxComponentFooter from './CardBoxComponentFooter'; +import { useAppSelector } from '../stores/hooks'; + +type Props = { + rounded?: string; + flex?: string; + className?: string; + hasComponentLayout?: boolean; + cardBoxClassName?: string; + hasTable?: boolean; + isHoverable?: boolean; + isModal?: boolean; + children?: ReactNode; + footer?: ReactNode; + isList?: boolean; + id?: string; + onClick?: (e: React.MouseEvent) => void; +}; + +export default function CardBox({ + rounded = 'rounded', + flex = 'flex-col', + className = '', + hasComponentLayout = false, + cardBoxClassName = '', + hasTable = false, + isHoverable = false, + isList = false, + isModal = false, + children, + footer, + id = '', + onClick, +}: Props) { + const corners = useAppSelector((state) => state.style.corners); + const cardsStyle = useAppSelector((state) => state.style.cardsStyle); + const componentClass = [ + `flex dark:border-dark-700 dark:bg-dark-900`, + className, + corners !== 'rounded-full' ? corners : 'rounded-3xl', + flex, + isList ? '' : `${cardsStyle}`, + hasTable ? '' : `border-dark-700 dark:border-dark-700`, + ]; + + if (isHoverable) { + componentClass.push('hover:shadow-lg transition-shadow duration-500'); + } + + return React.createElement( + 'div', + { className: componentClass.join(' '), onClick }, + hasComponentLayout ? ( + children + ) : ( + <> + + {children} + + {footer && {footer}} + + ), + ); +} diff --git a/frontend/src/components/CardBoxComponentBody.tsx b/frontend/src/components/CardBoxComponentBody.tsx new file mode 100644 index 0000000..12448d8 --- /dev/null +++ b/frontend/src/components/CardBoxComponentBody.tsx @@ -0,0 +1,21 @@ +import React, { ReactNode } from 'react'; + +type Props = { + noPadding?: boolean; + className?: string; + children?: ReactNode; + id?: string; +}; + +export default function CardBoxComponentBody({ + noPadding = false, + className, + children, + id, +}: Props) { + return ( +
    + {children} +
    + ); +} diff --git a/frontend/src/components/CardBoxComponentEmpty.tsx b/frontend/src/components/CardBoxComponentEmpty.tsx new file mode 100644 index 0000000..c9072bb --- /dev/null +++ b/frontend/src/components/CardBoxComponentEmpty.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const CardBoxComponentEmpty = () => { + return ( +
    +

    Nothing's here…

    +
    + ); +}; + +export default CardBoxComponentEmpty; diff --git a/frontend/src/components/CardBoxComponentFooter.tsx b/frontend/src/components/CardBoxComponentFooter.tsx new file mode 100644 index 0000000..184a058 --- /dev/null +++ b/frontend/src/components/CardBoxComponentFooter.tsx @@ -0,0 +1,10 @@ +import React, { ReactNode } from 'react'; + +type Props = { + className?: string; + children?: ReactNode; +}; + +export default function CardBoxComponentFooter({ className, children }: Props) { + return
    {children}
    ; +} diff --git a/frontend/src/components/CardBoxComponentTitle.tsx b/frontend/src/components/CardBoxComponentTitle.tsx new file mode 100644 index 0000000..20990e6 --- /dev/null +++ b/frontend/src/components/CardBoxComponentTitle.tsx @@ -0,0 +1,17 @@ +import React, { ReactNode } from 'react'; + +type Props = { + title: string; + children?: ReactNode; +}; + +const CardBoxComponentTitle = ({ title, children }: Props) => { + return ( +
    +

    {title}

    + {children} +
    + ); +}; + +export default CardBoxComponentTitle; diff --git a/frontend/src/components/CardBoxModal.tsx b/frontend/src/components/CardBoxModal.tsx new file mode 100644 index 0000000..200b4e7 --- /dev/null +++ b/frontend/src/components/CardBoxModal.tsx @@ -0,0 +1,75 @@ +import { mdiClose } from '@mdi/js'; +import { ReactNode } from 'react'; +import type { ColorButtonKey } from '../interfaces'; +import BaseButton from './BaseButton'; +import BaseButtons from './BaseButtons'; +import CardBox from './CardBox'; +import CardBoxComponentTitle from './CardBoxComponentTitle'; +import OverlayLayer from './OverlayLayer'; + +type Props = { + title: string; + buttonColor: ColorButtonKey; + buttonLabel: string; + isActive: boolean; + children?: ReactNode; + onConfirm: () => void; + onCancel?: () => void; +}; + +const CardBoxModal = ({ + title, + buttonColor, + buttonLabel, + isActive, + children, + onConfirm, + onCancel, +}: Props) => { + if (!isActive) { + return null; + } + + const footer = ( + + + {!!onCancel && ( + + )} + + ); + + return ( + + + + {!!onCancel && ( + + )} + + +
    {children}
    +
    +
    + ); +}; + +export default CardBoxModal; diff --git a/frontend/src/components/ChartLineSample/config.ts b/frontend/src/components/ChartLineSample/config.ts new file mode 100644 index 0000000..c29cbdd --- /dev/null +++ b/frontend/src/components/ChartLineSample/config.ts @@ -0,0 +1,54 @@ +export const chartColors = { + default: { + primary: '#00D1B2', + info: '#209CEE', + danger: '#FF3860', + }, +}; + +const randomChartData = (n: number) => { + const data = []; + + for (let i = 0; i < n; i++) { + data.push(Math.round(Math.random() * 200)); + } + + return data; +}; + +const datasetObject = (color: string, points: number) => { + return { + fill: false, + borderColor: chartColors.default[color], + borderWidth: 2, + borderDash: [], + borderDashOffset: 0.0, + pointBackgroundColor: chartColors.default[color], + pointBorderColor: 'rgba(255,255,255,0)', + pointHoverBackgroundColor: chartColors.default[color], + pointBorderWidth: 20, + pointHoverRadius: 4, + pointHoverBorderWidth: 15, + pointRadius: 4, + data: randomChartData(points), + tension: 0.5, + cubicInterpolationMode: 'default', + }; +}; + +export const sampleChartData = (points = 9) => { + const labels = []; + + for (let i = 1; i <= points; i++) { + labels.push(`0${i}`); + } + + return { + labels, + datasets: [ + datasetObject('primary', points), + datasetObject('info', points), + datasetObject('danger', points), + ], + }; +}; diff --git a/frontend/src/components/ChartLineSample/index.tsx b/frontend/src/components/ChartLineSample/index.tsx new file mode 100644 index 0000000..0761549 --- /dev/null +++ b/frontend/src/components/ChartLineSample/index.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { + Chart, + LineElement, + PointElement, + LineController, + LinearScale, + CategoryScale, + Tooltip, +} from 'chart.js'; +import { Line } from 'react-chartjs-2'; + +Chart.register( + LineElement, + PointElement, + LineController, + LinearScale, + CategoryScale, + Tooltip, +); + +const options = { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + display: false, + }, + x: { + display: true, + }, + }, + plugins: { + legend: { + display: false, + }, + }, +}; + +const ChartLineSample = ({ data }) => { + return ; +}; + +export default ChartLineSample; diff --git a/frontend/src/components/ClickOutside.tsx b/frontend/src/components/ClickOutside.tsx new file mode 100644 index 0000000..4e031ad --- /dev/null +++ b/frontend/src/components/ClickOutside.tsx @@ -0,0 +1,45 @@ +import React, { + useCallback, + useEffect, + useRef, + ReactNode, + MutableRefObject, +} from 'react'; + +interface ClickOutsideProps { + children?: ReactNode; + onClickOutside: () => void; + excludedElements: MutableRefObject[]; +} + +const ClickOutside = ({ + children, + onClickOutside, + excludedElements, +}: ClickOutsideProps) => { + const wrapperRef = useRef(null); + + const handleClickOutside = useCallback( + (event) => { + if ( + wrapperRef.current && + !wrapperRef.current.contains(event.target) && + !excludedElements.some((el) => el.current.contains(event.target)) + ) { + onClickOutside(); + } + }, + [wrapperRef, onClickOutside, ...excludedElements], + ); + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [handleClickOutside]); + + return
    {children}
    ; +}; + +export default ClickOutside; diff --git a/frontend/src/components/DataGridMultiSelect.tsx b/frontend/src/components/DataGridMultiSelect.tsx new file mode 100644 index 0000000..bb82434 --- /dev/null +++ b/frontend/src/components/DataGridMultiSelect.tsx @@ -0,0 +1,55 @@ +import { GridRenderEditCellParams, useGridApiContext } from '@mui/x-data-grid'; +import React, { useEffect, useState } from 'react'; +import axios from 'axios'; +import { MenuItem, Select } from '@mui/material'; + +interface Props { + entityName: string; +} + +const DataGridMultiSelect = (props: GridRenderEditCellParams & Props) => { + const { id, value, field, entityName } = props; + const apiRef = useGridApiContext(); + const [options, setOptions] = useState([]); + + async function callApi(entityName: string) { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } + + useEffect(() => { + callApi(entityName).then((data) => { + setOptions(data); + }); + }, []); + + const handleChange = (event) => { + const eventValue = event.target.value; // The new value entered by the user + + const newValue = + typeof eventValue === 'string' ? value.split(',') : eventValue; + + apiRef.current.setEditCellValue({ + id, + field, + value: newValue.filter((x) => x !== ''), + }); + }; + + return ( + + ); +}; + +export default DataGridMultiSelect; diff --git a/frontend/src/components/Data_connections/CardData_connections.tsx b/frontend/src/components/Data_connections/CardData_connections.tsx new file mode 100644 index 0000000..37aa24d --- /dev/null +++ b/frontend/src/components/Data_connections/CardData_connections.tsx @@ -0,0 +1,159 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + data_connections: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardData_connections = ({ + data_connections, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_DATA_CONNECTIONS', + ); + + return ( +
    + {loading && } +
      + {!loading && + data_connections.map((item, index) => ( +
    • +
      + + {item.name} + + +
      + +
      +
      +
      +
      +
      + Organization +
      +
      +
      + {dataFormatter.organizationsOneListFormatter(item.org)} +
      +
      +
      + +
      +
      Type
      +
      +
      {item.type}
      +
      +
      + +
      +
      Name
      +
      +
      {item.name}
      +
      +
      + +
      +
      + ConfigJSON +
      +
      +
      + {item.config_json} +
      +
      +
      + +
      +
      + Status +
      +
      +
      + {item.status} +
      +
      +
      + +
      +
      + LastSyncAt +
      +
      +
      + {dataFormatter.dateTimeFormatter(item.last_sync_at)} +
      +
      +
      +
      +
    • + ))} + {!loading && data_connections.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardData_connections; diff --git a/frontend/src/components/Data_connections/ListData_connections.tsx b/frontend/src/components/Data_connections/ListData_connections.tsx new file mode 100644 index 0000000..884619c --- /dev/null +++ b/frontend/src/components/Data_connections/ListData_connections.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + data_connections: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListData_connections = ({ + data_connections, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_DATA_CONNECTIONS', + ); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + data_connections.map((item) => ( +
    + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Organization

    +

    + {dataFormatter.organizationsOneListFormatter(item.org)} +

    +
    + +
    +

    Type

    +

    {item.type}

    +
    + +
    +

    Name

    +

    {item.name}

    +
    + +
    +

    ConfigJSON

    +

    {item.config_json}

    +
    + +
    +

    Status

    +

    {item.status}

    +
    + +
    +

    LastSyncAt

    +

    + {dataFormatter.dateTimeFormatter(item.last_sync_at)} +

    +
    + + +
    +
    +
    + ))} + {!loading && data_connections.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListData_connections; diff --git a/frontend/src/components/Data_connections/TableData_connections.tsx b/frontend/src/components/Data_connections/TableData_connections.tsx new file mode 100644 index 0000000..d473631 --- /dev/null +++ b/frontend/src/components/Data_connections/TableData_connections.tsx @@ -0,0 +1,500 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/data_connections/data_connectionsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureData_connectionsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +import ListData_connections from './ListData_connections'; + +const perPage = 10; + +const TableSampleData_connections = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + data_connections, + loading, + count, + notify: data_connectionsNotify, + refetch, + } = useAppSelector((state) => state.data_connections); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (data_connectionsNotify.showNotification) { + notify( + data_connectionsNotify.typeNotification, + data_connectionsNotify.textNotification, + ); + } + }, [data_connectionsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `data_connections`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={data_connections ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {data_connections && Array.isArray(data_connections) && !showGrid && ( + + )} + + {showGrid && dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleData_connections; diff --git a/frontend/src/components/Data_connections/configureData_connectionsCols.tsx b/frontend/src/components/Data_connections/configureData_connectionsCols.tsx new file mode 100644 index 0000000..658d308 --- /dev/null +++ b/frontend/src/components/Data_connections/configureData_connectionsCols.tsx @@ -0,0 +1,146 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_DATA_CONNECTIONS'); + + return [ + { + field: 'org', + headerName: 'Organization', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'type', + headerName: 'Type', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'config_json', + headerName: 'ConfigJSON', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'status', + headerName: 'Status', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'last_sync_at', + headerName: 'LastSyncAt', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.last_sync_at), + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/DevModeBadge.tsx b/frontend/src/components/DevModeBadge.tsx new file mode 100644 index 0000000..2d226a0 --- /dev/null +++ b/frontend/src/components/DevModeBadge.tsx @@ -0,0 +1,150 @@ +import React, { useState, useEffect } from 'react'; +import useDevCompilationStatus from '../hooks/useDevCompilationStatus'; +const DevModeBadge: React.FC = () => { + const [isVisible, setIsVisible] = useState(false); + const [isCollapsed, setIsCollapsed] = useState(true); + const compilationStatus = useDevCompilationStatus(); + + const [badgeStyles, setBadgeStyles] = useState({ + position: 'fixed', + bottom: '20px', + left: '70px', + background: 'rgba(0, 0, 0, 0.85)', + color: 'white', + padding: '15px', + borderRadius: '8px', + fontFamily: 'sans-serif', + fontSize: '14px', + lineHeight: '1.5', + textAlign: 'left', + zIndex: 2147483647, + boxShadow: '0 4px 10px rgba(0, 0, 0, 0.3)', + whiteSpace: 'pre-wrap', + transition: + 'width 0.3s cubic-bezier(0.25, 0.1, 0.25, 1), padding 0.3s ease-in-out, opacity 0.3s ease-in-out, background-color 0.3s ease-in-out', // Improved transition for width + opacity: 0, + pointerEvents: 'none', + width: '340px', + maxWidth: '340px', + height: 'auto', + overflow: 'hidden', + cursor: 'pointer', + }); + + const fullText = `🚧 Your app is running in development mode. +Current request is compiling and may take a few moments. + +💡 Tip: Set up a stable environment to run your app in production mode—pages will load instantly without compilation delays.`; + + const collapsedText = '🚧 DEV stage'; + + useEffect(() => { + if (compilationStatus === 'ready') { + setIsCollapsed(true); + } else { + setIsCollapsed(false); + } + }, [compilationStatus]); + + useEffect(() => { + if ( + process.env.NODE_ENV === 'development' || + process.env.NODE_ENV === 'dev_stage' + ) { + setIsVisible(true); + + setBadgeStyles((prev) => ({ + ...prev, + opacity: 1, + width: '120px', + maxWidth: '120px', + padding: '6px 10px', + borderRadius: '18px', + whiteSpace: 'nowrap', + fontSize: '12px', + cursor: 'pointer', + pointerEvents: 'auto', + })); + } else { + setIsVisible(false); + setBadgeStyles((prev) => ({ ...prev, opacity: 0 })); + } + }, []); + + useEffect(() => { + if (!isVisible) return; + + if (isCollapsed) { + setBadgeStyles((prev) => ({ + ...prev, + width: '140px', + maxWidth: '160px', + padding: '6px 20px', + borderRadius: '18px', + whiteSpace: 'nowrap', + fontSize: '12px', + })); + } else { + setBadgeStyles((prev) => ({ + ...prev, + width: '340px', + maxWidth: '340px', + padding: '15px', + borderRadius: '8px', + whiteSpace: 'pre-wrap', + fontSize: '14px', + })); + } + }, [isCollapsed, isVisible]); + + const handleToggleCollapse = (e: React.MouseEvent) => { + e.stopPropagation(); + setIsCollapsed((prev) => !prev); + }; + + if (!isVisible) { + return null; + } + + return ( +
    + + + {!isCollapsed &&
    {fullText}
    } + {isCollapsed && ( +
    {collapsedText}
    + )} +
    + ); +}; + +export default DevModeBadge; diff --git a/frontend/src/components/DragDropFilePicker.tsx b/frontend/src/components/DragDropFilePicker.tsx new file mode 100644 index 0000000..821570d --- /dev/null +++ b/frontend/src/components/DragDropFilePicker.tsx @@ -0,0 +1,124 @@ +import React, { ChangeEvent, useEffect, useState } from 'react'; +import BaseIcon from './BaseIcon'; +import { mdiFileUploadOutline } from '@mdi/js'; + +type Props = { + file: File | null; + setFile: (file: File) => void; + formats?: string; +}; + +const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => { + const [highlight, setHighlight] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + const fileInput = React.createRef(); + + useEffect(() => { + if (!file && fileInput) fileInput.current.value = ''; + }, [file, fileInput]); + + function onFilesAdded(files: FileList | null) { + if (files && files[0]) { + const newFile = files[0]; + const fileExtension = newFile.name.split('.').pop().toLowerCase(); + + if (formats.includes(fileExtension) || !formats) { + setFile(newFile); + setErrorMessage(''); + } else { + setErrorMessage(`Allowed formats: ${formats}`); + } + } + } + + function onDragOver(e) { + e.preventDefault(); + setHighlight(true); + } + + function onDragLeave() { + setHighlight(false); + } + + function onDrop(e) { + e.preventDefault(); + + const files = e.dataTransfer.files; + + onFilesAdded(files); + setHighlight(false); + } + + const onClear = () => { + setFile(null); + setErrorMessage(''); + }; + + return ( +
    + +
    + ); +}; + +export default DragDropFilePicker; diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..490bd95 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.tsx @@ -0,0 +1,218 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; +import { mdiAlertCircle } from '@mdi/js'; +import BaseIcon from './BaseIcon'; + +// Define the props and state interfaces +interface ErrorBoundaryProps { + children: ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; + errorInfo: ErrorInfo | null; + showStack: boolean; +} + +// Class-based ErrorBoundary Component +class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + + // Define state variables + this.state = { + hasError: false, + error: null, + errorInfo: null, + showStack: false, + }; + } + + static getDerivedStateFromError(error: Error): Partial { + // Update state so the next render will show the fallback UI + return { + hasError: true, + error: error, + }; + } + + componentDidUpdate( + prevProps: Readonly, + prevState: Readonly, + snapshot?: any, + ) { + if (process.env.NODE_ENV !== 'production') { + console.log('componentDidUpdate'); + } + } + + async componentWillUnmount() { + if (process.env.NODE_ENV !== 'production') { + console.log('componentWillUnmount'); + const response = await fetch('/api/logError', { + method: 'DELETE', + }); + + const data = await response.json(); + console.log('Error logs cleared:', data); + } + } + + async componentDidCatch(error: Error, errorInfo: ErrorInfo) { + // Update state with error details (always needed for UI) + this.setState({ + errorInfo: errorInfo, + }); + + // Only perform logging in non-production environments + if (process.env.NODE_ENV !== 'production') { + console.log('Error caught in boundary:', error, errorInfo); + + // Function to log errors to the server + const logErrorToServer = async () => { + try { + const response = await fetch('/api/logError', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: error.message, + stack: errorInfo.componentStack, + }), + }); + + const data = await response.json(); + console.log('Error logged:', data); + } catch (err) { + console.error('Failed to log error:', err); + } + }; + + // Function to fetch logged errors (optional) + const fetchLoggedErrors = async () => { + try { + const response = await fetch('/api/logError'); + const data = await response.json(); + console.log('Fetched logs:', data); + } catch (err) { + console.error('Failed to fetch logs:', err); + } + }; + + await logErrorToServer(); + await fetchLoggedErrors(); + } + } + + toggleStack = () => { + this.setState((prevState) => ({ + showStack: !prevState.showStack, + })); + }; + + resetError = () => { + this.setState({ + hasError: false, + error: null, + errorInfo: null, + showStack: false, + }); + }; + + tryAgain = async () => { + // Only clear error logs in non-production environments + if (process.env.NODE_ENV !== 'production') { + try { + const response = await fetch('/api/logError', { + method: 'DELETE', + }); + + const data = await response.json(); + console.log('Error logs cleared:', data); + } catch (e) { + console.error('Failed to clear error logs:', e); + } + } + + // Always reset the error state (needed for UI recovery) + this.setState({ hasError: false }); + }; + + render() { + if (this.state.hasError) { + // Extract error details + const { error, errorInfo, showStack } = this.state; + const errorMessage = error?.message || 'An unexpected error occurred'; + const stackTrace = + errorInfo?.componentStack || error?.stack || 'No stack trace available'; + + return ( +
    +
    +
    +
    + +
    + +
    +

    + Something went wrong +

    +

    + We're sorry, but we encountered an unexpected error. +

    +
    + +
    +

    + {errorMessage} +

    + +
    + + + {showStack && ( +
    +                      {stackTrace}
    +                    
    + )} +
    +
    + +
    + + + +
    +
    +
    +
    + ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/frontend/src/components/Fin_accounts/CardFin_accounts.tsx b/frontend/src/components/Fin_accounts/CardFin_accounts.tsx new file mode 100644 index 0000000..e5a0e08 --- /dev/null +++ b/frontend/src/components/Fin_accounts/CardFin_accounts.tsx @@ -0,0 +1,152 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + fin_accounts: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardFin_accounts = ({ + fin_accounts, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_FIN_ACCOUNTS'); + + return ( +
    + {loading && } +
      + {!loading && + fin_accounts.map((item, index) => ( +
    • +
      + + {item.name} + + +
      + +
      +
      +
      +
      +
      + Organization +
      +
      +
      + {dataFormatter.organizationsOneListFormatter(item.org)} +
      +
      +
      + +
      +
      Code
      +
      +
      {item.code}
      +
      +
      + +
      +
      Name
      +
      +
      {item.name}
      +
      +
      + +
      +
      Type
      +
      +
      {item.type}
      +
      +
      + +
      +
      + ParentAccount +
      +
      +
      + {dataFormatter.fin_accountsOneListFormatter(item.parent)} +
      +
      +
      + +
      +
      + IsActive +
      +
      +
      + {dataFormatter.booleanFormatter(item.is_active)} +
      +
      +
      +
      +
    • + ))} + {!loading && fin_accounts.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardFin_accounts; diff --git a/frontend/src/components/Fin_accounts/ListFin_accounts.tsx b/frontend/src/components/Fin_accounts/ListFin_accounts.tsx new file mode 100644 index 0000000..b39aa9f --- /dev/null +++ b/frontend/src/components/Fin_accounts/ListFin_accounts.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + fin_accounts: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListFin_accounts = ({ + fin_accounts, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_FIN_ACCOUNTS'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + fin_accounts.map((item) => ( +
    + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Organization

    +

    + {dataFormatter.organizationsOneListFormatter(item.org)} +

    +
    + +
    +

    Code

    +

    {item.code}

    +
    + +
    +

    Name

    +

    {item.name}

    +
    + +
    +

    Type

    +

    {item.type}

    +
    + +
    +

    + ParentAccount +

    +

    + {dataFormatter.fin_accountsOneListFormatter( + item.parent, + )} +

    +
    + +
    +

    IsActive

    +

    + {dataFormatter.booleanFormatter(item.is_active)} +

    +
    + + +
    +
    +
    + ))} + {!loading && fin_accounts.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListFin_accounts; diff --git a/frontend/src/components/Fin_accounts/TableFin_accounts.tsx b/frontend/src/components/Fin_accounts/TableFin_accounts.tsx new file mode 100644 index 0000000..a7c6d6e --- /dev/null +++ b/frontend/src/components/Fin_accounts/TableFin_accounts.tsx @@ -0,0 +1,500 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/fin_accounts/fin_accountsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureFin_accountsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +import ListFin_accounts from './ListFin_accounts'; + +const perPage = 10; + +const TableSampleFin_accounts = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + fin_accounts, + loading, + count, + notify: fin_accountsNotify, + refetch, + } = useAppSelector((state) => state.fin_accounts); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (fin_accountsNotify.showNotification) { + notify( + fin_accountsNotify.typeNotification, + fin_accountsNotify.textNotification, + ); + } + }, [fin_accountsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `fin_accounts`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={fin_accounts ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {fin_accounts && Array.isArray(fin_accounts) && !showGrid && ( + + )} + + {showGrid && dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleFin_accounts; diff --git a/frontend/src/components/Fin_accounts/configureFin_accountsCols.tsx b/frontend/src/components/Fin_accounts/configureFin_accountsCols.tsx new file mode 100644 index 0000000..4077ff6 --- /dev/null +++ b/frontend/src/components/Fin_accounts/configureFin_accountsCols.tsx @@ -0,0 +1,152 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_FIN_ACCOUNTS'); + + return [ + { + field: 'org', + headerName: 'Organization', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'code', + headerName: 'Code', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'type', + headerName: 'Type', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'parent', + headerName: 'ParentAccount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('fin_accounts'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'is_active', + headerName: 'IsActive', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'boolean', + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Fin_ap/CardFin_ap.tsx b/frontend/src/components/Fin_ap/CardFin_ap.tsx new file mode 100644 index 0000000..0f50e17 --- /dev/null +++ b/frontend/src/components/Fin_ap/CardFin_ap.tsx @@ -0,0 +1,164 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + fin_ap: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardFin_ap = ({ + fin_ap, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_FIN_AP'); + + return ( +
    + {loading && } +
      + {!loading && + fin_ap.map((item, index) => ( +
    • +
      + + {item.bill_no} + + +
      + +
      +
      +
      +
      +
      + Organization +
      +
      +
      + {dataFormatter.organizationsOneListFormatter(item.org)} +
      +
      +
      + +
      +
      + BillNumber +
      +
      +
      + {item.bill_no} +
      +
      +
      + +
      +
      + Vendor +
      +
      +
      + {dataFormatter.fin_vendorsOneListFormatter(item.vendor)} +
      +
      +
      + +
      +
      + DueDate +
      +
      +
      + {dataFormatter.dateTimeFormatter(item.due_date)} +
      +
      +
      + +
      +
      + Amount +
      +
      +
      + {item.amount} +
      +
      +
      + +
      +
      + Status +
      +
      +
      + {item.status} +
      +
      +
      +
      +
    • + ))} + {!loading && fin_ap.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardFin_ap; diff --git a/frontend/src/components/Fin_ap/ListFin_ap.tsx b/frontend/src/components/Fin_ap/ListFin_ap.tsx new file mode 100644 index 0000000..d6c9d1f --- /dev/null +++ b/frontend/src/components/Fin_ap/ListFin_ap.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + fin_ap: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListFin_ap = ({ + fin_ap, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_FIN_AP'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + fin_ap.map((item) => ( +
    + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Organization

    +

    + {dataFormatter.organizationsOneListFormatter(item.org)} +

    +
    + +
    +

    BillNumber

    +

    {item.bill_no}

    +
    + +
    +

    Vendor

    +

    + {dataFormatter.fin_vendorsOneListFormatter(item.vendor)} +

    +
    + +
    +

    DueDate

    +

    + {dataFormatter.dateTimeFormatter(item.due_date)} +

    +
    + +
    +

    Amount

    +

    {item.amount}

    +
    + +
    +

    Status

    +

    {item.status}

    +
    + + +
    +
    +
    + ))} + {!loading && fin_ap.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListFin_ap; diff --git a/frontend/src/components/Fin_ap/TableFin_ap.tsx b/frontend/src/components/Fin_ap/TableFin_ap.tsx new file mode 100644 index 0000000..4796bcc --- /dev/null +++ b/frontend/src/components/Fin_ap/TableFin_ap.tsx @@ -0,0 +1,497 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/fin_ap/fin_apSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureFin_apCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +import ListFin_ap from './ListFin_ap'; + +const perPage = 10; + +const TableSampleFin_ap = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + fin_ap, + loading, + count, + notify: fin_apNotify, + refetch, + } = useAppSelector((state) => state.fin_ap); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (fin_apNotify.showNotification) { + notify(fin_apNotify.typeNotification, fin_apNotify.textNotification); + } + }, [fin_apNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `fin_ap`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={fin_ap ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {fin_ap && Array.isArray(fin_ap) && !showGrid && ( + + )} + + {showGrid && dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleFin_ap; diff --git a/frontend/src/components/Fin_ap/configureFin_apCols.tsx b/frontend/src/components/Fin_ap/configureFin_apCols.tsx new file mode 100644 index 0000000..25dc858 --- /dev/null +++ b/frontend/src/components/Fin_ap/configureFin_apCols.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_FIN_AP'); + + return [ + { + field: 'org', + headerName: 'Organization', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'bill_no', + headerName: 'BillNumber', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'vendor', + headerName: 'Vendor', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('fin_vendors'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'due_date', + headerName: 'DueDate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.due_date), + }, + + { + field: 'amount', + headerName: 'Amount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'status', + headerName: 'Status', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Fin_ar/CardFin_ar.tsx b/frontend/src/components/Fin_ar/CardFin_ar.tsx new file mode 100644 index 0000000..59b8811 --- /dev/null +++ b/frontend/src/components/Fin_ar/CardFin_ar.tsx @@ -0,0 +1,166 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + fin_ar: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardFin_ar = ({ + fin_ar, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_FIN_AR'); + + return ( +
    + {loading && } +
      + {!loading && + fin_ar.map((item, index) => ( +
    • +
      + + {item.invoice_no} + + +
      + +
      +
      +
      +
      +
      + Organization +
      +
      +
      + {dataFormatter.organizationsOneListFormatter(item.org)} +
      +
      +
      + +
      +
      + InvoiceNumber +
      +
      +
      + {item.invoice_no} +
      +
      +
      + +
      +
      + Customer +
      +
      +
      + {dataFormatter.fin_customersOneListFormatter( + item.customer, + )} +
      +
      +
      + +
      +
      + DueDate +
      +
      +
      + {dataFormatter.dateTimeFormatter(item.due_date)} +
      +
      +
      + +
      +
      + Amount +
      +
      +
      + {item.amount} +
      +
      +
      + +
      +
      + Status +
      +
      +
      + {item.status} +
      +
      +
      +
      +
    • + ))} + {!loading && fin_ar.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardFin_ar; diff --git a/frontend/src/components/Fin_ar/ListFin_ar.tsx b/frontend/src/components/Fin_ar/ListFin_ar.tsx new file mode 100644 index 0000000..919916b --- /dev/null +++ b/frontend/src/components/Fin_ar/ListFin_ar.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + fin_ar: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListFin_ar = ({ + fin_ar, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_FIN_AR'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + fin_ar.map((item) => ( +
    + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Organization

    +

    + {dataFormatter.organizationsOneListFormatter(item.org)} +

    +
    + +
    +

    + InvoiceNumber +

    +

    {item.invoice_no}

    +
    + +
    +

    Customer

    +

    + {dataFormatter.fin_customersOneListFormatter( + item.customer, + )} +

    +
    + +
    +

    DueDate

    +

    + {dataFormatter.dateTimeFormatter(item.due_date)} +

    +
    + +
    +

    Amount

    +

    {item.amount}

    +
    + +
    +

    Status

    +

    {item.status}

    +
    + + +
    +
    +
    + ))} + {!loading && fin_ar.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListFin_ar; diff --git a/frontend/src/components/Fin_ar/TableFin_ar.tsx b/frontend/src/components/Fin_ar/TableFin_ar.tsx new file mode 100644 index 0000000..72f6c41 --- /dev/null +++ b/frontend/src/components/Fin_ar/TableFin_ar.tsx @@ -0,0 +1,497 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/fin_ar/fin_arSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureFin_arCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +import ListFin_ar from './ListFin_ar'; + +const perPage = 10; + +const TableSampleFin_ar = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + fin_ar, + loading, + count, + notify: fin_arNotify, + refetch, + } = useAppSelector((state) => state.fin_ar); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (fin_arNotify.showNotification) { + notify(fin_arNotify.typeNotification, fin_arNotify.textNotification); + } + }, [fin_arNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `fin_ar`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={fin_ar ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {fin_ar && Array.isArray(fin_ar) && !showGrid && ( + + )} + + {showGrid && dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleFin_ar; diff --git a/frontend/src/components/Fin_ar/configureFin_arCols.tsx b/frontend/src/components/Fin_ar/configureFin_arCols.tsx new file mode 100644 index 0000000..0b84130 --- /dev/null +++ b/frontend/src/components/Fin_ar/configureFin_arCols.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_FIN_AR'); + + return [ + { + field: 'org', + headerName: 'Organization', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'invoice_no', + headerName: 'InvoiceNumber', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'customer', + headerName: 'Customer', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('fin_customers'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'due_date', + headerName: 'DueDate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.due_date), + }, + + { + field: 'amount', + headerName: 'Amount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'status', + headerName: 'Status', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Fin_budgets/CardFin_budgets.tsx b/frontend/src/components/Fin_budgets/CardFin_budgets.tsx new file mode 100644 index 0000000..0e7e8cd --- /dev/null +++ b/frontend/src/components/Fin_budgets/CardFin_budgets.tsx @@ -0,0 +1,160 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + fin_budgets: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardFin_budgets = ({ + fin_budgets, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_FIN_BUDGETS'); + + return ( +
    + {loading && } +
      + {!loading && + fin_budgets.map((item, index) => ( +
    • +
      + + {item.fiscal_year} + + +
      + +
      +
      +
      +
      +
      + Organization +
      +
      +
      + {dataFormatter.organizationsOneListFormatter(item.org)} +
      +
      +
      + +
      +
      + FiscalYear +
      +
      +
      + {item.fiscal_year} +
      +
      +
      + +
      +
      + CostCenter +
      +
      +
      + {item.cost_center} +
      +
      +
      + +
      +
      + Account +
      +
      +
      + {dataFormatter.fin_accountsOneListFormatter(item.account)} +
      +
      +
      + +
      +
      Month
      +
      +
      {item.month}
      +
      +
      + +
      +
      + Amount +
      +
      +
      + {item.amount} +
      +
      +
      +
      +
    • + ))} + {!loading && fin_budgets.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardFin_budgets; diff --git a/frontend/src/components/Fin_budgets/ListFin_budgets.tsx b/frontend/src/components/Fin_budgets/ListFin_budgets.tsx new file mode 100644 index 0000000..129d073 --- /dev/null +++ b/frontend/src/components/Fin_budgets/ListFin_budgets.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + fin_budgets: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListFin_budgets = ({ + fin_budgets, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_FIN_BUDGETS'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + fin_budgets.map((item) => ( +
    + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Organization

    +

    + {dataFormatter.organizationsOneListFormatter(item.org)} +

    +
    + +
    +

    FiscalYear

    +

    {item.fiscal_year}

    +
    + +
    +

    CostCenter

    +

    {item.cost_center}

    +
    + +
    +

    Account

    +

    + {dataFormatter.fin_accountsOneListFormatter( + item.account, + )} +

    +
    + +
    +

    Month

    +

    {item.month}

    +
    + +
    +

    Amount

    +

    {item.amount}

    +
    + + +
    +
    +
    + ))} + {!loading && fin_budgets.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListFin_budgets; diff --git a/frontend/src/components/Fin_budgets/TableFin_budgets.tsx b/frontend/src/components/Fin_budgets/TableFin_budgets.tsx new file mode 100644 index 0000000..3c001c9 --- /dev/null +++ b/frontend/src/components/Fin_budgets/TableFin_budgets.tsx @@ -0,0 +1,500 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/fin_budgets/fin_budgetsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureFin_budgetsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +import ListFin_budgets from './ListFin_budgets'; + +const perPage = 10; + +const TableSampleFin_budgets = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + fin_budgets, + loading, + count, + notify: fin_budgetsNotify, + refetch, + } = useAppSelector((state) => state.fin_budgets); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (fin_budgetsNotify.showNotification) { + notify( + fin_budgetsNotify.typeNotification, + fin_budgetsNotify.textNotification, + ); + } + }, [fin_budgetsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `fin_budgets`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={fin_budgets ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {fin_budgets && Array.isArray(fin_budgets) && !showGrid && ( + + )} + + {showGrid && dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleFin_budgets; diff --git a/frontend/src/components/Fin_budgets/configureFin_budgetsCols.tsx b/frontend/src/components/Fin_budgets/configureFin_budgetsCols.tsx new file mode 100644 index 0000000..5a2f6da --- /dev/null +++ b/frontend/src/components/Fin_budgets/configureFin_budgetsCols.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_FIN_BUDGETS'); + + return [ + { + field: 'org', + headerName: 'Organization', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'fiscal_year', + headerName: 'FiscalYear', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'cost_center', + headerName: 'CostCenter', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'account', + headerName: 'Account', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('fin_accounts'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'month', + headerName: 'Month', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'amount', + headerName: 'Amount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Fin_customers/CardFin_customers.tsx b/frontend/src/components/Fin_customers/CardFin_customers.tsx new file mode 100644 index 0000000..233ff78 --- /dev/null +++ b/frontend/src/components/Fin_customers/CardFin_customers.tsx @@ -0,0 +1,141 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + fin_customers: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardFin_customers = ({ + fin_customers, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_FIN_CUSTOMERS', + ); + + return ( +
    + {loading && } +
      + {!loading && + fin_customers.map((item, index) => ( +
    • +
      + + {item.name} + + +
      + +
      +
      +
      +
      +
      + Organization +
      +
      +
      + {dataFormatter.organizationsOneListFormatter(item.org)} +
      +
      +
      + +
      +
      Name
      +
      +
      {item.name}
      +
      +
      + +
      +
      + Segment +
      +
      +
      + {item.segment} +
      +
      +
      + +
      +
      + Country +
      +
      +
      + {item.country} +
      +
      +
      +
      +
    • + ))} + {!loading && fin_customers.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardFin_customers; diff --git a/frontend/src/components/Fin_customers/ListFin_customers.tsx b/frontend/src/components/Fin_customers/ListFin_customers.tsx new file mode 100644 index 0000000..8f8834b --- /dev/null +++ b/frontend/src/components/Fin_customers/ListFin_customers.tsx @@ -0,0 +1,109 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + fin_customers: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListFin_customers = ({ + fin_customers, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_FIN_CUSTOMERS', + ); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + fin_customers.map((item) => ( +
    + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Organization

    +

    + {dataFormatter.organizationsOneListFormatter(item.org)} +

    +
    + +
    +

    Name

    +

    {item.name}

    +
    + +
    +

    Segment

    +

    {item.segment}

    +
    + +
    +

    Country

    +

    {item.country}

    +
    + + +
    +
    +
    + ))} + {!loading && fin_customers.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListFin_customers; diff --git a/frontend/src/components/Fin_customers/TableFin_customers.tsx b/frontend/src/components/Fin_customers/TableFin_customers.tsx new file mode 100644 index 0000000..2a3fcc8 --- /dev/null +++ b/frontend/src/components/Fin_customers/TableFin_customers.tsx @@ -0,0 +1,500 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/fin_customers/fin_customersSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureFin_customersCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +import ListFin_customers from './ListFin_customers'; + +const perPage = 10; + +const TableSampleFin_customers = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + fin_customers, + loading, + count, + notify: fin_customersNotify, + refetch, + } = useAppSelector((state) => state.fin_customers); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (fin_customersNotify.showNotification) { + notify( + fin_customersNotify.typeNotification, + fin_customersNotify.textNotification, + ); + } + }, [fin_customersNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `fin_customers`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={fin_customers ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {fin_customers && Array.isArray(fin_customers) && !showGrid && ( + + )} + + {showGrid && dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleFin_customers; diff --git a/frontend/src/components/Fin_customers/configureFin_customersCols.tsx b/frontend/src/components/Fin_customers/configureFin_customersCols.tsx new file mode 100644 index 0000000..e464f23 --- /dev/null +++ b/frontend/src/components/Fin_customers/configureFin_customersCols.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_FIN_CUSTOMERS'); + + return [ + { + field: 'org', + headerName: 'Organization', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'segment', + headerName: 'Segment', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'country', + headerName: 'Country', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Fin_forecasts/CardFin_forecasts.tsx b/frontend/src/components/Fin_forecasts/CardFin_forecasts.tsx new file mode 100644 index 0000000..7e74fbc --- /dev/null +++ b/frontend/src/components/Fin_forecasts/CardFin_forecasts.tsx @@ -0,0 +1,163 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + fin_forecasts: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardFin_forecasts = ({ + fin_forecasts, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_FIN_FORECASTS', + ); + + return ( +
    + {loading && } +
      + {!loading && + fin_forecasts.map((item, index) => ( +
    • +
      + + {item.fiscal_year} + + +
      + +
      +
      +
      +
      +
      + Organization +
      +
      +
      + {dataFormatter.organizationsOneListFormatter(item.org)} +
      +
      +
      + +
      +
      + FiscalYear +
      +
      +
      + {item.fiscal_year} +
      +
      +
      + +
      +
      + CostCenter +
      +
      +
      + {item.cost_center} +
      +
      +
      + +
      +
      + Account +
      +
      +
      + {dataFormatter.fin_accountsOneListFormatter(item.account)} +
      +
      +
      + +
      +
      Month
      +
      +
      {item.month}
      +
      +
      + +
      +
      + Amount +
      +
      +
      + {item.amount} +
      +
      +
      +
      +
    • + ))} + {!loading && fin_forecasts.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardFin_forecasts; diff --git a/frontend/src/components/Fin_forecasts/ListFin_forecasts.tsx b/frontend/src/components/Fin_forecasts/ListFin_forecasts.tsx new file mode 100644 index 0000000..a5b8b65 --- /dev/null +++ b/frontend/src/components/Fin_forecasts/ListFin_forecasts.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + fin_forecasts: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListFin_forecasts = ({ + fin_forecasts, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_FIN_FORECASTS', + ); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + fin_forecasts.map((item) => ( +
    + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Organization

    +

    + {dataFormatter.organizationsOneListFormatter(item.org)} +

    +
    + +
    +

    FiscalYear

    +

    {item.fiscal_year}

    +
    + +
    +

    CostCenter

    +

    {item.cost_center}

    +
    + +
    +

    Account

    +

    + {dataFormatter.fin_accountsOneListFormatter( + item.account, + )} +

    +
    + +
    +

    Month

    +

    {item.month}

    +
    + +
    +

    Amount

    +

    {item.amount}

    +
    + + +
    +
    +
    + ))} + {!loading && fin_forecasts.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListFin_forecasts; diff --git a/frontend/src/components/Fin_forecasts/TableFin_forecasts.tsx b/frontend/src/components/Fin_forecasts/TableFin_forecasts.tsx new file mode 100644 index 0000000..138205b --- /dev/null +++ b/frontend/src/components/Fin_forecasts/TableFin_forecasts.tsx @@ -0,0 +1,500 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/fin_forecasts/fin_forecastsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureFin_forecastsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +import ListFin_forecasts from './ListFin_forecasts'; + +const perPage = 10; + +const TableSampleFin_forecasts = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + fin_forecasts, + loading, + count, + notify: fin_forecastsNotify, + refetch, + } = useAppSelector((state) => state.fin_forecasts); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (fin_forecastsNotify.showNotification) { + notify( + fin_forecastsNotify.typeNotification, + fin_forecastsNotify.textNotification, + ); + } + }, [fin_forecastsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `fin_forecasts`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={fin_forecasts ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {fin_forecasts && Array.isArray(fin_forecasts) && !showGrid && ( + + )} + + {showGrid && dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleFin_forecasts; diff --git a/frontend/src/components/Fin_forecasts/configureFin_forecastsCols.tsx b/frontend/src/components/Fin_forecasts/configureFin_forecastsCols.tsx new file mode 100644 index 0000000..b2aebe4 --- /dev/null +++ b/frontend/src/components/Fin_forecasts/configureFin_forecastsCols.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_FIN_FORECASTS'); + + return [ + { + field: 'org', + headerName: 'Organization', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'fiscal_year', + headerName: 'FiscalYear', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'cost_center', + headerName: 'CostCenter', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'account', + headerName: 'Account', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('fin_accounts'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'month', + headerName: 'Month', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'amount', + headerName: 'Amount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Fin_transactions/CardFin_transactions.tsx b/frontend/src/components/Fin_transactions/CardFin_transactions.tsx new file mode 100644 index 0000000..93a0442 --- /dev/null +++ b/frontend/src/components/Fin_transactions/CardFin_transactions.tsx @@ -0,0 +1,231 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + fin_transactions: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardFin_transactions = ({ + fin_transactions, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_FIN_TRANSACTIONS', + ); + + return ( +
    + {loading && } +
      + {!loading && + fin_transactions.map((item, index) => ( +
    • +
      + + {item.memo} + + +
      + +
      +
      +
      +
      +
      + Organization +
      +
      +
      + {dataFormatter.organizationsOneListFormatter(item.org)} +
      +
      +
      + +
      +
      + Account +
      +
      +
      + {dataFormatter.fin_accountsOneListFormatter(item.account)} +
      +
      +
      + +
      +
      + TransactionTimestamp +
      +
      +
      + {dataFormatter.dateTimeFormatter(item.tx_ts)} +
      +
      +
      + +
      +
      + Amount +
      +
      +
      + {item.amount} +
      +
      +
      + +
      +
      + Currency +
      +
      +
      + {item.currency} +
      +
      +
      + +
      +
      + FXRate +
      +
      +
      + {item.fx_rate} +
      +
      +
      + +
      +
      + CostCenter +
      +
      +
      + {item.cost_center} +
      +
      +
      + +
      +
      + ProjectCode +
      +
      +
      + {item.project_code} +
      +
      +
      + +
      +
      + Vendor +
      +
      +
      + {dataFormatter.fin_vendorsOneListFormatter(item.vendor)} +
      +
      +
      + +
      +
      + Customer +
      +
      +
      + {dataFormatter.fin_customersOneListFormatter( + item.customer, + )} +
      +
      +
      + +
      +
      Memo
      +
      +
      {item.memo}
      +
      +
      + +
      +
      + Source +
      +
      +
      + {item.source} +
      +
      +
      +
      +
    • + ))} + {!loading && fin_transactions.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardFin_transactions; diff --git a/frontend/src/components/Fin_transactions/ListFin_transactions.tsx b/frontend/src/components/Fin_transactions/ListFin_transactions.tsx new file mode 100644 index 0000000..6a2acac --- /dev/null +++ b/frontend/src/components/Fin_transactions/ListFin_transactions.tsx @@ -0,0 +1,163 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + fin_transactions: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListFin_transactions = ({ + fin_transactions, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_FIN_TRANSACTIONS', + ); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + fin_transactions.map((item) => ( +
    + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Organization

    +

    + {dataFormatter.organizationsOneListFormatter(item.org)} +

    +
    + +
    +

    Account

    +

    + {dataFormatter.fin_accountsOneListFormatter( + item.account, + )} +

    +
    + +
    +

    + TransactionTimestamp +

    +

    + {dataFormatter.dateTimeFormatter(item.tx_ts)} +

    +
    + +
    +

    Amount

    +

    {item.amount}

    +
    + +
    +

    Currency

    +

    {item.currency}

    +
    + +
    +

    FXRate

    +

    {item.fx_rate}

    +
    + +
    +

    CostCenter

    +

    {item.cost_center}

    +
    + +
    +

    ProjectCode

    +

    {item.project_code}

    +
    + +
    +

    Vendor

    +

    + {dataFormatter.fin_vendorsOneListFormatter(item.vendor)} +

    +
    + +
    +

    Customer

    +

    + {dataFormatter.fin_customersOneListFormatter( + item.customer, + )} +

    +
    + +
    +

    Memo

    +

    {item.memo}

    +
    + +
    +

    Source

    +

    {item.source}

    +
    + + +
    +
    +
    + ))} + {!loading && fin_transactions.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListFin_transactions; diff --git a/frontend/src/components/Fin_transactions/TableFin_transactions.tsx b/frontend/src/components/Fin_transactions/TableFin_transactions.tsx new file mode 100644 index 0000000..662766e --- /dev/null +++ b/frontend/src/components/Fin_transactions/TableFin_transactions.tsx @@ -0,0 +1,500 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/fin_transactions/fin_transactionsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureFin_transactionsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +import ListFin_transactions from './ListFin_transactions'; + +const perPage = 10; + +const TableSampleFin_transactions = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + fin_transactions, + loading, + count, + notify: fin_transactionsNotify, + refetch, + } = useAppSelector((state) => state.fin_transactions); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (fin_transactionsNotify.showNotification) { + notify( + fin_transactionsNotify.typeNotification, + fin_transactionsNotify.textNotification, + ); + } + }, [fin_transactionsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `fin_transactions`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={fin_transactions ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {fin_transactions && Array.isArray(fin_transactions) && !showGrid && ( + + )} + + {showGrid && dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleFin_transactions; diff --git a/frontend/src/components/Fin_transactions/configureFin_transactionsCols.tsx b/frontend/src/components/Fin_transactions/configureFin_transactionsCols.tsx new file mode 100644 index 0000000..a8e6645 --- /dev/null +++ b/frontend/src/components/Fin_transactions/configureFin_transactionsCols.tsx @@ -0,0 +1,246 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_FIN_TRANSACTIONS'); + + return [ + { + field: 'org', + headerName: 'Organization', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'account', + headerName: 'Account', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('fin_accounts'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'tx_ts', + headerName: 'TransactionTimestamp', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.tx_ts), + }, + + { + field: 'amount', + headerName: 'Amount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'currency', + headerName: 'Currency', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'fx_rate', + headerName: 'FXRate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'cost_center', + headerName: 'CostCenter', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'project_code', + headerName: 'ProjectCode', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'vendor', + headerName: 'Vendor', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('fin_vendors'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'customer', + headerName: 'Customer', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('fin_customers'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'memo', + headerName: 'Memo', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'source', + headerName: 'Source', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Fin_vendors/CardFin_vendors.tsx b/frontend/src/components/Fin_vendors/CardFin_vendors.tsx new file mode 100644 index 0000000..29f5a11 --- /dev/null +++ b/frontend/src/components/Fin_vendors/CardFin_vendors.tsx @@ -0,0 +1,136 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + fin_vendors: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardFin_vendors = ({ + fin_vendors, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_FIN_VENDORS'); + + return ( +
    + {loading && } +
      + {!loading && + fin_vendors.map((item, index) => ( +
    • +
      + + {item.name} + + +
      + +
      +
      +
      +
      +
      + Organization +
      +
      +
      + {dataFormatter.organizationsOneListFormatter(item.org)} +
      +
      +
      + +
      +
      Name
      +
      +
      {item.name}
      +
      +
      + +
      +
      TaxID
      +
      +
      + {item.tax_id} +
      +
      +
      + +
      +
      + Country +
      +
      +
      + {item.country} +
      +
      +
      +
      +
    • + ))} + {!loading && fin_vendors.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardFin_vendors; diff --git a/frontend/src/components/Fin_vendors/ListFin_vendors.tsx b/frontend/src/components/Fin_vendors/ListFin_vendors.tsx new file mode 100644 index 0000000..8ba75e3 --- /dev/null +++ b/frontend/src/components/Fin_vendors/ListFin_vendors.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + fin_vendors: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListFin_vendors = ({ + fin_vendors, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_FIN_VENDORS'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + fin_vendors.map((item) => ( +
    + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Organization

    +

    + {dataFormatter.organizationsOneListFormatter(item.org)} +

    +
    + +
    +

    Name

    +

    {item.name}

    +
    + +
    +

    TaxID

    +

    {item.tax_id}

    +
    + +
    +

    Country

    +

    {item.country}

    +
    + + +
    +
    +
    + ))} + {!loading && fin_vendors.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListFin_vendors; diff --git a/frontend/src/components/Fin_vendors/TableFin_vendors.tsx b/frontend/src/components/Fin_vendors/TableFin_vendors.tsx new file mode 100644 index 0000000..e5915ad --- /dev/null +++ b/frontend/src/components/Fin_vendors/TableFin_vendors.tsx @@ -0,0 +1,500 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/fin_vendors/fin_vendorsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureFin_vendorsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +import ListFin_vendors from './ListFin_vendors'; + +const perPage = 10; + +const TableSampleFin_vendors = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + fin_vendors, + loading, + count, + notify: fin_vendorsNotify, + refetch, + } = useAppSelector((state) => state.fin_vendors); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (fin_vendorsNotify.showNotification) { + notify( + fin_vendorsNotify.typeNotification, + fin_vendorsNotify.textNotification, + ); + } + }, [fin_vendorsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `fin_vendors`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={fin_vendors ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {fin_vendors && Array.isArray(fin_vendors) && !showGrid && ( + + )} + + {showGrid && dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleFin_vendors; diff --git a/frontend/src/components/Fin_vendors/configureFin_vendorsCols.tsx b/frontend/src/components/Fin_vendors/configureFin_vendorsCols.tsx new file mode 100644 index 0000000..e061524 --- /dev/null +++ b/frontend/src/components/Fin_vendors/configureFin_vendorsCols.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_FIN_VENDORS'); + + return [ + { + field: 'org', + headerName: 'Organization', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'tax_id', + headerName: 'TaxID', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'country', + headerName: 'Country', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/FooterBar.tsx b/frontend/src/components/FooterBar.tsx new file mode 100644 index 0000000..7027c3d --- /dev/null +++ b/frontend/src/components/FooterBar.tsx @@ -0,0 +1,37 @@ +import React, { ReactNode } from 'react'; +import LanguageSwitcher from './LanguageSwitcher'; +import { containerMaxW } from '../config'; +import Logo from './Logo'; + +type Props = { + children?: ReactNode; +}; + +export default function FooterBar({ children }: Props) { + const year = new Date().getFullYear(); + + return ( + + ); +} diff --git a/frontend/src/components/FormCheckRadio.tsx b/frontend/src/components/FormCheckRadio.tsx new file mode 100644 index 0000000..17c00ea --- /dev/null +++ b/frontend/src/components/FormCheckRadio.tsx @@ -0,0 +1,20 @@ +import { ReactNode } from 'react'; + +type Props = { + children: ReactNode; + type: 'checkbox' | 'radio' | 'switch'; + label?: string; + className?: string; +}; + +const FormCheckRadio = (props: Props) => { + return ( + + ); +}; + +export default FormCheckRadio; diff --git a/frontend/src/components/FormCheckRadioGroup.tsx b/frontend/src/components/FormCheckRadioGroup.tsx new file mode 100644 index 0000000..4d1aa12 --- /dev/null +++ b/frontend/src/components/FormCheckRadioGroup.tsx @@ -0,0 +1,26 @@ +import { Children, cloneElement, ReactElement, ReactNode } from 'react'; + +type Props = { + isColumn?: boolean; + children: ReactNode; +}; + +const FormCheckRadioGroup = (props: Props) => { + return ( +
    + {Children.map(props.children, (child: ReactElement) => + cloneElement(child as ReactElement<{ className?: string }>, { + className: `mr-6 mb-3 last:mr-0 ${ + (child.props as { className?: string }).className || '' + }`, + }), + )} +
    + ); +}; + +export default FormCheckRadioGroup; diff --git a/frontend/src/components/FormField.tsx b/frontend/src/components/FormField.tsx new file mode 100644 index 0000000..ce5b38c --- /dev/null +++ b/frontend/src/components/FormField.tsx @@ -0,0 +1,92 @@ +import { Children, cloneElement, ReactElement, ReactNode } from 'react'; +import BaseIcon from './BaseIcon'; +import { useAppSelector } from '../stores/hooks'; + +type Props = { + label?: string; + labelFor?: string; + help?: string; + icons?: string[] | null[]; + isBorderless?: boolean; + isTransparent?: boolean; + hasTextareaHeight?: boolean; + children: ReactNode; + disabled?: boolean; + borderButtom?: boolean; + diversity?: boolean; + websiteBg?: boolean; +}; + +const FormField = ({ icons = [], ...props }: Props) => { + const childrenCount = Children.count(props.children); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const corners = useAppSelector((state) => state.style.corners); + const bgWebsiteColor = useAppSelector((state) => state.style.bgLayoutColor); + let elementWrapperClass = ''; + + switch (childrenCount) { + case 2: + elementWrapperClass = 'grid grid-cols-1 gap-3 md:grid-cols-2'; + break; + case 3: + elementWrapperClass = 'grid grid-cols-1 gap-3 md:grid-cols-3'; + } + + const controlClassName = [ + `px-3 py-2 max-w-full border-gray-300 dark:border-dark-700 ${corners} w-full dark:placeholder-gray-400`, + `${focusRing}`, + props.hasTextareaHeight ? 'h-24' : 'h-12', + props.isBorderless ? 'border-0' : 'border', + props.isTransparent + ? 'bg-transparent' + : `${props.websiteBg ? `${bgWebsiteColor} ` : bgColor} dark:bg-dark-800`, + props.disabled ? 'bg-gray-200 text-gray-100 dark:bg-dark-900 disabled' : '', + props.borderButtom + ? `border-0 border-b ${ + props.diversity + ? 'placeholder-primaryText border-primaryText' + : ' placeholder-white border-white ' + } rounded-none focus:ring-0` + : '', + ].join(' '); + + return ( +
    + {props.label && ( + + )} +
    + {Children.map(props.children, (child: ReactElement, index) => ( +
    + {cloneElement(child as ReactElement<{ className?: string }>, { + className: `${controlClassName} ${icons[index] ? 'pl-10' : ''}`, + })} + {icons[index] && ( + + )} +
    + ))} +
    + {props.help && ( +
    + {props.help} +
    + )} +
    + ); +}; + +export default FormField; diff --git a/frontend/src/components/FormFilePicker.tsx b/frontend/src/components/FormFilePicker.tsx new file mode 100644 index 0000000..7be6c35 --- /dev/null +++ b/frontend/src/components/FormFilePicker.tsx @@ -0,0 +1,101 @@ +import { useEffect, useState } from 'react'; +import { ColorButtonKey } from '../interfaces'; +import BaseButton from './BaseButton'; +import FileUploader from './Uploaders/UploadService'; +import { mdiReload } from '@mdi/js'; +import { useAppSelector } from '../stores/hooks'; + +type Props = { + label?: string; + icon?: string; + accept?: string; + color: ColorButtonKey; + isRoundIcon?: boolean; + path: string; + schema: object; + field: any; + form: any; +}; + +const FormFilePicker = ({ + label, + icon, + accept, + color, + isRoundIcon, + path, + schema, + form, + field, +}: Props) => { + const [file, setFile] = useState(null); + const [loading, setLoading] = useState(false); + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + let cornersRight; + if (corners === 'rounded') { + cornersRight = 'rounded-r'; + } else if (corners === 'rounded-lg') { + cornersRight = 'rounded-r-lg'; + } else if (corners === 'rounded-full') { + cornersRight = 'rounded-r-3xl'; + } else { + cornersRight = ''; + } + + useEffect(() => { + if (field.value) { + setFile(field.value[0]); + } + }, [field.value]); + const handleFileChange = async (event) => { + const file = event.currentTarget.files[0]; + setFile(file); + + FileUploader.validate(file, schema); + setLoading(true); + const remoteFile = await FileUploader.upload(path, file, schema); + + form.setFieldValue(field.name, [{ ...remoteFile }]); + setLoading(false); + }; + + const showFilename = !isRoundIcon && file; + + return ( +
    + + {showFilename && !loading && ( +
    + + {file.name} + +
    + )} +
    + ); +}; + +export default FormFilePicker; diff --git a/frontend/src/components/FormImagePicker.tsx b/frontend/src/components/FormImagePicker.tsx new file mode 100644 index 0000000..9c0b175 --- /dev/null +++ b/frontend/src/components/FormImagePicker.tsx @@ -0,0 +1,102 @@ +import { useState, useEffect } from 'react'; +import { ColorButtonKey } from '../interfaces'; +import BaseButton from './BaseButton'; +import ImagesUploader from './Uploaders/ImagesUploader'; +import FileUploader from './Uploaders/UploadService'; +import { mdiReload } from '@mdi/js'; +import { useAppSelector } from '../stores/hooks'; + +type Props = { + label?: string; + icon?: string; + accept?: string; + color: ColorButtonKey; + isRoundIcon?: boolean; + path: string; + schema: object; + field: any; + form: any; +}; + +const FormImagePicker = ({ + label, + icon, + accept, + color, + isRoundIcon, + path, + schema, + form, + field, +}: Props) => { + const [file, setFile] = useState(null); + const [loading, setLoading] = useState(false); + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + let cornersRight; + if (corners === 'rounded') { + cornersRight = 'rounded-r'; + } else if (corners === 'rounded-lg') { + cornersRight = 'rounded-r-lg'; + } else if (corners === 'rounded-full') { + cornersRight = 'rounded-r-3xl'; + } else { + cornersRight = ''; + } + + useEffect(() => { + if (field.value) { + setFile(field.value[0]); + } + }, [field.value]); + const handleFileChange = async (event) => { + const file = event.currentTarget.files[0]; + setFile(file); + + FileUploader.validate(file, schema); + setLoading(true); + const remoteFile = await FileUploader.upload(path, file, schema); + + form.setFieldValue(field.name, [{ ...remoteFile }]); + setLoading(false); + }; + + const showFilename = !isRoundIcon && file; + + return ( +
    + + {showFilename && !loading && ( +
    + + {file.name} + +
    + )} +
    + ); +}; + +export default FormImagePicker; diff --git a/frontend/src/components/Hr_applicants/CardHr_applicants.tsx b/frontend/src/components/Hr_applicants/CardHr_applicants.tsx new file mode 100644 index 0000000..3c87c28 --- /dev/null +++ b/frontend/src/components/Hr_applicants/CardHr_applicants.tsx @@ -0,0 +1,170 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + hr_applicants: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardHr_applicants = ({ + hr_applicants, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_HR_APPLICANTS', + ); + + return ( +
    + {loading && } +
      + {!loading && + hr_applicants.map((item, index) => ( +
    • +
      + + {item.name} + + +
      + +
      +
      +
      +
      +
      + Organization +
      +
      +
      + {dataFormatter.organizationsOneListFormatter(item.org)} +
      +
      +
      + +
      +
      + Requisition +
      +
      +
      + {dataFormatter.hr_reqsOneListFormatter(item.req)} +
      +
      +
      + +
      +
      Name
      +
      +
      {item.name}
      +
      +
      + +
      +
      Stage
      +
      +
      {item.stage}
      +
      +
      + +
      +
      + Source +
      +
      +
      + {item.source} +
      +
      +
      + +
      +
      + OfferExtendedAt +
      +
      +
      + {dataFormatter.dateTimeFormatter(item.offer_extended_at)} +
      +
      +
      + +
      +
      + OfferAcceptedAt +
      +
      +
      + {dataFormatter.dateTimeFormatter(item.offer_accepted_at)} +
      +
      +
      +
      +
    • + ))} + {!loading && hr_applicants.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardHr_applicants; diff --git a/frontend/src/components/Hr_applicants/ListHr_applicants.tsx b/frontend/src/components/Hr_applicants/ListHr_applicants.tsx new file mode 100644 index 0000000..69a9e5b --- /dev/null +++ b/frontend/src/components/Hr_applicants/ListHr_applicants.tsx @@ -0,0 +1,138 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + hr_applicants: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListHr_applicants = ({ + hr_applicants, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_HR_APPLICANTS', + ); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + hr_applicants.map((item) => ( +
    + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Organization

    +

    + {dataFormatter.organizationsOneListFormatter(item.org)} +

    +
    + +
    +

    Requisition

    +

    + {dataFormatter.hr_reqsOneListFormatter(item.req)} +

    +
    + +
    +

    Name

    +

    {item.name}

    +
    + +
    +

    Stage

    +

    {item.stage}

    +
    + +
    +

    Source

    +

    {item.source}

    +
    + +
    +

    + OfferExtendedAt +

    +

    + {dataFormatter.dateTimeFormatter( + item.offer_extended_at, + )} +

    +
    + +
    +

    + OfferAcceptedAt +

    +

    + {dataFormatter.dateTimeFormatter( + item.offer_accepted_at, + )} +

    +
    + + +
    +
    +
    + ))} + {!loading && hr_applicants.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListHr_applicants; diff --git a/frontend/src/components/Hr_applicants/TableHr_applicants.tsx b/frontend/src/components/Hr_applicants/TableHr_applicants.tsx new file mode 100644 index 0000000..754ffe7 --- /dev/null +++ b/frontend/src/components/Hr_applicants/TableHr_applicants.tsx @@ -0,0 +1,500 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/hr_applicants/hr_applicantsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureHr_applicantsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +import ListHr_applicants from './ListHr_applicants'; + +const perPage = 10; + +const TableSampleHr_applicants = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + hr_applicants, + loading, + count, + notify: hr_applicantsNotify, + refetch, + } = useAppSelector((state) => state.hr_applicants); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (hr_applicantsNotify.showNotification) { + notify( + hr_applicantsNotify.typeNotification, + hr_applicantsNotify.textNotification, + ); + } + }, [hr_applicantsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `hr_applicants`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={hr_applicants ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {hr_applicants && Array.isArray(hr_applicants) && !showGrid && ( + + )} + + {showGrid && dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleHr_applicants; diff --git a/frontend/src/components/Hr_applicants/configureHr_applicantsCols.tsx b/frontend/src/components/Hr_applicants/configureHr_applicantsCols.tsx new file mode 100644 index 0000000..a50fd6b --- /dev/null +++ b/frontend/src/components/Hr_applicants/configureHr_applicantsCols.tsx @@ -0,0 +1,170 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_HR_APPLICANTS'); + + return [ + { + field: 'org', + headerName: 'Organization', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'req', + headerName: 'Requisition', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('hr_reqs'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'stage', + headerName: 'Stage', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'source', + headerName: 'Source', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'offer_extended_at', + headerName: 'OfferExtendedAt', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.offer_extended_at), + }, + + { + field: 'offer_accepted_at', + headerName: 'OfferAcceptedAt', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.offer_accepted_at), + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Hr_attendance/CardHr_attendance.tsx b/frontend/src/components/Hr_attendance/CardHr_attendance.tsx new file mode 100644 index 0000000..a0dc120 --- /dev/null +++ b/frontend/src/components/Hr_attendance/CardHr_attendance.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + hr_attendance: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardHr_attendance = ({ + hr_attendance, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_HR_ATTENDANCE', + ); + + return ( +
    + {loading && } +
      + {!loading && + hr_attendance.map((item, index) => ( +
    • +
      + + {item.date} + + +
      + +
      +
      +
      +
      +
      + Organization +
      +
      +
      + {dataFormatter.organizationsOneListFormatter(item.org)} +
      +
      +
      + +
      +
      + Employee +
      +
      +
      + {dataFormatter.hr_employeesOneListFormatter( + item.employee, + )} +
      +
      +
      + +
      +
      Date
      +
      +
      + {dataFormatter.dateTimeFormatter(item.date)} +
      +
      +
      + +
      +
      + HoursWorked +
      +
      +
      + {item.hours_worked} +
      +
      +
      + +
      +
      + LeaveType +
      +
      +
      + {item.leave_type} +
      +
      +
      +
      +
    • + ))} + {!loading && hr_attendance.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardHr_attendance; diff --git a/frontend/src/components/Hr_attendance/ListHr_attendance.tsx b/frontend/src/components/Hr_attendance/ListHr_attendance.tsx new file mode 100644 index 0000000..b5c5587 --- /dev/null +++ b/frontend/src/components/Hr_attendance/ListHr_attendance.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + hr_attendance: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListHr_attendance = ({ + hr_attendance, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_HR_ATTENDANCE', + ); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + hr_attendance.map((item) => ( +
    + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Organization

    +

    + {dataFormatter.organizationsOneListFormatter(item.org)} +

    +
    + +
    +

    Employee

    +

    + {dataFormatter.hr_employeesOneListFormatter( + item.employee, + )} +

    +
    + +
    +

    Date

    +

    + {dataFormatter.dateTimeFormatter(item.date)} +

    +
    + +
    +

    HoursWorked

    +

    {item.hours_worked}

    +
    + +
    +

    LeaveType

    +

    {item.leave_type}

    +
    + + +
    +
    +
    + ))} + {!loading && hr_attendance.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListHr_attendance; diff --git a/frontend/src/components/Hr_attendance/TableHr_attendance.tsx b/frontend/src/components/Hr_attendance/TableHr_attendance.tsx new file mode 100644 index 0000000..76af373 --- /dev/null +++ b/frontend/src/components/Hr_attendance/TableHr_attendance.tsx @@ -0,0 +1,492 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/hr_attendance/hr_attendanceSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureHr_attendanceCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +import BigCalendar from '../BigCalendar'; +import { SlotInfo } from 'react-big-calendar'; + +const perPage = 100; + +const TableSampleHr_attendance = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + hr_attendance, + loading, + count, + notify: hr_attendanceNotify, + refetch, + } = useAppSelector((state) => state.hr_attendance); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (hr_attendanceNotify.showNotification) { + notify( + hr_attendanceNotify.typeNotification, + hr_attendanceNotify.textNotification, + ); + } + }, [hr_attendanceNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `hr_attendance`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={hr_attendance ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {dataGrid} + + {showGrid && dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleHr_attendance; diff --git a/frontend/src/components/Hr_attendance/configureHr_attendanceCols.tsx b/frontend/src/components/Hr_attendance/configureHr_attendanceCols.tsx new file mode 100644 index 0000000..c877e54 --- /dev/null +++ b/frontend/src/components/Hr_attendance/configureHr_attendanceCols.tsx @@ -0,0 +1,143 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_HR_ATTENDANCE'); + + return [ + { + field: 'org', + headerName: 'Organization', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'employee', + headerName: 'Employee', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('hr_employees'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'date', + headerName: 'Date', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => new Date(params.row.date), + }, + + { + field: 'hours_worked', + headerName: 'HoursWorked', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'leave_type', + headerName: 'LeaveType', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Hr_employees/CardHr_employees.tsx b/frontend/src/components/Hr_employees/CardHr_employees.tsx new file mode 100644 index 0000000..20e50e4 --- /dev/null +++ b/frontend/src/components/Hr_employees/CardHr_employees.tsx @@ -0,0 +1,207 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + hr_employees: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardHr_employees = ({ + hr_employees, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_HR_EMPLOYEES'); + + return ( +
    + {loading && } +
      + {!loading && + hr_employees.map((item, index) => ( +
    • +
      + + {item.name} + + +
      + +
      +
      +
      +
      +
      + Organization +
      +
      +
      + {dataFormatter.organizationsOneListFormatter(item.org)} +
      +
      +
      + +
      +
      + EmployeeNumber +
      +
      +
      + {item.employee_no} +
      +
      +
      + +
      +
      Name
      +
      +
      {item.name}
      +
      +
      + +
      +
      Email
      +
      +
      {item.email}
      +
      +
      + +
      +
      + Manager +
      +
      +
      + {dataFormatter.hr_employeesOneListFormatter(item.manager)} +
      +
      +
      + +
      +
      + Department +
      +
      +
      + {item.department} +
      +
      +
      + +
      +
      + Location +
      +
      +
      + {item.location} +
      +
      +
      + +
      +
      Grade
      +
      +
      {item.grade}
      +
      +
      + +
      +
      + HireDate +
      +
      +
      + {dataFormatter.dateTimeFormatter(item.hire_date)} +
      +
      +
      + +
      +
      + TerminationDate +
      +
      +
      + {dataFormatter.dateTimeFormatter(item.termination_date)} +
      +
      +
      + +
      +
      + Status +
      +
      +
      + {item.status} +
      +
      +
      +
      +
    • + ))} + {!loading && hr_employees.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardHr_employees; diff --git a/frontend/src/components/Hr_employees/ListHr_employees.tsx b/frontend/src/components/Hr_employees/ListHr_employees.tsx new file mode 100644 index 0000000..bf7a8b0 --- /dev/null +++ b/frontend/src/components/Hr_employees/ListHr_employees.tsx @@ -0,0 +1,153 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + hr_employees: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListHr_employees = ({ + hr_employees, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_HR_EMPLOYEES'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + hr_employees.map((item) => ( +
    + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Organization

    +

    + {dataFormatter.organizationsOneListFormatter(item.org)} +

    +
    + +
    +

    + EmployeeNumber +

    +

    {item.employee_no}

    +
    + +
    +

    Name

    +

    {item.name}

    +
    + +
    +

    Email

    +

    {item.email}

    +
    + +
    +

    Manager

    +

    + {dataFormatter.hr_employeesOneListFormatter( + item.manager, + )} +

    +
    + +
    +

    Department

    +

    {item.department}

    +
    + +
    +

    Location

    +

    {item.location}

    +
    + +
    +

    Grade

    +

    {item.grade}

    +
    + +
    +

    HireDate

    +

    + {dataFormatter.dateTimeFormatter(item.hire_date)} +

    +
    + +
    +

    + TerminationDate +

    +

    + {dataFormatter.dateTimeFormatter(item.termination_date)} +

    +
    + +
    +

    Status

    +

    {item.status}

    +
    + + +
    +
    +
    + ))} + {!loading && hr_employees.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListHr_employees; diff --git a/frontend/src/components/Hr_employees/TableHr_employees.tsx b/frontend/src/components/Hr_employees/TableHr_employees.tsx new file mode 100644 index 0000000..52123b2 --- /dev/null +++ b/frontend/src/components/Hr_employees/TableHr_employees.tsx @@ -0,0 +1,500 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/hr_employees/hr_employeesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureHr_employeesCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +import ListHr_employees from './ListHr_employees'; + +const perPage = 10; + +const TableSampleHr_employees = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + hr_employees, + loading, + count, + notify: hr_employeesNotify, + refetch, + } = useAppSelector((state) => state.hr_employees); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (hr_employeesNotify.showNotification) { + notify( + hr_employeesNotify.typeNotification, + hr_employeesNotify.textNotification, + ); + } + }, [hr_employeesNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `hr_employees`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={hr_employees ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {hr_employees && Array.isArray(hr_employees) && !showGrid && ( + + )} + + {showGrid && dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleHr_employees; diff --git a/frontend/src/components/Hr_employees/configureHr_employeesCols.tsx b/frontend/src/components/Hr_employees/configureHr_employeesCols.tsx new file mode 100644 index 0000000..5de240c --- /dev/null +++ b/frontend/src/components/Hr_employees/configureHr_employeesCols.tsx @@ -0,0 +1,218 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_HR_EMPLOYEES'); + + return [ + { + field: 'org', + headerName: 'Organization', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'employee_no', + headerName: 'EmployeeNumber', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'email', + headerName: 'Email', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'manager', + headerName: 'Manager', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('hr_employees'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'department', + headerName: 'Department', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'location', + headerName: 'Location', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'grade', + headerName: 'Grade', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'hire_date', + headerName: 'HireDate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.hire_date), + }, + + { + field: 'termination_date', + headerName: 'TerminationDate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.termination_date), + }, + + { + field: 'status', + headerName: 'Status', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Hr_payroll/CardHr_payroll.tsx b/frontend/src/components/Hr_payroll/CardHr_payroll.tsx new file mode 100644 index 0000000..80fcdbe --- /dev/null +++ b/frontend/src/components/Hr_payroll/CardHr_payroll.tsx @@ -0,0 +1,173 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + hr_payroll: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardHr_payroll = ({ + hr_payroll, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_HR_PAYROLL'); + + return ( +
    + {loading && } +
      + {!loading && + hr_payroll.map((item, index) => ( +
    • +
      + + {item.period} + + +
      + +
      +
      +
      +
      +
      + Organization +
      +
      +
      + {dataFormatter.organizationsOneListFormatter(item.org)} +
      +
      +
      + +
      +
      + Employee +
      +
      +
      + {dataFormatter.hr_employeesOneListFormatter( + item.employee, + )} +
      +
      +
      + +
      +
      + Period +
      +
      +
      + {dataFormatter.dateTimeFormatter(item.period)} +
      +
      +
      + +
      +
      + BasePay +
      +
      +
      + {item.base_pay} +
      +
      +
      + +
      +
      Bonus
      +
      +
      {item.bonus}
      +
      +
      + +
      +
      + OvertimeHours +
      +
      +
      + {item.overtime_hours} +
      +
      +
      + +
      +
      + Currency +
      +
      +
      + {item.currency} +
      +
      +
      +
      +
    • + ))} + {!loading && hr_payroll.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardHr_payroll; diff --git a/frontend/src/components/Hr_payroll/ListHr_payroll.tsx b/frontend/src/components/Hr_payroll/ListHr_payroll.tsx new file mode 100644 index 0000000..aebea51 --- /dev/null +++ b/frontend/src/components/Hr_payroll/ListHr_payroll.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + hr_payroll: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListHr_payroll = ({ + hr_payroll, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_HR_PAYROLL'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + hr_payroll.map((item) => ( +
    + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Organization

    +

    + {dataFormatter.organizationsOneListFormatter(item.org)} +

    +
    + +
    +

    Employee

    +

    + {dataFormatter.hr_employeesOneListFormatter( + item.employee, + )} +

    +
    + +
    +

    Period

    +

    + {dataFormatter.dateTimeFormatter(item.period)} +

    +
    + +
    +

    BasePay

    +

    {item.base_pay}

    +
    + +
    +

    Bonus

    +

    {item.bonus}

    +
    + +
    +

    + OvertimeHours +

    +

    {item.overtime_hours}

    +
    + +
    +

    Currency

    +

    {item.currency}

    +
    + + +
    +
    +
    + ))} + {!loading && hr_payroll.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListHr_payroll; diff --git a/frontend/src/components/Hr_payroll/TableHr_payroll.tsx b/frontend/src/components/Hr_payroll/TableHr_payroll.tsx new file mode 100644 index 0000000..caa3eff --- /dev/null +++ b/frontend/src/components/Hr_payroll/TableHr_payroll.tsx @@ -0,0 +1,500 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/hr_payroll/hr_payrollSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureHr_payrollCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +import ListHr_payroll from './ListHr_payroll'; + +const perPage = 10; + +const TableSampleHr_payroll = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + hr_payroll, + loading, + count, + notify: hr_payrollNotify, + refetch, + } = useAppSelector((state) => state.hr_payroll); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (hr_payrollNotify.showNotification) { + notify( + hr_payrollNotify.typeNotification, + hr_payrollNotify.textNotification, + ); + } + }, [hr_payrollNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `hr_payroll`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={hr_payroll ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {hr_payroll && Array.isArray(hr_payroll) && !showGrid && ( + + )} + + {showGrid && dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleHr_payroll; diff --git a/frontend/src/components/Hr_payroll/configureHr_payrollCols.tsx b/frontend/src/components/Hr_payroll/configureHr_payrollCols.tsx new file mode 100644 index 0000000..6c504f5 --- /dev/null +++ b/frontend/src/components/Hr_payroll/configureHr_payrollCols.tsx @@ -0,0 +1,172 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_HR_PAYROLL'); + + return [ + { + field: 'org', + headerName: 'Organization', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'employee', + headerName: 'Employee', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('hr_employees'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'period', + headerName: 'Period', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.period), + }, + + { + field: 'base_pay', + headerName: 'BasePay', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'bonus', + headerName: 'Bonus', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'overtime_hours', + headerName: 'OvertimeHours', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'currency', + headerName: 'Currency', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Hr_positions/CardHr_positions.tsx b/frontend/src/components/Hr_positions/CardHr_positions.tsx new file mode 100644 index 0000000..64043fc --- /dev/null +++ b/frontend/src/components/Hr_positions/CardHr_positions.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + hr_positions: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardHr_positions = ({ + hr_positions, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_HR_POSITIONS'); + + return ( +
    + {loading && } +
      + {!loading && + hr_positions.map((item, index) => ( +
    • +
      + + {item.title} + + +
      + +
      +
      +
      +
      +
      + Organization +
      +
      +
      + {dataFormatter.organizationsOneListFormatter(item.org)} +
      +
      +
      + +
      +
      Title
      +
      +
      {item.title}
      +
      +
      + +
      +
      + Department +
      +
      +
      + {item.department} +
      +
      +
      + +
      +
      Level
      +
      +
      {item.level}
      +
      +
      + +
      +
      + Location +
      +
      +
      + {item.location} +
      +
      +
      + +
      +
      + Status +
      +
      +
      + {item.status} +
      +
      +
      +
      +
    • + ))} + {!loading && hr_positions.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardHr_positions; diff --git a/frontend/src/components/Hr_positions/ListHr_positions.tsx b/frontend/src/components/Hr_positions/ListHr_positions.tsx new file mode 100644 index 0000000..6edcd02 --- /dev/null +++ b/frontend/src/components/Hr_positions/ListHr_positions.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + hr_positions: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListHr_positions = ({ + hr_positions, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_HR_POSITIONS'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + hr_positions.map((item) => ( +
    + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Organization

    +

    + {dataFormatter.organizationsOneListFormatter(item.org)} +

    +
    + +
    +

    Title

    +

    {item.title}

    +
    + +
    +

    Department

    +

    {item.department}

    +
    + +
    +

    Level

    +

    {item.level}

    +
    + +
    +

    Location

    +

    {item.location}

    +
    + +
    +

    Status

    +

    {item.status}

    +
    + + +
    +
    +
    + ))} + {!loading && hr_positions.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListHr_positions; diff --git a/frontend/src/components/Hr_positions/TableHr_positions.tsx b/frontend/src/components/Hr_positions/TableHr_positions.tsx new file mode 100644 index 0000000..3ddbb03 --- /dev/null +++ b/frontend/src/components/Hr_positions/TableHr_positions.tsx @@ -0,0 +1,500 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/hr_positions/hr_positionsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureHr_positionsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +import ListHr_positions from './ListHr_positions'; + +const perPage = 10; + +const TableSampleHr_positions = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + hr_positions, + loading, + count, + notify: hr_positionsNotify, + refetch, + } = useAppSelector((state) => state.hr_positions); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (hr_positionsNotify.showNotification) { + notify( + hr_positionsNotify.typeNotification, + hr_positionsNotify.textNotification, + ); + } + }, [hr_positionsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `hr_positions`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={hr_positions ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {hr_positions && Array.isArray(hr_positions) && !showGrid && ( + + )} + + {showGrid && dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleHr_positions; diff --git a/frontend/src/components/Hr_positions/configureHr_positionsCols.tsx b/frontend/src/components/Hr_positions/configureHr_positionsCols.tsx new file mode 100644 index 0000000..1e454b1 --- /dev/null +++ b/frontend/src/components/Hr_positions/configureHr_positionsCols.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_HR_POSITIONS'); + + return [ + { + field: 'org', + headerName: 'Organization', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'title', + headerName: 'Title', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'department', + headerName: 'Department', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'level', + headerName: 'Level', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'location', + headerName: 'Location', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'status', + headerName: 'Status', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Hr_reqs/CardHr_reqs.tsx b/frontend/src/components/Hr_reqs/CardHr_reqs.tsx new file mode 100644 index 0000000..e1bda52 --- /dev/null +++ b/frontend/src/components/Hr_reqs/CardHr_reqs.tsx @@ -0,0 +1,177 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + hr_reqs: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardHr_reqs = ({ + hr_reqs, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_HR_REQS'); + + return ( +
    + {loading && } +
      + {!loading && + hr_reqs.map((item, index) => ( +
    • +
      + + {item.recruiter} + + +
      + +
      +
      +
      +
      +
      + Organization +
      +
      +
      + {dataFormatter.organizationsOneListFormatter(item.org)} +
      +
      +
      + +
      +
      + Position +
      +
      +
      + {dataFormatter.hr_positionsOneListFormatter( + item.position, + )} +
      +
      +
      + +
      +
      + OpenedAt +
      +
      +
      + {dataFormatter.dateTimeFormatter(item.opened_at)} +
      +
      +
      + +
      +
      + FilledAt +
      +
      +
      + {dataFormatter.dateTimeFormatter(item.filled_at)} +
      +
      +
      + +
      +
      + Status +
      +
      +
      + {item.status} +
      +
      +
      + +
      +
      + Recruiter +
      +
      +
      + {item.recruiter} +
      +
      +
      + +
      +
      + Priority +
      +
      +
      + {item.priority} +
      +
      +
      +
      +
    • + ))} + {!loading && hr_reqs.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardHr_reqs; diff --git a/frontend/src/components/Hr_reqs/ListHr_reqs.tsx b/frontend/src/components/Hr_reqs/ListHr_reqs.tsx new file mode 100644 index 0000000..4b903f4 --- /dev/null +++ b/frontend/src/components/Hr_reqs/ListHr_reqs.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + hr_reqs: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListHr_reqs = ({ + hr_reqs, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_HR_REQS'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + hr_reqs.map((item) => ( +
    + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Organization

    +

    + {dataFormatter.organizationsOneListFormatter(item.org)} +

    +
    + +
    +

    Position

    +

    + {dataFormatter.hr_positionsOneListFormatter( + item.position, + )} +

    +
    + +
    +

    OpenedAt

    +

    + {dataFormatter.dateTimeFormatter(item.opened_at)} +

    +
    + +
    +

    FilledAt

    +

    + {dataFormatter.dateTimeFormatter(item.filled_at)} +

    +
    + +
    +

    Status

    +

    {item.status}

    +
    + +
    +

    Recruiter

    +

    {item.recruiter}

    +
    + +
    +

    Priority

    +

    {item.priority}

    +
    + + +
    +
    +
    + ))} + {!loading && hr_reqs.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListHr_reqs; diff --git a/frontend/src/components/Hr_reqs/TableHr_reqs.tsx b/frontend/src/components/Hr_reqs/TableHr_reqs.tsx new file mode 100644 index 0000000..70f93ac --- /dev/null +++ b/frontend/src/components/Hr_reqs/TableHr_reqs.tsx @@ -0,0 +1,497 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/hr_reqs/hr_reqsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureHr_reqsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +import ListHr_reqs from './ListHr_reqs'; + +const perPage = 10; + +const TableSampleHr_reqs = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + hr_reqs, + loading, + count, + notify: hr_reqsNotify, + refetch, + } = useAppSelector((state) => state.hr_reqs); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (hr_reqsNotify.showNotification) { + notify(hr_reqsNotify.typeNotification, hr_reqsNotify.textNotification); + } + }, [hr_reqsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `hr_reqs`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={hr_reqs ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {hr_reqs && Array.isArray(hr_reqs) && !showGrid && ( + + )} + + {showGrid && dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleHr_reqs; diff --git a/frontend/src/components/Hr_reqs/configureHr_reqsCols.tsx b/frontend/src/components/Hr_reqs/configureHr_reqsCols.tsx new file mode 100644 index 0000000..b2204ab --- /dev/null +++ b/frontend/src/components/Hr_reqs/configureHr_reqsCols.tsx @@ -0,0 +1,170 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_HR_REQS'); + + return [ + { + field: 'org', + headerName: 'Organization', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'position', + headerName: 'Position', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('hr_positions'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'opened_at', + headerName: 'OpenedAt', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.opened_at), + }, + + { + field: 'filled_at', + headerName: 'FilledAt', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.filled_at), + }, + + { + field: 'status', + headerName: 'Status', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'recruiter', + headerName: 'Recruiter', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'priority', + headerName: 'Priority', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/IconRounded.tsx b/frontend/src/components/IconRounded.tsx new file mode 100644 index 0000000..7ec5864 --- /dev/null +++ b/frontend/src/components/IconRounded.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { ColorKey } from '../interfaces'; +import { colorsBgLight, colorsText } from '../colors'; +import BaseIcon from './BaseIcon'; + +type Props = { + icon: string; + color: ColorKey; + w?: string; + h?: string; + bg?: boolean; + className?: string; +}; + +export default function IconRounded({ + icon, + color, + w = 'w-12', + h = 'h-12', + bg = false, + className = '', +}: Props) { + const classAddon = bg + ? colorsBgLight[color] + : `${colorsText[color]} bg-gray-50 dark:bg-slate-800`; + + return ( + + ); +} diff --git a/frontend/src/components/ImageField.tsx b/frontend/src/components/ImageField.tsx new file mode 100644 index 0000000..c575026 --- /dev/null +++ b/frontend/src/components/ImageField.tsx @@ -0,0 +1,51 @@ +/* eslint-disable @next/next/no-img-element */ +// Why disabled: +// avatars.dicebear.com provides svg avatars +// next/image needs dangerouslyAllowSVG option for that + +import React, { ReactNode } from 'react'; +import { mdiImageOutline } from '@mdi/js'; +import BaseIcon from './BaseIcon'; + +type Props = { + name: string; + image?: object | null; + api?: string; + className?: string; + imageClassName?: string; + children?: ReactNode; +}; + +export default function ImageField({ + name, + image, + className = '', + imageClassName = '', + children, +}: Props) { + const imageSrc = image && image[0] ? `${image[0].publicUrl}` : ''; + + return ( +
    + {imageSrc ? ( + {name} + ) : ( +
    + +
    + )} + + {children} +
    + ); +} diff --git a/frontend/src/components/IntroGuide.tsx b/frontend/src/components/IntroGuide.tsx new file mode 100644 index 0000000..7271c83 --- /dev/null +++ b/frontend/src/components/IntroGuide.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Steps, Hints } from 'intro.js-react'; +import { useRouter } from 'next/router'; +interface IntroGuideProps { + steps: Array<{ + element: string; + intro: string; + position?: string; + }>; + disableInteraction?: boolean; + stepsEnabled: boolean; + stepsName: string; + onExit: () => void; +} + +const IntroGuide: React.FC = ({ + steps, + stepsEnabled, + onExit, + stepsName, +}) => { + const router = useRouter(); + const handleStepChange = (stepIndex: number) => { + if (stepIndex === 7 && stepsName === 'appSteps') { + onExit(); + router.push('/users/users-list/'); + } else if (stepIndex === 2 && stepsName === 'usersSteps') { + onExit(); + router.push('/roles/roles-list/'); + } + }; + + const handleExit = () => { + localStorage.setItem(`completed_${stepsName}`, 'true'); + onExit(); + }; + return ( + <> + + + ); +}; + +export default IntroGuide; diff --git a/frontend/src/components/KanbanBoard/KanbanBoard.tsx b/frontend/src/components/KanbanBoard/KanbanBoard.tsx new file mode 100644 index 0000000..3fdcf7d --- /dev/null +++ b/frontend/src/components/KanbanBoard/KanbanBoard.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import KanbanColumn from './KanbanColumn'; +import { AsyncThunk } from '@reduxjs/toolkit'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; + +type Props = { + columns: Array<{ id: string; label: string }>; + filtersQuery: string; + entityName: string; + columnFieldName: string; + showFieldName: string; + deleteThunk: AsyncThunk; + updateThunk: AsyncThunk; +}; + +const KanbanBoard = ({ + columns, + entityName, + columnFieldName, + filtersQuery, + showFieldName, + deleteThunk, + updateThunk, +}: Props) => { + return ( +
    + + {columns.map((column) => ( +
    + +
    + ))} +
    +
    + ); +}; + +export default KanbanBoard; diff --git a/frontend/src/components/KanbanBoard/KanbanCard.tsx b/frontend/src/components/KanbanBoard/KanbanCard.tsx new file mode 100644 index 0000000..4265e0a --- /dev/null +++ b/frontend/src/components/KanbanBoard/KanbanCard.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import Link from 'next/link'; +import moment from 'moment'; +import ListActionsPopover from '../ListActionsPopover'; +import { DragSourceMonitor, useDrag } from 'react-dnd'; + +type Props = { + item: any; + column: { id: string; label: string }; + entityName: string; + showFieldName: string; + setItemIdToDelete: (id: string) => void; +}; + +const KanbanCard = ({ + item, + entityName, + showFieldName, + setItemIdToDelete, + column, +}: Props) => { + const [{ isDragging }, drag] = useDrag( + () => ({ + type: 'box', + item: { item, column }, + collect: (monitor: DragSourceMonitor) => ({ + isDragging: monitor.isDragging(), + }), + }), + [item], + ); + + return ( +
    +
    + + {item[showFieldName] ?? 'No data'} + +
    +
    +

    {moment(item.createdAt).format('MMM DD hh:mm a')}

    + setItemIdToDelete(id)} + hasUpdatePermission={true} + className={'w-2 h-2 text-white'} + iconClassName={'w-5'} + /> +
    +
    + ); +}; + +export default KanbanCard; diff --git a/frontend/src/components/KanbanBoard/KanbanColumn.tsx b/frontend/src/components/KanbanBoard/KanbanColumn.tsx new file mode 100644 index 0000000..3b9e67d --- /dev/null +++ b/frontend/src/components/KanbanBoard/KanbanColumn.tsx @@ -0,0 +1,215 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import Axios from 'axios'; +import CardBox from '../CardBox'; +import CardBoxModal from '../CardBoxModal'; +import { AsyncThunk } from '@reduxjs/toolkit'; +import { useDrop } from 'react-dnd'; +import KanbanCard from './KanbanCard'; + +type Props = { + column: { id: string; label: string }; + entityName: string; + columnFieldName: string; + showFieldName: string; + filtersQuery: any; + deleteThunk: AsyncThunk; + updateThunk: AsyncThunk; +}; + +type DropResult = { + sourceColumn: { id: string; label: string }; + item: any; +}; + +const perPage = 10; + +const KanbanColumn = ({ + column, + entityName, + columnFieldName, + showFieldName, + filtersQuery, + deleteThunk, + updateThunk, +}: Props) => { + const [currentPage, setCurrentPage] = useState(0); + const [count, setCount] = useState(0); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [itemIdToDelete, setItemIdToDelete] = useState(''); + const currentUser = useAppSelector((state) => state.auth.currentUser); + const listInnerRef = useRef(null); + const dispatch = useAppDispatch(); + + const [{ dropResult }, drop] = useDrop< + { + item: any; + column: { + id: string; + label: string; + }; + }, + unknown, + { + dropResult: DropResult; + } + >( + () => ({ + accept: 'box', + drop: ({ + item, + column: sourceColumn, + }: { + item: any; + column: { id: string; label: string }; + }) => { + if (sourceColumn.id === column.id) return; + + dispatch( + updateThunk({ + id: item.id, + data: { + [columnFieldName]: column.id, + }, + }), + ).then((res) => { + setData((prevState) => (prevState ? [...prevState, item] : [item])); + setCount((prevState) => prevState + 1); + }); + + return { sourceColumn, item }; + }, + collect: (monitor) => ({ + dropResult: monitor.getDropResult(), + }), + }), + [], + ); + + const loadData = useCallback( + (page: number, filters = '') => { + const query = `?page=${page}&limit=${perPage}&field=createdAt&sort=desc&${columnFieldName}=${column.id}&${filters}`; + setLoading(true); + Axios.get(`${entityName}${query}`) + .then((res) => { + setData((prevState) => + page === 0 ? res.data.rows : [...prevState, ...res.data.rows], + ); + setCount(res.data.count); + setCurrentPage(page); + }) + .catch((err) => { + console.error(err); + }) + .finally(() => { + setLoading(false); + }); + }, + [currentUser, column], + ); + + useEffect(() => { + if (!currentUser) return; + loadData(0, filtersQuery); + }, [currentUser, loadData, filtersQuery]); + + useEffect(() => { + loadData(0, filtersQuery); + }, [loadData, filtersQuery]); + + useEffect(() => { + if (dropResult?.sourceColumn && dropResult.sourceColumn.id === column.id) { + setData((prevState) => + prevState.filter((item) => item.id !== dropResult.item.id), + ); + setCount((prevState) => prevState - 1); + } + }, [dropResult]); + + const onScroll = () => { + if (listInnerRef.current) { + const { scrollTop, scrollHeight, clientHeight } = listInnerRef.current; + if (Math.floor(scrollTop + clientHeight) === scrollHeight) { + if (data.length < count && !loading) { + loadData(currentPage + 1, filtersQuery); + } + } + } + }; + + const onDeleteConfirm = () => { + if (!itemIdToDelete) return; + + dispatch(deleteThunk(itemIdToDelete)) + .then((res) => { + if (res.meta.requestStatus === 'fulfilled') { + setItemIdToDelete(''); + loadData(0, filtersQuery); + } + }) + .catch((err) => { + console.error(err); + }) + .finally(() => { + setItemIdToDelete(''); + }); + }; + + return ( + <> + +
    +

    {column.label}

    +

    {count}

    +
    +
    { + drop(node); + listInnerRef.current = node; + }} + className={'p-3 space-y-3 flex-1 overflow-y-auto max-h-[400px]'} + onScroll={onScroll} + > + {data?.map((item) => ( +
    + +
    + ))} + {!data?.length && ( +

    + No data +

    + )} +
    +
    + setItemIdToDelete('')} + > +

    Are you sure you want to delete this item?

    +
    + + ); +}; + +export default KanbanColumn; diff --git a/frontend/src/components/LanguageSwitcher.tsx b/frontend/src/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..c311bcf --- /dev/null +++ b/frontend/src/components/LanguageSwitcher.tsx @@ -0,0 +1,104 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'next-i18next'; +import Select, { + components, + SingleValueProps, + OptionProps, +} from 'react-select'; + +type LanguageOption = { label: string; value: string }; + +const LANGS: LanguageOption[] = [ + { value: 'en', label: '🇬🇧 EN' }, + { value: 'fr', label: '🇫🇷 FR' }, + { value: 'es', label: '🇪🇸 ES' }, + { value: 'de', label: '🇩🇪 DE' }, +]; + +const Option = (props: OptionProps) => ( + + {props.data.label} + +); + +const SingleVal = (props: SingleValueProps) => ( + + {props.data.label} + +); + +const LanguageSwitcher: React.FC = () => { + const { i18n } = useTranslation(); + const [mounted, setMounted] = useState(false); + const [selected, setSelected] = useState(LANGS[0]); + + useEffect(() => { + setMounted(true); + setSelected(LANGS.find((l) => l.value === i18n.language) ?? LANGS[0]); + }, [i18n.language]); + + const handleChange = (opt: LanguageOption | null) => { + if (!opt) return; + setSelected(opt); + i18n.changeLanguage(opt.value); + }; + + if (!mounted) return null; + + return ( +
    + + + ); + + return ( +
    + {readonly || (max && fileList().length >= max) ? null : uploadButton} + + {valuesArr() && valuesArr().length ? ( +
    + {valuesArr().map((item) => { + return ( +
    + + + {item.name} + + + {!readonly && ( + + )} +
    + ); + })} +
    + ) : null} +
    + ); +}; + +FilesUploader.propTypes = { + readonly: PropTypes.bool, + path: PropTypes.string, + max: PropTypes.number, + schema: PropTypes.shape({ + image: PropTypes.bool, + size: PropTypes.number, + formats: PropTypes.arrayOf(PropTypes.string), + }), + value: PropTypes.any, + onChange: PropTypes.func, +}; + +export default FilesUploader; diff --git a/frontend/src/components/Uploaders/ImagesUploader.js b/frontend/src/components/Uploaders/ImagesUploader.js new file mode 100644 index 0000000..f4e14d0 --- /dev/null +++ b/frontend/src/components/Uploaders/ImagesUploader.js @@ -0,0 +1,227 @@ +import React, { useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import Button from '@mui/material/Button'; +import CloseIcon from '@mui/icons-material/Close'; +import SearchIcon from '@mui/icons-material/Search'; +import Grid from '@mui/material/Grid'; +import Box from '@mui/material/Box'; +import Dialog from '@mui/material/Dialog'; +import FileUploader from 'components/FormItems/uploaders/UploadService'; +import Errors from '../../../components/FormItems/error/errors'; +import { makeStyles } from '@mui/styles'; + +const useStyles = makeStyles({ + actionButtonsWrapper: { + position: 'relative', + }, + previewContent: { + padding: '0px !important', + }, + imageItem: { + '&.MuiGrid-root': { + margin: 10, + boxShadow: '2px 2px 8px 0 rgb(0 0 0 / 40%)', + borderRadius: 10, + }, + height: '100px', + }, + actionButtons: { + position: 'absolute', + bottom: 5, + right: 4, + }, + previewContainer: { + '& button': { + position: 'absolute', + top: 10, + right: 10, + '& svg': { + height: 50, + width: 50, + fill: '#FFF', + stroke: '#909090', + strokeWidth: 0.5, + }, + }, + }, + button: { + padding: '0px !important', + minWidth: '45px !important', + '& svg': { + height: 36, + width: 36, + fill: '#FFF', + stroke: '#909090', + strokeWidth: 0.5, + }, + }, +}); + +const ImagesUploader = (props) => { + const classes = useStyles(); + const { value, onChange, schema, path, max, readonly, name } = props; + + const [loading, setLoading] = useState(false); + const [showPreview, setShowPreview] = useState(false); + const [imageMeta, setImageMeta] = useState({ + imageSrc: null, + imageAlt: null, + }); + const inputElement = useRef(null); + + const valuesArr = () => { + if (!value) { + return []; + } + return Array.isArray(value) ? value : [value]; + }; + + const fileList = () => { + return valuesArr().map((item) => ({ + uid: item.id || undefined, + name: item.name, + status: 'done', + url: item.publicUrl, + })); + }; + + const handleRemove = (id) => { + onChange(valuesArr().filter((item) => item.id !== id)); + }; + + const handleChange = async (event) => { + try { + const files = event.target.files; + + if (!files || !files.length) { + return; + } + + let file = files[0]; + + FileUploader.validate(file, schema); + + setLoading(true); + + file = await FileUploader.upload(path, file, schema); + + inputElement.current.value = ''; + setLoading(false); + onChange([...valuesArr(), file]); + } catch (error) { + inputElement.current.value = ''; + console.log('error', error); + setLoading(false); + Errors.showMessage(error); + } + }; + + const doPreviewImage = (image) => { + setImageMeta({ + imageSrc: image.publicUrl, + imageAlt: image.name, + }); + setShowPreview(true); + }; + + const doCloseImageModal = () => { + setImageMeta({ + imageSrc: null, + imageAlt: null, + }); + setShowPreview(false); + }; + + const uploadButton = ( + + + + ); + + return ( + + {readonly || (max && fileList().length >= max) ? null : uploadButton} + + {valuesArr() && valuesArr().length ? ( + + {valuesArr().map((item) => { + return ( + + {item.name} + +
    +
    + + {!readonly && ( + + )} +
    +
    +
    + ); + })} +
    + ) : null} + + + {imageMeta.imageAlt} + +
    + ); +}; + +ImagesUploader.propTypes = { + readonly: PropTypes.bool, + path: PropTypes.string, + max: PropTypes.number, + schema: PropTypes.shape({ + image: PropTypes.bool, + size: PropTypes.number, + formats: PropTypes.arrayOf(PropTypes.string), + }), + value: PropTypes.any, + onChange: PropTypes.func, + name: PropTypes.string, +}; + +export default ImagesUploader; diff --git a/frontend/src/components/Uploaders/UploadService.js b/frontend/src/components/Uploaders/UploadService.js new file mode 100644 index 0000000..19c5304 --- /dev/null +++ b/frontend/src/components/Uploaders/UploadService.js @@ -0,0 +1,82 @@ +import { v4 as uuidv4 } from 'uuid'; +import Axios from 'axios'; +import { baseURLApi } from '../../config'; + +function extractExtensionFrom(filename) { + if (!filename) { + return null; + } + + const regex = /(?:\.([^.]+))?$/; + return regex.exec(filename)[1]; +} + +export default class FileUploader { + static validate(file, schema) { + if (!schema) { + return; + } + + if (schema.image) { + if (!file.type.startsWith('image')) { + throw new Error('You must upload an image'); + } + } + + if (schema.size && file.size > schema.size) { + throw new Error('File is too big.'); + } + + const extension = extractExtensionFrom(file.name); + + if (schema.formats && !schema.formats.includes(extension)) { + throw new Error('Invalid format'); + } + } + + static async upload(path, file, schema) { + try { + this.validate(file, schema); + } catch (error) { + return Promise.reject(error); + } + + const extension = extractExtensionFrom(file.name); + const id = uuidv4(); + const filename = `${id}.${extension}`; + const privateUrl = `${path}/${filename}`; + + const publicUrl = await this.uploadToServer(file, path, filename); + + return { + id: id, + name: file.name, + sizeInBytes: file.size, + privateUrl, + publicUrl, + new: true, + }; + } + + static async uploadToServer(file, path, filename) { + const formData = new FormData(); + formData.append('file', file); + formData.append('filename', filename); + const uri = `/file/upload/${path}`; + await Axios.post(uri, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + const privateUrl = `${path}/${filename}`; + + console.log( + 'process.env.NODE_ENV in uploadToServer function', + process.env.NODE_ENV, + ); + console.log('baseURLApi in uploadToServer function', baseURLApi); + + return `${baseURLApi}/file/download?privateUrl=${privateUrl}`; + } +} diff --git a/frontend/src/components/UserAvatar.tsx b/frontend/src/components/UserAvatar.tsx new file mode 100644 index 0000000..a517a4c --- /dev/null +++ b/frontend/src/components/UserAvatar.tsx @@ -0,0 +1,48 @@ +/* eslint-disable @next/next/no-img-element */ +// Why disabled: +// avatars.dicebear.com provides svg avatars +// next/image needs dangerouslyAllowSVG option for that + +import React, { ReactNode } from 'react'; +import BaseIcon from './BaseIcon'; +import { mdiAccountCircleOutline } from '@mdi/js'; + +type Props = { + username: string; + avatar?: string | null; + image?: object | null; + api?: string; + className?: string; + children?: ReactNode; +}; + +export default function UserAvatar({ + username, + image, + avatar, + className = '', + children, +}: Props) { + const avatarImage = image && image[0] ? `${image[0].publicUrl}` : '#'; + + return ( +
    + {avatarImage === '#' ? ( + + ) : ( + {username} + )} + {children} +
    + ); +} diff --git a/frontend/src/components/UserAvatarCurrentUser.tsx b/frontend/src/components/UserAvatarCurrentUser.tsx new file mode 100644 index 0000000..1bcf833 --- /dev/null +++ b/frontend/src/components/UserAvatarCurrentUser.tsx @@ -0,0 +1,48 @@ +import React, { ReactNode, useEffect, useState } from 'react'; +import { useAppSelector } from '../stores/hooks'; +import UserAvatar from './UserAvatar'; + +type Props = { + className?: string; + children?: ReactNode; +}; + +export default function UserAvatarCurrentUser({ + className = '', + children, +}: Props) { + const userName = useAppSelector((state) => state.main.userName); + const userAvatar = useAppSelector((state) => state.main.userAvatar); + const { currentUser, isFetching, token } = useAppSelector( + (state) => state.auth, + ); + const { users, loading } = useAppSelector((state) => state.users); + + const [avatar, setAvatar] = useState(null); + + useEffect(() => { + currentUserAvatarCheck(); + }, []); + + useEffect(() => { + currentUserAvatarCheck(); + }, [currentUser?.id, users]); + + const currentUserAvatarCheck = () => { + if (currentUser?.id) { + const image = currentUser?.avatar; + setAvatar(image); + } + }; + + return ( + + {children} + + ); +} diff --git a/frontend/src/components/UserCard.tsx b/frontend/src/components/UserCard.tsx new file mode 100644 index 0000000..15fc7e5 --- /dev/null +++ b/frontend/src/components/UserCard.tsx @@ -0,0 +1,47 @@ +import { mdiCheckDecagram } from '@mdi/js'; +import { Field, Form, Formik } from 'formik'; +import { useAppSelector } from '../stores/hooks'; +import CardBox from './CardBox'; +import FormCheckRadio from './FormCheckRadio'; +import UserAvatarCurrentUser from './UserAvatarCurrentUser'; + +type Props = { + className?: string; +}; + +const UserCard = ({ className }: Props) => { + const userName = useAppSelector((state) => state.main.userName); + + return ( + +
    + +
    +
    + alert(JSON.stringify(values, null, 2))} + > +
    + + + +
    +
    +
    +

    + Howdy, {userName}! +

    +

    + Last login 12 mins ago from 127.0.0.1 +

    +
    Verified
    +
    +
    +
    + ); +}; + +export default UserCard; diff --git a/frontend/src/components/Users/CardUsers.tsx b/frontend/src/components/Users/CardUsers.tsx new file mode 100644 index 0000000..7d5e0d8 --- /dev/null +++ b/frontend/src/components/Users/CardUsers.tsx @@ -0,0 +1,209 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + users: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardUsers = ({ + users, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_USERS'); + + return ( +
    + {loading && } +
      + {!loading && + users.map((item, index) => ( +
    • +
      + + +

      {item.firstName}

      + + +
      + +
      +
      +
      +
      +
      + First Name +
      +
      +
      + {item.firstName} +
      +
      +
      + +
      +
      + Last Name +
      +
      +
      + {item.lastName} +
      +
      +
      + +
      +
      + Phone Number +
      +
      +
      + {item.phoneNumber} +
      +
      +
      + +
      +
      + E-Mail +
      +
      +
      {item.email}
      +
      +
      + +
      +
      + Disabled +
      +
      +
      + {dataFormatter.booleanFormatter(item.disabled)} +
      +
      +
      + +
      +
      + Avatar +
      +
      +
      + +
      +
      +
      + +
      +
      + App Role +
      +
      +
      + {dataFormatter.rolesOneListFormatter(item.app_role)} +
      +
      +
      + +
      +
      + Custom Permissions +
      +
      +
      + {dataFormatter + .permissionsManyListFormatter(item.custom_permissions) + .join(', ')} +
      +
      +
      + +
      +
      + Organizations +
      +
      +
      + {dataFormatter.organizationsOneListFormatter( + item.organizations, + )} +
      +
      +
      +
      +
    • + ))} + {!loading && users.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardUsers; diff --git a/frontend/src/components/Users/ListUsers.tsx b/frontend/src/components/Users/ListUsers.tsx new file mode 100644 index 0000000..069da77 --- /dev/null +++ b/frontend/src/components/Users/ListUsers.tsx @@ -0,0 +1,158 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + users: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListUsers = ({ + users, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_USERS'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + users.map((item) => ( +
    + +
    + + + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    First Name

    +

    {item.firstName}

    +
    + +
    +

    Last Name

    +

    {item.lastName}

    +
    + +
    +

    Phone Number

    +

    {item.phoneNumber}

    +
    + +
    +

    E-Mail

    +

    {item.email}

    +
    + +
    +

    Disabled

    +

    + {dataFormatter.booleanFormatter(item.disabled)} +

    +
    + +
    +

    Avatar

    + +
    + +
    +

    App Role

    +

    + {dataFormatter.rolesOneListFormatter(item.app_role)} +

    +
    + +
    +

    + Custom Permissions +

    +

    + {dataFormatter + .permissionsManyListFormatter(item.custom_permissions) + .join(', ')} +

    +
    + +
    +

    + Organizations +

    +

    + {dataFormatter.organizationsOneListFormatter( + item.organizations, + )} +

    +
    + + +
    +
    +
    + ))} + {!loading && users.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListUsers; diff --git a/frontend/src/components/Users/TableUsers.tsx b/frontend/src/components/Users/TableUsers.tsx new file mode 100644 index 0000000..ce6fbe7 --- /dev/null +++ b/frontend/src/components/Users/TableUsers.tsx @@ -0,0 +1,485 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/users/usersSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureUsersCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleUsers = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + users, + loading, + count, + notify: usersNotify, + refetch, + } = useAppSelector((state) => state.users); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (usersNotify.showNotification) { + notify(usersNotify.typeNotification, usersNotify.textNotification); + } + }, [usersNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `users`, currentUser).then((newCols) => + setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + delete data?.password; + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={users ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleUsers; diff --git a/frontend/src/components/Users/configureUsersCols.tsx b/frontend/src/components/Users/configureUsersCols.tsx new file mode 100644 index 0000000..6fc6b11 --- /dev/null +++ b/frontend/src/components/Users/configureUsersCols.tsx @@ -0,0 +1,203 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_USERS'); + + return [ + { + field: 'firstName', + headerName: 'First Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'lastName', + headerName: 'Last Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'phoneNumber', + headerName: 'Phone Number', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'email', + headerName: 'E-Mail', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'disabled', + headerName: 'Disabled', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'boolean', + }, + + { + field: 'avatar', + headerName: 'Avatar', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: false, + sortable: false, + renderCell: (params: GridValueGetterParams) => ( + + ), + }, + + { + field: 'app_role', + headerName: 'App Role', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('roles'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'custom_permissions', + headerName: 'Custom Permissions', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: false, + sortable: false, + type: 'singleSelect', + valueFormatter: ({ value }) => + dataFormatter.permissionsManyListFormatter(value).join(', '), + renderEditCell: (params) => ( + + ), + }, + + { + field: 'organizations', + headerName: 'Organizations', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/WebPageComponents/AboutUsComponent/designs/ImageLeft.tsx b/frontend/src/components/WebPageComponents/AboutUsComponent/designs/ImageLeft.tsx new file mode 100644 index 0000000..b02478c --- /dev/null +++ b/frontend/src/components/WebPageComponents/AboutUsComponent/designs/ImageLeft.tsx @@ -0,0 +1,54 @@ +import React, { useEffect, useState } from 'react'; +import { getMultiplePexelsImages } from '../../../../helpers/pexels'; +import { useAppSelector } from '../../../../stores/hooks'; +import BaseButton from '../../../BaseButton'; + +const ImageLeft = ({ + projectName, + mainText, + subTitle, + imageAbout, + buttonText, + corners, + textSecondary, +}) => { + return ( +
    +
    + + + {/* Text Section (Теперь справа) */} +
    +

    {mainText}

    +

    {subTitle}

    + +
    +
    +
    + ); +}; + +export default ImageLeft; diff --git a/frontend/src/components/WebPageComponents/AboutUsComponent/designs/ImageRight.tsx b/frontend/src/components/WebPageComponents/AboutUsComponent/designs/ImageRight.tsx new file mode 100644 index 0000000..3af73eb --- /dev/null +++ b/frontend/src/components/WebPageComponents/AboutUsComponent/designs/ImageRight.tsx @@ -0,0 +1,53 @@ +import React, { useEffect, useState } from 'react'; +import { getMultiplePexelsImages } from '../../../../helpers/pexels'; +import { useAppSelector } from '../../../../stores/hooks'; +import BaseButton from '../../../BaseButton'; + +const ImageRight = ({ + projectName, + mainText, + subTitle, + imageAbout, + buttonText, + corners, + textSecondary, +}) => { + return ( +
    +
    +
    +

    {mainText}

    +

    {subTitle}

    + +
    + + +
    +
    + ); +}; + +export default ImageRight; diff --git a/frontend/src/components/WebPageComponents/AboutUsComponent/index.tsx b/frontend/src/components/WebPageComponents/AboutUsComponent/index.tsx new file mode 100644 index 0000000..a7e7b33 --- /dev/null +++ b/frontend/src/components/WebPageComponents/AboutUsComponent/index.tsx @@ -0,0 +1,85 @@ +import React, { useEffect, useState } from 'react'; +import { getMultiplePexelsImages } from '../../../helpers/pexels'; +import { useAppSelector } from '../../../stores/hooks'; +import { AboutUsDesigns } from '../designs'; +import ImageLeft from './designs/ImageLeft'; +import ImageRight from './designs/ImageRight'; + +const AboutUsComponent = ({ + projectName, + image, + mainText, + subTitle, + design, + buttonText, +}) => { + const textSecondary = useAppSelector((state) => state.style.textSecondary); + const corners = useAppSelector((state) => state.style.corners); + const [imageAbout, setImages] = useState([]); + const pexelsQueriesWebSite = image; + + useEffect(() => { + const fetchImages = async () => { + try { + const images = await getMultiplePexelsImages(pexelsQueriesWebSite); + const formattedImages = (images || []).map((image) => ({ + src: image?.src || undefined, + photographer: image?.photographer || undefined, + photographer_url: image?.photographer_url || undefined, + })); + setImages(formattedImages); + } catch (error) { + console.error('Error fetching images:', error); + } + }; + + fetchImages(); + }, [pexelsQueriesWebSite]); + + const renderComponent = () => { + switch (design) { + case AboutUsDesigns.IMAGE_LEFT: + return ( + + ); + + case AboutUsDesigns.IMAGE_RIGHT: + return ( + + ); + + default: + return ( + + ); + } + }; + + return renderComponent(); +}; + +export default AboutUsComponent; diff --git a/frontend/src/components/WebPageComponents/ContactFormComponent/designs/FormWithImage.tsx b/frontend/src/components/WebPageComponents/ContactFormComponent/designs/FormWithImage.tsx new file mode 100644 index 0000000..1a94a31 --- /dev/null +++ b/frontend/src/components/WebPageComponents/ContactFormComponent/designs/FormWithImage.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { Formik, Form, Field } from 'formik'; +import BaseButton from '../../../BaseButton'; +import FormField from '../../../../components/FormField'; +import { useAppSelector, useAppDispatch } from '../../../../stores/hooks'; + +const FormWithImage = ({ mainText, subTitle, onSubmit, imageContactForm }) => { + const corners = useAppSelector((state) => state.style.corners); + const textSecondary = useAppSelector((state) => state.style.textSecondary); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + return ( +
    +
    +
    + + +
    +
    +

    {mainText}

    +

    {subTitle}

    + + + {({ isSubmitting }) => ( +
    + + + + + + + + + + + + + + + )} +
    +
    +
    +
    +
    +
    + ); +}; +export default FormWithImage; diff --git a/frontend/src/components/WebPageComponents/ContactFormComponent/designs/HighlightedForm.tsx b/frontend/src/components/WebPageComponents/ContactFormComponent/designs/HighlightedForm.tsx new file mode 100644 index 0000000..341371e --- /dev/null +++ b/frontend/src/components/WebPageComponents/ContactFormComponent/designs/HighlightedForm.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { Formik, Form, Field } from 'formik'; +import BaseButton from '../../../BaseButton'; +import BaseIcon from '../../../BaseIcon'; +import * as icon from '@mdi/js'; +import { ContactFormDesigns } from '../../designs'; +import FormField from '../../../../components/FormField'; +import { useAppSelector, useAppDispatch } from '../../../../stores/hooks'; +const HighlightedForm = ({ mainText, subTitle, onSubmit, design }) => { + const corners = useAppSelector((state) => state.style.corners); + const textSecondary = useAppSelector((state) => state.style.textSecondary); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + + return ( +
    +
    +
    +
    +

    {mainText}

    +

    {subTitle}

    +
    +

    + + +1XXX XXXX XXX +

    +

    + + + krystsinavaida@gmail.com + +

    +
    +
    +
    + + {({ isSubmitting }) => ( +
    + + + + + + + + + + + + + + + )} +
    +
    +
    +
    +
    + ); +}; + +export default HighlightedForm; diff --git a/frontend/src/components/WebPageComponents/ContactFormComponent/designs/SimpleAndCleanForm.tsx b/frontend/src/components/WebPageComponents/ContactFormComponent/designs/SimpleAndCleanForm.tsx new file mode 100644 index 0000000..59bd559 --- /dev/null +++ b/frontend/src/components/WebPageComponents/ContactFormComponent/designs/SimpleAndCleanForm.tsx @@ -0,0 +1,166 @@ +import React from 'react'; +import { Formik, Form, Field } from 'formik'; +import BaseButton from '../../../BaseButton'; +import FormField from '../../../../components/FormField'; +import BaseIcon from '../../../BaseIcon'; +import * as icon from '@mdi/js'; +import { ContactFormDesigns } from '../../designs'; +import { useAppSelector, useAppDispatch } from '../../../../stores/hooks'; +const SimpleAndCleanForm = ({ mainText, subTitle, onSubmit, design }) => { + const corners = useAppSelector((state) => state.style.corners); + const textSecondary = useAppSelector((state) => state.style.textSecondary); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + return ( +
    +
    +
    +
    +

    {mainText}

    +

    + {subTitle} +

    +
    +

    + + +1XXX XXXX XXX +

    +

    + + einkap1@gmail.com +

    +
    +
    +
    + + {({ isSubmitting }) => ( +
    + + + + + + + + + + + + + + + )} +
    +
    +
    +
    +
    + ); +}; + +export default SimpleAndCleanForm; diff --git a/frontend/src/components/WebPageComponents/ContactFormComponent/index.tsx b/frontend/src/components/WebPageComponents/ContactFormComponent/index.tsx new file mode 100644 index 0000000..c622cec --- /dev/null +++ b/frontend/src/components/WebPageComponents/ContactFormComponent/index.tsx @@ -0,0 +1,89 @@ +// src/components/WebPageComponents/ContactFormSection.tsx +import React, { useEffect, useState } from 'react'; +import { getMultiplePexelsImages } from '../../../helpers/pexels'; +import { useAppSelector } from '../../../stores/hooks'; +import { ContactFormDesigns } from '../designs'; +import SimpleAndCleanForm from './designs/SimpleAndCleanForm'; +import HighlightedForm from './designs/HighlightedForm'; +import FormWithImage from './designs/FormWithImage'; +import { ToastContainer, toast } from 'react-toastify'; +import axios from 'axios'; +import 'react-toastify/dist/ReactToastify.css'; + +export default function ContactFormSection({ + projectName, + withBg = 0, + mainText, + subTitle, + design, + image, +}) { + const [imageContactForm, setImages] = useState([]); + const pexelsQueriesWebSite = image; + const textSecondary = useAppSelector((state) => state.style.textSecondary); + + useEffect(() => { + const fetchImages = async () => { + if (design === ContactFormDesigns.WITH_IMAGE) { + try { + const images = await getMultiplePexelsImages(pexelsQueriesWebSite); + const formattedImages = (images || []).map((image) => ({ + src: image?.src || undefined, + photographer: image?.photographer || undefined, + photographer_url: image?.photographer_url || undefined, + })); + setImages(formattedImages); + } catch (error) { + console.error('Error fetching images:', error); + } + } + }; + + fetchImages(); + }, [pexelsQueriesWebSite, design]); + + const handleSubmit = async (values, { setSubmitting, resetForm }) => { + try { + await axios.post('/contact-form/send', values); + toast.success('Your message has been sent successfully!'); + resetForm(); + } catch (error) { + toast.error('There was an error sending your message'); + } finally { + setSubmitting(false); + } + }; + + let DesignComponent; + + switch (design) { + case ContactFormDesigns.SIMPLE_CLEAN: + case ContactFormDesigns.SIMPLE_CLEAN_DIVERSITY: + DesignComponent = SimpleAndCleanForm; + break; + case ContactFormDesigns.HIGHLIGHTED: + case ContactFormDesigns.HIGHLIGHTED_DIVERSITY: + DesignComponent = HighlightedForm; + break; + case ContactFormDesigns.WITH_IMAGE: + DesignComponent = FormWithImage; + break; + default: + DesignComponent = SimpleAndCleanForm; + break; + } + + return ( +
    + + +
    + ); +} diff --git a/frontend/src/components/WebPageComponents/FaqComponent/designs/FAQAccordion.tsx b/frontend/src/components/WebPageComponents/FaqComponent/designs/FAQAccordion.tsx new file mode 100644 index 0000000..00507c7 --- /dev/null +++ b/frontend/src/components/WebPageComponents/FaqComponent/designs/FAQAccordion.tsx @@ -0,0 +1,78 @@ +// components/FAQ/FAQAccordion.js +import React, { useState } from 'react'; + +const FAQAccordion = ({ + faqs, + projectName, + textSecondary, + corners, + borders, + mainText, +}) => { + const [openIndex, setOpenIndex] = useState(null); + + const toggleFAQ = (index) => { + setOpenIndex(openIndex === index ? null : index); + }; + + return ( +
    +
    +
    +
    +

    + {mainText} +

    +
    + +
    + {faqs.map((faq, index) => ( +
    +
    toggleFAQ(index)} + > +

    + {faq.question.replace(/\${projectName}/g, projectName)} +

    + + {openIndex === index ? '−' : '+'} + +
    + +
    +
    +

    {faq.answer.replace(/\${projectName}/g, projectName)}

    +
    +
    +
    + ))} +
    +
    +
    +
    + ); +}; + +export default FAQAccordion; diff --git a/frontend/src/components/WebPageComponents/FaqComponent/designs/FAQSplitList.tsx b/frontend/src/components/WebPageComponents/FaqComponent/designs/FAQSplitList.tsx new file mode 100644 index 0000000..cd41109 --- /dev/null +++ b/frontend/src/components/WebPageComponents/FaqComponent/designs/FAQSplitList.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { FaqDesigns } from '../../designs'; + +const FAQSplitList = ({ + faqs, + projectName, + textSecondary, + borders, + mainText, + websiteSectionStyle, + design, +}) => ( +
    +
    +
    +
    +

    + {mainText} +

    +
    + +
    + {faqs.map((faq, index) => ( +
    +
    +

    + {faq.question.replace(/\${projectName}/g, projectName)} +

    +
    + +
    +

    + {faq.answer.replace(/\${projectName}/g, projectName)} +

    +
    +
    + ))} +
    +
    +
    +
    +); + +export default FAQSplitList; diff --git a/frontend/src/components/WebPageComponents/FaqComponent/designs/FAQTwoColumn.tsx b/frontend/src/components/WebPageComponents/FaqComponent/designs/FAQTwoColumn.tsx new file mode 100644 index 0000000..09f5e8d --- /dev/null +++ b/frontend/src/components/WebPageComponents/FaqComponent/designs/FAQTwoColumn.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +const FAQTwoColumn = ({ faqs, projectName, textSecondary, mainText }) => ( +
    +
    +
    +
    +

    + FAQ +

    +

    {mainText}

    +
    +
    + {faqs.map((faq, index) => ( +
    +

    + {faq.question.replace(/\${projectName}/g, projectName)} +

    +

    + {faq.answer.replace(/\${projectName}/g, projectName)} +

    +
    + ))} +
    +
    +
    +
    +); + +export default FAQTwoColumn; diff --git a/frontend/src/components/WebPageComponents/FaqComponent/index.tsx b/frontend/src/components/WebPageComponents/FaqComponent/index.tsx new file mode 100644 index 0000000..edc77fe --- /dev/null +++ b/frontend/src/components/WebPageComponents/FaqComponent/index.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { useAppSelector } from '../../../stores/hooks'; +import FAQAccordion from './designs/FAQAccordion'; +import FAQSplitList from './designs/FAQSplitList'; +import FAQTwoColumn from './designs/FAQTwoColumn'; +import { FaqDesigns } from '../designs'; + +const FaqSection = ({ projectName, mainText, faqs, design }) => { + const textSecondary = useAppSelector((state) => state.style.textSecondary); + const borders = useAppSelector((state) => state.style.borders); + const corners = useAppSelector((state) => state.style.corners); + const websiteSectionStyle = useAppSelector( + (state) => state.style.websiteSectionStyle, + ); + + let designComponent; + + switch (design) { + case FaqDesigns.ACCORDION: + designComponent = ( + + ); + break; + + case FaqDesigns.SPLIT_LIST: + case FaqDesigns.SPLIT_LIST_DIVERSITY: + designComponent = ( + + ); + break; + + case FaqDesigns.TWO_COLUMN: + designComponent = ( + + ); + break; + + default: + designComponent = ( + + ); + break; + } + + return
    {designComponent}
    ; +}; + +export default FaqSection; diff --git a/frontend/src/components/WebPageComponents/FeaturesComponent/designs/CardsGridWithIcons.tsx b/frontend/src/components/WebPageComponents/FeaturesComponent/designs/CardsGridWithIcons.tsx new file mode 100644 index 0000000..a70edfc --- /dev/null +++ b/frontend/src/components/WebPageComponents/FeaturesComponent/designs/CardsGridWithIcons.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import BaseIcon from '../../../BaseIcon'; +import * as icon from '@mdi/js'; +import { FeaturesDesigns } from '../../designs'; + +const CardsGridWithIcons = ({ + features, + projectName, + design, + iconsColor, + corners, + mainText, + subTitle, + websiteSectionStyle, + textSecondary, + borders, + shadow, +}) => ( +
    +
    +
    +

    {mainText}

    +

    + {subTitle} +

    +
    + +
    + {features.map((feature: any, index) => ( +
    +
    +

    + {feature.name.replace(/\${projectName}/g, projectName)} +

    +

    + {feature.description.replace(/\${projectName}/g, projectName)} +

    +
    +
    + +
    +
    + ))} +
    +
    +
    +); + +export default CardsGridWithIcons; diff --git a/frontend/src/components/WebPageComponents/FeaturesComponent/designs/IconsTop.tsx b/frontend/src/components/WebPageComponents/FeaturesComponent/designs/IconsTop.tsx new file mode 100644 index 0000000..c918176 --- /dev/null +++ b/frontend/src/components/WebPageComponents/FeaturesComponent/designs/IconsTop.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import BaseIcon from '../../../BaseIcon'; +import * as icon from '@mdi/js'; + +const IconsTop = ({ + features, + projectName, + withBg, + iconsColor, + mainText, + subTitle, + textSecondary, + websiteSectionStyle, +}) => ( +
    +
    +
    +

    {mainText}

    +

    {subTitle}

    +
    + +
    + {features.map((feature: any, index) => ( +
    +
    + +
    +
    +

    + {feature.name.replace(/\${projectName}/g, projectName)} +

    +

    + {feature.description.replace(/\${projectName}/g, projectName)} +

    +
    +
    + ))} +
    +
    +
    +); + +export default IconsTop; diff --git a/frontend/src/components/WebPageComponents/FeaturesComponent/designs/IconsWithImage.tsx b/frontend/src/components/WebPageComponents/FeaturesComponent/designs/IconsWithImage.tsx new file mode 100644 index 0000000..093f135 --- /dev/null +++ b/frontend/src/components/WebPageComponents/FeaturesComponent/designs/IconsWithImage.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import BaseIcon from '../../../BaseIcon'; +import * as icon from '@mdi/js'; + +const IconsWithImage = ({ + features, + projectName, + withBg, + iconsColor, + mainText, + subTitle, + image, + textSecondary, + websiteSectionStyle, + corners, +}) => { + const displayedFeatures = features.slice(0, 4); + + return ( +
    +
    +
    + + +
    +
    + {displayedFeatures.map((feature, index) => ( +
    +
    + +
    + + {/* Текст */} +
    +

    + {feature.name.replace(/\${projectName}/g, projectName)} +

    +

    + {feature.description.replace( + /\${projectName}/g, + projectName, + )} +

    +
    +
    + ))} +
    +
    +
    +
    +
    + ); +}; + +export default IconsWithImage; diff --git a/frontend/src/components/WebPageComponents/FeaturesComponent/designs/LargeNumbers.tsx b/frontend/src/components/WebPageComponents/FeaturesComponent/designs/LargeNumbers.tsx new file mode 100644 index 0000000..122ea15 --- /dev/null +++ b/frontend/src/components/WebPageComponents/FeaturesComponent/designs/LargeNumbers.tsx @@ -0,0 +1,54 @@ +import React from 'react'; + +const LargeNumbers = ({ + features, + projectName, + withBg, + iconsColor, + mainText, + subTitle, + textSecondary, + websiteSectionStyle, +}) => ( +
    +
    +
    +

    {mainText}

    +

    {subTitle}

    +
    + +
    + {features.map((feature: any, index) => ( +
    +
    +
    + + {index + 1} + +
    +
    +
    +

    + {feature.name.replace(/\${projectName}/g, projectName)} +

    +

    + {feature.description.replace(/\${projectName}/g, projectName)} +

    +
    +
    + ))} +
    +
    +
    +); + +export default LargeNumbers; diff --git a/frontend/src/components/WebPageComponents/FeaturesComponent/index.tsx b/frontend/src/components/WebPageComponents/FeaturesComponent/index.tsx new file mode 100644 index 0000000..b0ce691 --- /dev/null +++ b/frontend/src/components/WebPageComponents/FeaturesComponent/index.tsx @@ -0,0 +1,136 @@ +import React, { useEffect, useState } from 'react'; +import { getMultiplePexelsImages } from '../../../helpers/pexels'; +import { useAppSelector } from '../../../stores/hooks'; +import IconsTop from './designs/IconsTop'; +import LargeNumbers from './designs/LargeNumbers'; +import CardsGridWithIcons from './designs/CardsGridWithIcons'; +import { FeaturesDesigns } from '../designs'; +import IconsWithImage from './designs/IconsWithImage'; + +export default function FeaturesSection({ + projectName, + withBg = 0, + features, + mainText, + subTitle, + design, + image, +}) { + const textColor = useAppSelector((state) => state.style.linkColor); + const iconsColor = useAppSelector((state) => state.style.iconsColor); + const corners = useAppSelector((state) => state.style.corners); + const shadow = useAppSelector((state) => state.style.shadow); + const websiteSectionStyle = useAppSelector( + (state) => state.style.websiteSectionStyle, + ); + const borders = useAppSelector((state) => state.style.borders); + const textSecondary = useAppSelector((state) => state.style.textSecondary); + const pexelsQueriesWebSite = image; + const [imageFeatures, setImages] = useState([]); + useEffect(() => { + const fetchImages = async () => { + if (design === FeaturesDesigns.ICONS_WITH_IMAGE) { + try { + const images = await getMultiplePexelsImages(pexelsQueriesWebSite); + const formattedImages = (images || []).map((image) => ({ + src: image?.src || undefined, + photographer: image?.photographer || undefined, + photographer_url: image?.photographer_url || undefined, + })); + setImages(formattedImages); + } catch (error) { + console.error('Error fetching images:', error); + } + } + }; + + fetchImages(); + }, [pexelsQueriesWebSite, design]); + + let designComponent; + + switch (design) { + case FeaturesDesigns.ICONS_TOP: + designComponent = ( + + ); + break; + + case FeaturesDesigns.LARGE_NUMBERS: + designComponent = ( + + ); + break; + + case FeaturesDesigns.CARDS_GRID_WITH_ICONS: + case FeaturesDesigns.CARDS_GRID_WITH_ICONS_DIVERSITY: + designComponent = ( + + ); + break; + + case FeaturesDesigns.ICONS_WITH_IMAGE: + designComponent = ( + + ); + break; + + default: + designComponent = ( + + ); + break; + } + + return
    {designComponent}
    ; +} diff --git a/frontend/src/components/WebPageComponents/Footer.tsx b/frontend/src/components/WebPageComponents/Footer.tsx new file mode 100644 index 0000000..7da214b --- /dev/null +++ b/frontend/src/components/WebPageComponents/Footer.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { FooterStyle } from './designs'; +import { FooterDesigns } from './designs'; +import Link from 'next/link'; +import { humanize } from '../../helpers/humanize'; +import { useRouter } from 'next/router'; +import { useAppSelector } from '../../stores/hooks'; +import { webPagesNavBar } from '../../menuNavBar'; + +interface WebSiteFooterProps { + projectName: string; +} + +export default function WebSiteFooter({ projectName }: WebSiteFooterProps) { + const currentYear = new Date().getFullYear(); + const router = useRouter(); + const borders = useAppSelector((state) => state.style.borders); + const websiteHeder = useAppSelector((state) => state.style.websiteHeder); + + const style = FooterStyle.WITH_PROJECT_NAME; + + const design = FooterDesigns.DESIGN_DIVERSITY; + + return ( +
    +
    +
    +

    + © {currentYear} Flatlogic. All rights reserved +

    + {style ? ( +

    {projectName}

    + ) : ( +
    + {webPagesNavBar.map((page, index) => { + const isRootRoute = router.pathname === '/'; + const isActive = isRootRoute + ? index === 0 + : router.pathname.includes(page.href); + + return ( + + {humanize(page.label)} + + ); + })} +
    + )} +
    + + Terms of Use + + + Privacy Policy + +
    +
    +
    +
    + ); +} diff --git a/frontend/src/components/WebPageComponents/GalleryPortfolioComponent/designs/HorizontalGalleryWithButtons.tsx b/frontend/src/components/WebPageComponents/GalleryPortfolioComponent/designs/HorizontalGalleryWithButtons.tsx new file mode 100644 index 0000000..732041c --- /dev/null +++ b/frontend/src/components/WebPageComponents/GalleryPortfolioComponent/designs/HorizontalGalleryWithButtons.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import BaseButton from '../../../BaseButton'; + +const HorizontalGallery = ({ + images, + currentIndex, + prevSlide, + nextSlide, + getPrevIndex, + getNextIndex, + mainText, + corners, +}) => { + return ( + + ); +}; + +export default HorizontalGallery; diff --git a/frontend/src/components/WebPageComponents/GalleryPortfolioComponent/designs/OverlappingCentralImage.tsx b/frontend/src/components/WebPageComponents/GalleryPortfolioComponent/designs/OverlappingCentralImage.tsx new file mode 100644 index 0000000..d9e0d95 --- /dev/null +++ b/frontend/src/components/WebPageComponents/GalleryPortfolioComponent/designs/OverlappingCentralImage.tsx @@ -0,0 +1,128 @@ +import React from 'react'; + +const OverlappingGallery = ({ + images, + currentIndex, + prevSlide, + nextSlide, + getPrevIndex, + getNextIndex, + mainText, + corners, +}) => { + const applyLeftCorner = (corners) => { + if (corners === 'rounded-full') { + return 'rounded-l-2xl'; + } + if (corners.startsWith('rounded-lg')) { + return 'rounded-l-lg'; + } + if (corners.startsWith('rounded-md')) { + return 'rounded-l-md'; + } + if (corners.startsWith('rounded-sm')) { + return 'rounded-l-sm'; + } + if (corners.startsWith('rounded-xl')) { + return 'rounded-l-xl'; + } + if (corners.startsWith('rounded')) { + return 'rounded-l'; + } + return corners; + }; + + const applyRightCorner = (corners) => { + if (corners === 'rounded-full') { + return 'rounded-r-2xl'; + } + if (corners.startsWith('rounded')) { + return corners.replace('rounded', 'rounded-r'); + } + return corners.replace('rounded-', 'rounded-r-'); + }; + const buttonCornersLeft = applyLeftCorner(corners); + const buttonCornersRight = applyRightCorner(corners); + return ( + + ); +}; + +export default OverlappingGallery; diff --git a/frontend/src/components/WebPageComponents/GalleryPortfolioComponent/index.tsx b/frontend/src/components/WebPageComponents/GalleryPortfolioComponent/index.tsx new file mode 100644 index 0000000..bcd7856 --- /dev/null +++ b/frontend/src/components/WebPageComponents/GalleryPortfolioComponent/index.tsx @@ -0,0 +1,75 @@ +import React, { useState } from 'react'; +import HorizontalGallery from './designs/HorizontalGalleryWithButtons'; +import OverlappingGallery from './designs/OverlappingCentralImage'; +import { GalleryPortfolioDesigns } from '../designs'; +import { useAppSelector } from '../../../stores/hooks'; + +export default function GalleryPortfolioSection({ + projectName, + images, + mainText, + design, +}) { + const [currentIndex, setCurrentIndex] = useState(0); + const corners = useAppSelector((state) => state.style.corners); + const prevSlide = () => { + setCurrentIndex((prevIndex) => + prevIndex === 0 ? images.length - 1 : prevIndex - 1, + ); + }; + + const nextSlide = () => { + setCurrentIndex((prevIndex) => + prevIndex === images.length - 1 ? 0 : prevIndex + 1, + ); + }; + + const getPrevIndex = () => + currentIndex === 0 ? images.length - 1 : currentIndex - 1; + const getNextIndex = () => + currentIndex === images.length - 1 ? 0 : currentIndex + 1; + + switch (design) { + case GalleryPortfolioDesigns.HORIZONTAL_WITH_BUTTONS: + return ( + + ); + + case GalleryPortfolioDesigns.OVERLAPPING_CENTRAL_IMAGE: + return ( + + ); + + default: + return ( + + ); + } +} diff --git a/frontend/src/components/WebPageComponents/Header.tsx b/frontend/src/components/WebPageComponents/Header.tsx new file mode 100644 index 0000000..1d56d49 --- /dev/null +++ b/frontend/src/components/WebPageComponents/Header.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import Link from 'next/link'; +import BaseButton from '../BaseButton'; +import { humanize } from '../../helpers/humanize'; +import { useRouter } from 'next/router'; +import { useAppSelector } from '../../stores/hooks'; +import { HeaderStyle } from './designs'; +import { HeaderDesigns } from './designs'; +import { webPagesNavBar } from '../../menuNavBar'; + +interface WebSiteHeaderProps { + projectName: string; +} + +export default function WebSiteHeader({ projectName }: WebSiteHeaderProps) { + const router = useRouter(); + const websiteHeder = useAppSelector((state) => state.style.websiteHeder); + const borders = useAppSelector((state) => state.style.borders); + + const style = HeaderStyle.PAGES_RIGHT; + + const design = HeaderDesigns.DESIGN_DIVERSITY; + return ( +
    +
    +
    +
    +
    +
    + {projectName} +
    +
    + {webPagesNavBar.map((page, index) => { + const isRootRoute = router.pathname === '/'; + const isActive = isRootRoute + ? index === 0 + : router.pathname.includes(page.href); + + return ( + + {humanize(page.label)} + + ); + })} +
    +
    +
    + +
    +
    +
    +
    +
    + ); +} diff --git a/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageBg.tsx b/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageBg.tsx new file mode 100644 index 0000000..53e43ef --- /dev/null +++ b/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageBg.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import BaseButton from '../../../BaseButton'; + +const HeroImageBg = ({ + mainText, + subTitle, + buttonText, + imageHero, + textSecondary, +}) => ( +
    +
    +
    +

    + {mainText} +

    +

    {subTitle}

    + +
    + +
    +); + +export default HeroImageBg; diff --git a/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageLeft.tsx b/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageLeft.tsx new file mode 100644 index 0000000..173e82d --- /dev/null +++ b/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageLeft.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import BaseButton from '../../../BaseButton'; + +const HeroImageLeft = ({ + mainText, + subTitle, + buttonText, + imageHero, + textSecondary, +}) => ( +
    +
    +
    +

    + {mainText} +

    +
    +

    {subTitle}

    +
    + +
    +
    + +
    +); + +export default HeroImageLeft; diff --git a/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageRight.tsx b/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageRight.tsx new file mode 100644 index 0000000..95184bb --- /dev/null +++ b/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageRight.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import BaseButton from '../../../BaseButton'; + +const HeroImageRight = ({ + mainText, + subTitle, + buttonText, + imageHero, + textSecondary, +}) => ( +
    + +
    +
    +

    + {mainText} +

    +
    +

    {subTitle}

    +
    + +
    +
    +
    +); + +export default HeroImageRight; diff --git a/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroTextCenter.tsx b/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroTextCenter.tsx new file mode 100644 index 0000000..940b926 --- /dev/null +++ b/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroTextCenter.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import BaseButton from '../../../BaseButton'; + +const HeroTextCenter = ({ mainText, subTitle, buttonText, textSecondary }) => ( +
    +
    +
    +

    + {mainText} +

    +

    {subTitle}

    + +
    +
    +); + +export default HeroTextCenter; diff --git a/frontend/src/components/WebPageComponents/HeroComponent/index.tsx b/frontend/src/components/WebPageComponents/HeroComponent/index.tsx new file mode 100644 index 0000000..8aac818 --- /dev/null +++ b/frontend/src/components/WebPageComponents/HeroComponent/index.tsx @@ -0,0 +1,73 @@ +// src/components/WebPageComponents/HeroSection.tsx +import React, { useEffect, useState } from 'react'; +import { getMultiplePexelsImages } from '../../../helpers/pexels'; +import { useAppSelector } from '../../../stores/hooks'; +import { HeroDesigns } from '../designs'; +import HeroImageLeft from './designs/HeroImageLeft'; +import HeroImageRight from './designs/HeroImageRight'; +import HeroImageBg from './designs/HeroImageBg'; +import HeroTextCenter from './designs/HeroTextCenter'; + +export default function HeroSection({ + projectName, + image, + mainText, + subTitle, + design, + buttonText, +}) { + const textSecondary = useAppSelector((state) => state.style.textSecondary); + + const [imageHero, setImages] = useState([]); + const pexelsQueriesWebSite = image; + + useEffect(() => { + const fetchImages = async () => { + if (design !== HeroDesigns.TEXT_CENTER) { + try { + const images = await getMultiplePexelsImages(pexelsQueriesWebSite); + const formattedImages = (images || []).map((image) => ({ + src: image?.src || undefined, + photographer: image?.photographer || undefined, + photographer_url: image?.photographer_url || undefined, + })); + setImages(formattedImages); + } catch (error) { + console.error('Error fetching images:', error); + } + } + }; + + fetchImages(); + }, [pexelsQueriesWebSite, design]); + + let DesignComponent; + + switch (design) { + case HeroDesigns.IMAGE_LEFT: + DesignComponent = HeroImageLeft; + break; + case HeroDesigns.IMAGE_RIGHT: + DesignComponent = HeroImageRight; + break; + case HeroDesigns.IMAGE_BG: + DesignComponent = HeroImageBg; + break; + case HeroDesigns.TEXT_CENTER: + DesignComponent = HeroTextCenter; + break; + default: + DesignComponent = HeroImageRight; + break; + } + + return ( + + ); +} diff --git a/frontend/src/components/WebPageComponents/PricingComponent/index.tsx b/frontend/src/components/WebPageComponents/PricingComponent/index.tsx new file mode 100644 index 0000000..f88c13a --- /dev/null +++ b/frontend/src/components/WebPageComponents/PricingComponent/index.tsx @@ -0,0 +1,242 @@ +import React, { useEffect, useState } from 'react'; +import Link from 'next/link'; +import { useAppSelector, useAppDispatch } from '../../../stores/hooks'; +import BaseButton from '../../BaseButton'; +import BaseIcon from '../../BaseIcon'; +import * as icon from '@mdi/js'; + +export default function PricingSection({ + projectName, + withBg = 0, + features, + description, +}) { + const textColor = useAppSelector((state) => state.style.linkColor); + const iconsColor = useAppSelector((state) => state.style.iconsColor); + const cardsStyle = useAppSelector((state) => state.style.cardsStyle); + const borders = useAppSelector((state) => state.style.borders); + const shadow = useAppSelector((state) => state.style.shadow); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const websiteSectionStyle = useAppSelector( + (state) => state.style.websiteSectionStyle, + ); + const navBarItemLabelActiveColorStyle = useAppSelector( + (state) => state.style.navBarItemLabelActiveColorStyle, + ); + const asideMenuItemActiveStyle = useAppSelector( + (state) => state.style.asideMenuItemActiveStyle, + ); + const corners = useAppSelector((state) => state.style.corners); + const textSecondary = useAppSelector((state) => state.style.textSecondary); + const activeLinkColor = useAppSelector( + (state) => state.style.activeLinkColor, + ); + + return ( +
    +
    +
    +

    + Choose the plan that’s right for you +

    +
    Pricing Table
    +
    +
    +
    +
    +

    Standard

    +

    + {description['standard'] + ? description['standard'] + : 'For solo designer'} +

    +
    +
    +
    + +

    29

    +
    +

    + per person, per month +

    +
    + +
      + {features.standard.features.map((feature, index) => ( +
    • + + {feature} +
    • + ))} +
    +

    Limited to

    +
      + {features.standard.limited_features.map((feature, index) => ( +
    • + + {feature} +
    • + ))} +
    +
    + +
    + +
    +
    +

    + Premium{' '} + + MOST POPULAR + +

    +

    + {description['premium'] + ? description['premium'] + : ' For small startup and agency'} +

    +
    +
    +
    + +

    49

    +
    +

    + per person, per month +

    +
    + +
      + {features.premium.features.map((feature, index) => ( +
    • + + {feature} +
    • + ))} +
    +

    Also Include

    + +
      + {features.premium.also_included.map((feature, index) => ( +
    • + + {feature} +
    • + ))} +
    +
    + +
    +
    +
    +

    + Business +

    +

    + {description['business'] + ? description['business'] + : ' Custom solution'} +

    +
    +
    +
    + +

    99

    +
    +

    + per person, per month +

    +
    + +
      + {features.business.features.map((feature, index) => ( +
    • + + {feature} +
    • + ))} +
    +
    + +
    +
    +
    +
    + ); +} diff --git a/frontend/src/components/WebPageComponents/TestimonialsComponent/designs/HorizontalCarousel.tsx b/frontend/src/components/WebPageComponents/TestimonialsComponent/designs/HorizontalCarousel.tsx new file mode 100644 index 0000000..7ae2d38 --- /dev/null +++ b/frontend/src/components/WebPageComponents/TestimonialsComponent/designs/HorizontalCarousel.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import BaseButton from '../../../BaseButton'; +import { TestimonialsDesigns } from '../../designs'; + +const HorizontalCarousel = ({ + projectName, + testimonials, + currentIndex, + handlePrev, + handleNext, + design, + websiteSectionStyle, + textSecondary, + iconsColor, +}) => { + return ( +
    +
    + + +
    +

    + “ +

    +

    + {testimonials[currentIndex].text.replace( + /\${projectName}/g, + projectName, + )} +

    +

    + {testimonials[currentIndex].company} +

    +

    + - {testimonials[currentIndex].user_name} +

    +
    + + +
    +
    + ); +}; + +export default HorizontalCarousel; diff --git a/frontend/src/components/WebPageComponents/TestimonialsComponent/designs/MultiCardDisplay.tsx b/frontend/src/components/WebPageComponents/TestimonialsComponent/designs/MultiCardDisplay.tsx new file mode 100644 index 0000000..14c9231 --- /dev/null +++ b/frontend/src/components/WebPageComponents/TestimonialsComponent/designs/MultiCardDisplay.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import BaseButton from '../../../BaseButton'; + +const MultiCardDisplay = ({ + projectName, + testimonials, + getVisibleTestimonials, + handlePrev, + handleNext, + textSecondary, + corners, + mainText, + shadow, +}) => { + return ( +
    +
    +

    + {mainText} +

    +
    + + +
    +
    + +
    + {getVisibleTestimonials().map((testimonial, index) => ( +
    +
    +

    + Efficient Collaborating +

    +

    + {testimonial.text.replace(/\${projectName}/g, projectName)} +

    +
    +
    +

    + {testimonial.company} +

    +

    + - {testimonial.user_name} +

    +
    +
    + ))} +
    +
    + ); +}; + +export default MultiCardDisplay; diff --git a/frontend/src/components/WebPageComponents/TestimonialsComponent/index.tsx b/frontend/src/components/WebPageComponents/TestimonialsComponent/index.tsx new file mode 100644 index 0000000..3b5c5b2 --- /dev/null +++ b/frontend/src/components/WebPageComponents/TestimonialsComponent/index.tsx @@ -0,0 +1,112 @@ +// TestimonialsSection.js +import React, { useEffect, useState } from 'react'; +import HorizontalCarousel from './designs/HorizontalCarousel'; +import MultiCardDisplay from './designs/MultiCardDisplay'; +import { useAppSelector } from '../../../stores/hooks'; +import { TestimonialsDesigns } from '../designs'; + +export default function TestimonialsSection({ + projectName, + mainText, + testimonials, + design, +}) { + const textSecondary = useAppSelector((state) => state.style.textSecondary); + const corners = useAppSelector((state) => state.style.corners); + const websiteSectionStyle = useAppSelector( + (state) => state.style.websiteSectionStyle, + ); + const iconsColor = useAppSelector((state) => state.style.iconsColor); + const shadow = useAppSelector((state) => state.style.shadow); + const [currentIndex, setCurrentIndex] = useState(0); + + const handlePrev = () => { + setCurrentIndex((prevIndex) => + prevIndex === 0 ? testimonials.length - 1 : prevIndex - 1, + ); + }; + + const handleNext = () => { + setCurrentIndex((prevIndex) => + prevIndex === testimonials.length - 1 ? 0 : prevIndex + 1, + ); + }; + + const getVisibleTestimonials = () => { + const visibleTestimonials = []; + for (let i = 0; i < 3; i++) { + visibleTestimonials.push( + testimonials[(currentIndex + i) % testimonials.length], + ); + } + return visibleTestimonials; + }; + + const renderDesign = () => { + switch (design) { + case TestimonialsDesigns.HORIZONTAL_CAROUSEL: + case TestimonialsDesigns.HORIZONTAL_CAROUSEL_WITH_BG: + case TestimonialsDesigns.HORIZONTAL_CAROUSEL_DIVERSITY: + return ( + + ); + + case TestimonialsDesigns.MULTI_CARD_DISPLAY: + return ( + + ); + + default: + return ( + + ); + } + }; + + return ( +
    + {renderDesign()} +
    + ); +} diff --git a/frontend/src/components/WebPageComponents/designs.ts b/frontend/src/components/WebPageComponents/designs.ts new file mode 100644 index 0000000..3b3e061 --- /dev/null +++ b/frontend/src/components/WebPageComponents/designs.ts @@ -0,0 +1,77 @@ +export enum GalleryPortfolioDesigns { + DEFAULT_DESIGN, + HORIZONTAL_WITH_BUTTONS, + OVERLAPPING_CENTRAL_IMAGE, +} + +export enum HeroDesigns { + DEFAULT_DESIGN, + IMAGE_LEFT, + IMAGE_RIGHT, + IMAGE_BG, + TEXT_CENTER, +} + +export enum ContactFormDesigns { + DEFAULT_DESIGN, + SIMPLE_CLEAN, + SIMPLE_CLEAN_DIVERSITY, + HIGHLIGHTED, + HIGHLIGHTED_DIVERSITY, + WITH_IMAGE, +} + +export enum FaqDesigns { + DEFAULT_DESIGN, + ACCORDION, + SPLIT_LIST, + SPLIT_LIST_DIVERSITY, + TWO_COLUMN, +} + +export enum FeaturesDesigns { + DEFAULT_DESIGN, + CARDS_GRID_WITH_ICONS, + CARDS_GRID_WITH_ICONS_DIVERSITY, + ICONS_TOP, + LARGE_NUMBERS, + ICONS_WITH_IMAGE, +} + +export enum TestimonialsDesigns { + DEFAULT_DESIGN, + HORIZONTAL_CAROUSEL, + HORIZONTAL_CAROUSEL_WITH_BG, + HORIZONTAL_CAROUSEL_DIVERSITY, + MULTI_CARD_DISPLAY, +} + +export enum AboutUsDesigns { + DEFAULT_DESIGN, + IMAGE_LEFT, + IMAGE_RIGHT, +} + +export enum PricingDesigns { + DEFAULT_DESIGN, +} + +export enum HeaderStyle { + PAGES_RIGHT, + PAGES_LEFT, +} + +export enum FooterStyle { + WITH_PAGES, + WITH_PROJECT_NAME, +} + +export enum HeaderDesigns { + DESIGN_DIVERSITY, + DEFAULT_DESIGN, +} + +export enum FooterDesigns { + DESIGN_DIVERSITY, + DEFAULT_DESIGN, +} diff --git a/frontend/src/components/WidgetCreator/RoleSelect.tsx b/frontend/src/components/WidgetCreator/RoleSelect.tsx new file mode 100644 index 0000000..c2da317 --- /dev/null +++ b/frontend/src/components/WidgetCreator/RoleSelect.tsx @@ -0,0 +1,65 @@ +import React, { useEffect, useId, useState } from 'react'; +import { AsyncPaginate } from 'react-select-async-paginate'; +import axios from 'axios'; + +export const RoleSelect = ({ + options, + field, + form, + itemRef, + disabled, + currentUser, +}) => { + const [value, setValue] = useState(null); + const PAGE_SIZE = 100; + + React.useEffect(() => { + if (currentUser.app_role.id) { + setValue({ + value: currentUser.app_role.id, + label: currentUser.app_role.name, + }); + } + }, [currentUser]); + + useEffect(() => { + if (options?.value && options?.label) { + setValue({ value: options.value, label: options.label }); + } + }, [options?.id, field?.value?.id]); + + const mapResponseToValuesAndLabels = (data) => ({ + value: data.id, + label: data.label, + }); + const handleChange = (option) => { + form.setFieldValue(field.name, option); + setValue(option); + }; + + async function callApi(inputValue: string, loadedOptions: any[]) { + const path = `/${itemRef}/autocomplete?limit=${PAGE_SIZE}&offset=${ + loadedOptions.length + }${inputValue ? `&query=${inputValue}` : ''}`; + const { data } = await axios(path); + return { + options: data.map(mapResponseToValuesAndLabels), + hasMore: data.length === PAGE_SIZE, + }; + } + return ( + 'px-1 py-2', + }} + classNamePrefix={'react-select'} + instanceId={useId()} + value={value} + debounceTimeout={1000} + loadOptions={callApi} + onChange={handleChange} + defaultOptions + isDisabled={disabled} + /> + ); +}; diff --git a/frontend/src/components/WidgetCreator/WidgetCreator.tsx b/frontend/src/components/WidgetCreator/WidgetCreator.tsx new file mode 100644 index 0000000..7364996 --- /dev/null +++ b/frontend/src/components/WidgetCreator/WidgetCreator.tsx @@ -0,0 +1,156 @@ +import CardBox from '../CardBox'; +import { mdiCog } from '@mdi/js'; +import { Field, Form, Formik } from 'formik'; +import { ToastContainer, toast } from 'react-toastify'; +import FormField from '../FormField'; +import React from 'react'; +import { useTranslation } from 'next-i18next'; +import { + aiPrompt, + setErrorNotification, + resetNotify, +} from '../../stores/openAiSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; + +import { fetchWidgets } from '../../stores/roles/rolesSlice'; + +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import { RoleSelect } from './RoleSelect'; + +export const WidgetCreator = ({ + currentUser, + isFetchingQuery, + setWidgetsRole, + widgetsRole, +}) => { + const { t } = useTranslation('common'); + const dispatch = useAppDispatch(); + const [isModalOpen, setIsModalOpen] = React.useState(false); + const { notify: openAiNotify } = useAppSelector((state) => state.openAi); + + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + React.useEffect(() => { + if (openAiNotify.showNotification) { + notify(openAiNotify.typeNotification, openAiNotify.textNotification); + dispatch(resetNotify()); + } + }, [openAiNotify.showNotification]); + + const openModal = (): void => { + setIsModalOpen(true); + }; + + const handleCloseModal = (value = {}) => { + setWidgetsRole(value); + setIsModalOpen(false); + }; + + const getWidgets = async () => { + await dispatch(fetchWidgets(widgetsRole?.role?.value || '')); + }; + + const smartSearch = async ( + values: { description: string }, + resetForm: any, + ) => { + const description = values.description; + const projectId = '34407'; + + const payload = { + roleId: widgetsRole?.role?.value, + description, + projectId, + userId: currentUser?.id, + }; + const { payload: responcePayload, error }: any = await dispatch( + aiPrompt(payload), + ); + + await getWidgets().then(); + + resetForm({ values: { description: '' } }); + if (responcePayload.data?.error || error) { + const errorMessage = + responcePayload.data?.error?.message || error?.message; + await dispatch( + setErrorNotification(errorMessage || 'Error with widget creation'), + ); + } + }; + + return ( + <> + + + smartSearch(values, resetForm)} + > +
    + + + +
    +
    +
    + handleCloseModal(values)} + > + {({ submitForm }) => ( + setIsModalOpen(false)} + > +

    What role are we showing and creating widgets for?

    + +
    + + + +
    +
    + )} +
    + + + ); +}; diff --git a/frontend/src/config.ts b/frontend/src/config.ts new file mode 100644 index 0000000..56947f1 --- /dev/null +++ b/frontend/src/config.ts @@ -0,0 +1,22 @@ +export const hostApi = + process.env.NODE_ENV === 'development' && !process.env.NEXT_PUBLIC_BACK_API + ? 'http://localhost' + : ''; +export const portApi = + process.env.NODE_ENV === 'development' && !process.env.NEXT_PUBLIC_BACK_API + ? 8080 + : ''; +export const baseURLApi = `${hostApi}${portApi ? `:${portApi}` : ``}/api`; + +export const localStorageDarkModeKey = 'darkMode'; + +export const localStorageStyleKey = 'style'; + +export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'; + +export const appTitle = 'created by Flatlogic generator!'; + +export const getPageTitle = (currentPageTitle: string) => + `${currentPageTitle} — ${appTitle}`; + +export const tinyKey = process.env.NEXT_PUBLIC_TINY_KEY || ''; diff --git a/frontend/src/css/_app.css b/frontend/src/css/_app.css new file mode 100644 index 0000000..4179b23 --- /dev/null +++ b/frontend/src/css/_app.css @@ -0,0 +1,34 @@ +html { + @apply h-full; +} + +body { + @apply pt-14 xl:pl-60 h-full; +} + +#app { + @apply w-screen transition-position lg:w-auto h-full flex flex-col; +} + +.dropdown { + @apply cursor-pointer; +} + +li.stack-item:not(:last-child):after { + content: '/'; + @apply inline-block pl-2; +} + +.m-clipped, +.m-clipped body { + @apply overflow-hidden lg:overflow-visible; +} + +.full-screen body { + @apply p-0; +} + +.main-navbar, +.app-sidebar-brand { + box-shadow: 0px -1px 40px rgba(112, 144, 176, 0.2); +} diff --git a/frontend/src/css/_calendar.css b/frontend/src/css/_calendar.css new file mode 100644 index 0000000..b541947 --- /dev/null +++ b/frontend/src/css/_calendar.css @@ -0,0 +1,62 @@ +.rbc-event { + @apply bg-midnightBlueTheme-buttonColor !important; +} + +.rbc-show-more { + @apply text-primaryText dark:text-white bg-transparent !important; +} + +.rbc-btn-group button { + @apply text-primaryText border-gray-400 dark:text-white !important; +} + +.rbc-btn-group button:hover { + @apply bg-midnightBlueTheme-buttonColor text-white dark:bg-dark-700 !important; +} + +.rbc-btn-group button.rbc-active { + @apply bg-midnightBlueTheme-buttonColor text-white dark:bg-blue-600 !important; +} + +.rbc-btn-group button:focus { + @apply bg-midnightBlueTheme-buttonColor dark:bg-blue-600 !important; +} + +.rbc-day-bg.rbc-off-range-bg { + @apply bg-midnightBlueTheme-mainBG dark:bg-dark-800 !important; +} +.rbc-current-time-indicator { + @apply h-1 !important; +} +.rbc-today { + @apply bg-dark-600/40 dark:bg-dark-600/40 !important; +} + +.rbc-day-bg.rbc-selected-cell { + @apply dark:bg-dark-500 !important; +} + +.rbc-day-bg, +.rbc-timeslot-group, +.rbc-time-header, +.rbc-event, +.rbc-btn-group, +.rbc-month-view, +.rbc-month-view .rbc-header, +.rbc-month-row, +.rbc-date-cell { + @apply border-gray-400 !important; +} + +.rbc-timeslot-group { + @apply border-gray-400 !important; +} + +.rbc-time-header, +.rbc-month-row { + @apply border-gray-400 !important; +} + +.rbc-date-cell { + @apply border-gray-400 !important; +} diff --git a/frontend/src/css/_checkbox-radio-switch.css b/frontend/src/css/_checkbox-radio-switch.css new file mode 100644 index 0000000..4136a86 --- /dev/null +++ b/frontend/src/css/_checkbox-radio-switch.css @@ -0,0 +1,73 @@ +@layer components { + .checkbox, + .radio, + .switch { + @apply inline-flex items-center cursor-pointer relative; + } + + .checkbox input[type='checkbox'], + .radio input[type='radio'], + .switch input[type='checkbox'] { + @apply absolute left-0 opacity-0 -z-1; + } + + .checkbox input[type='checkbox'] + .check, + .radio input[type='radio'] + .check, + .switch input[type='checkbox'] + .check { + @apply border-gray-700 border transition-colors duration-200 dark:bg-slate-800; + } + + .checkbox input[type='checkbox']:focus + .check, + .radio input[type='radio']:focus + .check, + .switch input[type='checkbox']:focus + .check { + @apply ring ring-midnightBlueTheme-800; + } + + .checkbox input[type='checkbox'] + .check, + .radio input[type='radio'] + .check { + @apply block w-5 h-5; + } + + .checkbox input[type='checkbox'] + .check { + @apply rounded; + } + + .switch input[type='checkbox'] + .check { + @apply flex items-center shrink-0 w-12 h-6 p-0.5 bg-gray-200; + } + + .radio input[type='radio'] + .check, + .switch input[type='checkbox'] + .check, + .switch input[type='checkbox'] + .check:before { + @apply rounded-full; + } + + .checkbox input[type='checkbox']:checked + .check, + .radio input[type='radio']:checked + .check { + @apply bg-no-repeat bg-center border-4; + } + + .checkbox input[type='checkbox']:checked + .check { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23fff' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E"); + } + + .radio input[type='radio']:checked + .check { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z' /%3E%3C/svg%3E"); + } + + .switch input[type='checkbox']:checked + .check, + .checkbox input[type='checkbox']:checked + .check, + .radio input[type='radio']:checked + .check { + @apply bg-midnightBlueTheme-buttonColor border-midnightBlueTheme-buttonColor; + } + + .switch input[type='checkbox'] + .check:before { + content: ''; + @apply block w-5 h-5 bg-white border border-gray-700; + } + + .switch input[type='checkbox']:checked + .check:before { + transform: translate3d(110%, 0, 0); + @apply border-blue-600; + } +} diff --git a/frontend/src/css/_helper.css b/frontend/src/css/_helper.css new file mode 100644 index 0000000..9425a7c --- /dev/null +++ b/frontend/src/css/_helper.css @@ -0,0 +1,24 @@ +.helper-container { + right: 0; + top: 70px; + transform: translateX(100%); + + .tab { + top: 0; + left: 0; + transform: translateX(-100%); + } + + .tab:hover { + @apply bg-gray-900 cursor-pointer; + } +} + +.helper-container.open { + transform: translateX(0); +} + +.react-datepicker-wrapper, +.react-datepicker-popper { + z-index: 10 !important; +} diff --git a/frontend/src/css/_progress.css b/frontend/src/css/_progress.css new file mode 100644 index 0000000..d419f78 --- /dev/null +++ b/frontend/src/css/_progress.css @@ -0,0 +1,21 @@ +@layer base { + progress { + @apply h-3 rounded-full overflow-hidden; + } + + progress::-webkit-progress-bar { + @apply bg-blue-200; + } + + progress::-webkit-progress-value { + @apply bg-blue-500; + } + + progress::-moz-progress-bar { + @apply bg-blue-500; + } + + progress::-ms-fill { + @apply bg-blue-500 border-0; + } +} diff --git a/frontend/src/css/_rich-text.css b/frontend/src/css/_rich-text.css new file mode 100644 index 0000000..00a1f52 --- /dev/null +++ b/frontend/src/css/_rich-text.css @@ -0,0 +1,63 @@ +/* Editor background */ +.tox .tox-editor-container { + @apply bg-midnightBlueTheme-mainBG dark:bg-dark-800 !important; +} + +/* Toolbar background */ +.tox .tox-toolbar, +.tox .tox-editor-header, +.tox .tox-menu, +.tox .tox-menubar, +.tox .tox-toolbar__primary { + @apply bg-midnightBlueTheme-cardColor dark:bg-dark-800 !important; +} + +.tox .tox-collection--toolbar .tox-collection__item--active, +.tox .tox-collection--toolbar .tox-collection__item--enabled { + @apply bg-midnightBlueTheme-diversityMain dark:bg-dark-600 !important; +} + +/* Toolbar button color */ +.tox .tox-toolbar__group button, +.tox .tox-mbtn { + @apply text-midnightBlueTheme-text dark:text-white !important; +} + +.tox .tox-tbtn svg { + @apply fill-midnightBlueTheme-text dark:text-white !important; +} + +/* Toolbar button hover/active state */ +.tox .tox-tbtn--enabled, +.tox .tox-tbtn--enabled:hover, +.tox .tox-tbtn:hover, +.tox .tox-mbtn:hover { + @apply bg-midnightBlueTheme-diversityMain dark:bg-dark-600 !important; +} + +/* Status bar styles */ +.tox .tox-statusbar, +.tox { + @apply bg-midnightBlueTheme-mainBG dark:bg-dark-800 text-midnightBlueTheme-text dark:text-white !important; +} + +.tox .tox-statusbar a, +.tox .tox-statusbar__path-item, +.tox .tox-statusbar__wordcount { + @apply text-midnightBlueTheme-text dark:text-white !important; +} + +/* Dropdown menu item text */ +.tox .tox-collection__item { + @apply text-midnightBlueTheme-text dark:text-white !important; +} + +/* Editor content area */ +.tox.tox-tinymce { + @apply border border-midnightBlueTheme-outsideCardColor dark:border-dark-700 text-midnightBlueTheme-text dark:text-white !important; +} + +/* Change the content area background color */ +.tox .tox-edit-area__iframe { + @apply bg-midnightBlueTheme-mainBG dark:bg-dark-800 text-midnightBlueTheme-text dark:text-white !important; +} diff --git a/frontend/src/css/_scrollbars.css b/frontend/src/css/_scrollbars.css new file mode 100644 index 0000000..11445a9 --- /dev/null +++ b/frontend/src/css/_scrollbars.css @@ -0,0 +1,41 @@ +@layer base { + html { + scrollbar-width: thin; + scrollbar-color: rgb(156, 163, 175) rgb(249, 250, 251); + } + + body::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + body::-webkit-scrollbar-track { + @apply bg-gray-50; + } + + body::-webkit-scrollbar-thumb { + @apply bg-gray-400 rounded; + } + + body::-webkit-scrollbar-thumb:hover { + @apply bg-gray-500; + } +} + +@layer utilities { + .dark-scrollbars-compat { + scrollbar-color: rgb(71, 85, 105) rgb(30, 41, 59); + } + + .dark-scrollbars::-webkit-scrollbar-track { + @apply bg-slate-800; + } + + .dark-scrollbars::-webkit-scrollbar-thumb { + @apply bg-slate-600; + } + + .dark-scrollbars::-webkit-scrollbar-thumb:hover { + @apply bg-slate-500; + } +} diff --git a/frontend/src/css/_select-dropdown.css b/frontend/src/css/_select-dropdown.css new file mode 100644 index 0000000..90fa422 --- /dev/null +++ b/frontend/src/css/_select-dropdown.css @@ -0,0 +1,32 @@ +.react-select__control { + @apply bg-midnightBlueTheme-cardColor rounded-lg border-gray-600 dark:bg-dark-800 dark:border-dark-700 !important; +} + +.react-select__single-value { + @apply text-primaryText dark:text-white !important; +} + +.react-select__menu { + @apply bg-midnightBlueTheme-cardColor dark:border-dark-700; +} + +.react-select__menu-list { + @apply bg-midnightBlueTheme-cardColor dark:bg-dark-800 dark:border-dark-700 dark:rounded !important; +} + +.react-select__option { + @apply cursor-pointer hover:bg-gray-200 hover:bg-midnightBlueTheme-cardColor dark:hover:bg-dark-700 !important; +} + +.react-select__option--is-focused { + @apply bg-midnightBlueTheme-cardColor text-primaryText dark:bg-dark-800 dark:text-white hover:dark:bg-dark-700 hover:dark:text-white !important; +} + +.react-select__option--is-selected, +.react-select__option--is-selected:hover { + @apply bg-midnightBlueTheme-cardColor dark:bg-dark-600 !important; +} + +.react-select__multi-value__remove { + @apply text-midnightBlueTheme-cardColor dark:bg-dark-600 dark:text-white hover:dark:bg-red-300 hover:dark:text-red-600 !important; +} diff --git a/frontend/src/css/_table.css b/frontend/src/css/_table.css new file mode 100644 index 0000000..f9358dd --- /dev/null +++ b/frontend/src/css/_table.css @@ -0,0 +1,123 @@ +@layer base { + table { + @apply w-full; + } + + thead { + @apply hidden lg:table-header-group; + } + + tr { + @apply max-w-full block relative border-b-4 border-gray-100 + lg:table-row lg:border-b-0 dark:border-slate-800; + } + + tr:last-child { + @apply border-b-0; + } + + td:not(:first-child) { + @apply lg:border-l lg:border-t-0 lg:border-r-0 lg:border-b-0 lg:border-gray-100 lg:dark:border-slate-700; + } + + th { + @apply lg:text-left lg:p-3 border-b; + } + + th.sortable { + cursor: pointer; + } + + th.sortable:hover:after { + transition: all 1s; + position: absolute; + + content: '↕'; + + margin-left: 1rem; + } + + th.sortable.asc:hover:after { + content: '↑'; + } + th.sortable.desc:hover:after { + content: '↓'; + } + + td { + @apply flex justify-between text-right py-3 px-4 align-top border-b border-gray-100 + lg:table-cell lg:text-left lg:p-3 lg:align-middle lg:border-b-0 dark:border-slate-800 dark:text-white; + } + + td:last-child { + @apply border-b-0; + } + + tbody tr, + tbody tr:nth-child(odd) { + @apply lg:hover:bg-midnightBlueTheme-mainBG; + } + + tbody tr:nth-child(even) { + @apply lg:bg-midnightBlueTheme-mainBG dark:bg-pavitra-300/70; + } + + td:before { + content: attr(data-label); + @apply font-semibold pr-3 text-left lg:hidden; + } + + tbody tr td { + @apply text-sm font-normal text-primaryText dark:text-white; + } + + .datagrid--table, + .MuiDataGrid-root { + @apply rounded border-none !important; + + @apply font-sans text-primaryText !important; + } + + .datagrid--header { + @apply uppercase !important; + } + + .datagrid--header, + .datagrid--header .MuiIconButton-root, + .datagrid--cell, + .datagrid--cell .MuiIconButton-root { + @apply text-primaryText dark:text-white; + } + + .datagrid--cell .MuiDataGrid-booleanCell { + @apply text-primaryText dark:text-white !important; + } + + .datagrid--cell .MuiIconButton-root:hover { + @apply text-primaryText dark:text-white dark:bg-dark-700; + } + + .datagrid--row { + @apply even:bg-midnightBlueTheme-mainBG dark:even:bg-[#1B1D22] dark:odd:bg-dark-900 !important; + } + + .datagrid--row:hover { + @apply bg-midnightBlueTheme-mainBG !important; + } + + .datagrid--table .MuiTablePagination-root { + @apply text-primaryText dark:text-white; + } + + .datagrid--table .MuiTablePagination-root .MuiButtonBase-root:disabled { + @apply text-primaryText dark:text-dark-700; + } + + .datagrid--table .MuiTablePagination-root .MuiButtonBase-root:hover { + @apply dark:bg-dark-700; + } + + .MuiButton-colorInherit { + @apply text-midnightBlueTheme-iconsColor dark:text-dark-700 !important; + } +} diff --git a/frontend/src/css/_theme.css b/frontend/src/css/_theme.css new file mode 100644 index 0000000..765b8d3 --- /dev/null +++ b/frontend/src/css/_theme.css @@ -0,0 +1,105 @@ +.theme-pink { + .app-sidebar { + @apply bg-pavitra-900 text-white; + + .menu-title, + .menu-item-icon, + .menu-item-link { + @apply text-white; + } + } + + .app-sidebar-brand { + @apply bg-white; + } + + .bg-blue-600 { + @apply bg-pavitra-800; + } + + .border-blue-700 { + @apply border-pink-700; + } + + .checkbox input[type='checkbox']:checked + .check, + .radio input[type='radio']:checked + .check { + @apply border-pavitra-800; + } + + .helper-container .tab { + @apply bg-pavitra-900; + } + + .focus\:ring:focus { + --tw-ring-color: #14142a; + } + + .checkbox input[type='checkbox']:focus + .check, + .radio input[type='radio']:focus + .check, + .switch input[type='checkbox']:focus + .check { + --tw-ring-color: #14142a; + } +} + +.theme-green { + .app-sidebar { + @apply bg-pavitra-800 text-white; + + .menu-title, + .menu-item-icon, + .menu-item-link { + @apply text-white; + } + } + + .app-sidebar-brand { + @apply bg-white; + } + + .bg-blue-600 { + @apply bg-pavitra-800; + } + + .border-blue-700 { + @apply bg-pavitra-700; + } + + .hover\:bg-blue-700:hover { + @apply bg-pavitra-700; + } + + .text-blue-600 { + @apply text-pavitra-900; + } + + .checkbox input[type='checkbox']:checked + .check, + .radio input[type='radio']:checked + .check { + @apply border-pavitra-800; + } + + .helper-container .tab { + @apply bg-pavitra-700; + } + + .focus\:ring:focus { + --tw-ring-color: #4e4b66; + } + + .checkbox input[type='checkbox']:focus + .check, + .radio input[type='radio']:focus + .check, + .switch input[type='checkbox']:focus + .check { + --tw-ring-color: #4e4b66; + } + + .text-blue-500 { + @apply text-pavitra-800; + } + + .hover\:text-blue-600:hover { + @apply text-pavitra-800; + } + + .active\:text-blue-700:active { + @apply text-pavitra-800; + } +} diff --git a/frontend/src/css/main.css b/frontend/src/css/main.css new file mode 100644 index 0000000..074fc96 --- /dev/null +++ b/frontend/src/css/main.css @@ -0,0 +1,62 @@ +@import 'tailwind/_base.css'; +@import 'tailwind/_components.css'; +@import 'tailwind/_utilities.css'; +@import 'intro.js/introjs.css'; +@import '_checkbox-radio-switch.css'; +@import '_progress.css'; +@import '_scrollbars.css'; +@import '_table.css'; +@import '_helper.css'; +@import '_calendar.css'; +@import '_select-dropdown.css'; +@import '_theme.css'; +@import '_rich-text.css'; + +@import url('https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&display=swap'); + +body { + @apply font-sans text-primaryText !important; +} +.MuiList-root, +.MuiListItemIcon-root { + @apply text-midnightBlueTheme-buttonColor bg-gray-600 !important; +} +.MuiCheckbox-colorPrimary { + @apply text-midnightBlueTheme-buttonColor !important; +} +.MuiDataGrid-row.Mui-selected { + @apply bg-midnightBlueTheme-mainBG !important; +} +.MuiDataGrid-withBorderColor { + @apply border-midnightBlueTheme-mainBG !important; +} +.MuiDataGrid-cell:focus { + @apply outline-midnightBlueTheme-buttonColor !important; +} + +.introjs-tooltip { + @apply min-w-[400px] max-w-[480px] p-2 !important; +} + +.introjs-tooltipReferenceLayer * { + @apply font-sans text-base text-gray-900; + + font-family: 'Ubuntu', sans-serif !important; +} + +.good-img { + @apply -mt-96 !important; +} +.end-img { + @apply -mt-72 !important; +} +.introjs-button { + @apply bg-midnightBlueTheme-buttonColor text-white !important; + text-shadow: none !important; +} +.introjs-bullets ul li a.active { + @apply bg-midnightBlueTheme-iconsColor !important; +} +.introjs-prevbutton { + @apply bg-transparent border border-midnightBlueTheme-buttonColor text-midnightBlueTheme-buttonColor !important; +} diff --git a/frontend/src/css/tailwind/_base.css b/frontend/src/css/tailwind/_base.css new file mode 100644 index 0000000..2f02db5 --- /dev/null +++ b/frontend/src/css/tailwind/_base.css @@ -0,0 +1 @@ +@tailwind base; diff --git a/frontend/src/css/tailwind/_components.css b/frontend/src/css/tailwind/_components.css new file mode 100644 index 0000000..020aaba --- /dev/null +++ b/frontend/src/css/tailwind/_components.css @@ -0,0 +1 @@ +@tailwind components; diff --git a/frontend/src/css/tailwind/_utilities.css b/frontend/src/css/tailwind/_utilities.css new file mode 100644 index 0000000..65dd5f6 --- /dev/null +++ b/frontend/src/css/tailwind/_utilities.css @@ -0,0 +1 @@ +@tailwind utilities; diff --git a/frontend/src/helpers/dataFormatter.js b/frontend/src/helpers/dataFormatter.js new file mode 100644 index 0000000..4a4abf3 --- /dev/null +++ b/frontend/src/helpers/dataFormatter.js @@ -0,0 +1,269 @@ +import dayjs from 'dayjs'; +import _ from 'lodash'; + +export default { + filesFormatter(arr) { + if (!arr || !arr.length) return []; + return arr.map((item) => item); + }, + imageFormatter(arr) { + if (!arr || !arr.length) return []; + return arr.map((item) => ({ + publicUrl: item.publicUrl || '', + })); + }, + oneImageFormatter(arr) { + if (!arr || !arr.length) return ''; + return arr[0].publicUrl || ''; + }, + dateFormatter(date) { + if (!date) return ''; + return dayjs(date).format('YYYY-MM-DD'); + }, + dateTimeFormatter(date) { + if (!date) return ''; + return dayjs(date).format('YYYY-MM-DD HH:mm'); + }, + booleanFormatter(val) { + return val ? 'Yes' : 'No'; + }, + dataGridEditFormatter(obj) { + return _.transform(obj, (result, value, key) => { + if (_.isArray(value)) { + result[key] = _.map(value, 'id'); + } else if (_.isObject(value)) { + result[key] = value.id; + } else { + result[key] = value; + } + }); + }, + + usersManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.firstName); + }, + usersOneListFormatter(val) { + if (!val) return ''; + return val.firstName; + }, + usersManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.firstName }; + }); + }, + usersOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.firstName, id: val.id }; + }, + + fin_accountsManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + fin_accountsOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + fin_accountsManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + fin_accountsOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, + + fin_customersManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + fin_customersOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + fin_customersManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + fin_customersOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, + + fin_vendorsManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + fin_vendorsOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + fin_vendorsManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + fin_vendorsOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, + + hr_employeesManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + hr_employeesOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + hr_employeesManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + hr_employeesOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, + + hr_positionsManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.title); + }, + hr_positionsOneListFormatter(val) { + if (!val) return ''; + return val.title; + }, + hr_positionsManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.title }; + }); + }, + hr_positionsOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.title, id: val.id }; + }, + + hr_reqsManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.recruiter); + }, + hr_reqsOneListFormatter(val) { + if (!val) return ''; + return val.recruiter; + }, + hr_reqsManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.recruiter }; + }); + }, + hr_reqsOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.recruiter, id: val.id }; + }, + + ops_linesManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + ops_linesOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + ops_linesManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + ops_linesOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, + + ops_plantsManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + ops_plantsOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + ops_plantsManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + ops_plantsOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, + + rolesManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + rolesOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + rolesManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + rolesOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, + + permissionsManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + permissionsOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + permissionsManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + permissionsOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, + + organizationsManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + organizationsOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + organizationsManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + organizationsOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, +}; diff --git a/frontend/src/helpers/fileSaver.ts b/frontend/src/helpers/fileSaver.ts new file mode 100644 index 0000000..242b540 --- /dev/null +++ b/frontend/src/helpers/fileSaver.ts @@ -0,0 +1,6 @@ +import { saveAs } from 'file-saver'; + +export const saveFile = (e, url: string, name: string) => { + e.stopPropagation(); + saveAs(url, name); +}; diff --git a/frontend/src/helpers/humanize.ts b/frontend/src/helpers/humanize.ts new file mode 100644 index 0000000..61b6407 --- /dev/null +++ b/frontend/src/helpers/humanize.ts @@ -0,0 +1,12 @@ +export function humanize(str: string) { + if (!str) { + return ''; + } + return str + .toString() + .replace(/^[\s_]+|[\s_]+$/g, '') + .replace(/[_\s]+/g, ' ') + .replace(/^[a-z]/, function (m) { + return m.toUpperCase(); + }); +} diff --git a/frontend/src/helpers/notifyStateHandler.ts b/frontend/src/helpers/notifyStateHandler.ts new file mode 100644 index 0000000..ae21899 --- /dev/null +++ b/frontend/src/helpers/notifyStateHandler.ts @@ -0,0 +1,32 @@ +export const resetNotify = (state) => { + state.notify.showNotification = false; + state.notify.typeNotification = ''; + state.notify.textNotification = ''; +}; +export const rejectNotify = (state, action) => { + if (typeof action.payload === 'string') { + state.notify.textNotification = action.payload; + } else if (typeof action === 'object') { + const obj = { ...action.payload?.errors }; + delete obj['_errors']; + + let msg = ''; + + for (const key in obj) { + msg += `${key}: ${obj[key]['_errors']}; \n `; + } + + state.notify.textNotification = msg; + } else { + state.notify.textNotification = 'Network error'; + } + state.notify.textNotification = + state.notify.textNotification || 'Network error'; + state.notify.typeNotification = 'error'; + state.notify.showNotification = true; +}; +export const fulfilledNotify = (state, msg) => { + state.notify.textNotification = msg; + state.notify.typeNotification = 'success'; + state.notify.showNotification = true; +}; diff --git a/frontend/src/helpers/pexels.ts b/frontend/src/helpers/pexels.ts new file mode 100644 index 0000000..524a1b2 --- /dev/null +++ b/frontend/src/helpers/pexels.ts @@ -0,0 +1,75 @@ +import axios from 'axios'; + +export async function getPexelsImage() { + try { + const response = await axios.get(`/pexels/image`); + return response.data; + } catch (error) { + console.error('Error fetching image:', error); + return null; + } +} + +export async function getPexelsVideo() { + try { + const response = await axios.get(`/pexels/video`); + return response.data; + } catch (error) { + console.error('Error fetching video:', error); + return null; + } +} + +let localStorageLock = false; + +export async function getMultiplePexelsImages( + queries = ['home', 'apple', 'pizza', 'mountains', 'cat'], +) { + const normalizeQuery = (query) => + query.trim().toLowerCase().replace(/\s+/g, ''); + + while (localStorageLock) { + await new Promise((resolve) => setTimeout(resolve, 50)); + } + localStorageLock = true; + + const cachedImages = + JSON.parse(localStorage.getItem('pexelsImagesCache')) || {}; + + const isImageCached = (query) => { + const normalizedQuery = normalizeQuery(query); + const cached = cachedImages[normalizedQuery]; + const isCached = + cached && cached.src && cached.photographer && cached.photographer_url; + return isCached; + }; + + const missingQueries = queries.filter((query) => !isImageCached(query)); + + if (missingQueries.length > 0) { + const queryString = missingQueries.join(','); + + try { + const response = await axios.get(`/pexels/multiple-images`, { + params: { queries: queryString }, + }); + + missingQueries.forEach((query, index) => { + const normalizedQuery = normalizeQuery(query); + if (!cachedImages[normalizedQuery]) { + cachedImages[normalizedQuery] = response.data[index]; + } + }); + + localStorage.setItem('pexelsImagesCache', JSON.stringify(cachedImages)); + } catch (error) { + console.error(error); + } + } + + const result = queries.map((query) => cachedImages[normalizeQuery(query)]); + + localStorageLock = false; + + return result; +} diff --git a/frontend/src/helpers/userPermissions.ts b/frontend/src/helpers/userPermissions.ts new file mode 100644 index 0000000..c2d9032 --- /dev/null +++ b/frontend/src/helpers/userPermissions.ts @@ -0,0 +1,16 @@ +export function hasPermission(user, permission_name: string | string[]) { + if (!user?.app_role?.name) return false; + if (!permission_name) { + return true; + } + const permissions = new Set([ + ...(user?.custom_permissions ?? []).map((p) => p.name), + ...(user?.app_role_permissions ?? []).map((p) => p.name), + ]); + + if (typeof permission_name === 'string') { + return permissions.has(permission_name) || user.app_role.globalAccess; + } else { + return permission_name.some((permission) => permissions.has(permission)); + } +} diff --git a/frontend/src/hooks/sampleData.ts b/frontend/src/hooks/sampleData.ts new file mode 100644 index 0000000..8c74ad5 --- /dev/null +++ b/frontend/src/hooks/sampleData.ts @@ -0,0 +1,22 @@ +import useSWR from 'swr'; +const fetcher = (url: string) => fetch(url).then((res) => res.json()); + +export const useSampleClients = () => { + const { data, error } = useSWR('/data-sources/clients.json', fetcher); + + return { + clients: data?.data ?? [], + isLoading: !error && !data, + isError: error, + }; +}; + +export const useSampleTransactions = () => { + const { data, error } = useSWR('/data-sources/history.json', fetcher); + + return { + transactions: data?.data ?? [], + isLoading: !error && !data, + isError: error, + }; +}; diff --git a/frontend/src/hooks/useDevCompilationStatus.ts b/frontend/src/hooks/useDevCompilationStatus.ts new file mode 100644 index 0000000..c1b9356 --- /dev/null +++ b/frontend/src/hooks/useDevCompilationStatus.ts @@ -0,0 +1,44 @@ +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; + +type CompilationStatus = 'ready' | 'compiling' | 'error' | 'initial'; + +const useDevCompilationStatus = (): CompilationStatus => { + const router = useRouter(); + const [status, setStatus] = useState('initial'); + + useEffect(() => { + if (process.env.NODE_ENV !== 'development') { + setStatus('ready'); + return; + } + + const handleRouteChangeStart = () => { + setStatus('compiling'); + }; + + const handleRouteChangeComplete = () => { + setTimeout(() => setStatus('ready'), 300); + }; + + const handleRouteChangeError = () => { + setTimeout(() => setStatus('error'), 300); + }; + + router.events.on('routeChangeStart', handleRouteChangeStart); + router.events.on('routeChangeComplete', handleRouteChangeComplete); + router.events.on('routeChangeError', handleRouteChangeError); + + setStatus('ready'); + + return () => { + router.events.off('routeChangeStart', handleRouteChangeStart); + router.events.off('routeChangeComplete', handleRouteChangeComplete); + router.events.off('routeChangeError', handleRouteChangeError); + }; + }, [router]); + + return status; +}; + +export default useDevCompilationStatus; diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts new file mode 100644 index 0000000..b0b33b3 --- /dev/null +++ b/frontend/src/i18n.ts @@ -0,0 +1,21 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import HttpApi from 'i18next-http-backend'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +i18n + .use(HttpApi) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: 'en', + detection: { + order: ['localStorage', 'navigator'], + lookupLocalStorage: 'app_lang_34407', + caches: ['localStorage'], + }, + backend: { + loadPath: '/locales/{{lng}}/{{ns}}.json', + }, + interpolation: { escapeValue: false }, + }); diff --git a/frontend/src/interfaces/index.ts b/frontend/src/interfaces/index.ts new file mode 100644 index 0000000..75048f3 --- /dev/null +++ b/frontend/src/interfaces/index.ts @@ -0,0 +1,122 @@ +export type UserPayloadObject = { + name: string; + email: string; + avatar: string; +}; + +export type MenuAsideItem = { + label: string; + icon?: string; + href?: string; + target?: string; + color?: ColorButtonKey; + isLogout?: boolean; + withDevider?: boolean; + menu?: MenuAsideItem[]; + permissions?: string | string[]; +}; + +export type MenuNavBarItem = { + label?: string; + icon?: string; + href?: string; + target?: string; + isDivider?: boolean; + isLogout?: boolean; + isDesktopNoLabel?: boolean; + isToggleLightDark?: boolean; + isCurrentUser?: boolean; + menu?: MenuNavBarItem[]; +}; + +export type ColorKey = + | 'white' + | 'light' + | 'contrast' + | 'success' + | 'danger' + | 'warning' + | 'info'; + +export type ColorButtonKey = + | 'white' + | 'whiteDark' + | 'lightDark' + | 'contrast' + | 'success' + | 'danger' + | 'warning' + | 'info' + | 'void'; + +export type BgKey = 'purplePink' | 'pinkRed' | 'violet'; + +export type TrendType = + | 'up' + | 'down' + | 'success' + | 'danger' + | 'warning' + | 'info'; + +export type TransactionType = 'withdraw' | 'deposit' | 'invoice' | 'payment'; + +export type Transaction = { + id: number; + amount: number; + account: string; + name: string; + date: string; + type: TransactionType; + business: string; +}; + +export type Client = { + id: number; + avatar: string; + login: string; + name: string; + city: string; + company: string; + firstName: string; + lastName: string; + phoneNumber: string; + email: string; + progress: number; + role: string; + disabled: boolean; + created: string; + created_mm_dd_yyyy: string; +}; + +export interface User { + id: string; + firstName: string; + lastName?: any; + phoneNumber?: any; + email: string; + role: string; + disabled: boolean; + password: string; + emailVerified: boolean; + emailVerificationToken?: any; + emailVerificationTokenExpiresAt?: any; + passwordResetToken?: any; + passwordResetTokenExpiresAt?: any; + provider: string; + importHash?: any; + createdAt: Date; + updatedAt: Date; + deletedAt?: any; + createdById?: any; + updatedById?: any; + avatar: any[]; + notes: any[]; +} + +export type StyleKey = 'white' | 'basic'; + +export type UserForm = { + name: string; + email: string; +}; diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx new file mode 100644 index 0000000..7d616a3 --- /dev/null +++ b/frontend/src/layouts/Authenticated.tsx @@ -0,0 +1,132 @@ +import React, { ReactNode, useEffect } from 'react'; +import { useState } from 'react'; +import jwt from 'jsonwebtoken'; +import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'; +import menuAside from '../menuAside'; +import menuNavBar from '../menuNavBar'; +import BaseIcon from '../components/BaseIcon'; +import NavBar from '../components/NavBar'; +import NavBarItemPlain from '../components/NavBarItemPlain'; +import AsideMenu from '../components/AsideMenu'; +import FooterBar from '../components/FooterBar'; +import { useAppDispatch, useAppSelector } from '../stores/hooks'; +import Search from '../components/Search'; +import { useRouter } from 'next/router'; +import { findMe, logoutUser } from '../stores/authSlice'; + +import { hasPermission } from '../helpers/userPermissions'; + +type Props = { + children: ReactNode; + + permission?: string; +}; + +export default function LayoutAuthenticated({ + children, + + permission, +}: Props) { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { token, currentUser } = useAppSelector((state) => state.auth); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + let localToken; + if (typeof window !== 'undefined') { + // Perform localStorage action + localToken = localStorage.getItem('token'); + } + + const isTokenValid = () => { + const token = localStorage.getItem('token'); + if (!token) return; + const date = new Date().getTime() / 1000; + const data = jwt.decode(token); + if (!data) return; + return date < data.exp; + }; + + useEffect(() => { + dispatch(findMe()); + if (!isTokenValid()) { + dispatch(logoutUser()); + router.push('/login'); + } + }, [token, localToken]); + + useEffect(() => { + if (!permission || !currentUser) return; + + if (!hasPermission(currentUser, permission)) router.push('/error'); + }, [currentUser, permission]); + + const darkMode = useAppSelector((state) => state.style.darkMode); + + const [isAsideMobileExpanded, setIsAsideMobileExpanded] = useState(false); + const [isAsideLgActive, setIsAsideLgActive] = useState(false); + + useEffect(() => { + const handleRouteChangeStart = () => { + setIsAsideMobileExpanded(false); + setIsAsideLgActive(false); + }; + + router.events.on('routeChangeStart', handleRouteChangeStart); + + // If the component is unmounted, unsubscribe + // from the event with the `off` method: + return () => { + router.events.off('routeChangeStart', handleRouteChangeStart); + }; + }, [router.events, dispatch]); + + const layoutAsidePadding = 'xl:pl-60'; + + return ( +
    +
    + + setIsAsideMobileExpanded(!isAsideMobileExpanded)} + > + + + setIsAsideLgActive(true)} + > + + + + + + + setIsAsideLgActive(false)} + /> + {children} + Hand-crafted & Made with ❤️ +
    +
    + ); +} diff --git a/frontend/src/layouts/Guest.tsx b/frontend/src/layouts/Guest.tsx new file mode 100644 index 0000000..49ac1b0 --- /dev/null +++ b/frontend/src/layouts/Guest.tsx @@ -0,0 +1,19 @@ +import React, { ReactNode } from 'react'; +import { useAppSelector } from '../stores/hooks'; + +type Props = { + children: ReactNode; +}; + +export default function LayoutGuest({ children }: Props) { + const darkMode = useAppSelector((state) => state.style.darkMode); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + + return ( +
    +
    + {children} +
    +
    + ); +} diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts new file mode 100644 index 0000000..06c9a21 --- /dev/null +++ b/frontend/src/menuAside.ts @@ -0,0 +1,317 @@ +import * as icon from '@mdi/js'; +import { MenuAsideItem } from './interfaces'; + +const menuAside: MenuAsideItem[] = [ + { + href: '/dashboard', + icon: icon.mdiViewDashboardOutline, + label: 'Dashboard', + }, + + { + href: '/users/users-list', + label: 'Users', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiAccountGroup ?? icon.mdiTable, + permissions: 'READ_USERS', + }, + { + href: '/audit_logs/audit_logs-list', + label: 'Audit logs', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiHistory' in icon + ? icon['mdiHistory' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_AUDIT_LOGS', + }, + { + href: '/data_connections/data_connections-list', + label: 'Data connections', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiDatabase' in icon + ? icon['mdiDatabase' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_DATA_CONNECTIONS', + }, + { + href: '/fin_accounts/fin_accounts-list', + label: 'Fin accounts', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiAccountBalance' in icon + ? icon['mdiAccountBalance' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_FIN_ACCOUNTS', + }, + { + href: '/fin_ap/fin_ap-list', + label: 'Fin ap', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiFileDocumentOutline' in icon + ? icon['mdiFileDocumentOutline' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_FIN_AP', + }, + { + href: '/fin_ar/fin_ar-list', + label: 'Fin ar', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiFileDocument' in icon + ? icon['mdiFileDocument' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_FIN_AR', + }, + { + href: '/fin_budgets/fin_budgets-list', + label: 'Fin budgets', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiFinance' in icon + ? icon['mdiFinance' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_FIN_BUDGETS', + }, + { + href: '/fin_customers/fin_customers-list', + label: 'Fin customers', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiAccountGroup' in icon + ? icon['mdiAccountGroup' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_FIN_CUSTOMERS', + }, + { + href: '/fin_forecasts/fin_forecasts-list', + label: 'Fin forecasts', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiChartLine' in icon + ? icon['mdiChartLine' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_FIN_FORECASTS', + }, + { + href: '/fin_transactions/fin_transactions-list', + label: 'Fin transactions', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiCurrencyUsd' in icon + ? icon['mdiCurrencyUsd' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_FIN_TRANSACTIONS', + }, + { + href: '/fin_vendors/fin_vendors-list', + label: 'Fin vendors', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiStore' in icon + ? icon['mdiStore' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_FIN_VENDORS', + }, + { + href: '/hr_applicants/hr_applicants-list', + label: 'Hr applicants', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiAccountCheck' in icon + ? icon['mdiAccountCheck' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_HR_APPLICANTS', + }, + { + href: '/hr_attendance/hr_attendance-list', + label: 'Hr attendance', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiCalendarCheck' in icon + ? icon['mdiCalendarCheck' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_HR_ATTENDANCE', + }, + { + href: '/hr_employees/hr_employees-list', + label: 'Hr employees', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiAccountTie' in icon + ? icon['mdiAccountTie' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_HR_EMPLOYEES', + }, + { + href: '/hr_payroll/hr_payroll-list', + label: 'Hr payroll', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiCurrencyUsdCircle' in icon + ? icon['mdiCurrencyUsdCircle' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_HR_PAYROLL', + }, + { + href: '/hr_positions/hr_positions-list', + label: 'Hr positions', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiBriefcaseOutline' in icon + ? icon['mdiBriefcaseOutline' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_HR_POSITIONS', + }, + { + href: '/hr_reqs/hr_reqs-list', + label: 'Hr reqs', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiClipboardList' in icon + ? icon['mdiClipboardList' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_HR_REQS', + }, + { + href: '/ops_incidents/ops_incidents-list', + label: 'Ops incidents', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiAlertCircle' in icon + ? icon['mdiAlertCircle' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_OPS_INCIDENTS', + }, + { + href: '/ops_inventory/ops_inventory-list', + label: 'Ops inventory', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiWarehouse' in icon + ? icon['mdiWarehouse' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_OPS_INVENTORY', + }, + { + href: '/ops_lines/ops_lines-list', + label: 'Ops lines', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiLineScan' in icon + ? icon['mdiLineScan' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_OPS_LINES', + }, + { + href: '/ops_orders/ops_orders-list', + label: 'Ops orders', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiPackageVariantClosed' in icon + ? icon['mdiPackageVariantClosed' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_OPS_ORDERS', + }, + { + href: '/ops_plants/ops_plants-list', + label: 'Ops plants', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiFactory' in icon + ? icon['mdiFactory' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_OPS_PLANTS', + }, + { + href: '/ops_production_log/ops_production_log-list', + label: 'Ops production log', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiChartBar' in icon + ? icon['mdiChartBar' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_OPS_PRODUCTION_LOG', + }, + { + href: '/ops_shipments/ops_shipments-list', + label: 'Ops shipments', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: + 'mdiTruckDelivery' in icon + ? icon['mdiTruckDelivery' as keyof typeof icon] + : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_OPS_SHIPMENTS', + }, + { + href: '/roles/roles-list', + label: 'Roles', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable, + permissions: 'READ_ROLES', + }, + { + href: '/permissions/permissions-list', + label: 'Permissions', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiShieldAccountOutline ?? icon.mdiTable, + permissions: 'READ_PERMISSIONS', + }, + { + href: '/organizations/organizations-list', + label: 'Organizations', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_ORGANIZATIONS', + }, + { + href: '/profile', + label: 'Profile', + icon: icon.mdiAccountCircle, + }, + + { + href: '/services', + label: 'Home page', + icon: icon.mdiHome, + withDevider: true, + }, + { + href: '/api-docs', + target: '_blank', + label: 'Swagger API', + icon: icon.mdiFileCode, + permissions: 'READ_API_DOCS', + }, +]; + +export default menuAside; diff --git a/frontend/src/menuNavBar.ts b/frontend/src/menuNavBar.ts new file mode 100644 index 0000000..2bf1111 --- /dev/null +++ b/frontend/src/menuNavBar.ts @@ -0,0 +1,64 @@ +import { + mdiMenu, + mdiClockOutline, + mdiCloud, + mdiCrop, + mdiAccount, + mdiCogOutline, + mdiEmail, + mdiLogout, + mdiThemeLightDark, + mdiGithub, + mdiVuejs, +} from '@mdi/js'; +import { MenuNavBarItem } from './interfaces'; + +const menuNavBar: MenuNavBarItem[] = [ + { + isCurrentUser: true, + menu: [ + { + icon: mdiAccount, + label: 'My Profile', + href: '/profile', + }, + { + isDivider: true, + }, + { + icon: mdiLogout, + label: 'Log Out', + isLogout: true, + }, + ], + }, + { + icon: mdiThemeLightDark, + label: 'Light/Dark', + isDesktopNoLabel: true, + isToggleLightDark: true, + }, + { + icon: mdiLogout, + label: 'Log out', + isDesktopNoLabel: true, + isLogout: true, + }, +]; + +export const webPagesNavBar = [ + { + href: '/services', + label: 'services', + }, + { + href: '/contact', + label: 'contact', + }, + { + href: '/faq', + label: 'FAQ', + }, +]; + +export default menuNavBar; diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx new file mode 100644 index 0000000..22f40b5 --- /dev/null +++ b/frontend/src/pages/_app.tsx @@ -0,0 +1,194 @@ +import React from 'react'; +import type { AppProps } from 'next/app'; +import type { ReactElement, ReactNode } from 'react'; +import type { NextPage } from 'next'; +import Head from 'next/head'; +import { store } from '../stores/store'; +import { Provider } from 'react-redux'; +import '../css/main.css'; +import axios from 'axios'; +import { baseURLApi } from '../config'; +import { useRouter } from 'next/router'; +import ErrorBoundary from '../components/ErrorBoundary'; +import DevModeBadge from '../components/DevModeBadge'; +import 'intro.js/introjs.css'; +import { appWithTranslation } from 'next-i18next'; +import '../i18n'; +import IntroGuide from '../components/IntroGuide'; +import { + appSteps, + landingSteps, + loginSteps, + usersSteps, + rolesSteps, +} from '../stores/introSteps'; + +// Initialize axios +axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API + ? process.env.NEXT_PUBLIC_BACK_API + : baseURLApi; + +axios.defaults.headers.common['Content-Type'] = 'application/json'; + +export type NextPageWithLayout

    , IP = P> = NextPage< + P, + IP +> & { + getLayout?: (page: ReactElement) => ReactNode; +}; + +type AppPropsWithLayout = AppProps & { + Component: NextPageWithLayout; +}; + +function MyApp({ Component, pageProps }: AppPropsWithLayout) { + // Use the layout defined at the page level, if available + const getLayout = Component.getLayout || ((page) => page); + const router = useRouter(); + const [stepsEnabled, setStepsEnabled] = React.useState(false); + const [stepName, setStepName] = React.useState(''); + const [steps, setSteps] = React.useState([]); + + axios.interceptors.request.use( + (config) => { + const token = localStorage.getItem('token'); + + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } else { + delete config.headers.Authorization; + } + + return config; + }, + (error) => { + return Promise.reject(error); + }, + ); + + // TODO: Remove this code in future releases + React.useEffect(() => { + const handleMessage = async (event: MessageEvent) => { + if (event.data === 'getLocation') { + event.source?.postMessage( + { iframeLocation: window.location.pathname }, + event.origin, + ); + return; + } + + if (event.data === 'getScreenshot') { + try { + const html2canvas = (await import('html2canvas')).default; + const canvas = await html2canvas(document.body, { useCORS: true }); + const url = canvas.toDataURL('image/jpeg', 0.8); + event.source?.postMessage({ iframeScreenshot: url }, event.origin); + } catch (e) { + console.error('html2canvas failed', e); + event.source?.postMessage({ iframeScreenshot: null }, event.origin); + } + } + }; + + window.addEventListener('message', handleMessage); + return () => window.removeEventListener('message', handleMessage); + }, []); + + React.useEffect(() => { + const isCompleted = (stepKey: string) => { + return localStorage.getItem(`completed_${stepKey}`) === 'true'; + }; + if (router.pathname === '/login' && !isCompleted('loginSteps')) { + setSteps(loginSteps); + setStepName('loginSteps'); + setStepsEnabled(true); + } else if (router.pathname === '/' && !isCompleted('landingSteps')) { + setSteps(landingSteps); + setStepName('landingSteps'); + setStepsEnabled(true); + } else if (router.pathname === '/dashboard' && !isCompleted('appSteps')) { + setTimeout(() => { + setSteps(appSteps); + setStepName('appSteps'); + setStepsEnabled(true); + }, 1000); + } else if ( + router.pathname === '/users/users-list' && + !isCompleted('usersSteps') + ) { + setTimeout(() => { + setSteps(usersSteps); + setStepName('usersSteps'); + setStepsEnabled(true); + }, 1000); + } else if ( + router.pathname === '/roles/roles-list' && + !isCompleted('rolesSteps') + ) { + setTimeout(() => { + setSteps(rolesSteps); + setStepName('rolesSteps'); + setStepsEnabled(true); + }, 1000); + } else { + setSteps([]); + setStepsEnabled(false); + } + }, [router.pathname]); + + const handleExit = () => { + setStepsEnabled(false); + }; + + const title = 'Title Enterprise Dashboards Finance HR Ops'; + const description = + 'Title Enterprise Dashboards Finance HR Ops generated by Flatlogic'; + const url = 'https://flatlogic.com/'; + const image = `https://flatlogic.com/logo.svg`; + const imageWidth = '1920'; + const imageHeight = '960'; + + return ( + + {getLayout( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + {(process.env.NODE_ENV === 'development' || + process.env.NODE_ENV === 'dev_stage') && } + , + )} + + ); +} + +export default appWithTranslation(MyApp); diff --git a/frontend/src/pages/api/hello.js b/frontend/src/pages/api/hello.js new file mode 100644 index 0000000..1c39e1f --- /dev/null +++ b/frontend/src/pages/api/hello.js @@ -0,0 +1,5 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction + +export default function helloAPI(req, res) { + res.status(200).json({ name: 'John Doe' }); +} diff --git a/frontend/src/pages/api/logError.ts b/frontend/src/pages/api/logError.ts new file mode 100644 index 0000000..b391ffc --- /dev/null +++ b/frontend/src/pages/api/logError.ts @@ -0,0 +1,83 @@ +import fsPromises from 'fs/promises'; +import path from 'path'; + +const dataFilePath = path.join(process.cwd(), 'json/runtimeError.json'); + +export default async function handler(req, res) { + // Ensure directory exists + try { + await fsPromises.mkdir(path.dirname(dataFilePath), { recursive: true }); + } catch (error) { + // Ignore if directory already exists + } + + if (req.method === 'GET') { + try { + // Check if file exists + try { + await fsPromises.access(dataFilePath); + } catch (error) { + // File doesn't exist, return empty object + return res.status(200).json({}); + } + + // Read the existing data from the JSON file + const jsonData = await fsPromises.readFile(dataFilePath, 'utf-8'); + + // Handle empty file + if (!jsonData || jsonData.trim() === '') { + // Write empty JSON object to file + await fsPromises.writeFile(dataFilePath, '{}', 'utf-8'); + return res.status(200).json({}); + } + + // Parse JSON data + try { + const objectData = JSON.parse(jsonData); + return res.status(200).json(objectData); + } catch (parseError) { + console.error('Error parsing JSON from file:', parseError); + // Reset the file with valid JSON if parsing fails + await fsPromises.writeFile(dataFilePath, '{}', 'utf-8'); + return res.status(200).json({}); + } + } catch (error) { + console.error('Error in GET handler:', error); + return res.status(200).json({}); // Return empty object instead of error + } + } else if (req.method === 'POST') { + try { + const updatedData = JSON.stringify(req.body); + + // Create directory if it doesn't exist + await fsPromises.mkdir(path.dirname(dataFilePath), { recursive: true }); + + // Write the updated data to the JSON file + await fsPromises.writeFile(dataFilePath, updatedData); + + // Send a success response + res.status(200).json({ message: 'Data stored successfully' }); + } catch (error) { + console.error('Error in POST handler:', error); + // Send an error response + res.status(500).json({ message: 'Error storing data' }); + } + } else if (req.method === 'DELETE') { + try { + // Create directory if it doesn't exist + await fsPromises.mkdir(path.dirname(dataFilePath), { recursive: true }); + + // Write empty JSON object to file + await fsPromises.writeFile(dataFilePath, '{}'); + + // Send a success response + res.status(200).json({ message: 'Data deleted successfully' }); + } catch (error) { + console.error('Error in DELETE handler:', error); + // Send an error response + res.status(500).json({ message: 'Error deleting data' }); + } + } else { + res.status(405).json({ message: 'Method not allowed' }); + } +} diff --git a/frontend/src/pages/audit_logs/[audit_logsId].tsx b/frontend/src/pages/audit_logs/[audit_logsId].tsx new file mode 100644 index 0000000..d0c1fae --- /dev/null +++ b/frontend/src/pages/audit_logs/[audit_logsId].tsx @@ -0,0 +1,187 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/audit_logs/audit_logsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditAudit_logs = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + user: null, + + action: '', + + entity: '', + + entity_id: '', + + diff_json: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { audit_logs } = useAppSelector((state) => state.audit_logs); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { audit_logsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: audit_logsId })); + }, [audit_logsId]); + + useEffect(() => { + if (typeof audit_logs === 'object') { + setInitialValues(audit_logs); + } + }, [audit_logs]); + + useEffect(() => { + if (typeof audit_logs === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = audit_logs[el]), + ); + + setInitialValues(newInitialVal); + } + }, [audit_logs]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: audit_logsId, data })); + await router.push('/audit_logs/audit_logs-list'); + }; + + return ( + <> + + {getPageTitle('Edit audit_logs')} + + + + {''} + + + handleSubmit(values)} + > +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/audit_logs/audit_logs-list')} + /> + + + + + + + ); +}; + +EditAudit_logs.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditAudit_logs; diff --git a/frontend/src/pages/audit_logs/audit_logs-edit.tsx b/frontend/src/pages/audit_logs/audit_logs-edit.tsx new file mode 100644 index 0000000..721d279 --- /dev/null +++ b/frontend/src/pages/audit_logs/audit_logs-edit.tsx @@ -0,0 +1,185 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/audit_logs/audit_logsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditAudit_logsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + user: null, + + action: '', + + entity: '', + + entity_id: '', + + diff_json: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { audit_logs } = useAppSelector((state) => state.audit_logs); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof audit_logs === 'object') { + setInitialValues(audit_logs); + } + }, [audit_logs]); + + useEffect(() => { + if (typeof audit_logs === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = audit_logs[el]), + ); + setInitialValues(newInitialVal); + } + }, [audit_logs]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/audit_logs/audit_logs-list'); + }; + + return ( + <> + + {getPageTitle('Edit audit_logs')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/audit_logs/audit_logs-list')} + /> + + +
    +
    +
    + + ); +}; + +EditAudit_logsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditAudit_logsPage; diff --git a/frontend/src/pages/audit_logs/audit_logs-list.tsx b/frontend/src/pages/audit_logs/audit_logs-list.tsx new file mode 100644 index 0000000..cafcd5e --- /dev/null +++ b/frontend/src/pages/audit_logs/audit_logs-list.tsx @@ -0,0 +1,173 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableAudit_logs from '../../components/Audit_logs/TableAudit_logs'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/audit_logs/audit_logsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Audit_logsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Action', title: 'action' }, + { label: 'Entity', title: 'entity' }, + { label: 'DiffJSON', title: 'diff_json' }, + { label: 'EntityID', title: 'entity_id', number: 'true' }, + + { label: 'User', title: 'user' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_AUDIT_LOGS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getAudit_logsCSV = async () => { + const response = await axios({ + url: '/audit_logs?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'audit_logsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Audit_logs')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + Switch to Table +
    +
    + + + + +
    + + + + + ); +}; + +Audit_logsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Audit_logsTablesPage; diff --git a/frontend/src/pages/audit_logs/audit_logs-new.tsx b/frontend/src/pages/audit_logs/audit_logs-new.tsx new file mode 100644 index 0000000..1f63e3f --- /dev/null +++ b/frontend/src/pages/audit_logs/audit_logs-new.tsx @@ -0,0 +1,152 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/audit_logs/audit_logsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + user: '', + + action: '', + + entity: '', + + entity_id: '', + + diff_json: '', + + organizations: '', +}; + +const Audit_logsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/audit_logs/audit_logs-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/audit_logs/audit_logs-list')} + /> + + +
    +
    +
    + + ); +}; + +Audit_logsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Audit_logsNew; diff --git a/frontend/src/pages/audit_logs/audit_logs-table.tsx b/frontend/src/pages/audit_logs/audit_logs-table.tsx new file mode 100644 index 0000000..2bf8645 --- /dev/null +++ b/frontend/src/pages/audit_logs/audit_logs-table.tsx @@ -0,0 +1,172 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableAudit_logs from '../../components/Audit_logs/TableAudit_logs'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/audit_logs/audit_logsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Audit_logsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Action', title: 'action' }, + { label: 'Entity', title: 'entity' }, + { label: 'DiffJSON', title: 'diff_json' }, + { label: 'EntityID', title: 'entity_id', number: 'true' }, + + { label: 'User', title: 'user' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_AUDIT_LOGS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getAudit_logsCSV = async () => { + const response = await axios({ + url: '/audit_logs?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'audit_logsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Audit_logs')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Audit_logsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Audit_logsTablesPage; diff --git a/frontend/src/pages/audit_logs/audit_logs-view.tsx b/frontend/src/pages/audit_logs/audit_logs-view.tsx new file mode 100644 index 0000000..27795bb --- /dev/null +++ b/frontend/src/pages/audit_logs/audit_logs-view.tsx @@ -0,0 +1,120 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/audit_logs/audit_logsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Audit_logsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { audit_logs } = useAppSelector((state) => state.audit_logs); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View audit_logs')} + + + + + + +
    +

    Organization

    + +

    {audit_logs?.org?.name ?? 'No data'}

    +
    + +
    +

    User

    + +

    {audit_logs?.user?.firstName ?? 'No data'}

    +
    + +
    +

    Action

    +

    {audit_logs?.action}

    +
    + +
    +

    Entity

    +

    {audit_logs?.entity}

    +
    + +
    +

    EntityID

    +

    {audit_logs?.entity_id || 'No data'}

    +
    + +
    +

    DiffJSON

    +

    {audit_logs?.diff_json}

    +
    + +
    +

    organizations

    + +

    {audit_logs?.organizations?.name ?? 'No data'}

    +
    + + + + router.push('/audit_logs/audit_logs-list')} + /> +
    +
    + + ); +}; + +Audit_logsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Audit_logsView; diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx new file mode 100644 index 0000000..983327e --- /dev/null +++ b/frontend/src/pages/dashboard.tsx @@ -0,0 +1,1211 @@ +import * as icon from '@mdi/js'; +import Head from 'next/head'; +import React from 'react'; +import axios from 'axios'; +import type { ReactElement } from 'react'; +import LayoutAuthenticated from '../layouts/Authenticated'; +import SectionMain from '../components/SectionMain'; +import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; +import BaseIcon from '../components/BaseIcon'; +import { getPageTitle } from '../config'; +import Link from 'next/link'; +import { useTranslation } from 'next-i18next'; + +import { hasPermission } from '../helpers/userPermissions'; +import { fetchWidgets } from '../stores/roles/rolesSlice'; +import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator'; +import { SmartWidget } from '../components/SmartWidget/SmartWidget'; + +import { useAppDispatch, useAppSelector } from '../stores/hooks'; +const Dashboard = () => { + const { t } = useTranslation('common'); + const dispatch = useAppDispatch(); + const iconsColor = useAppSelector((state) => state.style.iconsColor); + const corners = useAppSelector((state) => state.style.corners); + const cardsStyle = useAppSelector((state) => state.style.cardsStyle); + + const loadingMessage = t('pages.dashboard.loading', { + defaultValue: 'Loading...', + }); + + const [users, setUsers] = React.useState(loadingMessage); + const [audit_logs, setAudit_logs] = React.useState(loadingMessage); + const [data_connections, setData_connections] = + React.useState(loadingMessage); + const [fin_accounts, setFin_accounts] = React.useState(loadingMessage); + const [fin_ap, setFin_ap] = React.useState(loadingMessage); + const [fin_ar, setFin_ar] = React.useState(loadingMessage); + const [fin_budgets, setFin_budgets] = React.useState(loadingMessage); + const [fin_customers, setFin_customers] = React.useState(loadingMessage); + const [fin_forecasts, setFin_forecasts] = React.useState(loadingMessage); + const [fin_transactions, setFin_transactions] = + React.useState(loadingMessage); + const [fin_vendors, setFin_vendors] = React.useState(loadingMessage); + const [hr_applicants, setHr_applicants] = React.useState(loadingMessage); + const [hr_attendance, setHr_attendance] = React.useState(loadingMessage); + const [hr_employees, setHr_employees] = React.useState(loadingMessage); + const [hr_payroll, setHr_payroll] = React.useState(loadingMessage); + const [hr_positions, setHr_positions] = React.useState(loadingMessage); + const [hr_reqs, setHr_reqs] = React.useState(loadingMessage); + const [ops_incidents, setOps_incidents] = React.useState(loadingMessage); + const [ops_inventory, setOps_inventory] = React.useState(loadingMessage); + const [ops_lines, setOps_lines] = React.useState(loadingMessage); + const [ops_orders, setOps_orders] = React.useState(loadingMessage); + const [ops_plants, setOps_plants] = React.useState(loadingMessage); + const [ops_production_log, setOps_production_log] = + React.useState(loadingMessage); + const [ops_shipments, setOps_shipments] = React.useState(loadingMessage); + const [roles, setRoles] = React.useState(loadingMessage); + const [permissions, setPermissions] = React.useState(loadingMessage); + const [organizations, setOrganizations] = React.useState(loadingMessage); + + const [widgetsRole, setWidgetsRole] = React.useState({ + role: { value: '', label: '' }, + }); + const { currentUser } = useAppSelector((state) => state.auth); + const { isFetchingQuery } = useAppSelector((state) => state.openAi); + + const { rolesWidgets, loading } = useAppSelector((state) => state.roles); + + const organizationId = currentUser?.organizations?.id; + + async function loadData() { + const entities = [ + 'users', + 'audit_logs', + 'data_connections', + 'fin_accounts', + 'fin_ap', + 'fin_ar', + 'fin_budgets', + 'fin_customers', + 'fin_forecasts', + 'fin_transactions', + 'fin_vendors', + 'hr_applicants', + 'hr_attendance', + 'hr_employees', + 'hr_payroll', + 'hr_positions', + 'hr_reqs', + 'ops_incidents', + 'ops_inventory', + 'ops_lines', + 'ops_orders', + 'ops_plants', + 'ops_production_log', + 'ops_shipments', + 'roles', + 'permissions', + 'organizations', + ]; + const fns = [ + setUsers, + setAudit_logs, + setData_connections, + setFin_accounts, + setFin_ap, + setFin_ar, + setFin_budgets, + setFin_customers, + setFin_forecasts, + setFin_transactions, + setFin_vendors, + setHr_applicants, + setHr_attendance, + setHr_employees, + setHr_payroll, + setHr_positions, + setHr_reqs, + setOps_incidents, + setOps_inventory, + setOps_lines, + setOps_orders, + setOps_plants, + setOps_production_log, + setOps_shipments, + setRoles, + setPermissions, + setOrganizations, + ]; + + const requests = entities.map((entity, index) => { + if (hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) { + return axios.get(`/${entity.toLowerCase()}/count`); + } else { + fns[index](null); + return Promise.resolve({ data: { count: null } }); + } + }); + + Promise.allSettled(requests).then((results) => { + results.forEach((result, i) => { + if (result.status === 'fulfilled') { + fns[i](result.value.data.count); + } else { + fns[i](result.reason.message); + } + }); + }); + } + + async function getWidgets(roleId) { + await dispatch(fetchWidgets(roleId)); + } + React.useEffect(() => { + if (!currentUser) return; + loadData().then(); + setWidgetsRole({ + role: { + value: currentUser?.app_role?.id, + label: currentUser?.app_role?.name, + }, + }); + }, [currentUser]); + + React.useEffect(() => { + if (!currentUser || !widgetsRole?.role?.value) return; + getWidgets(widgetsRole?.role?.value || '').then(); + }, [widgetsRole?.role?.value]); + + return ( + <> + + + {getPageTitle( + t('pages.dashboard.pageTitle', { defaultValue: 'Overview' }), + )} + + + + + {''} + + + {hasPermission(currentUser, 'CREATE_ROLES') && ( + + )} + {!!rolesWidgets.length && + hasPermission(currentUser, 'CREATE_ROLES') && ( +

    + {`${widgetsRole?.role?.label || 'Users'}'s widgets`} +

    + )} + +
    + {(isFetchingQuery || loading) && ( +
    + {' '} + {t('pages.dashboard.loadingWidgets', { + defaultValue: 'Loading widgets...', + })} +
    + )} + + {rolesWidgets && + rolesWidgets.map((widget) => ( + + ))} +
    + + {!!rolesWidgets.length && ( +
    + )} + +
    + {hasPermission(currentUser, 'READ_USERS') && ( + +
    +
    +
    +
    + Users +
    +
    + {users} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_AUDIT_LOGS') && ( + +
    +
    +
    +
    + Audit logs +
    +
    + {audit_logs} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_DATA_CONNECTIONS') && ( + +
    +
    +
    +
    + Data connections +
    +
    + {data_connections} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_FIN_ACCOUNTS') && ( + +
    +
    +
    +
    + Fin accounts +
    +
    + {fin_accounts} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_FIN_AP') && ( + +
    +
    +
    +
    + Fin ap +
    +
    + {fin_ap} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_FIN_AR') && ( + +
    +
    +
    +
    + Fin ar +
    +
    + {fin_ar} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_FIN_BUDGETS') && ( + +
    +
    +
    +
    + Fin budgets +
    +
    + {fin_budgets} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_FIN_CUSTOMERS') && ( + +
    +
    +
    +
    + Fin customers +
    +
    + {fin_customers} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_FIN_FORECASTS') && ( + +
    +
    +
    +
    + Fin forecasts +
    +
    + {fin_forecasts} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_FIN_TRANSACTIONS') && ( + +
    +
    +
    +
    + Fin transactions +
    +
    + {fin_transactions} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_FIN_VENDORS') && ( + +
    +
    +
    +
    + Fin vendors +
    +
    + {fin_vendors} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_HR_APPLICANTS') && ( + +
    +
    +
    +
    + Hr applicants +
    +
    + {hr_applicants} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_HR_ATTENDANCE') && ( + +
    +
    +
    +
    + Hr attendance +
    +
    + {hr_attendance} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_HR_EMPLOYEES') && ( + +
    +
    +
    +
    + Hr employees +
    +
    + {hr_employees} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_HR_PAYROLL') && ( + +
    +
    +
    +
    + Hr payroll +
    +
    + {hr_payroll} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_HR_POSITIONS') && ( + +
    +
    +
    +
    + Hr positions +
    +
    + {hr_positions} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_HR_REQS') && ( + +
    +
    +
    +
    + Hr reqs +
    +
    + {hr_reqs} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_OPS_INCIDENTS') && ( + +
    +
    +
    +
    + Ops incidents +
    +
    + {ops_incidents} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_OPS_INVENTORY') && ( + +
    +
    +
    +
    + Ops inventory +
    +
    + {ops_inventory} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_OPS_LINES') && ( + +
    +
    +
    +
    + Ops lines +
    +
    + {ops_lines} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_OPS_ORDERS') && ( + +
    +
    +
    +
    + Ops orders +
    +
    + {ops_orders} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_OPS_PLANTS') && ( + +
    +
    +
    +
    + Ops plants +
    +
    + {ops_plants} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_OPS_PRODUCTION_LOG') && ( + +
    +
    +
    +
    + Ops production log +
    +
    + {ops_production_log} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_OPS_SHIPMENTS') && ( + +
    +
    +
    +
    + Ops shipments +
    +
    + {ops_shipments} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_ROLES') && ( + +
    +
    +
    +
    + Roles +
    +
    + {roles} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_PERMISSIONS') && ( + +
    +
    +
    +
    + Permissions +
    +
    + {permissions} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_ORGANIZATIONS') && ( + +
    +
    +
    +
    + Organizations +
    +
    + {organizations} +
    +
    +
    + +
    +
    +
    + + )} +
    +
    + + ); +}; + +Dashboard.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default Dashboard; diff --git a/frontend/src/pages/data_connections/[data_connectionsId].tsx b/frontend/src/pages/data_connections/[data_connectionsId].tsx new file mode 100644 index 0000000..93cf5b6 --- /dev/null +++ b/frontend/src/pages/data_connections/[data_connectionsId].tsx @@ -0,0 +1,214 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { + update, + fetch, +} from '../../stores/data_connections/data_connectionsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditData_connections = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + type: '', + + name: '', + + config_json: '', + + status: '', + + last_sync_at: new Date(), + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { data_connections } = useAppSelector( + (state) => state.data_connections, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { data_connectionsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: data_connectionsId })); + }, [data_connectionsId]); + + useEffect(() => { + if (typeof data_connections === 'object') { + setInitialValues(data_connections); + } + }, [data_connections]); + + useEffect(() => { + if (typeof data_connections === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = data_connections[el]), + ); + + setInitialValues(newInitialVal); + } + }, [data_connections]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: data_connectionsId, data })); + await router.push('/data_connections/data_connections-list'); + }; + + return ( + <> + + {getPageTitle('Edit data_connections')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setInitialValues({ ...initialValues, last_sync_at: date }) + } + /> + + + + + + + + + + + + router.push('/data_connections/data_connections-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditData_connections.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditData_connections; diff --git a/frontend/src/pages/data_connections/data_connections-edit.tsx b/frontend/src/pages/data_connections/data_connections-edit.tsx new file mode 100644 index 0000000..3418c69 --- /dev/null +++ b/frontend/src/pages/data_connections/data_connections-edit.tsx @@ -0,0 +1,212 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { + update, + fetch, +} from '../../stores/data_connections/data_connectionsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditData_connectionsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + type: '', + + name: '', + + config_json: '', + + status: '', + + last_sync_at: new Date(), + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { data_connections } = useAppSelector( + (state) => state.data_connections, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof data_connections === 'object') { + setInitialValues(data_connections); + } + }, [data_connections]); + + useEffect(() => { + if (typeof data_connections === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = data_connections[el]), + ); + setInitialValues(newInitialVal); + } + }, [data_connections]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/data_connections/data_connections-list'); + }; + + return ( + <> + + {getPageTitle('Edit data_connections')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setInitialValues({ ...initialValues, last_sync_at: date }) + } + /> + + + + + + + + + + + + router.push('/data_connections/data_connections-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditData_connectionsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditData_connectionsPage; diff --git a/frontend/src/pages/data_connections/data_connections-list.tsx b/frontend/src/pages/data_connections/data_connections-list.tsx new file mode 100644 index 0000000..ef1bdef --- /dev/null +++ b/frontend/src/pages/data_connections/data_connections-list.tsx @@ -0,0 +1,189 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableData_connections from '../../components/Data_connections/TableData_connections'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/data_connections/data_connectionsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Data_connectionsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'ConfigJSON', title: 'config_json' }, + + { label: 'LastSyncAt', title: 'last_sync_at', date: 'true' }, + + { + label: 'Type', + title: 'type', + type: 'enum', + options: ['Snowflake', 'BigQuery', 'Redshift', 'Postgres'], + }, + { + label: 'Status', + title: 'status', + type: 'enum', + options: ['Active', 'Inactive'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_DATA_CONNECTIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getData_connectionsCSV = async () => { + const response = await axios({ + url: '/data_connections?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'data_connectionsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Data_connections')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + + Switch to Table + +
    +
    + + + + +
    + + + + + ); +}; + +Data_connectionsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Data_connectionsTablesPage; diff --git a/frontend/src/pages/data_connections/data_connections-new.tsx b/frontend/src/pages/data_connections/data_connections-new.tsx new file mode 100644 index 0000000..583d067 --- /dev/null +++ b/frontend/src/pages/data_connections/data_connections-new.tsx @@ -0,0 +1,164 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/data_connections/data_connectionsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + type: 'Snowflake', + + name: '', + + config_json: '', + + status: 'Active', + + last_sync_at: '', + + organizations: '', +}; + +const Data_connectionsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/data_connections/data_connections-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/data_connections/data_connections-list') + } + /> + + +
    +
    +
    + + ); +}; + +Data_connectionsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Data_connectionsNew; diff --git a/frontend/src/pages/data_connections/data_connections-table.tsx b/frontend/src/pages/data_connections/data_connections-table.tsx new file mode 100644 index 0000000..488b8dc --- /dev/null +++ b/frontend/src/pages/data_connections/data_connections-table.tsx @@ -0,0 +1,186 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableData_connections from '../../components/Data_connections/TableData_connections'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/data_connections/data_connectionsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Data_connectionsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'ConfigJSON', title: 'config_json' }, + + { label: 'LastSyncAt', title: 'last_sync_at', date: 'true' }, + + { + label: 'Type', + title: 'type', + type: 'enum', + options: ['Snowflake', 'BigQuery', 'Redshift', 'Postgres'], + }, + { + label: 'Status', + title: 'status', + type: 'enum', + options: ['Active', 'Inactive'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_DATA_CONNECTIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getData_connectionsCSV = async () => { + const response = await axios({ + url: '/data_connections?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'data_connectionsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Data_connections')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Data_connectionsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Data_connectionsTablesPage; diff --git a/frontend/src/pages/data_connections/data_connections-view.tsx b/frontend/src/pages/data_connections/data_connections-view.tsx new file mode 100644 index 0000000..773af1d --- /dev/null +++ b/frontend/src/pages/data_connections/data_connections-view.tsx @@ -0,0 +1,139 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/data_connections/data_connectionsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Data_connectionsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { data_connections } = useAppSelector( + (state) => state.data_connections, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View data_connections')} + + + + + + +
    +

    Organization

    + +

    {data_connections?.org?.name ?? 'No data'}

    +
    + +
    +

    Type

    +

    {data_connections?.type ?? 'No data'}

    +
    + +
    +

    Name

    +

    {data_connections?.name}

    +
    + +
    +

    ConfigJSON

    +

    {data_connections?.config_json}

    +
    + +
    +

    Status

    +

    {data_connections?.status ?? 'No data'}

    +
    + + + {data_connections.last_sync_at ? ( + + ) : ( +

    No LastSyncAt

    + )} +
    + +
    +

    organizations

    + +

    {data_connections?.organizations?.name ?? 'No data'}

    +
    + + + + + router.push('/data_connections/data_connections-list') + } + /> +
    +
    + + ); +}; + +Data_connectionsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Data_connectionsView; diff --git a/frontend/src/pages/error.tsx b/frontend/src/pages/error.tsx new file mode 100644 index 0000000..0e80c27 --- /dev/null +++ b/frontend/src/pages/error.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import BaseButton from '../components/BaseButton'; +import CardBox from '../components/CardBox'; +import SectionFullScreen from '../components/SectionFullScreen'; +import LayoutGuest from '../layouts/Guest'; +import { getPageTitle } from '../config'; + +export default function Error() { + return ( + <> + + {getPageTitle('Error')} + + + + } + > +
    +

    Unhandled exception

    + +

    An Error Occurred

    +
    +
    +
    + + ); +} + +Error.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/pages/fin_accounts/[fin_accountsId].tsx b/frontend/src/pages/fin_accounts/[fin_accountsId].tsx new file mode 100644 index 0000000..dc175a1 --- /dev/null +++ b/frontend/src/pages/fin_accounts/[fin_accountsId].tsx @@ -0,0 +1,201 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/fin_accounts/fin_accountsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditFin_accounts = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + code: '', + + name: '', + + type: '', + + parent: null, + + is_active: false, + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { fin_accounts } = useAppSelector((state) => state.fin_accounts); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { fin_accountsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: fin_accountsId })); + }, [fin_accountsId]); + + useEffect(() => { + if (typeof fin_accounts === 'object') { + setInitialValues(fin_accounts); + } + }, [fin_accounts]); + + useEffect(() => { + if (typeof fin_accounts === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = fin_accounts[el]), + ); + + setInitialValues(newInitialVal); + } + }, [fin_accounts]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: fin_accountsId, data })); + await router.push('/fin_accounts/fin_accounts-list'); + }; + + return ( + <> + + {getPageTitle('Edit fin_accounts')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_accounts/fin_accounts-list')} + /> + + +
    +
    +
    + + ); +}; + +EditFin_accounts.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditFin_accounts; diff --git a/frontend/src/pages/fin_accounts/fin_accounts-edit.tsx b/frontend/src/pages/fin_accounts/fin_accounts-edit.tsx new file mode 100644 index 0000000..28a3871 --- /dev/null +++ b/frontend/src/pages/fin_accounts/fin_accounts-edit.tsx @@ -0,0 +1,199 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/fin_accounts/fin_accountsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditFin_accountsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + code: '', + + name: '', + + type: '', + + parent: null, + + is_active: false, + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { fin_accounts } = useAppSelector((state) => state.fin_accounts); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof fin_accounts === 'object') { + setInitialValues(fin_accounts); + } + }, [fin_accounts]); + + useEffect(() => { + if (typeof fin_accounts === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = fin_accounts[el]), + ); + setInitialValues(newInitialVal); + } + }, [fin_accounts]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/fin_accounts/fin_accounts-list'); + }; + + return ( + <> + + {getPageTitle('Edit fin_accounts')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_accounts/fin_accounts-list')} + /> + + +
    +
    +
    + + ); +}; + +EditFin_accountsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditFin_accountsPage; diff --git a/frontend/src/pages/fin_accounts/fin_accounts-list.tsx b/frontend/src/pages/fin_accounts/fin_accounts-list.tsx new file mode 100644 index 0000000..e5e490d --- /dev/null +++ b/frontend/src/pages/fin_accounts/fin_accounts-list.tsx @@ -0,0 +1,183 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableFin_accounts from '../../components/Fin_accounts/TableFin_accounts'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/fin_accounts/fin_accountsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_accountsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Code', title: 'code' }, + { label: 'Name', title: 'name' }, + + { label: 'ParentAccount', title: 'parent' }, + + { + label: 'Type', + title: 'type', + type: 'enum', + options: ['asset', 'liability', 'equity', 'revenue', 'expense'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_FIN_ACCOUNTS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getFin_accountsCSV = async () => { + const response = await axios({ + url: '/fin_accounts?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'fin_accountsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Fin_accounts')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + + Switch to Table + +
    +
    + + + + +
    + + + + + ); +}; + +Fin_accountsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_accountsTablesPage; diff --git a/frontend/src/pages/fin_accounts/fin_accounts-new.tsx b/frontend/src/pages/fin_accounts/fin_accounts-new.tsx new file mode 100644 index 0000000..ecce299 --- /dev/null +++ b/frontend/src/pages/fin_accounts/fin_accounts-new.tsx @@ -0,0 +1,166 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/fin_accounts/fin_accountsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + code: '', + + name: '', + + type: 'asset', + + parent: '', + + is_active: false, + + organizations: '', +}; + +const Fin_accountsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/fin_accounts/fin_accounts-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_accounts/fin_accounts-list')} + /> + + +
    +
    +
    + + ); +}; + +Fin_accountsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_accountsNew; diff --git a/frontend/src/pages/fin_accounts/fin_accounts-table.tsx b/frontend/src/pages/fin_accounts/fin_accounts-table.tsx new file mode 100644 index 0000000..2240ccc --- /dev/null +++ b/frontend/src/pages/fin_accounts/fin_accounts-table.tsx @@ -0,0 +1,180 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableFin_accounts from '../../components/Fin_accounts/TableFin_accounts'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/fin_accounts/fin_accountsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_accountsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Code', title: 'code' }, + { label: 'Name', title: 'name' }, + + { label: 'ParentAccount', title: 'parent' }, + + { + label: 'Type', + title: 'type', + type: 'enum', + options: ['asset', 'liability', 'equity', 'revenue', 'expense'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_FIN_ACCOUNTS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getFin_accountsCSV = async () => { + const response = await axios({ + url: '/fin_accounts?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'fin_accountsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Fin_accounts')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Fin_accountsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_accountsTablesPage; diff --git a/frontend/src/pages/fin_accounts/fin_accounts-view.tsx b/frontend/src/pages/fin_accounts/fin_accounts-view.tsx new file mode 100644 index 0000000..59609ed --- /dev/null +++ b/frontend/src/pages/fin_accounts/fin_accounts-view.tsx @@ -0,0 +1,288 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/fin_accounts/fin_accountsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_accountsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { fin_accounts } = useAppSelector((state) => state.fin_accounts); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View fin_accounts')} + + + + + + +
    +

    Organization

    + +

    {fin_accounts?.org?.name ?? 'No data'}

    +
    + +
    +

    Code

    +

    {fin_accounts?.code}

    +
    + +
    +

    Name

    +

    {fin_accounts?.name}

    +
    + +
    +

    Type

    +

    {fin_accounts?.type ?? 'No data'}

    +
    + +
    +

    ParentAccount

    + +

    {fin_accounts?.parent?.name ?? 'No data'}

    +
    + + + null }} + disabled + /> + + +
    +

    organizations

    + +

    {fin_accounts?.organizations?.name ?? 'No data'}

    +
    + + <> +

    Fin_budgets Account

    + +
    + + + + + + + + + + + + + + {fin_accounts.fin_budgets_account && + Array.isArray(fin_accounts.fin_budgets_account) && + fin_accounts.fin_budgets_account.map((item: any) => ( + + router.push( + `/fin_budgets/fin_budgets-view/?id=${item.id}`, + ) + } + > + + + + + + + + + ))} + +
    FiscalYearCostCenterMonthAmount
    {item.fiscal_year}{item.cost_center}{item.month}{item.amount}
    +
    + {!fin_accounts?.fin_budgets_account?.length && ( +
    No data
    + )} +
    + + + <> +

    Fin_forecasts Account

    + +
    + + + + + + + + + + + + + + {fin_accounts.fin_forecasts_account && + Array.isArray(fin_accounts.fin_forecasts_account) && + fin_accounts.fin_forecasts_account.map((item: any) => ( + + router.push( + `/fin_forecasts/fin_forecasts-view/?id=${item.id}`, + ) + } + > + + + + + + + + + ))} + +
    FiscalYearCostCenterMonthAmount
    {item.fiscal_year}{item.cost_center}{item.month}{item.amount}
    +
    + {!fin_accounts?.fin_forecasts_account?.length && ( +
    No data
    + )} +
    + + + <> +

    Fin_transactions Account

    + +
    + + + + + + + + + + + + + + + + + + + + + + {fin_accounts.fin_transactions_account && + Array.isArray(fin_accounts.fin_transactions_account) && + fin_accounts.fin_transactions_account.map((item: any) => ( + + router.push( + `/fin_transactions/fin_transactions-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + + + + + + + ))} + +
    TransactionTimestampAmountCurrencyFXRateCostCenterProjectCodeMemoSource
    + {dataFormatter.dateTimeFormatter(item.tx_ts)} + {item.amount}{item.currency}{item.fx_rate}{item.cost_center}{item.project_code}{item.memo}{item.source}
    +
    + {!fin_accounts?.fin_transactions_account?.length && ( +
    No data
    + )} +
    + + + + + router.push('/fin_accounts/fin_accounts-list')} + /> +
    +
    + + ); +}; + +Fin_accountsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_accountsView; diff --git a/frontend/src/pages/fin_ap/[fin_apId].tsx b/frontend/src/pages/fin_ap/[fin_apId].tsx new file mode 100644 index 0000000..aa538ea --- /dev/null +++ b/frontend/src/pages/fin_ap/[fin_apId].tsx @@ -0,0 +1,206 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/fin_ap/fin_apSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditFin_ap = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + bill_no: '', + + vendor: null, + + due_date: new Date(), + + amount: '', + + status: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { fin_ap } = useAppSelector((state) => state.fin_ap); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { fin_apId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: fin_apId })); + }, [fin_apId]); + + useEffect(() => { + if (typeof fin_ap === 'object') { + setInitialValues(fin_ap); + } + }, [fin_ap]); + + useEffect(() => { + if (typeof fin_ap === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = fin_ap[el])); + + setInitialValues(newInitialVal); + } + }, [fin_ap]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: fin_apId, data })); + await router.push('/fin_ap/fin_ap-list'); + }; + + return ( + <> + + {getPageTitle('Edit fin_ap')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + setInitialValues({ ...initialValues, due_date: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_ap/fin_ap-list')} + /> + + +
    +
    +
    + + ); +}; + +EditFin_ap.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditFin_ap; diff --git a/frontend/src/pages/fin_ap/fin_ap-edit.tsx b/frontend/src/pages/fin_ap/fin_ap-edit.tsx new file mode 100644 index 0000000..de60997 --- /dev/null +++ b/frontend/src/pages/fin_ap/fin_ap-edit.tsx @@ -0,0 +1,204 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/fin_ap/fin_apSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditFin_apPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + bill_no: '', + + vendor: null, + + due_date: new Date(), + + amount: '', + + status: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { fin_ap } = useAppSelector((state) => state.fin_ap); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof fin_ap === 'object') { + setInitialValues(fin_ap); + } + }, [fin_ap]); + + useEffect(() => { + if (typeof fin_ap === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = fin_ap[el])); + setInitialValues(newInitialVal); + } + }, [fin_ap]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/fin_ap/fin_ap-list'); + }; + + return ( + <> + + {getPageTitle('Edit fin_ap')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + setInitialValues({ ...initialValues, due_date: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_ap/fin_ap-list')} + /> + + +
    +
    +
    + + ); +}; + +EditFin_apPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditFin_apPage; diff --git a/frontend/src/pages/fin_ap/fin_ap-list.tsx b/frontend/src/pages/fin_ap/fin_ap-list.tsx new file mode 100644 index 0000000..4794d7a --- /dev/null +++ b/frontend/src/pages/fin_ap/fin_ap-list.tsx @@ -0,0 +1,178 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableFin_ap from '../../components/Fin_ap/TableFin_ap'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/fin_ap/fin_apSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_apTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'BillNumber', title: 'bill_no' }, + + { label: 'Amount', title: 'amount', number: 'true' }, + { label: 'DueDate', title: 'due_date', date: 'true' }, + + { label: 'Vendor', title: 'vendor' }, + + { + label: 'Status', + title: 'status', + type: 'enum', + options: ['Pending', 'Paid', 'Overdue'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_FIN_AP'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getFin_apCSV = async () => { + const response = await axios({ + url: '/fin_ap?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'fin_apCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Fin_ap')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + Switch to Table +
    +
    + + + + +
    + + + + + ); +}; + +Fin_apTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default Fin_apTablesPage; diff --git a/frontend/src/pages/fin_ap/fin_ap-new.tsx b/frontend/src/pages/fin_ap/fin_ap-new.tsx new file mode 100644 index 0000000..5ceadbb --- /dev/null +++ b/frontend/src/pages/fin_ap/fin_ap-new.tsx @@ -0,0 +1,162 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/fin_ap/fin_apSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + bill_no: '', + + vendor: '', + + due_date: '', + + amount: '', + + status: 'Pending', + + organizations: '', +}; + +const Fin_apNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/fin_ap/fin_ap-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_ap/fin_ap-list')} + /> + + +
    +
    +
    + + ); +}; + +Fin_apNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_apNew; diff --git a/frontend/src/pages/fin_ap/fin_ap-table.tsx b/frontend/src/pages/fin_ap/fin_ap-table.tsx new file mode 100644 index 0000000..012f821 --- /dev/null +++ b/frontend/src/pages/fin_ap/fin_ap-table.tsx @@ -0,0 +1,177 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableFin_ap from '../../components/Fin_ap/TableFin_ap'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/fin_ap/fin_apSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_apTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'BillNumber', title: 'bill_no' }, + + { label: 'Amount', title: 'amount', number: 'true' }, + { label: 'DueDate', title: 'due_date', date: 'true' }, + + { label: 'Vendor', title: 'vendor' }, + + { + label: 'Status', + title: 'status', + type: 'enum', + options: ['Pending', 'Paid', 'Overdue'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_FIN_AP'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getFin_apCSV = async () => { + const response = await axios({ + url: '/fin_ap?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'fin_apCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Fin_ap')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Fin_apTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default Fin_apTablesPage; diff --git a/frontend/src/pages/fin_ap/fin_ap-view.tsx b/frontend/src/pages/fin_ap/fin_ap-view.tsx new file mode 100644 index 0000000..81075bd --- /dev/null +++ b/frontend/src/pages/fin_ap/fin_ap-view.tsx @@ -0,0 +1,132 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/fin_ap/fin_apSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_apView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { fin_ap } = useAppSelector((state) => state.fin_ap); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View fin_ap')} + + + + + + +
    +

    Organization

    + +

    {fin_ap?.org?.name ?? 'No data'}

    +
    + +
    +

    BillNumber

    +

    {fin_ap?.bill_no}

    +
    + +
    +

    Vendor

    + +

    {fin_ap?.vendor?.name ?? 'No data'}

    +
    + + + {fin_ap.due_date ? ( + + ) : ( +

    No DueDate

    + )} +
    + +
    +

    Amount

    +

    {fin_ap?.amount || 'No data'}

    +
    + +
    +

    Status

    +

    {fin_ap?.status ?? 'No data'}

    +
    + +
    +

    organizations

    + +

    {fin_ap?.organizations?.name ?? 'No data'}

    +
    + + + + router.push('/fin_ap/fin_ap-list')} + /> +
    +
    + + ); +}; + +Fin_apView.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default Fin_apView; diff --git a/frontend/src/pages/fin_ar/[fin_arId].tsx b/frontend/src/pages/fin_ar/[fin_arId].tsx new file mode 100644 index 0000000..68f32c8 --- /dev/null +++ b/frontend/src/pages/fin_ar/[fin_arId].tsx @@ -0,0 +1,206 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/fin_ar/fin_arSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditFin_ar = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + invoice_no: '', + + customer: null, + + due_date: new Date(), + + amount: '', + + status: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { fin_ar } = useAppSelector((state) => state.fin_ar); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { fin_arId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: fin_arId })); + }, [fin_arId]); + + useEffect(() => { + if (typeof fin_ar === 'object') { + setInitialValues(fin_ar); + } + }, [fin_ar]); + + useEffect(() => { + if (typeof fin_ar === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = fin_ar[el])); + + setInitialValues(newInitialVal); + } + }, [fin_ar]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: fin_arId, data })); + await router.push('/fin_ar/fin_ar-list'); + }; + + return ( + <> + + {getPageTitle('Edit fin_ar')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + setInitialValues({ ...initialValues, due_date: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_ar/fin_ar-list')} + /> + + +
    +
    +
    + + ); +}; + +EditFin_ar.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditFin_ar; diff --git a/frontend/src/pages/fin_ar/fin_ar-edit.tsx b/frontend/src/pages/fin_ar/fin_ar-edit.tsx new file mode 100644 index 0000000..458dc98 --- /dev/null +++ b/frontend/src/pages/fin_ar/fin_ar-edit.tsx @@ -0,0 +1,204 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/fin_ar/fin_arSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditFin_arPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + invoice_no: '', + + customer: null, + + due_date: new Date(), + + amount: '', + + status: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { fin_ar } = useAppSelector((state) => state.fin_ar); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof fin_ar === 'object') { + setInitialValues(fin_ar); + } + }, [fin_ar]); + + useEffect(() => { + if (typeof fin_ar === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = fin_ar[el])); + setInitialValues(newInitialVal); + } + }, [fin_ar]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/fin_ar/fin_ar-list'); + }; + + return ( + <> + + {getPageTitle('Edit fin_ar')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + setInitialValues({ ...initialValues, due_date: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_ar/fin_ar-list')} + /> + + +
    +
    +
    + + ); +}; + +EditFin_arPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditFin_arPage; diff --git a/frontend/src/pages/fin_ar/fin_ar-list.tsx b/frontend/src/pages/fin_ar/fin_ar-list.tsx new file mode 100644 index 0000000..a485777 --- /dev/null +++ b/frontend/src/pages/fin_ar/fin_ar-list.tsx @@ -0,0 +1,178 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableFin_ar from '../../components/Fin_ar/TableFin_ar'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/fin_ar/fin_arSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_arTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'InvoiceNumber', title: 'invoice_no' }, + + { label: 'Amount', title: 'amount', number: 'true' }, + { label: 'DueDate', title: 'due_date', date: 'true' }, + + { label: 'Customer', title: 'customer' }, + + { + label: 'Status', + title: 'status', + type: 'enum', + options: ['Pending', 'Paid', 'Overdue'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_FIN_AR'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getFin_arCSV = async () => { + const response = await axios({ + url: '/fin_ar?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'fin_arCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Fin_ar')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + Switch to Table +
    +
    + + + + +
    + + + + + ); +}; + +Fin_arTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default Fin_arTablesPage; diff --git a/frontend/src/pages/fin_ar/fin_ar-new.tsx b/frontend/src/pages/fin_ar/fin_ar-new.tsx new file mode 100644 index 0000000..e94988d --- /dev/null +++ b/frontend/src/pages/fin_ar/fin_ar-new.tsx @@ -0,0 +1,162 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/fin_ar/fin_arSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + invoice_no: '', + + customer: '', + + due_date: '', + + amount: '', + + status: 'Pending', + + organizations: '', +}; + +const Fin_arNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/fin_ar/fin_ar-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_ar/fin_ar-list')} + /> + + +
    +
    +
    + + ); +}; + +Fin_arNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_arNew; diff --git a/frontend/src/pages/fin_ar/fin_ar-table.tsx b/frontend/src/pages/fin_ar/fin_ar-table.tsx new file mode 100644 index 0000000..f357d5c --- /dev/null +++ b/frontend/src/pages/fin_ar/fin_ar-table.tsx @@ -0,0 +1,177 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableFin_ar from '../../components/Fin_ar/TableFin_ar'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/fin_ar/fin_arSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_arTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'InvoiceNumber', title: 'invoice_no' }, + + { label: 'Amount', title: 'amount', number: 'true' }, + { label: 'DueDate', title: 'due_date', date: 'true' }, + + { label: 'Customer', title: 'customer' }, + + { + label: 'Status', + title: 'status', + type: 'enum', + options: ['Pending', 'Paid', 'Overdue'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_FIN_AR'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getFin_arCSV = async () => { + const response = await axios({ + url: '/fin_ar?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'fin_arCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Fin_ar')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Fin_arTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default Fin_arTablesPage; diff --git a/frontend/src/pages/fin_ar/fin_ar-view.tsx b/frontend/src/pages/fin_ar/fin_ar-view.tsx new file mode 100644 index 0000000..2c10056 --- /dev/null +++ b/frontend/src/pages/fin_ar/fin_ar-view.tsx @@ -0,0 +1,132 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/fin_ar/fin_arSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_arView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { fin_ar } = useAppSelector((state) => state.fin_ar); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View fin_ar')} + + + + + + +
    +

    Organization

    + +

    {fin_ar?.org?.name ?? 'No data'}

    +
    + +
    +

    InvoiceNumber

    +

    {fin_ar?.invoice_no}

    +
    + +
    +

    Customer

    + +

    {fin_ar?.customer?.name ?? 'No data'}

    +
    + + + {fin_ar.due_date ? ( + + ) : ( +

    No DueDate

    + )} +
    + +
    +

    Amount

    +

    {fin_ar?.amount || 'No data'}

    +
    + +
    +

    Status

    +

    {fin_ar?.status ?? 'No data'}

    +
    + +
    +

    organizations

    + +

    {fin_ar?.organizations?.name ?? 'No data'}

    +
    + + + + router.push('/fin_ar/fin_ar-list')} + /> +
    +
    + + ); +}; + +Fin_arView.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default Fin_arView; diff --git a/frontend/src/pages/fin_budgets/[fin_budgetsId].tsx b/frontend/src/pages/fin_budgets/[fin_budgetsId].tsx new file mode 100644 index 0000000..d554562 --- /dev/null +++ b/frontend/src/pages/fin_budgets/[fin_budgetsId].tsx @@ -0,0 +1,191 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/fin_budgets/fin_budgetsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditFin_budgets = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + fiscal_year: '', + + cost_center: '', + + account: null, + + month: '', + + amount: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { fin_budgets } = useAppSelector((state) => state.fin_budgets); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { fin_budgetsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: fin_budgetsId })); + }, [fin_budgetsId]); + + useEffect(() => { + if (typeof fin_budgets === 'object') { + setInitialValues(fin_budgets); + } + }, [fin_budgets]); + + useEffect(() => { + if (typeof fin_budgets === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = fin_budgets[el]), + ); + + setInitialValues(newInitialVal); + } + }, [fin_budgets]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: fin_budgetsId, data })); + await router.push('/fin_budgets/fin_budgets-list'); + }; + + return ( + <> + + {getPageTitle('Edit fin_budgets')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_budgets/fin_budgets-list')} + /> + + +
    +
    +
    + + ); +}; + +EditFin_budgets.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditFin_budgets; diff --git a/frontend/src/pages/fin_budgets/fin_budgets-edit.tsx b/frontend/src/pages/fin_budgets/fin_budgets-edit.tsx new file mode 100644 index 0000000..34a87d4 --- /dev/null +++ b/frontend/src/pages/fin_budgets/fin_budgets-edit.tsx @@ -0,0 +1,189 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/fin_budgets/fin_budgetsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditFin_budgetsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + fiscal_year: '', + + cost_center: '', + + account: null, + + month: '', + + amount: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { fin_budgets } = useAppSelector((state) => state.fin_budgets); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof fin_budgets === 'object') { + setInitialValues(fin_budgets); + } + }, [fin_budgets]); + + useEffect(() => { + if (typeof fin_budgets === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = fin_budgets[el]), + ); + setInitialValues(newInitialVal); + } + }, [fin_budgets]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/fin_budgets/fin_budgets-list'); + }; + + return ( + <> + + {getPageTitle('Edit fin_budgets')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_budgets/fin_budgets-list')} + /> + + +
    +
    +
    + + ); +}; + +EditFin_budgetsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditFin_budgetsPage; diff --git a/frontend/src/pages/fin_budgets/fin_budgets-list.tsx b/frontend/src/pages/fin_budgets/fin_budgets-list.tsx new file mode 100644 index 0000000..f0c5a07 --- /dev/null +++ b/frontend/src/pages/fin_budgets/fin_budgets-list.tsx @@ -0,0 +1,176 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableFin_budgets from '../../components/Fin_budgets/TableFin_budgets'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/fin_budgets/fin_budgetsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_budgetsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'CostCenter', title: 'cost_center' }, + { label: 'FiscalYear', title: 'fiscal_year', number: 'true' }, + { label: 'Month', title: 'month', number: 'true' }, + { label: 'Amount', title: 'amount', number: 'true' }, + + { label: 'Account', title: 'account' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_FIN_BUDGETS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getFin_budgetsCSV = async () => { + const response = await axios({ + url: '/fin_budgets?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'fin_budgetsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Fin_budgets')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + Switch to Table +
    +
    + + + + +
    + + + + + ); +}; + +Fin_budgetsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_budgetsTablesPage; diff --git a/frontend/src/pages/fin_budgets/fin_budgets-new.tsx b/frontend/src/pages/fin_budgets/fin_budgets-new.tsx new file mode 100644 index 0000000..084e6da --- /dev/null +++ b/frontend/src/pages/fin_budgets/fin_budgets-new.tsx @@ -0,0 +1,156 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/fin_budgets/fin_budgetsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + fiscal_year: '', + + cost_center: '', + + account: '', + + month: '', + + amount: '', + + organizations: '', +}; + +const Fin_budgetsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/fin_budgets/fin_budgets-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_budgets/fin_budgets-list')} + /> + + +
    +
    +
    + + ); +}; + +Fin_budgetsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_budgetsNew; diff --git a/frontend/src/pages/fin_budgets/fin_budgets-table.tsx b/frontend/src/pages/fin_budgets/fin_budgets-table.tsx new file mode 100644 index 0000000..b6379ee --- /dev/null +++ b/frontend/src/pages/fin_budgets/fin_budgets-table.tsx @@ -0,0 +1,175 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableFin_budgets from '../../components/Fin_budgets/TableFin_budgets'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/fin_budgets/fin_budgetsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_budgetsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'CostCenter', title: 'cost_center' }, + { label: 'FiscalYear', title: 'fiscal_year', number: 'true' }, + { label: 'Month', title: 'month', number: 'true' }, + { label: 'Amount', title: 'amount', number: 'true' }, + + { label: 'Account', title: 'account' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_FIN_BUDGETS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getFin_budgetsCSV = async () => { + const response = await axios({ + url: '/fin_budgets?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'fin_budgetsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Fin_budgets')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Fin_budgetsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_budgetsTablesPage; diff --git a/frontend/src/pages/fin_budgets/fin_budgets-view.tsx b/frontend/src/pages/fin_budgets/fin_budgets-view.tsx new file mode 100644 index 0000000..b5d26a9 --- /dev/null +++ b/frontend/src/pages/fin_budgets/fin_budgets-view.tsx @@ -0,0 +1,120 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/fin_budgets/fin_budgetsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_budgetsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { fin_budgets } = useAppSelector((state) => state.fin_budgets); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View fin_budgets')} + + + + + + +
    +

    Organization

    + +

    {fin_budgets?.org?.name ?? 'No data'}

    +
    + +
    +

    FiscalYear

    +

    {fin_budgets?.fiscal_year || 'No data'}

    +
    + +
    +

    CostCenter

    +

    {fin_budgets?.cost_center}

    +
    + +
    +

    Account

    + +

    {fin_budgets?.account?.name ?? 'No data'}

    +
    + +
    +

    Month

    +

    {fin_budgets?.month || 'No data'}

    +
    + +
    +

    Amount

    +

    {fin_budgets?.amount || 'No data'}

    +
    + +
    +

    organizations

    + +

    {fin_budgets?.organizations?.name ?? 'No data'}

    +
    + + + + router.push('/fin_budgets/fin_budgets-list')} + /> +
    +
    + + ); +}; + +Fin_budgetsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_budgetsView; diff --git a/frontend/src/pages/fin_customers/[fin_customersId].tsx b/frontend/src/pages/fin_customers/[fin_customersId].tsx new file mode 100644 index 0000000..469cd01 --- /dev/null +++ b/frontend/src/pages/fin_customers/[fin_customersId].tsx @@ -0,0 +1,170 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/fin_customers/fin_customersSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditFin_customers = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + name: '', + + segment: '', + + country: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { fin_customers } = useAppSelector((state) => state.fin_customers); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { fin_customersId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: fin_customersId })); + }, [fin_customersId]); + + useEffect(() => { + if (typeof fin_customers === 'object') { + setInitialValues(fin_customers); + } + }, [fin_customers]); + + useEffect(() => { + if (typeof fin_customers === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = fin_customers[el]), + ); + + setInitialValues(newInitialVal); + } + }, [fin_customers]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: fin_customersId, data })); + await router.push('/fin_customers/fin_customers-list'); + }; + + return ( + <> + + {getPageTitle('Edit fin_customers')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_customers/fin_customers-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditFin_customers.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditFin_customers; diff --git a/frontend/src/pages/fin_customers/fin_customers-edit.tsx b/frontend/src/pages/fin_customers/fin_customers-edit.tsx new file mode 100644 index 0000000..7460c1b --- /dev/null +++ b/frontend/src/pages/fin_customers/fin_customers-edit.tsx @@ -0,0 +1,168 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/fin_customers/fin_customersSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditFin_customersPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + name: '', + + segment: '', + + country: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { fin_customers } = useAppSelector((state) => state.fin_customers); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof fin_customers === 'object') { + setInitialValues(fin_customers); + } + }, [fin_customers]); + + useEffect(() => { + if (typeof fin_customers === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = fin_customers[el]), + ); + setInitialValues(newInitialVal); + } + }, [fin_customers]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/fin_customers/fin_customers-list'); + }; + + return ( + <> + + {getPageTitle('Edit fin_customers')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_customers/fin_customers-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditFin_customersPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditFin_customersPage; diff --git a/frontend/src/pages/fin_customers/fin_customers-list.tsx b/frontend/src/pages/fin_customers/fin_customers-list.tsx new file mode 100644 index 0000000..7679ccb --- /dev/null +++ b/frontend/src/pages/fin_customers/fin_customers-list.tsx @@ -0,0 +1,175 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableFin_customers from '../../components/Fin_customers/TableFin_customers'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/fin_customers/fin_customersSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_customersTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'Segment', title: 'segment' }, + { label: 'Country', title: 'country' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_FIN_CUSTOMERS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getFin_customersCSV = async () => { + const response = await axios({ + url: '/fin_customers?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'fin_customersCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Fin_customers')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + + Switch to Table + +
    +
    + + + + +
    + + + + + ); +}; + +Fin_customersTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_customersTablesPage; diff --git a/frontend/src/pages/fin_customers/fin_customers-new.tsx b/frontend/src/pages/fin_customers/fin_customers-new.tsx new file mode 100644 index 0000000..42d630d --- /dev/null +++ b/frontend/src/pages/fin_customers/fin_customers-new.tsx @@ -0,0 +1,136 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/fin_customers/fin_customersSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + name: '', + + segment: '', + + country: '', + + organizations: '', +}; + +const Fin_customersNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/fin_customers/fin_customers-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_customers/fin_customers-list') + } + /> + + +
    +
    +
    + + ); +}; + +Fin_customersNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_customersNew; diff --git a/frontend/src/pages/fin_customers/fin_customers-table.tsx b/frontend/src/pages/fin_customers/fin_customers-table.tsx new file mode 100644 index 0000000..fde2021 --- /dev/null +++ b/frontend/src/pages/fin_customers/fin_customers-table.tsx @@ -0,0 +1,172 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableFin_customers from '../../components/Fin_customers/TableFin_customers'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/fin_customers/fin_customersSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_customersTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'Segment', title: 'segment' }, + { label: 'Country', title: 'country' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_FIN_CUSTOMERS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getFin_customersCSV = async () => { + const response = await axios({ + url: '/fin_customers?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'fin_customersCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Fin_customers')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Fin_customersTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_customersTablesPage; diff --git a/frontend/src/pages/fin_customers/fin_customers-view.tsx b/frontend/src/pages/fin_customers/fin_customers-view.tsx new file mode 100644 index 0000000..d52956d --- /dev/null +++ b/frontend/src/pages/fin_customers/fin_customers-view.tsx @@ -0,0 +1,280 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/fin_customers/fin_customersSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_customersView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { fin_customers } = useAppSelector((state) => state.fin_customers); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View fin_customers')} + + + + + + +
    +

    Organization

    + +

    {fin_customers?.org?.name ?? 'No data'}

    +
    + +
    +

    Name

    +

    {fin_customers?.name}

    +
    + +
    +

    Segment

    +

    {fin_customers?.segment}

    +
    + +
    +

    Country

    +

    {fin_customers?.country}

    +
    + +
    +

    organizations

    + +

    {fin_customers?.organizations?.name ?? 'No data'}

    +
    + + <> +

    Fin_ar Customer

    + +
    + + + + + + + + + + + + + + {fin_customers.fin_ar_customer && + Array.isArray(fin_customers.fin_ar_customer) && + fin_customers.fin_ar_customer.map((item: any) => ( + + router.push(`/fin_ar/fin_ar-view/?id=${item.id}`) + } + > + + + + + + + + + ))} + +
    InvoiceNumberDueDateAmountStatus
    {item.invoice_no} + {dataFormatter.dateTimeFormatter(item.due_date)} + {item.amount}{item.status}
    +
    + {!fin_customers?.fin_ar_customer?.length && ( +
    No data
    + )} +
    + + + <> +

    Fin_transactions Customer

    + +
    + + + + + + + + + + + + + + + + + + + + + + {fin_customers.fin_transactions_customer && + Array.isArray(fin_customers.fin_transactions_customer) && + fin_customers.fin_transactions_customer.map( + (item: any) => ( + + router.push( + `/fin_transactions/fin_transactions-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + + + + + + + ), + )} + +
    TransactionTimestampAmountCurrencyFXRateCostCenterProjectCodeMemoSource
    + {dataFormatter.dateTimeFormatter(item.tx_ts)} + {item.amount}{item.currency}{item.fx_rate}{item.cost_center} + {item.project_code} + {item.memo}{item.source}
    +
    + {!fin_customers?.fin_transactions_customer?.length && ( +
    No data
    + )} +
    + + + <> +

    Ops_orders Customer

    + +
    + + + + + + + + + + + + + + {fin_customers.ops_orders_customer && + Array.isArray(fin_customers.ops_orders_customer) && + fin_customers.ops_orders_customer.map((item: any) => ( + + router.push( + `/ops_orders/ops_orders-view/?id=${item.id}`, + ) + } + > + + + + + + + + + ))} + +
    OrderNumberDueDateQuantityStatus
    {item.order_no} + {dataFormatter.dateTimeFormatter(item.due_date)} + {item.qty}{item.status}
    +
    + {!fin_customers?.ops_orders_customer?.length && ( +
    No data
    + )} +
    + + + + + router.push('/fin_customers/fin_customers-list')} + /> +
    +
    + + ); +}; + +Fin_customersView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_customersView; diff --git a/frontend/src/pages/fin_forecasts/[fin_forecastsId].tsx b/frontend/src/pages/fin_forecasts/[fin_forecastsId].tsx new file mode 100644 index 0000000..74218d8 --- /dev/null +++ b/frontend/src/pages/fin_forecasts/[fin_forecastsId].tsx @@ -0,0 +1,193 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/fin_forecasts/fin_forecastsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditFin_forecasts = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + fiscal_year: '', + + cost_center: '', + + account: null, + + month: '', + + amount: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { fin_forecasts } = useAppSelector((state) => state.fin_forecasts); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { fin_forecastsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: fin_forecastsId })); + }, [fin_forecastsId]); + + useEffect(() => { + if (typeof fin_forecasts === 'object') { + setInitialValues(fin_forecasts); + } + }, [fin_forecasts]); + + useEffect(() => { + if (typeof fin_forecasts === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = fin_forecasts[el]), + ); + + setInitialValues(newInitialVal); + } + }, [fin_forecasts]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: fin_forecastsId, data })); + await router.push('/fin_forecasts/fin_forecasts-list'); + }; + + return ( + <> + + {getPageTitle('Edit fin_forecasts')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_forecasts/fin_forecasts-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditFin_forecasts.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditFin_forecasts; diff --git a/frontend/src/pages/fin_forecasts/fin_forecasts-edit.tsx b/frontend/src/pages/fin_forecasts/fin_forecasts-edit.tsx new file mode 100644 index 0000000..d3036d0 --- /dev/null +++ b/frontend/src/pages/fin_forecasts/fin_forecasts-edit.tsx @@ -0,0 +1,191 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/fin_forecasts/fin_forecastsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditFin_forecastsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + fiscal_year: '', + + cost_center: '', + + account: null, + + month: '', + + amount: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { fin_forecasts } = useAppSelector((state) => state.fin_forecasts); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof fin_forecasts === 'object') { + setInitialValues(fin_forecasts); + } + }, [fin_forecasts]); + + useEffect(() => { + if (typeof fin_forecasts === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = fin_forecasts[el]), + ); + setInitialValues(newInitialVal); + } + }, [fin_forecasts]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/fin_forecasts/fin_forecasts-list'); + }; + + return ( + <> + + {getPageTitle('Edit fin_forecasts')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_forecasts/fin_forecasts-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditFin_forecastsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditFin_forecastsPage; diff --git a/frontend/src/pages/fin_forecasts/fin_forecasts-list.tsx b/frontend/src/pages/fin_forecasts/fin_forecasts-list.tsx new file mode 100644 index 0000000..3c9482f --- /dev/null +++ b/frontend/src/pages/fin_forecasts/fin_forecasts-list.tsx @@ -0,0 +1,178 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableFin_forecasts from '../../components/Fin_forecasts/TableFin_forecasts'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/fin_forecasts/fin_forecastsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_forecastsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'CostCenter', title: 'cost_center' }, + { label: 'FiscalYear', title: 'fiscal_year', number: 'true' }, + { label: 'Month', title: 'month', number: 'true' }, + { label: 'Amount', title: 'amount', number: 'true' }, + + { label: 'Account', title: 'account' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_FIN_FORECASTS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getFin_forecastsCSV = async () => { + const response = await axios({ + url: '/fin_forecasts?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'fin_forecastsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Fin_forecasts')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + + Switch to Table + +
    +
    + + + + +
    + + + + + ); +}; + +Fin_forecastsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_forecastsTablesPage; diff --git a/frontend/src/pages/fin_forecasts/fin_forecasts-new.tsx b/frontend/src/pages/fin_forecasts/fin_forecasts-new.tsx new file mode 100644 index 0000000..ccb7f47 --- /dev/null +++ b/frontend/src/pages/fin_forecasts/fin_forecasts-new.tsx @@ -0,0 +1,158 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/fin_forecasts/fin_forecastsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + fiscal_year: '', + + cost_center: '', + + account: '', + + month: '', + + amount: '', + + organizations: '', +}; + +const Fin_forecastsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/fin_forecasts/fin_forecasts-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_forecasts/fin_forecasts-list') + } + /> + + +
    +
    +
    + + ); +}; + +Fin_forecastsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_forecastsNew; diff --git a/frontend/src/pages/fin_forecasts/fin_forecasts-table.tsx b/frontend/src/pages/fin_forecasts/fin_forecasts-table.tsx new file mode 100644 index 0000000..795e295 --- /dev/null +++ b/frontend/src/pages/fin_forecasts/fin_forecasts-table.tsx @@ -0,0 +1,175 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableFin_forecasts from '../../components/Fin_forecasts/TableFin_forecasts'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/fin_forecasts/fin_forecastsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_forecastsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'CostCenter', title: 'cost_center' }, + { label: 'FiscalYear', title: 'fiscal_year', number: 'true' }, + { label: 'Month', title: 'month', number: 'true' }, + { label: 'Amount', title: 'amount', number: 'true' }, + + { label: 'Account', title: 'account' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_FIN_FORECASTS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getFin_forecastsCSV = async () => { + const response = await axios({ + url: '/fin_forecasts?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'fin_forecastsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Fin_forecasts')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Fin_forecastsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_forecastsTablesPage; diff --git a/frontend/src/pages/fin_forecasts/fin_forecasts-view.tsx b/frontend/src/pages/fin_forecasts/fin_forecasts-view.tsx new file mode 100644 index 0000000..a95c35e --- /dev/null +++ b/frontend/src/pages/fin_forecasts/fin_forecasts-view.tsx @@ -0,0 +1,120 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/fin_forecasts/fin_forecastsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_forecastsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { fin_forecasts } = useAppSelector((state) => state.fin_forecasts); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View fin_forecasts')} + + + + + + +
    +

    Organization

    + +

    {fin_forecasts?.org?.name ?? 'No data'}

    +
    + +
    +

    FiscalYear

    +

    {fin_forecasts?.fiscal_year || 'No data'}

    +
    + +
    +

    CostCenter

    +

    {fin_forecasts?.cost_center}

    +
    + +
    +

    Account

    + +

    {fin_forecasts?.account?.name ?? 'No data'}

    +
    + +
    +

    Month

    +

    {fin_forecasts?.month || 'No data'}

    +
    + +
    +

    Amount

    +

    {fin_forecasts?.amount || 'No data'}

    +
    + +
    +

    organizations

    + +

    {fin_forecasts?.organizations?.name ?? 'No data'}

    +
    + + + + router.push('/fin_forecasts/fin_forecasts-list')} + /> +
    +
    + + ); +}; + +Fin_forecastsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_forecastsView; diff --git a/frontend/src/pages/fin_transactions/[fin_transactionsId].tsx b/frontend/src/pages/fin_transactions/[fin_transactionsId].tsx new file mode 100644 index 0000000..331531c --- /dev/null +++ b/frontend/src/pages/fin_transactions/[fin_transactionsId].tsx @@ -0,0 +1,257 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { + update, + fetch, +} from '../../stores/fin_transactions/fin_transactionsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditFin_transactions = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + account: null, + + tx_ts: new Date(), + + amount: '', + + currency: '', + + fx_rate: '', + + cost_center: '', + + project_code: '', + + vendor: null, + + customer: null, + + memo: '', + + source: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { fin_transactions } = useAppSelector( + (state) => state.fin_transactions, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { fin_transactionsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: fin_transactionsId })); + }, [fin_transactionsId]); + + useEffect(() => { + if (typeof fin_transactions === 'object') { + setInitialValues(fin_transactions); + } + }, [fin_transactions]); + + useEffect(() => { + if (typeof fin_transactions === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = fin_transactions[el]), + ); + + setInitialValues(newInitialVal); + } + }, [fin_transactions]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: fin_transactionsId, data })); + await router.push('/fin_transactions/fin_transactions-list'); + }; + + return ( + <> + + {getPageTitle('Edit fin_transactions')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + setInitialValues({ ...initialValues, tx_ts: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_transactions/fin_transactions-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditFin_transactions.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditFin_transactions; diff --git a/frontend/src/pages/fin_transactions/fin_transactions-edit.tsx b/frontend/src/pages/fin_transactions/fin_transactions-edit.tsx new file mode 100644 index 0000000..02d42b9 --- /dev/null +++ b/frontend/src/pages/fin_transactions/fin_transactions-edit.tsx @@ -0,0 +1,255 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { + update, + fetch, +} from '../../stores/fin_transactions/fin_transactionsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditFin_transactionsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + account: null, + + tx_ts: new Date(), + + amount: '', + + currency: '', + + fx_rate: '', + + cost_center: '', + + project_code: '', + + vendor: null, + + customer: null, + + memo: '', + + source: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { fin_transactions } = useAppSelector( + (state) => state.fin_transactions, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof fin_transactions === 'object') { + setInitialValues(fin_transactions); + } + }, [fin_transactions]); + + useEffect(() => { + if (typeof fin_transactions === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = fin_transactions[el]), + ); + setInitialValues(newInitialVal); + } + }, [fin_transactions]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/fin_transactions/fin_transactions-list'); + }; + + return ( + <> + + {getPageTitle('Edit fin_transactions')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + setInitialValues({ ...initialValues, tx_ts: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_transactions/fin_transactions-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditFin_transactionsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditFin_transactionsPage; diff --git a/frontend/src/pages/fin_transactions/fin_transactions-list.tsx b/frontend/src/pages/fin_transactions/fin_transactions-list.tsx new file mode 100644 index 0000000..207303d --- /dev/null +++ b/frontend/src/pages/fin_transactions/fin_transactions-list.tsx @@ -0,0 +1,187 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableFin_transactions from '../../components/Fin_transactions/TableFin_transactions'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/fin_transactions/fin_transactionsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_transactionsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Currency', title: 'currency' }, + { label: 'CostCenter', title: 'cost_center' }, + { label: 'ProjectCode', title: 'project_code' }, + { label: 'Memo', title: 'memo' }, + { label: 'Source', title: 'source' }, + + { label: 'Amount', title: 'amount', number: 'true' }, + { label: 'FXRate', title: 'fx_rate', number: 'true' }, + { label: 'TransactionTimestamp', title: 'tx_ts', date: 'true' }, + + { label: 'Account', title: 'account' }, + + { label: 'Vendor', title: 'vendor' }, + + { label: 'Customer', title: 'customer' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_FIN_TRANSACTIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getFin_transactionsCSV = async () => { + const response = await axios({ + url: '/fin_transactions?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'fin_transactionsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Fin_transactions')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + + Switch to Table + +
    +
    + + + + +
    + + + + + ); +}; + +Fin_transactionsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_transactionsTablesPage; diff --git a/frontend/src/pages/fin_transactions/fin_transactions-new.tsx b/frontend/src/pages/fin_transactions/fin_transactions-new.tsx new file mode 100644 index 0000000..92917ac --- /dev/null +++ b/frontend/src/pages/fin_transactions/fin_transactions-new.tsx @@ -0,0 +1,206 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/fin_transactions/fin_transactionsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + account: '', + + tx_ts: '', + + amount: '', + + currency: '', + + fx_rate: '', + + cost_center: '', + + project_code: '', + + vendor: '', + + customer: '', + + memo: '', + + source: '', + + organizations: '', +}; + +const Fin_transactionsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/fin_transactions/fin_transactions-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_transactions/fin_transactions-list') + } + /> + + +
    +
    +
    + + ); +}; + +Fin_transactionsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_transactionsNew; diff --git a/frontend/src/pages/fin_transactions/fin_transactions-table.tsx b/frontend/src/pages/fin_transactions/fin_transactions-table.tsx new file mode 100644 index 0000000..8b5f5b6 --- /dev/null +++ b/frontend/src/pages/fin_transactions/fin_transactions-table.tsx @@ -0,0 +1,184 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableFin_transactions from '../../components/Fin_transactions/TableFin_transactions'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/fin_transactions/fin_transactionsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_transactionsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Currency', title: 'currency' }, + { label: 'CostCenter', title: 'cost_center' }, + { label: 'ProjectCode', title: 'project_code' }, + { label: 'Memo', title: 'memo' }, + { label: 'Source', title: 'source' }, + + { label: 'Amount', title: 'amount', number: 'true' }, + { label: 'FXRate', title: 'fx_rate', number: 'true' }, + { label: 'TransactionTimestamp', title: 'tx_ts', date: 'true' }, + + { label: 'Account', title: 'account' }, + + { label: 'Vendor', title: 'vendor' }, + + { label: 'Customer', title: 'customer' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_FIN_TRANSACTIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getFin_transactionsCSV = async () => { + const response = await axios({ + url: '/fin_transactions?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'fin_transactionsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Fin_transactions')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Fin_transactionsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_transactionsTablesPage; diff --git a/frontend/src/pages/fin_transactions/fin_transactions-view.tsx b/frontend/src/pages/fin_transactions/fin_transactions-view.tsx new file mode 100644 index 0000000..d30f600 --- /dev/null +++ b/frontend/src/pages/fin_transactions/fin_transactions-view.tsx @@ -0,0 +1,172 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/fin_transactions/fin_transactionsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_transactionsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { fin_transactions } = useAppSelector( + (state) => state.fin_transactions, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View fin_transactions')} + + + + + + +
    +

    Organization

    + +

    {fin_transactions?.org?.name ?? 'No data'}

    +
    + +
    +

    Account

    + +

    {fin_transactions?.account?.name ?? 'No data'}

    +
    + + + {fin_transactions.tx_ts ? ( + + ) : ( +

    No TransactionTimestamp

    + )} +
    + +
    +

    Amount

    +

    {fin_transactions?.amount || 'No data'}

    +
    + +
    +

    Currency

    +

    {fin_transactions?.currency}

    +
    + +
    +

    FXRate

    +

    {fin_transactions?.fx_rate || 'No data'}

    +
    + +
    +

    CostCenter

    +

    {fin_transactions?.cost_center}

    +
    + +
    +

    ProjectCode

    +

    {fin_transactions?.project_code}

    +
    + +
    +

    Vendor

    + +

    {fin_transactions?.vendor?.name ?? 'No data'}

    +
    + +
    +

    Customer

    + +

    {fin_transactions?.customer?.name ?? 'No data'}

    +
    + +
    +

    Memo

    +

    {fin_transactions?.memo}

    +
    + +
    +

    Source

    +

    {fin_transactions?.source}

    +
    + +
    +

    organizations

    + +

    {fin_transactions?.organizations?.name ?? 'No data'}

    +
    + + + + + router.push('/fin_transactions/fin_transactions-list') + } + /> +
    +
    + + ); +}; + +Fin_transactionsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_transactionsView; diff --git a/frontend/src/pages/fin_vendors/[fin_vendorsId].tsx b/frontend/src/pages/fin_vendors/[fin_vendorsId].tsx new file mode 100644 index 0000000..17549db --- /dev/null +++ b/frontend/src/pages/fin_vendors/[fin_vendorsId].tsx @@ -0,0 +1,168 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/fin_vendors/fin_vendorsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditFin_vendors = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + name: '', + + tax_id: '', + + country: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { fin_vendors } = useAppSelector((state) => state.fin_vendors); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { fin_vendorsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: fin_vendorsId })); + }, [fin_vendorsId]); + + useEffect(() => { + if (typeof fin_vendors === 'object') { + setInitialValues(fin_vendors); + } + }, [fin_vendors]); + + useEffect(() => { + if (typeof fin_vendors === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = fin_vendors[el]), + ); + + setInitialValues(newInitialVal); + } + }, [fin_vendors]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: fin_vendorsId, data })); + await router.push('/fin_vendors/fin_vendors-list'); + }; + + return ( + <> + + {getPageTitle('Edit fin_vendors')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_vendors/fin_vendors-list')} + /> + + +
    +
    +
    + + ); +}; + +EditFin_vendors.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditFin_vendors; diff --git a/frontend/src/pages/fin_vendors/fin_vendors-edit.tsx b/frontend/src/pages/fin_vendors/fin_vendors-edit.tsx new file mode 100644 index 0000000..8be77c1 --- /dev/null +++ b/frontend/src/pages/fin_vendors/fin_vendors-edit.tsx @@ -0,0 +1,166 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/fin_vendors/fin_vendorsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditFin_vendorsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + name: '', + + tax_id: '', + + country: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { fin_vendors } = useAppSelector((state) => state.fin_vendors); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof fin_vendors === 'object') { + setInitialValues(fin_vendors); + } + }, [fin_vendors]); + + useEffect(() => { + if (typeof fin_vendors === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = fin_vendors[el]), + ); + setInitialValues(newInitialVal); + } + }, [fin_vendors]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/fin_vendors/fin_vendors-list'); + }; + + return ( + <> + + {getPageTitle('Edit fin_vendors')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_vendors/fin_vendors-list')} + /> + + +
    +
    +
    + + ); +}; + +EditFin_vendorsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditFin_vendorsPage; diff --git a/frontend/src/pages/fin_vendors/fin_vendors-list.tsx b/frontend/src/pages/fin_vendors/fin_vendors-list.tsx new file mode 100644 index 0000000..708c17f --- /dev/null +++ b/frontend/src/pages/fin_vendors/fin_vendors-list.tsx @@ -0,0 +1,173 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableFin_vendors from '../../components/Fin_vendors/TableFin_vendors'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/fin_vendors/fin_vendorsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_vendorsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'TaxID', title: 'tax_id' }, + { label: 'Country', title: 'country' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_FIN_VENDORS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getFin_vendorsCSV = async () => { + const response = await axios({ + url: '/fin_vendors?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'fin_vendorsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Fin_vendors')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + Switch to Table +
    +
    + + + + +
    + + + + + ); +}; + +Fin_vendorsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_vendorsTablesPage; diff --git a/frontend/src/pages/fin_vendors/fin_vendors-new.tsx b/frontend/src/pages/fin_vendors/fin_vendors-new.tsx new file mode 100644 index 0000000..bc5ea3e --- /dev/null +++ b/frontend/src/pages/fin_vendors/fin_vendors-new.tsx @@ -0,0 +1,134 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/fin_vendors/fin_vendorsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + name: '', + + tax_id: '', + + country: '', + + organizations: '', +}; + +const Fin_vendorsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/fin_vendors/fin_vendors-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/fin_vendors/fin_vendors-list')} + /> + + +
    +
    +
    + + ); +}; + +Fin_vendorsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_vendorsNew; diff --git a/frontend/src/pages/fin_vendors/fin_vendors-table.tsx b/frontend/src/pages/fin_vendors/fin_vendors-table.tsx new file mode 100644 index 0000000..b9f6ab9 --- /dev/null +++ b/frontend/src/pages/fin_vendors/fin_vendors-table.tsx @@ -0,0 +1,172 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableFin_vendors from '../../components/Fin_vendors/TableFin_vendors'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/fin_vendors/fin_vendorsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_vendorsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'TaxID', title: 'tax_id' }, + { label: 'Country', title: 'country' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_FIN_VENDORS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getFin_vendorsCSV = async () => { + const response = await axios({ + url: '/fin_vendors?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'fin_vendorsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Fin_vendors')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Fin_vendorsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_vendorsTablesPage; diff --git a/frontend/src/pages/fin_vendors/fin_vendors-view.tsx b/frontend/src/pages/fin_vendors/fin_vendors-view.tsx new file mode 100644 index 0000000..0e4989e --- /dev/null +++ b/frontend/src/pages/fin_vendors/fin_vendors-view.tsx @@ -0,0 +1,225 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/fin_vendors/fin_vendorsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Fin_vendorsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { fin_vendors } = useAppSelector((state) => state.fin_vendors); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View fin_vendors')} + + + + + + +
    +

    Organization

    + +

    {fin_vendors?.org?.name ?? 'No data'}

    +
    + +
    +

    Name

    +

    {fin_vendors?.name}

    +
    + +
    +

    TaxID

    +

    {fin_vendors?.tax_id}

    +
    + +
    +

    Country

    +

    {fin_vendors?.country}

    +
    + +
    +

    organizations

    + +

    {fin_vendors?.organizations?.name ?? 'No data'}

    +
    + + <> +

    Fin_ap Vendor

    + +
    + + + + + + + + + + + + + + {fin_vendors.fin_ap_vendor && + Array.isArray(fin_vendors.fin_ap_vendor) && + fin_vendors.fin_ap_vendor.map((item: any) => ( + + router.push(`/fin_ap/fin_ap-view/?id=${item.id}`) + } + > + + + + + + + + + ))} + +
    BillNumberDueDateAmountStatus
    {item.bill_no} + {dataFormatter.dateTimeFormatter(item.due_date)} + {item.amount}{item.status}
    +
    + {!fin_vendors?.fin_ap_vendor?.length && ( +
    No data
    + )} +
    + + + <> +

    Fin_transactions Vendor

    + +
    + + + + + + + + + + + + + + + + + + + + + + {fin_vendors.fin_transactions_vendor && + Array.isArray(fin_vendors.fin_transactions_vendor) && + fin_vendors.fin_transactions_vendor.map((item: any) => ( + + router.push( + `/fin_transactions/fin_transactions-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + + + + + + + ))} + +
    TransactionTimestampAmountCurrencyFXRateCostCenterProjectCodeMemoSource
    + {dataFormatter.dateTimeFormatter(item.tx_ts)} + {item.amount}{item.currency}{item.fx_rate}{item.cost_center}{item.project_code}{item.memo}{item.source}
    +
    + {!fin_vendors?.fin_transactions_vendor?.length && ( +
    No data
    + )} +
    + + + + + router.push('/fin_vendors/fin_vendors-list')} + /> +
    +
    + + ); +}; + +Fin_vendorsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Fin_vendorsView; diff --git a/frontend/src/pages/forgot.tsx b/frontend/src/pages/forgot.tsx new file mode 100644 index 0000000..071239b --- /dev/null +++ b/frontend/src/pages/forgot.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import type { ReactElement } from 'react'; +import { ToastContainer, toast } from 'react-toastify'; +import Head from 'next/head'; +import BaseButton from '../components/BaseButton'; +import CardBox from '../components/CardBox'; +import SectionFullScreen from '../components/SectionFullScreen'; +import LayoutGuest from '../layouts/Guest'; +import { Field, Form, Formik } from 'formik'; +import FormField from '../components/FormField'; +import BaseDivider from '../components/BaseDivider'; +import BaseButtons from '../components/BaseButtons'; +import { useRouter } from 'next/router'; +import { getPageTitle } from '../config'; +import axios from 'axios'; + +export default function Forgot() { + const [loading, setLoading] = React.useState(false); + const router = useRouter(); + const notify = (type, msg) => toast(msg, { type }); + + const handleSubmit = async (value) => { + setLoading(true); + try { + const { data: response } = await axios.post( + '/auth/send-password-reset-email', + value, + ); + setLoading(false); + notify('success', 'Please check your email for verification link'); + setTimeout(async () => { + await router.push('/login'); + }, 3000); + } catch (error) { + setLoading(false); + console.log('error: ', error); + notify('error', 'Something was wrong. Try again'); + } + }; + + return ( + <> + + {getPageTitle('Login')} + + + + + handleSubmit(values)} + > +
    + + + + + + + + + + + +
    +
    +
    + + + ); +} + +Forgot.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/pages/forms.tsx b/frontend/src/pages/forms.tsx new file mode 100644 index 0000000..45de29b --- /dev/null +++ b/frontend/src/pages/forms.tsx @@ -0,0 +1,162 @@ +import { + mdiAccount, + mdiBallotOutline, + mdiGithub, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import { Field, Form, Formik } from 'formik'; +import Head from 'next/head'; +import { ReactElement } from 'react'; +import BaseButton from '../components/BaseButton'; +import BaseButtons from '../components/BaseButtons'; +import BaseDivider from '../components/BaseDivider'; +import CardBox from '../components/CardBox'; +import FormCheckRadio from '../components/FormCheckRadio'; +import FormCheckRadioGroup from '../components/FormCheckRadioGroup'; +import FormField from '../components/FormField'; +import LayoutAuthenticated from '../layouts/Authenticated'; +import SectionMain from '../components/SectionMain'; +import SectionTitle from '../components/SectionTitle'; +import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../config'; + +const FormsPage = () => { + return ( + <> + + {getPageTitle('Forms')} + + + + + {''} + + + + alert(JSON.stringify(values, null, 2))} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    + + Custom elements + + + + null} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +
    + + ); +}; + +FormsPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default FormsPage; diff --git a/frontend/src/pages/hr_applicants/[hr_applicantsId].tsx b/frontend/src/pages/hr_applicants/[hr_applicantsId].tsx new file mode 100644 index 0000000..409589b --- /dev/null +++ b/frontend/src/pages/hr_applicants/[hr_applicantsId].tsx @@ -0,0 +1,239 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/hr_applicants/hr_applicantsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditHr_applicants = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + req: null, + + name: '', + + stage: '', + + source: '', + + offer_extended_at: new Date(), + + offer_accepted_at: new Date(), + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { hr_applicants } = useAppSelector((state) => state.hr_applicants); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { hr_applicantsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: hr_applicantsId })); + }, [hr_applicantsId]); + + useEffect(() => { + if (typeof hr_applicants === 'object') { + setInitialValues(hr_applicants); + } + }, [hr_applicants]); + + useEffect(() => { + if (typeof hr_applicants === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = hr_applicants[el]), + ); + + setInitialValues(newInitialVal); + } + }, [hr_applicants]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: hr_applicantsId, data })); + await router.push('/hr_applicants/hr_applicants-list'); + }; + + return ( + <> + + {getPageTitle('Edit hr_applicants')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setInitialValues({ + ...initialValues, + offer_extended_at: date, + }) + } + /> + + + + + setInitialValues({ + ...initialValues, + offer_accepted_at: date, + }) + } + /> + + + + + + + + + + + + router.push('/hr_applicants/hr_applicants-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditHr_applicants.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditHr_applicants; diff --git a/frontend/src/pages/hr_applicants/hr_applicants-edit.tsx b/frontend/src/pages/hr_applicants/hr_applicants-edit.tsx new file mode 100644 index 0000000..25d45fe --- /dev/null +++ b/frontend/src/pages/hr_applicants/hr_applicants-edit.tsx @@ -0,0 +1,237 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/hr_applicants/hr_applicantsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditHr_applicantsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + req: null, + + name: '', + + stage: '', + + source: '', + + offer_extended_at: new Date(), + + offer_accepted_at: new Date(), + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { hr_applicants } = useAppSelector((state) => state.hr_applicants); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof hr_applicants === 'object') { + setInitialValues(hr_applicants); + } + }, [hr_applicants]); + + useEffect(() => { + if (typeof hr_applicants === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = hr_applicants[el]), + ); + setInitialValues(newInitialVal); + } + }, [hr_applicants]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/hr_applicants/hr_applicants-list'); + }; + + return ( + <> + + {getPageTitle('Edit hr_applicants')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setInitialValues({ + ...initialValues, + offer_extended_at: date, + }) + } + /> + + + + + setInitialValues({ + ...initialValues, + offer_accepted_at: date, + }) + } + /> + + + + + + + + + + + + router.push('/hr_applicants/hr_applicants-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditHr_applicantsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditHr_applicantsPage; diff --git a/frontend/src/pages/hr_applicants/hr_applicants-list.tsx b/frontend/src/pages/hr_applicants/hr_applicants-list.tsx new file mode 100644 index 0000000..363ebe2 --- /dev/null +++ b/frontend/src/pages/hr_applicants/hr_applicants-list.tsx @@ -0,0 +1,186 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableHr_applicants from '../../components/Hr_applicants/TableHr_applicants'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/hr_applicants/hr_applicantsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Hr_applicantsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'Source', title: 'source' }, + + { label: 'OfferExtendedAt', title: 'offer_extended_at', date: 'true' }, + { label: 'OfferAcceptedAt', title: 'offer_accepted_at', date: 'true' }, + + { label: 'Requisition', title: 'req' }, + + { + label: 'Stage', + title: 'stage', + type: 'enum', + options: ['Applied', 'Interview', 'Offer', 'Hired'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_HR_APPLICANTS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getHr_applicantsCSV = async () => { + const response = await axios({ + url: '/hr_applicants?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'hr_applicantsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Hr_applicants')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + + Switch to Table + +
    +
    + + + + +
    + + + + + ); +}; + +Hr_applicantsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_applicantsTablesPage; diff --git a/frontend/src/pages/hr_applicants/hr_applicants-new.tsx b/frontend/src/pages/hr_applicants/hr_applicants-new.tsx new file mode 100644 index 0000000..2f5e986 --- /dev/null +++ b/frontend/src/pages/hr_applicants/hr_applicants-new.tsx @@ -0,0 +1,176 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/hr_applicants/hr_applicantsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + req: '', + + name: '', + + stage: 'Applied', + + source: '', + + offer_extended_at: '', + + offer_accepted_at: '', + + organizations: '', +}; + +const Hr_applicantsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/hr_applicants/hr_applicants-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/hr_applicants/hr_applicants-list') + } + /> + + +
    +
    +
    + + ); +}; + +Hr_applicantsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_applicantsNew; diff --git a/frontend/src/pages/hr_applicants/hr_applicants-table.tsx b/frontend/src/pages/hr_applicants/hr_applicants-table.tsx new file mode 100644 index 0000000..f14fd78 --- /dev/null +++ b/frontend/src/pages/hr_applicants/hr_applicants-table.tsx @@ -0,0 +1,183 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableHr_applicants from '../../components/Hr_applicants/TableHr_applicants'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/hr_applicants/hr_applicantsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Hr_applicantsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'Source', title: 'source' }, + + { label: 'OfferExtendedAt', title: 'offer_extended_at', date: 'true' }, + { label: 'OfferAcceptedAt', title: 'offer_accepted_at', date: 'true' }, + + { label: 'Requisition', title: 'req' }, + + { + label: 'Stage', + title: 'stage', + type: 'enum', + options: ['Applied', 'Interview', 'Offer', 'Hired'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_HR_APPLICANTS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getHr_applicantsCSV = async () => { + const response = await axios({ + url: '/hr_applicants?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'hr_applicantsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Hr_applicants')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Hr_applicantsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_applicantsTablesPage; diff --git a/frontend/src/pages/hr_applicants/hr_applicants-view.tsx b/frontend/src/pages/hr_applicants/hr_applicants-view.tsx new file mode 100644 index 0000000..5d4e83b --- /dev/null +++ b/frontend/src/pages/hr_applicants/hr_applicants-view.tsx @@ -0,0 +1,157 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/hr_applicants/hr_applicantsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Hr_applicantsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { hr_applicants } = useAppSelector((state) => state.hr_applicants); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View hr_applicants')} + + + + + + +
    +

    Organization

    + +

    {hr_applicants?.org?.name ?? 'No data'}

    +
    + +
    +

    Requisition

    + +

    {hr_applicants?.req?.recruiter ?? 'No data'}

    +
    + +
    +

    Name

    +

    {hr_applicants?.name}

    +
    + +
    +

    Stage

    +

    {hr_applicants?.stage ?? 'No data'}

    +
    + +
    +

    Source

    +

    {hr_applicants?.source}

    +
    + + + {hr_applicants.offer_extended_at ? ( + + ) : ( +

    No OfferExtendedAt

    + )} +
    + + + {hr_applicants.offer_accepted_at ? ( + + ) : ( +

    No OfferAcceptedAt

    + )} +
    + +
    +

    organizations

    + +

    {hr_applicants?.organizations?.name ?? 'No data'}

    +
    + + + + router.push('/hr_applicants/hr_applicants-list')} + /> +
    +
    + + ); +}; + +Hr_applicantsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_applicantsView; diff --git a/frontend/src/pages/hr_attendance/[hr_attendanceId].tsx b/frontend/src/pages/hr_attendance/[hr_attendanceId].tsx new file mode 100644 index 0000000..9cc2af5 --- /dev/null +++ b/frontend/src/pages/hr_attendance/[hr_attendanceId].tsx @@ -0,0 +1,206 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/hr_attendance/hr_attendanceSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditHr_attendance = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + employee: null, + + date: new Date(), + + hours_worked: '', + + leave_type: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { hr_attendance } = useAppSelector((state) => state.hr_attendance); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { hr_attendanceId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: hr_attendanceId })); + }, [hr_attendanceId]); + + useEffect(() => { + if (typeof hr_attendance === 'object') { + setInitialValues(hr_attendance); + } + }, [hr_attendance]); + + useEffect(() => { + if (typeof hr_attendance === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = hr_attendance[el]), + ); + + setInitialValues(newInitialVal); + } + }, [hr_attendance]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: hr_attendanceId, data })); + await router.push('/hr_attendance/hr_attendance-list'); + }; + + return ( + <> + + {getPageTitle('Edit hr_attendance')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + setInitialValues({ ...initialValues, date: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/hr_attendance/hr_attendance-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditHr_attendance.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditHr_attendance; diff --git a/frontend/src/pages/hr_attendance/hr_attendance-edit.tsx b/frontend/src/pages/hr_attendance/hr_attendance-edit.tsx new file mode 100644 index 0000000..d36757d --- /dev/null +++ b/frontend/src/pages/hr_attendance/hr_attendance-edit.tsx @@ -0,0 +1,204 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/hr_attendance/hr_attendanceSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditHr_attendancePage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + employee: null, + + date: new Date(), + + hours_worked: '', + + leave_type: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { hr_attendance } = useAppSelector((state) => state.hr_attendance); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof hr_attendance === 'object') { + setInitialValues(hr_attendance); + } + }, [hr_attendance]); + + useEffect(() => { + if (typeof hr_attendance === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = hr_attendance[el]), + ); + setInitialValues(newInitialVal); + } + }, [hr_attendance]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/hr_attendance/hr_attendance-list'); + }; + + return ( + <> + + {getPageTitle('Edit hr_attendance')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + setInitialValues({ ...initialValues, date: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/hr_attendance/hr_attendance-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditHr_attendancePage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditHr_attendancePage; diff --git a/frontend/src/pages/hr_attendance/hr_attendance-list.tsx b/frontend/src/pages/hr_attendance/hr_attendance-list.tsx new file mode 100644 index 0000000..69a6616 --- /dev/null +++ b/frontend/src/pages/hr_attendance/hr_attendance-list.tsx @@ -0,0 +1,183 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableHr_attendance from '../../components/Hr_attendance/TableHr_attendance'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/hr_attendance/hr_attendanceSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Hr_attendanceTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'HoursWorked', title: 'hours_worked', number: 'true' }, + { label: 'Date', title: 'date', date: 'true' }, + + { label: 'Employee', title: 'employee' }, + + { + label: 'LeaveType', + title: 'leave_type', + type: 'enum', + options: ['Sick', 'Vacation', 'Unpaid'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_HR_ATTENDANCE'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getHr_attendanceCSV = async () => { + const response = await axios({ + url: '/hr_attendance?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'hr_attendanceCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Hr_attendance')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + + Switch to Table + +
    +
    + + + + +
    + + + + + ); +}; + +Hr_attendanceTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_attendanceTablesPage; diff --git a/frontend/src/pages/hr_attendance/hr_attendance-new.tsx b/frontend/src/pages/hr_attendance/hr_attendance-new.tsx new file mode 100644 index 0000000..75090a3 --- /dev/null +++ b/frontend/src/pages/hr_attendance/hr_attendance-new.tsx @@ -0,0 +1,158 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/hr_attendance/hr_attendanceSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + employee: '', + + date: '', + + hours_worked: '', + + leave_type: 'Sick', + + organizations: '', +}; + +const Hr_attendanceNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/hr_attendance/hr_attendance-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/hr_attendance/hr_attendance-list') + } + /> + + +
    +
    +
    + + ); +}; + +Hr_attendanceNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_attendanceNew; diff --git a/frontend/src/pages/hr_attendance/hr_attendance-table.tsx b/frontend/src/pages/hr_attendance/hr_attendance-table.tsx new file mode 100644 index 0000000..39cf44e --- /dev/null +++ b/frontend/src/pages/hr_attendance/hr_attendance-table.tsx @@ -0,0 +1,180 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableHr_attendance from '../../components/Hr_attendance/TableHr_attendance'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/hr_attendance/hr_attendanceSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Hr_attendanceTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'HoursWorked', title: 'hours_worked', number: 'true' }, + { label: 'Date', title: 'date', date: 'true' }, + + { label: 'Employee', title: 'employee' }, + + { + label: 'LeaveType', + title: 'leave_type', + type: 'enum', + options: ['Sick', 'Vacation', 'Unpaid'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_HR_ATTENDANCE'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getHr_attendanceCSV = async () => { + const response = await axios({ + url: '/hr_attendance?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'hr_attendanceCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Hr_attendance')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to calendar + +
    +
    + + + +
    + + + + + ); +}; + +Hr_attendanceTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_attendanceTablesPage; diff --git a/frontend/src/pages/hr_attendance/hr_attendance-view.tsx b/frontend/src/pages/hr_attendance/hr_attendance-view.tsx new file mode 100644 index 0000000..1099a4f --- /dev/null +++ b/frontend/src/pages/hr_attendance/hr_attendance-view.tsx @@ -0,0 +1,129 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/hr_attendance/hr_attendanceSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Hr_attendanceView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { hr_attendance } = useAppSelector((state) => state.hr_attendance); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View hr_attendance')} + + + + + + +
    +

    Organization

    + +

    {hr_attendance?.org?.name ?? 'No data'}

    +
    + +
    +

    Employee

    + +

    {hr_attendance?.employee?.name ?? 'No data'}

    +
    + + + {hr_attendance.date ? ( + + ) : ( +

    No Date

    + )} +
    + +
    +

    HoursWorked

    +

    {hr_attendance?.hours_worked || 'No data'}

    +
    + +
    +

    LeaveType

    +

    {hr_attendance?.leave_type ?? 'No data'}

    +
    + +
    +

    organizations

    + +

    {hr_attendance?.organizations?.name ?? 'No data'}

    +
    + + + + router.push('/hr_attendance/hr_attendance-list')} + /> +
    +
    + + ); +}; + +Hr_attendanceView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_attendanceView; diff --git a/frontend/src/pages/hr_employees/[hr_employeesId].tsx b/frontend/src/pages/hr_employees/[hr_employeesId].tsx new file mode 100644 index 0000000..cda7742 --- /dev/null +++ b/frontend/src/pages/hr_employees/[hr_employeesId].tsx @@ -0,0 +1,254 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/hr_employees/hr_employeesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditHr_employees = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + employee_no: '', + + name: '', + + email: '', + + manager: null, + + department: '', + + location: '', + + grade: '', + + hire_date: new Date(), + + termination_date: new Date(), + + status: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { hr_employees } = useAppSelector((state) => state.hr_employees); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { hr_employeesId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: hr_employeesId })); + }, [hr_employeesId]); + + useEffect(() => { + if (typeof hr_employees === 'object') { + setInitialValues(hr_employees); + } + }, [hr_employees]); + + useEffect(() => { + if (typeof hr_employees === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = hr_employees[el]), + ); + + setInitialValues(newInitialVal); + } + }, [hr_employees]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: hr_employeesId, data })); + await router.push('/hr_employees/hr_employees-list'); + }; + + return ( + <> + + {getPageTitle('Edit hr_employees')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setInitialValues({ ...initialValues, hire_date: date }) + } + /> + + + + + setInitialValues({ + ...initialValues, + termination_date: date, + }) + } + /> + + + + + + + + + + + + + + + + + + + router.push('/hr_employees/hr_employees-list')} + /> + + +
    +
    +
    + + ); +}; + +EditHr_employees.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditHr_employees; diff --git a/frontend/src/pages/hr_employees/hr_employees-edit.tsx b/frontend/src/pages/hr_employees/hr_employees-edit.tsx new file mode 100644 index 0000000..cc4bd92 --- /dev/null +++ b/frontend/src/pages/hr_employees/hr_employees-edit.tsx @@ -0,0 +1,252 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/hr_employees/hr_employeesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditHr_employeesPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + employee_no: '', + + name: '', + + email: '', + + manager: null, + + department: '', + + location: '', + + grade: '', + + hire_date: new Date(), + + termination_date: new Date(), + + status: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { hr_employees } = useAppSelector((state) => state.hr_employees); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof hr_employees === 'object') { + setInitialValues(hr_employees); + } + }, [hr_employees]); + + useEffect(() => { + if (typeof hr_employees === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = hr_employees[el]), + ); + setInitialValues(newInitialVal); + } + }, [hr_employees]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/hr_employees/hr_employees-list'); + }; + + return ( + <> + + {getPageTitle('Edit hr_employees')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setInitialValues({ ...initialValues, hire_date: date }) + } + /> + + + + + setInitialValues({ + ...initialValues, + termination_date: date, + }) + } + /> + + + + + + + + + + + + + + + + + + + router.push('/hr_employees/hr_employees-list')} + /> + + +
    +
    +
    + + ); +}; + +EditHr_employeesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditHr_employeesPage; diff --git a/frontend/src/pages/hr_employees/hr_employees-list.tsx b/frontend/src/pages/hr_employees/hr_employees-list.tsx new file mode 100644 index 0000000..ddf62de --- /dev/null +++ b/frontend/src/pages/hr_employees/hr_employees-list.tsx @@ -0,0 +1,190 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableHr_employees from '../../components/Hr_employees/TableHr_employees'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/hr_employees/hr_employeesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Hr_employeesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'EmployeeNumber', title: 'employee_no' }, + { label: 'Name', title: 'name' }, + { label: 'Email', title: 'email' }, + { label: 'Department', title: 'department' }, + { label: 'Location', title: 'location' }, + { label: 'Grade', title: 'grade' }, + + { label: 'HireDate', title: 'hire_date', date: 'true' }, + { label: 'TerminationDate', title: 'termination_date', date: 'true' }, + + { label: 'Manager', title: 'manager' }, + + { + label: 'Status', + title: 'status', + type: 'enum', + options: ['Active', 'Inactive'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_HR_EMPLOYEES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getHr_employeesCSV = async () => { + const response = await axios({ + url: '/hr_employees?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'hr_employeesCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Hr_employees')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + + Switch to Table + +
    +
    + + + + +
    + + + + + ); +}; + +Hr_employeesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_employeesTablesPage; diff --git a/frontend/src/pages/hr_employees/hr_employees-new.tsx b/frontend/src/pages/hr_employees/hr_employees-new.tsx new file mode 100644 index 0000000..ebfb444 --- /dev/null +++ b/frontend/src/pages/hr_employees/hr_employees-new.tsx @@ -0,0 +1,194 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/hr_employees/hr_employeesSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + employee_no: '', + + name: '', + + email: '', + + manager: '', + + department: '', + + location: '', + + grade: '', + + hire_date: '', + + termination_date: '', + + status: 'Active', + + organizations: '', +}; + +const Hr_employeesNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/hr_employees/hr_employees-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/hr_employees/hr_employees-list')} + /> + + +
    +
    +
    + + ); +}; + +Hr_employeesNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_employeesNew; diff --git a/frontend/src/pages/hr_employees/hr_employees-table.tsx b/frontend/src/pages/hr_employees/hr_employees-table.tsx new file mode 100644 index 0000000..108ead6 --- /dev/null +++ b/frontend/src/pages/hr_employees/hr_employees-table.tsx @@ -0,0 +1,187 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableHr_employees from '../../components/Hr_employees/TableHr_employees'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/hr_employees/hr_employeesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Hr_employeesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'EmployeeNumber', title: 'employee_no' }, + { label: 'Name', title: 'name' }, + { label: 'Email', title: 'email' }, + { label: 'Department', title: 'department' }, + { label: 'Location', title: 'location' }, + { label: 'Grade', title: 'grade' }, + + { label: 'HireDate', title: 'hire_date', date: 'true' }, + { label: 'TerminationDate', title: 'termination_date', date: 'true' }, + + { label: 'Manager', title: 'manager' }, + + { + label: 'Status', + title: 'status', + type: 'enum', + options: ['Active', 'Inactive'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_HR_EMPLOYEES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getHr_employeesCSV = async () => { + const response = await axios({ + url: '/hr_employees?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'hr_employeesCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Hr_employees')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Hr_employeesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_employeesTablesPage; diff --git a/frontend/src/pages/hr_employees/hr_employees-view.tsx b/frontend/src/pages/hr_employees/hr_employees-view.tsx new file mode 100644 index 0000000..51b5538 --- /dev/null +++ b/frontend/src/pages/hr_employees/hr_employees-view.tsx @@ -0,0 +1,281 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/hr_employees/hr_employeesSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Hr_employeesView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { hr_employees } = useAppSelector((state) => state.hr_employees); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View hr_employees')} + + + + + + +
    +

    Organization

    + +

    {hr_employees?.org?.name ?? 'No data'}

    +
    + +
    +

    EmployeeNumber

    +

    {hr_employees?.employee_no}

    +
    + +
    +

    Name

    +

    {hr_employees?.name}

    +
    + +
    +

    Email

    +

    {hr_employees?.email}

    +
    + +
    +

    Manager

    + +

    {hr_employees?.manager?.name ?? 'No data'}

    +
    + +
    +

    Department

    +

    {hr_employees?.department}

    +
    + +
    +

    Location

    +

    {hr_employees?.location}

    +
    + +
    +

    Grade

    +

    {hr_employees?.grade}

    +
    + + + {hr_employees.hire_date ? ( + + ) : ( +

    No HireDate

    + )} +
    + + + {hr_employees.termination_date ? ( + + ) : ( +

    No TerminationDate

    + )} +
    + +
    +

    Status

    +

    {hr_employees?.status ?? 'No data'}

    +
    + +
    +

    organizations

    + +

    {hr_employees?.organizations?.name ?? 'No data'}

    +
    + + <> +

    Hr_attendance Employee

    + +
    + + + + + + + + + + + + {hr_employees.hr_attendance_employee && + Array.isArray(hr_employees.hr_attendance_employee) && + hr_employees.hr_attendance_employee.map((item: any) => ( + + router.push( + `/hr_attendance/hr_attendance-view/?id=${item.id}`, + ) + } + > + + + + + + + ))} + +
    DateHoursWorkedLeaveType
    + {dataFormatter.dateTimeFormatter(item.date)} + {item.hours_worked}{item.leave_type}
    +
    + {!hr_employees?.hr_attendance_employee?.length && ( +
    No data
    + )} +
    + + + <> +

    Hr_payroll Employee

    + +
    + + + + + + + + + + + + + + + + {hr_employees.hr_payroll_employee && + Array.isArray(hr_employees.hr_payroll_employee) && + hr_employees.hr_payroll_employee.map((item: any) => ( + + router.push( + `/hr_payroll/hr_payroll-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ))} + +
    PeriodBasePayBonusOvertimeHoursCurrency
    + {dataFormatter.dateTimeFormatter(item.period)} + {item.base_pay}{item.bonus} + {item.overtime_hours} + {item.currency}
    +
    + {!hr_employees?.hr_payroll_employee?.length && ( +
    No data
    + )} +
    + + + + + router.push('/hr_employees/hr_employees-list')} + /> +
    +
    + + ); +}; + +Hr_employeesView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_employeesView; diff --git a/frontend/src/pages/hr_payroll/[hr_payrollId].tsx b/frontend/src/pages/hr_payroll/[hr_payrollId].tsx new file mode 100644 index 0000000..dec1819 --- /dev/null +++ b/frontend/src/pages/hr_payroll/[hr_payrollId].tsx @@ -0,0 +1,212 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/hr_payroll/hr_payrollSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditHr_payroll = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + employee: null, + + period: new Date(), + + base_pay: '', + + bonus: '', + + overtime_hours: '', + + currency: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { hr_payroll } = useAppSelector((state) => state.hr_payroll); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { hr_payrollId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: hr_payrollId })); + }, [hr_payrollId]); + + useEffect(() => { + if (typeof hr_payroll === 'object') { + setInitialValues(hr_payroll); + } + }, [hr_payroll]); + + useEffect(() => { + if (typeof hr_payroll === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = hr_payroll[el]), + ); + + setInitialValues(newInitialVal); + } + }, [hr_payroll]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: hr_payrollId, data })); + await router.push('/hr_payroll/hr_payroll-list'); + }; + + return ( + <> + + {getPageTitle('Edit hr_payroll')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + setInitialValues({ ...initialValues, period: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/hr_payroll/hr_payroll-list')} + /> + + +
    +
    +
    + + ); +}; + +EditHr_payroll.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditHr_payroll; diff --git a/frontend/src/pages/hr_payroll/hr_payroll-edit.tsx b/frontend/src/pages/hr_payroll/hr_payroll-edit.tsx new file mode 100644 index 0000000..450f033 --- /dev/null +++ b/frontend/src/pages/hr_payroll/hr_payroll-edit.tsx @@ -0,0 +1,210 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/hr_payroll/hr_payrollSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditHr_payrollPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + employee: null, + + period: new Date(), + + base_pay: '', + + bonus: '', + + overtime_hours: '', + + currency: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { hr_payroll } = useAppSelector((state) => state.hr_payroll); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof hr_payroll === 'object') { + setInitialValues(hr_payroll); + } + }, [hr_payroll]); + + useEffect(() => { + if (typeof hr_payroll === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = hr_payroll[el]), + ); + setInitialValues(newInitialVal); + } + }, [hr_payroll]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/hr_payroll/hr_payroll-list'); + }; + + return ( + <> + + {getPageTitle('Edit hr_payroll')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + setInitialValues({ ...initialValues, period: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/hr_payroll/hr_payroll-list')} + /> + + +
    +
    +
    + + ); +}; + +EditHr_payrollPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditHr_payrollPage; diff --git a/frontend/src/pages/hr_payroll/hr_payroll-list.tsx b/frontend/src/pages/hr_payroll/hr_payroll-list.tsx new file mode 100644 index 0000000..2656e48 --- /dev/null +++ b/frontend/src/pages/hr_payroll/hr_payroll-list.tsx @@ -0,0 +1,175 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableHr_payroll from '../../components/Hr_payroll/TableHr_payroll'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/hr_payroll/hr_payrollSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Hr_payrollTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Currency', title: 'currency' }, + + { label: 'BasePay', title: 'base_pay', number: 'true' }, + { label: 'Bonus', title: 'bonus', number: 'true' }, + { label: 'OvertimeHours', title: 'overtime_hours', number: 'true' }, + { label: 'Period', title: 'period', date: 'true' }, + + { label: 'Employee', title: 'employee' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_HR_PAYROLL'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getHr_payrollCSV = async () => { + const response = await axios({ + url: '/hr_payroll?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'hr_payrollCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Hr_payroll')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + Switch to Table +
    +
    + + + + +
    + + + + + ); +}; + +Hr_payrollTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_payrollTablesPage; diff --git a/frontend/src/pages/hr_payroll/hr_payroll-new.tsx b/frontend/src/pages/hr_payroll/hr_payroll-new.tsx new file mode 100644 index 0000000..d17a7e3 --- /dev/null +++ b/frontend/src/pages/hr_payroll/hr_payroll-new.tsx @@ -0,0 +1,166 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/hr_payroll/hr_payrollSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + employee: '', + + period: '', + + base_pay: '', + + bonus: '', + + overtime_hours: '', + + currency: '', + + organizations: '', +}; + +const Hr_payrollNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/hr_payroll/hr_payroll-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/hr_payroll/hr_payroll-list')} + /> + + +
    +
    +
    + + ); +}; + +Hr_payrollNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_payrollNew; diff --git a/frontend/src/pages/hr_payroll/hr_payroll-table.tsx b/frontend/src/pages/hr_payroll/hr_payroll-table.tsx new file mode 100644 index 0000000..684da8a --- /dev/null +++ b/frontend/src/pages/hr_payroll/hr_payroll-table.tsx @@ -0,0 +1,174 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableHr_payroll from '../../components/Hr_payroll/TableHr_payroll'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/hr_payroll/hr_payrollSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Hr_payrollTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Currency', title: 'currency' }, + + { label: 'BasePay', title: 'base_pay', number: 'true' }, + { label: 'Bonus', title: 'bonus', number: 'true' }, + { label: 'OvertimeHours', title: 'overtime_hours', number: 'true' }, + { label: 'Period', title: 'period', date: 'true' }, + + { label: 'Employee', title: 'employee' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_HR_PAYROLL'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getHr_payrollCSV = async () => { + const response = await axios({ + url: '/hr_payroll?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'hr_payrollCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Hr_payroll')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Hr_payrollTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_payrollTablesPage; diff --git a/frontend/src/pages/hr_payroll/hr_payroll-view.tsx b/frontend/src/pages/hr_payroll/hr_payroll-view.tsx new file mode 100644 index 0000000..48480ce --- /dev/null +++ b/frontend/src/pages/hr_payroll/hr_payroll-view.tsx @@ -0,0 +1,139 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/hr_payroll/hr_payrollSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Hr_payrollView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { hr_payroll } = useAppSelector((state) => state.hr_payroll); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View hr_payroll')} + + + + + + +
    +

    Organization

    + +

    {hr_payroll?.org?.name ?? 'No data'}

    +
    + +
    +

    Employee

    + +

    {hr_payroll?.employee?.name ?? 'No data'}

    +
    + + + {hr_payroll.period ? ( + + ) : ( +

    No Period

    + )} +
    + +
    +

    BasePay

    +

    {hr_payroll?.base_pay || 'No data'}

    +
    + +
    +

    Bonus

    +

    {hr_payroll?.bonus || 'No data'}

    +
    + +
    +

    OvertimeHours

    +

    {hr_payroll?.overtime_hours || 'No data'}

    +
    + +
    +

    Currency

    +

    {hr_payroll?.currency}

    +
    + +
    +

    organizations

    + +

    {hr_payroll?.organizations?.name ?? 'No data'}

    +
    + + + + router.push('/hr_payroll/hr_payroll-list')} + /> +
    +
    + + ); +}; + +Hr_payrollView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_payrollView; diff --git a/frontend/src/pages/hr_positions/[hr_positionsId].tsx b/frontend/src/pages/hr_positions/[hr_positionsId].tsx new file mode 100644 index 0000000..e8ca5f5 --- /dev/null +++ b/frontend/src/pages/hr_positions/[hr_positionsId].tsx @@ -0,0 +1,184 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/hr_positions/hr_positionsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditHr_positions = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + title: '', + + department: '', + + level: '', + + location: '', + + status: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { hr_positions } = useAppSelector((state) => state.hr_positions); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { hr_positionsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: hr_positionsId })); + }, [hr_positionsId]); + + useEffect(() => { + if (typeof hr_positions === 'object') { + setInitialValues(hr_positions); + } + }, [hr_positions]); + + useEffect(() => { + if (typeof hr_positions === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = hr_positions[el]), + ); + + setInitialValues(newInitialVal); + } + }, [hr_positions]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: hr_positionsId, data })); + await router.push('/hr_positions/hr_positions-list'); + }; + + return ( + <> + + {getPageTitle('Edit hr_positions')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/hr_positions/hr_positions-list')} + /> + + +
    +
    +
    + + ); +}; + +EditHr_positions.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditHr_positions; diff --git a/frontend/src/pages/hr_positions/hr_positions-edit.tsx b/frontend/src/pages/hr_positions/hr_positions-edit.tsx new file mode 100644 index 0000000..4ccc021 --- /dev/null +++ b/frontend/src/pages/hr_positions/hr_positions-edit.tsx @@ -0,0 +1,182 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/hr_positions/hr_positionsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditHr_positionsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + title: '', + + department: '', + + level: '', + + location: '', + + status: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { hr_positions } = useAppSelector((state) => state.hr_positions); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof hr_positions === 'object') { + setInitialValues(hr_positions); + } + }, [hr_positions]); + + useEffect(() => { + if (typeof hr_positions === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = hr_positions[el]), + ); + setInitialValues(newInitialVal); + } + }, [hr_positions]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/hr_positions/hr_positions-list'); + }; + + return ( + <> + + {getPageTitle('Edit hr_positions')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/hr_positions/hr_positions-list')} + /> + + +
    +
    +
    + + ); +}; + +EditHr_positionsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditHr_positionsPage; diff --git a/frontend/src/pages/hr_positions/hr_positions-list.tsx b/frontend/src/pages/hr_positions/hr_positions-list.tsx new file mode 100644 index 0000000..ed0281e --- /dev/null +++ b/frontend/src/pages/hr_positions/hr_positions-list.tsx @@ -0,0 +1,183 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableHr_positions from '../../components/Hr_positions/TableHr_positions'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/hr_positions/hr_positionsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Hr_positionsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Title', title: 'title' }, + { label: 'Department', title: 'department' }, + { label: 'Level', title: 'level' }, + { label: 'Location', title: 'location' }, + + { + label: 'Status', + title: 'status', + type: 'enum', + options: ['Open', 'Closed'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_HR_POSITIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getHr_positionsCSV = async () => { + const response = await axios({ + url: '/hr_positions?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'hr_positionsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Hr_positions')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + + Switch to Table + +
    +
    + + + + +
    + + + + + ); +}; + +Hr_positionsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_positionsTablesPage; diff --git a/frontend/src/pages/hr_positions/hr_positions-new.tsx b/frontend/src/pages/hr_positions/hr_positions-new.tsx new file mode 100644 index 0000000..af21f68 --- /dev/null +++ b/frontend/src/pages/hr_positions/hr_positions-new.tsx @@ -0,0 +1,150 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/hr_positions/hr_positionsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + title: '', + + department: '', + + level: '', + + location: '', + + status: 'Open', + + organizations: '', +}; + +const Hr_positionsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/hr_positions/hr_positions-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/hr_positions/hr_positions-list')} + /> + + +
    +
    +
    + + ); +}; + +Hr_positionsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_positionsNew; diff --git a/frontend/src/pages/hr_positions/hr_positions-table.tsx b/frontend/src/pages/hr_positions/hr_positions-table.tsx new file mode 100644 index 0000000..902a107 --- /dev/null +++ b/frontend/src/pages/hr_positions/hr_positions-table.tsx @@ -0,0 +1,180 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableHr_positions from '../../components/Hr_positions/TableHr_positions'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/hr_positions/hr_positionsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Hr_positionsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Title', title: 'title' }, + { label: 'Department', title: 'department' }, + { label: 'Level', title: 'level' }, + { label: 'Location', title: 'location' }, + + { + label: 'Status', + title: 'status', + type: 'enum', + options: ['Open', 'Closed'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_HR_POSITIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getHr_positionsCSV = async () => { + const response = await axios({ + url: '/hr_positions?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'hr_positionsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Hr_positions')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Hr_positionsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_positionsTablesPage; diff --git a/frontend/src/pages/hr_positions/hr_positions-view.tsx b/frontend/src/pages/hr_positions/hr_positions-view.tsx new file mode 100644 index 0000000..0b67936 --- /dev/null +++ b/frontend/src/pages/hr_positions/hr_positions-view.tsx @@ -0,0 +1,174 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/hr_positions/hr_positionsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Hr_positionsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { hr_positions } = useAppSelector((state) => state.hr_positions); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View hr_positions')} + + + + + + +
    +

    Organization

    + +

    {hr_positions?.org?.name ?? 'No data'}

    +
    + +
    +

    Title

    +

    {hr_positions?.title}

    +
    + +
    +

    Department

    +

    {hr_positions?.department}

    +
    + +
    +

    Level

    +

    {hr_positions?.level}

    +
    + +
    +

    Location

    +

    {hr_positions?.location}

    +
    + +
    +

    Status

    +

    {hr_positions?.status ?? 'No data'}

    +
    + +
    +

    organizations

    + +

    {hr_positions?.organizations?.name ?? 'No data'}

    +
    + + <> +

    Hr_reqs Position

    + +
    + + + + + + + + + + + + + + + + {hr_positions.hr_reqs_position && + Array.isArray(hr_positions.hr_reqs_position) && + hr_positions.hr_reqs_position.map((item: any) => ( + + router.push(`/hr_reqs/hr_reqs-view/?id=${item.id}`) + } + > + + + + + + + + + + + ))} + +
    OpenedAtFilledAtStatusRecruiterPriority
    + {dataFormatter.dateTimeFormatter(item.opened_at)} + + {dataFormatter.dateTimeFormatter(item.filled_at)} + {item.status}{item.recruiter}{item.priority}
    +
    + {!hr_positions?.hr_reqs_position?.length && ( +
    No data
    + )} +
    + + + + + router.push('/hr_positions/hr_positions-list')} + /> +
    +
    + + ); +}; + +Hr_positionsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_positionsView; diff --git a/frontend/src/pages/hr_reqs/[hr_reqsId].tsx b/frontend/src/pages/hr_reqs/[hr_reqsId].tsx new file mode 100644 index 0000000..1b25af5 --- /dev/null +++ b/frontend/src/pages/hr_reqs/[hr_reqsId].tsx @@ -0,0 +1,233 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/hr_reqs/hr_reqsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditHr_reqs = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + position: null, + + opened_at: new Date(), + + filled_at: new Date(), + + status: '', + + recruiter: '', + + priority: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { hr_reqs } = useAppSelector((state) => state.hr_reqs); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { hr_reqsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: hr_reqsId })); + }, [hr_reqsId]); + + useEffect(() => { + if (typeof hr_reqs === 'object') { + setInitialValues(hr_reqs); + } + }, [hr_reqs]); + + useEffect(() => { + if (typeof hr_reqs === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = hr_reqs[el])); + + setInitialValues(newInitialVal); + } + }, [hr_reqs]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: hr_reqsId, data })); + await router.push('/hr_reqs/hr_reqs-list'); + }; + + return ( + <> + + {getPageTitle('Edit hr_reqs')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + setInitialValues({ ...initialValues, opened_at: date }) + } + /> + + + + + setInitialValues({ ...initialValues, filled_at: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/hr_reqs/hr_reqs-list')} + /> + + +
    +
    +
    + + ); +}; + +EditHr_reqs.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditHr_reqs; diff --git a/frontend/src/pages/hr_reqs/hr_reqs-edit.tsx b/frontend/src/pages/hr_reqs/hr_reqs-edit.tsx new file mode 100644 index 0000000..71200d1 --- /dev/null +++ b/frontend/src/pages/hr_reqs/hr_reqs-edit.tsx @@ -0,0 +1,231 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/hr_reqs/hr_reqsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditHr_reqsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + position: null, + + opened_at: new Date(), + + filled_at: new Date(), + + status: '', + + recruiter: '', + + priority: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { hr_reqs } = useAppSelector((state) => state.hr_reqs); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof hr_reqs === 'object') { + setInitialValues(hr_reqs); + } + }, [hr_reqs]); + + useEffect(() => { + if (typeof hr_reqs === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = hr_reqs[el])); + setInitialValues(newInitialVal); + } + }, [hr_reqs]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/hr_reqs/hr_reqs-list'); + }; + + return ( + <> + + {getPageTitle('Edit hr_reqs')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + setInitialValues({ ...initialValues, opened_at: date }) + } + /> + + + + + setInitialValues({ ...initialValues, filled_at: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/hr_reqs/hr_reqs-list')} + /> + + +
    +
    +
    + + ); +}; + +EditHr_reqsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditHr_reqsPage; diff --git a/frontend/src/pages/hr_reqs/hr_reqs-list.tsx b/frontend/src/pages/hr_reqs/hr_reqs-list.tsx new file mode 100644 index 0000000..4856729 --- /dev/null +++ b/frontend/src/pages/hr_reqs/hr_reqs-list.tsx @@ -0,0 +1,186 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableHr_reqs from '../../components/Hr_reqs/TableHr_reqs'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/hr_reqs/hr_reqsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Hr_reqsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Recruiter', title: 'recruiter' }, + + { label: 'OpenedAt', title: 'opened_at', date: 'true' }, + { label: 'FilledAt', title: 'filled_at', date: 'true' }, + + { label: 'Position', title: 'position' }, + + { + label: 'Status', + title: 'status', + type: 'enum', + options: ['Open', 'Closed', 'OnHold'], + }, + { + label: 'Priority', + title: 'priority', + type: 'enum', + options: ['High', 'Medium', 'Low'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_HR_REQS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getHr_reqsCSV = async () => { + const response = await axios({ + url: '/hr_reqs?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'hr_reqsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Hr_reqs')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + Switch to Table +
    +
    + + + + +
    + + + + + ); +}; + +Hr_reqsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_reqsTablesPage; diff --git a/frontend/src/pages/hr_reqs/hr_reqs-new.tsx b/frontend/src/pages/hr_reqs/hr_reqs-new.tsx new file mode 100644 index 0000000..c0c6329 --- /dev/null +++ b/frontend/src/pages/hr_reqs/hr_reqs-new.tsx @@ -0,0 +1,178 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/hr_reqs/hr_reqsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + position: '', + + opened_at: '', + + filled_at: '', + + status: 'Open', + + recruiter: '', + + priority: 'High', + + organizations: '', +}; + +const Hr_reqsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/hr_reqs/hr_reqs-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/hr_reqs/hr_reqs-list')} + /> + + +
    +
    +
    + + ); +}; + +Hr_reqsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_reqsNew; diff --git a/frontend/src/pages/hr_reqs/hr_reqs-table.tsx b/frontend/src/pages/hr_reqs/hr_reqs-table.tsx new file mode 100644 index 0000000..9347f77 --- /dev/null +++ b/frontend/src/pages/hr_reqs/hr_reqs-table.tsx @@ -0,0 +1,185 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableHr_reqs from '../../components/Hr_reqs/TableHr_reqs'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/hr_reqs/hr_reqsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Hr_reqsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Recruiter', title: 'recruiter' }, + + { label: 'OpenedAt', title: 'opened_at', date: 'true' }, + { label: 'FilledAt', title: 'filled_at', date: 'true' }, + + { label: 'Position', title: 'position' }, + + { + label: 'Status', + title: 'status', + type: 'enum', + options: ['Open', 'Closed', 'OnHold'], + }, + { + label: 'Priority', + title: 'priority', + type: 'enum', + options: ['High', 'Medium', 'Low'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_HR_REQS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getHr_reqsCSV = async () => { + const response = await axios({ + url: '/hr_reqs?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'hr_reqsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Hr_reqs')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Hr_reqsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_reqsTablesPage; diff --git a/frontend/src/pages/hr_reqs/hr_reqs-view.tsx b/frontend/src/pages/hr_reqs/hr_reqs-view.tsx new file mode 100644 index 0000000..5e53006 --- /dev/null +++ b/frontend/src/pages/hr_reqs/hr_reqs-view.tsx @@ -0,0 +1,214 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/hr_reqs/hr_reqsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Hr_reqsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { hr_reqs } = useAppSelector((state) => state.hr_reqs); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View hr_reqs')} + + + + + + +
    +

    Organization

    + +

    {hr_reqs?.org?.name ?? 'No data'}

    +
    + +
    +

    Position

    + +

    {hr_reqs?.position?.title ?? 'No data'}

    +
    + + + {hr_reqs.opened_at ? ( + + ) : ( +

    No OpenedAt

    + )} +
    + + + {hr_reqs.filled_at ? ( + + ) : ( +

    No FilledAt

    + )} +
    + +
    +

    Status

    +

    {hr_reqs?.status ?? 'No data'}

    +
    + +
    +

    Recruiter

    +

    {hr_reqs?.recruiter}

    +
    + +
    +

    Priority

    +

    {hr_reqs?.priority ?? 'No data'}

    +
    + +
    +

    organizations

    + +

    {hr_reqs?.organizations?.name ?? 'No data'}

    +
    + + <> +

    Hr_applicants Requisition

    + +
    + + + + + + + + + + + + + + + + {hr_reqs.hr_applicants_req && + Array.isArray(hr_reqs.hr_applicants_req) && + hr_reqs.hr_applicants_req.map((item: any) => ( + + router.push( + `/hr_applicants/hr_applicants-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ))} + +
    NameStageSourceOfferExtendedAtOfferAcceptedAt
    {item.name}{item.stage}{item.source} + {dataFormatter.dateTimeFormatter( + item.offer_extended_at, + )} + + {dataFormatter.dateTimeFormatter( + item.offer_accepted_at, + )} +
    +
    + {!hr_reqs?.hr_applicants_req?.length && ( +
    No data
    + )} +
    + + + + + router.push('/hr_reqs/hr_reqs-list')} + /> +
    +
    + + ); +}; + +Hr_reqsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Hr_reqsView; diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx new file mode 100644 index 0000000..d4802c7 --- /dev/null +++ b/frontend/src/pages/index.tsx @@ -0,0 +1,206 @@ +import React, { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import Link from 'next/link'; +import { useAppSelector } from '../stores/hooks'; +import LayoutGuest from '../layouts/Guest'; +import WebSiteHeader from '../components/WebPageComponents/Header'; +import WebSiteFooter from '../components/WebPageComponents/Footer'; +import { + HeroDesigns, + FeaturesDesigns, + PricingDesigns, + FaqDesigns, +} from '../components/WebPageComponents/designs'; + +import HeroSection from '../components/WebPageComponents/HeroComponent'; + +import FeaturesSection from '../components/WebPageComponents/FeaturesComponent'; + +import PricingSection from '../components/WebPageComponents/PricingComponent'; + +import FaqSection from '../components/WebPageComponents/FaqComponent'; + +export default function WebSite() { + const cardsStyle = useAppSelector((state) => state.style.cardsStyle); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const projectName = 'Title Enterprise Dashboards Finance HR Ops'; + + useEffect(() => { + const darkElement = document.querySelector('body .dark'); + if (darkElement) { + darkElement.classList.remove('dark'); + } + }, []); + const pages = [ + { + href: '/services', + label: 'services', + }, + + { + href: '/contact', + label: 'contact', + }, + + { + href: '/faq', + label: 'FAQ', + }, + ]; + + const features_points = [ + { + name: 'Seamless Integrations', + description: + 'Connect effortlessly with leading data warehouses and ERP systems to centralize your data and enhance decision-making.', + icon: 'mdiLinkVariant', + }, + { + name: 'Advanced Analytics', + description: + 'Leverage real-time analytics to gain insights into your finance, HR, and operations, enabling data-driven strategies.', + icon: 'mdiChartLine', + }, + { + name: 'Customizable Dashboards', + description: + 'Tailor your dashboards to fit your unique business needs, ensuring you have the right information at your fingertips.', + icon: 'mdiViewDashboardOutline', + }, + ]; + + const pricing_features = { + standard: { + features: ['Basic Dashboard Access', 'Email Support', 'Monthly Reports'], + limited_features: ['Limited Integrations', 'Basic Analytics'], + }, + premium: { + features: [ + 'Advanced Dashboard Access', + 'Priority Email Support', + 'Weekly Reports', + ], + also_included: [ + 'Enhanced Integrations', + 'Advanced Analytics', + 'Customizable Dashboards', + ], + }, + business: { + features: [ + 'Full Dashboard Access', + 'Dedicated Account Manager', + 'Daily Reports', + 'Comprehensive Integrations', + 'Real-Time Analytics', + 'Custom Solutions', + ], + }, + }; + + const description = { + standard: + 'Ideal for individuals or small teams looking to get started with essential dashboard features and basic support.', + premium: + 'Perfect for small businesses or startups seeking advanced features, enhanced support, and more frequent reporting.', + business: + 'Designed for large enterprises requiring comprehensive access, dedicated support, and tailored solutions to meet complex needs.', + }; + + const faqs = [ + { + question: 'What is ${projectName} and how does it work?', + answer: + '${projectName} is an enterprise dashboard solution that integrates with your existing systems to provide real-time analytics and insights across finance, HR, and operations. It centralizes data from various sources, allowing you to make informed decisions.', + }, + { + question: 'How secure is my data with ${projectName}?', + answer: + 'We prioritize data security by implementing robust encryption, role-based access controls, and regular security audits. Your data is stored securely and is only accessible to authorized users.', + }, + { + question: 'Can I customize the dashboards in ${projectName}?', + answer: + 'Yes, ${projectName} offers customizable dashboards that allow you to tailor the layout and data visualization to meet your specific business needs. You can add, remove, or rearrange widgets as required.', + }, + { + question: 'What integrations are available with ${projectName}?', + answer: + '${projectName} supports integrations with major data warehouses like Snowflake and BigQuery, ERP systems like SAP and Oracle, and HR platforms like Workday. This ensures seamless data flow and comprehensive analytics.', + }, + { + question: 'What kind of support can I expect with ${projectName}?', + answer: + 'We offer various support levels depending on your plan. Standard users receive email support, while Premium and Business users have access to priority support and dedicated account managers.', + }, + { + question: 'Is there a free trial available for ${projectName}?', + answer: + 'Yes, we offer a free trial period for you to explore the features and capabilities of ${projectName}. This allows you to evaluate its fit for your organization before committing to a plan.', + }, + { + question: 'How often is the data updated in ${projectName}?', + answer: + 'Data updates depend on the integration and plan. Finance and HR data are typically refreshed nightly, while operations data can be near real-time, ensuring you have the latest insights.', + }, + ]; + + return ( +
    + + {`Enterprise Dashboard Services`} + + + +
    + + + + + + + +
    + +
    + ); +} + +WebSite.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx new file mode 100644 index 0000000..199bc18 --- /dev/null +++ b/frontend/src/pages/login.tsx @@ -0,0 +1,382 @@ +import React, { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import { useTranslation } from 'next-i18next'; +import BaseButton from '../components/BaseButton'; +import CardBox from '../components/CardBox'; +import BaseIcon from '../components/BaseIcon'; +import { mdiInformation, mdiEye, mdiEyeOff } from '@mdi/js'; +import SectionFullScreen from '../components/SectionFullScreen'; +import LayoutGuest from '../layouts/Guest'; +import { Field, Form, Formik } from 'formik'; +import FormField from '../components/FormField'; +import FormCheckRadio from '../components/FormCheckRadio'; +import BaseDivider from '../components/BaseDivider'; +import BaseButtons from '../components/BaseButtons'; +import { useRouter } from 'next/router'; +import { getPageTitle } from '../config'; +import { findMe, loginUser, resetAction } from '../stores/authSlice'; +import { useAppDispatch, useAppSelector } from '../stores/hooks'; +import Link from 'next/link'; +import { toast, ToastContainer } from 'react-toastify'; +import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; + +export default function Login() { + const { t } = useTranslation('common'); + const router = useRouter(); + const dispatch = useAppDispatch(); + const textColor = useAppSelector((state) => state.style.linkColor); + const iconsColor = useAppSelector((state) => state.style.iconsColor); + const notify = (type, msg) => toast(msg, { type }); + const [illustrationImage, setIllustrationImage] = useState({ + src: undefined, + photographer: undefined, + photographer_url: undefined, + }); + const [illustrationVideo, setIllustrationVideo] = useState({ + video_files: [], + }); + const [contentType, setContentType] = useState('image'); + const [contentPosition, setContentPosition] = useState('left'); + const [showPassword, setShowPassword] = useState(false); + const { + currentUser, + isFetching, + errorMessage, + token, + notify: notifyState, + } = useAppSelector((state) => state.auth); + const [initialValues, setInitialValues] = React.useState({ + email: 'super_admin@flatlogic.com', + password: 'f606f4c2', + remember: true, + }); + + const title = 'Title Enterprise Dashboards Finance HR Ops'; + + // Fetch Pexels image/video + useEffect(() => { + async function fetchData() { + const image = await getPexelsImage(); + const video = await getPexelsVideo(); + setIllustrationImage(image); + setIllustrationVideo(video); + } + fetchData(); + }, []); + // Fetch user data + useEffect(() => { + if (token) { + dispatch(findMe()); + } + }, [token, dispatch]); + // Redirect to dashboard if user is logged in + useEffect(() => { + if (currentUser?.id) { + router.push('/dashboard'); + } + }, [currentUser?.id, router]); + // Show error message if there is one + useEffect(() => { + if (errorMessage) { + notify('error', errorMessage); + } + }, [errorMessage]); + // Show notification if there is one + useEffect(() => { + if (notifyState?.showNotification) { + notify('success', notifyState?.textNotification); + dispatch(resetAction()); + } + }, [notifyState?.showNotification]); + + const togglePasswordVisibility = () => { + setShowPassword(!showPassword); + }; + + const handleSubmit = async (value) => { + const { remember, ...rest } = value; + await dispatch(loginUser(rest)); + }; + + const setLogin = (target: HTMLElement) => { + setInitialValues((prev) => ({ + ...prev, + email: target.innerText.trim(), + password: target.dataset.password ?? '', + })); + }; + + const imageBlock = (image) => ( + + ); + + const videoBlock = (video) => { + if (video?.video_files?.length > 0) { + return ( +
    + + +
    + ); + } + }; + + return ( +
    + + + {getPageTitle(t('pages.login.pageTitle', { defaultValue: 'Login' }))} + + + + +
    + {contentType === 'image' && contentPosition !== 'background' + ? imageBlock(illustrationImage) + : null} + {contentType === 'video' && contentPosition !== 'background' + ? videoBlock(illustrationVideo) + : null} +
    + + +

    {title}

    + + +
    +
    +

    + Use{' '} + setLogin(e.target)} + > + super_admin@flatlogic.com + + {' / '} + f606f4c2 + {' / '} + to login as Super Admin +

    + +

    + Use{' '} + setLogin(e.target)} + > + admin@flatlogic.com + + {' / '} + f606f4c2 + {' / '} + to login as Admin +

    +

    + Use{' '} + setLogin(e.target)} + > + client@hello.com + + {' / '} + 146ab785f002 + {' / '} + to login as User +

    +
    +
    + +
    +
    +
    + + + handleSubmit(values)} + > +
    + + + + +
    + + + +
    + +
    +
    + +
    + + + + + + {t('pages.login.form.forgotPassword', { + defaultValue: 'Forgot password?', + })} + +
    + + + + + + +
    +

    + {t('pages.login.form.noAccountYet', { + defaultValue: 'Don’t have an account yet?', + })}{' '} + + {t('pages.login.form.newAccount', { + defaultValue: 'New Account', + })} + +

    + +
    +
    +
    +
    +
    +
    +

    + © 2024 {title}.{' '} + {t('pages.login.footer.copyright', { + defaultValue: '© All rights reserved', + })} +

    + + {t('pages.login.footer.privacy', { defaultValue: 'Privacy Policy' })} + +
    + +
    + ); +} + +Login.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/pages/ops_incidents/[ops_incidentsId].tsx b/frontend/src/pages/ops_incidents/[ops_incidentsId].tsx new file mode 100644 index 0000000..e58377e --- /dev/null +++ b/frontend/src/pages/ops_incidents/[ops_incidentsId].tsx @@ -0,0 +1,233 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/ops_incidents/ops_incidentsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditOps_incidents = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + plant: null, + + line: null, + + occurred_at: new Date(), + + category: '', + + severity: '', + + duration_min: '', + + root_cause: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { ops_incidents } = useAppSelector((state) => state.ops_incidents); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { ops_incidentsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: ops_incidentsId })); + }, [ops_incidentsId]); + + useEffect(() => { + if (typeof ops_incidents === 'object') { + setInitialValues(ops_incidents); + } + }, [ops_incidents]); + + useEffect(() => { + if (typeof ops_incidents === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = ops_incidents[el]), + ); + + setInitialValues(newInitialVal); + } + }, [ops_incidents]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: ops_incidentsId, data })); + await router.push('/ops_incidents/ops_incidents-list'); + }; + + return ( + <> + + {getPageTitle('Edit ops_incidents')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + setInitialValues({ ...initialValues, occurred_at: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/ops_incidents/ops_incidents-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditOps_incidents.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOps_incidents; diff --git a/frontend/src/pages/ops_incidents/ops_incidents-edit.tsx b/frontend/src/pages/ops_incidents/ops_incidents-edit.tsx new file mode 100644 index 0000000..a708ba7 --- /dev/null +++ b/frontend/src/pages/ops_incidents/ops_incidents-edit.tsx @@ -0,0 +1,231 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/ops_incidents/ops_incidentsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditOps_incidentsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + plant: null, + + line: null, + + occurred_at: new Date(), + + category: '', + + severity: '', + + duration_min: '', + + root_cause: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { ops_incidents } = useAppSelector((state) => state.ops_incidents); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof ops_incidents === 'object') { + setInitialValues(ops_incidents); + } + }, [ops_incidents]); + + useEffect(() => { + if (typeof ops_incidents === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = ops_incidents[el]), + ); + setInitialValues(newInitialVal); + } + }, [ops_incidents]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/ops_incidents/ops_incidents-list'); + }; + + return ( + <> + + {getPageTitle('Edit ops_incidents')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + setInitialValues({ ...initialValues, occurred_at: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/ops_incidents/ops_incidents-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditOps_incidentsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOps_incidentsPage; diff --git a/frontend/src/pages/ops_incidents/ops_incidents-list.tsx b/frontend/src/pages/ops_incidents/ops_incidents-list.tsx new file mode 100644 index 0000000..1a070a9 --- /dev/null +++ b/frontend/src/pages/ops_incidents/ops_incidents-list.tsx @@ -0,0 +1,188 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableOps_incidents from '../../components/Ops_incidents/TableOps_incidents'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/ops_incidents/ops_incidentsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_incidentsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Category', title: 'category' }, + { label: 'RootCause', title: 'root_cause' }, + { label: 'DurationMinutes', title: 'duration_min', number: 'true' }, + + { label: 'OccurredAt', title: 'occurred_at', date: 'true' }, + + { label: 'Plant', title: 'plant' }, + + { label: 'Line', title: 'line' }, + + { + label: 'Severity', + title: 'severity', + type: 'enum', + options: ['Low', 'Medium', 'High'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_OPS_INCIDENTS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOps_incidentsCSV = async () => { + const response = await axios({ + url: '/ops_incidents?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'ops_incidentsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Ops_incidents')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + + Switch to Table + +
    +
    + + + + +
    + + + + + ); +}; + +Ops_incidentsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_incidentsTablesPage; diff --git a/frontend/src/pages/ops_incidents/ops_incidents-new.tsx b/frontend/src/pages/ops_incidents/ops_incidents-new.tsx new file mode 100644 index 0000000..da44656 --- /dev/null +++ b/frontend/src/pages/ops_incidents/ops_incidents-new.tsx @@ -0,0 +1,186 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/ops_incidents/ops_incidentsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + plant: '', + + line: '', + + occurred_at: '', + + category: '', + + severity: 'Low', + + duration_min: '', + + root_cause: '', + + organizations: '', +}; + +const Ops_incidentsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/ops_incidents/ops_incidents-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/ops_incidents/ops_incidents-list') + } + /> + + +
    +
    +
    + + ); +}; + +Ops_incidentsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_incidentsNew; diff --git a/frontend/src/pages/ops_incidents/ops_incidents-table.tsx b/frontend/src/pages/ops_incidents/ops_incidents-table.tsx new file mode 100644 index 0000000..84f28b5 --- /dev/null +++ b/frontend/src/pages/ops_incidents/ops_incidents-table.tsx @@ -0,0 +1,185 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableOps_incidents from '../../components/Ops_incidents/TableOps_incidents'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/ops_incidents/ops_incidentsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_incidentsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Category', title: 'category' }, + { label: 'RootCause', title: 'root_cause' }, + { label: 'DurationMinutes', title: 'duration_min', number: 'true' }, + + { label: 'OccurredAt', title: 'occurred_at', date: 'true' }, + + { label: 'Plant', title: 'plant' }, + + { label: 'Line', title: 'line' }, + + { + label: 'Severity', + title: 'severity', + type: 'enum', + options: ['Low', 'Medium', 'High'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_OPS_INCIDENTS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOps_incidentsCSV = async () => { + const response = await axios({ + url: '/ops_incidents?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'ops_incidentsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Ops_incidents')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Ops_incidentsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_incidentsTablesPage; diff --git a/frontend/src/pages/ops_incidents/ops_incidents-view.tsx b/frontend/src/pages/ops_incidents/ops_incidents-view.tsx new file mode 100644 index 0000000..6758e51 --- /dev/null +++ b/frontend/src/pages/ops_incidents/ops_incidents-view.tsx @@ -0,0 +1,147 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/ops_incidents/ops_incidentsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_incidentsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { ops_incidents } = useAppSelector((state) => state.ops_incidents); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View ops_incidents')} + + + + + + +
    +

    Organization

    + +

    {ops_incidents?.org?.name ?? 'No data'}

    +
    + +
    +

    Plant

    + +

    {ops_incidents?.plant?.name ?? 'No data'}

    +
    + +
    +

    Line

    + +

    {ops_incidents?.line?.name ?? 'No data'}

    +
    + + + {ops_incidents.occurred_at ? ( + + ) : ( +

    No OccurredAt

    + )} +
    + +
    +

    Category

    +

    {ops_incidents?.category}

    +
    + +
    +

    Severity

    +

    {ops_incidents?.severity ?? 'No data'}

    +
    + +
    +

    DurationMinutes

    +

    {ops_incidents?.duration_min || 'No data'}

    +
    + +
    +

    RootCause

    +

    {ops_incidents?.root_cause}

    +
    + +
    +

    organizations

    + +

    {ops_incidents?.organizations?.name ?? 'No data'}

    +
    + + + + router.push('/ops_incidents/ops_incidents-list')} + /> +
    +
    + + ); +}; + +Ops_incidentsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_incidentsView; diff --git a/frontend/src/pages/ops_inventory/[ops_inventoryId].tsx b/frontend/src/pages/ops_inventory/[ops_inventoryId].tsx new file mode 100644 index 0000000..4b6ae97 --- /dev/null +++ b/frontend/src/pages/ops_inventory/[ops_inventoryId].tsx @@ -0,0 +1,192 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/ops_inventory/ops_inventorySlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditOps_inventory = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + sku: '', + + location: '', + + on_hand: '', + + on_order: '', + + safety_stock: '', + + unit_cost: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { ops_inventory } = useAppSelector((state) => state.ops_inventory); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { ops_inventoryId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: ops_inventoryId })); + }, [ops_inventoryId]); + + useEffect(() => { + if (typeof ops_inventory === 'object') { + setInitialValues(ops_inventory); + } + }, [ops_inventory]); + + useEffect(() => { + if (typeof ops_inventory === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = ops_inventory[el]), + ); + + setInitialValues(newInitialVal); + } + }, [ops_inventory]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: ops_inventoryId, data })); + await router.push('/ops_inventory/ops_inventory-list'); + }; + + return ( + <> + + {getPageTitle('Edit ops_inventory')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/ops_inventory/ops_inventory-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditOps_inventory.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOps_inventory; diff --git a/frontend/src/pages/ops_inventory/ops_inventory-edit.tsx b/frontend/src/pages/ops_inventory/ops_inventory-edit.tsx new file mode 100644 index 0000000..63a2a80 --- /dev/null +++ b/frontend/src/pages/ops_inventory/ops_inventory-edit.tsx @@ -0,0 +1,190 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/ops_inventory/ops_inventorySlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditOps_inventoryPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + sku: '', + + location: '', + + on_hand: '', + + on_order: '', + + safety_stock: '', + + unit_cost: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { ops_inventory } = useAppSelector((state) => state.ops_inventory); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof ops_inventory === 'object') { + setInitialValues(ops_inventory); + } + }, [ops_inventory]); + + useEffect(() => { + if (typeof ops_inventory === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = ops_inventory[el]), + ); + setInitialValues(newInitialVal); + } + }, [ops_inventory]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/ops_inventory/ops_inventory-list'); + }; + + return ( + <> + + {getPageTitle('Edit ops_inventory')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/ops_inventory/ops_inventory-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditOps_inventoryPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOps_inventoryPage; diff --git a/frontend/src/pages/ops_inventory/ops_inventory-list.tsx b/frontend/src/pages/ops_inventory/ops_inventory-list.tsx new file mode 100644 index 0000000..dcf1279 --- /dev/null +++ b/frontend/src/pages/ops_inventory/ops_inventory-list.tsx @@ -0,0 +1,178 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableOps_inventory from '../../components/Ops_inventory/TableOps_inventory'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/ops_inventory/ops_inventorySlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_inventoryTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'SKU', title: 'sku' }, + { label: 'Location', title: 'location' }, + { label: 'OnHand', title: 'on_hand', number: 'true' }, + { label: 'OnOrder', title: 'on_order', number: 'true' }, + { label: 'SafetyStock', title: 'safety_stock', number: 'true' }, + { label: 'UnitCost', title: 'unit_cost', number: 'true' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_OPS_INVENTORY'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOps_inventoryCSV = async () => { + const response = await axios({ + url: '/ops_inventory?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'ops_inventoryCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Ops_inventory')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + + Switch to Table + +
    +
    + + + + +
    + + + + + ); +}; + +Ops_inventoryTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_inventoryTablesPage; diff --git a/frontend/src/pages/ops_inventory/ops_inventory-new.tsx b/frontend/src/pages/ops_inventory/ops_inventory-new.tsx new file mode 100644 index 0000000..f5c6960 --- /dev/null +++ b/frontend/src/pages/ops_inventory/ops_inventory-new.tsx @@ -0,0 +1,158 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/ops_inventory/ops_inventorySlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + sku: '', + + location: '', + + on_hand: '', + + on_order: '', + + safety_stock: '', + + unit_cost: '', + + organizations: '', +}; + +const Ops_inventoryNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/ops_inventory/ops_inventory-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/ops_inventory/ops_inventory-list') + } + /> + + +
    +
    +
    + + ); +}; + +Ops_inventoryNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_inventoryNew; diff --git a/frontend/src/pages/ops_inventory/ops_inventory-table.tsx b/frontend/src/pages/ops_inventory/ops_inventory-table.tsx new file mode 100644 index 0000000..5cbed07 --- /dev/null +++ b/frontend/src/pages/ops_inventory/ops_inventory-table.tsx @@ -0,0 +1,175 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableOps_inventory from '../../components/Ops_inventory/TableOps_inventory'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/ops_inventory/ops_inventorySlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_inventoryTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'SKU', title: 'sku' }, + { label: 'Location', title: 'location' }, + { label: 'OnHand', title: 'on_hand', number: 'true' }, + { label: 'OnOrder', title: 'on_order', number: 'true' }, + { label: 'SafetyStock', title: 'safety_stock', number: 'true' }, + { label: 'UnitCost', title: 'unit_cost', number: 'true' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_OPS_INVENTORY'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOps_inventoryCSV = async () => { + const response = await axios({ + url: '/ops_inventory?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'ops_inventoryCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Ops_inventory')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Ops_inventoryTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_inventoryTablesPage; diff --git a/frontend/src/pages/ops_inventory/ops_inventory-view.tsx b/frontend/src/pages/ops_inventory/ops_inventory-view.tsx new file mode 100644 index 0000000..5cc34bc --- /dev/null +++ b/frontend/src/pages/ops_inventory/ops_inventory-view.tsx @@ -0,0 +1,124 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/ops_inventory/ops_inventorySlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_inventoryView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { ops_inventory } = useAppSelector((state) => state.ops_inventory); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View ops_inventory')} + + + + + + +
    +

    Organization

    + +

    {ops_inventory?.org?.name ?? 'No data'}

    +
    + +
    +

    SKU

    +

    {ops_inventory?.sku}

    +
    + +
    +

    Location

    +

    {ops_inventory?.location}

    +
    + +
    +

    OnHand

    +

    {ops_inventory?.on_hand || 'No data'}

    +
    + +
    +

    OnOrder

    +

    {ops_inventory?.on_order || 'No data'}

    +
    + +
    +

    SafetyStock

    +

    {ops_inventory?.safety_stock || 'No data'}

    +
    + +
    +

    UnitCost

    +

    {ops_inventory?.unit_cost || 'No data'}

    +
    + +
    +

    organizations

    + +

    {ops_inventory?.organizations?.name ?? 'No data'}

    +
    + + + + router.push('/ops_inventory/ops_inventory-list')} + /> +
    +
    + + ); +}; + +Ops_inventoryView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_inventoryView; diff --git a/frontend/src/pages/ops_lines/[ops_linesId].tsx b/frontend/src/pages/ops_lines/[ops_linesId].tsx new file mode 100644 index 0000000..6ee2f9f --- /dev/null +++ b/frontend/src/pages/ops_lines/[ops_linesId].tsx @@ -0,0 +1,169 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/ops_lines/ops_linesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditOps_lines = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + plant: null, + + name: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { ops_lines } = useAppSelector((state) => state.ops_lines); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { ops_linesId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: ops_linesId })); + }, [ops_linesId]); + + useEffect(() => { + if (typeof ops_lines === 'object') { + setInitialValues(ops_lines); + } + }, [ops_lines]); + + useEffect(() => { + if (typeof ops_lines === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = ops_lines[el]), + ); + + setInitialValues(newInitialVal); + } + }, [ops_lines]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: ops_linesId, data })); + await router.push('/ops_lines/ops_lines-list'); + }; + + return ( + <> + + {getPageTitle('Edit ops_lines')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + router.push('/ops_lines/ops_lines-list')} + /> + + +
    +
    +
    + + ); +}; + +EditOps_lines.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOps_lines; diff --git a/frontend/src/pages/ops_lines/ops_lines-edit.tsx b/frontend/src/pages/ops_lines/ops_lines-edit.tsx new file mode 100644 index 0000000..35fc11a --- /dev/null +++ b/frontend/src/pages/ops_lines/ops_lines-edit.tsx @@ -0,0 +1,167 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/ops_lines/ops_linesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditOps_linesPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + plant: null, + + name: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { ops_lines } = useAppSelector((state) => state.ops_lines); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof ops_lines === 'object') { + setInitialValues(ops_lines); + } + }, [ops_lines]); + + useEffect(() => { + if (typeof ops_lines === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = ops_lines[el]), + ); + setInitialValues(newInitialVal); + } + }, [ops_lines]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/ops_lines/ops_lines-list'); + }; + + return ( + <> + + {getPageTitle('Edit ops_lines')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + router.push('/ops_lines/ops_lines-list')} + /> + + +
    +
    +
    + + ); +}; + +EditOps_linesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOps_linesPage; diff --git a/frontend/src/pages/ops_lines/ops_lines-list.tsx b/frontend/src/pages/ops_lines/ops_lines-list.tsx new file mode 100644 index 0000000..cd543cd --- /dev/null +++ b/frontend/src/pages/ops_lines/ops_lines-list.tsx @@ -0,0 +1,170 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableOps_lines from '../../components/Ops_lines/TableOps_lines'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/ops_lines/ops_linesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_linesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + + { label: 'Plant', title: 'plant' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_OPS_LINES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOps_linesCSV = async () => { + const response = await axios({ + url: '/ops_lines?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'ops_linesCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Ops_lines')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + Switch to Table +
    +
    + + + + +
    + + + + + ); +}; + +Ops_linesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_linesTablesPage; diff --git a/frontend/src/pages/ops_lines/ops_lines-new.tsx b/frontend/src/pages/ops_lines/ops_lines-new.tsx new file mode 100644 index 0000000..daca2b3 --- /dev/null +++ b/frontend/src/pages/ops_lines/ops_lines-new.tsx @@ -0,0 +1,134 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/ops_lines/ops_linesSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + plant: '', + + name: '', + + organizations: '', +}; + +const Ops_linesNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/ops_lines/ops_lines-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + router.push('/ops_lines/ops_lines-list')} + /> + + +
    +
    +
    + + ); +}; + +Ops_linesNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_linesNew; diff --git a/frontend/src/pages/ops_lines/ops_lines-table.tsx b/frontend/src/pages/ops_lines/ops_lines-table.tsx new file mode 100644 index 0000000..d345f5d --- /dev/null +++ b/frontend/src/pages/ops_lines/ops_lines-table.tsx @@ -0,0 +1,169 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableOps_lines from '../../components/Ops_lines/TableOps_lines'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/ops_lines/ops_linesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_linesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + + { label: 'Plant', title: 'plant' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_OPS_LINES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOps_linesCSV = async () => { + const response = await axios({ + url: '/ops_lines?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'ops_linesCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Ops_lines')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Ops_linesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_linesTablesPage; diff --git a/frontend/src/pages/ops_lines/ops_lines-view.tsx b/frontend/src/pages/ops_lines/ops_lines-view.tsx new file mode 100644 index 0000000..a48c08e --- /dev/null +++ b/frontend/src/pages/ops_lines/ops_lines-view.tsx @@ -0,0 +1,219 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/ops_lines/ops_linesSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_linesView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { ops_lines } = useAppSelector((state) => state.ops_lines); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View ops_lines')} + + + + + + +
    +

    Organization

    + +

    {ops_lines?.org?.name ?? 'No data'}

    +
    + +
    +

    Plant

    + +

    {ops_lines?.plant?.name ?? 'No data'}

    +
    + +
    +

    Name

    +

    {ops_lines?.name}

    +
    + +
    +

    organizations

    + +

    {ops_lines?.organizations?.name ?? 'No data'}

    +
    + + <> +

    Ops_incidents Line

    + +
    + + + + + + + + + + + + + + + + {ops_lines.ops_incidents_line && + Array.isArray(ops_lines.ops_incidents_line) && + ops_lines.ops_incidents_line.map((item: any) => ( + + router.push( + `/ops_incidents/ops_incidents-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ))} + +
    OccurredAtCategorySeverityDurationMinutesRootCause
    + {dataFormatter.dateTimeFormatter(item.occurred_at)} + {item.category}{item.severity}{item.duration_min}{item.root_cause}
    +
    + {!ops_lines?.ops_incidents_line?.length && ( +
    No data
    + )} +
    + + + <> +

    Ops_production_log Line

    + +
    + + + + + + + + + + + + + + + + {ops_lines.ops_production_log_line && + Array.isArray(ops_lines.ops_production_log_line) && + ops_lines.ops_production_log_line.map((item: any) => ( + + router.push( + `/ops_production_log/ops_production_log-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ))} + +
    TimestampUnitsProducedPlannedUnitsDowntimeMinutesDefects
    + {dataFormatter.dateTimeFormatter(item.ts)} + + {item.units_produced} + + {item.planned_units} + {item.downtime_min}{item.defects}
    +
    + {!ops_lines?.ops_production_log_line?.length && ( +
    No data
    + )} +
    + + + + + router.push('/ops_lines/ops_lines-list')} + /> +
    +
    + + ); +}; + +Ops_linesView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_linesView; diff --git a/frontend/src/pages/ops_orders/[ops_ordersId].tsx b/frontend/src/pages/ops_orders/[ops_ordersId].tsx new file mode 100644 index 0000000..a4d2600 --- /dev/null +++ b/frontend/src/pages/ops_orders/[ops_ordersId].tsx @@ -0,0 +1,208 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/ops_orders/ops_ordersSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditOps_orders = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + order_no: '', + + customer: null, + + due_date: new Date(), + + qty: '', + + status: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { ops_orders } = useAppSelector((state) => state.ops_orders); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { ops_ordersId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: ops_ordersId })); + }, [ops_ordersId]); + + useEffect(() => { + if (typeof ops_orders === 'object') { + setInitialValues(ops_orders); + } + }, [ops_orders]); + + useEffect(() => { + if (typeof ops_orders === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = ops_orders[el]), + ); + + setInitialValues(newInitialVal); + } + }, [ops_orders]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: ops_ordersId, data })); + await router.push('/ops_orders/ops_orders-list'); + }; + + return ( + <> + + {getPageTitle('Edit ops_orders')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + setInitialValues({ ...initialValues, due_date: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/ops_orders/ops_orders-list')} + /> + + +
    +
    +
    + + ); +}; + +EditOps_orders.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOps_orders; diff --git a/frontend/src/pages/ops_orders/ops_orders-edit.tsx b/frontend/src/pages/ops_orders/ops_orders-edit.tsx new file mode 100644 index 0000000..a39a042 --- /dev/null +++ b/frontend/src/pages/ops_orders/ops_orders-edit.tsx @@ -0,0 +1,206 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/ops_orders/ops_ordersSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditOps_ordersPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + order_no: '', + + customer: null, + + due_date: new Date(), + + qty: '', + + status: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { ops_orders } = useAppSelector((state) => state.ops_orders); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof ops_orders === 'object') { + setInitialValues(ops_orders); + } + }, [ops_orders]); + + useEffect(() => { + if (typeof ops_orders === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = ops_orders[el]), + ); + setInitialValues(newInitialVal); + } + }, [ops_orders]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/ops_orders/ops_orders-list'); + }; + + return ( + <> + + {getPageTitle('Edit ops_orders')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + setInitialValues({ ...initialValues, due_date: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/ops_orders/ops_orders-list')} + /> + + +
    +
    +
    + + ); +}; + +EditOps_ordersPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOps_ordersPage; diff --git a/frontend/src/pages/ops_orders/ops_orders-list.tsx b/frontend/src/pages/ops_orders/ops_orders-list.tsx new file mode 100644 index 0000000..92d303d --- /dev/null +++ b/frontend/src/pages/ops_orders/ops_orders-list.tsx @@ -0,0 +1,180 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableOps_orders from '../../components/Ops_orders/TableOps_orders'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/ops_orders/ops_ordersSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_ordersTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'OrderNumber', title: 'order_no' }, + { label: 'Quantity', title: 'qty', number: 'true' }, + + { label: 'DueDate', title: 'due_date', date: 'true' }, + + { label: 'Customer', title: 'customer' }, + + { + label: 'Status', + title: 'status', + type: 'enum', + options: ['Pending', 'Completed', 'Cancelled'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_OPS_ORDERS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOps_ordersCSV = async () => { + const response = await axios({ + url: '/ops_orders?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'ops_ordersCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Ops_orders')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + Switch to Table +
    +
    + + + + +
    + + + + + ); +}; + +Ops_ordersTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_ordersTablesPage; diff --git a/frontend/src/pages/ops_orders/ops_orders-new.tsx b/frontend/src/pages/ops_orders/ops_orders-new.tsx new file mode 100644 index 0000000..69d39bf --- /dev/null +++ b/frontend/src/pages/ops_orders/ops_orders-new.tsx @@ -0,0 +1,162 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/ops_orders/ops_ordersSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + order_no: '', + + customer: '', + + due_date: '', + + qty: '', + + status: 'Pending', + + organizations: '', +}; + +const Ops_ordersNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/ops_orders/ops_orders-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/ops_orders/ops_orders-list')} + /> + + +
    +
    +
    + + ); +}; + +Ops_ordersNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_ordersNew; diff --git a/frontend/src/pages/ops_orders/ops_orders-table.tsx b/frontend/src/pages/ops_orders/ops_orders-table.tsx new file mode 100644 index 0000000..a1174dc --- /dev/null +++ b/frontend/src/pages/ops_orders/ops_orders-table.tsx @@ -0,0 +1,179 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableOps_orders from '../../components/Ops_orders/TableOps_orders'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/ops_orders/ops_ordersSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_ordersTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'OrderNumber', title: 'order_no' }, + { label: 'Quantity', title: 'qty', number: 'true' }, + + { label: 'DueDate', title: 'due_date', date: 'true' }, + + { label: 'Customer', title: 'customer' }, + + { + label: 'Status', + title: 'status', + type: 'enum', + options: ['Pending', 'Completed', 'Cancelled'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_OPS_ORDERS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOps_ordersCSV = async () => { + const response = await axios({ + url: '/ops_orders?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'ops_ordersCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Ops_orders')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Ops_ordersTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_ordersTablesPage; diff --git a/frontend/src/pages/ops_orders/ops_orders-view.tsx b/frontend/src/pages/ops_orders/ops_orders-view.tsx new file mode 100644 index 0000000..e0dfe85 --- /dev/null +++ b/frontend/src/pages/ops_orders/ops_orders-view.tsx @@ -0,0 +1,134 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/ops_orders/ops_ordersSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_ordersView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { ops_orders } = useAppSelector((state) => state.ops_orders); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View ops_orders')} + + + + + + +
    +

    Organization

    + +

    {ops_orders?.org?.name ?? 'No data'}

    +
    + +
    +

    OrderNumber

    +

    {ops_orders?.order_no}

    +
    + +
    +

    Customer

    + +

    {ops_orders?.customer?.name ?? 'No data'}

    +
    + + + {ops_orders.due_date ? ( + + ) : ( +

    No DueDate

    + )} +
    + +
    +

    Quantity

    +

    {ops_orders?.qty || 'No data'}

    +
    + +
    +

    Status

    +

    {ops_orders?.status ?? 'No data'}

    +
    + +
    +

    organizations

    + +

    {ops_orders?.organizations?.name ?? 'No data'}

    +
    + + + + router.push('/ops_orders/ops_orders-list')} + /> +
    +
    + + ); +}; + +Ops_ordersView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_ordersView; diff --git a/frontend/src/pages/ops_plants/[ops_plantsId].tsx b/frontend/src/pages/ops_plants/[ops_plantsId].tsx new file mode 100644 index 0000000..6eebdb4 --- /dev/null +++ b/frontend/src/pages/ops_plants/[ops_plantsId].tsx @@ -0,0 +1,168 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/ops_plants/ops_plantsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditOps_plants = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + name: '', + + location: '', + + timezone: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { ops_plants } = useAppSelector((state) => state.ops_plants); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { ops_plantsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: ops_plantsId })); + }, [ops_plantsId]); + + useEffect(() => { + if (typeof ops_plants === 'object') { + setInitialValues(ops_plants); + } + }, [ops_plants]); + + useEffect(() => { + if (typeof ops_plants === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = ops_plants[el]), + ); + + setInitialValues(newInitialVal); + } + }, [ops_plants]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: ops_plantsId, data })); + await router.push('/ops_plants/ops_plants-list'); + }; + + return ( + <> + + {getPageTitle('Edit ops_plants')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/ops_plants/ops_plants-list')} + /> + + +
    +
    +
    + + ); +}; + +EditOps_plants.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOps_plants; diff --git a/frontend/src/pages/ops_plants/ops_plants-edit.tsx b/frontend/src/pages/ops_plants/ops_plants-edit.tsx new file mode 100644 index 0000000..8f49cd9 --- /dev/null +++ b/frontend/src/pages/ops_plants/ops_plants-edit.tsx @@ -0,0 +1,166 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/ops_plants/ops_plantsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditOps_plantsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + name: '', + + location: '', + + timezone: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { ops_plants } = useAppSelector((state) => state.ops_plants); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof ops_plants === 'object') { + setInitialValues(ops_plants); + } + }, [ops_plants]); + + useEffect(() => { + if (typeof ops_plants === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = ops_plants[el]), + ); + setInitialValues(newInitialVal); + } + }, [ops_plants]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/ops_plants/ops_plants-list'); + }; + + return ( + <> + + {getPageTitle('Edit ops_plants')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/ops_plants/ops_plants-list')} + /> + + +
    +
    +
    + + ); +}; + +EditOps_plantsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOps_plantsPage; diff --git a/frontend/src/pages/ops_plants/ops_plants-list.tsx b/frontend/src/pages/ops_plants/ops_plants-list.tsx new file mode 100644 index 0000000..13c21d9 --- /dev/null +++ b/frontend/src/pages/ops_plants/ops_plants-list.tsx @@ -0,0 +1,170 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableOps_plants from '../../components/Ops_plants/TableOps_plants'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/ops_plants/ops_plantsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_plantsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'Location', title: 'location' }, + { label: 'Timezone', title: 'timezone' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_OPS_PLANTS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOps_plantsCSV = async () => { + const response = await axios({ + url: '/ops_plants?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'ops_plantsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Ops_plants')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + Switch to Table +
    +
    + + + + +
    + + + + + ); +}; + +Ops_plantsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_plantsTablesPage; diff --git a/frontend/src/pages/ops_plants/ops_plants-new.tsx b/frontend/src/pages/ops_plants/ops_plants-new.tsx new file mode 100644 index 0000000..40d1dcb --- /dev/null +++ b/frontend/src/pages/ops_plants/ops_plants-new.tsx @@ -0,0 +1,134 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/ops_plants/ops_plantsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + name: '', + + location: '', + + timezone: '', + + organizations: '', +}; + +const Ops_plantsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/ops_plants/ops_plants-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/ops_plants/ops_plants-list')} + /> + + +
    +
    +
    + + ); +}; + +Ops_plantsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_plantsNew; diff --git a/frontend/src/pages/ops_plants/ops_plants-table.tsx b/frontend/src/pages/ops_plants/ops_plants-table.tsx new file mode 100644 index 0000000..e9c18d1 --- /dev/null +++ b/frontend/src/pages/ops_plants/ops_plants-table.tsx @@ -0,0 +1,169 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableOps_plants from '../../components/Ops_plants/TableOps_plants'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/ops_plants/ops_plantsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_plantsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'Location', title: 'location' }, + { label: 'Timezone', title: 'timezone' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_OPS_PLANTS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOps_plantsCSV = async () => { + const response = await axios({ + url: '/ops_plants?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'ops_plantsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Ops_plants')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Ops_plantsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_plantsTablesPage; diff --git a/frontend/src/pages/ops_plants/ops_plants-view.tsx b/frontend/src/pages/ops_plants/ops_plants-view.tsx new file mode 100644 index 0000000..d3a3da6 --- /dev/null +++ b/frontend/src/pages/ops_plants/ops_plants-view.tsx @@ -0,0 +1,201 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/ops_plants/ops_plantsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_plantsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { ops_plants } = useAppSelector((state) => state.ops_plants); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View ops_plants')} + + + + + + +
    +

    Organization

    + +

    {ops_plants?.org?.name ?? 'No data'}

    +
    + +
    +

    Name

    +

    {ops_plants?.name}

    +
    + +
    +

    Location

    +

    {ops_plants?.location}

    +
    + +
    +

    Timezone

    +

    {ops_plants?.timezone}

    +
    + +
    +

    organizations

    + +

    {ops_plants?.organizations?.name ?? 'No data'}

    +
    + + <> +

    Ops_incidents Plant

    + +
    + + + + + + + + + + + + + + + + {ops_plants.ops_incidents_plant && + Array.isArray(ops_plants.ops_incidents_plant) && + ops_plants.ops_incidents_plant.map((item: any) => ( + + router.push( + `/ops_incidents/ops_incidents-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ))} + +
    OccurredAtCategorySeverityDurationMinutesRootCause
    + {dataFormatter.dateTimeFormatter(item.occurred_at)} + {item.category}{item.severity}{item.duration_min}{item.root_cause}
    +
    + {!ops_plants?.ops_incidents_plant?.length && ( +
    No data
    + )} +
    + + + <> +

    Ops_lines Plant

    + +
    + + + + + + + + {ops_plants.ops_lines_plant && + Array.isArray(ops_plants.ops_lines_plant) && + ops_plants.ops_lines_plant.map((item: any) => ( + + router.push( + `/ops_lines/ops_lines-view/?id=${item.id}`, + ) + } + > + + + ))} + +
    Name
    {item.name}
    +
    + {!ops_plants?.ops_lines_plant?.length && ( +
    No data
    + )} +
    + + + + + router.push('/ops_plants/ops_plants-list')} + /> +
    +
    + + ); +}; + +Ops_plantsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_plantsView; diff --git a/frontend/src/pages/ops_production_log/[ops_production_logId].tsx b/frontend/src/pages/ops_production_log/[ops_production_logId].tsx new file mode 100644 index 0000000..b70fb1c --- /dev/null +++ b/frontend/src/pages/ops_production_log/[ops_production_logId].tsx @@ -0,0 +1,225 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { + update, + fetch, +} from '../../stores/ops_production_log/ops_production_logSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditOps_production_log = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + line: null, + + ts: new Date(), + + units_produced: '', + + planned_units: '', + + downtime_min: '', + + defects: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { ops_production_log } = useAppSelector( + (state) => state.ops_production_log, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { ops_production_logId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: ops_production_logId })); + }, [ops_production_logId]); + + useEffect(() => { + if (typeof ops_production_log === 'object') { + setInitialValues(ops_production_log); + } + }, [ops_production_log]); + + useEffect(() => { + if (typeof ops_production_log === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = ops_production_log[el]), + ); + + setInitialValues(newInitialVal); + } + }, [ops_production_log]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: ops_production_logId, data })); + await router.push('/ops_production_log/ops_production_log-list'); + }; + + return ( + <> + + {getPageTitle('Edit ops_production_log')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + setInitialValues({ ...initialValues, ts: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/ops_production_log/ops_production_log-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditOps_production_log.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOps_production_log; diff --git a/frontend/src/pages/ops_production_log/ops_production_log-edit.tsx b/frontend/src/pages/ops_production_log/ops_production_log-edit.tsx new file mode 100644 index 0000000..9f47a10 --- /dev/null +++ b/frontend/src/pages/ops_production_log/ops_production_log-edit.tsx @@ -0,0 +1,223 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { + update, + fetch, +} from '../../stores/ops_production_log/ops_production_logSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditOps_production_logPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + line: null, + + ts: new Date(), + + units_produced: '', + + planned_units: '', + + downtime_min: '', + + defects: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { ops_production_log } = useAppSelector( + (state) => state.ops_production_log, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof ops_production_log === 'object') { + setInitialValues(ops_production_log); + } + }, [ops_production_log]); + + useEffect(() => { + if (typeof ops_production_log === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = ops_production_log[el]), + ); + setInitialValues(newInitialVal); + } + }, [ops_production_log]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/ops_production_log/ops_production_log-list'); + }; + + return ( + <> + + {getPageTitle('Edit ops_production_log')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + setInitialValues({ ...initialValues, ts: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/ops_production_log/ops_production_log-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditOps_production_logPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOps_production_logPage; diff --git a/frontend/src/pages/ops_production_log/ops_production_log-list.tsx b/frontend/src/pages/ops_production_log/ops_production_log-list.tsx new file mode 100644 index 0000000..21c5f15 --- /dev/null +++ b/frontend/src/pages/ops_production_log/ops_production_log-list.tsx @@ -0,0 +1,182 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableOps_production_log from '../../components/Ops_production_log/TableOps_production_log'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/ops_production_log/ops_production_logSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_production_logTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'UnitsProduced', title: 'units_produced', number: 'true' }, + { label: 'PlannedUnits', title: 'planned_units', number: 'true' }, + { label: 'DowntimeMinutes', title: 'downtime_min', number: 'true' }, + { label: 'Defects', title: 'defects', number: 'true' }, + + { label: 'Timestamp', title: 'ts', date: 'true' }, + + { label: 'Line', title: 'line' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_OPS_PRODUCTION_LOG'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOps_production_logCSV = async () => { + const response = await axios({ + url: '/ops_production_log?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'ops_production_logCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Ops_production_log')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + + Switch to Table + +
    +
    + + + + +
    + + + + + ); +}; + +Ops_production_logTablesPage.getLayout = function getLayout( + page: ReactElement, +) { + return ( + + {page} + + ); +}; + +export default Ops_production_logTablesPage; diff --git a/frontend/src/pages/ops_production_log/ops_production_log-new.tsx b/frontend/src/pages/ops_production_log/ops_production_log-new.tsx new file mode 100644 index 0000000..483bc91 --- /dev/null +++ b/frontend/src/pages/ops_production_log/ops_production_log-new.tsx @@ -0,0 +1,176 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/ops_production_log/ops_production_logSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + line: '', + + ts: '', + + units_produced: '', + + planned_units: '', + + downtime_min: '', + + defects: '', + + organizations: '', +}; + +const Ops_production_logNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/ops_production_log/ops_production_log-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/ops_production_log/ops_production_log-list') + } + /> + + +
    +
    +
    + + ); +}; + +Ops_production_logNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_production_logNew; diff --git a/frontend/src/pages/ops_production_log/ops_production_log-table.tsx b/frontend/src/pages/ops_production_log/ops_production_log-table.tsx new file mode 100644 index 0000000..1ed6a3f --- /dev/null +++ b/frontend/src/pages/ops_production_log/ops_production_log-table.tsx @@ -0,0 +1,179 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableOps_production_log from '../../components/Ops_production_log/TableOps_production_log'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/ops_production_log/ops_production_logSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_production_logTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'UnitsProduced', title: 'units_produced', number: 'true' }, + { label: 'PlannedUnits', title: 'planned_units', number: 'true' }, + { label: 'DowntimeMinutes', title: 'downtime_min', number: 'true' }, + { label: 'Defects', title: 'defects', number: 'true' }, + + { label: 'Timestamp', title: 'ts', date: 'true' }, + + { label: 'Line', title: 'line' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_OPS_PRODUCTION_LOG'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOps_production_logCSV = async () => { + const response = await axios({ + url: '/ops_production_log?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'ops_production_logCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Ops_production_log')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Ops_production_logTablesPage.getLayout = function getLayout( + page: ReactElement, +) { + return ( + + {page} + + ); +}; + +export default Ops_production_logTablesPage; diff --git a/frontend/src/pages/ops_production_log/ops_production_log-view.tsx b/frontend/src/pages/ops_production_log/ops_production_log-view.tsx new file mode 100644 index 0000000..61f7863 --- /dev/null +++ b/frontend/src/pages/ops_production_log/ops_production_log-view.tsx @@ -0,0 +1,143 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/ops_production_log/ops_production_logSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_production_logView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { ops_production_log } = useAppSelector( + (state) => state.ops_production_log, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View ops_production_log')} + + + + + + +
    +

    Organization

    + +

    {ops_production_log?.org?.name ?? 'No data'}

    +
    + +
    +

    Line

    + +

    {ops_production_log?.line?.name ?? 'No data'}

    +
    + + + {ops_production_log.ts ? ( + + ) : ( +

    No Timestamp

    + )} +
    + +
    +

    UnitsProduced

    +

    {ops_production_log?.units_produced || 'No data'}

    +
    + +
    +

    PlannedUnits

    +

    {ops_production_log?.planned_units || 'No data'}

    +
    + +
    +

    DowntimeMinutes

    +

    {ops_production_log?.downtime_min || 'No data'}

    +
    + +
    +

    Defects

    +

    {ops_production_log?.defects || 'No data'}

    +
    + +
    +

    organizations

    + +

    {ops_production_log?.organizations?.name ?? 'No data'}

    +
    + + + + + router.push('/ops_production_log/ops_production_log-list') + } + /> +
    +
    + + ); +}; + +Ops_production_logView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_production_logView; diff --git a/frontend/src/pages/ops_shipments/[ops_shipmentsId].tsx b/frontend/src/pages/ops_shipments/[ops_shipmentsId].tsx new file mode 100644 index 0000000..5709aec --- /dev/null +++ b/frontend/src/pages/ops_shipments/[ops_shipmentsId].tsx @@ -0,0 +1,218 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/ops_shipments/ops_shipmentsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditOps_shipments = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + order_no: '', + + shipped_at: new Date(), + + carrier: '', + + delivered_at: new Date(), + + status: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { ops_shipments } = useAppSelector((state) => state.ops_shipments); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { ops_shipmentsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: ops_shipmentsId })); + }, [ops_shipmentsId]); + + useEffect(() => { + if (typeof ops_shipments === 'object') { + setInitialValues(ops_shipments); + } + }, [ops_shipments]); + + useEffect(() => { + if (typeof ops_shipments === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = ops_shipments[el]), + ); + + setInitialValues(newInitialVal); + } + }, [ops_shipments]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: ops_shipmentsId, data })); + await router.push('/ops_shipments/ops_shipments-list'); + }; + + return ( + <> + + {getPageTitle('Edit ops_shipments')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + setInitialValues({ ...initialValues, shipped_at: date }) + } + /> + + + + + + + + + setInitialValues({ ...initialValues, delivered_at: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + router.push('/ops_shipments/ops_shipments-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditOps_shipments.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOps_shipments; diff --git a/frontend/src/pages/ops_shipments/ops_shipments-edit.tsx b/frontend/src/pages/ops_shipments/ops_shipments-edit.tsx new file mode 100644 index 0000000..b1f3062 --- /dev/null +++ b/frontend/src/pages/ops_shipments/ops_shipments-edit.tsx @@ -0,0 +1,216 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/ops_shipments/ops_shipmentsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditOps_shipmentsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + org: null, + + order_no: '', + + shipped_at: new Date(), + + carrier: '', + + delivered_at: new Date(), + + status: '', + + organizations: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { ops_shipments } = useAppSelector((state) => state.ops_shipments); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof ops_shipments === 'object') { + setInitialValues(ops_shipments); + } + }, [ops_shipments]); + + useEffect(() => { + if (typeof ops_shipments === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = ops_shipments[el]), + ); + setInitialValues(newInitialVal); + } + }, [ops_shipments]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/ops_shipments/ops_shipments-list'); + }; + + return ( + <> + + {getPageTitle('Edit ops_shipments')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + setInitialValues({ ...initialValues, shipped_at: date }) + } + /> + + + + + + + + + setInitialValues({ ...initialValues, delivered_at: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + router.push('/ops_shipments/ops_shipments-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditOps_shipmentsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOps_shipmentsPage; diff --git a/frontend/src/pages/ops_shipments/ops_shipments-list.tsx b/frontend/src/pages/ops_shipments/ops_shipments-list.tsx new file mode 100644 index 0000000..e6788b3 --- /dev/null +++ b/frontend/src/pages/ops_shipments/ops_shipments-list.tsx @@ -0,0 +1,184 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableOps_shipments from '../../components/Ops_shipments/TableOps_shipments'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/ops_shipments/ops_shipmentsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_shipmentsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'OrderNumber', title: 'order_no' }, + { label: 'Carrier', title: 'carrier' }, + + { label: 'ShippedAt', title: 'shipped_at', date: 'true' }, + { label: 'DeliveredAt', title: 'delivered_at', date: 'true' }, + + { + label: 'Status', + title: 'status', + type: 'enum', + options: ['InTransit', 'Delivered', 'Delayed'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_OPS_SHIPMENTS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOps_shipmentsCSV = async () => { + const response = await axios({ + url: '/ops_shipments?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'ops_shipmentsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Ops_shipments')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + + Switch to Table + +
    +
    + + + + +
    + + + + + ); +}; + +Ops_shipmentsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_shipmentsTablesPage; diff --git a/frontend/src/pages/ops_shipments/ops_shipments-new.tsx b/frontend/src/pages/ops_shipments/ops_shipments-new.tsx new file mode 100644 index 0000000..68d931b --- /dev/null +++ b/frontend/src/pages/ops_shipments/ops_shipments-new.tsx @@ -0,0 +1,162 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/ops_shipments/ops_shipmentsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + org: '', + + order_no: '', + + shipped_at: '', + + carrier: '', + + delivered_at: '', + + status: 'InTransit', + + organizations: '', +}; + +const Ops_shipmentsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/ops_shipments/ops_shipments-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/ops_shipments/ops_shipments-list') + } + /> + + +
    +
    +
    + + ); +}; + +Ops_shipmentsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_shipmentsNew; diff --git a/frontend/src/pages/ops_shipments/ops_shipments-table.tsx b/frontend/src/pages/ops_shipments/ops_shipments-table.tsx new file mode 100644 index 0000000..737f432 --- /dev/null +++ b/frontend/src/pages/ops_shipments/ops_shipments-table.tsx @@ -0,0 +1,181 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableOps_shipments from '../../components/Ops_shipments/TableOps_shipments'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/ops_shipments/ops_shipmentsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_shipmentsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'OrderNumber', title: 'order_no' }, + { label: 'Carrier', title: 'carrier' }, + + { label: 'ShippedAt', title: 'shipped_at', date: 'true' }, + { label: 'DeliveredAt', title: 'delivered_at', date: 'true' }, + + { + label: 'Status', + title: 'status', + type: 'enum', + options: ['InTransit', 'Delivered', 'Delayed'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_OPS_SHIPMENTS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOps_shipmentsCSV = async () => { + const response = await axios({ + url: '/ops_shipments?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'ops_shipmentsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Ops_shipments')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Ops_shipmentsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_shipmentsTablesPage; diff --git a/frontend/src/pages/ops_shipments/ops_shipments-view.tsx b/frontend/src/pages/ops_shipments/ops_shipments-view.tsx new file mode 100644 index 0000000..07dc1f5 --- /dev/null +++ b/frontend/src/pages/ops_shipments/ops_shipments-view.tsx @@ -0,0 +1,151 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/ops_shipments/ops_shipmentsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Ops_shipmentsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { ops_shipments } = useAppSelector((state) => state.ops_shipments); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View ops_shipments')} + + + + + + +
    +

    Organization

    + +

    {ops_shipments?.org?.name ?? 'No data'}

    +
    + +
    +

    OrderNumber

    +

    {ops_shipments?.order_no}

    +
    + + + {ops_shipments.shipped_at ? ( + + ) : ( +

    No ShippedAt

    + )} +
    + +
    +

    Carrier

    +

    {ops_shipments?.carrier}

    +
    + + + {ops_shipments.delivered_at ? ( + + ) : ( +

    No DeliveredAt

    + )} +
    + +
    +

    Status

    +

    {ops_shipments?.status ?? 'No data'}

    +
    + +
    +

    organizations

    + +

    {ops_shipments?.organizations?.name ?? 'No data'}

    +
    + + + + router.push('/ops_shipments/ops_shipments-list')} + /> +
    +
    + + ); +}; + +Ops_shipmentsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Ops_shipmentsView; diff --git a/frontend/src/pages/organizations/[organizationsId].tsx b/frontend/src/pages/organizations/[organizationsId].tsx new file mode 100644 index 0000000..25053b2 --- /dev/null +++ b/frontend/src/pages/organizations/[organizationsId].tsx @@ -0,0 +1,132 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/organizations/organizationsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditOrganizations = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + name: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { organizations } = useAppSelector((state) => state.organizations); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { organizationsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: organizationsId })); + }, [organizationsId]); + + useEffect(() => { + if (typeof organizations === 'object') { + setInitialValues(organizations); + } + }, [organizations]); + + useEffect(() => { + if (typeof organizations === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = organizations[el]), + ); + + setInitialValues(newInitialVal); + } + }, [organizations]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: organizationsId, data })); + await router.push('/organizations/organizations-list'); + }; + + return ( + <> + + {getPageTitle('Edit organizations')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + router.push('/organizations/organizations-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditOrganizations.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOrganizations; diff --git a/frontend/src/pages/organizations/organizations-edit.tsx b/frontend/src/pages/organizations/organizations-edit.tsx new file mode 100644 index 0000000..ab8b893 --- /dev/null +++ b/frontend/src/pages/organizations/organizations-edit.tsx @@ -0,0 +1,130 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/organizations/organizationsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditOrganizationsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + name: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { organizations } = useAppSelector((state) => state.organizations); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof organizations === 'object') { + setInitialValues(organizations); + } + }, [organizations]); + + useEffect(() => { + if (typeof organizations === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = organizations[el]), + ); + setInitialValues(newInitialVal); + } + }, [organizations]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/organizations/organizations-list'); + }; + + return ( + <> + + {getPageTitle('Edit organizations')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + router.push('/organizations/organizations-list') + } + /> + + +
    +
    +
    + + ); +}; + +EditOrganizationsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOrganizationsPage; diff --git a/frontend/src/pages/organizations/organizations-list.tsx b/frontend/src/pages/organizations/organizations-list.tsx new file mode 100644 index 0000000..9e0a09b --- /dev/null +++ b/frontend/src/pages/organizations/organizations-list.tsx @@ -0,0 +1,165 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableOrganizations from '../../components/Organizations/TableOrganizations'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/organizations/organizationsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const OrganizationsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([{ label: 'Name', title: 'name' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_ORGANIZATIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOrganizationsCSV = async () => { + const response = await axios({ + url: '/organizations?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'organizationsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Organizations')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    +
    + + + + +
    + + + + + ); +}; + +OrganizationsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default OrganizationsTablesPage; diff --git a/frontend/src/pages/organizations/organizations-new.tsx b/frontend/src/pages/organizations/organizations-new.tsx new file mode 100644 index 0000000..2787572 --- /dev/null +++ b/frontend/src/pages/organizations/organizations-new.tsx @@ -0,0 +1,100 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/organizations/organizationsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + name: '', +}; + +const OrganizationsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/organizations/organizations-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + router.push('/organizations/organizations-list') + } + /> + + +
    +
    +
    + + ); +}; + +OrganizationsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default OrganizationsNew; diff --git a/frontend/src/pages/organizations/organizations-table.tsx b/frontend/src/pages/organizations/organizations-table.tsx new file mode 100644 index 0000000..489c393 --- /dev/null +++ b/frontend/src/pages/organizations/organizations-table.tsx @@ -0,0 +1,164 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableOrganizations from '../../components/Organizations/TableOrganizations'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/organizations/organizationsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const OrganizationsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([{ label: 'Name', title: 'name' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_ORGANIZATIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOrganizationsCSV = async () => { + const response = await axios({ + url: '/organizations?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'organizationsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Organizations')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    +
    + + + +
    + + + + + ); +}; + +OrganizationsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default OrganizationsTablesPage; diff --git a/frontend/src/pages/organizations/organizations-view.tsx b/frontend/src/pages/organizations/organizations-view.tsx new file mode 100644 index 0000000..b6f9397 --- /dev/null +++ b/frontend/src/pages/organizations/organizations-view.tsx @@ -0,0 +1,2674 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/organizations/organizationsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const OrganizationsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { organizations } = useAppSelector((state) => state.organizations); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View organizations')} + + + + + + +
    +

    Name

    +

    {organizations?.name}

    +
    + + <> +

    Users Organizations

    + +
    + + + + + + + + + + + + + + + + {organizations.users_organizations && + Array.isArray(organizations.users_organizations) && + organizations.users_organizations.map((item: any) => ( + + router.push(`/users/users-view/?id=${item.id}`) + } + > + + + + + + + + + + + ))} + +
    First NameLast NamePhone NumberE-MailDisabled
    {item.firstName}{item.lastName}{item.phoneNumber}{item.email} + {dataFormatter.booleanFormatter(item.disabled)} +
    +
    + {!organizations?.users_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Audit_logs Organization

    + +
    + + + + + + + + + + + + + + {organizations.audit_logs_org && + Array.isArray(organizations.audit_logs_org) && + organizations.audit_logs_org.map((item: any) => ( + + router.push( + `/audit_logs/audit_logs-view/?id=${item.id}`, + ) + } + > + + + + + + + + + ))} + +
    ActionEntityEntityIDDiffJSON
    {item.action}{item.entity}{item.entity_id}{item.diff_json}
    +
    + {!organizations?.audit_logs_org?.length && ( +
    No data
    + )} +
    + + + <> +

    Audit_logs organizations

    + +
    + + + + + + + + + + + + + + {organizations.audit_logs_organizations && + Array.isArray(organizations.audit_logs_organizations) && + organizations.audit_logs_organizations.map( + (item: any) => ( + + router.push( + `/audit_logs/audit_logs-view/?id=${item.id}`, + ) + } + > + + + + + + + + + ), + )} + +
    ActionEntityEntityIDDiffJSON
    {item.action}{item.entity}{item.entity_id}{item.diff_json}
    +
    + {!organizations?.audit_logs_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    + Data_connections Organization +

    + +
    + + + + + + + + + + + + + + + + {organizations.data_connections_org && + Array.isArray(organizations.data_connections_org) && + organizations.data_connections_org.map((item: any) => ( + + router.push( + `/data_connections/data_connections-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ))} + +
    TypeNameConfigJSONStatusLastSyncAt
    {item.type}{item.name}{item.config_json}{item.status} + {dataFormatter.dateTimeFormatter(item.last_sync_at)} +
    +
    + {!organizations?.data_connections_org?.length && ( +
    No data
    + )} +
    + + + <> +

    + Data_connections organizations +

    + +
    + + + + + + + + + + + + + + + + {organizations.data_connections_organizations && + Array.isArray( + organizations.data_connections_organizations, + ) && + organizations.data_connections_organizations.map( + (item: any) => ( + + router.push( + `/data_connections/data_connections-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ), + )} + +
    TypeNameConfigJSONStatusLastSyncAt
    {item.type}{item.name}{item.config_json}{item.status} + {dataFormatter.dateTimeFormatter( + item.last_sync_at, + )} +
    +
    + {!organizations?.data_connections_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Fin_accounts Organization

    + +
    + + + + + + + + + + + + + + {organizations.fin_accounts_org && + Array.isArray(organizations.fin_accounts_org) && + organizations.fin_accounts_org.map((item: any) => ( + + router.push( + `/fin_accounts/fin_accounts-view/?id=${item.id}`, + ) + } + > + + + + + + + + + ))} + +
    CodeNameTypeIsActive
    {item.code}{item.name}{item.type} + {dataFormatter.booleanFormatter(item.is_active)} +
    +
    + {!organizations?.fin_accounts_org?.length && ( +
    No data
    + )} +
    + + + <> +

    Fin_accounts organizations

    + +
    + + + + + + + + + + + + + + {organizations.fin_accounts_organizations && + Array.isArray(organizations.fin_accounts_organizations) && + organizations.fin_accounts_organizations.map( + (item: any) => ( + + router.push( + `/fin_accounts/fin_accounts-view/?id=${item.id}`, + ) + } + > + + + + + + + + + ), + )} + +
    CodeNameTypeIsActive
    {item.code}{item.name}{item.type} + {dataFormatter.booleanFormatter(item.is_active)} +
    +
    + {!organizations?.fin_accounts_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Fin_ap Organization

    + +
    + + + + + + + + + + + + + + {organizations.fin_ap_org && + Array.isArray(organizations.fin_ap_org) && + organizations.fin_ap_org.map((item: any) => ( + + router.push(`/fin_ap/fin_ap-view/?id=${item.id}`) + } + > + + + + + + + + + ))} + +
    BillNumberDueDateAmountStatus
    {item.bill_no} + {dataFormatter.dateTimeFormatter(item.due_date)} + {item.amount}{item.status}
    +
    + {!organizations?.fin_ap_org?.length && ( +
    No data
    + )} +
    + + + <> +

    Fin_ap organizations

    + +
    + + + + + + + + + + + + + + {organizations.fin_ap_organizations && + Array.isArray(organizations.fin_ap_organizations) && + organizations.fin_ap_organizations.map((item: any) => ( + + router.push(`/fin_ap/fin_ap-view/?id=${item.id}`) + } + > + + + + + + + + + ))} + +
    BillNumberDueDateAmountStatus
    {item.bill_no} + {dataFormatter.dateTimeFormatter(item.due_date)} + {item.amount}{item.status}
    +
    + {!organizations?.fin_ap_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Fin_ar Organization

    + +
    + + + + + + + + + + + + + + {organizations.fin_ar_org && + Array.isArray(organizations.fin_ar_org) && + organizations.fin_ar_org.map((item: any) => ( + + router.push(`/fin_ar/fin_ar-view/?id=${item.id}`) + } + > + + + + + + + + + ))} + +
    InvoiceNumberDueDateAmountStatus
    {item.invoice_no} + {dataFormatter.dateTimeFormatter(item.due_date)} + {item.amount}{item.status}
    +
    + {!organizations?.fin_ar_org?.length && ( +
    No data
    + )} +
    + + + <> +

    Fin_ar organizations

    + +
    + + + + + + + + + + + + + + {organizations.fin_ar_organizations && + Array.isArray(organizations.fin_ar_organizations) && + organizations.fin_ar_organizations.map((item: any) => ( + + router.push(`/fin_ar/fin_ar-view/?id=${item.id}`) + } + > + + + + + + + + + ))} + +
    InvoiceNumberDueDateAmountStatus
    {item.invoice_no} + {dataFormatter.dateTimeFormatter(item.due_date)} + {item.amount}{item.status}
    +
    + {!organizations?.fin_ar_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Fin_budgets Organization

    + +
    + + + + + + + + + + + + + + {organizations.fin_budgets_org && + Array.isArray(organizations.fin_budgets_org) && + organizations.fin_budgets_org.map((item: any) => ( + + router.push( + `/fin_budgets/fin_budgets-view/?id=${item.id}`, + ) + } + > + + + + + + + + + ))} + +
    FiscalYearCostCenterMonthAmount
    {item.fiscal_year}{item.cost_center}{item.month}{item.amount}
    +
    + {!organizations?.fin_budgets_org?.length && ( +
    No data
    + )} +
    + + + <> +

    Fin_budgets organizations

    + +
    + + + + + + + + + + + + + + {organizations.fin_budgets_organizations && + Array.isArray(organizations.fin_budgets_organizations) && + organizations.fin_budgets_organizations.map( + (item: any) => ( + + router.push( + `/fin_budgets/fin_budgets-view/?id=${item.id}`, + ) + } + > + + + + + + + + + ), + )} + +
    FiscalYearCostCenterMonthAmount
    {item.fiscal_year}{item.cost_center}{item.month}{item.amount}
    +
    + {!organizations?.fin_budgets_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Fin_customers Organization

    + +
    + + + + + + + + + + + + {organizations.fin_customers_org && + Array.isArray(organizations.fin_customers_org) && + organizations.fin_customers_org.map((item: any) => ( + + router.push( + `/fin_customers/fin_customers-view/?id=${item.id}`, + ) + } + > + + + + + + + ))} + +
    NameSegmentCountry
    {item.name}{item.segment}{item.country}
    +
    + {!organizations?.fin_customers_org?.length && ( +
    No data
    + )} +
    + + + <> +

    + Fin_customers organizations +

    + +
    + + + + + + + + + + + + {organizations.fin_customers_organizations && + Array.isArray( + organizations.fin_customers_organizations, + ) && + organizations.fin_customers_organizations.map( + (item: any) => ( + + router.push( + `/fin_customers/fin_customers-view/?id=${item.id}`, + ) + } + > + + + + + + + ), + )} + +
    NameSegmentCountry
    {item.name}{item.segment}{item.country}
    +
    + {!organizations?.fin_customers_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Fin_forecasts Organization

    + +
    + + + + + + + + + + + + + + {organizations.fin_forecasts_org && + Array.isArray(organizations.fin_forecasts_org) && + organizations.fin_forecasts_org.map((item: any) => ( + + router.push( + `/fin_forecasts/fin_forecasts-view/?id=${item.id}`, + ) + } + > + + + + + + + + + ))} + +
    FiscalYearCostCenterMonthAmount
    {item.fiscal_year}{item.cost_center}{item.month}{item.amount}
    +
    + {!organizations?.fin_forecasts_org?.length && ( +
    No data
    + )} +
    + + + <> +

    + Fin_forecasts organizations +

    + +
    + + + + + + + + + + + + + + {organizations.fin_forecasts_organizations && + Array.isArray( + organizations.fin_forecasts_organizations, + ) && + organizations.fin_forecasts_organizations.map( + (item: any) => ( + + router.push( + `/fin_forecasts/fin_forecasts-view/?id=${item.id}`, + ) + } + > + + + + + + + + + ), + )} + +
    FiscalYearCostCenterMonthAmount
    {item.fiscal_year}{item.cost_center}{item.month}{item.amount}
    +
    + {!organizations?.fin_forecasts_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    + Fin_transactions Organization +

    + +
    + + + + + + + + + + + + + + + + + + + + + + {organizations.fin_transactions_org && + Array.isArray(organizations.fin_transactions_org) && + organizations.fin_transactions_org.map((item: any) => ( + + router.push( + `/fin_transactions/fin_transactions-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + + + + + + + ))} + +
    TransactionTimestampAmountCurrencyFXRateCostCenterProjectCodeMemoSource
    + {dataFormatter.dateTimeFormatter(item.tx_ts)} + {item.amount}{item.currency}{item.fx_rate}{item.cost_center}{item.project_code}{item.memo}{item.source}
    +
    + {!organizations?.fin_transactions_org?.length && ( +
    No data
    + )} +
    + + + <> +

    + Fin_transactions organizations +

    + +
    + + + + + + + + + + + + + + + + + + + + + + {organizations.fin_transactions_organizations && + Array.isArray( + organizations.fin_transactions_organizations, + ) && + organizations.fin_transactions_organizations.map( + (item: any) => ( + + router.push( + `/fin_transactions/fin_transactions-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + + + + + + + ), + )} + +
    TransactionTimestampAmountCurrencyFXRateCostCenterProjectCodeMemoSource
    + {dataFormatter.dateTimeFormatter(item.tx_ts)} + {item.amount}{item.currency}{item.fx_rate}{item.cost_center} + {item.project_code} + {item.memo}{item.source}
    +
    + {!organizations?.fin_transactions_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Fin_vendors Organization

    + +
    + + + + + + + + + + + + {organizations.fin_vendors_org && + Array.isArray(organizations.fin_vendors_org) && + organizations.fin_vendors_org.map((item: any) => ( + + router.push( + `/fin_vendors/fin_vendors-view/?id=${item.id}`, + ) + } + > + + + + + + + ))} + +
    NameTaxIDCountry
    {item.name}{item.tax_id}{item.country}
    +
    + {!organizations?.fin_vendors_org?.length && ( +
    No data
    + )} +
    + + + <> +

    Fin_vendors organizations

    + +
    + + + + + + + + + + + + {organizations.fin_vendors_organizations && + Array.isArray(organizations.fin_vendors_organizations) && + organizations.fin_vendors_organizations.map( + (item: any) => ( + + router.push( + `/fin_vendors/fin_vendors-view/?id=${item.id}`, + ) + } + > + + + + + + + ), + )} + +
    NameTaxIDCountry
    {item.name}{item.tax_id}{item.country}
    +
    + {!organizations?.fin_vendors_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Hr_applicants Organization

    + +
    + + + + + + + + + + + + + + + + {organizations.hr_applicants_org && + Array.isArray(organizations.hr_applicants_org) && + organizations.hr_applicants_org.map((item: any) => ( + + router.push( + `/hr_applicants/hr_applicants-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ))} + +
    NameStageSourceOfferExtendedAtOfferAcceptedAt
    {item.name}{item.stage}{item.source} + {dataFormatter.dateTimeFormatter( + item.offer_extended_at, + )} + + {dataFormatter.dateTimeFormatter( + item.offer_accepted_at, + )} +
    +
    + {!organizations?.hr_applicants_org?.length && ( +
    No data
    + )} +
    + + + <> +

    + Hr_applicants organizations +

    + +
    + + + + + + + + + + + + + + + + {organizations.hr_applicants_organizations && + Array.isArray( + organizations.hr_applicants_organizations, + ) && + organizations.hr_applicants_organizations.map( + (item: any) => ( + + router.push( + `/hr_applicants/hr_applicants-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ), + )} + +
    NameStageSourceOfferExtendedAtOfferAcceptedAt
    {item.name}{item.stage}{item.source} + {dataFormatter.dateTimeFormatter( + item.offer_extended_at, + )} + + {dataFormatter.dateTimeFormatter( + item.offer_accepted_at, + )} +
    +
    + {!organizations?.hr_applicants_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Hr_attendance Organization

    + +
    + + + + + + + + + + + + {organizations.hr_attendance_org && + Array.isArray(organizations.hr_attendance_org) && + organizations.hr_attendance_org.map((item: any) => ( + + router.push( + `/hr_attendance/hr_attendance-view/?id=${item.id}`, + ) + } + > + + + + + + + ))} + +
    DateHoursWorkedLeaveType
    + {dataFormatter.dateTimeFormatter(item.date)} + {item.hours_worked}{item.leave_type}
    +
    + {!organizations?.hr_attendance_org?.length && ( +
    No data
    + )} +
    + + + <> +

    + Hr_attendance organizations +

    + +
    + + + + + + + + + + + + {organizations.hr_attendance_organizations && + Array.isArray( + organizations.hr_attendance_organizations, + ) && + organizations.hr_attendance_organizations.map( + (item: any) => ( + + router.push( + `/hr_attendance/hr_attendance-view/?id=${item.id}`, + ) + } + > + + + + + + + ), + )} + +
    DateHoursWorkedLeaveType
    + {dataFormatter.dateTimeFormatter(item.date)} + + {item.hours_worked} + {item.leave_type}
    +
    + {!organizations?.hr_attendance_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Hr_employees Organization

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + {organizations.hr_employees_org && + Array.isArray(organizations.hr_employees_org) && + organizations.hr_employees_org.map((item: any) => ( + + router.push( + `/hr_employees/hr_employees-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + + + + + + + + + ))} + +
    EmployeeNumberNameEmailDepartmentLocationGradeHireDateTerminationDateStatus
    {item.employee_no}{item.name}{item.email}{item.department}{item.location}{item.grade} + {dataFormatter.dateTimeFormatter(item.hire_date)} + + {dataFormatter.dateTimeFormatter( + item.termination_date, + )} + {item.status}
    +
    + {!organizations?.hr_employees_org?.length && ( +
    No data
    + )} +
    + + + <> +

    Hr_employees organizations

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + {organizations.hr_employees_organizations && + Array.isArray(organizations.hr_employees_organizations) && + organizations.hr_employees_organizations.map( + (item: any) => ( + + router.push( + `/hr_employees/hr_employees-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + + + + + + + + + ), + )} + +
    EmployeeNumberNameEmailDepartmentLocationGradeHireDateTerminationDateStatus
    {item.employee_no}{item.name}{item.email}{item.department}{item.location}{item.grade} + {dataFormatter.dateTimeFormatter(item.hire_date)} + + {dataFormatter.dateTimeFormatter( + item.termination_date, + )} + {item.status}
    +
    + {!organizations?.hr_employees_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Hr_payroll Organization

    + +
    + + + + + + + + + + + + + + + + {organizations.hr_payroll_org && + Array.isArray(organizations.hr_payroll_org) && + organizations.hr_payroll_org.map((item: any) => ( + + router.push( + `/hr_payroll/hr_payroll-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ))} + +
    PeriodBasePayBonusOvertimeHoursCurrency
    + {dataFormatter.dateTimeFormatter(item.period)} + {item.base_pay}{item.bonus} + {item.overtime_hours} + {item.currency}
    +
    + {!organizations?.hr_payroll_org?.length && ( +
    No data
    + )} +
    + + + <> +

    Hr_payroll organizations

    + +
    + + + + + + + + + + + + + + + + {organizations.hr_payroll_organizations && + Array.isArray(organizations.hr_payroll_organizations) && + organizations.hr_payroll_organizations.map( + (item: any) => ( + + router.push( + `/hr_payroll/hr_payroll-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ), + )} + +
    PeriodBasePayBonusOvertimeHoursCurrency
    + {dataFormatter.dateTimeFormatter(item.period)} + {item.base_pay}{item.bonus} + {item.overtime_hours} + {item.currency}
    +
    + {!organizations?.hr_payroll_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Hr_positions Organization

    + +
    + + + + + + + + + + + + + + + + {organizations.hr_positions_org && + Array.isArray(organizations.hr_positions_org) && + organizations.hr_positions_org.map((item: any) => ( + + router.push( + `/hr_positions/hr_positions-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ))} + +
    TitleDepartmentLevelLocationStatus
    {item.title}{item.department}{item.level}{item.location}{item.status}
    +
    + {!organizations?.hr_positions_org?.length && ( +
    No data
    + )} +
    + + + <> +

    Hr_positions organizations

    + +
    + + + + + + + + + + + + + + + + {organizations.hr_positions_organizations && + Array.isArray(organizations.hr_positions_organizations) && + organizations.hr_positions_organizations.map( + (item: any) => ( + + router.push( + `/hr_positions/hr_positions-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ), + )} + +
    TitleDepartmentLevelLocationStatus
    {item.title}{item.department}{item.level}{item.location}{item.status}
    +
    + {!organizations?.hr_positions_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Hr_reqs Organization

    + +
    + + + + + + + + + + + + + + + + {organizations.hr_reqs_org && + Array.isArray(organizations.hr_reqs_org) && + organizations.hr_reqs_org.map((item: any) => ( + + router.push(`/hr_reqs/hr_reqs-view/?id=${item.id}`) + } + > + + + + + + + + + + + ))} + +
    OpenedAtFilledAtStatusRecruiterPriority
    + {dataFormatter.dateTimeFormatter(item.opened_at)} + + {dataFormatter.dateTimeFormatter(item.filled_at)} + {item.status}{item.recruiter}{item.priority}
    +
    + {!organizations?.hr_reqs_org?.length && ( +
    No data
    + )} +
    + + + <> +

    Hr_reqs organizations

    + +
    + + + + + + + + + + + + + + + + {organizations.hr_reqs_organizations && + Array.isArray(organizations.hr_reqs_organizations) && + organizations.hr_reqs_organizations.map((item: any) => ( + + router.push(`/hr_reqs/hr_reqs-view/?id=${item.id}`) + } + > + + + + + + + + + + + ))} + +
    OpenedAtFilledAtStatusRecruiterPriority
    + {dataFormatter.dateTimeFormatter(item.opened_at)} + + {dataFormatter.dateTimeFormatter(item.filled_at)} + {item.status}{item.recruiter}{item.priority}
    +
    + {!organizations?.hr_reqs_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Ops_incidents Organization

    + +
    + + + + + + + + + + + + + + + + {organizations.ops_incidents_org && + Array.isArray(organizations.ops_incidents_org) && + organizations.ops_incidents_org.map((item: any) => ( + + router.push( + `/ops_incidents/ops_incidents-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ))} + +
    OccurredAtCategorySeverityDurationMinutesRootCause
    + {dataFormatter.dateTimeFormatter(item.occurred_at)} + {item.category}{item.severity}{item.duration_min}{item.root_cause}
    +
    + {!organizations?.ops_incidents_org?.length && ( +
    No data
    + )} +
    + + + <> +

    + Ops_incidents organizations +

    + +
    + + + + + + + + + + + + + + + + {organizations.ops_incidents_organizations && + Array.isArray( + organizations.ops_incidents_organizations, + ) && + organizations.ops_incidents_organizations.map( + (item: any) => ( + + router.push( + `/ops_incidents/ops_incidents-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ), + )} + +
    OccurredAtCategorySeverityDurationMinutesRootCause
    + {dataFormatter.dateTimeFormatter( + item.occurred_at, + )} + {item.category}{item.severity} + {item.duration_min} + {item.root_cause}
    +
    + {!organizations?.ops_incidents_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Ops_inventory Organization

    + +
    + + + + + + + + + + + + + + + + + + {organizations.ops_inventory_org && + Array.isArray(organizations.ops_inventory_org) && + organizations.ops_inventory_org.map((item: any) => ( + + router.push( + `/ops_inventory/ops_inventory-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + + + ))} + +
    SKULocationOnHandOnOrderSafetyStockUnitCost
    {item.sku}{item.location}{item.on_hand}{item.on_order}{item.safety_stock}{item.unit_cost}
    +
    + {!organizations?.ops_inventory_org?.length && ( +
    No data
    + )} +
    + + + <> +

    + Ops_inventory organizations +

    + +
    + + + + + + + + + + + + + + + + + + {organizations.ops_inventory_organizations && + Array.isArray( + organizations.ops_inventory_organizations, + ) && + organizations.ops_inventory_organizations.map( + (item: any) => ( + + router.push( + `/ops_inventory/ops_inventory-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + + + ), + )} + +
    SKULocationOnHandOnOrderSafetyStockUnitCost
    {item.sku}{item.location}{item.on_hand}{item.on_order} + {item.safety_stock} + {item.unit_cost}
    +
    + {!organizations?.ops_inventory_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Ops_lines Organization

    + +
    + + + + + + + + {organizations.ops_lines_org && + Array.isArray(organizations.ops_lines_org) && + organizations.ops_lines_org.map((item: any) => ( + + router.push( + `/ops_lines/ops_lines-view/?id=${item.id}`, + ) + } + > + + + ))} + +
    Name
    {item.name}
    +
    + {!organizations?.ops_lines_org?.length && ( +
    No data
    + )} +
    + + + <> +

    Ops_lines organizations

    + +
    + + + + + + + + {organizations.ops_lines_organizations && + Array.isArray(organizations.ops_lines_organizations) && + organizations.ops_lines_organizations.map((item: any) => ( + + router.push( + `/ops_lines/ops_lines-view/?id=${item.id}`, + ) + } + > + + + ))} + +
    Name
    {item.name}
    +
    + {!organizations?.ops_lines_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Ops_orders Organization

    + +
    + + + + + + + + + + + + + + {organizations.ops_orders_org && + Array.isArray(organizations.ops_orders_org) && + organizations.ops_orders_org.map((item: any) => ( + + router.push( + `/ops_orders/ops_orders-view/?id=${item.id}`, + ) + } + > + + + + + + + + + ))} + +
    OrderNumberDueDateQuantityStatus
    {item.order_no} + {dataFormatter.dateTimeFormatter(item.due_date)} + {item.qty}{item.status}
    +
    + {!organizations?.ops_orders_org?.length && ( +
    No data
    + )} +
    + + + <> +

    Ops_orders organizations

    + +
    + + + + + + + + + + + + + + {organizations.ops_orders_organizations && + Array.isArray(organizations.ops_orders_organizations) && + organizations.ops_orders_organizations.map( + (item: any) => ( + + router.push( + `/ops_orders/ops_orders-view/?id=${item.id}`, + ) + } + > + + + + + + + + + ), + )} + +
    OrderNumberDueDateQuantityStatus
    {item.order_no} + {dataFormatter.dateTimeFormatter(item.due_date)} + {item.qty}{item.status}
    +
    + {!organizations?.ops_orders_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Ops_plants Organization

    + +
    + + + + + + + + + + + + {organizations.ops_plants_org && + Array.isArray(organizations.ops_plants_org) && + organizations.ops_plants_org.map((item: any) => ( + + router.push( + `/ops_plants/ops_plants-view/?id=${item.id}`, + ) + } + > + + + + + + + ))} + +
    NameLocationTimezone
    {item.name}{item.location}{item.timezone}
    +
    + {!organizations?.ops_plants_org?.length && ( +
    No data
    + )} +
    + + + <> +

    Ops_plants organizations

    + +
    + + + + + + + + + + + + {organizations.ops_plants_organizations && + Array.isArray(organizations.ops_plants_organizations) && + organizations.ops_plants_organizations.map( + (item: any) => ( + + router.push( + `/ops_plants/ops_plants-view/?id=${item.id}`, + ) + } + > + + + + + + + ), + )} + +
    NameLocationTimezone
    {item.name}{item.location}{item.timezone}
    +
    + {!organizations?.ops_plants_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    + Ops_production_log Organization +

    + +
    + + + + + + + + + + + + + + + + {organizations.ops_production_log_org && + Array.isArray(organizations.ops_production_log_org) && + organizations.ops_production_log_org.map((item: any) => ( + + router.push( + `/ops_production_log/ops_production_log-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ))} + +
    TimestampUnitsProducedPlannedUnitsDowntimeMinutesDefects
    + {dataFormatter.dateTimeFormatter(item.ts)} + + {item.units_produced} + + {item.planned_units} + {item.downtime_min}{item.defects}
    +
    + {!organizations?.ops_production_log_org?.length && ( +
    No data
    + )} +
    + + + <> +

    + Ops_production_log organizations +

    + +
    + + + + + + + + + + + + + + + + {organizations.ops_production_log_organizations && + Array.isArray( + organizations.ops_production_log_organizations, + ) && + organizations.ops_production_log_organizations.map( + (item: any) => ( + + router.push( + `/ops_production_log/ops_production_log-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ), + )} + +
    TimestampUnitsProducedPlannedUnitsDowntimeMinutesDefects
    + {dataFormatter.dateTimeFormatter(item.ts)} + + {item.units_produced} + + {item.planned_units} + + {item.downtime_min} + {item.defects}
    +
    + {!organizations?.ops_production_log_organizations?.length && ( +
    No data
    + )} +
    + + + <> +

    Ops_shipments Organization

    + +
    + + + + + + + + + + + + + + + + {organizations.ops_shipments_org && + Array.isArray(organizations.ops_shipments_org) && + organizations.ops_shipments_org.map((item: any) => ( + + router.push( + `/ops_shipments/ops_shipments-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ))} + +
    OrderNumberShippedAtCarrierDeliveredAtStatus
    {item.order_no} + {dataFormatter.dateTimeFormatter(item.shipped_at)} + {item.carrier} + {dataFormatter.dateTimeFormatter(item.delivered_at)} + {item.status}
    +
    + {!organizations?.ops_shipments_org?.length && ( +
    No data
    + )} +
    + + + <> +

    + Ops_shipments organizations +

    + +
    + + + + + + + + + + + + + + + + {organizations.ops_shipments_organizations && + Array.isArray( + organizations.ops_shipments_organizations, + ) && + organizations.ops_shipments_organizations.map( + (item: any) => ( + + router.push( + `/ops_shipments/ops_shipments-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ), + )} + +
    OrderNumberShippedAtCarrierDeliveredAtStatus
    {item.order_no} + {dataFormatter.dateTimeFormatter(item.shipped_at)} + {item.carrier} + {dataFormatter.dateTimeFormatter( + item.delivered_at, + )} + {item.status}
    +
    + {!organizations?.ops_shipments_organizations?.length && ( +
    No data
    + )} +
    + + + + + router.push('/organizations/organizations-list')} + /> +
    +
    + + ); +}; + +OrganizationsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default OrganizationsView; diff --git a/frontend/src/pages/password-reset.tsx b/frontend/src/pages/password-reset.tsx new file mode 100644 index 0000000..e75c2f4 --- /dev/null +++ b/frontend/src/pages/password-reset.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import type { ReactElement } from 'react'; +import LayoutGuest from '../layouts/Guest'; +import PasswordSetOrReset from '../components/PasswordSetOrReset'; + +export default function Reset() { + return ; +} + +Reset.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/pages/permissions/[permissionsId].tsx b/frontend/src/pages/permissions/[permissionsId].tsx new file mode 100644 index 0000000..c49d722 --- /dev/null +++ b/frontend/src/pages/permissions/[permissionsId].tsx @@ -0,0 +1,130 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/permissions/permissionsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditPermissions = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + name: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { permissions } = useAppSelector((state) => state.permissions); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { permissionsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: permissionsId })); + }, [permissionsId]); + + useEffect(() => { + if (typeof permissions === 'object') { + setInitialValues(permissions); + } + }, [permissions]); + + useEffect(() => { + if (typeof permissions === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = permissions[el]), + ); + + setInitialValues(newInitialVal); + } + }, [permissions]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: permissionsId, data })); + await router.push('/permissions/permissions-list'); + }; + + return ( + <> + + {getPageTitle('Edit permissions')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + router.push('/permissions/permissions-list')} + /> + + +
    +
    +
    + + ); +}; + +EditPermissions.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditPermissions; diff --git a/frontend/src/pages/permissions/permissions-edit.tsx b/frontend/src/pages/permissions/permissions-edit.tsx new file mode 100644 index 0000000..c540bf7 --- /dev/null +++ b/frontend/src/pages/permissions/permissions-edit.tsx @@ -0,0 +1,128 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/permissions/permissionsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditPermissionsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + name: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { permissions } = useAppSelector((state) => state.permissions); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof permissions === 'object') { + setInitialValues(permissions); + } + }, [permissions]); + + useEffect(() => { + if (typeof permissions === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = permissions[el]), + ); + setInitialValues(newInitialVal); + } + }, [permissions]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/permissions/permissions-list'); + }; + + return ( + <> + + {getPageTitle('Edit permissions')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + router.push('/permissions/permissions-list')} + /> + + +
    +
    +
    + + ); +}; + +EditPermissionsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditPermissionsPage; diff --git a/frontend/src/pages/permissions/permissions-list.tsx b/frontend/src/pages/permissions/permissions-list.tsx new file mode 100644 index 0000000..97955e4 --- /dev/null +++ b/frontend/src/pages/permissions/permissions-list.tsx @@ -0,0 +1,165 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TablePermissions from '../../components/Permissions/TablePermissions'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/permissions/permissionsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const PermissionsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([{ label: 'Name', title: 'name' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_PERMISSIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getPermissionsCSV = async () => { + const response = await axios({ + url: '/permissions?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'permissionsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Permissions')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    +
    + + + + +
    + + + + + ); +}; + +PermissionsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default PermissionsTablesPage; diff --git a/frontend/src/pages/permissions/permissions-new.tsx b/frontend/src/pages/permissions/permissions-new.tsx new file mode 100644 index 0000000..e5a9eb0 --- /dev/null +++ b/frontend/src/pages/permissions/permissions-new.tsx @@ -0,0 +1,98 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/permissions/permissionsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + name: '', +}; + +const PermissionsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/permissions/permissions-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + router.push('/permissions/permissions-list')} + /> + + +
    +
    +
    + + ); +}; + +PermissionsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default PermissionsNew; diff --git a/frontend/src/pages/permissions/permissions-table.tsx b/frontend/src/pages/permissions/permissions-table.tsx new file mode 100644 index 0000000..4f54815 --- /dev/null +++ b/frontend/src/pages/permissions/permissions-table.tsx @@ -0,0 +1,164 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TablePermissions from '../../components/Permissions/TablePermissions'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/permissions/permissionsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const PermissionsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([{ label: 'Name', title: 'name' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_PERMISSIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getPermissionsCSV = async () => { + const response = await axios({ + url: '/permissions?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'permissionsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Permissions')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    +
    + + + +
    + + + + + ); +}; + +PermissionsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default PermissionsTablesPage; diff --git a/frontend/src/pages/permissions/permissions-view.tsx b/frontend/src/pages/permissions/permissions-view.tsx new file mode 100644 index 0000000..aae7c5e --- /dev/null +++ b/frontend/src/pages/permissions/permissions-view.tsx @@ -0,0 +1,87 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/permissions/permissionsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const PermissionsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { permissions } = useAppSelector((state) => state.permissions); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View permissions')} + + + + + + +
    +

    Name

    +

    {permissions?.name}

    +
    + + + + router.push('/permissions/permissions-list')} + /> +
    +
    + + ); +}; + +PermissionsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default PermissionsView; diff --git a/frontend/src/pages/privacy-policy.tsx b/frontend/src/pages/privacy-policy.tsx new file mode 100644 index 0000000..394db59 --- /dev/null +++ b/frontend/src/pages/privacy-policy.tsx @@ -0,0 +1,292 @@ +import React, { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import LayoutGuest from '../layouts/Guest'; +import { getPageTitle } from '../config'; + +export default function PrivacyPolicy() { + const title = 'Title Enterprise Dashboards Finance HR Ops'; + const [projectUrl, setProjectUrl] = useState(''); + + useEffect(() => { + setProjectUrl(location.origin); + }, []); + + const Introduction = () => { + return ( + <> +

    1. Introduction

    +

    + {/* eslint-disable-next-line react/no-unescaped-entities */} + We at {title} ("we", "us", "our") are committed to + protecting your privacy. This Privacy Policy explains how we collect, + use, disclose, and safeguard your information when you visit our + website {projectUrl}, use our services, or + interact with us in other ways. By using our services, you agree to + the collection and use of information in accordance with this policy. +

    + + ); + }; + + const Information = () => { + return ( + <> +

    2. Information We Collect

    +
    +

    2.1 Personal Identification Information

    +

    + We collect various types of personal information in connection with + the services we provide, including: +

    +
      +
    • + Contact Information: Name, email address, phone number, mailing + address. +
    • +
    • Account Information: Username, password, profile picture.
    • +
    • Payment Information: Credit card details, billing address.
    • +
    • Demographic Information: Age, gender, interests.
    • +
    +

    2.2 Technical Data

    +

    + We automatically collect certain information when you visit, use, or + navigate our services. This information may include: +

    +
      +
    • + Device Information: IP address, browser type, operating system, + device type. +
    • +
    • + Usage Data: Pages visited, time spent on each page, links clicked, + and other actions taken on our site. +
    • +
    +

    2.3 Cookies and Tracking Technologies

    +

    + We use cookies and similar tracking technologies to track the + activity on our service and hold certain information. You can + instruct your browser to refuse all cookies or to indicate when a + cookie is being sent. +

    +
    + + ); + }; + + const HowToUser = () => { + return ( + <> +

    3. How We Use Your Information

    +

    We use the information we collect in various ways, including to:

    +
      +
    • Provide, operate, and maintain our website and services.
    • +
    • Improve, personalize, and expand our website and services.
    • +
    • Understand and analyze how you use our website and services.
    • +
    • Develop new products, services, features, and functionality.
    • +
    • + Communicate with you, either directly or through one of our + partners, including for customer service, to provide you with + updates and other information relating to the website, and for + marketing and promotional purposes. +
    • +
    • + Process your transactions and send you related information, + including purchase confirmations and invoices. +
    • +
    • Find and prevent fraud.
    • +
    • Comply with legal obligations.
    • +
    + + ); + }; + + const DataProtection = () => { + return ( + <> +

    4. Data Protection and Security

    +

    + We implement a variety of security measures to maintain the safety of + your personal information. These measures include: +

    +
      +
    • + Encryption: We use encryption to protect sensitive information + transmitted online. Access Controls: We restrict access to your + personal data to authorized personnel only. Regular Security Audits: + We conduct regular audits to identify and address potential security + vulnerabilities. +
    • +
    + + ); + }; + + const Sharing = () => { + return ( + <> +

    5. Sharing Your Information

    +

    + We do not sell, trade, or otherwise transfer your Personally + Identifiable Information to outside parties without your consent, + except in the following cases: +

    +
      +
    • + Service Providers: We may share your information with third-party + service providers who perform services on our behalf, such as + payment processing, data analysis, email delivery, hosting services, + customer service, and marketing assistance. +
    • +
    • + Business Transfers: In the event of a merger, acquisition, or sale + of all or a portion of our assets, your information may be + transferred as part of that transaction. +
    • +
    • + Legal Requirements: We may disclose your information if required to + do so by law or in response to valid requests by public authorities + (e.g., a court or a government agency). +
    • +
    + + ); + }; + + const ProtectionRights = () => { + return ( + <> +

    6. Your Data Protection Rights

    +

    + Depending on your location, you may have the following rights + regarding your personal data: +

    +
      +
    • + The Right to Access: You have the right to request copies of your + personal data. +
    • +
    • + The Right to Rectification: You have the right to request that we + correct any information you believe is inaccurate or complete + information you believe is incomplete. +
    • +
    • + The Right to Erasure: You have the right to request that we erase + your personal data, under certain conditions. +
    • +
    • + The Right to Restrict Processing: You have the right to request that + we restrict the processing of your personal data, under certain + conditions. +
    • +
    • + The Right to Object to Processing: You have the right to object to + our processing of your personal data, under certain conditions. +
    • +
    • + The Right to Data Portability: You have the right to request that we + transfer the data that we have collected to another organization, or + directly to you, under certain conditions. +
    • +
    + + ); + }; + + const DataTransfers = () => { + return ( + <> +

    7. International Data Transfers

    +

    + Your information, including personal data, may be transferred to — and + maintained on — computers located outside of your state, province, + country, or other governmental jurisdiction where the data protection + laws may differ from those of your jurisdiction. We will take all + steps reasonably necessary to ensure that your data is treated + securely and in accordance with this Privacy Policy. +

    + + ); + }; + + const RetentionOfData = () => { + return ( + <> +

    8. Retention of Data

    +

    + We will retain your personal data only for as long as is necessary for + the purposes set out in this Privacy Policy. We will retain and use + your personal data to the extent necessary to comply with our legal + obligations, resolve disputes, and enforce our policies. +

    + + ); + }; + + const ChangePrivacy = () => { + return ( + <> +

    9. Changes to This Privacy Policy

    +

    + We may update our Privacy Policy from time to time. We will notify you + of any changes by posting the new Privacy Policy on this page. You are + advised to review this Privacy Policy periodically for any changes. + Changes to this Privacy Policy are effective when they are posted on + this page. +

    + + ); + }; + + const ContactUs = () => { + return ( + <> +

    10. Contact Us

    +

    + If you have any questions about this Privacy Policy, please contact + us: +

    +
    + By email:{' '} + [support@flatlogic.com] +
    +
    + By visiting this page on our website:{' '} + Contact Us +
    + + ); + }; + + return ( +
    + + {getPageTitle('Privacy Policy')} + + +
    +
    +
    +

    Privacy Policy

    + + + + + + + + + + +
    +
    +
    +
    + ); +} + +PrivacyPolicy.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/pages/profile.tsx b/frontend/src/pages/profile.tsx new file mode 100644 index 0000000..efc9070 --- /dev/null +++ b/frontend/src/pages/profile.tsx @@ -0,0 +1,178 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import { ToastContainer, toast } from 'react-toastify'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; + +import CardBox from '../components/CardBox'; +import LayoutAuthenticated from '../layouts/Authenticated'; +import SectionMain from '../components/SectionMain'; +import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../components/FormField'; +import BaseDivider from '../components/BaseDivider'; +import BaseButtons from '../components/BaseButtons'; +import BaseButton from '../components/BaseButton'; +import FormCheckRadio from '../components/FormCheckRadio'; +import FormCheckRadioGroup from '../components/FormCheckRadioGroup'; +import FormImagePicker from '../components/FormImagePicker'; +import { SwitchField } from '../components/SwitchField'; +import { SelectField } from '../components/SelectField'; + +import { update, fetch } from '../stores/users/usersSlice'; +import { useAppDispatch, useAppSelector } from '../stores/hooks'; +import { useRouter } from 'next/router'; +import { findMe } from '../stores/authSlice'; + +const EditUsers = () => { + const { currentUser, isFetching, token } = useAppSelector( + (state) => state.auth, + ); + const router = useRouter(); + const dispatch = useAppDispatch(); + const notify = (type, msg) => toast(msg, { type }); + const initVals = { + firstName: '', + lastName: '', + phoneNumber: '', + email: '', + app_role: '', + disabled: false, + avatar: [], + password: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + useEffect(() => { + if (currentUser?.id && typeof currentUser === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = currentUser[el]), + ); + + setInitialValues(newInitialVal); + } + }, [currentUser]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: currentUser.id, data })); + await dispatch(findMe()); + await router.push('/users/users-list'); + notify('success', 'Profile was updated!'); + }; + + return ( + <> + + {getPageTitle('Edit profile')} + + + + {''} + + + {currentUser?.avatar[0]?.publicUrl && ( +
    +
    + Avatar +
    +
    + )} + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/users/users-list')} + /> + + +
    +
    +
    + + ); +}; + +EditUsers.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default EditUsers; diff --git a/frontend/src/pages/register.tsx b/frontend/src/pages/register.tsx new file mode 100644 index 0000000..35bf2d4 --- /dev/null +++ b/frontend/src/pages/register.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import type { ReactElement } from 'react'; +import { ToastContainer, toast } from 'react-toastify'; +import Head from 'next/head'; +import BaseButton from '../components/BaseButton'; +import CardBox from '../components/CardBox'; +import SectionFullScreen from '../components/SectionFullScreen'; +import LayoutGuest from '../layouts/Guest'; +import { Field, Form, Formik } from 'formik'; +import FormField from '../components/FormField'; +import BaseDivider from '../components/BaseDivider'; +import BaseButtons from '../components/BaseButtons'; +import { useRouter } from 'next/router'; +import { getPageTitle } from '../config'; + +import Select from 'react-select'; +import { useAppDispatch } from '../stores/hooks'; +import { createAsyncThunk } from '@reduxjs/toolkit'; + +import axios from 'axios'; + +export default function Register() { + const [loading, setLoading] = React.useState(false); + const router = useRouter(); + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const [organizations, setOrganizations] = React.useState(null); + const [selectedOrganization, setSelectedOrganization] = React.useState(null); + const dispatch = useAppDispatch(); + const fetchOrganizations = createAsyncThunk('/org-for-auth', async () => { + try { + const response = await axios.get('/org-for-auth'); + setOrganizations(response.data); + return response.data; + } catch (error) { + console.error(error.response); + throw error; + } + }); + React.useEffect(() => { + dispatch(fetchOrganizations()); + }, [dispatch]); + const options = organizations?.map((org) => ({ + value: org.id, + label: org.name, + })); + + const handleSubmit = async (value) => { + setLoading(true); + try { + const formData = { ...value, organizationId: selectedOrganization.value }; + + const { data: response } = await axios.post('/auth/signup', formData); + await router.push('/login'); + setLoading(false); + notify('success', 'Please check your email for verification link'); + } catch (error) { + setLoading(false); + console.log('error: ', error); + notify('error', 'Something was wrong. Try again'); + } + }; + + return ( + <> + + {getPageTitle('Login')} + + + + + handleSubmit(values)} + > +
    + + +